aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMathieu Suen <mathieu.suen@sonarsource.com>2023-04-24 17:18:45 +0200
committersonartech <sonartech@sonarsource.com>2023-05-11 20:03:13 +0000
commit47d837b5736ca2e0d71520e9d2cc9deec3388b3f (patch)
treefddbf0d67d2cf97d637ef19f7743987a3bcbd19f
parentc84b4032a56be146536423221b5c82aea7c418e3 (diff)
downloadsonarqube-47d837b5736ca2e0d71520e9d2cc9deec3388b3f.tar.gz
sonarqube-47d837b5736ca2e0d71520e9d2cc9deec3388b3f.zip
SONAR-19084 Improve github authentication setting
-rw-r--r--server/sonar-web/src/main/js/app/components/extensions/CreateApplicationForm.tsx4
-rw-r--r--server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/CreateApplicationForm-test.tsx.snap1
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionFormRenderer.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx28
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationFormField.tsx (renamed from server/sonar-web/src/main/js/apps/settings/components/authentication/SamlFormField.tsx)18
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationSecuredField.tsx (renamed from server/sonar-web/src/main/js/apps/settings/components/authentication/SamlSecuredField.tsx)37
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationToggleField.tsx (renamed from server/sonar-web/src/main/js/apps/settings/components/authentication/SamlToggleField.tsx)14
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/ConfigurationForm.tsx (renamed from server/sonar-web/src/main/js/apps/settings/components/authentication/SamlConfigurationForm.tsx)38
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAutheticationTab.tsx142
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx (renamed from server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthentication.tsx)37
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useConfiguration.ts125
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useGithubConfiguration.ts54
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useLoadSamlSettings.ts105
-rw-r--r--server/sonar-web/src/main/js/components/controls/BoxedTabs.tsx2
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties9
16 files changed, 452 insertions, 170 deletions
diff --git a/server/sonar-web/src/main/js/app/components/extensions/CreateApplicationForm.tsx b/server/sonar-web/src/main/js/app/components/extensions/CreateApplicationForm.tsx
index 674dc5bd20e..450c50989d7 100644
--- a/server/sonar-web/src/main/js/app/components/extensions/CreateApplicationForm.tsx
+++ b/server/sonar-web/src/main/js/app/components/extensions/CreateApplicationForm.tsx
@@ -19,9 +19,9 @@
*/
import * as React from 'react';
import { createApplication } from '../../../api/application';
-import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
import Radio from '../../../components/controls/Radio';
import SimpleModal from '../../../components/controls/SimpleModal';
+import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
import DeferredSpinner from '../../../components/ui/DeferredSpinner';
import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker';
import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
@@ -104,7 +104,7 @@ export default class CreateApplicationForm extends React.PureComponent<Props, St
size="small"
>
{({ onCloseClick, onFormSubmit, submitting }) => (
- <form className="views-form" onSubmit={onFormSubmit}>
+ <form onSubmit={onFormSubmit}>
<div className="modal-head">
<h2>{header}</h2>
</div>
diff --git a/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/CreateApplicationForm-test.tsx.snap b/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/CreateApplicationForm-test.tsx.snap
index eca852deb51..a193f2cd305 100644
--- a/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/CreateApplicationForm-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/CreateApplicationForm-test.tsx.snap
@@ -18,7 +18,6 @@ exports[`should render correctly: form 1`] = `
size="small"
>
<form
- className="views-form"
onSubmit={[Function]}
>
<div
diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionFormRenderer.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionFormRenderer.tsx
index 238b2585192..5e95c87d2c3 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionFormRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionFormRenderer.tsx
@@ -18,8 +18,8 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { ResetButtonLink, SubmitButton } from '../../../../components/controls/buttons';
import Modal from '../../../../components/controls/Modal';
+import { ResetButtonLink, SubmitButton } from '../../../../components/controls/buttons';
import { Alert } from '../../../../components/ui/Alert';
import DeferredSpinner from '../../../../components/ui/DeferredSpinner';
import { translate } from '../../../../helpers/l10n';
@@ -112,7 +112,7 @@ export default class AlmBindingDefinitionFormRenderer extends React.PureComponen
shouldCloseOnOverlayClick={false}
size="medium"
>
- <form className="views-form" onSubmit={handleSubmit}>
+ <form onSubmit={handleSubmit}>
<div className="modal-head">
<h2>{header}</h2>
</div>
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx
index 361c2495e63..a10aa8293f2 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx
@@ -37,7 +37,8 @@ import { Feature } from '../../../../types/features';
import { ExtendedSettingDefinition } from '../../../../types/settings';
import { AUTHENTICATION_CATEGORY } from '../../constants';
import CategoryDefinitionsList from '../CategoryDefinitionsList';
-import SamlAuthentication, { SAML } from './SamlAuthentication';
+import GithubAithentication from './GithubAutheticationTab';
+import SamlAuthenticationTab, { SAML } from './SamlAuthenticationTab';
interface Props {
definitions: ExtendedSettingDefinition[];
@@ -52,7 +53,7 @@ export type AuthenticationTabs =
| AlmKeys.GitLab
| AlmKeys.BitbucketServer;
-const DOCUMENTATION_LINK_SUFFIXES = {
+export const DOCUMENTATION_LINK_SUFFIXES = {
[SAML]: 'saml/overview',
[AlmKeys.GitHub]: 'github',
[AlmKeys.GitLab]: 'gitlab',
@@ -109,7 +110,7 @@ export function Authentication(props: Props & WithAvailableFeaturesProps) {
</>
),
},
- ];
+ ] as const;
return (
<>
@@ -151,7 +152,10 @@ export function Authentication(props: Props & WithAvailableFeaturesProps) {
{tabs.map((tab) => (
<div
style={{
- maxHeight: tab.key !== SAML ? `calc(100vh - ${top + HEIGHT_ADJUSTMENT}px)` : '',
+ maxHeight:
+ tab.key !== SAML && tab.key !== AlmKeys.GitHub
+ ? `calc(100vh - ${top + HEIGHT_ADJUSTMENT}px)`
+ : '',
}}
className={classNames('bordered overflow-y-auto tabbed-definitions', {
hidden: currentTab !== tab.key,
@@ -162,9 +166,19 @@ export function Authentication(props: Props & WithAvailableFeaturesProps) {
id={getTabPanelId(tab.key)}
>
<div className="big-padded-top big-padded-left big-padded-right">
- {tab.key === SAML && <SamlAuthentication definitions={definitions} />}
+ {tab.key === SAML && (
+ <SamlAuthenticationTab
+ definitions={definitions.filter((def) => def.subCategory === SAML)}
+ />
+ )}
+
+ {tab.key === AlmKeys.GitHub && (
+ <GithubAithentication
+ definitions={definitions.filter((def) => def.subCategory === AlmKeys.GitHub)}
+ />
+ )}
- {tab.key !== SAML && (
+ {tab.key !== SAML && tab.key !== AlmKeys.GitHub && (
<>
<Alert variant="info">
<FormattedMessage
@@ -174,7 +188,7 @@ export function Authentication(props: Props & WithAvailableFeaturesProps) {
link: (
<DocLink
to={`/instance-administration/authentication/${
- DOCUMENTATION_LINK_SUFFIXES[tab.key as AuthenticationTabs]
+ DOCUMENTATION_LINK_SUFFIXES[tab.key]
}/`}
>
{translate('settings.authentication.help.link')}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlFormField.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationFormField.tsx
index d86afef207d..85706584951 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlFormField.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationFormField.tsx
@@ -23,8 +23,9 @@ import ValidationInput, {
} from '../../../../components/controls/ValidationInput';
import MandatoryFieldMarker from '../../../../components/ui/MandatoryFieldMarker';
import { ExtendedSettingDefinition, SettingType } from '../../../../types/settings';
-import SamlSecuredField from './SamlSecuredField';
-import SamlToggleField from './SamlToggleField';
+import { isSecuredDefinition } from '../../utils';
+import AuthenticationSecuredField from './AuthenticationSecuredField';
+import AuthenticationToggleField from './AuthenticationToggleField';
interface SamlToggleFieldProps {
settingValue?: string | boolean;
@@ -35,7 +36,7 @@ interface SamlToggleFieldProps {
error?: string;
}
-export default function SamlFormField(props: SamlToggleFieldProps) {
+export default function AuthenticationFormField(props: SamlToggleFieldProps) {
const { mandatory = false, definition, settingValue, isNotSet, error } = props;
return (
@@ -50,23 +51,22 @@ export default function SamlFormField(props: SamlToggleFieldProps) {
)}
</div>
<div className="settings-definition-right big-padded-top display-flex-column">
- {definition.type === SettingType.PASSWORD && (
- <SamlSecuredField
+ {isSecuredDefinition(definition) && (
+ <AuthenticationSecuredField
definition={definition}
settingValue={String(settingValue ?? '')}
onFieldChange={props.onFieldChange}
isNotSet={isNotSet}
/>
)}
- {definition.type === SettingType.BOOLEAN && (
- <SamlToggleField
+ {!isSecuredDefinition(definition) && definition.type === SettingType.BOOLEAN && (
+ <AuthenticationToggleField
definition={definition}
settingValue={settingValue}
- toggleDisabled={false}
onChange={(value) => props.onFieldChange(definition.key, value)}
/>
)}
- {definition.type === undefined && (
+ {!isSecuredDefinition(definition) && definition.type === undefined && (
<ValidationInput
error={error}
errorPlacement={ValidationInputErrorPlacement.Bottom}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlSecuredField.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationSecuredField.tsx
index a7177a2a114..ed1345b1398 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlSecuredField.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationSecuredField.tsx
@@ -20,7 +20,7 @@
import React, { useEffect } from 'react';
import { ButtonLink } from '../../../../components/controls/buttons';
import { translate } from '../../../../helpers/l10n';
-import { ExtendedSettingDefinition } from '../../../../types/settings';
+import { ExtendedSettingDefinition, SettingType } from '../../../../types/settings';
import { isSecuredDefinition } from '../../utils';
interface SamlToggleFieldProps {
@@ -31,7 +31,7 @@ interface SamlToggleFieldProps {
isNotSet: boolean;
}
-export default function SamlSecuredField(props: SamlToggleFieldProps) {
+export default function AuthenticationSecuredField(props: SamlToggleFieldProps) {
const { settingValue, definition, optional = true, isNotSet } = props;
const [showSecretField, setShowSecretField] = React.useState(
!isNotSet && isSecuredDefinition(definition)
@@ -43,17 +43,28 @@ export default function SamlSecuredField(props: SamlToggleFieldProps) {
return (
<>
- {!showSecretField && (
- <textarea
- className="width-100"
- id={definition.key}
- maxLength={4000}
- onChange={(e) => props.onFieldChange(definition.key, e.currentTarget.value)}
- required={!optional}
- rows={5}
- value={settingValue ?? ''}
- />
- )}
+ {!showSecretField &&
+ (definition.type === SettingType.TEXT ? (
+ <textarea
+ className="width-100"
+ id={definition.key}
+ maxLength={4000}
+ onChange={(e) => props.onFieldChange(definition.key, e.currentTarget.value)}
+ required={!optional}
+ rows={5}
+ value={settingValue ?? ''}
+ />
+ ) : (
+ <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 ?? '')}
+ />
+ ))}
{showSecretField && (
<div>
<p>{translate('settings.almintegration.form.secret.field')}</p>
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlToggleField.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationToggleField.tsx
index a7c787c2cd0..40b71d67ec9 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlToggleField.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationToggleField.tsx
@@ -22,21 +22,13 @@ import Toggle from '../../../../components/controls/Toggle';
import { ExtendedSettingDefinition } from '../../../../types/settings';
interface SamlToggleFieldProps {
- toggleDisabled: boolean;
onChange: (value: boolean) => void;
settingValue?: string | boolean;
definition: ExtendedSettingDefinition;
}
-export default function SamlToggleField(props: SamlToggleFieldProps) {
- const { toggleDisabled, settingValue, definition } = props;
+export default function AuthenticationToggleField(props: SamlToggleFieldProps) {
+ const { settingValue, definition } = props;
- return (
- <Toggle
- name={definition.key}
- onChange={props.onChange}
- value={settingValue ?? ''}
- disabled={toggleDisabled}
- />
- );
+ return <Toggle name={definition.key} onChange={props.onChange} value={settingValue ?? ''} />;
}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlConfigurationForm.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/ConfigurationForm.tsx
index 028429f6031..5920471a229 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlConfigurationForm.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/ConfigurationForm.tsx
@@ -22,28 +22,26 @@ import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { setSettingValue } from '../../../../api/settings';
import DocLink from '../../../../components/common/DocLink';
-import { ResetButtonLink, SubmitButton } from '../../../../components/controls/buttons';
import Modal from '../../../../components/controls/Modal';
+import { ResetButtonLink, SubmitButton } from '../../../../components/controls/buttons';
import { Alert } from '../../../../components/ui/Alert';
import DeferredSpinner from '../../../../components/ui/DeferredSpinner';
import { translate } from '../../../../helpers/l10n';
import { Dict } from '../../../../types/types';
-import {
- SamlSettingValue,
- SAML_ENABLED_FIELD,
- SAML_GROUP_NAME,
- SAML_SCIM_DEPRECATED,
-} from './hook/useLoadSamlSettings';
-import SamlFormField from './SamlFormField';
+import { AuthenticationTabs, DOCUMENTATION_LINK_SUFFIXES } from './Authentication';
+import AuthenticationFormField from './AuthenticationFormField';
+import { SettingValue } from './hook/useConfiguration';
interface Props {
create: boolean;
loading: boolean;
- values: Dict<SamlSettingValue>;
+ values: Dict<SettingValue>;
setNewValue: (key: string, value: string | boolean) => void;
canBeSave: boolean;
onClose: () => void;
onReload: () => Promise<void>;
+ tab: AuthenticationTabs;
+ excludedField: string[];
}
interface ErrorValue {
@@ -51,15 +49,11 @@ interface ErrorValue {
message: string;
}
-export const SAML = 'saml';
-
-const SAML_EXCLUDED_FIELD = [SAML_ENABLED_FIELD, SAML_GROUP_NAME, SAML_SCIM_DEPRECATED];
-
-export default function SamlConfigurationForm(props: Props) {
- const { create, loading, values, setNewValue, canBeSave } = props;
+export default function ConfigurationForm(props: Props) {
+ const { create, loading, values, setNewValue, canBeSave, tab, excludedField } = props;
const [errors, setErrors] = React.useState<Dict<ErrorValue>>({});
- const headerLabel = translate('settings.authentication.saml.form', create ? 'create' : 'edit');
+ const headerLabel = translate('settings.authentication.form', create ? 'create' : 'edit', tab);
const handleSubmit = async (event: React.SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();
@@ -95,14 +89,14 @@ export default function SamlConfigurationForm(props: Props) {
return (
<Modal contentLabel={headerLabel} shouldCloseOnOverlayClick={false} size="medium">
- <form className="views-form create-saml-form" onSubmit={handleSubmit}>
+ <form onSubmit={handleSubmit}>
<div className="modal-head">
<h2>{headerLabel}</h2>
</div>
<div className="modal-body modal-container">
<DeferredSpinner
loading={loading}
- ariaLabel={translate('settings.authentication.saml.form.loading')}
+ ariaLabel={translate('settings.authentication.form.loading')}
>
<Alert variant="info">
<FormattedMessage
@@ -110,7 +104,9 @@ export default function SamlConfigurationForm(props: Props) {
defaultMessage={translate('settings.authentication.help')}
values={{
link: (
- <DocLink to="/instance-administration/authentication/saml/overview/">
+ <DocLink
+ to={`/instance-administration/authentication/${DOCUMENTATION_LINK_SUFFIXES[tab]}/`}
+ >
{translate('settings.authentication.help.link')}
</DocLink>
),
@@ -118,12 +114,12 @@ export default function SamlConfigurationForm(props: Props) {
/>
</Alert>
{Object.values(values).map((val) => {
- if (SAML_EXCLUDED_FIELD.includes(val.key)) {
+ if (excludedField.includes(val.key)) {
return null;
}
return (
<div key={val.key}>
- <SamlFormField
+ <AuthenticationFormField
settingValue={values[val.key]?.newValue ?? values[val.key]?.value}
definition={val.definition}
mandatory={val.mandatory}
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
new file mode 100644
index 00000000000..837ef38530e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAutheticationTab.tsx
@@ -0,0 +1,142 @@
+/*
+ * 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 { setSettingValue } from '../../../../api/settings';
+import { Button } from '../../../../components/controls/buttons';
+import CheckIcon from '../../../../components/icons/CheckIcon';
+import DeleteIcon from '../../../../components/icons/DeleteIcon';
+import EditIcon from '../../../../components/icons/EditIcon';
+import { translate } from '../../../../helpers/l10n';
+import { AlmKeys } from '../../../../types/alm-settings';
+import { ExtendedSettingDefinition } from '../../../../types/settings';
+import ConfigurationForm from './ConfigurationForm';
+import useGithubConfiguration, { GITHUB_ENABLED_FIELD } from './hook/useGithubConfiguration';
+
+interface SamlAuthenticationProps {
+ definitions: ExtendedSettingDefinition[];
+}
+
+const GITHUB_EXCLUDED_FIELD = [
+ 'sonar.auth.github.enabled',
+ 'sonar.auth.github.groupsSync',
+ 'sonar.auth.github.allowUsersToSignUp',
+];
+
+export default function GithubAithentication(props: SamlAuthenticationProps) {
+ const [showEditModal, setShowEditModal] = React.useState(false);
+ const {
+ hasConfiguration,
+ loading,
+ values,
+ setNewValue,
+ canBeSave,
+ reload,
+ url,
+ appId,
+ enabled,
+ deleteConfiguration,
+ } = useGithubConfiguration(props.definitions);
+
+ const handleCreateConfiguration = () => {
+ setShowEditModal(true);
+ };
+
+ const handleCancelConfiguration = () => {
+ setShowEditModal(false);
+ };
+
+ const handleToggleEnable = async () => {
+ const value = values[GITHUB_ENABLED_FIELD];
+ await setSettingValue(value.definition, !enabled);
+ await reload();
+ };
+
+ return (
+ <div className="saml-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 saml-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="saml-enabled spacer-left">
+ <CheckIcon className="spacer-right" />
+ {translate('settings.authentication.saml.form.enabled')}
+ </span>
+ ) : (
+ translate('settings.authentication.saml.form.not_enabled')
+ )}
+ </p>
+ <Button className="spacer-top" onClick={handleToggleEnable}>
+ {enabled
+ ? translate('settings.authentication.saml.form.disable')
+ : translate('settings.authentication.saml.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">
+ Provisioning TODO
+ </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/SamlAuthentication.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx
index e6e123b5f39..43ab887e349 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthentication.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx
@@ -40,8 +40,12 @@ import { getBaseUrl } from '../../../../helpers/system';
import { ExtendedSettingDefinition } from '../../../../types/settings';
import { getPropertyName } from '../../utils';
import DefinitionDescription from '../DefinitionDescription';
-import SamlConfigurationForm from './SamlConfigurationForm';
-import useSamlConfiguration, { SAML_ENABLED_FIELD } from './hook/useLoadSamlSettings';
+import ConfigurationForm from './ConfigurationForm';
+import useSamlConfiguration, {
+ SAML_ENABLED_FIELD,
+ SAML_GROUP_NAME,
+ SAML_SCIM_DEPRECATED,
+} from './hook/useLoadSamlSettings';
interface SamlAuthenticationProps {
definitions: ExtendedSettingDefinition[];
@@ -50,8 +54,9 @@ interface SamlAuthenticationProps {
export const SAML = 'saml';
const CONFIG_TEST_PATH = '/saml/validation_init';
+const SAML_EXCLUDED_FIELD = [SAML_ENABLED_FIELD, SAML_GROUP_NAME, SAML_SCIM_DEPRECATED];
-export default function SamlAuthentication(props: SamlAuthenticationProps) {
+export default function SamlAuthenticationTab(props: SamlAuthenticationProps) {
const { definitions } = props;
const [showEditModal, setShowEditModal] = React.useState(false);
const [showConfirmProvisioningModal, setShowConfirmProvisioningModal] = React.useState(false);
@@ -71,14 +76,10 @@ export default function SamlAuthentication(props: SamlAuthenticationProps) {
newScimStatus,
setNewScimStatus,
setNewGroupSetting,
- onReload,
+ reload,
+ deleteConfiguration,
} = useSamlConfiguration(definitions);
- const handleDeleteConfiguration = async () => {
- await resetSettingValue({ keys: Object.keys(values).join(',') });
- await onReload();
- };
-
const handleCreateConfiguration = () => {
setShowEditModal(true);
};
@@ -90,7 +91,7 @@ export default function SamlAuthentication(props: SamlAuthenticationProps) {
const handleToggleEnable = async () => {
const value = values[SAML_ENABLED_FIELD];
await setSettingValue(value.definition, !samlEnabled);
- await onReload();
+ await reload();
};
const handleSaveGroup = async () => {
@@ -100,7 +101,7 @@ export default function SamlAuthentication(props: SamlAuthenticationProps) {
} else {
await setSettingValue(groupValue.definition, groupValue.newValue);
}
- await onReload();
+ await reload();
}
};
@@ -111,7 +112,7 @@ export default function SamlAuthentication(props: SamlAuthenticationProps) {
await deactivateScim();
await handleSaveGroup();
}
- await onReload();
+ await reload();
};
return (
@@ -167,11 +168,7 @@ export default function SamlAuthentication(props: SamlAuthenticationProps) {
<EditIcon />
{translate('settings.authentication.form.edit')}
</Button>
- <Button
- className="button-red"
- disabled={samlEnabled}
- onClick={handleDeleteConfiguration}
- >
+ <Button className="button-red" disabled={samlEnabled} onClick={deleteConfiguration}>
<DeleteIcon />
{translate('settings.authentication.form.delete')}
</Button>
@@ -318,14 +315,16 @@ export default function SamlAuthentication(props: SamlAuthenticationProps) {
</>
)}
{showEditModal && (
- <SamlConfigurationForm
+ <ConfigurationForm
+ tab={SAML}
+ excludedField={SAML_EXCLUDED_FIELD}
loading={loading}
values={values}
setNewValue={setNewValue}
canBeSave={canBeSave}
onClose={handleCancelConfiguration}
create={!hasConfiguration}
- onReload={onReload}
+ onReload={reload}
/>
)}
</div>
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx
index b8136d361e4..3b7b730add5 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx
@@ -84,7 +84,7 @@ const ui = {
createConfiguration: async (user: UserEvent) => {
const { saml } = ui;
await act(async () => {
- await user.click(await saml.createConfigButton.find());
+ await user.click((await saml.createConfigButton.findAll())[0]);
});
await saml.fillForm(user);
await act(async () => {
@@ -135,7 +135,7 @@ describe('SAML tab', () => {
const user = userEvent.setup();
renderAuthentication();
- await user.click(await saml.createConfigButton.find());
+ await user.click((await saml.createConfigButton.findAll())[0]);
expect(saml.saveConfigButton.get()).toBeDisabled();
await saml.fillForm(user);
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useConfiguration.ts b/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useConfiguration.ts
new file mode 100644
index 00000000000..65596864b49
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useConfiguration.ts
@@ -0,0 +1,125 @@
+/*
+ * 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 { every, isEmpty, keyBy } from 'lodash';
+import React 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 default function useConfiguration(
+ definitions: ExtendedSettingDefinition[],
+ optionalFields: string[]
+) {
+ const [loading, setLoading] = React.useState(true);
+ const [values, setValues] = React.useState<Dict<SettingValue>>({});
+
+ const reload = React.useCallback(async () => {
+ const keys = definitions.map((definition) => definition.key);
+
+ setLoading(true);
+
+ try {
+ const values = await getValues({
+ keys,
+ });
+
+ 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,
+ })),
+ 'key'
+ )
+ );
+ } finally {
+ setLoading(false);
+ }
+ }, [...definitions]);
+
+ React.useEffect(() => {
+ (async () => {
+ await reload();
+ })();
+ }, [...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 canBeSave = every(
+ Object.values(values).filter((v) => v.mandatory),
+ (v) =>
+ (v.newValue !== undefined && !isEmpty(v.newValue)) ||
+ (!v.isNotSet && v.newValue === undefined)
+ );
+
+ const hasConfiguration = every(
+ Object.values(values).filter((v) => v.mandatory),
+ (v) => !v.isNotSet
+ );
+
+ const deleteConfiguration = React.useCallback(async () => {
+ await resetSettingValue({ keys: Object.keys(values).join(',') });
+ await reload();
+ }, [reload, values]);
+
+ const isValueChange = React.useCallback(
+ (setting: string) => {
+ const value = values[setting];
+ return value && value.newValue !== undefined && (value.value ?? '') !== value.newValue;
+ },
+ [values]
+ );
+
+ return {
+ values,
+ reload,
+ setNewValue,
+ canBeSave,
+ loading,
+ hasConfiguration,
+ isValueChange,
+ deleteConfiguration,
+ };
+}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useGithubConfiguration.ts b/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useGithubConfiguration.ts
new file mode 100644
index 00000000000..05ec87e7c7d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useGithubConfiguration.ts
@@ -0,0 +1,54 @@
+/*
+ * 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 { 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,
+ 'sonar.auth.github.organizations',
+ 'sonar.auth.github.allowUsersToSignUp',
+ 'sonar.auth.github.groupsSync',
+ 'sonar.auth.github.organizations',
+];
+
+export interface SamlSettingValue {
+ key: string;
+ mandatory: boolean;
+ isNotSet: boolean;
+ value?: string;
+ newValue?: string | boolean;
+ definition: ExtendedSettingDefinition;
+}
+
+export default function useGithubConfiguration(definitions: ExtendedSettingDefinition[]) {
+ const config = useConfiguration(definitions, OPTIONAL_FIELDS);
+
+ const { values } = config;
+
+ 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 };
+}
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
index af29e0ea0c5..7c06147aacf 100644
--- 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
@@ -17,15 +17,12 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { every, isEmpty, keyBy } from 'lodash';
import React from 'react';
-import { fetchIsScimEnabled, getValues } from '../../../../../api/settings';
+import { fetchIsScimEnabled } from '../../../../../api/settings';
import { AvailableFeaturesContext } from '../../../../../app/components/available-features/AvailableFeaturesContext';
import { Feature } from '../../../../../types/features';
import { ExtendedSettingDefinition } from '../../../../../types/settings';
-import { Dict } from '../../../../../types/types';
-
-const SAML = 'saml';
+import useConfiguration from './useConfiguration';
export const SAML_ENABLED_FIELD = 'sonar.auth.saml.enabled';
export const SAML_GROUP_NAME = 'sonar.auth.saml.group.name';
@@ -42,87 +39,28 @@ const OPTIONAL_FIELDS = [
SAML_SCIM_DEPRECATED,
];
-export interface SamlSettingValue {
- key: string;
- mandatory: boolean;
- isNotSet: boolean;
- value?: string;
- newValue?: string | boolean;
- definition: ExtendedSettingDefinition;
-}
-
export default function useSamlConfiguration(definitions: ExtendedSettingDefinition[]) {
- const [loading, setLoading] = React.useState(true);
const [scimStatus, setScimStatus] = React.useState<boolean>(false);
- const [values, setValues] = React.useState<Dict<SamlSettingValue>>({});
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 onReload = React.useCallback(async () => {
- const samlDefinition = definitions.filter((def) => def.subCategory === SAML);
- const keys = samlDefinition.map((definition) => definition.key);
-
- setLoading(true);
-
- try {
- const values = await getValues({
- keys,
- });
-
- setValues(
- keyBy(
- samlDefinition.map((definition) => ({
- key: definition.key,
- value: values.find((v) => v.key === definition.key)?.value,
- mandatory: !OPTIONAL_FIELDS.includes(definition.key),
- isNotSet: values.find((v) => v.key === definition.key) === undefined,
- definition,
- })),
- 'key'
- )
- );
-
+ React.useEffect(() => {
+ (async () => {
if (hasScim) {
setScimStatus(await fetchIsScimEnabled());
}
- } finally {
- setLoading(false);
- }
- }, [...definitions]);
-
- React.useEffect(() => {
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- (async () => {
- await onReload();
})();
- }, [...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 canBeSave = every(
- Object.values(values).filter((v) => v.mandatory),
- (v) =>
- (v.newValue !== undefined && !isEmpty(v.newValue)) ||
- (!v.isNotSet && v.newValue === undefined)
- );
-
- const hasConfiguration = every(
- Object.values(values).filter((v) => v.mandatory),
- (v) => !v.isNotSet
- );
+ }, [hasScim]);
const name = values[SAML_PROVIDER_NAME]?.value;
const url = values[SAML_LOGIN_URL]?.value;
@@ -134,10 +72,12 @@ export default function useSamlConfiguration(definitions: ExtendedSettingDefinit
};
const hasScimConfigChange =
- newScimStatus !== undefined &&
- groupValue &&
- (newScimStatus !== scimStatus ||
- (groupValue.newValue !== undefined && (groupValue.value ?? '') !== groupValue.newValue));
+ isValueChange(SAML_GROUP_NAME) || (newScimStatus !== undefined && newScimStatus !== scimStatus);
+
+ const reload = React.useCallback(async () => {
+ await reloadConfig();
+ setScimStatus(await fetchIsScimEnabled());
+ }, [reloadConfig]);
return {
hasScim,
@@ -151,10 +91,11 @@ export default function useSamlConfiguration(definitions: ExtendedSettingDefinit
canBeSave,
values,
setNewValue,
- onReload,
+ reload,
hasScimConfigChange,
newScimStatus,
setNewScimStatus,
setNewGroupSetting,
+ deleteConfiguration,
};
}
diff --git a/server/sonar-web/src/main/js/components/controls/BoxedTabs.tsx b/server/sonar-web/src/main/js/components/controls/BoxedTabs.tsx
index 1df7333356d..1794ecd47c7 100644
--- a/server/sonar-web/src/main/js/components/controls/BoxedTabs.tsx
+++ b/server/sonar-web/src/main/js/components/controls/BoxedTabs.tsx
@@ -25,7 +25,7 @@ export interface BoxedTabsProps<K extends string | number> {
className?: string;
onSelect: (key: K) => void;
selected?: K;
- tabs: Array<{ key: K; label: React.ReactNode }>;
+ tabs: ReadonlyArray<{ key: K; label: React.ReactNode }>;
}
const TabContainer = styled.div`
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
index 4702ad16e86..0416859153b 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -1323,6 +1323,15 @@ settings.authentication.help.link=documentation
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.create.github=New Github configuration
+settings.authentication.form.edit.github=Edit Github configuration
+
+settings.authentication.github.configuration=Github Configuration
+settings.authentication.github.form.not_configured=Github App is not configured
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