aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorViktor Vorona <viktor.vorona@sonarsource.com>2024-02-01 11:47:38 +0100
committersonartech <sonartech@sonarsource.com>2024-02-01 20:02:48 +0000
commitcab06a5a89e4f3f085f4e3a7fe5fbcfea4a8e5cc (patch)
treef1b6ff84b7773d092617c2183957f4574ff5a15e /server
parentd72acd5707f3ac1234f60a326dffbb197b1e103f (diff)
downloadsonarqube-cab06a5a89e4f3f085f4e3a7fe5fbcfea4a8e5cc.tar.gz
sonarqube-cab06a5a89e4f3f085f4e3a7fe5fbcfea4a8e5cc.zip
SONAR-21507 Show a warning for Bitbucket Authentication in case of insecure config
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/api/mocks/SettingsServiceMock.ts39
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/Definition.tsx268
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/DefinitionActions.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/DefinitionRenderer.tsx114
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/__tests__/SettingsApp-it.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx36
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/AutoProvisionningConsent.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/BitbucketAuthenticationTab.tsx89
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Bitbucket-it.tsx105
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useConfiguration.ts2
-rw-r--r--server/sonar-web/src/main/js/queries/settings.ts33
11 files changed, 408 insertions, 286 deletions
diff --git a/server/sonar-web/src/main/js/api/mocks/SettingsServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/SettingsServiceMock.ts
index 7a0c6be4c92..f6d83f8b986 100644
--- a/server/sonar-web/src/main/js/api/mocks/SettingsServiceMock.ts
+++ b/server/sonar-web/src/main/js/api/mocks/SettingsServiceMock.ts
@@ -29,6 +29,7 @@ import {
SettingValue,
SettingsKey,
} from '../../types/settings';
+import { Dict } from '../../types/types';
import {
checkSecretKey,
encryptValue,
@@ -159,7 +160,16 @@ export default class SettingsServiceMock {
handleGetValue = (data: { key: string; component?: string } & BranchParameters) => {
const setting = this.#settingValues.find((s) => s.key === data.key) as SettingValue;
- return this.reply(setting ?? {});
+ const definition = this.#definitions.find(
+ (d) => d.key === data.key,
+ ) as ExtendedSettingDefinition;
+ if (!setting && definition?.defaultValue !== undefined) {
+ const fields = definition.multiValues
+ ? { values: definition.defaultValue?.split(',') }
+ : { value: definition.defaultValue };
+ return this.reply({ key: data.key, ...fields });
+ }
+ return this.reply(setting ?? undefined);
};
handleGetValues = (data: { keys: string[]; component?: string } & BranchParameters) => {
@@ -215,11 +225,26 @@ export default class SettingsServiceMock {
(s) => s.key !== 'sonar.auth.github.userConsentForPermissionProvisioningRequired',
);
} else if (definition.type === SettingType.PROPERTY_SET) {
- setting.fieldValues = [];
+ const fieldValues: Dict<string>[] = [];
+ if (setting) {
+ setting.fieldValues = fieldValues;
+ } else {
+ this.#settingValues.push({ key: data.keys, fieldValues });
+ }
} else if (definition.multiValues === true) {
- setting.values = definition.defaultValue?.split(',') ?? [];
- } else if (setting) {
- setting.value = definition.defaultValue ?? '';
+ const values = definition.defaultValue?.split(',') ?? [];
+ if (setting) {
+ setting.values = values;
+ } else {
+ this.#settingValues.push({ key: data.keys, values });
+ }
+ } else {
+ const value = definition.defaultValue ?? '';
+ if (setting) {
+ setting.value = value;
+ } else {
+ this.#settingValues.push({ key: data.keys, value });
+ }
}
return this.reply(undefined);
@@ -246,6 +271,10 @@ export default class SettingsServiceMock {
this.#definitions.push(definition);
};
+ setDefinitions = (definitions: ExtendedSettingDefinition[]) => {
+ this.#definitions = definitions;
+ };
+
handleCheckSecretKey = () => {
return this.reply({ secretKeyAvailable: this.#secretKeyAvailable });
};
diff --git a/server/sonar-web/src/main/js/apps/settings/components/Definition.tsx b/server/sonar-web/src/main/js/apps/settings/components/Definition.tsx
index 7cb6a927bba..d8379e27c81 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/Definition.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/Definition.tsx
@@ -17,14 +17,27 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { FlagMessage, Note, Spinner, TextError } from 'design-system';
import * as React from 'react';
-import { getValue, resetSettingValue, setSettingValue } from '../../../api/settings';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { parseError } from '../../../helpers/request';
+import {
+ useGetValueQuery,
+ useResetSettingsMutation,
+ useSaveValueMutation,
+} from '../../../queries/settings';
import { ExtendedSettingDefinition, SettingType, SettingValue } from '../../../types/settings';
import { Component } from '../../../types/types';
-import { isEmptyValue, isURLKind } from '../utils';
-import DefinitionRenderer from './DefinitionRenderer';
+import {
+ combineDefinitionAndSettingValue,
+ getSettingValue,
+ isDefaultOrInherited,
+ isEmptyValue,
+ isURLKind,
+} from '../utils';
+import DefinitionActions from './DefinitionActions';
+import DefinitionDescription from './DefinitionDescription';
+import Input from './inputs/Input';
interface Props {
component?: Component;
@@ -32,88 +45,68 @@ interface Props {
initialSettingValue?: SettingValue;
}
-interface State {
- changedValue?: string;
- isEditing: boolean;
- loading: boolean;
- success: boolean;
- validationMessage?: string;
- settingValue?: SettingValue;
-}
-
const SAFE_SET_STATE_DELAY = 3000;
-
-export default class Definition extends React.PureComponent<Props, State> {
- timeout?: number;
- mounted = false;
-
- constructor(props: Props) {
- super(props);
-
- this.state = {
- isEditing: false,
- loading: false,
- success: false,
- settingValue: props.initialSettingValue,
- };
- }
-
- componentDidMount() {
- this.mounted = true;
- }
-
- componentWillUnmount() {
- this.mounted = false;
- clearTimeout(this.timeout);
- }
-
- handleChange = (changedValue: any) => {
- clearTimeout(this.timeout);
-
- this.setState({ changedValue, success: false }, this.handleCheck);
+const formNoop = (e: React.FormEvent<HTMLFormElement>) => e.preventDefault();
+type FieldValue = string | string[] | boolean;
+
+export default function Definition(props: Readonly<Props>) {
+ const { component, definition, initialSettingValue } = props;
+ const timeout = React.useRef<number | undefined>();
+ const [isEditing, setIsEditing] = React.useState(false);
+ const [loading, setLoading] = React.useState(false);
+ const [success, setSuccess] = React.useState(false);
+ const [changedValue, setChangedValue] = React.useState<FieldValue>();
+ const [validationMessage, setValidationMessage] = React.useState<string>();
+ const { data: loadedSettingValue, isLoading } = useGetValueQuery(definition.key, component?.key);
+ const settingValue = isLoading ? initialSettingValue : loadedSettingValue ?? undefined;
+
+ const { mutateAsync: resetSettingValue } = useResetSettingsMutation();
+ const { mutateAsync: saveSettingValue } = useSaveValueMutation();
+
+ React.useEffect(() => () => clearTimeout(timeout.current), []);
+
+ const handleChange = (changedValue: FieldValue) => {
+ clearTimeout(timeout.current);
+
+ setChangedValue(changedValue);
+ setSuccess(false);
+ handleCheck(changedValue);
};
- handleReset = async () => {
- const { component, definition } = this.props;
-
- this.setState({ loading: true, success: false });
+ const handleReset = async () => {
+ setLoading(true);
+ setSuccess(false);
try {
- await resetSettingValue({ keys: definition.key, component: component?.key });
- const settingValue = await getValue({ key: definition.key, component: component?.key });
-
- this.setState({
- changedValue: undefined,
- loading: false,
- success: true,
- validationMessage: undefined,
- settingValue,
- });
-
- this.timeout = window.setTimeout(() => {
- this.setState({ success: false });
+ await resetSettingValue({ keys: [definition.key], component: component?.key });
+
+ setChangedValue(undefined);
+ setLoading(false);
+ setSuccess(true);
+ setValidationMessage(undefined);
+
+ timeout.current = window.setTimeout(() => {
+ setSuccess(false);
}, SAFE_SET_STATE_DELAY);
} catch (e) {
const validationMessage = await parseError(e as Response);
- this.setState({ loading: false, validationMessage });
+ setLoading(false);
+ setValidationMessage(validationMessage);
}
};
- handleCancel = () => {
- this.setState({ changedValue: undefined, validationMessage: undefined, isEditing: false });
+ const handleCancel = () => {
+ setChangedValue(undefined);
+ setValidationMessage(undefined);
+ setIsEditing(false);
};
- handleCheck = () => {
- const { definition } = this.props;
- const { changedValue } = this.state;
-
- if (isEmptyValue(definition, changedValue)) {
+ const handleCheck = (value?: FieldValue) => {
+ if (isEmptyValue(definition, value)) {
if (definition.defaultValue === undefined) {
- this.setState({
- validationMessage: translate('settings.state.value_cant_be_empty_no_default'),
- });
+ setValidationMessage(translate('settings.state.value_cant_be_empty_no_default'));
} else {
- this.setState({ validationMessage: translate('settings.state.value_cant_be_empty') });
+ setValidationMessage(translate('settings.state.value_cant_be_empty'));
}
return false;
}
@@ -121,85 +114,122 @@ export default class Definition extends React.PureComponent<Props, State> {
if (isURLKind(definition)) {
try {
// eslint-disable-next-line no-new
- new URL(changedValue ?? '');
+ new URL(value?.toString() ?? '');
} catch (e) {
- this.setState({
- validationMessage: translateWithParameters(
- 'settings.state.url_not_valid',
- changedValue ?? '',
- ),
- });
+ setValidationMessage(
+ translateWithParameters('settings.state.url_not_valid', value?.toString() ?? ''),
+ );
return false;
}
}
if (definition.type === SettingType.JSON) {
try {
- JSON.parse(changedValue ?? '');
+ JSON.parse(value?.toString() ?? '');
} catch (e) {
- this.setState({ validationMessage: (e as Error).message });
+ setValidationMessage((e as Error).message);
return false;
}
}
- this.setState({ validationMessage: undefined });
+ setValidationMessage(undefined);
return true;
};
- handleEditing = () => {
- this.setState({ isEditing: true });
- };
-
- handleSave = async () => {
- const { component, definition } = this.props;
- const { changedValue } = this.state;
-
+ const handleSave = async () => {
if (changedValue !== undefined) {
- this.setState({ success: false });
+ setSuccess(false);
if (isEmptyValue(definition, changedValue)) {
- this.setState({ validationMessage: translate('settings.state.value_cant_be_empty') });
+ setValidationMessage(translate('settings.state.value_cant_be_empty'));
return;
}
- this.setState({ loading: true });
+ setLoading(true);
try {
- await setSettingValue(definition, changedValue, component?.key);
- const settingValue = await getValue({ key: definition.key, component: component?.key });
-
- this.setState({
- changedValue: undefined,
- isEditing: false,
- loading: false,
- success: true,
- settingValue,
- });
-
- this.timeout = window.setTimeout(() => {
- this.setState({ success: false });
+ await saveSettingValue({ definition, newValue: changedValue, component: component?.key });
+
+ setChangedValue(undefined);
+ setIsEditing(false);
+ setLoading(false);
+ setSuccess(true);
+
+ timeout.current = window.setTimeout(() => {
+ setSuccess(false);
}, SAFE_SET_STATE_DELAY);
} catch (e) {
const validationMessage = await parseError(e as Response);
- this.setState({ loading: false, validationMessage });
+ setLoading(false);
+ setValidationMessage(validationMessage);
}
}
};
- render() {
- const { definition } = this.props;
- return (
- <DefinitionRenderer
- definition={definition}
- onCancel={this.handleCancel}
- onChange={this.handleChange}
- onEditing={this.handleEditing}
- onReset={this.handleReset}
- onSave={this.handleSave}
- {...this.state}
- />
- );
- }
+ const hasError = validationMessage != null;
+ const hasValueChanged = changedValue != null;
+ const effectiveValue = hasValueChanged ? changedValue : getSettingValue(definition, settingValue);
+ const isDefault = isDefaultOrInherited(settingValue);
+
+ const settingDefinitionAndValue = combineDefinitionAndSettingValue(definition, settingValue);
+
+ return (
+ <div data-key={definition.key} data-testid={definition.key} className="sw-flex sw-gap-12">
+ <DefinitionDescription definition={definition} />
+
+ <div className="sw-flex-1">
+ <form onSubmit={formNoop}>
+ <Input
+ hasValueChanged={hasValueChanged}
+ onCancel={handleCancel}
+ onChange={handleChange}
+ onSave={handleSave}
+ onEditing={() => setIsEditing(true)}
+ isEditing={isEditing}
+ isInvalid={hasError}
+ setting={settingDefinitionAndValue}
+ value={effectiveValue}
+ />
+
+ <div className="sw-mt-2">
+ {loading && (
+ <div className="sw-flex">
+ <Spinner />
+ <Note className="sw-ml-2">{translate('settings.state.saving')}</Note>
+ </div>
+ )}
+
+ {!loading && validationMessage && (
+ <div>
+ <TextError
+ text={translateWithParameters(
+ 'settings.state.validation_failed',
+ validationMessage,
+ )}
+ />
+ </div>
+ )}
+
+ {!loading && !hasError && success && (
+ <FlagMessage variant="success">{translate('settings.state.saved')}</FlagMessage>
+ )}
+ </div>
+
+ <DefinitionActions
+ changedValue={changedValue}
+ hasError={hasError}
+ hasValueChanged={hasValueChanged}
+ isDefault={isDefault}
+ isEditing={isEditing}
+ onCancel={handleCancel}
+ onReset={handleReset}
+ onSave={handleSave}
+ setting={settingDefinitionAndValue}
+ />
+ </form>
+ </div>
+ </div>
+ );
}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/DefinitionActions.tsx b/server/sonar-web/src/main/js/apps/settings/components/DefinitionActions.tsx
index a3d31d5fd0e..158dce175b5 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/DefinitionActions.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/DefinitionActions.tsx
@@ -24,7 +24,7 @@ import { Setting } from '../../../types/settings';
import { getDefaultValue, getPropertyName, isEmptyValue } from '../utils';
type Props = {
- changedValue?: string;
+ changedValue?: string | string[] | boolean;
hasError: boolean;
hasValueChanged: boolean;
isDefault: boolean;
diff --git a/server/sonar-web/src/main/js/apps/settings/components/DefinitionRenderer.tsx b/server/sonar-web/src/main/js/apps/settings/components/DefinitionRenderer.tsx
deleted file mode 100644
index bb2a71cfa3e..00000000000
--- a/server/sonar-web/src/main/js/apps/settings/components/DefinitionRenderer.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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 { FlagMessage, Note, Spinner, TextError } from 'design-system';
-import * as React from 'react';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { ExtendedSettingDefinition, SettingValue } from '../../../types/settings';
-import { combineDefinitionAndSettingValue, getSettingValue, isDefaultOrInherited } from '../utils';
-import DefinitionActions from './DefinitionActions';
-import DefinitionDescription from './DefinitionDescription';
-import Input from './inputs/Input';
-
-export interface DefinitionRendererProps {
- definition: ExtendedSettingDefinition;
- changedValue?: string;
- loading: boolean;
- success: boolean;
- validationMessage?: string;
- settingValue?: SettingValue;
- isEditing: boolean;
- onCancel: () => void;
- onChange: (value: any) => void;
- onEditing: () => void;
- onSave: () => void;
- onReset: () => void;
-}
-
-const formNoop = (e: React.FormEvent<HTMLFormElement>) => e.preventDefault();
-
-export default function DefinitionRenderer(props: Readonly<DefinitionRendererProps>) {
- const { changedValue, loading, validationMessage, settingValue, success, definition, isEditing } =
- props;
-
- const hasError = validationMessage != null;
- const hasValueChanged = changedValue != null;
- const effectiveValue = hasValueChanged ? changedValue : getSettingValue(definition, settingValue);
- const isDefault = isDefaultOrInherited(settingValue);
-
- const settingDefinitionAndValue = combineDefinitionAndSettingValue(definition, settingValue);
-
- return (
- <div data-key={definition.key} className="sw-flex sw-gap-12">
- <DefinitionDescription definition={definition} />
-
- <div className="sw-flex-1">
- <form onSubmit={formNoop}>
- <Input
- hasValueChanged={hasValueChanged}
- onCancel={props.onCancel}
- onChange={props.onChange}
- onSave={props.onSave}
- onEditing={props.onEditing}
- isEditing={isEditing}
- isInvalid={hasError}
- setting={settingDefinitionAndValue}
- value={effectiveValue}
- />
-
- <div className="sw-mt-2">
- {loading && (
- <div className="sw-flex">
- <Spinner />
- <Note className="sw-ml-2">{translate('settings.state.saving')}</Note>
- </div>
- )}
-
- {!loading && validationMessage && (
- <div>
- <TextError
- text={translateWithParameters(
- 'settings.state.validation_failed',
- validationMessage,
- )}
- />
- </div>
- )}
-
- {!loading && !hasError && success && (
- <FlagMessage variant="success">{translate('settings.state.saved')}</FlagMessage>
- )}
- </div>
-
- <DefinitionActions
- changedValue={changedValue}
- hasError={hasError}
- hasValueChanged={hasValueChanged}
- isDefault={isDefault}
- isEditing={isEditing}
- onCancel={props.onCancel}
- onReset={props.onReset}
- onSave={props.onSave}
- setting={settingDefinitionAndValue}
- />
- </form>
- </div>
- </div>
- );
-}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/SettingsApp-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/__tests__/SettingsApp-it.tsx
index d1795c5b788..c666234f5a7 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/SettingsApp-it.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/SettingsApp-it.tsx
@@ -45,7 +45,9 @@ afterEach(() => {
settingsMock.reset();
});
-beforeEach(jest.clearAllMocks);
+beforeEach(() => {
+ jest.clearAllMocks();
+});
const ui = {
categoryLink: (category: string) => byRole('link', { name: category }),
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 7e72e9c3721..1363b3287a3 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
@@ -25,7 +25,6 @@ import { useSearchParams } from 'react-router-dom';
import withAvailableFeatures, {
WithAvailableFeaturesProps,
} from '../../../../app/components/available-features/withAvailableFeatures';
-import DocumentationLink from '../../../../components/common/DocumentationLink';
import { getTabId, getTabPanelId } from '../../../../components/controls/BoxedTabs';
import { translate } from '../../../../helpers/l10n';
import { getBaseUrl } from '../../../../helpers/system';
@@ -33,8 +32,7 @@ import { searchParamsToQuery } from '../../../../helpers/urls';
import { AlmKeys } from '../../../../types/alm-settings';
import { Feature } from '../../../../types/features';
import { ExtendedSettingDefinition } from '../../../../types/settings';
-import { AUTHENTICATION_CATEGORY } from '../../constants';
-import CategoryDefinitionsList from '../CategoryDefinitionsList';
+import BitbucketAuthenticationTab from './BitbucketAuthenticationTab';
import GitLabAuthenticationTab from './GitLabAuthenticationTab';
import GithubAuthenticationTab from './GithubAuthenticationTab';
import SamlAuthenticationTab, { SAML } from './SamlAuthenticationTab';
@@ -108,10 +106,11 @@ export function Authentication(props: Props & WithAvailableFeaturesProps) {
},
] as const;
- const [samlDefinitions, githubDefinitions] = React.useMemo(
+ const [samlDefinitions, githubDefinitions, bitbucketDefinitions] = React.useMemo(
() => [
definitions.filter((def) => def.subCategory === SAML),
definitions.filter((def) => def.subCategory === AlmKeys.GitHub),
+ definitions.filter((def) => def.subCategory === AlmKeys.BitbucketServer),
],
[definitions],
);
@@ -171,34 +170,7 @@ export function Authentication(props: Props & WithAvailableFeaturesProps) {
{tab.value === AlmKeys.GitLab && <GitLabAuthenticationTab />}
{tab.value === AlmKeys.BitbucketServer && (
- <>
- <FlagMessage variant="info">
- <div>
- <FormattedMessage
- id="settings.authentication.help"
- defaultMessage={translate('settings.authentication.help')}
- values={{
- link: (
- <DocumentationLink
- to={`/instance-administration/authentication/${
- DOCUMENTATION_LINK_SUFFIXES[tab.value]
- }/`}
- >
- {translate('settings.authentication.help.link')}
- </DocumentationLink>
- ),
- }}
- />
- </div>
- </FlagMessage>
- <CategoryDefinitionsList
- category={AUTHENTICATION_CATEGORY}
- definitions={definitions}
- subCategory={tab.value}
- displaySubCategoryTitle={false}
- noPadding
- />
- </>
+ <BitbucketAuthenticationTab definitions={bitbucketDefinitions} />
)}
</div>
)}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/AutoProvisionningConsent.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/AutoProvisionningConsent.tsx
index 04e0c464105..14f5b85ba27 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/authentication/AutoProvisionningConsent.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/AutoProvisionningConsent.tsx
@@ -37,7 +37,7 @@ export default function AutoProvisioningConsent() {
const header = translate('settings.authentication.github.confirm_auto_provisioning.header');
const removeConsentFlag = () => {
- resetSettingsMutation.mutate([GITHUB_PERMISSION_USER_CONSENT]);
+ resetSettingsMutation.mutate({ keys: [GITHUB_PERMISSION_USER_CONSENT] });
};
const switchToJIT = async () => {
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/BitbucketAuthenticationTab.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/BitbucketAuthenticationTab.tsx
new file mode 100644
index 00000000000..6716e498fc3
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/BitbucketAuthenticationTab.tsx
@@ -0,0 +1,89 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { FlagMessage } from 'design-system';
+import React from 'react';
+import { FormattedMessage } from 'react-intl';
+import DocumentationLink from '../../../../components/common/DocumentationLink';
+import { translate } from '../../../../helpers/l10n';
+import { useGetValueQuery } from '../../../../queries/settings';
+import { AlmKeys } from '../../../../types/alm-settings';
+import { ExtendedSettingDefinition } from '../../../../types/settings';
+import { AUTHENTICATION_CATEGORY } from '../../constants';
+import CategoryDefinitionsList from '../CategoryDefinitionsList';
+
+interface Props {
+ definitions: ExtendedSettingDefinition[];
+}
+
+export default function BitbucketAuthenticationTab(props: Readonly<Props>) {
+ const { definitions } = props;
+
+ const { data: allowToSignUpEnabled } = useGetValueQuery(
+ 'sonar.auth.bitbucket.allowUsersToSignUp',
+ );
+ const { data: workspaces } = useGetValueQuery('sonar.auth.bitbucket.workspaces');
+
+ const isConfigurationUnsafe =
+ allowToSignUpEnabled?.value === 'true' &&
+ (!workspaces?.values || workspaces?.values.length === 0);
+
+ return (
+ <>
+ {isConfigurationUnsafe && (
+ <FlagMessage variant="error" className="sw-mb-2">
+ <div>
+ <FormattedMessage
+ id="settings.authentication.gitlab.configuration.insecure"
+ values={{
+ documentation: (
+ <DocumentationLink to="/instance-administration/authentication/bitbucket-cloud/#setting-your-authentication-settings-in-sonarqube">
+ {translate('documentation')}
+ </DocumentationLink>
+ ),
+ }}
+ />
+ </div>
+ </FlagMessage>
+ )}
+ <FlagMessage variant="info">
+ <div>
+ <FormattedMessage
+ id="settings.authentication.help"
+ defaultMessage={translate('settings.authentication.help')}
+ values={{
+ link: (
+ <DocumentationLink to="/instance-administration/authentication/bitbucket-cloud/">
+ {translate('settings.authentication.help.link')}
+ </DocumentationLink>
+ ),
+ }}
+ />
+ </div>
+ </FlagMessage>
+ <CategoryDefinitionsList
+ category={AUTHENTICATION_CATEGORY}
+ definitions={definitions}
+ subCategory={AlmKeys.BitbucketServer}
+ displaySubCategoryTitle={false}
+ noPadding
+ />
+ </>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Bitbucket-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Bitbucket-it.tsx
new file mode 100644
index 00000000000..8ce6a40fbcf
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Bitbucket-it.tsx
@@ -0,0 +1,105 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 userEvent from '@testing-library/user-event';
+import React from 'react';
+import SettingsServiceMock from '../../../../../api/mocks/SettingsServiceMock';
+import { AvailableFeaturesContext } from '../../../../../app/components/available-features/AvailableFeaturesContext';
+import { definitions } from '../../../../../helpers/mocks/definitions-list';
+import { renderComponent } from '../../../../../helpers/testReactTestingUtils';
+import { byRole, byTestId, byText } from '../../../../../helpers/testSelector';
+import { AlmKeys } from '../../../../../types/alm-settings';
+import { Feature } from '../../../../../types/features';
+import Authentication from '../Authentication';
+
+let settingsHandler: SettingsServiceMock;
+
+beforeEach(() => {
+ settingsHandler = new SettingsServiceMock();
+ settingsHandler.setDefinitions(definitions);
+});
+
+afterEach(() => {
+ settingsHandler.reset();
+});
+
+const enabledDefinition = byTestId('sonar.auth.bitbucket.enabled');
+const consumerKeyDefinition = byTestId('sonar.auth.bitbucket.clientId.secured');
+const consumerSecretDefinition = byTestId('sonar.auth.bitbucket.clientSecret.secured');
+const allowUsersToSignUpDefinition = byTestId('sonar.auth.bitbucket.allowUsersToSignUp');
+const workspacesDefinition = byTestId('sonar.auth.bitbucket.workspaces');
+
+const ui = {
+ save: byRole('button', { name: 'save' }),
+ cancel: byRole('button', { name: 'cancel' }),
+ reset: byRole('button', { name: /settings.definition.reset/ }),
+ confirmReset: byRole('dialog').byRole('button', { name: 'reset_verb' }),
+ change: byRole('button', { name: 'change_verb' }),
+ enabledDefinition,
+ enabled: enabledDefinition.byRole('switch'),
+ consumerKeyDefinition,
+ consumerKey: consumerKeyDefinition.byRole('textbox'),
+ consumerSecretDefinition,
+ consumerSecret: consumerSecretDefinition.byRole('textbox'),
+ allowUsersToSignUpDefinition,
+ allowUsersToSignUp: allowUsersToSignUpDefinition.byRole('switch'),
+ workspacesDefinition,
+ workspaces: workspacesDefinition.byRole('textbox'),
+ workspacesDelete: workspacesDefinition.byRole('button', {
+ name: /settings.definition.delete_value/,
+ }),
+ insecureWarning: byText(/settings.authentication.gitlab.configuration.insecure/),
+};
+
+it('should show warning if sign up is enabled and there are no workspaces', async () => {
+ renderAuthentication();
+ const user = userEvent.setup();
+
+ expect(await ui.allowUsersToSignUpDefinition.find()).toBeInTheDocument();
+ expect(ui.allowUsersToSignUp.get()).toBeChecked();
+ expect(ui.workspaces.get()).toHaveValue('');
+ expect(ui.insecureWarning.get()).toBeInTheDocument();
+
+ await user.click(ui.allowUsersToSignUp.get());
+ await user.click(ui.allowUsersToSignUpDefinition.by(ui.save).get());
+ expect(ui.allowUsersToSignUp.get()).not.toBeChecked();
+ expect(ui.insecureWarning.query()).not.toBeInTheDocument();
+
+ await user.click(ui.allowUsersToSignUp.get());
+ await user.click(ui.allowUsersToSignUpDefinition.by(ui.save).get());
+ expect(ui.allowUsersToSignUp.get()).toBeChecked();
+ expect(await ui.insecureWarning.find()).toBeInTheDocument();
+
+ await user.type(ui.workspaces.get(), 'test');
+ await user.click(ui.workspacesDefinition.by(ui.save).get());
+ expect(ui.insecureWarning.query()).not.toBeInTheDocument();
+
+ await user.click(ui.workspacesDefinition.by(ui.reset).get());
+ await user.click(ui.confirmReset.get());
+ expect(await ui.insecureWarning.find()).toBeInTheDocument();
+});
+
+function renderAuthentication(features: Feature[] = []) {
+ renderComponent(
+ <AvailableFeaturesContext.Provider value={features}>
+ <Authentication definitions={definitions} />
+ </AvailableFeaturesContext.Provider>,
+ `?tab=${AlmKeys.BitbucketServer}`,
+ );
+}
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
index e643ee29038..edd550dc35c 100644
--- 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
@@ -113,7 +113,7 @@ export default function useConfiguration(
const deleteMutation = update(
useResetSettingsMutation(),
'mutate',
- (mutate) => () => mutate(Object.keys(values)),
+ (mutate) => () => mutate({ keys: Object.keys(values) }),
) as Omit<UseMutationResult<void, unknown, void, unknown>, 'mutateAsync'>;
const isValueChange = useCallback(
diff --git a/server/sonar-web/src/main/js/queries/settings.ts b/server/sonar-web/src/main/js/queries/settings.ts
index 0ecaa2a37fe..f61420dcbac 100644
--- a/server/sonar-web/src/main/js/queries/settings.ts
+++ b/server/sonar-web/src/main/js/queries/settings.ts
@@ -31,18 +31,22 @@ export function useGetValuesQuery(keys: string[]) {
});
}
-export function useGetValueQuery(key: string) {
+export function useGetValueQuery(key: string, component?: string) {
return useQuery(['settings', 'details', key] as const, ({ queryKey: [_a, _b, key] }) => {
- return getValue({ key }).then((v) => v ?? null);
+ return getValue({ key, component }).then((v) => v ?? null);
});
}
export function useResetSettingsMutation() {
const queryClient = useQueryClient();
return useMutation({
- mutationFn: (keys: string[]) => resetSettingValue({ keys: keys.join(',') }),
- onSuccess: () => {
- queryClient.invalidateQueries(['settings']);
+ mutationFn: ({ keys, component }: { keys: string[]; component?: string }) =>
+ resetSettingValue({ keys: keys.join(','), component }),
+ onSuccess: (_, { keys }) => {
+ keys.forEach((key) => {
+ queryClient.invalidateQueries(['settings', 'details', key]);
+ });
+ queryClient.invalidateQueries(['settings', 'values']);
},
});
}
@@ -75,7 +79,10 @@ export function useSaveValuesMutation() {
},
onSuccess: (data) => {
if (data.length > 0) {
- queryClient.invalidateQueries(['settings']);
+ data.forEach(({ key }) => {
+ queryClient.invalidateQueries(['settings', 'details', key]);
+ });
+ queryClient.invalidateQueries(['settings', 'values']);
addGlobalSuccessMessage(translate('settings.authentication.form.settings.save_success'));
}
},
@@ -85,21 +92,23 @@ export function useSaveValuesMutation() {
export function useSaveValueMutation() {
const queryClient = useQueryClient();
return useMutation({
- mutationFn: async ({
+ mutationFn: ({
newValue,
definition,
+ component,
}: {
newValue: SettingValue;
definition: ExtendedSettingDefinition;
+ component?: string;
}) => {
if (isDefaultValue(newValue, definition)) {
- await resetSettingValue({ keys: definition.key });
- } else {
- await setSettingValue(definition, newValue);
+ return resetSettingValue({ keys: definition.key, component });
}
+ return setSettingValue(definition, newValue, component);
},
- onSuccess: () => {
- queryClient.invalidateQueries(['settings']);
+ onSuccess: (_, { definition }) => {
+ queryClient.invalidateQueries(['settings', 'details', definition.key]);
+ queryClient.invalidateQueries(['settings', 'values']);
addGlobalSuccessMessage(translate('settings.authentication.form.settings.save_success'));
},
});