]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19084 GitHub Provisioning in Authentication settings
authorguillaume-peoch-sonarsource <guillaume.peoch@sonarsource.com>
Wed, 26 Apr 2023 15:08:11 +0000 (17:08 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 11 May 2023 20:03:14 +0000 (20:03 +0000)
16 files changed:
server/sonar-web/src/main/js/api/mocks/AuthenticationServiceMock.ts
server/sonar-web/src/main/js/api/settings.ts
server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationFormField.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationMultiValuesField.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationToggleField.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/ConfigurationForm.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAutheticationTab.tsx
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/useConfiguration.ts
server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useGithubConfiguration.ts
server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useLoadSamlSettings.ts [deleted file]
server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useSamlConfiguration.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/styles.css
server/sonar-web/src/main/js/types/features.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index f4986f2d69bed82449ea52136f03a265cb0a138f..a248742588c41bc574f089568272a08878fd7ae3 100644 (file)
@@ -22,8 +22,11 @@ import { mockSettingValue } from '../../helpers/mocks/settings';
 import { BranchParameters } from '../../types/branch-like';
 import { SettingDefinition, SettingValue } from '../../types/settings';
 import {
+  activateGithubProvisioning,
   activateScim,
+  deactivateGithubProvisioning,
   deactivateScim,
+  fetchIsGithubProvisioningEnabled,
   fetchIsScimEnabled,
   getValues,
   resetSettingValue,
@@ -33,6 +36,7 @@ import {
 export default class AuthenticationServiceMock {
   settingValues: SettingValue[];
   scimStatus: boolean;
+  githubProvisioningStatus: boolean;
   defaulSettingValues: SettingValue[] = [
     mockSettingValue({ key: 'test1', value: '' }),
     mockSettingValue({ key: 'test2', value: 'test2' }),
@@ -61,13 +65,22 @@ export default class AuthenticationServiceMock {
   constructor() {
     this.settingValues = cloneDeep(this.defaulSettingValues);
     this.scimStatus = false;
+    this.githubProvisioningStatus = false;
     jest.mocked(getValues).mockImplementation(this.handleGetValues);
     jest.mocked(setSettingValue).mockImplementation(this.handleSetValue);
     jest.mocked(resetSettingValue).mockImplementation(this.handleResetValue);
     jest.mocked(activateScim).mockImplementation(this.handleActivateScim);
     jest.mocked(deactivateScim).mockImplementation(this.handleDeactivateScim);
-
     jest.mocked(fetchIsScimEnabled).mockImplementation(this.handleFetchIsScimEnabled);
+    jest
+      .mocked(activateGithubProvisioning)
+      .mockImplementation(this.handleActivateGithubProvisioning);
+    jest
+      .mocked(deactivateGithubProvisioning)
+      .mockImplementation(this.handleDeactivateGithubProvisioning);
+    jest
+      .mocked(fetchIsGithubProvisioningEnabled)
+      .mockImplementation(this.handleFetchIsGithubProvisioningEnabled);
   }
 
   handleActivateScim = () => {
@@ -84,6 +97,20 @@ export default class AuthenticationServiceMock {
     return Promise.resolve(this.scimStatus);
   };
 
+  handleActivateGithubProvisioning = () => {
+    this.githubProvisioningStatus = true;
+    return Promise.resolve();
+  };
+
+  handleDeactivateGithubProvisioning = () => {
+    this.githubProvisioningStatus = false;
+    return Promise.resolve();
+  };
+
+  handleFetchIsGithubProvisioningEnabled = () => {
+    return Promise.resolve(this.githubProvisioningStatus);
+  };
+
   handleGetValues = (
     data: { keys: string[]; component?: string } & BranchParameters
   ): Promise<SettingValue[]> => {
index d741cb0bc005db1608f04e39f84860416b471f5f..8e17b5e6894056a907c586929d1f348d291ab723 100644 (file)
@@ -130,3 +130,17 @@ export function activateScim(): Promise<void> {
 export function deactivateScim(): Promise<void> {
   return post('/api/scim_management/disable').catch(throwGlobalError);
 }
+
+export function fetchIsGithubProvisioningEnabled(): Promise<boolean> {
+  return getJSON('/api/github_provisioning/status')
+    .then((r) => r.enabled)
+    .catch(throwGlobalError);
+}
+
+export function activateGithubProvisioning(): Promise<void> {
+  return post('/api/github_provisioning/enable').catch(throwGlobalError);
+}
+
+export function deactivateGithubProvisioning(): Promise<void> {
+  return post('/api/github_provisioning/disable').catch(throwGlobalError);
+}
index 8570658495121c8171ca1a1fed5168641088b9c6..f4197d89ebc0ee4edecf18513db23e0fa74a39e1 100644 (file)
@@ -24,14 +24,15 @@ import ValidationInput, {
 import MandatoryFieldMarker from '../../../../components/ui/MandatoryFieldMarker';
 import { ExtendedSettingDefinition, SettingType } from '../../../../types/settings';
 import { isSecuredDefinition } from '../../utils';
+import AuthenticationMultiValueField from './AuthenticationMultiValuesField';
 import AuthenticationSecuredField from './AuthenticationSecuredField';
 import AuthenticationToggleField from './AuthenticationToggleField';
 
 interface SamlToggleFieldProps {
-  settingValue?: string | boolean;
+  settingValue?: string | boolean | string[];
   definition: ExtendedSettingDefinition;
   mandatory?: boolean;
-  onFieldChange: (key: string, value: string | boolean) => void;
+  onFieldChange: (key: string, value: string | boolean | string[]) => void;
   isNotSet: boolean;
   error?: string;
 }
@@ -51,6 +52,13 @@ export default function AuthenticationFormField(props: SamlToggleFieldProps) {
         )}
       </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)}
+          />
+        )}
         {isSecuredDefinition(definition) && (
           <AuthenticationSecuredField
             definition={definition}
@@ -62,28 +70,30 @@ export default function AuthenticationFormField(props: SamlToggleFieldProps) {
         {!isSecuredDefinition(definition) && definition.type === SettingType.BOOLEAN && (
           <AuthenticationToggleField
             definition={definition}
-            settingValue={settingValue}
+            settingValue={settingValue as string | boolean}
             onChange={(value) => props.onFieldChange(definition.key, value)}
           />
         )}
-        {!isSecuredDefinition(definition) && definition.type === undefined && (
-          <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) &&
+          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>
   );
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationMultiValuesField.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationMultiValuesField.tsx
new file mode 100644 (file)
index 0000000..1dfd21f
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * 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 * as React from 'react';
+import { DeleteButton } from '../../../../components/controls/buttons';
+import { translateWithParameters } from '../../../../helpers/l10n';
+import { ExtendedSettingDefinition } from '../../../../types/settings';
+import { getPropertyName } from '../../utils';
+
+interface Props {
+  onFieldChange: (value: string[]) => void;
+  settingValue?: string[];
+  definition: ExtendedSettingDefinition;
+}
+
+export default function AuthenticationMultiValueField(props: Props) {
+  const { settingValue = [], definition } = props;
+
+  const displayValue = [...settingValue, ''];
+
+  const handleSingleInputChange = (index: number, value: string) => {
+    const newValue = [...settingValue];
+    newValue.splice(index, 1, value);
+    props.onFieldChange(newValue);
+  };
+
+  const handleDeleteValue = (index: number) => {
+    const newValue = [...settingValue];
+    newValue.splice(index, 1);
+    props.onFieldChange(newValue);
+  };
+
+  return (
+    <div>
+      <ul>
+        {displayValue.map((value, index) => {
+          const isNotLast = index !== displayValue.length - 1;
+          return (
+            <li className="spacer-bottom" key={index}>
+              <input
+                className="width-80"
+                id={definition.key}
+                maxLength={4000}
+                name={definition.key}
+                onChange={(e) => handleSingleInputChange(index, e.currentTarget.value)}
+                type="text"
+                value={displayValue[index]}
+              />
+
+              {isNotLast && (
+                <div className="display-inline-block spacer-left">
+                  <DeleteButton
+                    className="js-remove-value"
+                    aria-label={translateWithParameters(
+                      'settings.definition.delete_value',
+                      getPropertyName(definition),
+                      value
+                    )}
+                    onClick={() => handleDeleteValue(index)}
+                  />
+                </div>
+              )}
+            </li>
+          );
+        })}
+      </ul>
+    </div>
+  );
+}
index 40b71d67ec9e130a7a39f259aa138debf3cf52bd..cb82310df920f57b7543cda155892b77b533e723 100644 (file)
@@ -30,5 +30,12 @@ interface SamlToggleFieldProps {
 export default function AuthenticationToggleField(props: SamlToggleFieldProps) {
   const { settingValue, definition } = props;
 
-  return <Toggle name={definition.key} onChange={props.onChange} value={settingValue ?? ''} />;
+  return (
+    <Toggle
+      ariaLabel={definition.key}
+      name={definition.key}
+      onChange={props.onChange}
+      value={settingValue ?? ''}
+    />
+  );
 }
index 5920471a2294dd609262f6f0237080bd4e1af5c0..8f6406b86720d7645fa728a24e3fb183cf0bb8bf 100644 (file)
@@ -88,7 +88,13 @@ export default function ConfigurationForm(props: Props) {
   };
 
   return (
-    <Modal contentLabel={headerLabel} shouldCloseOnOverlayClick={false} size="medium">
+    <Modal
+      contentLabel={headerLabel}
+      onRequestClose={props.onClose}
+      shouldCloseOnOverlayClick={false}
+      shouldCloseOnEsc={true}
+      size="medium"
+    >
       <form onSubmit={handleSubmit}>
         <div className="modal-head">
           <h2>{headerLabel}</h2>
index 837ef38530e6ec714aefc398945413a47381d2c0..70b7e03a6768be24686732bb66792da7d2185f86 100644 (file)
  * 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 from 'react';
-import { setSettingValue } from '../../../../api/settings';
-import { Button } from '../../../../components/controls/buttons';
+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 } from './hook/useGithubConfiguration';
+import useGithubConfiguration, {
+  GITHUB_ENABLED_FIELD,
+  GITHUB_JIT_FIELDS,
+} from './hook/useGithubConfiguration';
 
-interface SamlAuthenticationProps {
+interface GithubAuthenticationProps {
   definitions: ExtendedSettingDefinition[];
 }
 
@@ -37,12 +53,17 @@ 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: SamlAuthenticationProps) {
-  const [showEditModal, setShowEditModal] = React.useState(false);
+export default function GithubAithentication(props: GithubAuthenticationProps) {
+  const [showEditModal, setShowEditModal] = useState(false);
+  const [showConfirmProvisioningModal, setShowConfirmProvisioningModal] = useState(false);
+
   const {
     hasConfiguration,
+    hasGithubProvisioning,
+    githubProvisioningStatus,
     loading,
     values,
     setNewValue,
@@ -52,6 +73,10 @@ export default function GithubAithentication(props: SamlAuthenticationProps) {
     appId,
     enabled,
     deleteConfiguration,
+    newGithubProvisioningStatus,
+    setNewGithubProvisioningStatus,
+    hasGithubProvisioningConfigChange,
+    resetJitSetting,
   } = useGithubConfiguration(props.definitions);
 
   const handleCreateConfiguration = () => {
@@ -62,6 +87,35 @@ export default function GithubAithentication(props: SamlAuthenticationProps) {
     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);
@@ -69,7 +123,7 @@ export default function GithubAithentication(props: SamlAuthenticationProps) {
   };
 
   return (
-    <div className="saml-configuration">
+    <div className="authentication-configuration">
       <div className="spacer-bottom display-flex-space-between display-flex-center">
         <h4>{translate('settings.authentication.github.configuration')}</h4>
 
@@ -82,7 +136,7 @@ export default function GithubAithentication(props: SamlAuthenticationProps) {
         )}
       </div>
       {!hasConfiguration ? (
-        <div className="big-padded text-center huge-spacer-bottom saml-no-config">
+        <div className="big-padded text-center huge-spacer-bottom authentication-no-config">
           {translate('settings.authentication.github.form.not_configured')}
         </div>
       ) : (
@@ -93,18 +147,18 @@ export default function GithubAithentication(props: SamlAuthenticationProps) {
               <p>{url}</p>
               <p className="big-spacer-top big-spacer-bottom">
                 {enabled ? (
-                  <span className="saml-enabled spacer-left">
+                  <span className="authentication-enabled spacer-left">
                     <CheckIcon className="spacer-right" />
-                    {translate('settings.authentication.saml.form.enabled')}
+                    {translate('settings.authentication.form.enabled')}
                   </span>
                 ) : (
-                  translate('settings.authentication.saml.form.not_enabled')
+                  translate('settings.authentication.form.not_enabled')
                 )}
               </p>
               <Button className="spacer-top" onClick={handleToggleEnable}>
                 {enabled
-                  ? translate('settings.authentication.saml.form.disable')
-                  : translate('settings.authentication.saml.form.enable')}
+                  ? translate('settings.authentication.form.disable')
+                  : translate('settings.authentication.form.enable')}
               </Button>
             </div>
             <div>
@@ -118,9 +172,128 @@ export default function GithubAithentication(props: SamlAuthenticationProps) {
               </Button>
             </div>
           </div>
-          <div className="spacer-bottom big-padded bordered display-flex-space-between">
-            Provisioning TODO
-          </div>
+          {hasGithubProvisioning && (
+            <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)}
+                      >
+                        <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>
+                      </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>
+          )}
         </>
       )}
 
index 43ab887e3492e76fa109ccc7851b802500c46020..d9bc32db7fab7fc8a8dd7c2cf0d27c0e8e15937c 100644 (file)
@@ -45,7 +45,7 @@ import useSamlConfiguration, {
   SAML_ENABLED_FIELD,
   SAML_GROUP_NAME,
   SAML_SCIM_DEPRECATED,
-} from './hook/useLoadSamlSettings';
+} from './hook/useSamlConfiguration';
 
 interface SamlAuthenticationProps {
   definitions: ExtendedSettingDefinition[];
@@ -116,7 +116,7 @@ export default function SamlAuthenticationTab(props: SamlAuthenticationProps) {
   };
 
   return (
-    <div className="saml-configuration">
+    <div className="authentication-configuration">
       <div className="spacer-bottom display-flex-space-between display-flex-center">
         <h4>{translate('settings.authentication.saml.configuration')}</h4>
 
@@ -129,7 +129,7 @@ export default function SamlAuthenticationTab(props: SamlAuthenticationProps) {
         )}
       </div>
       {!hasConfiguration && (
-        <div className="big-padded text-center huge-spacer-bottom saml-no-config">
+        <div className="big-padded text-center huge-spacer-bottom authentication-no-config">
           {translate('settings.authentication.saml.form.not_configured')}
         </div>
       )}
@@ -142,18 +142,18 @@ export default function SamlAuthenticationTab(props: SamlAuthenticationProps) {
               <p>{url}</p>
               <p className="big-spacer-top big-spacer-bottom">
                 {samlEnabled ? (
-                  <span className="saml-enabled spacer-left">
+                  <span className="authentication-enabled spacer-left">
                     <CheckIcon className="spacer-right" />
-                    {translate('settings.authentication.saml.form.enabled')}
+                    {translate('settings.authentication.form.enabled')}
                   </span>
                 ) : (
-                  translate('settings.authentication.saml.form.not_enabled')
+                  translate('settings.authentication.form.not_enabled')
                 )}
               </p>
               <Button className="spacer-top" disabled={scimStatus} onClick={handleToggleEnable}>
                 {samlEnabled
-                  ? translate('settings.authentication.saml.form.disable')
-                  : translate('settings.authentication.saml.form.enable')}
+                  ? translate('settings.authentication.form.disable')
+                  : translate('settings.authentication.form.enable')}
               </Button>
             </div>
             <div>
index 3b7b730add5aa38f9f99f0cd344decfae53324fa..dc5235e8cd50df0df535136024a558f315e49770 100644 (file)
@@ -59,12 +59,12 @@ const ui = {
     confirmProvisioningButton: byRole('button', { name: 'yes' }),
     saveScim: byRole('button', { name: 'save' }),
     groupAttribute: byRole('textbox', { name: 'property.sonar.auth.saml.group.name.name' }),
-    enableConfigButton: byRole('button', { name: 'settings.authentication.saml.form.enable' }),
-    disableConfigButton: byRole('button', { name: 'settings.authentication.saml.form.disable' }),
+    enableConfigButton: byRole('button', { name: 'settings.authentication.form.enable' }),
+    disableConfigButton: byRole('button', { name: 'settings.authentication.form.disable' }),
     editConfigButton: byRole('button', { name: 'settings.authentication.form.edit' }),
     enableFirstMessage: byText('settings.authentication.saml.enable_first'),
     jitProvisioningButton: byRole('radio', {
-      name: 'settings.authentication.saml.form.provisioning_at_login',
+      name: 'settings.authentication.form.provisioning_at_login',
     }),
     scimProvisioningButton: byRole('radio', {
       name: 'settings.authentication.saml.form.provisioning_with_scim',
@@ -92,6 +92,56 @@ const ui = {
       });
     },
   },
+  github: {
+    tab: byRole('tab', { name: 'github GitHub' }),
+    noGithubConfiguration: byText('settings.authentication.github.form.not_configured'),
+    createConfigButton: byRole('button', { name: 'settings.authentication.form.create' }),
+    clientId: byRole('textbox', { name: 'Client ID' }),
+    clientSecret: byRole('textbox', { name: 'Client Secret' }),
+    githubAppId: byRole('textbox', { name: 'GitHub App ID' }), // not working
+    privateKey: byRole('textarea', { name: 'Private Key' }), // not working
+    githubApiUrl: byRole('textbox', { name: 'The API url for a GitHub instance.' }),
+    githubWebUrl: byRole('textbox', { name: 'The WEB url for a GitHub instance.' }),
+    allowUserToSignUp: byRole('switch', {
+      name: 'sonar.auth.github.allowUsersToSignUp',
+    }),
+    syncGroupsAsTeams: byRole('switch', { name: 'sonar.auth.github.groupsSync' }),
+    organizations: byRole('textbox', { name: 'Organizations' }),
+    saveConfigButton: byRole('button', { name: 'settings.almintegration.form.save' }),
+    confirmProvisioningButton: byRole('button', { name: 'yes' }),
+    saveGithubProvisioning: byRole('button', { name: 'save' }),
+    groupAttribute: byRole('textbox', { name: 'property.sonar.auth.github.group.name.name' }),
+    enableConfigButton: byRole('button', { name: 'settings.authentication.form.enable' }),
+    editConfigButton: byRole('button', { name: 'settings.authentication.form.edit' }),
+    enableFirstMessage: byText('settings.authentication.github.enable_first'),
+    jitProvisioningButton: byRole('radio', {
+      name: 'settings.authentication.form.provisioning_at_login',
+    }),
+    githubProvisioningButton: byRole('radio', {
+      name: 'settings.authentication.github.form.provisioning_with_github',
+    }),
+    fillForm: async (user: UserEvent) => {
+      const { github } = ui;
+      await act(async () => {
+        await user.type(await github.clientId.find(), 'Awsome GITHUB config');
+        await user.type(github.clientSecret.get(), 'Client shut');
+        // await user.type(github.githubAppId.get(), 'http://test.org');
+        // await user.type(github.privateKey.get(), '-secret-');
+        await user.type(github.githubApiUrl.get(), 'API Url');
+        await user.type(github.githubWebUrl.get(), 'WEb Url');
+      });
+    },
+    createConfiguration: async (user: UserEvent) => {
+      const { github } = ui;
+      await act(async () => {
+        await user.click((await github.createConfigButton.findAll())[1]);
+      });
+      await github.fillForm(user);
+      await act(async () => {
+        await user.click(github.saveConfigButton.get());
+      });
+    },
+  },
 };
 
 it('should render tabs and allow navigation', async () => {
@@ -204,6 +254,86 @@ describe('SAML tab', () => {
   });
 });
 
+describe('Github tab', () => {
+  const { github } = ui;
+
+  it('should render an empty Github configuration', async () => {
+    renderAuthentication();
+    const user = userEvent.setup();
+    await user.click(await github.tab.find());
+    expect(await github.noGithubConfiguration.find()).toBeInTheDocument();
+  });
+
+  it('should be able to create a configuration', async () => {
+    const user = userEvent.setup();
+    renderAuthentication();
+
+    await user.click(await github.tab.find());
+    await user.click((await github.createConfigButton.findAll())[1]);
+
+    expect(github.saveConfigButton.get()).toBeDisabled();
+
+    await github.fillForm(user);
+    expect(github.saveConfigButton.get()).toBeEnabled();
+
+    await act(async () => {
+      await user.click(github.saveConfigButton.get());
+    });
+
+    expect(await github.editConfigButton.find()).toBeInTheDocument();
+  });
+
+  it('should be able to enable/disable configuration', async () => {
+    const { github, saml } = ui;
+    const user = userEvent.setup();
+    renderAuthentication();
+    await user.click(await github.tab.find());
+
+    await github.createConfiguration(user);
+
+    await user.click(await saml.enableConfigButton.find());
+
+    expect(await saml.disableConfigButton.find()).toBeInTheDocument();
+    await user.click(saml.disableConfigButton.get());
+    expect(saml.disableConfigButton.query()).not.toBeInTheDocument();
+
+    expect(await saml.enableConfigButton.find()).toBeInTheDocument();
+  });
+
+  it('should be able to choose provisioning', async () => {
+    const { github } = ui;
+    const user = userEvent.setup();
+
+    renderAuthentication([Feature.GithubProvisioning]);
+    await user.click(await github.tab.find());
+
+    await github.createConfiguration(user);
+
+    expect(await github.enableFirstMessage.find()).toBeInTheDocument();
+    await user.click(await github.enableConfigButton.find());
+
+    expect(await github.jitProvisioningButton.find()).toBeChecked();
+
+    expect(github.saveGithubProvisioning.get()).toBeDisabled();
+    await user.click(github.allowUserToSignUp.get());
+    await user.click(github.syncGroupsAsTeams.get());
+    await user.type(github.organizations.get(), 'organization1, organization2');
+
+    expect(github.saveGithubProvisioning.get()).toBeEnabled();
+    await user.click(github.saveGithubProvisioning.get());
+    expect(await github.saveGithubProvisioning.find()).toBeDisabled();
+
+    await user.click(github.githubProvisioningButton.get());
+
+    expect(github.saveGithubProvisioning.get()).toBeEnabled();
+    await user.click(github.saveGithubProvisioning.get());
+    await user.click(github.confirmProvisioningButton.get());
+
+    expect(await github.githubProvisioningButton.find()).toBeChecked();
+    expect(await github.saveGithubProvisioning.find()).toBeDisabled();
+  });
+});
+
 function renderAuthentication(features: Feature[] = []) {
   renderComponent(
     <AvailableFeaturesContext.Provider value={features}>
index 65596864b4970cca75ff0cb8b8dfa4e5b75260d5..73eb9d79c9cff3d7e887908c8c1fc031893f5a47 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { every, isEmpty, keyBy } from 'lodash';
-import React from 'react';
+import React, { useCallback, useState } from 'react';
 import { getValues, resetSettingValue } from '../../../../../api/settings';
 import { ExtendedSettingDefinition } from '../../../../../types/settings';
 import { Dict } from '../../../../../types/types';
 
-export interface SettingValue {
-  key: string;
-  mandatory: boolean;
-  isNotSet: boolean;
-  value?: string;
-  newValue?: string | boolean;
-  definition: ExtendedSettingDefinition;
-}
+export type SettingValue =
+  | {
+      key: string;
+      multiValues: false;
+      mandatory: boolean;
+      isNotSet: boolean;
+      value?: string;
+      newValue?: string | boolean;
+      definition: ExtendedSettingDefinition;
+    }
+  | {
+      key: string;
+      multiValues: true;
+      mandatory: boolean;
+      isNotSet: boolean;
+      value?: string[];
+      newValue?: string[];
+      definition: ExtendedSettingDefinition;
+    };
 
 export default function useConfiguration(
   definitions: ExtendedSettingDefinition[],
   optionalFields: string[]
 ) {
-  const [loading, setLoading] = React.useState(true);
-  const [values, setValues] = React.useState<Dict<SettingValue>>({});
+  const [loading, setLoading] = useState(true);
+  const [values, setValues] = useState<Dict<SettingValue>>({});
 
-  const reload = React.useCallback(async () => {
+  const reload = useCallback(async () => {
     const keys = definitions.map((definition) => definition.key);
 
     setLoading(true);
@@ -51,13 +62,28 @@ export default function useConfiguration(
 
       setValues(
         keyBy(
-          definitions.map((definition) => ({
-            key: definition.key,
-            value: values.find((v) => v.key === definition.key)?.value,
-            mandatory: !optionalFields.includes(definition.key),
-            isNotSet: values.find((v) => v.key === definition.key) === undefined,
-            definition,
-          })),
+          definitions.map((definition) => {
+            const value = values.find((v) => v.key === definition.key);
+            const multiValues = definition.multiValues ?? false;
+            if (multiValues) {
+              return {
+                key: definition.key,
+                multiValues,
+                value: value?.values,
+                mandatory: !optionalFields.includes(definition.key),
+                isNotSet: value === undefined,
+                definition,
+              };
+            }
+            return {
+              key: definition.key,
+              multiValues,
+              value: value?.value,
+              mandatory: !optionalFields.includes(definition.key),
+              isNotSet: value === undefined,
+              definition,
+            };
+          }),
           'key'
         )
       );
@@ -72,19 +98,27 @@ export default function useConfiguration(
     })();
   }, [...definitions]);
 
-  const setNewValue = (key: string, newValue?: string | boolean) => {
-    const newValues = {
-      ...values,
-      [key]: {
-        key,
-        newValue,
-        mandatory: values[key]?.mandatory,
-        isNotSet: values[key]?.isNotSet,
-        value: values[key]?.value,
-        definition: values[key]?.definition,
-      },
-    };
-    setValues(newValues);
+  const setNewValue = (key: string, newValue?: string | boolean | string[]) => {
+    const value = values[key];
+    if (value.multiValues) {
+      const newValues = {
+        ...values,
+        [key]: {
+          ...value,
+          newValue: newValue as string[],
+        },
+      };
+      setValues(newValues);
+    } else {
+      const newValues = {
+        ...values,
+        [key]: {
+          ...value,
+          newValue: newValue as string | boolean,
+        },
+      };
+      setValues(newValues);
+    }
   };
 
   const canBeSave = every(
@@ -99,12 +133,12 @@ export default function useConfiguration(
     (v) => !v.isNotSet
   );
 
-  const deleteConfiguration = React.useCallback(async () => {
+  const deleteConfiguration = useCallback(async () => {
     await resetSettingValue({ keys: Object.keys(values).join(',') });
     await reload();
   }, [reload, values]);
 
-  const isValueChange = React.useCallback(
+  const isValueChange = useCallback(
     (setting: string) => {
       const value = values[setting];
       return value && value.newValue !== undefined && (value.value ?? '') !== value.newValue;
index 05ec87e7c7dac10c9f814d508f45d3894812ad2d..65d932da6ec7a2cc8575ad03aa12792e147bba8a 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { some } from 'lodash';
+import { useCallback, useContext, useEffect, useState } from 'react';
+import { fetchIsGithubProvisioningEnabled } from '../../../../../api/settings';
+import { AvailableFeaturesContext } from '../../../../../app/components/available-features/AvailableFeaturesContext';
+import { Feature } from '../../../../../types/features';
 import { ExtendedSettingDefinition } from '../../../../../types/settings';
 import useConfiguration from './useConfiguration';
 
 export const GITHUB_ENABLED_FIELD = 'sonar.auth.github.enabled';
 export const GITHUB_APP_ID_FIELD = 'sonar.auth.github.appId';
 export const GITHUB_API_URL_FIELD = 'sonar.auth.github.apiUrl';
-
-const OPTIONAL_FIELDS = [
-  GITHUB_ENABLED_FIELD,
+export const GITHUB_JIT_FIELDS = [
   'sonar.auth.github.organizations',
   'sonar.auth.github.allowUsersToSignUp',
   'sonar.auth.github.groupsSync',
   'sonar.auth.github.organizations',
 ];
+export const OPTIONAL_FIELDS = [GITHUB_ENABLED_FIELD, ...GITHUB_JIT_FIELDS];
 
 export interface SamlSettingValue {
   key: string;
@@ -43,12 +47,50 @@ export interface SamlSettingValue {
 
 export default function useGithubConfiguration(definitions: ExtendedSettingDefinition[]) {
   const config = useConfiguration(definitions, OPTIONAL_FIELDS);
+  const { values, isValueChange, setNewValue, reload: reloadConfig } = config;
+  const hasGithubProvisioning = useContext(AvailableFeaturesContext).includes(
+    Feature.GithubProvisioning
+  );
+  const [githubProvisioningStatus, setGithubProvisioningStatus] = useState(false);
+  const [newGithubProvisioningStatus, setNewGithubProvisioningStatus] = useState<boolean>();
+  const hasGithubProvisioningConfigChange =
+    some(GITHUB_JIT_FIELDS, isValueChange) ||
+    (newGithubProvisioningStatus !== undefined &&
+      newGithubProvisioningStatus !== githubProvisioningStatus);
+
+  const resetJitSetting = () => {
+    GITHUB_JIT_FIELDS.forEach((s) => setNewValue(s));
+  };
 
-  const { values } = config;
+  useEffect(() => {
+    (async () => {
+      if (hasGithubProvisioning) {
+        setGithubProvisioningStatus(await fetchIsGithubProvisioningEnabled());
+      }
+    })();
+  }, [hasGithubProvisioning]);
 
   const enabled = values[GITHUB_ENABLED_FIELD]?.value === 'true';
   const appId = values[GITHUB_APP_ID_FIELD]?.value;
   const url = values[GITHUB_API_URL_FIELD]?.value;
 
-  return { ...config, url, enabled, appId };
+  const reload = useCallback(async () => {
+    await reloadConfig();
+    setGithubProvisioningStatus(await fetchIsGithubProvisioningEnabled());
+  }, [reloadConfig]);
+
+  return {
+    ...config,
+    reload,
+    url,
+    enabled,
+    appId,
+    hasGithubProvisioning,
+    setGithubProvisioningStatus,
+    githubProvisioningStatus,
+    newGithubProvisioningStatus,
+    setNewGithubProvisioningStatus,
+    hasGithubProvisioningConfigChange,
+    resetJitSetting,
+  };
 }
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useLoadSamlSettings.ts b/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useLoadSamlSettings.ts
deleted file mode 100644 (file)
index 7c06147..0000000
+++ /dev/null
@@ -1,101 +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 React from 'react';
-import { fetchIsScimEnabled } from '../../../../../api/settings';
-import { AvailableFeaturesContext } from '../../../../../app/components/available-features/AvailableFeaturesContext';
-import { Feature } from '../../../../../types/features';
-import { ExtendedSettingDefinition } from '../../../../../types/settings';
-import useConfiguration from './useConfiguration';
-
-export const SAML_ENABLED_FIELD = 'sonar.auth.saml.enabled';
-export const SAML_GROUP_NAME = 'sonar.auth.saml.group.name';
-export const SAML_SCIM_DEPRECATED = 'sonar.scim.enabled';
-const SAML_PROVIDER_NAME = 'sonar.auth.saml.providerName';
-const SAML_LOGIN_URL = 'sonar.auth.saml.loginUrl';
-
-const OPTIONAL_FIELDS = [
-  'sonar.auth.saml.sp.certificate.secured',
-  'sonar.auth.saml.sp.privateKey.secured',
-  'sonar.auth.saml.signature.enabled',
-  'sonar.auth.saml.user.email',
-  'sonar.auth.saml.group.name',
-  SAML_SCIM_DEPRECATED,
-];
-
-export default function useSamlConfiguration(definitions: ExtendedSettingDefinition[]) {
-  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);
-
-  React.useEffect(() => {
-    (async () => {
-      if (hasScim) {
-        setScimStatus(await fetchIsScimEnabled());
-      }
-    })();
-  }, [hasScim]);
-
-  const name = values[SAML_PROVIDER_NAME]?.value;
-  const url = values[SAML_LOGIN_URL]?.value;
-  const samlEnabled = values[SAML_ENABLED_FIELD]?.value === 'true';
-  const groupValue = values[SAML_GROUP_NAME];
-
-  const setNewGroupSetting = (value?: string) => {
-    setNewValue(SAML_GROUP_NAME, value);
-  };
-
-  const hasScimConfigChange =
-    isValueChange(SAML_GROUP_NAME) || (newScimStatus !== undefined && newScimStatus !== scimStatus);
-
-  const reload = React.useCallback(async () => {
-    await reloadConfig();
-    setScimStatus(await fetchIsScimEnabled());
-  }, [reloadConfig]);
-
-  return {
-    hasScim,
-    scimStatus,
-    loading,
-    samlEnabled,
-    name,
-    url,
-    groupValue,
-    hasConfiguration,
-    canBeSave,
-    values,
-    setNewValue,
-    reload,
-    hasScimConfigChange,
-    newScimStatus,
-    setNewScimStatus,
-    setNewGroupSetting,
-    deleteConfiguration,
-  };
-}
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
new file mode 100644 (file)
index 0000000..7c06147
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+ * 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 from 'react';
+import { fetchIsScimEnabled } from '../../../../../api/settings';
+import { AvailableFeaturesContext } from '../../../../../app/components/available-features/AvailableFeaturesContext';
+import { Feature } from '../../../../../types/features';
+import { ExtendedSettingDefinition } from '../../../../../types/settings';
+import useConfiguration from './useConfiguration';
+
+export const SAML_ENABLED_FIELD = 'sonar.auth.saml.enabled';
+export const SAML_GROUP_NAME = 'sonar.auth.saml.group.name';
+export const SAML_SCIM_DEPRECATED = 'sonar.scim.enabled';
+const SAML_PROVIDER_NAME = 'sonar.auth.saml.providerName';
+const SAML_LOGIN_URL = 'sonar.auth.saml.loginUrl';
+
+const OPTIONAL_FIELDS = [
+  'sonar.auth.saml.sp.certificate.secured',
+  'sonar.auth.saml.sp.privateKey.secured',
+  'sonar.auth.saml.signature.enabled',
+  'sonar.auth.saml.user.email',
+  'sonar.auth.saml.group.name',
+  SAML_SCIM_DEPRECATED,
+];
+
+export default function useSamlConfiguration(definitions: ExtendedSettingDefinition[]) {
+  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);
+
+  React.useEffect(() => {
+    (async () => {
+      if (hasScim) {
+        setScimStatus(await fetchIsScimEnabled());
+      }
+    })();
+  }, [hasScim]);
+
+  const name = values[SAML_PROVIDER_NAME]?.value;
+  const url = values[SAML_LOGIN_URL]?.value;
+  const samlEnabled = values[SAML_ENABLED_FIELD]?.value === 'true';
+  const groupValue = values[SAML_GROUP_NAME];
+
+  const setNewGroupSetting = (value?: string) => {
+    setNewValue(SAML_GROUP_NAME, value);
+  };
+
+  const hasScimConfigChange =
+    isValueChange(SAML_GROUP_NAME) || (newScimStatus !== undefined && newScimStatus !== scimStatus);
+
+  const reload = React.useCallback(async () => {
+    await reloadConfig();
+    setScimStatus(await fetchIsScimEnabled());
+  }, [reloadConfig]);
+
+  return {
+    hasScim,
+    scimStatus,
+    loading,
+    samlEnabled,
+    name,
+    url,
+    groupValue,
+    hasConfiguration,
+    canBeSave,
+    values,
+    setNewValue,
+    reload,
+    hasScimConfigChange,
+    newScimStatus,
+    setNewScimStatus,
+    setNewGroupSetting,
+    deleteConfiguration,
+  };
+}
index d116f640ea796ca7de6e4b8a9ee50c21a8d0b850..a886b08c3feddf6705645fba95b1b4a4032de031 100644 (file)
   box-sizing: border-box;
 }
 
+.radio-card .settings-definition-left {
+  padding-right: 0;
+}
+
 .settings-definition-right {
   position: relative;
   width: calc(100% - 330px);
   box-sizing: border-box;
 }
 
+.radio-card .settings-definition-right input {
+  width: 100%;
+}
+
 .settings-definition-name {
   text-overflow: ellipsis;
 }
   overflow-wrap: break-word;
 }
 
-.saml-enabled {
+.authentication-enabled {
   color: var(--success500);
 }
 
-.saml-no-config {
+.authentication-no-config {
   background-color: var(--neutral50);
   color: var(--blacka60);
 }
 
-.saml-configuration .radio-card {
+.authentication-configuration .radio-card {
   width: 50%;
   background-color: var(--neutral50);
   border: 1px solid var(--neutral200);
 }
 
-.saml-configuration .radio-card.selected {
+.authentication-configuration .radio-card.selected {
   background-color: var(--info50);
   border: 1px solid var(--info500);
 }
 
-.saml-configuration .radio-card:hover:not(.selected) {
+.authentication-configuration .radio-card:hover:not(.selected) {
   border: 1px solid var(--info500);
 }
 
-.saml-configuration fieldset > div {
+.authentication-configuration fieldset > div {
   justify-content: space-between;
 }
 
-.saml-configuration .radio-card-header {
+.authentication-configuration .radio-card-header {
   justify-content: space-around;
 }
 
-.saml-configuration .radio-card-body {
+.authentication-configuration .radio-card-body {
   justify-content: flex-start;
 }
 
-.saml-configuration .settings-definition-left {
+.authentication-configuration .settings-definition-left {
   width: 50%;
 }
 
-.saml-configuration .settings-definition-right {
+.authentication-configuration .settings-definition-right {
   display: flex;
   align-items: center;
   width: 50%;
index a586efb67a082f81e5fc007c1ae3bce31cfe701a..fdeb6ab31a5350107f8fd394db665268fe256367 100644 (file)
@@ -26,4 +26,5 @@ export enum Feature {
   ProjectImport = 'project-import',
   RegulatoryReport = 'regulatory-reports',
   Scim = 'scim',
+  GithubProvisioning = 'github-provisioning',
 }
index 0416859153b17b075425db4935d8f75263d73f44..64fb16e4836f09d3d5583a462abee27656e586cf 100644 (file)
@@ -1314,6 +1314,7 @@ settings.almintegration.feature.alm_repo_import.disabled=Disabled
 settings.almintegration.feature.alm_repo_import.disabled.no_url=This feature is disabled because your configured instance has no URL.
 settings.almintegration.tabs.authentication_moved=You can delegate authentication to this DevOps Platform. The relevant settings are under the {link} section.
 
+# Authentication Common
 settings.authentication.title=Authentication
 settings.authentication.custom_message_information=You can define a custom log-in message to appear on the log-in page to help your users authenticate. The relevant settings are available under the {link} section.
 settings.authentication.custom_message_information.link=General
@@ -1324,14 +1325,30 @@ settings.authentication.form.create=Create configuration
 settings.authentication.form.edit=Edit
 settings.authentication.form.delete=Delete
 settings.authentication.form.loading=Loading configuration
-
-settings.authentication.form.create.saml=New SAML configuration
-settings.authentication.form.edit.saml=Edit SAML configuration
+settings.authentication.form.enabled=Enabled
+settings.authentication.form.not_enabled=This configuration is disabled
+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)
+
+# GITHUB
 settings.authentication.form.create.github=New Github configuration
 settings.authentication.form.edit.github=Edit Github configuration
-
+settings.authentication.github.confirm.auto=Switch to automatic provisioning
+settings.authentication.github.confirm.jit=Switch to Just-in-Time provisioning
+settings.authentication.github.confirm.auto.description=After you switch to automatic provisioning, you will no longer be able to edit groups, users, and group memberships within SonarQube. Are you sure?
+settings.authentication.github.confirm.jit.description=Switching to Just-in-Time provisioning removes all information provided while automatic provisioning through SCIM was active. These changes cannot be reverted. Are you sure?
 settings.authentication.github.configuration=Github Configuration
 settings.authentication.github.form.not_configured=Github App is not configured
+settings.authentication.github.enable_first=Enable your Github configuration for more provisioning options.
+settings.authentication.github.form.provisioning_with_github=Automatic user and group provisioning
+settings.authentication.github.form.provisioning_with_github.description=Users and groups are automatically provisioned from your GitHub organizations. Once activated, managed users and groups can only be modified from your GitHub organizations/teams. Existing local users and groups will be kept.
+settings.authentication.github.form.provisioning_with_github.description.doc=For more details, see {documentation}.
+
+# SAML
+settings.authentication.form.create.saml=New SAML configuration
+settings.authentication.form.edit.saml=Edit SAML configuration
 settings.authentication.saml.configuration=SAML Configuration
 settings.authentication.saml.confirm.scim=Switch to automatic provisioning
 settings.authentication.saml.confirm.jit=Switch to Just-in-Time provisioning
@@ -1339,10 +1356,6 @@ settings.authentication.saml.confirm.scim.description=After you switch to automa
 settings.authentication.saml.confirm.jit.description=Switching to Just-in-Time provisioning removes all information provided while automatic provisioning through SCIM was active. These changes cannot be reverted. Are you sure?
 settings.authentication.saml.form.loading=Loading SAML configuration
 settings.authentication.saml.form.not_configured=SAML is not configured
-settings.authentication.saml.form.enable=Enable configuration
-settings.authentication.saml.form.disable=Disable configuration
-settings.authentication.saml.form.enabled=Enabled
-settings.authentication.saml.form.not_enabled=This configuration is disabled
 settings.authentication.saml.form.create=New SAML configuration
 settings.authentication.saml.form.edit=Edit SAML configuration
 settings.authentication.saml.form.save=Save configuration
@@ -1351,8 +1364,6 @@ 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=Provisioning
-settings.authentication.saml.form.provisioning_at_login=Just-in-Time user and group provisioning (default)
 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.
@@ -1360,6 +1371,7 @@ settings.authentication.saml.form.provisioning_with_scim.description=Users and g
 settings.authentication.saml.form.provisioning_with_scim.description.doc=For a list of supported providers and more details on automatic provisioning, see {documentation}.
 settings.authentication.saml.form.provisioning.disabled=Your current edition does not support provisioning with SCIM. See the {documentation} for more information. 
 settings.authentication.saml.enable_first=Enable your SAML configuration to benefit from automatic user provisioning options.
+
 settings.pr_decoration.binding.category=DevOps Platform Integration
 settings.pr_decoration.binding.no_bindings=A system administrator needs to enable this feature in the global settings.
 settings.pr_decoration.binding.no_bindings.admin=Set up a {link} first before you and your team can enable Pull Request Decoration.