From c4819c189044e302cfc9ab38ee30d617199181a3 Mon Sep 17 00:00:00 2001 From: guillaume-peoch-sonarsource Date: Wed, 3 May 2023 11:46:18 +0200 Subject: [PATCH] SONAR-19084 Disable Provisioning when another provisioning is enabled --- .../authentication/Authentication.tsx | 21 ++++++++++++++-- ...ionTab.tsx => GithubAuthenticationTab.tsx} | 17 ++++++++++--- .../authentication/SamlAuthenticationTab.tsx | 17 ++++++++++--- .../__tests__/Authentication-it.tsx | 13 +++++++--- .../hook/useGithubConfiguration.ts | 8 ++++-- .../hook/useSamlConfiguration.ts | 25 +++++++------------ .../resources/org/sonar/l10n/core.properties | 2 ++ 7 files changed, 72 insertions(+), 31 deletions(-) rename server/sonar-web/src/main/js/apps/settings/components/authentication/{GithubAutheticationTab.tsx => GithubAuthenticationTab.tsx} (95%) diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx index a10aa8293f2..96d3160caef 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx @@ -19,8 +19,10 @@ */ import classNames from 'classnames'; import * as React from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { useSearchParams } from 'react-router-dom'; +import { getSystemInfo } from '../../../../api/system'; import withAvailableFeatures, { WithAvailableFeaturesProps, } from '../../../../app/components/available-features/withAvailableFeatures'; @@ -35,9 +37,10 @@ import { searchParamsToQuery } from '../../../../helpers/urls'; import { AlmKeys } from '../../../../types/alm-settings'; import { Feature } from '../../../../types/features'; import { ExtendedSettingDefinition } from '../../../../types/settings'; +import { SysInfoCluster } from '../../../../types/types'; import { AUTHENTICATION_CATEGORY } from '../../constants'; import CategoryDefinitionsList from '../CategoryDefinitionsList'; -import GithubAithentication from './GithubAutheticationTab'; +import GithubAuthenticationTab from './GithubAuthenticationTab'; import SamlAuthenticationTab, { SAML } from './SamlAuthenticationTab'; interface Props { @@ -75,6 +78,16 @@ export function Authentication(props: Props & WithAvailableFeaturesProps) { const { definitions } = props; const [query, setSearchParams] = useSearchParams(); + const [provider, setProvider] = useState(); + + const loadProvider = useCallback(async () => { + const info = (await getSystemInfo()) as SysInfoCluster; + setProvider(info.System['External Users and Groups Provisioning']); + }, []); + + useEffect(() => { + loadProvider(); + }, []); const currentTab = (query.get('tab') || SAML) as AuthenticationTabs; @@ -169,12 +182,16 @@ export function Authentication(props: Props & WithAvailableFeaturesProps) { {tab.key === SAML && ( def.subCategory === SAML)} + provider={provider} + onReload={() => loadProvider()} /> )} {tab.key === AlmKeys.GitHub && ( - def.subCategory === AlmKeys.GitHub)} + provider={provider} + onReload={() => loadProvider()} /> )} diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAutheticationTab.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx similarity index 95% rename from server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAutheticationTab.tsx rename to server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx index da6fb4043ae..b1092b998a0 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAutheticationTab.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx @@ -47,6 +47,8 @@ import useGithubConfiguration, { interface GithubAuthenticationProps { definitions: ExtendedSettingDefinition[]; + provider: string | undefined; + onReload: () => void; } const GITHUB_EXCLUDED_FIELD = [ @@ -56,7 +58,8 @@ const GITHUB_EXCLUDED_FIELD = [ 'sonar.auth.github.organizations', ]; -export default function GithubAithentication(props: GithubAuthenticationProps) { +export default function GithubAuthenticationTab(props: GithubAuthenticationProps) { + const { definitions, provider } = props; const [showEditModal, setShowEditModal] = useState(false); const [showConfirmProvisioningModal, setShowConfirmProvisioningModal] = useState(false); @@ -77,7 +80,9 @@ export default function GithubAithentication(props: GithubAuthenticationProps) { setNewGithubProvisioningStatus, hasGithubProvisioningConfigChange, resetJitSetting, - } = useGithubConfiguration(props.definitions); + } = useGithubConfiguration(definitions, props.onReload); + + const hasDifferentProvider = provider !== undefined && provider !== 'github'; const handleCreateConfiguration = () => { setShowEditModal(true); @@ -104,7 +109,6 @@ export default function GithubAithentication(props: GithubAuthenticationProps) { GITHUB_JIT_FIELDS.map(async (settingKey) => { const value = values[settingKey]; if (value.newValue !== undefined) { - // isEmpty always return true for booleans... if (isEmpty(value.newValue) && typeof value.newValue !== 'boolean') { await resetSettingValue({ keys: value.definition.key }); } else { @@ -199,10 +203,15 @@ export default function GithubAithentication(props: GithubAuthenticationProps) { )} selected={newGithubProvisioningStatus ?? githubProvisioningStatus} onClick={() => setNewGithubProvisioningStatus(true)} - disabled={!hasGithubProvisioning} + disabled={!hasGithubProvisioning || hasDifferentProvider} > {hasGithubProvisioning ? ( <> + {hasDifferentProvider && ( +

+ {translate('settings.authentication.form.other_provisioning_enabled')} +

+ )}

{translate( 'settings.authentication.github.form.provisioning_with_github.description' diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx index d9bc32db7fa..d376ecc3f59 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx @@ -49,6 +49,8 @@ import useSamlConfiguration, { interface SamlAuthenticationProps { definitions: ExtendedSettingDefinition[]; + provider: string | undefined; + onReload: () => void; } export const SAML = 'saml'; @@ -57,7 +59,7 @@ const CONFIG_TEST_PATH = '/saml/validation_init'; const SAML_EXCLUDED_FIELD = [SAML_ENABLED_FIELD, SAML_GROUP_NAME, SAML_SCIM_DEPRECATED]; export default function SamlAuthenticationTab(props: SamlAuthenticationProps) { - const { definitions } = props; + const { definitions, provider, onReload } = props; const [showEditModal, setShowEditModal] = React.useState(false); const [showConfirmProvisioningModal, setShowConfirmProvisioningModal] = React.useState(false); const { @@ -78,7 +80,9 @@ export default function SamlAuthenticationTab(props: SamlAuthenticationProps) { setNewGroupSetting, reload, deleteConfiguration, - } = useSamlConfiguration(definitions); + } = useSamlConfiguration(definitions, onReload); + + const hasDifferentProvider = provider !== undefined && provider !== name; const handleCreateConfiguration = () => { setShowEditModal(true); @@ -187,7 +191,7 @@ export default function SamlAuthenticationTab(props: SamlAuthenticationProps) { >

{samlEnabled ? (
@@ -196,7 +200,7 @@ export default function SamlAuthenticationTab(props: SamlAuthenticationProps) { title={translate('settings.authentication.saml.form.provisioning_with_scim')} selected={newScimStatus ?? scimStatus} onClick={() => setNewScimStatus(true)} - disabled={!hasScim} + disabled={!hasScim || hasDifferentProvider} > {!hasScim ? (

@@ -216,6 +220,11 @@ export default function SamlAuthenticationTab(props: SamlAuthenticationProps) {

) : ( <> + {hasDifferentProvider && ( +

+ {translate('settings.authentication.form.other_provisioning_enabled')} +

+ )}

{translate( 'settings.authentication.saml.form.provisioning_with_scim.sub' diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx index d3a55581d5f..dfb6e21e5b5 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx @@ -23,6 +23,7 @@ import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup'; import React from 'react'; import { byRole, byText } from 'testing-library-selector'; import AuthenticationServiceMock from '../../../../../api/mocks/AuthenticationServiceMock'; +import SystemServiceMock from '../../../../../api/mocks/SystemServiceMock'; import { AvailableFeaturesContext } from '../../../../../app/components/available-features/AvailableFeaturesContext'; import { definitions } from '../../../../../helpers/mocks/definitions-list'; import { renderComponent } from '../../../../../helpers/testReactTestingUtils'; @@ -30,14 +31,20 @@ import { Feature } from '../../../../../types/features'; import Authentication from '../Authentication'; jest.mock('../../../../../api/settings'); +jest.mock('../../../../../api/system'); let handler: AuthenticationServiceMock; +let system: SystemServiceMock; beforeEach(() => { handler = new AuthenticationServiceMock(); + system = new SystemServiceMock(); }); -afterEach(() => handler.resetValues()); +afterEach(() => { + handler.resetValues(); + system.reset(); +}); const ui = { saveButton: byRole('button', { name: 'settings.authentication.saml.form.save' }), @@ -64,7 +71,7 @@ const ui = { editConfigButton: byRole('button', { name: 'settings.authentication.form.edit' }), enableFirstMessage: byText('settings.authentication.saml.enable_first'), jitProvisioningButton: byRole('radio', { - name: 'settings.authentication.form.provisioning_at_login', + name: 'settings.authentication.saml.form.provisioning_at_login', }), scimProvisioningButton: byRole('radio', { name: 'settings.authentication.saml.form.provisioning_with_scim', @@ -239,7 +246,7 @@ describe('SAML tab', () => { expect(await saml.saveScim.find()).toBeDisabled(); }); - it('should not allow edtion below Enterprise to select SCIM provisioning', async () => { + it('should not allow editions below Enterprise to select SCIM provisioning', async () => { const { saml } = ui; const user = userEvent.setup(); diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useGithubConfiguration.ts b/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useGithubConfiguration.ts index 65d932da6ec..a8eaa7eb533 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useGithubConfiguration.ts +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useGithubConfiguration.ts @@ -45,7 +45,10 @@ export interface SamlSettingValue { definition: ExtendedSettingDefinition; } -export default function useGithubConfiguration(definitions: ExtendedSettingDefinition[]) { +export default function useGithubConfiguration( + definitions: ExtendedSettingDefinition[], + onReload: () => void +) { const config = useConfiguration(definitions, OPTIONAL_FIELDS); const { values, isValueChange, setNewValue, reload: reloadConfig } = config; const hasGithubProvisioning = useContext(AvailableFeaturesContext).includes( @@ -77,7 +80,8 @@ export default function useGithubConfiguration(definitions: ExtendedSettingDefin const reload = useCallback(async () => { await reloadConfig(); setGithubProvisioningStatus(await fetchIsGithubProvisioningEnabled()); - }, [reloadConfig]); + onReload(); + }, [reloadConfig, onReload]); return { ...config, diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useSamlConfiguration.ts b/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useSamlConfiguration.ts index 7c06147aacf..034c8ee5cfa 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useSamlConfiguration.ts +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useSamlConfiguration.ts @@ -39,20 +39,15 @@ const OPTIONAL_FIELDS = [ SAML_SCIM_DEPRECATED, ]; -export default function useSamlConfiguration(definitions: ExtendedSettingDefinition[]) { +export default function useSamlConfiguration( + definitions: ExtendedSettingDefinition[], + onReload: () => void +) { const [scimStatus, setScimStatus] = React.useState(false); const [newScimStatus, setNewScimStatus] = React.useState(); const hasScim = React.useContext(AvailableFeaturesContext).includes(Feature.Scim); - const { - loading, - reload: reloadConfig, - values, - setNewValue, - canBeSave, - hasConfiguration, - deleteConfiguration, - isValueChange, - } = useConfiguration(definitions, OPTIONAL_FIELDS); + const config = useConfiguration(definitions, OPTIONAL_FIELDS); + const { reload: reloadConfig, values, setNewValue, isValueChange } = config; React.useEffect(() => { (async () => { @@ -77,18 +72,17 @@ export default function useSamlConfiguration(definitions: ExtendedSettingDefinit const reload = React.useCallback(async () => { await reloadConfig(); setScimStatus(await fetchIsScimEnabled()); - }, [reloadConfig]); + onReload(); + }, [reloadConfig, onReload]); return { + ...config, hasScim, scimStatus, - loading, samlEnabled, name, url, groupValue, - hasConfiguration, - canBeSave, values, setNewValue, reload, @@ -96,6 +90,5 @@ export default function useSamlConfiguration(definitions: ExtendedSettingDefinit newScimStatus, setNewScimStatus, setNewGroupSetting, - deleteConfiguration, }; } diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index cf2f7c9fd62..2aa64b28d0c 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -1331,6 +1331,7 @@ settings.authentication.form.enable=Enable configuration settings.authentication.form.disable=Disable configuration settings.authentication.form.provisioning=Provisioning settings.authentication.form.provisioning_at_login=Just-in-Time user and group provisioning (default) +settings.authentication.form.other_provisioning_enabled=Only one provider can have automatic user and group provisioning. # GITHUB settings.authentication.form.create.github=New Github configuration @@ -1365,6 +1366,7 @@ settings.authentication.saml.form.test.help.dirty=You must save your changes settings.authentication.saml.form.test.help.incomplete=Some mandatory fields are empty settings.authentication.saml.form.save_success=Saved successfully settings.authentication.saml.form.save_partial=Saved partially +settings.authentication.saml.form.provisioning_at_login=Use this option if your identity provider does not support the SCIM protocol. settings.authentication.saml.form.provisioning_at_login.sub=Use this option if your identity provider does not support the SCIM protocol. settings.authentication.saml.form.provisioning_with_scim=Automatic user and group provisioning with SCIM settings.authentication.saml.form.provisioning_with_scim.sub=Preferred option when using a supported identity provider. -- 2.39.5