Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,6 @@ describe('Image Integrations Test', () => {

getHelperElementByLabel('Integration name').contains('An integration name is required');
getHelperElementByLabel('Endpoint').contains('An endpoint is required');
getHelperElementByLabel('OAuth token').contains('An OAuth token is required');
cy.get(selectors.buttons.test).should('be.disabled');
cy.get(selectors.buttons.save).should('be.disabled');

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import React, { ReactElement } from 'react';
import {
Alert,
Checkbox,
Form,
Grid,
GridItem,
List,
ListItem,
PageSection,
TextInput,
ToggleGroup,
Expand All @@ -16,6 +21,8 @@ import FormMessage from 'Components/PatternFly/FormMessage';
import FormTestButton from 'Components/PatternFly/FormTestButton';
import FormSaveButton from 'Components/PatternFly/FormSaveButton';
import FormCancelButton from 'Components/PatternFly/FormCancelButton';
import useFeatureFlags from 'hooks/useFeatureFlags';

import useIntegrationForm from '../useIntegrationForm';
import { IntegrationFormProps } from '../integrationFormTypes';

Expand All @@ -42,25 +49,6 @@ export const validationSchema = yup.object().shape({
.required('A category is required'),
quay: yup.object().shape({
endpoint: yup.string().trim().required('An endpoint is required'),
oauthToken: yup
.string()
.test(
'oauthToken-test',
'An OAuth token is required',
(value, context: yup.TestContext) => {
const requirePasswordField =
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
context?.from[2]?.value?.updatePassword || false;

if (!requirePasswordField) {
return true;
}

const trimmedValue = value?.trim();
return !!trimmedValue;
}
),
insecure: yup.bool(),
}),
skipTestIntegration: yup.bool(),
Expand All @@ -78,6 +66,7 @@ export const defaultValues: QuayIntegrationFormValues = {
endpoint: '',
oauthToken: '',
insecure: false,
registryRobotCredentials: null,
},
autogenerated: false,
clusterId: '',
Expand All @@ -91,12 +80,33 @@ function QuayIntegrationForm({
initialValues = null,
isEditable = false,
}: IntegrationFormProps<QuayImageIntegration>): ReactElement {
const formInitialValues = { ...defaultValues, ...initialValues };
const { isFeatureFlagEnabled } = useFeatureFlags();
const isQuayRobotAccountsEnabled = isFeatureFlagEnabled('ROX_QUAY_ROBOT_ACCOUNTS');

// Refer to stored token only if it exists initially.
const hasInitialOauthToken = Boolean(initialValues?.quay.oauthToken);

const formInitialValues = { ...defaultValues };
if (initialValues) {
formInitialValues.config = { ...formInitialValues.config, ...initialValues };
// We want to clear the password because backend returns '******' to represent that there
// are currently stored credentials

// We want to clear the token or password because backend returns '******'
// to represent that there are currently stored credentials.
formInitialValues.config.quay.oauthToken = '';
if (formInitialValues.config.quay.registryRobotCredentials?.password) {
formInitialValues.config.quay.registryRobotCredentials.password = '';
}

/*
* Special case for Quay integration,
* because unauthenticated is an implicit instead of explicit property.
* If an existing integration does not have stored credentials,
* updatePassword is initially cleared, so user must tick it to add stored credentials.
*/
const hasInitialRobotAccount = Boolean(initialValues.quay.registryRobotCredentials);
if (!hasInitialOauthToken && !hasInitialRobotAccount && isQuayRobotAccountsEnabled) {
formInitialValues.updatePassword = false;
}
}
const {
values,
Expand Down Expand Up @@ -199,6 +209,43 @@ function QuayIntegrationForm({
isDisabled={!isEditable}
/>
</FormLabelGroup>
{isEditable && isQuayRobotAccountsEnabled && (
<Alert variant="info" isInline title="Authentication">
<List>
{values.config.categories.includes('SCANNER') ? (
<ListItem>
Private repositories: <strong>OAuth token</strong> is{' '}
<strong>required</strong> to scan images.
</ListItem>
) : (
<ListItem>
Private repositories: <strong>OAuth token</strong> is{' '}
<strong>deprecated</strong> for any access to images (except
for scanning).
</ListItem>
)}
{values.config.categories.includes('REGISTRY') && (
<ListItem>
Private repositories: <strong>Robot account</strong> is{' '}
<strong>recommended</strong> for any access to images{' '}
(except for scanning).
</ListItem>
)}
{values.config.categories.includes('REGISTRY') &&
values.config.categories.includes('SCANNER') && (
<ListItem>
Private repositories: You can omit{' '}
<strong>Robot username</strong> and{' '}
<strong>Robot password</strong> to use{' '}
<strong>OAuth token</strong> for all access to images.
</ListItem>
)}
<ListItem>
Public repositories: Omit authentication for access to images.
</ListItem>
</List>
</Alert>
)}
{!isCreating && isEditable && (
<FormLabelGroup
fieldId="updatePassword"
Expand All @@ -216,27 +263,75 @@ function QuayIntegrationForm({
</FormLabelGroup>
)}
<FormLabelGroup
isRequired={values.updatePassword}
label="OAuth token"
fieldId="config.quay.oauthToken"
touched={touched}
errors={errors}
>
<TextInput
isRequired={values.updatePassword}
type="text"
id="config.quay.oauthToken"
value={values.config.quay.oauthToken}
onChange={onChange}
onBlur={handleBlur}
isDisabled={!isEditable || !values.updatePassword}
placeholder={
values.updatePassword
values.updatePassword || !hasInitialOauthToken
? ''
: 'Currently-stored password will be used.'
: 'Currently-stored token will be used.'
}
/>
</FormLabelGroup>
{values.config.categories.includes('REGISTRY') && isQuayRobotAccountsEnabled && (
<Grid hasGutter>
<GridItem span={12} lg={6}>
<FormLabelGroup
label="Robot username"
fieldId="config.quay.registryRobotCredentials.username"
touched={touched}
errors={errors}
>
<TextInput
type="text"
id="config.quay.registryRobotCredentials.username"
value={
values.config.quay.registryRobotCredentials?.username ??
''
}
onChange={onChange}
onBlur={handleBlur}
isDisabled={!isEditable || !values.updatePassword}
/>
</FormLabelGroup>
</GridItem>
<GridItem span={12} lg={6}>
<FormLabelGroup
label="Robot password"
fieldId="config.quay.registryRobotCredentials.password"
touched={touched}
errors={errors}
>
<TextInput
type="text"
id="config.quay.registryRobotCredentials.password"
value={
values.config.quay.registryRobotCredentials?.password ??
''
}
onChange={onChange}
onBlur={handleBlur}
isDisabled={!isEditable || !values.updatePassword}
placeholder={
values.updatePassword ||
!values.config.quay.registryRobotCredentials?.username
? ''
: 'Currently-stored password will be used.'
}
/>
</FormLabelGroup>
</GridItem>
</Grid>
)}
<FormLabelGroup
fieldId="config.quay.insecure"
touched={touched}
Expand Down
10 changes: 10 additions & 0 deletions ui/apps/platform/src/types/imageIntegration.proto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,16 @@ export type QuayConfig = {
// The OAuth token for the integration. The server will mask the value of this credential in responses and logs.
oauthToken: string; // scrub: always
insecure: boolean;
// For registry integrations, Quay recommends using robot accounts. oauthToken will continue to be used for scanner integration.
registryRobotCredentials: QuayRobotAccount | null;
};

// Robot account is Quay's named tokens that can be granted permissions on multiple repositories under an organization.
// It's Quay's recommended authentication model when possible (i.e. registry integration)
export type QuayRobotAccount = {
username: string;
// The server will mask the value of this password in responses and logs.
password: string; // scrub: always
};

export type RhelImageIntegration = {
Expand Down