*/
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';
size="small"
>
{({ onCloseClick, onFormSubmit, submitting }) => (
- <form className="views-form" onSubmit={onFormSubmit}>
+ <form onSubmit={onFormSubmit}>
<div className="modal-head">
<h2>{header}</h2>
</div>
size="small"
>
<form
- className="views-form"
onSubmit={[Function]}
>
<div
* 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';
shouldCloseOnOverlayClick={false}
size="medium"
>
- <form className="views-form" onSubmit={handleSubmit}>
+ <form onSubmit={handleSubmit}>
<div className="modal-head">
<h2>{header}</h2>
</div>
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[];
| AlmKeys.GitLab
| AlmKeys.BitbucketServer;
-const DOCUMENTATION_LINK_SUFFIXES = {
+export const DOCUMENTATION_LINK_SUFFIXES = {
[SAML]: 'saml/overview',
[AlmKeys.GitHub]: 'github',
[AlmKeys.GitLab]: 'gitlab',
</>
),
},
- ];
+ ] as const;
return (
<>
{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,
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
link: (
<DocLink
to={`/instance-administration/authentication/${
- DOCUMENTATION_LINK_SUFFIXES[tab.key as AuthenticationTabs]
+ DOCUMENTATION_LINK_SUFFIXES[tab.key]
}/`}
>
{translate('settings.authentication.help.link')}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React from 'react';
+import ValidationInput, {
+ ValidationInputErrorPlacement,
+} from '../../../../components/controls/ValidationInput';
+import MandatoryFieldMarker from '../../../../components/ui/MandatoryFieldMarker';
+import { ExtendedSettingDefinition, SettingType } from '../../../../types/settings';
+import { isSecuredDefinition } from '../../utils';
+import AuthenticationSecuredField from './AuthenticationSecuredField';
+import AuthenticationToggleField from './AuthenticationToggleField';
+
+interface SamlToggleFieldProps {
+ settingValue?: string | boolean;
+ definition: ExtendedSettingDefinition;
+ mandatory?: boolean;
+ onFieldChange: (key: string, value: string | boolean) => void;
+ isNotSet: boolean;
+ error?: string;
+}
+
+export default function AuthenticationFormField(props: SamlToggleFieldProps) {
+ const { mandatory = false, definition, settingValue, isNotSet, error } = props;
+
+ return (
+ <div className="settings-definition">
+ <div className="settings-definition-left">
+ <label className="h3" htmlFor={definition.key}>
+ {definition.name}
+ </label>
+ {mandatory && <MandatoryFieldMarker />}
+ {definition.description && (
+ <div className="markdown small spacer-top">{definition.description}</div>
+ )}
+ </div>
+ <div className="settings-definition-right big-padded-top display-flex-column">
+ {isSecuredDefinition(definition) && (
+ <AuthenticationSecuredField
+ definition={definition}
+ settingValue={String(settingValue ?? '')}
+ onFieldChange={props.onFieldChange}
+ isNotSet={isNotSet}
+ />
+ )}
+ {!isSecuredDefinition(definition) && definition.type === SettingType.BOOLEAN && (
+ <AuthenticationToggleField
+ definition={definition}
+ settingValue={settingValue}
+ 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>
+ )}
+ </div>
+ </div>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React, { useEffect } from 'react';
+import { ButtonLink } from '../../../../components/controls/buttons';
+import { translate } from '../../../../helpers/l10n';
+import { ExtendedSettingDefinition, SettingType } from '../../../../types/settings';
+import { isSecuredDefinition } from '../../utils';
+
+interface SamlToggleFieldProps {
+ onFieldChange: (key: string, value: string) => void;
+ settingValue?: string;
+ definition: ExtendedSettingDefinition;
+ optional?: boolean;
+ isNotSet: boolean;
+}
+
+export default function AuthenticationSecuredField(props: SamlToggleFieldProps) {
+ const { settingValue, definition, optional = true, isNotSet } = props;
+ const [showSecretField, setShowSecretField] = React.useState(
+ !isNotSet && isSecuredDefinition(definition)
+ );
+
+ useEffect(() => {
+ setShowSecretField(!isNotSet && isSecuredDefinition(definition));
+ }, [isNotSet, definition]);
+
+ return (
+ <>
+ {!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>
+ <ButtonLink
+ onClick={() => {
+ setShowSecretField(false);
+ }}
+ >
+ {translate('settings.almintegration.form.secret.update_field')}
+ </ButtonLink>
+ </div>
+ )}
+ </>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React from 'react';
+import Toggle from '../../../../components/controls/Toggle';
+import { ExtendedSettingDefinition } from '../../../../types/settings';
+
+interface SamlToggleFieldProps {
+ onChange: (value: boolean) => void;
+ settingValue?: string | boolean;
+ definition: ExtendedSettingDefinition;
+}
+
+export default function AuthenticationToggleField(props: SamlToggleFieldProps) {
+ const { settingValue, definition } = props;
+
+ return <Toggle name={definition.key} onChange={props.onChange} value={settingValue ?? ''} />;
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { isEmpty, keyBy } from 'lodash';
+import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { setSettingValue } from '../../../../api/settings';
+import DocLink from '../../../../components/common/DocLink';
+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 { AuthenticationTabs, DOCUMENTATION_LINK_SUFFIXES } from './Authentication';
+import AuthenticationFormField from './AuthenticationFormField';
+import { SettingValue } from './hook/useConfiguration';
+
+interface Props {
+ create: boolean;
+ loading: boolean;
+ values: Dict<SettingValue>;
+ setNewValue: (key: string, value: string | boolean) => void;
+ canBeSave: boolean;
+ onClose: () => void;
+ onReload: () => Promise<void>;
+ tab: AuthenticationTabs;
+ excludedField: string[];
+}
+
+interface ErrorValue {
+ key: string;
+ message: string;
+}
+
+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.form', create ? 'create' : 'edit', tab);
+
+ const handleSubmit = async (event: React.SyntheticEvent<HTMLFormElement>) => {
+ event.preventDefault();
+
+ if (canBeSave) {
+ const r = await Promise.all(
+ Object.values(values)
+ .filter((v) => v.newValue !== undefined)
+ .map(async ({ key, newValue, definition }) => {
+ try {
+ await setSettingValue(definition, newValue);
+ return { key, success: true };
+ } catch (error) {
+ return { key, success: false };
+ }
+ })
+ );
+ const errors = r
+ .filter(({ success }) => !success)
+ .map(({ key }) => ({ key, message: translate('default_save_field_error_message') }));
+ setErrors(keyBy(errors, 'key'));
+ if (isEmpty(errors)) {
+ await props.onReload();
+ props.onClose();
+ }
+ } else {
+ const errors = Object.values(values)
+ .filter((v) => v.newValue === undefined && v.value === undefined && v.mandatory)
+ .map((v) => ({ key: v.key, message: translate('field_required') }));
+ setErrors(keyBy(errors, 'key'));
+ }
+ };
+
+ return (
+ <Modal contentLabel={headerLabel} shouldCloseOnOverlayClick={false} size="medium">
+ <form onSubmit={handleSubmit}>
+ <div className="modal-head">
+ <h2>{headerLabel}</h2>
+ </div>
+ <div className="modal-body modal-container">
+ <DeferredSpinner
+ loading={loading}
+ ariaLabel={translate('settings.authentication.form.loading')}
+ >
+ <Alert variant="info">
+ <FormattedMessage
+ id="settings.authentication.help"
+ defaultMessage={translate('settings.authentication.help')}
+ values={{
+ link: (
+ <DocLink
+ to={`/instance-administration/authentication/${DOCUMENTATION_LINK_SUFFIXES[tab]}/`}
+ >
+ {translate('settings.authentication.help.link')}
+ </DocLink>
+ ),
+ }}
+ />
+ </Alert>
+ {Object.values(values).map((val) => {
+ if (excludedField.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}
+ error={errors[val.key]?.message}
+ />
+ </div>
+ );
+ })}
+ </DeferredSpinner>
+ </div>
+
+ <div className="modal-foot">
+ <SubmitButton disabled={!canBeSave}>
+ {translate('settings.almintegration.form.save')}
+ <DeferredSpinner className="spacer-left" loading={loading} />
+ </SubmitButton>
+ <ResetButtonLink onClick={props.onClose}>{translate('cancel')}</ResetButtonLink>
+ </div>
+ </form>
+ </Modal>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import 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>
+ );
+}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { isEmpty } from 'lodash';
-import React from 'react';
-import { FormattedMessage } from 'react-intl';
-import {
- activateScim,
- deactivateScim,
- resetSettingValue,
- setSettingValue,
-} from '../../../../api/settings';
-import DocLink from '../../../../components/common/DocLink';
-import Link from '../../../../components/common/Link';
-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 { 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';
-
-interface SamlAuthenticationProps {
- definitions: ExtendedSettingDefinition[];
-}
-
-export const SAML = 'saml';
-
-const CONFIG_TEST_PATH = '/saml/validation_init';
-
-export default function SamlAuthentication(props: SamlAuthenticationProps) {
- const { definitions } = props;
- const [showEditModal, setShowEditModal] = React.useState(false);
- const [showConfirmProvisioningModal, setShowConfirmProvisioningModal] = React.useState(false);
- const {
- hasScim,
- scimStatus,
- loading,
- samlEnabled,
- name,
- groupValue,
- url,
- hasConfiguration,
- values,
- setNewValue,
- canBeSave,
- hasScimConfigChange,
- newScimStatus,
- setNewScimStatus,
- setNewGroupSetting,
- onReload,
- } = useSamlConfiguration(definitions);
-
- const handleDeleteConfiguration = async () => {
- await resetSettingValue({ keys: Object.keys(values).join(',') });
- await onReload();
- };
-
- const handleCreateConfiguration = () => {
- setShowEditModal(true);
- };
-
- const handleCancelConfiguration = () => {
- setShowEditModal(false);
- };
-
- const handleToggleEnable = async () => {
- const value = values[SAML_ENABLED_FIELD];
- await setSettingValue(value.definition, !samlEnabled);
- await onReload();
- };
-
- const handleSaveGroup = async () => {
- if (groupValue.newValue !== undefined) {
- if (isEmpty(groupValue.newValue)) {
- await resetSettingValue({ keys: groupValue.definition.key });
- } else {
- await setSettingValue(groupValue.definition, groupValue.newValue);
- }
- await onReload();
- }
- };
-
- const handleConfirmChangeProvisioning = async () => {
- if (newScimStatus) {
- await activateScim();
- } else {
- await deactivateScim();
- await handleSaveGroup();
- }
- await onReload();
- };
-
- return (
- <div className="saml-configuration">
- <div className="spacer-bottom display-flex-space-between display-flex-center">
- <h4>{translate('settings.authentication.saml.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.saml.form.not_configured')}
- </div>
- )}
-
- {hasConfiguration && (
- <>
- <div className="spacer-bottom big-padded bordered display-flex-space-between">
- <div>
- <h5>{name}</h5>
- <p>{url}</p>
- <p className="big-spacer-top big-spacer-bottom">
- {samlEnabled ? (
- <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" disabled={scimStatus} onClick={handleToggleEnable}>
- {samlEnabled
- ? translate('settings.authentication.saml.form.disable')
- : translate('settings.authentication.saml.form.enable')}
- </Button>
- </div>
- <div>
- <Link
- className="button spacer-right"
- target="_blank"
- to={`${getBaseUrl()}${CONFIG_TEST_PATH}`}
- >
- {translate('settings.authentication.saml.form.test')}
- </Link>
- <Button className="spacer-right" onClick={handleCreateConfiguration}>
- <EditIcon />
- {translate('settings.authentication.form.edit')}
- </Button>
- <Button
- className="button-red"
- disabled={samlEnabled}
- onClick={handleDeleteConfiguration}
- >
- <DeleteIcon />
- {translate('settings.authentication.form.delete')}
- </Button>
- </div>
- </div>
- <div className="spacer-bottom big-padded bordered display-flex-space-between">
- <form
- onSubmit={(e) => {
- e.preventDefault();
- if (newScimStatus !== scimStatus) {
- setShowConfirmProvisioningModal(true);
- } else {
- handleSaveGroup();
- }
- }}
- >
- <fieldset className="display-flex-column big-spacer-bottom">
- <label className="h5">
- {translate('settings.authentication.saml.form.provisioning')}
- </label>
- {samlEnabled ? (
- <div className="display-flex-row spacer-top">
- <RadioCard
- label={translate('settings.authentication.saml.form.provisioning_with_scim')}
- title={translate('settings.authentication.saml.form.provisioning_with_scim')}
- selected={newScimStatus ?? scimStatus}
- onClick={() => setNewScimStatus(true)}
- disabled={!hasScim}
- >
- {!hasScim ? (
- <p>
- <FormattedMessage
- id="settings.authentication.saml.form.provisioning.disabled"
- defaultMessage={translate(
- 'settings.authentication.saml.form.provisioning.disabled'
- )}
- values={{
- documentation: (
- <DocLink to="/instance-administration/authentication/saml/scim/overview">
- {translate('documentation')}
- </DocLink>
- ),
- }}
- />
- </p>
- ) : (
- <>
- <p className="spacer-bottom">
- {translate(
- 'settings.authentication.saml.form.provisioning_with_scim.sub'
- )}
- </p>
- <p className="spacer-bottom">
- {translate(
- 'settings.authentication.saml.form.provisioning_with_scim.description'
- )}
- </p>
- <p>
- <FormattedMessage
- id="settings.authentication.saml.form.provisioning_with_scim.description.doc"
- defaultMessage={translate(
- 'settings.authentication.saml.form.provisioning_with_scim.description.doc'
- )}
- values={{
- documentation: (
- <DocLink to="/instance-administration/authentication/saml/scim/overview">
- {translate('documentation')}
- </DocLink>
- ),
- }}
- />
- </p>
- </>
- )}
- </RadioCard>
- <RadioCard
- label={translate('settings.authentication.saml.form.provisioning_at_login')}
- title={translate('settings.authentication.saml.form.provisioning_at_login')}
- selected={!(newScimStatus ?? scimStatus)}
- onClick={() => setNewScimStatus(false)}
- >
- <p>
- {translate('settings.authentication.saml.form.provisioning_at_login.sub')}
- </p>
- {groupValue && (
- <div className="settings-definition">
- <DefinitionDescription definition={groupValue.definition} />
- <div className="settings-definition-right">
- <input
- id={groupValue.definition.key}
- maxLength={4000}
- name={groupValue.definition.key}
- onChange={(e) => setNewGroupSetting(e.currentTarget.value)}
- type="text"
- value={String(groupValue.newValue ?? groupValue.value ?? '')}
- aria-label={getPropertyName(groupValue.definition)}
- />
- </div>
- </div>
- )}
- </RadioCard>
- </div>
- ) : (
- <Alert className="big-spacer-top" variant="info">
- {translate('settings.authentication.saml.enable_first')}
- </Alert>
- )}
- </fieldset>
- {samlEnabled && (
- <>
- <SubmitButton disabled={!hasScimConfigChange}>{translate('save')}</SubmitButton>
- <ResetButtonLink
- className="spacer-left"
- onClick={() => {
- setNewScimStatus(undefined);
- setNewGroupSetting();
- }}
- disabled={!hasScimConfigChange}
- >
- {translate('cancel')}
- </ResetButtonLink>
- </>
- )}
- {showConfirmProvisioningModal && (
- <ConfirmModal
- onConfirm={() => handleConfirmChangeProvisioning()}
- header={translate(
- 'settings.authentication.saml.confirm',
- newScimStatus ? 'scim' : 'jit'
- )}
- onClose={() => setShowConfirmProvisioningModal(false)}
- isDestructive={!newScimStatus}
- confirmButtonText={translate('yes')}
- >
- {translate(
- 'settings.authentication.saml.confirm',
- newScimStatus ? 'scim' : 'jit',
- 'description'
- )}
- </ConfirmModal>
- )}
- </form>
- </div>
- </>
- )}
- {showEditModal && (
- <SamlConfigurationForm
- loading={loading}
- values={values}
- setNewValue={setNewValue}
- canBeSave={canBeSave}
- onClose={handleCancelConfiguration}
- create={!hasConfiguration}
- onReload={onReload}
- />
- )}
- </div>
- );
-}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { isEmpty } from 'lodash';
+import React from 'react';
+import { FormattedMessage } from 'react-intl';
+import {
+ activateScim,
+ deactivateScim,
+ resetSettingValue,
+ setSettingValue,
+} from '../../../../api/settings';
+import DocLink from '../../../../components/common/DocLink';
+import Link from '../../../../components/common/Link';
+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 { getBaseUrl } from '../../../../helpers/system';
+import { ExtendedSettingDefinition } from '../../../../types/settings';
+import { getPropertyName } from '../../utils';
+import DefinitionDescription from '../DefinitionDescription';
+import ConfigurationForm from './ConfigurationForm';
+import useSamlConfiguration, {
+ SAML_ENABLED_FIELD,
+ SAML_GROUP_NAME,
+ SAML_SCIM_DEPRECATED,
+} from './hook/useLoadSamlSettings';
+
+interface SamlAuthenticationProps {
+ definitions: ExtendedSettingDefinition[];
+}
+
+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 SamlAuthenticationTab(props: SamlAuthenticationProps) {
+ const { definitions } = props;
+ const [showEditModal, setShowEditModal] = React.useState(false);
+ const [showConfirmProvisioningModal, setShowConfirmProvisioningModal] = React.useState(false);
+ const {
+ hasScim,
+ scimStatus,
+ loading,
+ samlEnabled,
+ name,
+ groupValue,
+ url,
+ hasConfiguration,
+ values,
+ setNewValue,
+ canBeSave,
+ hasScimConfigChange,
+ newScimStatus,
+ setNewScimStatus,
+ setNewGroupSetting,
+ reload,
+ deleteConfiguration,
+ } = useSamlConfiguration(definitions);
+
+ const handleCreateConfiguration = () => {
+ setShowEditModal(true);
+ };
+
+ const handleCancelConfiguration = () => {
+ setShowEditModal(false);
+ };
+
+ const handleToggleEnable = async () => {
+ const value = values[SAML_ENABLED_FIELD];
+ await setSettingValue(value.definition, !samlEnabled);
+ await reload();
+ };
+
+ const handleSaveGroup = async () => {
+ if (groupValue.newValue !== undefined) {
+ if (isEmpty(groupValue.newValue)) {
+ await resetSettingValue({ keys: groupValue.definition.key });
+ } else {
+ await setSettingValue(groupValue.definition, groupValue.newValue);
+ }
+ await reload();
+ }
+ };
+
+ const handleConfirmChangeProvisioning = async () => {
+ if (newScimStatus) {
+ await activateScim();
+ } else {
+ await deactivateScim();
+ await handleSaveGroup();
+ }
+ await reload();
+ };
+
+ return (
+ <div className="saml-configuration">
+ <div className="spacer-bottom display-flex-space-between display-flex-center">
+ <h4>{translate('settings.authentication.saml.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.saml.form.not_configured')}
+ </div>
+ )}
+
+ {hasConfiguration && (
+ <>
+ <div className="spacer-bottom big-padded bordered display-flex-space-between">
+ <div>
+ <h5>{name}</h5>
+ <p>{url}</p>
+ <p className="big-spacer-top big-spacer-bottom">
+ {samlEnabled ? (
+ <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" disabled={scimStatus} onClick={handleToggleEnable}>
+ {samlEnabled
+ ? translate('settings.authentication.saml.form.disable')
+ : translate('settings.authentication.saml.form.enable')}
+ </Button>
+ </div>
+ <div>
+ <Link
+ className="button spacer-right"
+ target="_blank"
+ to={`${getBaseUrl()}${CONFIG_TEST_PATH}`}
+ >
+ {translate('settings.authentication.saml.form.test')}
+ </Link>
+ <Button className="spacer-right" onClick={handleCreateConfiguration}>
+ <EditIcon />
+ {translate('settings.authentication.form.edit')}
+ </Button>
+ <Button className="button-red" disabled={samlEnabled} onClick={deleteConfiguration}>
+ <DeleteIcon />
+ {translate('settings.authentication.form.delete')}
+ </Button>
+ </div>
+ </div>
+ <div className="spacer-bottom big-padded bordered display-flex-space-between">
+ <form
+ onSubmit={(e) => {
+ e.preventDefault();
+ if (newScimStatus !== scimStatus) {
+ setShowConfirmProvisioningModal(true);
+ } else {
+ handleSaveGroup();
+ }
+ }}
+ >
+ <fieldset className="display-flex-column big-spacer-bottom">
+ <label className="h5">
+ {translate('settings.authentication.saml.form.provisioning')}
+ </label>
+ {samlEnabled ? (
+ <div className="display-flex-row spacer-top">
+ <RadioCard
+ label={translate('settings.authentication.saml.form.provisioning_with_scim')}
+ title={translate('settings.authentication.saml.form.provisioning_with_scim')}
+ selected={newScimStatus ?? scimStatus}
+ onClick={() => setNewScimStatus(true)}
+ disabled={!hasScim}
+ >
+ {!hasScim ? (
+ <p>
+ <FormattedMessage
+ id="settings.authentication.saml.form.provisioning.disabled"
+ defaultMessage={translate(
+ 'settings.authentication.saml.form.provisioning.disabled'
+ )}
+ values={{
+ documentation: (
+ <DocLink to="/instance-administration/authentication/saml/scim/overview">
+ {translate('documentation')}
+ </DocLink>
+ ),
+ }}
+ />
+ </p>
+ ) : (
+ <>
+ <p className="spacer-bottom">
+ {translate(
+ 'settings.authentication.saml.form.provisioning_with_scim.sub'
+ )}
+ </p>
+ <p className="spacer-bottom">
+ {translate(
+ 'settings.authentication.saml.form.provisioning_with_scim.description'
+ )}
+ </p>
+ <p>
+ <FormattedMessage
+ id="settings.authentication.saml.form.provisioning_with_scim.description.doc"
+ defaultMessage={translate(
+ 'settings.authentication.saml.form.provisioning_with_scim.description.doc'
+ )}
+ values={{
+ documentation: (
+ <DocLink to="/instance-administration/authentication/saml/scim/overview">
+ {translate('documentation')}
+ </DocLink>
+ ),
+ }}
+ />
+ </p>
+ </>
+ )}
+ </RadioCard>
+ <RadioCard
+ label={translate('settings.authentication.saml.form.provisioning_at_login')}
+ title={translate('settings.authentication.saml.form.provisioning_at_login')}
+ selected={!(newScimStatus ?? scimStatus)}
+ onClick={() => setNewScimStatus(false)}
+ >
+ <p>
+ {translate('settings.authentication.saml.form.provisioning_at_login.sub')}
+ </p>
+ {groupValue && (
+ <div className="settings-definition">
+ <DefinitionDescription definition={groupValue.definition} />
+ <div className="settings-definition-right">
+ <input
+ id={groupValue.definition.key}
+ maxLength={4000}
+ name={groupValue.definition.key}
+ onChange={(e) => setNewGroupSetting(e.currentTarget.value)}
+ type="text"
+ value={String(groupValue.newValue ?? groupValue.value ?? '')}
+ aria-label={getPropertyName(groupValue.definition)}
+ />
+ </div>
+ </div>
+ )}
+ </RadioCard>
+ </div>
+ ) : (
+ <Alert className="big-spacer-top" variant="info">
+ {translate('settings.authentication.saml.enable_first')}
+ </Alert>
+ )}
+ </fieldset>
+ {samlEnabled && (
+ <>
+ <SubmitButton disabled={!hasScimConfigChange}>{translate('save')}</SubmitButton>
+ <ResetButtonLink
+ className="spacer-left"
+ onClick={() => {
+ setNewScimStatus(undefined);
+ setNewGroupSetting();
+ }}
+ disabled={!hasScimConfigChange}
+ >
+ {translate('cancel')}
+ </ResetButtonLink>
+ </>
+ )}
+ {showConfirmProvisioningModal && (
+ <ConfirmModal
+ onConfirm={() => handleConfirmChangeProvisioning()}
+ header={translate(
+ 'settings.authentication.saml.confirm',
+ newScimStatus ? 'scim' : 'jit'
+ )}
+ onClose={() => setShowConfirmProvisioningModal(false)}
+ isDestructive={!newScimStatus}
+ confirmButtonText={translate('yes')}
+ >
+ {translate(
+ 'settings.authentication.saml.confirm',
+ newScimStatus ? 'scim' : 'jit',
+ 'description'
+ )}
+ </ConfirmModal>
+ )}
+ </form>
+ </div>
+ </>
+ )}
+ {showEditModal && (
+ <ConfigurationForm
+ tab={SAML}
+ excludedField={SAML_EXCLUDED_FIELD}
+ loading={loading}
+ values={values}
+ setNewValue={setNewValue}
+ canBeSave={canBeSave}
+ onClose={handleCancelConfiguration}
+ create={!hasConfiguration}
+ onReload={reload}
+ />
+ )}
+ </div>
+ );
+}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { isEmpty, keyBy } from 'lodash';
-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 { 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';
-
-interface Props {
- create: boolean;
- loading: boolean;
- values: Dict<SamlSettingValue>;
- setNewValue: (key: string, value: string | boolean) => void;
- canBeSave: boolean;
- onClose: () => void;
- onReload: () => Promise<void>;
-}
-
-interface ErrorValue {
- key: string;
- 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;
- const [errors, setErrors] = React.useState<Dict<ErrorValue>>({});
-
- const headerLabel = translate('settings.authentication.saml.form', create ? 'create' : 'edit');
-
- const handleSubmit = async (event: React.SyntheticEvent<HTMLFormElement>) => {
- event.preventDefault();
-
- if (canBeSave) {
- const r = await Promise.all(
- Object.values(values)
- .filter((v) => v.newValue !== undefined)
- .map(async ({ key, newValue, definition }) => {
- try {
- await setSettingValue(definition, newValue);
- return { key, success: true };
- } catch (error) {
- return { key, success: false };
- }
- })
- );
- const errors = r
- .filter(({ success }) => !success)
- .map(({ key }) => ({ key, message: translate('default_save_field_error_message') }));
- setErrors(keyBy(errors, 'key'));
- if (isEmpty(errors)) {
- await props.onReload();
- props.onClose();
- }
- } else {
- const errors = Object.values(values)
- .filter((v) => v.newValue === undefined && v.value === undefined && v.mandatory)
- .map((v) => ({ key: v.key, message: translate('field_required') }));
- setErrors(keyBy(errors, 'key'));
- }
- };
-
- return (
- <Modal contentLabel={headerLabel} shouldCloseOnOverlayClick={false} size="medium">
- <form className="views-form create-saml-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')}
- >
- <Alert variant="info">
- <FormattedMessage
- id="settings.authentication.help"
- defaultMessage={translate('settings.authentication.help')}
- values={{
- link: (
- <DocLink to="/instance-administration/authentication/saml/overview/">
- {translate('settings.authentication.help.link')}
- </DocLink>
- ),
- }}
- />
- </Alert>
- {Object.values(values).map((val) => {
- if (SAML_EXCLUDED_FIELD.includes(val.key)) {
- return null;
- }
- return (
- <div key={val.key}>
- <SamlFormField
- settingValue={values[val.key]?.newValue ?? values[val.key]?.value}
- definition={val.definition}
- mandatory={val.mandatory}
- onFieldChange={setNewValue}
- isNotSet={val.isNotSet}
- error={errors[val.key]?.message}
- />
- </div>
- );
- })}
- </DeferredSpinner>
- </div>
-
- <div className="modal-foot">
- <SubmitButton disabled={!canBeSave}>
- {translate('settings.almintegration.form.save')}
- <DeferredSpinner className="spacer-left" loading={loading} />
- </SubmitButton>
- <ResetButtonLink onClick={props.onClose}>{translate('cancel')}</ResetButtonLink>
- </div>
- </form>
- </Modal>
- );
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import React from 'react';
-import ValidationInput, {
- ValidationInputErrorPlacement,
-} from '../../../../components/controls/ValidationInput';
-import MandatoryFieldMarker from '../../../../components/ui/MandatoryFieldMarker';
-import { ExtendedSettingDefinition, SettingType } from '../../../../types/settings';
-import SamlSecuredField from './SamlSecuredField';
-import SamlToggleField from './SamlToggleField';
-
-interface SamlToggleFieldProps {
- settingValue?: string | boolean;
- definition: ExtendedSettingDefinition;
- mandatory?: boolean;
- onFieldChange: (key: string, value: string | boolean) => void;
- isNotSet: boolean;
- error?: string;
-}
-
-export default function SamlFormField(props: SamlToggleFieldProps) {
- const { mandatory = false, definition, settingValue, isNotSet, error } = props;
-
- return (
- <div className="settings-definition">
- <div className="settings-definition-left">
- <label className="h3" htmlFor={definition.key}>
- {definition.name}
- </label>
- {mandatory && <MandatoryFieldMarker />}
- {definition.description && (
- <div className="markdown small spacer-top">{definition.description}</div>
- )}
- </div>
- <div className="settings-definition-right big-padded-top display-flex-column">
- {definition.type === SettingType.PASSWORD && (
- <SamlSecuredField
- definition={definition}
- settingValue={String(settingValue ?? '')}
- onFieldChange={props.onFieldChange}
- isNotSet={isNotSet}
- />
- )}
- {definition.type === SettingType.BOOLEAN && (
- <SamlToggleField
- definition={definition}
- settingValue={settingValue}
- toggleDisabled={false}
- onChange={(value) => props.onFieldChange(definition.key, value)}
- />
- )}
- {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>
- )}
- </div>
- </div>
- );
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import React, { useEffect } from 'react';
-import { ButtonLink } from '../../../../components/controls/buttons';
-import { translate } from '../../../../helpers/l10n';
-import { ExtendedSettingDefinition } from '../../../../types/settings';
-import { isSecuredDefinition } from '../../utils';
-
-interface SamlToggleFieldProps {
- onFieldChange: (key: string, value: string) => void;
- settingValue?: string;
- definition: ExtendedSettingDefinition;
- optional?: boolean;
- isNotSet: boolean;
-}
-
-export default function SamlSecuredField(props: SamlToggleFieldProps) {
- const { settingValue, definition, optional = true, isNotSet } = props;
- const [showSecretField, setShowSecretField] = React.useState(
- !isNotSet && isSecuredDefinition(definition)
- );
-
- useEffect(() => {
- setShowSecretField(!isNotSet && isSecuredDefinition(definition));
- }, [isNotSet, definition]);
-
- 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 && (
- <div>
- <p>{translate('settings.almintegration.form.secret.field')}</p>
- <ButtonLink
- onClick={() => {
- setShowSecretField(false);
- }}
- >
- {translate('settings.almintegration.form.secret.update_field')}
- </ButtonLink>
- </div>
- )}
- </>
- );
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import React from 'react';
-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;
-
- return (
- <Toggle
- name={definition.key}
- onChange={props.onChange}
- value={settingValue ?? ''}
- disabled={toggleDisabled}
- />
- );
-}
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 () => {
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);
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { 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,
+ };
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { 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 };
+}
* 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';
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;
};
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,
canBeSave,
values,
setNewValue,
- onReload,
+ reload,
hasScimConfigChange,
newScimStatus,
setNewScimStatus,
setNewGroupSetting,
+ deleteConfiguration,
};
}
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`
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