]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19084 Disable Provisioning when another provisioning is enabled
authorguillaume-peoch-sonarsource <guillaume.peoch@sonarsource.com>
Wed, 3 May 2023 09:46:18 +0000 (11:46 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 11 May 2023 20:03:14 +0000 (20:03 +0000)
server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAutheticationTab.tsx [deleted file]
server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useGithubConfiguration.ts
server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useSamlConfiguration.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index a10aa8293f26d030f20aad94c73bce4b7c6aa4af..96d3160caef68e4a65fd46584e38e2e7796e39b8 100644 (file)
  */
 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<string>();
+
+  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 && (
                     <SamlAuthenticationTab
                       definitions={definitions.filter((def) => def.subCategory === SAML)}
+                      provider={provider}
+                      onReload={() => loadProvider()}
                     />
                   )}
 
                   {tab.key === AlmKeys.GitHub && (
-                    <GithubAithentication
+                    <GithubAuthenticationTab
                       definitions={definitions.filter((def) => def.subCategory === AlmKeys.GitHub)}
+                      provider={provider}
+                      onReload={() => loadProvider()}
                     />
                   )}
 
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx
new file mode 100644 (file)
index 0000000..b1092b9
--- /dev/null
@@ -0,0 +1,344 @@
+/*
+ * 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 { isEmpty } from 'lodash';
+import React, { useState } from 'react';
+import { FormattedMessage } from 'react-intl';
+import {
+  activateGithubProvisioning,
+  deactivateGithubProvisioning,
+  resetSettingValue,
+  setSettingValue,
+} from '../../../../api/settings';
+import DocLink from '../../../../components/common/DocLink';
+import ConfirmModal from '../../../../components/controls/ConfirmModal';
+import RadioCard from '../../../../components/controls/RadioCard';
+import { Button, ResetButtonLink, SubmitButton } from '../../../../components/controls/buttons';
+import CheckIcon from '../../../../components/icons/CheckIcon';
+import DeleteIcon from '../../../../components/icons/DeleteIcon';
+import EditIcon from '../../../../components/icons/EditIcon';
+import { Alert } from '../../../../components/ui/Alert';
+import { translate } from '../../../../helpers/l10n';
+import { AlmKeys } from '../../../../types/alm-settings';
+import { ExtendedSettingDefinition } from '../../../../types/settings';
+import { DOCUMENTATION_LINK_SUFFIXES } from './Authentication';
+import AuthenticationFormField from './AuthenticationFormField';
+import ConfigurationForm from './ConfigurationForm';
+import useGithubConfiguration, {
+  GITHUB_ENABLED_FIELD,
+  GITHUB_JIT_FIELDS,
+} from './hook/useGithubConfiguration';
+
+interface GithubAuthenticationProps {
+  definitions: ExtendedSettingDefinition[];
+  provider: string | undefined;
+  onReload: () => void;
+}
+
+const GITHUB_EXCLUDED_FIELD = [
+  'sonar.auth.github.enabled',
+  'sonar.auth.github.groupsSync',
+  'sonar.auth.github.allowUsersToSignUp',
+  'sonar.auth.github.organizations',
+];
+
+export default function GithubAuthenticationTab(props: GithubAuthenticationProps) {
+  const { definitions, provider } = props;
+  const [showEditModal, setShowEditModal] = useState(false);
+  const [showConfirmProvisioningModal, setShowConfirmProvisioningModal] = useState(false);
+
+  const {
+    hasConfiguration,
+    hasGithubProvisioning,
+    githubProvisioningStatus,
+    loading,
+    values,
+    setNewValue,
+    canBeSave,
+    reload,
+    url,
+    appId,
+    enabled,
+    deleteConfiguration,
+    newGithubProvisioningStatus,
+    setNewGithubProvisioningStatus,
+    hasGithubProvisioningConfigChange,
+    resetJitSetting,
+  } = useGithubConfiguration(definitions, props.onReload);
+
+  const hasDifferentProvider = provider !== undefined && provider !== 'github';
+
+  const handleCreateConfiguration = () => {
+    setShowEditModal(true);
+  };
+
+  const handleCancelConfiguration = () => {
+    setShowEditModal(false);
+  };
+
+  const handleConfirmChangeProvisioning = async () => {
+    if (newGithubProvisioningStatus && newGithubProvisioningStatus !== githubProvisioningStatus) {
+      await activateGithubProvisioning();
+      await reload();
+    } else {
+      if (newGithubProvisioningStatus !== githubProvisioningStatus) {
+        await deactivateGithubProvisioning();
+      }
+      await handleSaveGroup();
+    }
+  };
+
+  const handleSaveGroup = async () => {
+    await Promise.all(
+      GITHUB_JIT_FIELDS.map(async (settingKey) => {
+        const value = values[settingKey];
+        if (value.newValue !== undefined) {
+          if (isEmpty(value.newValue) && typeof value.newValue !== 'boolean') {
+            await resetSettingValue({ keys: value.definition.key });
+          } else {
+            await setSettingValue(value.definition, value.newValue);
+          }
+        }
+      })
+    );
+    await reload();
+  };
+
+  const handleToggleEnable = async () => {
+    const value = values[GITHUB_ENABLED_FIELD];
+    await setSettingValue(value.definition, !enabled);
+    await reload();
+  };
+
+  return (
+    <div className="authentication-configuration">
+      <div className="spacer-bottom display-flex-space-between display-flex-center">
+        <h4>{translate('settings.authentication.github.configuration')}</h4>
+
+        {!hasConfiguration && (
+          <div>
+            <Button onClick={handleCreateConfiguration}>
+              {translate('settings.authentication.form.create')}
+            </Button>
+          </div>
+        )}
+      </div>
+      {!hasConfiguration ? (
+        <div className="big-padded text-center huge-spacer-bottom authentication-no-config">
+          {translate('settings.authentication.github.form.not_configured')}
+        </div>
+      ) : (
+        <>
+          <div className="spacer-bottom big-padded bordered display-flex-space-between">
+            <div>
+              <h5>{appId}</h5>
+              <p>{url}</p>
+              <p className="big-spacer-top big-spacer-bottom">
+                {enabled ? (
+                  <span className="authentication-enabled spacer-left">
+                    <CheckIcon className="spacer-right" />
+                    {translate('settings.authentication.form.enabled')}
+                  </span>
+                ) : (
+                  translate('settings.authentication.form.not_enabled')
+                )}
+              </p>
+              <Button className="spacer-top" onClick={handleToggleEnable}>
+                {enabled
+                  ? translate('settings.authentication.form.disable')
+                  : translate('settings.authentication.form.enable')}
+              </Button>
+            </div>
+            <div>
+              <Button className="spacer-right" onClick={handleCreateConfiguration}>
+                <EditIcon />
+                {translate('settings.authentication.form.edit')}
+              </Button>
+              <Button className="button-red" disabled={enabled} onClick={deleteConfiguration}>
+                <DeleteIcon />
+                {translate('settings.authentication.form.delete')}
+              </Button>
+            </div>
+          </div>
+          <div className="spacer-bottom big-padded bordered display-flex-space-between">
+            <form
+              onSubmit={async (e) => {
+                e.preventDefault();
+                if (newGithubProvisioningStatus !== githubProvisioningStatus) {
+                  setShowConfirmProvisioningModal(true);
+                } else {
+                  await handleSaveGroup();
+                }
+              }}
+            >
+              <fieldset className="display-flex-column big-spacer-bottom">
+                <label className="h5">
+                  {translate('settings.authentication.form.provisioning')}
+                </label>
+
+                {enabled ? (
+                  <div className="display-flex-row spacer-top">
+                    <RadioCard
+                      label={translate(
+                        'settings.authentication.github.form.provisioning_with_github'
+                      )}
+                      title={translate(
+                        'settings.authentication.github.form.provisioning_with_github'
+                      )}
+                      selected={newGithubProvisioningStatus ?? githubProvisioningStatus}
+                      onClick={() => setNewGithubProvisioningStatus(true)}
+                      disabled={!hasGithubProvisioning || hasDifferentProvider}
+                    >
+                      {hasGithubProvisioning ? (
+                        <>
+                          {hasDifferentProvider && (
+                            <p className="spacer-bottom text-bold">
+                              {translate('settings.authentication.form.other_provisioning_enabled')}
+                            </p>
+                          )}
+                          <p className="spacer-bottom">
+                            {translate(
+                              'settings.authentication.github.form.provisioning_with_github.description'
+                            )}
+                          </p>
+                          <p>
+                            <FormattedMessage
+                              id="settings.authentication.github.form.provisioning_with_github.description.doc"
+                              defaultMessage={translate(
+                                'settings.authentication.github.form.provisioning_with_github.description.doc'
+                              )}
+                              values={{
+                                documentation: (
+                                  <DocLink
+                                    to={`/instance-administration/authentication/${
+                                      DOCUMENTATION_LINK_SUFFIXES[AlmKeys.GitHub]
+                                    }/`}
+                                  >
+                                    {translate('documentation')}
+                                  </DocLink>
+                                ),
+                              }}
+                            />
+                          </p>
+                        </>
+                      ) : (
+                        <p>
+                          <FormattedMessage
+                            id="settings.authentication.github.form.provisioning.disabled"
+                            defaultMessage={translate(
+                              'settings.authentication.github.form.provisioning.disabled'
+                            )}
+                            values={{
+                              documentation: (
+                                // Documentation page not ready yet.
+                                <DocLink to="/instance-administration/authentication/github">
+                                  {translate('documentation')}
+                                </DocLink>
+                              ),
+                            }}
+                          />
+                        </p>
+                      )}
+                    </RadioCard>
+                    <RadioCard
+                      label={translate('settings.authentication.form.provisioning_at_login')}
+                      title={translate('settings.authentication.form.provisioning_at_login')}
+                      selected={!(newGithubProvisioningStatus ?? githubProvisioningStatus)}
+                      onClick={() => setNewGithubProvisioningStatus(false)}
+                    >
+                      {Object.values(values).map((val) => {
+                        if (!GITHUB_JIT_FIELDS.includes(val.key)) {
+                          return null;
+                        }
+                        return (
+                          <div key={val.key}>
+                            <AuthenticationFormField
+                              settingValue={values[val.key]?.newValue ?? values[val.key]?.value}
+                              definition={val.definition}
+                              mandatory={val.mandatory}
+                              onFieldChange={setNewValue}
+                              isNotSet={val.isNotSet}
+                            />
+                          </div>
+                        );
+                      })}
+                    </RadioCard>
+                  </div>
+                ) : (
+                  <Alert className="big-spacer-top" variant="info">
+                    {translate('settings.authentication.github.enable_first')}
+                  </Alert>
+                )}
+              </fieldset>
+              {enabled && (
+                <>
+                  <SubmitButton disabled={!hasGithubProvisioningConfigChange}>
+                    {translate('save')}
+                  </SubmitButton>
+                  <ResetButtonLink
+                    className="spacer-left"
+                    onClick={() => {
+                      setNewGithubProvisioningStatus(undefined);
+                      resetJitSetting();
+                    }}
+                    disabled={!hasGithubProvisioningConfigChange}
+                  >
+                    {translate('cancel')}
+                  </ResetButtonLink>
+                </>
+              )}
+              {showConfirmProvisioningModal && (
+                <ConfirmModal
+                  onConfirm={() => handleConfirmChangeProvisioning()}
+                  header={translate(
+                    'settings.authentication.github.confirm',
+                    newGithubProvisioningStatus ? 'auto' : 'jit'
+                  )}
+                  onClose={() => setShowConfirmProvisioningModal(false)}
+                  isDestructive={!newGithubProvisioningStatus}
+                  confirmButtonText={translate('yes')}
+                >
+                  {translate(
+                    'settings.authentication.github.confirm',
+                    newGithubProvisioningStatus ? 'auto' : 'jit',
+                    'description'
+                  )}
+                </ConfirmModal>
+              )}
+            </form>
+          </div>
+        </>
+      )}
+
+      {showEditModal && (
+        <ConfigurationForm
+          tab={AlmKeys.GitHub}
+          excludedField={GITHUB_EXCLUDED_FIELD}
+          loading={loading}
+          values={values}
+          setNewValue={setNewValue}
+          canBeSave={canBeSave}
+          onClose={handleCancelConfiguration}
+          create={!hasConfiguration}
+          onReload={reload}
+        />
+      )}
+    </div>
+  );
+}
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/GithubAutheticationTab.tsx
deleted file mode 100644 (file)
index da6fb40..0000000
+++ /dev/null
@@ -1,335 +0,0 @@
-/*
- * 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 { isEmpty } from 'lodash';
-import React, { useState } from 'react';
-import { FormattedMessage } from 'react-intl';
-import {
-  activateGithubProvisioning,
-  deactivateGithubProvisioning,
-  resetSettingValue,
-  setSettingValue,
-} from '../../../../api/settings';
-import DocLink from '../../../../components/common/DocLink';
-import ConfirmModal from '../../../../components/controls/ConfirmModal';
-import RadioCard from '../../../../components/controls/RadioCard';
-import { Button, ResetButtonLink, SubmitButton } from '../../../../components/controls/buttons';
-import CheckIcon from '../../../../components/icons/CheckIcon';
-import DeleteIcon from '../../../../components/icons/DeleteIcon';
-import EditIcon from '../../../../components/icons/EditIcon';
-import { Alert } from '../../../../components/ui/Alert';
-import { translate } from '../../../../helpers/l10n';
-import { AlmKeys } from '../../../../types/alm-settings';
-import { ExtendedSettingDefinition } from '../../../../types/settings';
-import { DOCUMENTATION_LINK_SUFFIXES } from './Authentication';
-import AuthenticationFormField from './AuthenticationFormField';
-import ConfigurationForm from './ConfigurationForm';
-import useGithubConfiguration, {
-  GITHUB_ENABLED_FIELD,
-  GITHUB_JIT_FIELDS,
-} from './hook/useGithubConfiguration';
-
-interface GithubAuthenticationProps {
-  definitions: ExtendedSettingDefinition[];
-}
-
-const GITHUB_EXCLUDED_FIELD = [
-  'sonar.auth.github.enabled',
-  'sonar.auth.github.groupsSync',
-  'sonar.auth.github.allowUsersToSignUp',
-  'sonar.auth.github.organizations',
-];
-
-export default function GithubAithentication(props: GithubAuthenticationProps) {
-  const [showEditModal, setShowEditModal] = useState(false);
-  const [showConfirmProvisioningModal, setShowConfirmProvisioningModal] = useState(false);
-
-  const {
-    hasConfiguration,
-    hasGithubProvisioning,
-    githubProvisioningStatus,
-    loading,
-    values,
-    setNewValue,
-    canBeSave,
-    reload,
-    url,
-    appId,
-    enabled,
-    deleteConfiguration,
-    newGithubProvisioningStatus,
-    setNewGithubProvisioningStatus,
-    hasGithubProvisioningConfigChange,
-    resetJitSetting,
-  } = useGithubConfiguration(props.definitions);
-
-  const handleCreateConfiguration = () => {
-    setShowEditModal(true);
-  };
-
-  const handleCancelConfiguration = () => {
-    setShowEditModal(false);
-  };
-
-  const handleConfirmChangeProvisioning = async () => {
-    if (newGithubProvisioningStatus && newGithubProvisioningStatus !== githubProvisioningStatus) {
-      await activateGithubProvisioning();
-      await reload();
-    } else {
-      if (newGithubProvisioningStatus !== githubProvisioningStatus) {
-        await deactivateGithubProvisioning();
-      }
-      await handleSaveGroup();
-    }
-  };
-
-  const handleSaveGroup = async () => {
-    await Promise.all(
-      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 {
-            await setSettingValue(value.definition, value.newValue);
-          }
-        }
-      })
-    );
-    await reload();
-  };
-
-  const handleToggleEnable = async () => {
-    const value = values[GITHUB_ENABLED_FIELD];
-    await setSettingValue(value.definition, !enabled);
-    await reload();
-  };
-
-  return (
-    <div className="authentication-configuration">
-      <div className="spacer-bottom display-flex-space-between display-flex-center">
-        <h4>{translate('settings.authentication.github.configuration')}</h4>
-
-        {!hasConfiguration && (
-          <div>
-            <Button onClick={handleCreateConfiguration}>
-              {translate('settings.authentication.form.create')}
-            </Button>
-          </div>
-        )}
-      </div>
-      {!hasConfiguration ? (
-        <div className="big-padded text-center huge-spacer-bottom authentication-no-config">
-          {translate('settings.authentication.github.form.not_configured')}
-        </div>
-      ) : (
-        <>
-          <div className="spacer-bottom big-padded bordered display-flex-space-between">
-            <div>
-              <h5>{appId}</h5>
-              <p>{url}</p>
-              <p className="big-spacer-top big-spacer-bottom">
-                {enabled ? (
-                  <span className="authentication-enabled spacer-left">
-                    <CheckIcon className="spacer-right" />
-                    {translate('settings.authentication.form.enabled')}
-                  </span>
-                ) : (
-                  translate('settings.authentication.form.not_enabled')
-                )}
-              </p>
-              <Button className="spacer-top" onClick={handleToggleEnable}>
-                {enabled
-                  ? translate('settings.authentication.form.disable')
-                  : translate('settings.authentication.form.enable')}
-              </Button>
-            </div>
-            <div>
-              <Button className="spacer-right" onClick={handleCreateConfiguration}>
-                <EditIcon />
-                {translate('settings.authentication.form.edit')}
-              </Button>
-              <Button className="button-red" disabled={enabled} onClick={deleteConfiguration}>
-                <DeleteIcon />
-                {translate('settings.authentication.form.delete')}
-              </Button>
-            </div>
-          </div>
-          <div className="spacer-bottom big-padded bordered display-flex-space-between">
-            <form
-              onSubmit={async (e) => {
-                e.preventDefault();
-                if (newGithubProvisioningStatus !== githubProvisioningStatus) {
-                  setShowConfirmProvisioningModal(true);
-                } else {
-                  await handleSaveGroup();
-                }
-              }}
-            >
-              <fieldset className="display-flex-column big-spacer-bottom">
-                <label className="h5">
-                  {translate('settings.authentication.form.provisioning')}
-                </label>
-
-                {enabled ? (
-                  <div className="display-flex-row spacer-top">
-                    <RadioCard
-                      label={translate(
-                        'settings.authentication.github.form.provisioning_with_github'
-                      )}
-                      title={translate(
-                        'settings.authentication.github.form.provisioning_with_github'
-                      )}
-                      selected={newGithubProvisioningStatus ?? githubProvisioningStatus}
-                      onClick={() => setNewGithubProvisioningStatus(true)}
-                      disabled={!hasGithubProvisioning}
-                    >
-                      {hasGithubProvisioning ? (
-                        <>
-                          <p className="spacer-bottom">
-                            {translate(
-                              'settings.authentication.github.form.provisioning_with_github.description'
-                            )}
-                          </p>
-                          <p>
-                            <FormattedMessage
-                              id="settings.authentication.github.form.provisioning_with_github.description.doc"
-                              defaultMessage={translate(
-                                'settings.authentication.github.form.provisioning_with_github.description.doc'
-                              )}
-                              values={{
-                                documentation: (
-                                  <DocLink
-                                    to={`/instance-administration/authentication/${
-                                      DOCUMENTATION_LINK_SUFFIXES[AlmKeys.GitHub]
-                                    }/`}
-                                  >
-                                    {translate('documentation')}
-                                  </DocLink>
-                                ),
-                              }}
-                            />
-                          </p>
-                        </>
-                      ) : (
-                        <p>
-                          <FormattedMessage
-                            id="settings.authentication.github.form.provisioning.disabled"
-                            defaultMessage={translate(
-                              'settings.authentication.github.form.provisioning.disabled'
-                            )}
-                            values={{
-                              documentation: (
-                                // Documentation page not ready yet.
-                                <DocLink to="/instance-administration/authentication/github">
-                                  {translate('documentation')}
-                                </DocLink>
-                              ),
-                            }}
-                          />
-                        </p>
-                      )}
-                    </RadioCard>
-                    <RadioCard
-                      label={translate('settings.authentication.form.provisioning_at_login')}
-                      title={translate('settings.authentication.form.provisioning_at_login')}
-                      selected={!(newGithubProvisioningStatus ?? githubProvisioningStatus)}
-                      onClick={() => setNewGithubProvisioningStatus(false)}
-                    >
-                      {Object.values(values).map((val) => {
-                        if (!GITHUB_JIT_FIELDS.includes(val.key)) {
-                          return null;
-                        }
-                        return (
-                          <div key={val.key}>
-                            <AuthenticationFormField
-                              settingValue={values[val.key]?.newValue ?? values[val.key]?.value}
-                              definition={val.definition}
-                              mandatory={val.mandatory}
-                              onFieldChange={setNewValue}
-                              isNotSet={val.isNotSet}
-                            />
-                          </div>
-                        );
-                      })}
-                    </RadioCard>
-                  </div>
-                ) : (
-                  <Alert className="big-spacer-top" variant="info">
-                    {translate('settings.authentication.github.enable_first')}
-                  </Alert>
-                )}
-              </fieldset>
-              {enabled && (
-                <>
-                  <SubmitButton disabled={!hasGithubProvisioningConfigChange}>
-                    {translate('save')}
-                  </SubmitButton>
-                  <ResetButtonLink
-                    className="spacer-left"
-                    onClick={() => {
-                      setNewGithubProvisioningStatus(undefined);
-                      resetJitSetting();
-                    }}
-                    disabled={!hasGithubProvisioningConfigChange}
-                  >
-                    {translate('cancel')}
-                  </ResetButtonLink>
-                </>
-              )}
-              {showConfirmProvisioningModal && (
-                <ConfirmModal
-                  onConfirm={() => handleConfirmChangeProvisioning()}
-                  header={translate(
-                    'settings.authentication.github.confirm',
-                    newGithubProvisioningStatus ? 'auto' : 'jit'
-                  )}
-                  onClose={() => setShowConfirmProvisioningModal(false)}
-                  isDestructive={!newGithubProvisioningStatus}
-                  confirmButtonText={translate('yes')}
-                >
-                  {translate(
-                    'settings.authentication.github.confirm',
-                    newGithubProvisioningStatus ? 'auto' : 'jit',
-                    'description'
-                  )}
-                </ConfirmModal>
-              )}
-            </form>
-          </div>
-        </>
-      )}
-
-      {showEditModal && (
-        <ConfigurationForm
-          tab={AlmKeys.GitHub}
-          excludedField={GITHUB_EXCLUDED_FIELD}
-          loading={loading}
-          values={values}
-          setNewValue={setNewValue}
-          canBeSave={canBeSave}
-          onClose={handleCancelConfiguration}
-          create={!hasConfiguration}
-          onReload={reload}
-        />
-      )}
-    </div>
-  );
-}
index d9bc32db7fab7fc8a8dd7c2cf0d27c0e8e15937c..d376ecc3f59249f4edd59019210cc55475999951 100644 (file)
@@ -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) {
             >
               <fieldset className="display-flex-column big-spacer-bottom">
                 <label className="h5">
-                  {translate('settings.authentication.saml.form.provisioning')}
+                  {translate('settings.authentication.form.provisioning')}
                 </label>
                 {samlEnabled ? (
                   <div className="display-flex-row spacer-top">
@@ -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 ? (
                         <p>
@@ -216,6 +220,11 @@ export default function SamlAuthenticationTab(props: SamlAuthenticationProps) {
                         </p>
                       ) : (
                         <>
+                          {hasDifferentProvider && (
+                            <p className="spacer-bottom text-bold">
+                              {translate('settings.authentication.form.other_provisioning_enabled')}
+                            </p>
+                          )}
                           <p className="spacer-bottom">
                             {translate(
                               'settings.authentication.saml.form.provisioning_with_scim.sub'
index d3a55581d5f1aabd339091ddbee9940f6fcd1866..dfb6e21e5b5d15fdf85bbd63d458e49a7cd3c7ca 100644 (file)
@@ -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();
 
index 65d932da6ec7a2cc8575ad03aa12792e147bba8a..a8eaa7eb5331eabe1cf6ec6acd430172218b1ce2 100644 (file)
@@ -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,
index 7c06147aacf7a94fb8796c48a4e49e7c8ca8aded..034c8ee5cfa37f240925a595dba0ba821fda3b3e 100644 (file)
@@ -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<boolean>(false);
   const [newScimStatus, setNewScimStatus] = React.useState<boolean>();
   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,
   };
 }
index cf2f7c9fd628fc7a17e45655366760b0134f6cc6..2aa64b28d0c63f17e562699005bc6cff7b6af2b0 100644 (file)
@@ -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.