handleUpdateGithubRolesMapping: typeof updateGithubRolesMapping = (id, data) => {
this.githubMapping = this.githubMapping.map((mapping) =>
- mapping.id === id ? { ...mapping, ...data } : mapping
+ mapping.id === id ? { ...mapping, ...data } : mapping,
);
return Promise.resolve(
- this.githubMapping.find((mapping) => mapping.id === id) as GitHubMapping
+ this.githubMapping.find((mapping) => mapping.id === id) as GitHubMapping,
);
};
return post('/api/github_provisioning/sync').catch(throwGlobalError);
}
-export function fetchGithubRolesMapping(): Promise<GitHubMapping[]> {
- return axios.get('/api/v2/github-permissions-mapping');
+export function fetchGithubRolesMapping() {
+ return axios
+ .get<unknown, { githubPermissionsMappings: GitHubMapping[] }>(
+ '/api/v2/github-permission-mappings',
+ )
+ .then((data) => data.githubPermissionsMappings);
}
export function updateGithubRolesMapping(
role: string,
- data: Partial<Pick<GitHubMapping, 'permissions'>>
+ data: Partial<Pick<GitHubMapping, 'permissions'>>,
): Promise<GitHubMapping> {
- return axios.patch(`/api/v2/github-permissions-mapping/${role}`, data);
+ return axios.patch(`/api/v2/github-permission-mappings/${role}`, data);
}
async function initApplication() {
axiosToCatch.interceptors.response.use((response) => response.data);
+ axiosToCatch.defaults.headers.patch['Content-Type'] = 'application/merge-patch+json';
+ axios.defaults.headers.patch['Content-Type'] = 'application/merge-patch+json';
axios.interceptors.response.use(
(response) => response.data,
(error) => {
const { response } = error;
addGlobalErrorMessage(parseErrorResponse(response));
return Promise.reject(response);
- }
+ },
);
const [l10nBundle, currentUser, appState, availableFeatures] = await Promise.all([
loadL10nBundle(),
import ValidationInput, {
ValidationInputErrorPlacement,
} from '../../../../components/controls/ValidationInput';
-import MandatoryFieldMarker from '../../../../components/ui/MandatoryFieldMarker';
import { ExtendedSettingDefinition, SettingType } from '../../../../types/settings';
import { getPropertyDescription, getPropertyName, isSecuredDefinition } from '../../utils';
+import AuthenticationFormFieldWrapper from './AuthenticationFormFieldWrapper';
import AuthenticationMultiValueField from './AuthenticationMultiValuesField';
import AuthenticationSecuredField from './AuthenticationSecuredField';
import AuthenticationToggleField from './AuthenticationToggleField';
const description = getPropertyDescription(definition);
return (
- <div className="settings-definition">
- <div className="settings-definition-left">
- <label className="h3" htmlFor={definition.key}>
- {name}
- </label>
- {mandatory && <MandatoryFieldMarker />}
- {definition.description && <div className="markdown small spacer-top">{description}</div>}
- </div>
- <div className="settings-definition-right big-padded-top display-flex-column">
- {definition.multiValues && (
- <AuthenticationMultiValueField
- definition={definition}
- settingValue={settingValue as string[]}
- onFieldChange={(value) => props.onFieldChange(definition.key, value)}
- />
+ <AuthenticationFormFieldWrapper
+ title={name}
+ defKey={definition.key}
+ mandatory={mandatory}
+ description={description}
+ >
+ {definition.multiValues && (
+ <AuthenticationMultiValueField
+ definition={definition}
+ settingValue={settingValue as string[]}
+ onFieldChange={(value) => props.onFieldChange(definition.key, value)}
+ />
+ )}
+ {isSecuredDefinition(definition) && (
+ <AuthenticationSecuredField
+ definition={definition}
+ settingValue={String(settingValue ?? '')}
+ onFieldChange={props.onFieldChange}
+ isNotSet={isNotSet}
+ />
+ )}
+ {!isSecuredDefinition(definition) && definition.type === SettingType.BOOLEAN && (
+ <AuthenticationToggleField
+ definition={definition}
+ settingValue={settingValue as string | boolean}
+ onChange={(value) => props.onFieldChange(definition.key, value)}
+ />
+ )}
+ {!isSecuredDefinition(definition) &&
+ definition.type === undefined &&
+ !definition.multiValues && (
+ <ValidationInput
+ error={error}
+ errorPlacement={ValidationInputErrorPlacement.Bottom}
+ isValid={false}
+ isInvalid={Boolean(error)}
+ >
+ <input
+ className="width-100"
+ id={definition.key}
+ maxLength={4000}
+ name={definition.key}
+ onChange={(e) => props.onFieldChange(definition.key, e.currentTarget.value)}
+ type="text"
+ value={String(settingValue ?? '')}
+ />
+ </ValidationInput>
)}
- {isSecuredDefinition(definition) && (
- <AuthenticationSecuredField
- definition={definition}
- settingValue={String(settingValue ?? '')}
- onFieldChange={props.onFieldChange}
- isNotSet={isNotSet}
- />
- )}
- {!isSecuredDefinition(definition) && definition.type === SettingType.BOOLEAN && (
- <AuthenticationToggleField
- definition={definition}
- settingValue={settingValue as string | boolean}
- onChange={(value) => props.onFieldChange(definition.key, value)}
- />
- )}
- {!isSecuredDefinition(definition) &&
- definition.type === undefined &&
- !definition.multiValues && (
- <ValidationInput
- error={error}
- errorPlacement={ValidationInputErrorPlacement.Bottom}
- isValid={false}
- isInvalid={Boolean(error)}
- >
- <input
- className="width-100"
- id={definition.key}
- maxLength={4000}
- name={definition.key}
- onChange={(e) => props.onFieldChange(definition.key, e.currentTarget.value)}
- type="text"
- value={String(settingValue ?? '')}
- />
- </ValidationInput>
- )}
- </div>
- </div>
+ </AuthenticationFormFieldWrapper>
);
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React, { PropsWithChildren } from 'react';
+import MandatoryFieldMarker from '../../../../components/ui/MandatoryFieldMarker';
+
+interface Props {
+ readonly title: string;
+ readonly description?: string;
+ readonly defKey?: string;
+ readonly mandatory?: boolean;
+}
+
+export default function AuthenticationFormFieldWrapper(props: PropsWithChildren<Props>) {
+ const { mandatory = false, title, description, defKey, children } = props;
+
+ return (
+ <div className="settings-definition">
+ <div className="settings-definition-left">
+ <label className="h3" htmlFor={defKey}>
+ {title}
+ </label>
+ {mandatory && <MandatoryFieldMarker />}
+ {description && <div className="markdown small spacer-top">{description}</div>}
+ </div>
+ <div className="settings-definition-right big-padded-top display-flex-column">{children}</div>
+ </div>
+ );
+}
import { GitHubMapping } from '../../../../types/provisioning';
interface Props {
- mapping: GitHubMapping[] | null;
- setMapping: React.Dispatch<React.SetStateAction<GitHubMapping[] | null>>;
- onClose: () => void;
+ readonly mapping: GitHubMapping[] | null;
+ readonly setMapping: React.Dispatch<React.SetStateAction<GitHubMapping[] | null>>;
+ readonly onClose: () => void;
}
export default function GitHubMappingModal({ mapping, setMapping, onClose }: Props) {
const { data: roles, isLoading } = useGithubRolesMappingQuery();
const permissions = convertToPermissionDefinitions(
PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE,
- 'projects_role'
+ 'projects_role',
);
- React.useEffect(() => {
- if (!mapping && roles) {
- setMapping(roles);
- }
- }, [roles, mapping, setMapping]);
-
const header = translate(
- 'settings.authentication.github.configuration.roles_mapping.dialog.title'
+ 'settings.authentication.github.configuration.roles_mapping.dialog.title',
);
return (
<th scope="col" className="nowrap bordered-bottom sw-pl-[10px] sw-align-middle">
<b>
{translate(
- 'settings.authentication.github.configuration.roles_mapping.dialog.roles_column'
+ 'settings.authentication.github.configuration.roles_mapping.dialog.roles_column',
)}
</b>
</th>
key={
isPermissionDefinitionGroup(permission) ? permission.category : permission.key
}
- onSelectPermission={() => {}}
permission={permission}
/>
))}
</tr>
</thead>
<tbody>
- {mapping?.map(({ id, roleName, permissions }) => (
+ {(mapping ?? roles)?.map(({ id, roleName, permissions }) => (
<tr key={id}>
<th scope="row" className="nowrap text-middle sw-pl-[10px]">
<b>{roleName}</b>
checked={value}
onCheck={(newValue) =>
setMapping(
- mapping.map((item) =>
+ (mapping ?? roles)?.map((item) =>
item.id === id
? { ...item, permissions: { ...item.permissions, [key]: newValue } }
- : item
- )
+ : item,
+ ) ?? null,
)
}
/>
import { ExtendedSettingDefinition } from '../../../../types/settings';
import { AuthenticationTabs, DOCUMENTATION_LINK_SUFFIXES } from './Authentication';
import AuthenticationFormField from './AuthenticationFormField';
+import AuthenticationFormFieldWrapper from './AuthenticationFormFieldWrapper';
import AutoProvisioningConsent from './AutoProvisionningConsent';
import ConfigurationForm from './ConfigurationForm';
import GitHubConfigurationValidity from './GitHubConfigurationValidity';
hasGithubProvisioningTypeChange,
hasGithubProvisioningConfigChange,
resetJitSetting,
- saveGroup,
changeProvisioning,
toggleEnable,
rolesMapping,
setRolesMapping,
- saveMapping,
+ applyAdditionalOptions,
hasLegacyConfiguration,
deleteMutation: { isLoading: isDeleting, mutate: deleteConfiguration },
} = useGithubConfiguration(definitions);
if (hasGithubProvisioningTypeChange) {
setShowConfirmProvisioningModal(true);
} else {
- saveGroup();
- if (newGithubProvisioningStatus ?? githubProvisioningStatus) {
- saveMapping();
- }
+ applyAdditionalOptions();
}
};
{enabled ? (
<div className="display-flex-column spacer-top">
<RadioCard
+ className="sw-min-h-0"
label={translate('settings.authentication.form.provisioning_at_login')}
title={translate('settings.authentication.form.provisioning_at_login')}
selected={!(newGithubProvisioningStatus ?? githubProvisioningStatus)}
)}
</RadioCard>
<RadioCard
- className="spacer-top"
+ className="spacer-top sw-min-h-0"
label={translate(
'settings.authentication.github.form.provisioning_with_github',
)}
</p>
{githubProvisioningStatus && <GitHubSynchronisationWarning />}
-
- <div className="sw-flex sw-flex-1 spacer-bottom">
- <Button
- className="spacer-top width-30"
- onClick={synchronizeNow}
- disabled={!canSyncNow}
- >
- {translate('settings.authentication.github.synchronize_now')}
- </Button>
- </div>
{(newGithubProvisioningStatus ?? githubProvisioningStatus) && (
<>
+ <div className="sw-flex sw-flex-1 spacer-bottom">
+ <Button
+ className="spacer-top width-30"
+ onClick={synchronizeNow}
+ disabled={!canSyncNow}
+ >
+ {translate('settings.authentication.github.synchronize_now')}
+ </Button>
+ </div>
<hr />
{Object.values(values).map((val) => {
if (!GITHUB_PROVISIONING_FIELDS.includes(val.key)) {
</div>
);
})}
+ <AuthenticationFormFieldWrapper
+ title={translate(
+ 'settings.authentication.github.configuration.roles_mapping.title',
+ )}
+ description={translate(
+ 'settings.authentication.github.configuration.roles_mapping.description',
+ )}
+ >
+ <Button
+ className="spacer-top"
+ onClick={() => setShowMappingModal(true)}
+ >
+ {translate(
+ 'settings.authentication.github.configuration.roles_mapping.button_label',
+ )}
+ </Button>
+ </AuthenticationFormFieldWrapper>
</>
)}
</>
)}
</fieldset>
{enabled && (
- <>
+ <div className="sw-flex sw-gap-2 sw-h-8 sw-items-center">
<SubmitButton disabled={!hasGithubProvisioningConfigChange}>
{translate('save')}
</SubmitButton>
<ResetButtonLink
- className="spacer-left"
onClick={() => {
setProvisioningType(undefined);
resetJitSetting();
>
{translate('cancel')}
</ResetButtonLink>
- </>
+ <Alert variant="warning" className="sw-w-[300px] sw-mb-0">
+ {hasGithubProvisioningConfigChange &&
+ translate('settings.authentication.github.configuration.unsaved_changes')}
+ </Alert>
+ </div>
)}
{showConfirmProvisioningModal && (
<ConfirmModal
{samlEnabled ? (
<div className="display-flex-column spacer-top">
<RadioCard
+ className="sw-min-h-0"
label={translate('settings.authentication.saml.form.provisioning_at_login')}
title={translate('settings.authentication.saml.form.provisioning_at_login')}
selected={!(newScimStatus ?? scimStatus)}
</p>
</RadioCard>
<RadioCard
- className="spacer-top"
+ className="spacer-top sw-min-h-0"
label={translate('settings.authentication.saml.form.provisioning_with_scim')}
title={translate('settings.authentication.saml.form.provisioning_with_scim')}
selected={newScimStatus ?? scimStatus}
newGithubProvisioningStatus !== undefined &&
newGithubProvisioningStatus !== githubProvisioningStatus;
const hasGithubProvisioningConfigChange =
- some(GITHUB_ADDITIONAL_FIELDS, isValueChange) || hasGithubProvisioningTypeChange || rolesMapping;
+ some(GITHUB_ADDITIONAL_FIELDS, isValueChange) ||
+ hasGithubProvisioningTypeChange ||
+ rolesMapping;
const resetJitSetting = () => {
GITHUB_ADDITIONAL_FIELDS.forEach((s) => setNewValue(s));
const { mutate: saveSetting } = useSaveValueMutation();
const { mutate: saveSettings } = useSaveValuesMutation();
- const { mutate: updateMapping } = useGithubRolesMappingMutation();
+ const { mutateAsync: updateMapping } = useGithubRolesMappingMutation();
const enabled = values[GITHUB_ENABLED_FIELD]?.value === 'true';
const appId = values[GITHUB_APP_ID_FIELD]?.value as string;
if (hasGithubProvisioningTypeChange) {
await toggleGithubProvisioning.mutateAsync(!!newGithubProvisioningStatus);
}
- if (!newGithubProvisioningStatus || !githubProvisioningStatus) {
- saveGroup();
- }
- if (newGithubProvisioningStatus ?? githubProvisioningStatus) {
- saveMapping();
- }
+ applyAdditionalOptions();
};
- const saveGroup = () => {
+ const applyAdditionalOptions = () => {
const newValues = GITHUB_ADDITIONAL_FIELDS.map((settingKey) => values[settingKey]);
saveSettings(newValues);
- };
-
- const saveMapping = () => {
- if (rolesMapping) {
- updateMapping(rolesMapping);
+ if (newGithubProvisioningStatus ?? githubProvisioningStatus) {
+ if (rolesMapping) {
+ updateMapping(rolesMapping)
+ .then(() => {
+ setRolesMapping(null);
+ })
+ .catch(() => {});
+ }
}
};
hasGithubProvisioningTypeChange,
hasGithubProvisioningConfigChange,
changeProvisioning,
- saveGroup,
resetJitSetting,
toggleEnable,
rolesMapping,
setRolesMapping,
- saveMapping,
+ applyAdditionalOptions,
hasLegacyConfiguration,
};
}
return useQuery(['identity_provider', 'github_mapping'], fetchGithubRolesMapping, {
staleTime: MAPPING_STALE_TIME,
select: (data) =>
- data.sort((a, b) => {
- const hardcodedValues = ['admin', 'maintain', 'write', 'triage', 'read'];
- if (hardcodedValues.includes(a.id) || hardcodedValues.includes(b.id)) {
- return hardcodedValues.indexOf(b.id) - hardcodedValues.indexOf(a.id);
+ [...data].sort((a, b) => {
+ // Order is reversed to put non-existing roles at the end (their index is -1)
+ const defaultRoleOrder = ['admin', 'maintain', 'write', 'triage', 'read'];
+ if (defaultRoleOrder.includes(a.id) || defaultRoleOrder.includes(b.id)) {
+ return defaultRoleOrder.indexOf(b.id) - defaultRoleOrder.indexOf(a.id);
}
return a.roleName.localeCompare(b.roleName);
}),
(item) =>
!isEqual(
item,
- state.find((el) => el.id === item.id)
- )
+ state.find((el) => el.id === item.id),
+ ),
)
: mapping;
return Promise.all(
- changedRoles.map((data) => updateGithubRolesMapping(data.id, omit(data, 'id', 'roleName')))
+ changedRoles.map((data) => updateGithubRolesMapping(data.id, omit(data, 'id', 'roleName'))),
);
},
onSuccess: (data) => {
state.map((item) => {
const changed = data.find((el) => el.id === item.id);
return changed ?? item;
- })
+ }),
);
}
},
}),
);
},
- onSuccess: () => {
- queryClient.invalidateQueries(['settings']);
+ onSuccess: (data) => {
+ if (data.length > 0) {
+ queryClient.invalidateQueries(['settings']);
+ }
},
});
}
"@typescript-eslint/parser": 5.59.11
"@wojtekmaj/enzyme-adapter-react-17": 0.8.0
autoprefixer: 10.4.15
+ axios: 1.5.0
chalk: 4.1.2
chokidar: 3.5.3
classnames: 2.3.2
languageName: node
linkType: hard
+"axios@npm:1.5.0":
+ version: 1.5.0
+ resolution: "axios@npm:1.5.0"
+ dependencies:
+ follow-redirects: ^1.15.0
+ form-data: ^4.0.0
+ proxy-from-env: ^1.1.0
+ checksum: e7405a5dbbea97760d0e6cd58fecba311b0401ddb4a8efbc4108f5537da9b3f278bde566deb777935a960beec4fa18e7b8353881f2f465e4f2c0e949fead35be
+ languageName: node
+ linkType: hard
+
"axobject-query@npm:^3.1.1":
version: 3.1.1
resolution: "axobject-query@npm:3.1.1"
languageName: node
linkType: hard
+"follow-redirects@npm:^1.15.0":
+ version: 1.15.2
+ resolution: "follow-redirects@npm:1.15.2"
+ peerDependenciesMeta:
+ debug:
+ optional: true
+ checksum: faa66059b66358ba65c234c2f2a37fcec029dc22775f35d9ad6abac56003268baf41e55f9ee645957b32c7d9f62baf1f0b906e68267276f54ec4b4c597c2b190
+ languageName: node
+ linkType: hard
+
"for-each@npm:^0.3.3":
version: 0.3.3
resolution: "for-each@npm:0.3.3"
languageName: node
linkType: hard
+"proxy-from-env@npm:^1.1.0":
+ version: 1.1.0
+ resolution: "proxy-from-env@npm:1.1.0"
+ checksum: ed7fcc2ba0a33404958e34d95d18638249a68c430e30fcb6c478497d72739ba64ce9810a24f53a7d921d0c065e5b78e3822759800698167256b04659366ca4d4
+ languageName: node
+ linkType: hard
+
"psl@npm:^1.1.33":
version: 1.8.0
resolution: "psl@npm:1.8.0"
settings.authentication.github.configuration.validation.details.valid_label=Valid
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.title=Role permission mapping
+settings.authentication.github.configuration.roles_mapping.description=When synchronizing users and groups we rely on GitHub user and team roles to assign sonarqube permissions. You can customize how the mapping will be applied. Be aware that the mapping will take effect from the next synchronization.
+settings.authentication.github.configuration.roles_mapping.button_label=Edit mapping
settings.authentication.github.configuration.roles_mapping.dialog.title=GitHub Roles Mapping
settings.authentication.github.configuration.roles_mapping.dialog.roles_column=Roles
+settings.authentication.github.configuration.unsaved_changes=You have unsaved changes.
# SAML
settings.authentication.form.create.saml=New SAML configuration