data,
);
}
+
+export function addGitlabRolesMapping(data: Omit<GitLabMapping, 'id'>) {
+ return axios.post<GitLabMapping>(GITLAB_PERMISSION_MAPPINGS, data);
+}
+
+export function deleteGitlabRolesMapping(role: string) {
+ return axios.delete(`${GITLAB_PERMISSION_MAPPINGS}/${encodeURIComponent(role)}`);
+}
import { mockPaging } from '../../helpers/testMocks';
import { GitLabMapping, GitlabConfiguration } from '../../types/provisioning';
import {
+ addGitlabRolesMapping,
createGitLabConfiguration,
deleteGitLabConfiguration,
+ deleteGitlabRolesMapping,
fetchGitLabConfiguration,
fetchGitLabConfigurations,
fetchGitlabRolesMapping,
mockGitlabConfiguration({ id: '1', enabled: true }),
];
-const gitlabMappingMock = (id: string, permissions: (keyof GitLabMapping['permissions'])[]) => ({
+const gitlabMappingMock = (
+ id: string,
+ permissions: (keyof GitLabMapping['permissions'])[],
+ baseRole = false,
+) => ({
id,
gitlabRole: id,
+ baseRole,
permissions: {
user: permissions.includes('user'),
codeViewer: permissions.includes('codeViewer'),
});
const defaultMapping: GitLabMapping[] = [
- gitlabMappingMock('guest', ['user', 'codeViewer']),
- gitlabMappingMock('reporter', ['user', 'codeViewer']),
- gitlabMappingMock('developer', [
- 'user',
- 'codeViewer',
- 'issueAdmin',
- 'securityHotspotAdmin',
- 'scan',
- ]),
- gitlabMappingMock('maintainer', [
- 'user',
- 'codeViewer',
- 'issueAdmin',
- 'securityHotspotAdmin',
- 'scan',
- ]),
- gitlabMappingMock('owner', [
- 'user',
- 'codeViewer',
- 'issueAdmin',
- 'securityHotspotAdmin',
- 'admin',
- 'scan',
- ]),
+ gitlabMappingMock('guest', ['user', 'codeViewer'], true),
+ gitlabMappingMock('reporter', ['user', 'codeViewer'], true),
+ gitlabMappingMock(
+ 'developer',
+ ['user', 'codeViewer', 'issueAdmin', 'securityHotspotAdmin', 'scan'],
+ true,
+ ),
+ gitlabMappingMock(
+ 'maintainer',
+ ['user', 'codeViewer', 'issueAdmin', 'securityHotspotAdmin', 'scan'],
+ true,
+ ),
+ gitlabMappingMock(
+ 'owner',
+ ['user', 'codeViewer', 'issueAdmin', 'securityHotspotAdmin', 'admin', 'scan'],
+ true,
+ ),
];
export default class GitlabProvisioningServiceMock {
);
};
+ handleAddGitlabRolesMapping: typeof addGitlabRolesMapping = (data) => {
+ const newRole = { ...data, id: data.gitlabRole };
+ this.gitlabMapping = [...this.gitlabMapping, newRole];
+
+ return Promise.resolve(newRole);
+ };
+
+ handleDeleteGitlabRolesMapping: typeof deleteGitlabRolesMapping = (id) => {
+ this.gitlabMapping = this.gitlabMapping.filter((el) => el.id !== id);
+ return Promise.resolve();
+ };
+
+ addGitLabCustomRole = (id: string, permissions: (keyof GitLabMapping['permissions'])[]) => {
+ this.gitlabMapping = [...this.gitlabMapping, gitlabMappingMock(id, permissions)];
+ };
+
reset = () => {
this.gitlabConfigurations = cloneDeep(defaultGitlabConfiguration);
};
type RolesMapping = GitHubMapping[] | GitLabMapping[] | null;
interface Props {
- canAddCustomRole?: boolean;
isLoading: boolean;
mapping: RolesMapping;
mappingFor: AlmKeys.GitHub | AlmKeys.GitLab;
};
function PermissionRow(props: Readonly<PermissionCellProps>) {
- const { mapping } = props;
+ const { list, mapping } = props;
const isGitHubMapping = 'githubRole' in mapping;
const role = isGitHubMapping ? mapping.githubRole : mapping.gitlabRole;
- const isBaseRole = isGitHubMapping ? mapping.baseRole : true;
- const list = props.list as GitHubMapping[];
+ const isBaseRole = mapping.baseRole;
+
+ const setMapping = () => {
+ if (isGitHubMapping) {
+ return props.setMapping(
+ (list as GitHubMapping[])?.filter((r) => r.githubRole !== role) ?? null,
+ );
+ }
+ return props.setMapping(
+ (list as GitLabMapping[])?.filter((r) => r.gitlabRole !== role) ?? null,
+ );
+ };
return (
<TableRowInteractive>
'settings.authentication.configuration.roles_mapping.dialog.delete_custom_role',
role,
)}
- onClick={() => {
- props.setMapping(list?.filter((r) => r.githubRole !== role) ?? null);
- }}
+ onClick={setMapping}
Icon={TrashIcon}
size="small"
/>
checked={value}
onCheck={(newValue) =>
props.setMapping(
- list?.map((item) =>
+ (list?.map((item) =>
item.id === mapping.id
? { ...item, permissions: { ...item.permissions, [key]: newValue } }
: item,
- ) ?? null,
+ ) ?? null) as RolesMapping,
)
}
/>
}
export function DevopsRolesMappingModal(props: Readonly<Props>) {
- const { canAddCustomRole, isLoading, mapping, mappingFor, onClose, roles, setMapping } = props;
+ const { isLoading, mapping, mappingFor, onClose, roles, setMapping } = props;
const permissions = convertToPermissionDefinitions(
PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE,
'projects_role',
e.preventDefault();
const value = customRoleInput.trim();
if (
+ mappingFor === AlmKeys.GitHub &&
!(list as GitHubMapping[])?.some((el) =>
el.baseRole ? el.githubRole.toLowerCase() === value.toLowerCase() : el.githubRole === value,
)
...((list as GitHubMapping[]) ?? []),
]);
setCustomRoleInput('');
+ } else if (
+ mappingFor === AlmKeys.GitLab &&
+ !(list as GitLabMapping[])?.some((el) =>
+ el.baseRole ? el.gitlabRole.toLowerCase() === value.toLowerCase() : el.gitlabRole === value,
+ )
+ ) {
+ setMapping([
+ {
+ id: customRoleInput,
+ gitlabRole: customRoleInput,
+ permissions: { ...DEFAULT_CUSTOM_ROLE_PERMISSIONS },
+ },
+ ...((list as GitLabMapping[]) ?? []),
+ ]);
+ setCustomRoleInput('');
} else {
setCustomRoleError(true);
}
};
- const haveEmptyCustomRoles =
- mappingFor === AlmKeys.GitHub &&
- !!mapping?.some((el) => !el.baseRole && !Object.values(el.permissions).some(Boolean));
+ const haveEmptyCustomRoles = !!mapping?.some(
+ (el) => !el.baseRole && !Object.values(el.permissions).some(Boolean),
+ );
const formBody = (
<div className="sw-p-0">
}
>
{list
- ?.filter((r) => ('githubRole' in r ? r.baseRole : true))
+ ?.filter((r) => r.baseRole)
.map((mapping) => (
<PermissionRow key={mapping.id} mapping={mapping} setMapping={setMapping} list={list} />
))}
- {canAddCustomRole && (
- <>
- <TableRow>
- <ContentCell colSpan={7}>
- <div className="sw-flex sw-items-end">
- <form className="sw-flex sw-items-end" onSubmit={validateAndAddCustomRole}>
- <FormField
- htmlFor="custom-role-input"
- label={translate(
- 'settings.authentication.configuration.roles_mapping.dialog.add_custom_role',
- )}
- >
- <InputField
- className="sw-w-[300px]"
- id="custom-role-input"
- maxLength={4000}
- value={customRoleInput}
- onChange={(event) => {
- setCustomRoleError(false);
- setCustomRoleInput(event.currentTarget.value);
- }}
- type="text"
- />
- </FormField>
- <ButtonSecondary
- type="submit"
- className="sw-ml-2 sw-mr-4"
- disabled={customRoleInput.trim() === '' || customRoleError}
- >
- {translate('add_verb')}
- </ButtonSecondary>
- </form>
- {customRoleError && (
- <FlagMessage variant="error">
- {translate('settings.authentication.configuration.roles_mapping.role_exists')}
- </FlagMessage>
+ <TableRow>
+ <ContentCell colSpan={7}>
+ <div className="sw-flex sw-items-end">
+ <form className="sw-flex sw-items-end" onSubmit={validateAndAddCustomRole}>
+ <FormField
+ htmlFor="custom-role-input"
+ label={translate(
+ 'settings.authentication.configuration.roles_mapping.dialog.add_custom_role',
)}
- </div>
- </ContentCell>
- </TableRow>
+ >
+ <InputField
+ className="sw-w-[300px]"
+ id="custom-role-input"
+ maxLength={4000}
+ value={customRoleInput}
+ onChange={(event) => {
+ setCustomRoleError(false);
+ setCustomRoleInput(event.currentTarget.value);
+ }}
+ type="text"
+ />
+ </FormField>
+ <ButtonSecondary
+ type="submit"
+ className="sw-ml-2 sw-mr-4"
+ disabled={customRoleInput.trim() === '' || customRoleError}
+ >
+ {translate('add_verb')}
+ </ButtonSecondary>
+ </form>
+ {customRoleError && (
+ <FlagMessage variant="error">
+ {translate('settings.authentication.configuration.roles_mapping.role_exists')}
+ </FlagMessage>
+ )}
+ </div>
+ </ContentCell>
+ </TableRow>
- {list
- ?.filter((r) => !r.baseRole)
- .map((mapping) => (
- <PermissionRow
- key={mapping.id}
- mapping={mapping}
- setMapping={setMapping}
- list={list}
- />
- ))}
- </>
- )}
+ {list
+ ?.filter((r) => !r.baseRole)
+ .map((mapping) => (
+ <PermissionRow key={mapping.id} mapping={mapping} setMapping={setMapping} list={list} />
+ ))}
</Table>
-
- {canAddCustomRole && (
- <FlagMessage variant="info">
- {translate(
- 'settings.authentication.github.configuration.roles_mapping.dialog.custom_roles_description',
- )}
- </FlagMessage>
- )}
+ <FlagMessage variant="info">
+ {translateWithParameters(
+ 'settings.authentication.configuration.roles_mapping.dialog.custom_roles_description',
+ translate('alm', mappingFor),
+ )}
+ </FlagMessage>
<Spinner isLoading={isLoading} />
</div>
);
return (
- <Modal onClose={onClose} isLarge>
+ <Modal closeOnOverlayClick={!haveEmptyCustomRoles} onClose={onClose} isLarge>
<Modal.Header title={header} />
<Modal.Body>{formBody}</Modal.Body>
<Modal.Footer
const { data: roles, isPending } = useGithubRolesMappingQuery();
return (
<DevopsRolesMappingModal
- canAddCustomRole
isLoading={isPending}
mappingFor={AlmKeys.GitHub}
roles={roles}
}).byRole('button', {
name: 'close',
}),
+ customRoleInput: byRole('textbox', {
+ name: 'settings.authentication.configuration.roles_mapping.dialog.add_custom_role',
+ }),
+ customRoleAddBtn: byRole('dialog', {
+ name: 'settings.authentication.configuration.roles_mapping.dialog.title.alm.gitlab',
+ }).byRole('button', { name: 'add_verb' }),
+ roleExistsError: byRole('dialog', {
+ name: 'settings.authentication.configuration.roles_mapping.dialog.title.alm.gitlab',
+ }).byText('settings.authentication.configuration.roles_mapping.role_exists'),
+ emptyRoleError: byRole('dialog', {
+ name: 'settings.authentication.configuration.roles_mapping.dialog.title.alm.gitlab',
+ }).byText('settings.authentication.configuration.roles_mapping.empty_custom_role'),
+ deleteCustomRoleCustom2: byRole('button', {
+ name: 'settings.authentication.configuration.roles_mapping.dialog.delete_custom_role.custom2',
+ }),
getMappingRowByRole: (text: string) =>
ui.mappingRow.getAll().find((row) => within(row).queryByText(text) !== null),
};
await user.click(ui.autoProvisioningRadioButton.get());
expect(await ui.editMappingButton.find()).toBeInTheDocument();
await user.click(ui.editMappingButton.get());
-
- expect(await ui.mappingRow.findAll()).toHaveLength(6);
+ expect(await ui.mappingRow.findAll()).toHaveLength(7);
let guestCheckboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('guest'));
let ownerCheckboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('owner'));
expect(ownerCheckboxes[5]).not.toBeChecked();
await user.click(ui.mappingDialogClose.get());
});
+
+ it('should add/remove/update custom roles', async () => {
+ const user = userEvent.setup();
+ handler.setGitlabConfigurations([
+ mockGitlabConfiguration({
+ allowUsersToSignUp: false,
+ enabled: true,
+ provisioningType: ProvisioningType.auto,
+ allowedGroups: ['D12'],
+ isProvisioningTokenSet: true,
+ }),
+ ]);
+ handler.addGitLabCustomRole('custom1', ['user', 'codeViewer', 'scan']);
+ handler.addGitLabCustomRole('custom2', ['user', 'codeViewer', 'issueAdmin', 'scan']);
+ renderAuthentication([Feature.GitlabProvisioning]);
+
+ expect(await ui.saveProvisioning.find()).toBeDisabled();
+ await user.click(ui.editMappingButton.get());
+
+ const rows = (await ui.mappingRow.findAll()).filter(
+ (row) => within(row).queryAllByRole('checkbox').length > 0,
+ );
+
+ expect(rows).toHaveLength(7);
+
+ let custom1Checkboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('custom1'));
+
+ expect(custom1Checkboxes[0]).toBeChecked();
+ expect(custom1Checkboxes[1]).toBeChecked();
+ expect(custom1Checkboxes[2]).not.toBeChecked();
+ expect(custom1Checkboxes[3]).not.toBeChecked();
+ expect(custom1Checkboxes[4]).not.toBeChecked();
+ expect(custom1Checkboxes[5]).toBeChecked();
+
+ await user.click(custom1Checkboxes[1]);
+ await user.click(custom1Checkboxes[2]);
+
+ await user.click(ui.deleteCustomRoleCustom2.get());
+
+ expect(ui.customRoleInput.get()).toHaveValue('');
+ await user.type(ui.customRoleInput.get(), 'guest');
+ await user.click(ui.customRoleAddBtn.get());
+ expect(await ui.roleExistsError.find()).toBeInTheDocument();
+ expect(ui.customRoleAddBtn.get()).toBeDisabled();
+ await user.clear(ui.customRoleInput.get());
+ expect(ui.roleExistsError.query()).not.toBeInTheDocument();
+ await user.type(ui.customRoleInput.get(), 'custom1');
+ await user.click(ui.customRoleAddBtn.get());
+ expect(await ui.roleExistsError.find()).toBeInTheDocument();
+ expect(ui.customRoleAddBtn.get()).toBeDisabled();
+ await user.clear(ui.customRoleInput.get());
+ await user.type(ui.customRoleInput.get(), 'custom3');
+ expect(ui.roleExistsError.query()).not.toBeInTheDocument();
+ expect(ui.customRoleAddBtn.get()).toBeEnabled();
+ await user.click(ui.customRoleAddBtn.get());
+
+ let custom3Checkboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('custom3'));
+ expect(custom3Checkboxes[0]).toBeChecked();
+ expect(custom3Checkboxes[1]).not.toBeChecked();
+ expect(custom3Checkboxes[2]).not.toBeChecked();
+ expect(custom3Checkboxes[3]).not.toBeChecked();
+ expect(custom3Checkboxes[4]).not.toBeChecked();
+ expect(custom3Checkboxes[5]).not.toBeChecked();
+ await user.click(custom3Checkboxes[0]);
+ expect(await ui.emptyRoleError.find()).toBeInTheDocument();
+ expect(ui.mappingDialogClose.get()).toBeDisabled();
+ await user.click(custom3Checkboxes[1]);
+ expect(ui.emptyRoleError.query()).not.toBeInTheDocument();
+ expect(ui.mappingDialogClose.get()).toBeEnabled();
+ await user.click(ui.mappingDialogClose.get());
+
+ expect(await ui.saveProvisioning.find()).toBeEnabled();
+ await user.click(ui.saveProvisioning.get());
+
+ // Clean local mapping state
+ await user.click(ui.jitProvisioningRadioButton.get());
+ await user.click(ui.autoProvisioningRadioButton.get());
+
+ await user.click(ui.editMappingButton.get());
+
+ expect(
+ (await ui.mappingRow.findAll()).filter(
+ (row) => within(row).queryAllByRole('checkbox').length > 0,
+ ),
+ ).toHaveLength(7);
+ custom1Checkboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('custom1'));
+ custom3Checkboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('custom3'));
+ expect(ui.getMappingRowByRole('custom2')).toBeUndefined();
+ expect(custom1Checkboxes[0]).toBeChecked();
+ expect(custom1Checkboxes[1]).not.toBeChecked();
+ expect(custom1Checkboxes[2]).toBeChecked();
+ expect(custom1Checkboxes[3]).not.toBeChecked();
+ expect(custom1Checkboxes[4]).not.toBeChecked();
+ expect(custom1Checkboxes[5]).toBeChecked();
+ expect(custom3Checkboxes[0]).not.toBeChecked();
+ expect(custom3Checkboxes[1]).toBeChecked();
+ expect(custom3Checkboxes[2]).not.toBeChecked();
+ expect(custom3Checkboxes[3]).not.toBeChecked();
+ expect(custom3Checkboxes[4]).not.toBeChecked();
+ expect(custom3Checkboxes[5]).not.toBeChecked();
+ await user.click(ui.mappingDialogClose.get());
+ });
});
function renderAuthentication(features: Feature[] = []) {
if (state) {
const newData = unionBy(
addedOrChanged,
- state.filter((s) => !deleted.find((id) => id === s.id)),
+ state.filter((s) => deleted.find((id) => id === s.id) === undefined),
(el) => el.id,
);
client.setQueryData(queryKey, newData);
import { isEqual, keyBy, partition, pick, unionBy } from 'lodash';
import { getActivity } from '../../api/ce';
import {
+ addGitlabRolesMapping,
createGitLabConfiguration,
deleteGitLabConfiguration,
+ deleteGitlabRolesMapping,
fetchGitLabConfigurations,
fetchGitlabRolesMapping,
syncNowGitLabProvisioning,
mutationFn: async (mapping: GitLabMapping[]) => {
const state = keyBy(client.getQueryData<GitLabMapping[]>(queryKey), (m) => m.id);
- const [maybeChangedRoles] = partition(mapping, (m) => state[m.id]);
+ const [maybeChangedRoles, newRoles] = partition(mapping, (m) => state[m.id]);
const changedRoles = maybeChangedRoles.filter((item) => !isEqual(item, state[item.id]));
+ const deletedRoles = Object.values(state).filter(
+ (m) => !m.baseRole && !mapping.some((cm) => m.id === cm.id),
+ );
return {
addedOrChanged: await Promise.all([
...changedRoles.map((data) =>
updateGitlabRolesMapping(data.id, pick(data, 'permissions')),
),
+ ...newRoles.map((m) => addGitlabRolesMapping(m)),
]),
+ deleted: await Promise.all([
+ deletedRoles.map((dm) => deleteGitlabRolesMapping(dm.id)),
+ ]).then(() => deletedRoles.map((dm) => dm.id)),
};
},
- onSuccess: ({ addedOrChanged }) => {
+ onSuccess: ({ addedOrChanged, deleted }) => {
const state = client.getQueryData<GitLabMapping[]>(queryKey);
if (state) {
- const newData = unionBy(addedOrChanged, state, (el) => el.id);
+ const newData = unionBy(
+ addedOrChanged,
+ state.filter((s) => deleted.find((id) => id === s.id) === undefined),
+ (el) => el.id,
+ );
client.setQueryData(queryKey, newData);
}
addGlobalSuccessMessage(
settings.authentication.github.configuration.validation.details.invalid_label=Invalid
settings.authentication.github.configuration.validation.details.org_not_found={0} (not found or app not installed)
settings.authentication.github.configuration.roles_mapping.description=When synchronizing users and groups, SonarQube assigns permissions based on GitHub user and team roles. You can customize the mapping of permissions. The new mapping will take effect at the next synchronization.
-settings.authentication.github.configuration.roles_mapping.dialog.custom_roles_description=When a custom role name added here matches an existing GitHub custom role in any of your organizations, the mapping applies to all users with this custom role. If an existing GitHub custom role has no exact match in this list, the permissions of its inherited base role are mapped.
settings.authentication.github.configuration.roles_mapping.save_success=GitHub roles mapping saved successfully.
settings.authentication.github.configuration.unsaved_changes=You have unsaved changes.
settings.authentication.configuration.roles_mapping.dialog.add_custom_role=Add custom role:
settings.authentication.configuration.roles_mapping.role_exists=Role already exists.
settings.authentication.configuration.roles_mapping.empty_custom_role=Custom roles should have some permissions.
+settings.authentication.configuration.roles_mapping.dialog.custom_roles_description=When a custom role name added here matches an existing {0} custom role in any of your organizations, the mapping applies to all users with this custom role. If an existing {0} custom role has no exact match in this list, the permissions of its inherited base role are mapped.
# SAML
settings.authentication.form.create.saml=New SAML configuration