Browse Source

SONAR-21413 Fix SSF-530 - Add Groups in jit and auto cards

tags/10.4.0.87286
guillaume-peoch-sonarsource 4 months ago
parent
commit
0f5f5412c1

+ 1
- 0
server/sonar-web/src/main/js/api/gitlab-provisioning.ts View File

return axios.post(GITLAB_CONFIGURATIONS, { return axios.post(GITLAB_CONFIGURATIONS, {
...data, ...data,
provisioningType: ProvisioningType.jit, provisioningType: ProvisioningType.jit,
allowedGroups: [],
allowUsersToSignUp: false, allowUsersToSignUp: false,
enabled: true, enabled: true,
}); });

+ 3
- 1
server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationFormField.tsx View File

onFieldChange: (key: string, value: string | boolean | string[]) => void; onFieldChange: (key: string, value: string | boolean | string[]) => void;
isNotSet: boolean; isNotSet: boolean;
error?: string; error?: string;
className?: string;
} }


export default function AuthenticationFormField(props: Readonly<Props>) { export default function AuthenticationFormField(props: Readonly<Props>) {
const { mandatory = false, definition, settingValue, isNotSet, error } = props;
const { mandatory = false, definition, settingValue, isNotSet, error, className } = props;


const intl = useIntl(); const intl = useIntl();




return ( return (
<FormField <FormField
className={className}
htmlFor={definition.key} htmlFor={definition.key}
ariaLabel={name} ariaLabel={name}
label={name} label={name}

+ 100
- 40
server/sonar-web/src/main/js/apps/settings/components/authentication/GitLabAuthenticationTab.tsx View File

provisioningType?: GitLabConfigurationUpdateBody['provisioningType']; provisioningType?: GitLabConfigurationUpdateBody['provisioningType'];
allowUsersToSignUp?: GitLabConfigurationUpdateBody['allowUsersToSignUp']; allowUsersToSignUp?: GitLabConfigurationUpdateBody['allowUsersToSignUp'];
provisioningToken?: GitLabConfigurationUpdateBody['provisioningToken']; provisioningToken?: GitLabConfigurationUpdateBody['provisioningToken'];
allowedGroups?: GitLabConfigurationUpdateBody['allowedGroups'];
} }


const definitions: Record<keyof Omit<ChangesForm, 'provisioningType'>, DefinitionV2> = {
allowUsersToSignUp: {
name: translate('settings.authentication.gitlab.form.allowUsersToSignUp.name'),
secured: false,
key: 'allowUsersToSignUp',
description: translate('settings.authentication.gitlab.form.allowUsersToSignUp.description'),
type: SettingType.BOOLEAN,
},
provisioningToken: {
name: translate('settings.authentication.gitlab.form.provisioningToken.name'),
secured: true,
key: 'provisioningToken',
description: translate('settings.authentication.gitlab.form.provisioningToken.description'),
},
const getDefinitions = (
provisioningType: ProvisioningType,
): Record<keyof Omit<ChangesForm, 'provisioningType'>, DefinitionV2> => {
return {
allowUsersToSignUp: {
name: translate('settings.authentication.gitlab.form.allowUsersToSignUp.name'),
secured: false,
key: 'allowUsersToSignUp',
description: translate('settings.authentication.gitlab.form.allowUsersToSignUp.description'),
type: SettingType.BOOLEAN,
},
provisioningToken: {
name: translate('settings.authentication.gitlab.form.provisioningToken.name'),
secured: true,
key: 'provisioningToken',
description: translate('settings.authentication.gitlab.form.provisioningToken.description'),
},
allowedGroups: {
name: translate('settings.authentication.gitlab.form.allowedGroups.name'),
secured: false,
key: 'allowedGroups',
description: translate(
`settings.authentication.gitlab.form.allowedGroups.description.${provisioningType}`,
),
multiValues: true,
},
};
}; };


export default function GitLabAuthenticationTab() { export default function GitLabAuthenticationTab() {
const { mutate: updateConfig, isLoading: isUpdating } = useUpdateGitLabConfigurationMutation(); const { mutate: updateConfig, isLoading: isUpdating } = useUpdateGitLabConfigurationMutation();
const { mutate: deleteConfig, isLoading: isDeleting } = useDeleteGitLabConfigurationMutation(); const { mutate: deleteConfig, isLoading: isDeleting } = useDeleteGitLabConfigurationMutation();


const definitions = getDefinitions(
changes?.provisioningType ?? configuration?.provisioningType ?? ProvisioningType.jit,
);
const toggleEnable = () => { const toggleEnable = () => {
if (!configuration) { if (!configuration) {
return; return;
const setJIT = () => const setJIT = () =>
setChangesWithCheck({ setChangesWithCheck({
provisioningType: ProvisioningType.jit, provisioningType: ProvisioningType.jit,
allowedGroups: changes?.allowedGroups,
provisioningToken: undefined, provisioningToken: undefined,
}); });


const setAuto = () => const setAuto = () =>
setChangesWithCheck({ setChangesWithCheck({
provisioningType: ProvisioningType.auto, provisioningType: ProvisioningType.auto,
allowedGroups: changes?.allowedGroups,
allowUsersToSignUp: undefined, allowUsersToSignUp: undefined,
}); });


const hasDifferentProvider = const hasDifferentProvider =
identityProvider?.provider !== undefined && identityProvider.provider !== Provider.Gitlab; identityProvider?.provider !== undefined && identityProvider.provider !== Provider.Gitlab;
const allowUsersToSignUpDefinition = definitions.allowUsersToSignUp; const allowUsersToSignUpDefinition = definitions.allowUsersToSignUp;
const allowedGroupsDefinition = definitions.allowedGroups;
const provisioningTokenDefinition = definitions.provisioningToken; const provisioningTokenDefinition = definitions.provisioningToken;


const provisioningType = changes?.provisioningType ?? configuration?.provisioningType; const provisioningType = changes?.provisioningType ?? configuration?.provisioningType;
const allowUsersToSignUp = changes?.allowUsersToSignUp ?? configuration?.allowUsersToSignUp; const allowUsersToSignUp = changes?.allowUsersToSignUp ?? configuration?.allowUsersToSignUp;
const allowedGroups = changes?.allowedGroups ?? configuration?.allowedGroups;
const provisioningToken = changes?.provisioningToken; const provisioningToken = changes?.provisioningToken;


const canSave = () => { const canSave = () => {
} }
const type = changes.provisioningType ?? configuration.provisioningType; const type = changes.provisioningType ?? configuration.provisioningType;
if (type === ProvisioningType.auto) { if (type === ProvisioningType.auto) {
return configuration.isProvisioningTokenSet || !!changes.provisioningToken;
const areGroupsDefined =
changes.allowedGroups?.some((val) => val !== '') ??
configuration.allowedGroups?.some((val) => val !== '');
return (
(configuration.isProvisioningTokenSet || !!changes.provisioningToken) && areGroupsDefined
);
} }
return true; return true;
}; };
configuration?.allowUsersToSignUp === newChanges.allowUsersToSignUp configuration?.allowUsersToSignUp === newChanges.allowUsersToSignUp
? undefined ? undefined
: newChanges.allowUsersToSignUp, : newChanges.allowUsersToSignUp,
allowedGroups:
configuration?.allowedGroups === newChanges.allowedGroups
? undefined
: newChanges.allowedGroups,
provisioningToken: newChanges.provisioningToken, provisioningToken: newChanges.provisioningToken,
}; };
if (Object.values(newValue).some((v) => v !== undefined)) { if (Object.values(newValue).some((v) => v !== undefined)) {
onToggle={toggleEnable} onToggle={toggleEnable}
/> />
<ProvisioningSection <ProvisioningSection
isLoading={isUpdating}
provisioningType={provisioningType ?? ProvisioningType.jit} provisioningType={provisioningType ?? ProvisioningType.jit}
onChangeProvisioningType={(val: ProvisioningType) => onChangeProvisioningType={(val: ProvisioningType) =>
val === ProvisioningType.auto ? setAuto() : setJIT() val === ProvisioningType.auto ? setAuto() : setJIT()
/> />
} }
jitSettings={ jitSettings={
<AuthenticationFormField
settingValue={allowUsersToSignUp}
definition={allowUsersToSignUpDefinition}
mandatory
onFieldChange={(_, value) =>
setChangesWithCheck({
...changes,
allowUsersToSignUp: value as boolean,
})
}
isNotSet={configuration.provisioningType !== ProvisioningType.auto}
/>
<>
<AuthenticationFormField
settingValue={allowUsersToSignUp}
definition={allowUsersToSignUpDefinition}
mandatory
onFieldChange={(_, value) =>
setChangesWithCheck({
...changes,
allowUsersToSignUp: value as boolean,
})
}
isNotSet={configuration.provisioningType !== ProvisioningType.auto}
/>
<AuthenticationFormField
className="sw-mt-8"
settingValue={allowedGroups}
definition={allowedGroupsDefinition}
onFieldChange={(_, values) =>
setChangesWithCheck({
...changes,
allowedGroups: values as string[],
})
}
isNotSet={configuration.provisioningType !== ProvisioningType.auto}
/>
</>
} }
autoTitle={translate('settings.authentication.gitlab.form.provisioning_with_gitlab')} autoTitle={translate('settings.authentication.gitlab.form.provisioning_with_gitlab')}
hasDifferentProvider={hasDifferentProvider} hasDifferentProvider={hasDifferentProvider}
canSync={canSyncNow} canSync={canSyncNow}
synchronizationDetails={<GitLabSynchronisationWarning />} synchronizationDetails={<GitLabSynchronisationWarning />}
autoSettings={ autoSettings={
<AuthenticationFormField
settingValue={provisioningToken}
key={tokenKey}
definition={provisioningTokenDefinition}
mandatory
onFieldChange={(_, value) =>
setChangesWithCheck({
...changes,
provisioningToken: value as string,
})
}
isNotSet={!configuration.isProvisioningTokenSet}
/>
<>
<AuthenticationFormField
settingValue={provisioningToken}
key={tokenKey}
definition={provisioningTokenDefinition}
mandatory
onFieldChange={(_, value) =>
setChangesWithCheck({
...changes,
provisioningToken: value as string,
})
}
isNotSet={!configuration.isProvisioningTokenSet}
/>
<AuthenticationFormField
className="sw-mt-8"
settingValue={allowedGroups}
definition={allowedGroupsDefinition}
mandatory
onFieldChange={(_, values) =>
setChangesWithCheck({
...changes,
allowedGroups: values as string[],
})
}
isNotSet={configuration.provisioningType !== ProvisioningType.auto}
/>
</>
} }
/> />
</> </>

+ 2
- 17
server/sonar-web/src/main/js/apps/settings/components/authentication/GitLabConfigurationForm.tsx View File

*/ */


import { ButtonPrimary, FlagMessage, Modal, Spinner } from 'design-system'; import { ButtonPrimary, FlagMessage, Modal, Spinner } from 'design-system';
import { isArray, keyBy } from 'lodash';
import { keyBy } from 'lodash';
import * as React from 'react'; import * as React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import DocumentationLink from '../../../../components/common/DocumentationLink'; import DocumentationLink from '../../../../components/common/DocumentationLink';
type: SettingType.BOOLEAN, type: SettingType.BOOLEAN,
}, },
}, },
allowedGroups: {
value: data?.allowedGroups ?? [],
required: true,
definition: {
name: translate('settings.authentication.gitlab.form.allowedGroups.name'),
secured: false,
key: 'allowedGroups',
description: translate('settings.authentication.gitlab.form.allowedGroups.description'),
multiValues: true,
},
},
}); });


const header = translate('settings.authentication.gitlab.form', isCreate ? 'create' : 'edit'); const header = translate('settings.authentication.gitlab.form', isCreate ? 'create' : 'edit');


const canBeSaved = Object.values(formData).every(({ definition, required, value }) => { const canBeSaved = Object.values(formData).every(({ definition, required, value }) => {
return (
(!isCreate && definition.secured) ||
!required ||
(isArray(value) ? value.some((val) => val !== '') : value !== '')
);
return (!isCreate && definition.secured) || !required || value !== '';
}); });


const handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => { const handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {

+ 5
- 0
server/sonar-web/src/main/js/apps/settings/components/authentication/ProvisioningSection.tsx View File

ButtonSecondary, ButtonSecondary,
FlagMessage, FlagMessage,
RadioButton, RadioButton,
Spinner,
SubHeading, SubHeading,
} from 'design-system'; } from 'design-system';
import React, { FormEvent, ReactElement } from 'react'; import React, { FormEvent, ReactElement } from 'react';
import { ProvisioningType } from '../../../../types/provisioning'; import { ProvisioningType } from '../../../../types/provisioning';


interface Props { interface Props {
isLoading?: boolean;
provisioningType: ProvisioningType; provisioningType: ProvisioningType;
onChangeProvisioningType: (val: ProvisioningType) => void; onChangeProvisioningType: (val: ProvisioningType) => void;
disabledConfigText: string; disabledConfigText: string;


export default function ProvisioningSection(props: Readonly<Props>) { export default function ProvisioningSection(props: Readonly<Props>) {
const { const {
isLoading,
provisioningType, provisioningType,
jitTitle, jitTitle,
jitDescription, jitDescription,
<ButtonSecondary onClick={onCancel} disabled={!hasUnsavedChanges}> <ButtonSecondary onClick={onCancel} disabled={!hasUnsavedChanges}>
{translate('cancel')} {translate('cancel')}
</ButtonSecondary> </ButtonSecondary>
<Spinner loading={!!isLoading} />
<FlagMessage variant="warning" className="sw-mb-0"> <FlagMessage variant="warning" className="sw-mb-0">
{hasUnsavedChanges && {hasUnsavedChanges &&
!isLoading &&
translate('settings.authentication.github.configuration.unsaved_changes')} translate('settings.authentication.github.configuration.unsaved_changes')}
</FlagMessage> </FlagMessage>
</div> </div>

+ 59
- 14
server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Gitlab-it.tsx View File

expect(ui.saveConfigButton.get()).toBeDisabled(); expect(ui.saveConfigButton.get()).toBeDisabled();
await user.type(ui.url.get(), 'https://company.ui.com'); await user.type(ui.url.get(), 'https://company.ui.com');
await user.type(ui.secret.get(), '123'); await user.type(ui.secret.get(), '123');
expect(ui.saveConfigButton.get()).toBeDisabled();
await user.type(ui.groups.get(), 'NWA');
expect(ui.saveConfigButton.get()).toBeEnabled(); expect(ui.saveConfigButton.get()).toBeEnabled();
await user.click(ui.synchronizeGroups.get()); await user.click(ui.synchronizeGroups.get());
await user.click(ui.saveConfigButton.get()); await user.click(ui.saveConfigButton.get());
expect(ui.url.get()).toHaveValue('URL'); expect(ui.url.get()).toHaveValue('URL');
expect(ui.applicationId.get()).toBeInTheDocument(); expect(ui.applicationId.get()).toBeInTheDocument();
expect(ui.secret.query()).not.toBeInTheDocument(); expect(ui.secret.query()).not.toBeInTheDocument();
expect(ui.groups.get()).toHaveValue('Cypress Hill');
expect(ui.synchronizeGroups.get()).toBeChecked(); expect(ui.synchronizeGroups.get()).toBeChecked();


expect(ui.applicationId.get()).toBeInTheDocument(); expect(ui.applicationId.get()).toBeInTheDocument();
expect(ui.saveConfigButton.get()).toBeDisabled(); expect(ui.saveConfigButton.get()).toBeDisabled();
await user.type(ui.url.get(), 'www.internet.com'); await user.type(ui.url.get(), 'www.internet.com');
expect(ui.saveConfigButton.get()).toBeEnabled(); expect(ui.saveConfigButton.get()).toBeEnabled();

expect(ui.groups.get()).toHaveValue('Cypress Hill');
await user.click(ui.groups.get());
await user.click(ui.deleteGroupButton.get());
expect(ui.groups.get()).not.toHaveValue('Cypress Hill');
expect(ui.saveConfigButton.get()).toBeDisabled();
await user.click(ui.groups.get());
await user.type(ui.groups.get(), 'Run DMC');
expect(ui.saveConfigButton.get()).toBeEnabled();
await user.click(ui.saveConfigButton.get()); await user.click(ui.saveConfigButton.get());


expect(glContainer.get()).not.toHaveTextContent('URL'); expect(glContainer.get()).not.toHaveTextContent('URL');
expect(ui.editConfigButton.query()).not.toBeInTheDocument(); expect(ui.editConfigButton.query()).not.toBeInTheDocument();
}); });


it('should change from just-in-time to Auto Provisioning if auto was never set', async () => {
it('should be able to save just-in-time with no organizations', async () => {
const user = userEvent.setup();
renderAuthentication([Feature.GitlabProvisioning]);

expect(await ui.jitProvisioningRadioButton.find()).toBeChecked();

expect(ui.groups.get()).toHaveValue('Cypress Hill');
expect(await ui.saveProvisioning.find()).toBeDisabled();
await user.click(ui.deleteGroupButton.get());
expect(await ui.saveProvisioning.find()).toBeEnabled();
});

it('should not be able to save Auto provisioning with no organizations', async () => {
const user = userEvent.setup();
handler.setGitlabConfigurations([
mockGitlabConfiguration({
allowUsersToSignUp: false,
enabled: true,
provisioningType: ProvisioningType.auto,
allowedGroups: ['D12'],
isProvisioningTokenSet: true,
}),
]);
renderAuthentication([Feature.GitlabProvisioning]);

expect(await ui.autoProvisioningRadioButton.find()).toBeChecked();

expect(ui.groups.get()).toHaveValue('D12');
expect(ui.saveProvisioning.get()).toBeDisabled();
await user.click(ui.deleteGroupButton.get());
expect(await ui.saveProvisioning.find()).toBeDisabled();
});

it('should change from just-in-time to Auto Provisioning if auto was never set before', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
handler.setGitlabConfigurations([
mockGitlabConfiguration({
allowUsersToSignUp: false,
enabled: true,
provisioningType: ProvisioningType.jit,
allowedGroups: [],
isProvisioningTokenSet: false,
}),
]);
renderAuthentication([Feature.GitlabProvisioning]); renderAuthentication([Feature.GitlabProvisioning]);


expect(await ui.editConfigButton.find()).toBeInTheDocument(); expect(await ui.editConfigButton.find()).toBeInTheDocument();
expect(ui.jitProvisioningRadioButton.get()).toBeChecked(); expect(ui.jitProvisioningRadioButton.get()).toBeChecked();


user.click(ui.autoProvisioningRadioButton.get());
await user.click(ui.autoProvisioningRadioButton.get());
expect(await ui.autoProvisioningRadioButton.find()).toBeEnabled(); expect(await ui.autoProvisioningRadioButton.find()).toBeEnabled();
expect(ui.saveProvisioning.get()).toBeDisabled(); expect(ui.saveProvisioning.get()).toBeDisabled();


await user.type(ui.autoProvisioningToken.get(), 'JRR Tolkien'); await user.type(ui.autoProvisioningToken.get(), 'JRR Tolkien');
expect(await ui.saveProvisioning.find()).toBeDisabled();

await user.type(ui.groups.get(), 'Run DMC');
expect(await ui.saveProvisioning.find()).toBeEnabled();
await user.click(ui.deleteGroupButton.get());
expect(await ui.saveProvisioning.find()).toBeDisabled();

await user.type(ui.groups.get(), 'Public Enemy');
expect(await ui.saveProvisioning.find()).toBeEnabled(); expect(await ui.saveProvisioning.find()).toBeEnabled();
}); });




user.click(ui.autoProvisioningRadioButton.get()); user.click(ui.autoProvisioningRadioButton.get());
expect(await ui.autoProvisioningRadioButton.find()).toBeEnabled(); expect(await ui.autoProvisioningRadioButton.find()).toBeEnabled();
expect(await ui.saveProvisioning.find()).toBeEnabled();

expect(ui.groups.get()).toHaveValue('D12');
await user.click(ui.deleteGroupButton.get());
expect(await ui.saveProvisioning.find()).toBeDisabled();
await user.type(ui.groups.get(), 'Wu Tang Clan');

expect(ui.saveProvisioning.get()).toBeEnabled(); expect(ui.saveProvisioning.get()).toBeEnabled();
}); });



+ 0
- 1
server/sonar-web/src/main/js/types/provisioning.ts View File

url: string; url: string;
secret: string; secret: string;
synchronizeGroups: boolean; synchronizeGroups: boolean;
allowedGroups: string[];
} }


export type GitLabConfigurationUpdateBody = { export type GitLabConfigurationUpdateBody = {

+ 2
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

settings.authentication.gitlab.form.synchronizeGroups.name=Synchronize user groups settings.authentication.gitlab.form.synchronizeGroups.name=Synchronize user groups
settings.authentication.gitlab.form.synchronizeGroups.description=For each GitLab group they belong to, the user will be associated to a group with the same name (if it exists) in SonarQube. If enabled, the GitLab OAuth 2 application will need to provide the api scope. settings.authentication.gitlab.form.synchronizeGroups.description=For each GitLab group they belong to, the user will be associated to a group with the same name (if it exists) in SonarQube. If enabled, the GitLab OAuth 2 application will need to provide the api scope.
settings.authentication.gitlab.form.allowedGroups.name=Allowed groups settings.authentication.gitlab.form.allowedGroups.name=Allowed groups
settings.authentication.gitlab.form.allowedGroups.description=Only members of these groups (and sub-groups) will be allowed to authenticate. Please enter the group slug as it appears in the GitLab URL, for instance `my-gitlab-group`. If you use Auto-provisioning, only members of these groups (and sub-groups) will be provisioned.
settings.authentication.gitlab.form.allowedGroups.description.JIT=Only members of these groups (and sub-groups) will be allowed to authenticate. Please enter the group slug as it appears in the GitLab URL, for instance `my-gitlab-group`. ⚠︎ if not set and `Allow users to sign up` is enabled, any user from GitLab will be able to login to this SonarQube instance.
settings.authentication.gitlab.form.allowedGroups.description.AUTO_PROVISIONING=Only members of these groups (and sub-groups) will be provisioned. Please enter the group slug as it appears in the GitLab URL, for instance `my-gitlab-group`.
settings.authentication.gitlab.form.allowUsersToSignUp.name=Allow users to sign up settings.authentication.gitlab.form.allowUsersToSignUp.name=Allow users to sign up
settings.authentication.gitlab.form.allowUsersToSignUp.description=Allow new users to authenticate. When set to disabled, only existing users will be able to authenticate to the server. settings.authentication.gitlab.form.allowUsersToSignUp.description=Allow new users to authenticate. When set to disabled, only existing users will be able to authenticate to the server.
settings.authentication.gitlab.form.provisioningToken.name=Provisioning token settings.authentication.gitlab.form.provisioningToken.name=Provisioning token

Loading…
Cancel
Save