Sfoglia il codice sorgente

SONAR-21507 Show a warning for Bitbucket Authentication in case of insecure config

tags/10.4.0.87286
Viktor Vorona 4 mesi fa
parent
commit
cab06a5a89

+ 34
- 5
server/sonar-web/src/main/js/api/mocks/SettingsServiceMock.ts Vedi File

@@ -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 });
};

+ 149
- 119
server/sonar-web/src/main/js/apps/settings/components/Definition.tsx Vedi File

@@ -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>
);
}

+ 1
- 1
server/sonar-web/src/main/js/apps/settings/components/DefinitionActions.tsx Vedi File

@@ -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;

+ 0
- 114
server/sonar-web/src/main/js/apps/settings/components/DefinitionRenderer.tsx Vedi File

@@ -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>
);
}

+ 3
- 1
server/sonar-web/src/main/js/apps/settings/components/__tests__/SettingsApp-it.tsx Vedi File

@@ -45,7 +45,9 @@ afterEach(() => {
settingsMock.reset();
});

beforeEach(jest.clearAllMocks);
beforeEach(() => {
jest.clearAllMocks();
});

const ui = {
categoryLink: (category: string) => byRole('link', { name: category }),

+ 4
- 32
server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx Vedi File

@@ -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>
)}

+ 1
- 1
server/sonar-web/src/main/js/apps/settings/components/authentication/AutoProvisionningConsent.tsx Vedi File

@@ -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 () => {

+ 89
- 0
server/sonar-web/src/main/js/apps/settings/components/authentication/BitbucketAuthenticationTab.tsx Vedi File

@@ -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
/>
</>
);
}

+ 105
- 0
server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Bitbucket-it.tsx Vedi File

@@ -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}`,
);
}

+ 1
- 1
server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useConfiguration.ts Vedi File

@@ -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(

+ 21
- 12
server/sonar-web/src/main/js/queries/settings.ts Vedi File

@@ -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'));
},
});

+ 3
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties Vedi File

@@ -1616,6 +1616,9 @@ settings.authentication.gitlab.configuration.unsaved_changes=You have unsaved ch
settings.authentication.gitlab.configuration.valid.JIT=Configuration is valid for Just-in-Time provisioning.
settings.authentication.gitlab.configuration.valid.AUTO_PROVISIONING=Configuration is valid for Automatic provisioning.

# BITBUCKET
settings.authentication.gitlab.configuration.insecure=BitBucket Authentication allows users to sign up, but no list of allowed workspaces was provided. This is potentially insecure. We recommend entering a list of allowed workspaces. {documentation}

# COMMON
settings.authentication.configuration.validity_check_loading=Checking the configuration
settings.authentication.configuration.test=Test configuration

Loading…
Annulla
Salva