"@react-spring/rafz": "9.7.3",
"@react-spring/web": "9.7.3",
"@tanstack/react-query": "4.33.0",
+ "axios": "1.5.0",
"classnames": "2.3.2",
"clipboard": "2.0.11",
"core-js": "3.32.1",
*/
import { cloneDeep } from 'lodash';
import { mockTask } from '../../helpers/mocks/tasks';
-import { GitHubConfigurationStatus, GitHubProvisioningStatus } from '../../types/provisioning';
+import {
+ GitHubConfigurationStatus,
+ GitHubMapping,
+ GitHubProvisioningStatus,
+} from '../../types/provisioning';
import { Task, TaskStatuses, TaskTypes } from '../../types/tasks';
import {
activateGithubProvisioning,
deactivateGithubProvisioning,
deactivateScim,
fetchGithubProvisioningStatus,
+ fetchGithubRolesMapping,
fetchIsScimEnabled,
+ updateGithubRolesMapping,
} from '../provisioning';
jest.mock('../provisioning');
],
};
+const defaultMapping: GitHubMapping[] = [
+ {
+ id: 'read',
+ roleName: 'read',
+ permissions: {
+ user: true,
+ codeviewer: true,
+ issueadmin: false,
+ securityhotspotadmin: false,
+ admin: false,
+ scan: false,
+ },
+ },
+ {
+ id: 'write',
+ roleName: 'write',
+ permissions: {
+ user: true,
+ codeviewer: true,
+ issueadmin: true,
+ securityhotspotadmin: true,
+ admin: false,
+ scan: true,
+ },
+ },
+ {
+ id: 'triage',
+ roleName: 'triage',
+ permissions: {
+ user: true,
+ codeviewer: true,
+ issueadmin: false,
+ securityhotspotadmin: false,
+ admin: false,
+ scan: false,
+ },
+ },
+ {
+ id: 'maintain',
+ roleName: 'maintain',
+ permissions: {
+ user: true,
+ codeviewer: true,
+ issueadmin: true,
+ securityhotspotadmin: true,
+ admin: false,
+ scan: true,
+ },
+ },
+ {
+ id: 'admin',
+ roleName: 'admin',
+ permissions: {
+ user: true,
+ codeviewer: true,
+ issueadmin: true,
+ securityhotspotadmin: true,
+ admin: true,
+ scan: true,
+ },
+ },
+];
+
export default class AuthenticationServiceMock {
scimStatus: boolean;
githubProvisioningStatus: boolean;
githubConfigurationStatus: GitHubConfigurationStatus;
+ githubMapping: GitHubMapping[];
tasks: Task[];
constructor() {
this.scimStatus = false;
this.githubProvisioningStatus = false;
this.githubConfigurationStatus = cloneDeep(defaultConfigurationStatus);
+ this.githubMapping = cloneDeep(defaultMapping);
this.tasks = [];
jest.mocked(activateScim).mockImplementation(this.handleActivateScim);
jest.mocked(deactivateScim).mockImplementation(this.handleDeactivateScim);
jest
.mocked(checkConfigurationValidity)
.mockImplementation(this.handleCheckConfigurationValidity);
+ jest.mocked(fetchGithubRolesMapping).mockImplementation(this.handleFetchGithubRolesMapping);
+ jest.mocked(updateGithubRolesMapping).mockImplementation(this.handleUpdateGithubRolesMapping);
}
addProvisioningTask = (overrides: Partial<Omit<Task, 'type'>> = {}) => {
return Promise.resolve(this.githubConfigurationStatus);
};
+ handleFetchGithubRolesMapping: typeof fetchGithubRolesMapping = () => {
+ return Promise.resolve(this.githubMapping);
+ };
+
+ handleUpdateGithubRolesMapping: typeof updateGithubRolesMapping = (id, data) => {
+ this.githubMapping = this.githubMapping.map((mapping) =>
+ mapping.id === id ? { ...mapping, ...data } : mapping
+ );
+
+ return Promise.resolve(
+ this.githubMapping.find((mapping) => mapping.id === id) as GitHubMapping
+ );
+ };
+
reset = () => {
this.scimStatus = false;
this.githubProvisioningStatus = false;
this.githubConfigurationStatus = cloneDeep(defaultConfigurationStatus);
+ this.githubMapping = cloneDeep(defaultMapping);
this.tasks = [];
};
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import axios from 'axios';
import { throwGlobalError } from '../helpers/error';
import { getJSON, post, postJSON } from '../helpers/request';
-import { GitHubConfigurationStatus, GithubStatus } from '../types/provisioning';
+import { GitHubConfigurationStatus, GitHubMapping, GithubStatus } from '../types/provisioning';
export function fetchIsScimEnabled(): Promise<boolean> {
return getJSON('/api/scim_management/status')
export function syncNowGithubProvisioning(): Promise<void> {
return post('/api/github_provisioning/sync').catch(throwGlobalError);
}
+
+export function fetchGithubRolesMapping(): Promise<GitHubMapping[]> {
+ return axios.get('/api/v2/github-permissions-mapping');
+}
+
+export function updateGithubRolesMapping(
+ role: string,
+ data: Partial<Pick<GitHubMapping, 'permissions'>>
+): Promise<GitHubMapping> {
+ return axios.patch(`/api/v2/github-permissions-mapping/${role}`, data);
+}
/* String.prototype.replaceAll. This is why we also import core-js. */
import 'core-js/stable';
/* */
+import axios from 'axios';
import { getAvailableFeatures } from '../api/features';
import { getGlobalNavigation } from '../api/navigation';
import { getCurrentUser } from '../api/users';
import { installExtensionsHandler, installWebAnalyticsHandler } from '../helpers/extensionsHandler';
+import { addGlobalErrorMessage } from '../helpers/globalMessages';
import { loadL10nBundle } from '../helpers/l10nBundle';
+import { axiosToCatch, parseErrorResponse } from '../helpers/request';
import { getBaseUrl, getSystemStatus, initAppVariables } from '../helpers/system';
import './styles/sonar.ts';
initApplication();
async function initApplication() {
+ axiosToCatch.interceptors.response.use((response) => response.data);
+ 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(),
isMainApp() ? getCurrentUser() : undefined,
--- /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 classNames from 'classnames';
+import * as React from 'react';
+import Checkbox from '../../../../components/controls/Checkbox';
+import Modal from '../../../../components/controls/Modal';
+import { SubmitButton } from '../../../../components/controls/buttons';
+import PermissionHeader from '../../../../components/permissions/PermissionHeader';
+import Spinner from '../../../../components/ui/Spinner';
+import { translate } from '../../../../helpers/l10n';
+import {
+ PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE,
+ convertToPermissionDefinitions,
+ isPermissionDefinitionGroup,
+} from '../../../../helpers/permissions';
+import { useGithubRolesMappingQuery } from '../../../../queries/identity-provider';
+import { GitHubMapping } from '../../../../types/provisioning';
+
+interface Props {
+ mapping: GitHubMapping[] | null;
+ setMapping: React.Dispatch<React.SetStateAction<GitHubMapping[] | null>>;
+ 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'
+ );
+
+ React.useEffect(() => {
+ if (!mapping && roles) {
+ setMapping(roles);
+ }
+ }, [roles, mapping, setMapping]);
+
+ const header = translate(
+ 'settings.authentication.github.configuration.roles_mapping.dialog.title'
+ );
+
+ return (
+ <Modal contentLabel={header} onRequestClose={onClose} shouldCloseOnEsc size="medium">
+ <div className="modal-head">
+ <h2>{header}</h2>
+ </div>
+ <div className="modal-body modal-container sw-p-0">
+ <table className="data zebra permissions-table">
+ <thead>
+ <tr>
+ <th scope="col" className="nowrap bordered-bottom sw-pl-[10px] sw-align-middle">
+ <b>
+ {translate(
+ 'settings.authentication.github.configuration.roles_mapping.dialog.roles_column'
+ )}
+ </b>
+ </th>
+ {permissions.map((permission) => (
+ <PermissionHeader
+ key={
+ isPermissionDefinitionGroup(permission) ? permission.category : permission.key
+ }
+ onSelectPermission={() => {}}
+ permission={permission}
+ />
+ ))}
+ </tr>
+ </thead>
+ <tbody>
+ {mapping?.map(({ id, roleName, permissions }) => (
+ <tr key={id}>
+ <th scope="row" className="nowrap text-middle sw-pl-[10px]">
+ <b>{roleName}</b>
+ </th>
+ {Object.entries(permissions).map(([key, value]) => (
+ <td key={key} className={classNames('permission-column text-center text-middle')}>
+ <Checkbox
+ checked={value}
+ onCheck={(newValue) =>
+ setMapping(
+ mapping.map((item) =>
+ item.id === id
+ ? { ...item, permissions: { ...item.permissions, [key]: newValue } }
+ : item
+ )
+ )
+ }
+ />
+ </td>
+ ))}
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ <Spinner loading={isLoading} />
+ </div>
+ <div className="modal-foot">
+ <SubmitButton onClick={onClose}>{translate('close')}</SubmitButton>
+ </div>
+ </Modal>
+ );
+}
* 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, { useState } from 'react';
+import React, { FormEvent, useState } from 'react';
import { FormattedMessage } from 'react-intl';
import GitHubSynchronisationWarning from '../../../../app/components/GitHubSynchronisationWarning';
import DocLink from '../../../../components/common/DocLink';
import AutoProvisioningConsent from './AutoProvisionningConsent';
import ConfigurationForm from './ConfigurationForm';
import GitHubConfigurationValidity from './GitHubConfigurationValidity';
+import GitHubMappingModal from './GitHubMappingModal';
import useGithubConfiguration, {
GITHUB_ADDITIONAL_FIELDS,
GITHUB_JIT_FIELDS,
const { definitions, currentTab } = props;
const { data } = useIdentityProviderQuery();
const [showEditModal, setShowEditModal] = useState(false);
+ const [showMappingModal, setShowMappingModal] = useState(false);
const [showConfirmProvisioningModal, setShowConfirmProvisioningModal] = useState(false);
const {
appId,
enabled,
newGithubProvisioningStatus,
- setNewGithubProvisioningStatus,
+ setProvisioningType,
hasGithubProvisioningTypeChange,
hasGithubProvisioningConfigChange,
resetJitSetting,
saveGroup,
changeProvisioning,
toggleEnable,
+ rolesMapping,
+ setRolesMapping,
+ saveMapping,
hasLegacyConfiguration,
deleteMutation: { isLoading: isDeleting, mutate: deleteConfiguration },
} = useGithubConfiguration(definitions);
setShowEditModal(false);
};
+ const handleSubmit = (e: FormEvent) => {
+ e.preventDefault();
+ if (hasGithubProvisioningTypeChange) {
+ setShowConfirmProvisioningModal(true);
+ } else {
+ saveGroup();
+ if (newGithubProvisioningStatus ?? githubProvisioningStatus) {
+ saveMapping();
+ }
+ }
+ };
+
return (
<div className="authentication-configuration">
<div className="spacer-bottom display-flex-space-between display-flex-center">
</div>
</div>
<div className="spacer-bottom big-padded bordered display-flex-space-between">
- <form
- onSubmit={async (e) => {
- e.preventDefault();
- if (hasGithubProvisioningTypeChange) {
- setShowConfirmProvisioningModal(true);
- } else {
- await saveGroup();
- }
- }}
- >
+ <form onSubmit={handleSubmit}>
<fieldset className="display-flex-column big-spacer-bottom">
<label className="h5">
{translate('settings.authentication.form.provisioning')}
label={translate('settings.authentication.form.provisioning_at_login')}
title={translate('settings.authentication.form.provisioning_at_login')}
selected={!(newGithubProvisioningStatus ?? githubProvisioningStatus)}
- onClick={() => setNewGithubProvisioningStatus(false)}
+ onClick={() => setProvisioningType(false)}
>
<p className="spacer-bottom">
<FormattedMessage id="settings.authentication.github.form.provisioning_at_login.description" />
'settings.authentication.github.form.provisioning_with_github',
)}
selected={newGithubProvisioningStatus ?? githubProvisioningStatus}
- onClick={() => setNewGithubProvisioningStatus(true)}
+ onClick={() => setProvisioningType(true)}
disabled={!hasGithubProvisioning || hasDifferentProvider}
>
{hasGithubProvisioning ? (
<ResetButtonLink
className="spacer-left"
onClick={() => {
- setNewGithubProvisioningStatus(undefined);
+ setProvisioningType(undefined);
resetJitSetting();
}}
disabled={!hasGithubProvisioningConfigChange}
)}
</ConfirmModal>
)}
+ {showMappingModal && (
+ <GitHubMappingModal
+ mapping={rolesMapping}
+ setMapping={setRolesMapping}
+ onClose={() => setShowMappingModal(false)}
+ />
+ )}
</form>
</div>
</>
enableConfigButton: byRole('button', { name: 'settings.authentication.form.enable' }),
disableConfigButton: byRole('button', { name: 'settings.authentication.form.disable' }),
editConfigButton: byRole('button', { name: 'settings.authentication.form.edit' }),
+ editMappingButton: byRole('button', {
+ name: 'settings.authentication.github.configuration.roles_mapping.button_label',
+ }),
+ mappingRow: byRole('dialog', {
+ name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
+ }).byRole('row'),
+ mappingCheckbox: byRole('checkbox'),
+ mappingDialogClose: byRole('dialog', {
+ name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
+ }).byRole('button', {
+ name: 'close',
+ }),
deleteOrg: (org: string) =>
byRole('button', {
name: `settings.definition.delete_value.property.sonar.auth.github.organizations.name.${org}`,
expect(await github.jitProvisioningButton.find()).toBeChecked();
expect(github.consentDialog.query()).not.toBeInTheDocument();
});
+
+ it('should sort mapping rows', async () => {
+ const user = userEvent.setup();
+ settingsHandler.presetGithubAutoProvisioning();
+ handler.enableGithubProvisioning();
+ renderAuthentication([Feature.GithubProvisioning]);
+ await user.click(await github.tab.find());
+
+ expect(await github.editMappingButton.find()).toBeInTheDocument();
+ await user.click(github.editMappingButton.get());
+
+ expect(await github.mappingRow.findAll()).toHaveLength(6);
+ expect(github.mappingRow.getAt(1)).toHaveTextContent('read');
+ expect(github.mappingRow.getAt(2)).toHaveTextContent('triage');
+ expect(github.mappingRow.getAt(3)).toHaveTextContent('write');
+ expect(github.mappingRow.getAt(4)).toHaveTextContent('maintain');
+ expect(github.mappingRow.getAt(5)).toHaveTextContent('admin');
+ });
+
+ it('should apply new mapping and new provisioning type at the same time', async () => {
+ const user = userEvent.setup();
+ renderAuthentication([Feature.GithubProvisioning]);
+ await user.click(await github.tab.find());
+
+ await github.createConfiguration(user);
+ await user.click(await github.enableConfigButton.find());
+
+ expect(await github.jitProvisioningButton.find()).toBeChecked();
+ expect(github.editMappingButton.query()).not.toBeInTheDocument();
+ await user.click(github.githubProvisioningButton.get());
+ expect(await github.editMappingButton.find()).toBeInTheDocument();
+ await user.click(github.editMappingButton.get());
+
+ expect(await github.mappingRow.findAll()).toHaveLength(6);
+
+ let rowOneCheckboxes = github.mappingCheckbox.getAll(github.mappingRow.getAt(1));
+ let rowFiveCheckboxes = github.mappingCheckbox.getAll(github.mappingRow.getAt(5));
+
+ expect(rowOneCheckboxes[0]).toBeChecked();
+ expect(rowOneCheckboxes[5]).not.toBeChecked();
+ expect(rowFiveCheckboxes[5]).toBeChecked();
+
+ await user.click(rowOneCheckboxes[0]);
+ await user.click(rowOneCheckboxes[5]);
+ await user.click(rowFiveCheckboxes[5]);
+ await user.click(github.mappingDialogClose.get());
+
+ await user.click(github.saveGithubProvisioning.get());
+ await act(() => user.click(github.confirmProvisioningButton.get()));
+
+ // Clean local mapping state
+ await user.click(github.jitProvisioningButton.get());
+ await user.click(github.githubProvisioningButton.get());
+
+ await user.click(github.editMappingButton.get());
+ rowOneCheckboxes = github.mappingCheckbox.getAll(github.mappingRow.getAt(1));
+ rowFiveCheckboxes = github.mappingCheckbox.getAll(github.mappingRow.getAt(5));
+
+ expect(rowOneCheckboxes[0]).not.toBeChecked();
+ expect(rowOneCheckboxes[5]).toBeChecked();
+ expect(rowFiveCheckboxes[5]).not.toBeChecked();
+ await user.click(github.mappingDialogClose.get());
+ });
+
+ it('should apply new mapping on auto-provisioning', async () => {
+ const user = userEvent.setup();
+ settingsHandler.presetGithubAutoProvisioning();
+ handler.enableGithubProvisioning();
+ renderAuthentication([Feature.GithubProvisioning]);
+ await user.click(await github.tab.find());
+
+ expect(await github.saveGithubProvisioning.find()).toBeDisabled();
+ await user.click(github.editMappingButton.get());
+
+ expect(await github.mappingRow.findAll()).toHaveLength(6);
+
+ let rowOneCheckbox = github.mappingCheckbox.getAll(github.mappingRow.getAt(1))[0];
+
+ expect(rowOneCheckbox).toBeChecked();
+
+ await user.click(rowOneCheckbox);
+ await user.click(github.mappingDialogClose.get());
+
+ expect(await github.saveGithubProvisioning.find()).toBeEnabled();
+
+ await act(() => user.click(github.saveGithubProvisioning.get()));
+
+ // Clean local mapping state
+ await user.click(github.jitProvisioningButton.get());
+ await user.click(github.githubProvisioningButton.get());
+
+ await user.click(github.editMappingButton.get());
+ rowOneCheckbox = github.mappingCheckbox.getAll(github.mappingRow.getAt(1))[0];
+
+ expect(rowOneCheckbox).not.toBeChecked();
+ await user.click(github.mappingDialogClose.get());
+ });
});
});
import { AvailableFeaturesContext } from '../../../../../app/components/available-features/AvailableFeaturesContext';
import {
useGithubProvisioningEnabledQuery,
+ useGithubRolesMappingMutation,
useToggleGithubProvisioningMutation,
} from '../../../../../queries/identity-provider';
import { useSaveValueMutation, useSaveValuesMutation } from '../../../../../queries/settings';
import { Feature } from '../../../../../types/features';
+import { GitHubMapping } from '../../../../../types/provisioning';
import { ExtendedSettingDefinition } from '../../../../../types/settings';
import useConfiguration from './useConfiguration';
const { data: githubProvisioningStatus } = useGithubProvisioningEnabledQuery();
const toggleGithubProvisioning = useToggleGithubProvisioningMutation();
const [newGithubProvisioningStatus, setNewGithubProvisioningStatus] = useState<boolean>();
+ const [rolesMapping, setRolesMapping] = useState<GitHubMapping[] | null>(null);
const hasGithubProvisioningTypeChange =
newGithubProvisioningStatus !== undefined &&
newGithubProvisioningStatus !== githubProvisioningStatus;
const hasGithubProvisioningConfigChange =
- some(GITHUB_ADDITIONAL_FIELDS, isValueChange) || hasGithubProvisioningTypeChange;
+ 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 enabled = values[GITHUB_ENABLED_FIELD]?.value === 'true';
const appId = values[GITHUB_APP_ID_FIELD]?.value as string;
if (!newGithubProvisioningStatus || !githubProvisioningStatus) {
saveGroup();
}
+ if (newGithubProvisioningStatus ?? githubProvisioningStatus) {
+ saveMapping();
+ }
};
const saveGroup = () => {
saveSettings(newValues);
};
+ const saveMapping = () => {
+ if (rolesMapping) {
+ updateMapping(rolesMapping);
+ }
+ };
+
const toggleEnable = () => {
const value = values[GITHUB_ENABLED_FIELD];
saveSetting({ newValue: !enabled, definition: value.definition });
const hasLegacyConfiguration = appId === undefined && !clientIdIsNotSet;
+ const setProvisioningType = (value: boolean | undefined) => {
+ setRolesMapping(null);
+ setNewGithubProvisioningStatus(value);
+ };
+
return {
...config,
url,
hasGithubProvisioning,
githubProvisioningStatus,
newGithubProvisioningStatus,
- setNewGithubProvisioningStatus,
+ setProvisioningType,
hasGithubProvisioningTypeChange,
hasGithubProvisioningConfigChange,
changeProvisioning,
saveGroup,
resetJitSetting,
toggleEnable,
+ rolesMapping,
+ setRolesMapping,
+ saveMapping,
hasLegacyConfiguration,
};
}
}
return (
<th
+ scope="col"
className={classNames('permission-column text-center text-middle', {
selected:
!isPermissionDefinitionGroup(permission) &&
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import axios, { AxiosResponse } from 'axios';
import { isNil, omitBy } from 'lodash';
import { Dict } from '../types/types';
import { getCookie } from './cookies';
export function parseError(response: Response): Promise<string> {
const DEFAULT_MESSAGE = translate('default_error_message');
return parseJSON(response)
- .then(({ errors, message }) => message ?? errors.map((error: any) => error.msg).join('. '))
+ .then(parseErrorResponse)
.catch(() => DEFAULT_MESSAGE);
}
+export function parseErrorResponse(response?: AxiosResponse | Response): string {
+ const DEFAULT_MESSAGE = translate('default_error_message');
+ let data;
+ if (!response) {
+ return DEFAULT_MESSAGE;
+ }
+ if ('data' in response) {
+ ({ data } = response);
+ } else {
+ data = response;
+ }
+ const { message, errors } = data;
+ return (
+ message ?? errors?.map((error: { msg: string }) => error.msg).join('. ') ?? DEFAULT_MESSAGE
+ );
+}
+
/**
* Shortcut to do a GET request and return a Response
*/
ServiceUnavailable = 503,
GatewayTimeout = 504,
}
+
+export const axiosToCatch = axios.create();
*/
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { isEqual, omit } from 'lodash';
import { useContext } from 'react';
import {
activateGithubProvisioning,
deactivateGithubProvisioning,
deactivateScim,
fetchGithubProvisioningStatus,
+ fetchGithubRolesMapping,
fetchIsScimEnabled,
syncNowGithubProvisioning,
+ updateGithubRolesMapping,
} from '../api/provisioning';
import { getSystemInfo } from '../api/system';
import { AvailableFeaturesContext } from '../app/components/available-features/AvailableFeaturesContext';
import { mapReactQueryResult } from '../helpers/react-query';
import { Feature } from '../types/features';
+import { GitHubMapping } from '../types/provisioning';
import { SysInfoCluster } from '../types/types';
+const MAPPING_STALE_TIME = 60_000;
+
export function useIdentityProviderQuery() {
return useQuery(['identity_provider'], async () => {
const info = (await getSystemInfo()) as SysInfoCluster;
canSyncNow: data?.enabled && !data.nextSync && !mutation.isLoading,
};
}
+
+export function useGithubRolesMappingQuery() {
+ 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);
+ }
+ return a.roleName.localeCompare(b.roleName);
+ }),
+ });
+}
+
+export function useGithubRolesMappingMutation() {
+ const client = useQueryClient();
+ const queryKey = ['identity_provider', 'github_mapping'];
+ return useMutation({
+ mutationFn: (mapping: GitHubMapping[]) => {
+ const state = client.getQueryData<GitHubMapping[]>(queryKey);
+ const changedRoles = state
+ ? mapping.filter(
+ (item) =>
+ !isEqual(
+ item,
+ state.find((el) => el.id === item.id)
+ )
+ )
+ : mapping;
+ return Promise.all(
+ changedRoles.map((data) => updateGithubRolesMapping(data.id, omit(data, 'id', 'roleName')))
+ );
+ },
+ onSuccess: (data) => {
+ const state = client.getQueryData<GitHubMapping[]>(queryKey);
+ if (state) {
+ client.setQueryData(
+ queryKey,
+ state.map((item) => {
+ const changed = data.find((el) => el.id === item.id);
+ return changed ?? item;
+ })
+ );
+ }
+ },
+ });
+}
autoProvisioning: GitHubProvisioning;
}[];
}
+
+export interface GitHubMapping {
+ id: string;
+ roleName: string;
+ permissions: {
+ user: boolean;
+ codeviewer: boolean;
+ issueadmin: boolean;
+ securityhotspotadmin: boolean;
+ admin: boolean;
+ scan: boolean;
+ };
+}
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.dialog.title=GitHub Roles Mapping
+settings.authentication.github.configuration.roles_mapping.dialog.roles_column=Roles
# SAML
settings.authentication.form.create.saml=New SAML configuration