* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { cloneDeep } from 'lodash';
+import { cloneDeep, omit } from 'lodash';
+import { mockGitlabConfiguration } from '../../helpers/mocks/alm-integrations';
import { mockTask } from '../../helpers/mocks/tasks';
+import { mockPaging } from '../../helpers/testMocks';
import {
GitHubConfigurationStatus,
GitHubMapping,
GitHubProvisioningStatus,
+ GitlabConfiguration,
} from '../../types/provisioning';
import { Task, TaskStatuses, TaskTypes } from '../../types/tasks';
import {
activateScim,
addGithubRolesMapping,
checkConfigurationValidity,
+ createGitLabConfiguration,
deactivateGithubProvisioning,
deactivateScim,
+ deleteGitLabConfiguration,
deleteGithubRolesMapping,
+ fetchGitLabConfiguration,
+ fetchGitLabConfigurations,
fetchGithubProvisioningStatus,
fetchGithubRolesMapping,
fetchIsScimEnabled,
+ updateGitLabConfiguration,
updateGithubRolesMapping,
} from '../provisioning';
],
};
+const defaultGitlabConfiguration: GitlabConfiguration[] = [
+ mockGitlabConfiguration({ id: '1', enabled: true }),
+];
+
const githubMappingMock = (
id: string,
permissions: (keyof GitHubMapping['permissions'])[],
githubConfigurationStatus: GitHubConfigurationStatus;
githubMapping: GitHubMapping[];
tasks: Task[];
+ gitlabConfigurations: GitlabConfiguration[];
constructor() {
this.scimStatus = false;
this.githubConfigurationStatus = cloneDeep(defaultConfigurationStatus);
this.githubMapping = cloneDeep(defaultMapping);
this.tasks = [];
+ this.gitlabConfigurations = cloneDeep(defaultGitlabConfiguration);
jest.mocked(activateScim).mockImplementation(this.handleActivateScim);
jest.mocked(deactivateScim).mockImplementation(this.handleDeactivateScim);
jest.mocked(fetchIsScimEnabled).mockImplementation(this.handleFetchIsScimEnabled);
jest.mocked(updateGithubRolesMapping).mockImplementation(this.handleUpdateGithubRolesMapping);
jest.mocked(addGithubRolesMapping).mockImplementation(this.handleAddGithubRolesMapping);
jest.mocked(deleteGithubRolesMapping).mockImplementation(this.handleDeleteGithubRolesMapping);
+ jest.mocked(fetchGitLabConfigurations).mockImplementation(this.handleFetchGitLabConfigurations);
+ jest.mocked(fetchGitLabConfiguration).mockImplementation(this.handleFetchGitLabConfiguration);
+ jest.mocked(createGitLabConfiguration).mockImplementation(this.handleCreateGitLabConfiguration);
+ jest.mocked(updateGitLabConfiguration).mockImplementation(this.handleUpdateGitLabConfiguration);
+ jest.mocked(deleteGitLabConfiguration).mockImplementation(this.handleDeleteGitLabConfiguration);
}
addProvisioningTask = (overrides: Partial<Omit<Task, 'type'>> = {}) => {
this.githubMapping = [...this.githubMapping, githubMappingMock(id, permissions)];
};
+ handleFetchGitLabConfigurations: typeof fetchGitLabConfigurations = () => {
+ return Promise.resolve({
+ configurations: this.gitlabConfigurations,
+ page: mockPaging({ total: this.gitlabConfigurations.length }),
+ });
+ };
+
+ handleFetchGitLabConfiguration: typeof fetchGitLabConfiguration = (id: string) => {
+ const configuration = this.gitlabConfigurations.find((c) => c.id === id);
+ if (!configuration) {
+ return Promise.reject();
+ }
+ return Promise.resolve(configuration);
+ };
+
+ handleCreateGitLabConfiguration: typeof createGitLabConfiguration = (data) => {
+ const newConfig = mockGitlabConfiguration({
+ ...omit(data, 'applicationId', 'clientSecret'),
+ id: '1',
+ enabled: true,
+ });
+ this.gitlabConfigurations = [...this.gitlabConfigurations, newConfig];
+ return Promise.resolve(newConfig);
+ };
+
+ handleUpdateGitLabConfiguration: typeof updateGitLabConfiguration = (id, data) => {
+ const index = this.gitlabConfigurations.findIndex((c) => c.id === id);
+ this.gitlabConfigurations[index] = { ...this.gitlabConfigurations[index], ...data };
+ return Promise.resolve(this.gitlabConfigurations[index]);
+ };
+
+ handleDeleteGitLabConfiguration: typeof deleteGitLabConfiguration = (id) => {
+ this.gitlabConfigurations = this.gitlabConfigurations.filter((c) => c.id !== id);
+ return Promise.resolve();
+ };
+
+ setGitlabConfigurations = (gitlabConfigurations: GitlabConfiguration[]) => {
+ this.gitlabConfigurations = gitlabConfigurations;
+ };
+
reset = () => {
this.scimStatus = false;
this.githubProvisioningStatus = false;
this.githubConfigurationStatus = cloneDeep(defaultConfigurationStatus);
this.githubMapping = cloneDeep(defaultMapping);
this.tasks = [];
+ this.gitlabConfigurations = cloneDeep(defaultGitlabConfiguration);
};
}
*/
import { cloneDeep } from 'lodash';
-import { Provider } from '../../components/hooks/useManageProvider';
import {
mockGroup,
mockIdentityProvider,
mockPaging,
mockUserGroupMember,
} from '../../helpers/testMocks';
-import { Group, IdentityProvider, Paging } from '../../types/types';
+import { Group, IdentityProvider, Paging, Provider } from '../../types/types';
import { createGroup, deleteGroup, getUsersGroups, updateGroup } from '../user_groups';
jest.mock('../user_groups');
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { cloneDeep } from 'lodash';
-import { SysInfoCluster, SysInfoLogging, SysInfoStandalone } from '../../types/types';
+import { Provider, SysInfoCluster, SysInfoLogging, SysInfoStandalone } from '../../types/types';
import { LogsLevels } from '../../apps/system/utils';
-import { Provider } from '../../components/hooks/useManageProvider';
import { mockClusterSysInfo, mockLogs, mockStandaloneSysInfo } from '../../helpers/testMocks';
import { getSystemInfo, setLogLevel } from '../system';
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import axios from 'axios';
+import { keyBy } from 'lodash';
import { throwGlobalError } from '../helpers/error';
import { getJSON, post, postJSON } from '../helpers/request';
-import { GitHubConfigurationStatus, GitHubMapping, GithubStatus } from '../types/provisioning';
+import {
+ GitHubConfigurationStatus,
+ GitHubMapping,
+ GitLabConfigurationCreateBody,
+ GitLabConfigurationUpdateBody,
+ GithubStatus,
+ GitlabConfiguration,
+ ProvisioningType,
+} from '../types/provisioning';
+import { Paging } from '../types/types';
+import { getValues, resetSettingValue, setSimpleSettingValue } from './settings';
const GITHUB_PERMISSION_MAPPINGS = '/api/v2/dop-translation/github-permission-mappings';
export function deleteGithubRolesMapping(role: string) {
return axios.delete(`${GITHUB_PERMISSION_MAPPINGS}/${encodeURIComponent(role)}`);
}
+
+const GITLAB_SETTING_ENABLED = 'sonar.auth.gitlab.enabled';
+const GITLAB_SETTING_URL = 'sonar.auth.gitlab.url';
+const GITLAB_SETTING_APP_ID = 'sonar.auth.gitlab.applicationId.secured';
+const GITLAB_SETTING_SECRET = 'sonar.auth.gitlab.secret.secured';
+export const GITLAB_SETTING_ALLOW_SIGNUP = 'sonar.auth.gitlab.allowUsersToSignUp';
+const GITLAB_SETTING_GROUPS_SYNC = 'sonar.auth.gitlab.groupsSync';
+const GITLAB_SETTING_PROVISIONING_ENABLED = 'provisioning.gitlab.enabled';
+export const GITLAB_SETTING_GROUP_TOKEN = 'provisioning.gitlab.token.secured';
+export const GITLAB_SETTING_GROUPS = 'provisioning.gitlab.groups';
+
+const gitlabKeys = [
+ GITLAB_SETTING_ENABLED,
+ GITLAB_SETTING_URL,
+ GITLAB_SETTING_APP_ID,
+ GITLAB_SETTING_SECRET,
+ GITLAB_SETTING_ALLOW_SIGNUP,
+ GITLAB_SETTING_GROUPS_SYNC,
+ GITLAB_SETTING_PROVISIONING_ENABLED,
+ GITLAB_SETTING_GROUP_TOKEN,
+ GITLAB_SETTING_GROUPS,
+];
+
+const fieldKeyMap = {
+ enabled: GITLAB_SETTING_ENABLED,
+ url: GITLAB_SETTING_URL,
+ applicationId: GITLAB_SETTING_APP_ID,
+ clientSecret: GITLAB_SETTING_SECRET,
+ allowUsersToSignUp: GITLAB_SETTING_ALLOW_SIGNUP,
+ synchronizeUserGroups: GITLAB_SETTING_GROUPS_SYNC,
+ type: GITLAB_SETTING_PROVISIONING_ENABLED,
+ provisioningToken: GITLAB_SETTING_GROUP_TOKEN,
+ groups: GITLAB_SETTING_GROUPS,
+};
+
+const getGitLabConfiguration = async (): Promise<GitlabConfiguration | null> => {
+ const values = await getValues({
+ keys: gitlabKeys,
+ });
+ const valuesMap = keyBy(values, 'key');
+ if (!valuesMap[GITLAB_SETTING_APP_ID] || !valuesMap[GITLAB_SETTING_SECRET]) {
+ return null;
+ }
+ return {
+ id: '1',
+ enabled: valuesMap[GITLAB_SETTING_ENABLED]?.value === 'true',
+ url: valuesMap[GITLAB_SETTING_URL]?.value ?? 'https://gitlab.com',
+ synchronizeUserGroups: valuesMap[GITLAB_SETTING_GROUPS_SYNC]?.value === 'true',
+ type:
+ valuesMap[GITLAB_SETTING_PROVISIONING_ENABLED]?.value === 'true'
+ ? ProvisioningType.auto
+ : ProvisioningType.jit,
+ groups: valuesMap[GITLAB_SETTING_GROUPS]?.values
+ ? valuesMap[GITLAB_SETTING_GROUPS]?.values
+ : [],
+ allowUsersToSignUp: valuesMap[GITLAB_SETTING_ALLOW_SIGNUP]?.value === 'true',
+ };
+};
+
+export async function fetchGitLabConfigurations(): Promise<{
+ configurations: GitlabConfiguration[];
+ page: Paging;
+}> {
+ const config = await getGitLabConfiguration();
+ return {
+ configurations: config ? [config] : [],
+ page: {
+ pageIndex: 1,
+ pageSize: 1,
+ total: config ? 1 : 0,
+ },
+ };
+}
+
+export async function fetchGitLabConfiguration(_id: string): Promise<GitlabConfiguration> {
+ const configuration = await getGitLabConfiguration();
+ if (!configuration) {
+ return Promise.reject(new Error('GitLab configuration not found'));
+ }
+ return Promise.resolve(configuration);
+}
+
+export async function createGitLabConfiguration(
+ configuration: GitLabConfigurationCreateBody,
+): Promise<GitlabConfiguration> {
+ await Promise.all(
+ Object.entries(configuration).map(
+ ([key, value]: [key: keyof GitLabConfigurationCreateBody, value: string]) =>
+ setSimpleSettingValue({ key: fieldKeyMap[key], value }),
+ ),
+ );
+ await setSimpleSettingValue({ key: fieldKeyMap.enabled, value: 'true' });
+ return fetchGitLabConfiguration('');
+}
+
+export async function updateGitLabConfiguration(
+ _id: string,
+ configuration: Partial<GitLabConfigurationUpdateBody>,
+): Promise<GitlabConfiguration> {
+ await Promise.all(
+ Object.entries(configuration).map(
+ ([key, value]: [key: keyof typeof fieldKeyMap, value: string | string[]]) => {
+ if (fieldKeyMap[key] === GITLAB_SETTING_PROVISIONING_ENABLED) {
+ return setSimpleSettingValue({
+ key: fieldKeyMap[key],
+ value: value === ProvisioningType.auto ? 'true' : 'false',
+ });
+ } else if (typeof value === 'boolean') {
+ return setSimpleSettingValue({ key: fieldKeyMap[key], value: value ? 'true' : 'false' });
+ } else if (Array.isArray(value)) {
+ return setSimpleSettingValue({ key: fieldKeyMap[key], values: value });
+ }
+ return setSimpleSettingValue({ key: fieldKeyMap[key], value });
+ },
+ ),
+ );
+ return fetchGitLabConfiguration('');
+}
+
+export function deleteGitLabConfiguration(_id: string): Promise<void> {
+ return resetSettingValue({ keys: gitlabKeys.join(',') });
+}
}
export function setSimpleSettingValue(
- data: { component?: string; value: string; key: string } & BranchParameters,
+ data: { component?: string; value?: string; values?: string[]; key: string } & BranchParameters,
): Promise<void | Response> {
return post('/api/settings/set', data).catch(throwGlobalError);
}
--- /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 { formatDistance } from 'date-fns';
+import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import Link from '../../components/common/Link';
+import CheckIcon from '../../components/icons/CheckIcon';
+import WarningIcon from '../../components/icons/WarningIcon';
+import { Alert } from '../../components/ui/Alert';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+import { AlmSyncStatus } from '../../types/provisioning';
+import { TaskStatuses } from '../../types/tasks';
+import './SystemAnnouncement.css';
+
+interface SynchronisationWarningProps {
+ short?: boolean;
+ data: AlmSyncStatus;
+}
+
+interface LastSyncProps {
+ short?: boolean;
+ info: AlmSyncStatus['lastSync'];
+}
+
+function LastSyncAlert({ info, short }: Readonly<LastSyncProps>) {
+ if (info === undefined) {
+ return null;
+ }
+ const { finishedAt, errorMessage, status, summary, warningMessage } = info;
+
+ const formattedDate = finishedAt ? formatDistance(new Date(finishedAt), new Date()) : '';
+
+ if (short) {
+ return status === TaskStatuses.Success ? (
+ <div>
+ <span className="authentication-enabled spacer-left">
+ {warningMessage ? (
+ <WarningIcon className="spacer-right" />
+ ) : (
+ <CheckIcon className="spacer-right" />
+ )}
+ </span>
+ <i>
+ {warningMessage ? (
+ <FormattedMessage
+ id="settings.authentication.github.synchronization_successful.with_warning"
+ defaultMessage={translate(
+ 'settings.authentication.github.synchronization_successful.with_warning',
+ )}
+ values={{
+ date: formattedDate,
+ details: (
+ <Link to="/admin/settings?category=authentication&tab=github">
+ {translate('settings.authentication.github.synchronization_details_link')}
+ </Link>
+ ),
+ }}
+ />
+ ) : (
+ translateWithParameters(
+ 'settings.authentication.github.synchronization_successful',
+ formattedDate,
+ )
+ )}
+ </i>
+ </div>
+ ) : (
+ <Alert variant="error">
+ <FormattedMessage
+ id="settings.authentication.github.synchronization_failed_short"
+ defaultMessage={translate('settings.authentication.github.synchronization_failed_short')}
+ values={{
+ details: (
+ <Link to="/admin/settings?category=authentication&tab=github">
+ {translate('settings.authentication.github.synchronization_details_link')}
+ </Link>
+ ),
+ }}
+ />
+ </Alert>
+ );
+ }
+
+ return (
+ <>
+ <Alert
+ variant={status === TaskStatuses.Success ? 'success' : 'error'}
+ role="alert"
+ aria-live="assertive"
+ >
+ {status === TaskStatuses.Success ? (
+ <>
+ {translateWithParameters(
+ 'settings.authentication.github.synchronization_successful',
+ formattedDate,
+ )}
+ <br />
+ {summary ?? ''}
+ </>
+ ) : (
+ <React.Fragment key={`synch-alert-${finishedAt}`}>
+ <div>
+ {translateWithParameters(
+ 'settings.authentication.github.synchronization_failed',
+ formattedDate,
+ )}
+ </div>
+ <br />
+ {errorMessage ?? ''}
+ </React.Fragment>
+ )}
+ </Alert>
+ <Alert variant="warning" role="alert" aria-live="assertive">
+ {warningMessage}
+ </Alert>
+ </>
+ );
+}
+
+export default function AlmSynchronisationWarning({
+ short,
+ data,
+}: Readonly<SynchronisationWarningProps>) {
+ return (
+ <>
+ <Alert
+ variant="loading"
+ className="spacer-bottom"
+ aria-atomic
+ role="alert"
+ aria-live="assertive"
+ aria-label={
+ data.nextSync === undefined
+ ? translate('settings.authentication.github.synchronization_finish')
+ : ''
+ }
+ >
+ {!short &&
+ data?.nextSync &&
+ translate(
+ data.nextSync.status === TaskStatuses.Pending
+ ? 'settings.authentication.github.synchronization_pending'
+ : 'settings.authentication.github.synchronization_in_progress',
+ )}
+ </Alert>
+
+ <LastSyncAlert short={short} info={data.lastSync} />
+ </>
+ );
+}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { formatDistance } from 'date-fns';
import * as React from 'react';
-import { FormattedMessage } from 'react-intl';
-import Link from '../../components/common/Link';
-import CheckIcon from '../../components/icons/CheckIcon';
-import WarningIcon from '../../components/icons/WarningIcon';
-import { Alert } from '../../components/ui/Alert';
-import { translate, translateWithParameters } from '../../helpers/l10n';
import { useGitHubSyncStatusQuery } from '../../queries/identity-provider';
-import { GithubStatusEnabled } from '../../types/provisioning';
-import { TaskStatuses } from '../../types/tasks';
+import AlmSynchronisationWarning from './AlmSynchronisationWarning';
import './SystemAnnouncement.css';
-interface LastSyncProps {
+interface Props {
short?: boolean;
- info: GithubStatusEnabled['lastSync'];
}
-interface GitHubSynchronisationWarningProps {
- short?: boolean;
-}
-
-function LastSyncAlert({ info, short }: LastSyncProps) {
- if (info === undefined) {
- return null;
- }
- const { finishedAt, errorMessage, status, summary, warningMessage } = info;
-
- const formattedDate = finishedAt ? formatDistance(new Date(finishedAt), new Date()) : '';
-
- if (short) {
- return status === TaskStatuses.Success ? (
- <div>
- <span className="authentication-enabled spacer-left">
- {warningMessage ? (
- <WarningIcon className="spacer-right" />
- ) : (
- <CheckIcon className="spacer-right" />
- )}
- </span>
- <i>
- {warningMessage ? (
- <FormattedMessage
- id="settings.authentication.github.synchronization_successful.with_warning"
- defaultMessage={translate(
- 'settings.authentication.github.synchronization_successful.with_warning',
- )}
- values={{
- date: formattedDate,
- details: (
- <Link to="/admin/settings?category=authentication&tab=github">
- {translate('settings.authentication.github.synchronization_details_link')}
- </Link>
- ),
- }}
- />
- ) : (
- translateWithParameters(
- 'settings.authentication.github.synchronization_successful',
- formattedDate,
- )
- )}
- </i>
- </div>
- ) : (
- <Alert variant="error">
- <FormattedMessage
- id="settings.authentication.github.synchronization_failed_short"
- defaultMessage={translate('settings.authentication.github.synchronization_failed_short')}
- values={{
- details: (
- <Link to="/admin/settings?category=authentication&tab=github">
- {translate('settings.authentication.github.synchronization_details_link')}
- </Link>
- ),
- }}
- />
- </Alert>
- );
- }
-
- return (
- <>
- <Alert
- variant={status === TaskStatuses.Success ? 'success' : 'error'}
- role="alert"
- aria-live="assertive"
- >
- {status === TaskStatuses.Success ? (
- <>
- {translateWithParameters(
- 'settings.authentication.github.synchronization_successful',
- formattedDate,
- )}
- <br />
- {summary ?? ''}
- </>
- ) : (
- <React.Fragment key={`synch-alert-${finishedAt}`}>
- <div>
- {translateWithParameters(
- 'settings.authentication.github.synchronization_failed',
- formattedDate,
- )}
- </div>
- <br />
- {errorMessage ?? ''}
- </React.Fragment>
- )}
- </Alert>
- <Alert variant="warning" role="alert" aria-live="assertive">
- {warningMessage}
- </Alert>
- </>
- );
-}
-
-function GitHubSynchronisationWarning({ short }: GitHubSynchronisationWarningProps) {
+function GitHubSynchronisationWarning({ short }: Readonly<Props>) {
const { data } = useGitHubSyncStatusQuery();
if (!data) {
return null;
}
- return (
- <>
- <Alert
- variant="loading"
- className="spacer-bottom"
- aria-atomic
- role="alert"
- aria-live="assertive"
- aria-label={
- data.nextSync === undefined
- ? translate('settings.authentication.github.synchronization_finish')
- : ''
- }
- >
- {!short &&
- data?.nextSync &&
- translate(
- data.nextSync.status === TaskStatuses.Pending
- ? 'settings.authentication.github.synchronization_pending'
- : 'settings.authentication.github.synchronization_in_progress',
- )}
- </Alert>
-
- <LastSyncAlert short={short} info={data.lastSync} />
- </>
- );
+ return <AlmSynchronisationWarning short={short} data={data} />;
}
export default GitHubSynchronisationWarning;
--- /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 * as React from 'react';
+import { useGitLabSyncStatusQuery } from '../../queries/identity-provider';
+import AlmSynchronisationWarning from './AlmSynchronisationWarning';
+import './SystemAnnouncement.css';
+
+interface Props {
+ short?: boolean;
+}
+
+function GitLabSynchronisationWarning({ short }: Readonly<Props>) {
+ const { data } = useGitLabSyncStatusQuery();
+
+ if (!data) {
+ return null;
+ }
+
+ return <AlmSynchronisationWarning short={short} data={data} />;
+}
+
+export default GitLabSynchronisationWarning;
import { ManagedFilter } from '../../components/controls/ManagedFilter';
import SearchBox from '../../components/controls/SearchBox';
import Suggestions from '../../components/embed-docs-modal/Suggestions';
-import { Provider, useManageProvider } from '../../components/hooks/useManageProvider';
import { translate } from '../../helpers/l10n';
import { useGroupsQueries } from '../../queries/groups';
+import { useIdentityProviderQuery } from '../../queries/identity-provider';
+import { Provider } from '../../types/types';
import Header from './components/Header';
import List from './components/List';
import './groups.css';
export default function GroupsApp() {
const [search, setSearch] = useState<string>('');
const [managed, setManaged] = useState<boolean | undefined>();
- const manageProvider = useManageProvider();
+ const { data: manageProvider } = useIdentityProviderQuery();
const { data, isLoading, fetchNextPage } = useGroupsQueries({
q: search,
<Suggestions suggestions="user_groups" />
<Helmet defer={false} title={translate('user_groups.page')} />
<main className="page page-limited" id="groups-page">
- <Header manageProvider={manageProvider} />
- {manageProvider === Provider.Github && <GitHubSynchronisationWarning short />}
+ <Header manageProvider={manageProvider?.provider} />
+ {manageProvider?.provider === Provider.Github && <GitHubSynchronisationWarning short />}
<div className="display-flex-justify-start big-spacer-bottom big-spacer-top">
<ManagedFilter
- manageProvider={manageProvider}
+ manageProvider={manageProvider?.provider}
loading={isLoading}
managed={managed}
setManaged={setManaged}
/>
</div>
- <List groups={groups} manageProvider={manageProvider} />
+ <List groups={groups} manageProvider={manageProvider?.provider} />
<div id="groups-list-footer">
<ListFooter
import GroupsServiceMock from '../../../api/mocks/GroupsServiceMock';
import SystemServiceMock from '../../../api/mocks/SystemServiceMock';
import UsersServiceMock from '../../../api/mocks/UsersServiceMock';
-import { Provider } from '../../../components/hooks/useManageProvider';
import { mockGroupMembership, mockRestUser } from '../../../helpers/testMocks';
import { renderApp } from '../../../helpers/testReactTestingUtils';
import { byRole, byText } from '../../../helpers/testSelector';
import { Feature } from '../../../types/features';
import { TaskStatuses } from '../../../types/tasks';
+import { Provider } from '../../../types/types';
import GroupsApp from '../GroupsApp';
const systemHandler = new SystemServiceMock();
it('should not be able to create a group', async () => {
renderGroupsApp();
+ expect(await ui.createGroupButton.find()).toBeInTheDocument();
expect(await ui.createGroupButton.find()).toBeDisabled();
expect(ui.infoManageMode.get()).toBeInTheDocument();
});
import { Button } from '../../../components/controls/buttons';
import { Alert } from '../../../components/ui/Alert';
import { translate } from '../../../helpers/l10n';
+import { Provider } from '../../../types/types';
import GroupForm from './GroupForm';
interface HeaderProps {
- manageProvider?: string;
+ manageProvider: Provider | undefined;
}
-export default function Header(props: HeaderProps) {
- const { manageProvider } = props;
+export default function Header({ manageProvider }: Readonly<HeaderProps>) {
const [createModal, setCreateModal] = React.useState(false);
return (
import { sortBy } from 'lodash';
import * as React from 'react';
import { translate } from '../../../helpers/l10n';
-import { Group } from '../../../types/types';
+import { Group, Provider } from '../../../types/types';
import ListItem from './ListItem';
interface Props {
groups: Group[];
- manageProvider: string | undefined;
+ manageProvider: Provider | undefined;
}
export default function List(props: Props) {
ActionsDropdownDivider,
ActionsDropdownItem,
} from '../../../components/controls/ActionsDropdown';
-import { Provider } from '../../../components/hooks/useManageProvider';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { getBaseUrl } from '../../../helpers/system';
import { useGroupMembersCountQuery } from '../../../queries/group-memberships';
-import { Group } from '../../../types/types';
+import { Group, Provider } from '../../../types/types';
import DeleteGroupForm from './DeleteGroupForm';
import GroupForm from './GroupForm';
import Members from './Members';
export interface ListItemProps {
group: Group;
- manageProvider: string | undefined;
+ manageProvider: Provider | undefined;
}
export default function ListItem(props: ListItemProps) {
import AlmSettingsServiceMock from '../../../../../api/mocks/AlmSettingsServiceMock';
import AuthenticationServiceMock from '../../../../../api/mocks/AuthenticationServiceMock';
import PermissionsServiceMock from '../../../../../api/mocks/PermissionsServiceMock';
+import SystemServiceMock from '../../../../../api/mocks/SystemServiceMock';
import { mockComponent } from '../../../../../helpers/mocks/component';
import { mockPermissionGroup, mockPermissionUser } from '../../../../../helpers/mocks/permissions';
import {
} from '../../../../../types/component';
import { Feature } from '../../../../../types/features';
import { Permissions } from '../../../../../types/permissions';
-import { Component, PermissionGroup, PermissionUser } from '../../../../../types/types';
+import { Component, PermissionGroup, PermissionUser, Provider } from '../../../../../types/types';
import { projectPermissionsRoutes } from '../../../routes';
import { getPageObject } from '../../../test-utils';
let serviceMock: PermissionsServiceMock;
let authHandler: AuthenticationServiceMock;
let almHandler: AlmSettingsServiceMock;
+let systemHandler: SystemServiceMock;
beforeAll(() => {
serviceMock = new PermissionsServiceMock();
authHandler = new AuthenticationServiceMock();
almHandler = new AlmSettingsServiceMock();
+ systemHandler = new SystemServiceMock();
});
afterEach(() => {
expect(screen.getAllByRole('row').length).toBe(21);
});
-it('should not allow to change visibility for GH Project with auto-provisioning', async () => {
- const user = userEvent.setup();
- const ui = getPageObject(user);
- authHandler.githubProvisioningStatus = true;
- almHandler.handleSetProjectBinding(AlmKeys.GitHub, {
- almSetting: 'test',
- repository: 'test',
- monorepo: false,
- project: 'my-project',
+describe('GH provisioning', () => {
+ beforeEach(() => {
+ systemHandler.setProvider(Provider.Github);
});
- renderPermissionsProjectApp({}, { featureList: [Feature.GithubProvisioning] });
- await ui.appLoaded();
- expect(ui.visibilityRadio(Visibility.Public).get()).toBeDisabled();
- expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked();
- expect(ui.visibilityRadio(Visibility.Private).get()).toBeDisabled();
- await act(async () => {
- await ui.turnProjectPrivate();
- });
- expect(ui.visibilityRadio(Visibility.Private).get()).not.toBeChecked();
-});
+ it('should not allow to change visibility for GH Project with auto-provisioning', async () => {
+ const user = userEvent.setup();
+ const ui = getPageObject(user);
+ authHandler.githubProvisioningStatus = true;
+ almHandler.handleSetProjectBinding(AlmKeys.GitHub, {
+ almSetting: 'test',
+ repository: 'test',
+ monorepo: false,
+ project: 'my-project',
+ });
+ renderPermissionsProjectApp({}, { featureList: [Feature.GithubProvisioning] });
+ await ui.appLoaded();
-it('should allow to change visibility for non-GH Project', async () => {
- const user = userEvent.setup();
- const ui = getPageObject(user);
- authHandler.githubProvisioningStatus = true;
- almHandler.handleSetProjectBinding(AlmKeys.Azure, {
- almSetting: 'test',
- repository: 'test',
- monorepo: false,
- project: 'my-project',
+ expect(ui.visibilityRadio(Visibility.Public).get()).toBeDisabled();
+ expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked();
+ expect(ui.visibilityRadio(Visibility.Private).get()).toBeDisabled();
+ await act(async () => {
+ await ui.turnProjectPrivate();
+ });
+ expect(ui.visibilityRadio(Visibility.Private).get()).not.toBeChecked();
});
- renderPermissionsProjectApp({}, { featureList: [Feature.GithubProvisioning] });
- await ui.appLoaded();
- expect(ui.visibilityRadio(Visibility.Public).get()).not.toHaveClass('disabled');
- expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked();
- expect(ui.visibilityRadio(Visibility.Private).get()).not.toHaveClass('disabled');
- await act(async () => {
- await ui.turnProjectPrivate();
- });
- expect(ui.visibilityRadio(Visibility.Private).get()).toBeChecked();
-});
+ it('should allow to change visibility for non-GH Project', async () => {
+ const user = userEvent.setup();
+ const ui = getPageObject(user);
+ authHandler.githubProvisioningStatus = true;
+ almHandler.handleSetProjectBinding(AlmKeys.Azure, {
+ almSetting: 'test',
+ repository: 'test',
+ monorepo: false,
+ project: 'my-project',
+ });
+ renderPermissionsProjectApp({}, { featureList: [Feature.GithubProvisioning] });
+ await ui.appLoaded();
-it('should allow to change visibility for GH Project with disabled auto-provisioning', async () => {
- const user = userEvent.setup();
- const ui = getPageObject(user);
- authHandler.githubProvisioningStatus = false;
- almHandler.handleSetProjectBinding(AlmKeys.GitHub, {
- almSetting: 'test',
- repository: 'test',
- monorepo: false,
- project: 'my-project',
+ expect(ui.visibilityRadio(Visibility.Public).get()).not.toHaveClass('disabled');
+ expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked();
+ expect(ui.visibilityRadio(Visibility.Private).get()).not.toHaveClass('disabled');
+ await act(async () => {
+ await ui.turnProjectPrivate();
+ });
+ expect(ui.visibilityRadio(Visibility.Private).get()).toBeChecked();
});
- renderPermissionsProjectApp({}, { featureList: [Feature.GithubProvisioning] });
- await ui.appLoaded();
- expect(ui.visibilityRadio(Visibility.Public).get()).not.toHaveClass('disabled');
- expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked();
- expect(ui.visibilityRadio(Visibility.Private).get()).not.toHaveClass('disabled');
- await act(async () => {
- await ui.turnProjectPrivate();
- });
- expect(ui.visibilityRadio(Visibility.Private).get()).toBeChecked();
-});
+ it('should allow to change visibility for GH Project with disabled auto-provisioning', async () => {
+ const user = userEvent.setup();
+ const ui = getPageObject(user);
+ authHandler.githubProvisioningStatus = false;
+ almHandler.handleSetProjectBinding(AlmKeys.GitHub, {
+ almSetting: 'test',
+ repository: 'test',
+ monorepo: false,
+ project: 'my-project',
+ });
+ renderPermissionsProjectApp({}, { featureList: [Feature.GithubProvisioning] });
+ await ui.appLoaded();
-it('should have disabled permissions for GH Project', async () => {
- const user = userEvent.setup();
- const ui = getPageObject(user);
- authHandler.githubProvisioningStatus = true;
- almHandler.handleSetProjectBinding(AlmKeys.GitHub, {
- almSetting: 'test',
- repository: 'test',
- monorepo: false,
- project: 'my-project',
+ expect(ui.visibilityRadio(Visibility.Public).get()).not.toHaveClass('disabled');
+ expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked();
+ expect(ui.visibilityRadio(Visibility.Private).get()).not.toHaveClass('disabled');
+ await act(async () => {
+ await ui.turnProjectPrivate();
+ });
+ expect(ui.visibilityRadio(Visibility.Private).get()).toBeChecked();
});
- renderPermissionsProjectApp(
- {},
- { featureList: [Feature.GithubProvisioning] },
- {
- component: mockComponent({ visibility: Visibility.Private }),
- },
- );
- await ui.appLoaded();
-
- expect(ui.pageTitle.get()).toBeInTheDocument();
- await waitFor(() =>
- expect(ui.pageTitle.get()).toHaveAccessibleName(/project_permission.github_managed/),
- );
- expect(ui.pageTitle.byRole('img').get()).toBeInTheDocument();
- expect(ui.githubExplanations.get()).toBeInTheDocument();
-
- expect(ui.projectPermissionCheckbox('John', Permissions.Admin).get()).toBeChecked();
- expect(ui.projectPermissionCheckbox('John', Permissions.Admin).get()).toBeDisabled();
- expect(ui.projectPermissionCheckbox('Alexa', Permissions.IssueAdmin).get()).toBeChecked();
- expect(ui.projectPermissionCheckbox('Alexa', Permissions.IssueAdmin).get()).toBeEnabled();
- await ui.toggleProjectPermission('Alexa', Permissions.IssueAdmin);
- expect(ui.confirmRemovePermissionDialog.get()).toBeInTheDocument();
- expect(ui.confirmRemovePermissionDialog.get()).toHaveTextContent(
- `${Permissions.IssueAdmin}Alexa`,
- );
- await act(() =>
- user.click(ui.confirmRemovePermissionDialog.byRole('button', { name: 'confirm' }).get()),
- );
- expect(ui.projectPermissionCheckbox('Alexa', Permissions.IssueAdmin).get()).not.toBeChecked();
-
- expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).get()).toBeChecked();
- expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).get()).toBeEnabled();
- await ui.toggleProjectPermission('sonar-users', Permissions.Browse);
- expect(ui.confirmRemovePermissionDialog.get()).toBeInTheDocument();
- expect(ui.confirmRemovePermissionDialog.get()).toHaveTextContent(
- `${Permissions.Browse}sonar-users`,
- );
- await act(() =>
- user.click(ui.confirmRemovePermissionDialog.byRole('button', { name: 'confirm' }).get()),
- );
- expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).get()).not.toBeChecked();
- expect(ui.projectPermissionCheckbox('sonar-admins', Permissions.Admin).get()).toBeChecked();
- expect(ui.projectPermissionCheckbox('sonar-admins', Permissions.Admin).get()).toHaveAttribute(
- 'disabled',
- );
- const johnRow = screen.getAllByRole('row')[4];
- expect(johnRow).toHaveTextContent('John');
- expect(ui.githubLogo.get(johnRow)).toBeInTheDocument();
- const alexaRow = screen.getAllByRole('row')[5];
- expect(alexaRow).toHaveTextContent('Alexa');
- expect(ui.githubLogo.query(alexaRow)).not.toBeInTheDocument();
- const usersGroupRow = screen.getAllByRole('row')[1];
- expect(usersGroupRow).toHaveTextContent('sonar-users');
- expect(ui.githubLogo.query(usersGroupRow)).not.toBeInTheDocument();
- const adminsGroupRow = screen.getAllByRole('row')[2];
- expect(adminsGroupRow).toHaveTextContent('sonar-admins');
- expect(ui.githubLogo.query(adminsGroupRow)).toBeInTheDocument();
-
- expect(ui.applyTemplateBtn.query()).not.toBeInTheDocument();
-
- // not possible to grant permissions at all
- expect(
- screen
- .getAllByRole('checkbox', { checked: false })
- .every((item) => item.getAttributeNames().includes('disabled')),
- ).toBe(true);
-});
+ it('should have disabled permissions for GH Project', async () => {
+ const user = userEvent.setup();
+ const ui = getPageObject(user);
+ authHandler.githubProvisioningStatus = true;
+ almHandler.handleSetProjectBinding(AlmKeys.GitHub, {
+ almSetting: 'test',
+ repository: 'test',
+ monorepo: false,
+ project: 'my-project',
+ });
+ renderPermissionsProjectApp(
+ {},
+ { featureList: [Feature.GithubProvisioning] },
+ {
+ component: mockComponent({ visibility: Visibility.Private }),
+ },
+ );
+ await ui.appLoaded();
-it('should allow to change permissions for GH Project without auto-provisioning', async () => {
- const user = userEvent.setup();
- const ui = getPageObject(user);
- authHandler.githubProvisioningStatus = false;
- almHandler.handleSetProjectBinding(AlmKeys.GitHub, {
- almSetting: 'test',
- repository: 'test',
- monorepo: false,
- project: 'my-project',
+ expect(ui.pageTitle.get()).toBeInTheDocument();
+ await waitFor(() =>
+ expect(ui.pageTitle.get()).toHaveAccessibleName(/project_permission.github_managed/),
+ );
+ expect(ui.pageTitle.byRole('img').get()).toBeInTheDocument();
+ expect(ui.githubExplanations.get()).toBeInTheDocument();
+
+ expect(ui.projectPermissionCheckbox('John', Permissions.Admin).get()).toBeChecked();
+ expect(ui.projectPermissionCheckbox('John', Permissions.Admin).get()).toBeDisabled();
+ expect(ui.projectPermissionCheckbox('Alexa', Permissions.IssueAdmin).get()).toBeChecked();
+ expect(ui.projectPermissionCheckbox('Alexa', Permissions.IssueAdmin).get()).toBeEnabled();
+ await ui.toggleProjectPermission('Alexa', Permissions.IssueAdmin);
+ expect(ui.confirmRemovePermissionDialog.get()).toBeInTheDocument();
+ expect(ui.confirmRemovePermissionDialog.get()).toHaveTextContent(
+ `${Permissions.IssueAdmin}Alexa`,
+ );
+ await act(() =>
+ user.click(ui.confirmRemovePermissionDialog.byRole('button', { name: 'confirm' }).get()),
+ );
+ expect(ui.projectPermissionCheckbox('Alexa', Permissions.IssueAdmin).get()).not.toBeChecked();
+
+ expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).get()).toBeChecked();
+ expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).get()).toBeEnabled();
+ await ui.toggleProjectPermission('sonar-users', Permissions.Browse);
+ expect(ui.confirmRemovePermissionDialog.get()).toBeInTheDocument();
+ expect(ui.confirmRemovePermissionDialog.get()).toHaveTextContent(
+ `${Permissions.Browse}sonar-users`,
+ );
+ await act(() =>
+ user.click(ui.confirmRemovePermissionDialog.byRole('button', { name: 'confirm' }).get()),
+ );
+ expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).get()).not.toBeChecked();
+ expect(ui.projectPermissionCheckbox('sonar-admins', Permissions.Admin).get()).toBeChecked();
+ expect(ui.projectPermissionCheckbox('sonar-admins', Permissions.Admin).get()).toHaveAttribute(
+ 'disabled',
+ );
+
+ const johnRow = screen.getAllByRole('row')[4];
+ expect(johnRow).toHaveTextContent('John');
+ expect(ui.githubLogo.get(johnRow)).toBeInTheDocument();
+ const alexaRow = screen.getAllByRole('row')[5];
+ expect(alexaRow).toHaveTextContent('Alexa');
+ expect(ui.githubLogo.query(alexaRow)).not.toBeInTheDocument();
+ const usersGroupRow = screen.getAllByRole('row')[1];
+ expect(usersGroupRow).toHaveTextContent('sonar-users');
+ expect(ui.githubLogo.query(usersGroupRow)).not.toBeInTheDocument();
+ const adminsGroupRow = screen.getAllByRole('row')[2];
+ expect(adminsGroupRow).toHaveTextContent('sonar-admins');
+ expect(ui.githubLogo.query(adminsGroupRow)).toBeInTheDocument();
+
+ expect(ui.applyTemplateBtn.query()).not.toBeInTheDocument();
+
+ // not possible to grant permissions at all
+ expect(
+ screen
+ .getAllByRole('checkbox', { checked: false })
+ .every((item) => item.getAttributeNames().includes('disabled')),
+ ).toBe(true);
});
- renderPermissionsProjectApp(
- { visibility: Visibility.Private },
- { featureList: [Feature.GithubProvisioning] },
- );
- await ui.appLoaded();
- expect(ui.pageTitle.get()).toBeInTheDocument();
- expect(ui.pageTitle.byRole('img').query()).not.toBeInTheDocument();
+ it('should allow to change permissions for GH Project without auto-provisioning', async () => {
+ const user = userEvent.setup();
+ const ui = getPageObject(user);
+ authHandler.githubProvisioningStatus = false;
+ almHandler.handleSetProjectBinding(AlmKeys.GitHub, {
+ almSetting: 'test',
+ repository: 'test',
+ monorepo: false,
+ project: 'my-project',
+ });
+ renderPermissionsProjectApp(
+ { visibility: Visibility.Private },
+ { featureList: [Feature.GithubProvisioning] },
+ );
+ await ui.appLoaded();
- expect(ui.applyTemplateBtn.get()).toBeInTheDocument();
+ expect(ui.pageTitle.get()).toBeInTheDocument();
+ expect(ui.pageTitle.byRole('img').query()).not.toBeInTheDocument();
- // no restrictions
- expect(
- screen.getAllByRole('checkbox').every((item) => item.getAttributeNames().includes('disabled')),
- ).toBe(false);
-});
+ expect(ui.applyTemplateBtn.get()).toBeInTheDocument();
-it('should allow to change permissions for non-GH Project', async () => {
- const user = userEvent.setup();
- const ui = getPageObject(user);
- authHandler.githubProvisioningStatus = true;
- renderPermissionsProjectApp({}, { featureList: [Feature.GithubProvisioning] });
- await ui.appLoaded();
+ // no restrictions
+ expect(
+ screen
+ .getAllByRole('checkbox')
+ .every((item) => item.getAttributeNames().includes('disabled')),
+ ).toBe(false);
+ });
+
+ it('should allow to change permissions for non-GH Project', async () => {
+ const user = userEvent.setup();
+ const ui = getPageObject(user);
+ authHandler.githubProvisioningStatus = true;
+ renderPermissionsProjectApp({}, { featureList: [Feature.GithubProvisioning] });
+ await ui.appLoaded();
- expect(ui.pageTitle.get()).toBeInTheDocument();
- expect(ui.nonGHProjectWarning.get()).toBeInTheDocument();
- expect(ui.pageTitle.byRole('img').query()).not.toBeInTheDocument();
+ expect(ui.pageTitle.get()).toBeInTheDocument();
+ expect(ui.nonGHProjectWarning.get()).toBeInTheDocument();
+ expect(ui.pageTitle.byRole('img').query()).not.toBeInTheDocument();
- expect(ui.applyTemplateBtn.get()).toBeInTheDocument();
+ expect(ui.applyTemplateBtn.get()).toBeInTheDocument();
- // no restrictions
- expect(
- screen.getAllByRole('checkbox').every((item) => item.getAttributeNames().includes('disabled')),
- ).toBe(false);
+ // no restrictions
+ expect(
+ screen
+ .getAllByRole('checkbox')
+ .every((item) => item.getAttributeNames().includes('disabled')),
+ ).toBe(false);
+ });
});
function renderPermissionsProjectApp(
import { ExtendedSettingDefinition } from '../../../../types/settings';
import { AUTHENTICATION_CATEGORY } from '../../constants';
import CategoryDefinitionsList from '../CategoryDefinitionsList';
+import GitLabAuthenticationTab from './GitLabAuthenticationTab';
import GithubAuthenticationTab from './GithubAuthenticationTab';
import SamlAuthenticationTab, { SAML } from './SamlAuthenticationTab';
},
] as const;
- const [samlDefinitions, githubDefinitions] = React.useMemo(
+ const [samlDefinitions, githubDefinitions, gitlabDefinitions] = React.useMemo(
() => [
definitions.filter((def) => def.subCategory === SAML),
definitions.filter((def) => def.subCategory === AlmKeys.GitHub),
+ definitions.filter((def) => def.subCategory === AlmKeys.GitLab),
],
[definitions],
);
<div
style={{
maxHeight:
- tab.key !== SAML && tab.key !== AlmKeys.GitHub
+ tab.key === AlmKeys.BitbucketServer
? `calc(100vh - ${top + HEIGHT_ADJUSTMENT}px)`
: '',
}}
/>
)}
- {tab.key !== SAML && tab.key !== AlmKeys.GitHub && (
+ {tab.key === AlmKeys.GitLab && (
+ <GitLabAuthenticationTab definitions={gitlabDefinitions} />
+ )}
+
+ {tab.key === AlmKeys.BitbucketServer && (
<>
<Alert variant="info">
<FormattedMessage
import ValidationInput, {
ValidationInputErrorPlacement,
} from '../../../../components/controls/ValidationInput';
-import { ExtendedSettingDefinition, SettingType } from '../../../../types/settings';
+import { DefinitionV2, ExtendedSettingDefinition, SettingType } from '../../../../types/settings';
import { getPropertyDescription, getPropertyName, isSecuredDefinition } from '../../utils';
import AuthenticationFormFieldWrapper from './AuthenticationFormFieldWrapper';
import AuthenticationMultiValueField from './AuthenticationMultiValuesField';
import AuthenticationSecuredField from './AuthenticationSecuredField';
import AuthenticationToggleField from './AuthenticationToggleField';
-interface SamlToggleFieldProps {
+interface Props {
settingValue?: string | boolean | string[];
- definition: ExtendedSettingDefinition;
+ definition: ExtendedSettingDefinition | DefinitionV2;
mandatory?: boolean;
onFieldChange: (key: string, value: string | boolean | string[]) => void;
isNotSet: boolean;
error?: string;
}
-export default function AuthenticationFormField(props: SamlToggleFieldProps) {
+export default function AuthenticationFormField(props: Readonly<Props>) {
const { mandatory = false, definition, settingValue, isNotSet, error } = props;
const name = getPropertyName(definition);
import * as React from 'react';
import { DeleteButton } from '../../../../components/controls/buttons';
import { translateWithParameters } from '../../../../helpers/l10n';
-import { ExtendedSettingDefinition } from '../../../../types/settings';
+import { DefinitionV2, ExtendedSettingDefinition } from '../../../../types/settings';
import { getPropertyName } from '../../utils';
interface Props {
onFieldChange: (value: string[]) => void;
settingValue?: string[];
- definition: ExtendedSettingDefinition;
+ definition: ExtendedSettingDefinition | DefinitionV2;
}
export default function AuthenticationMultiValueField(props: Props) {
import React, { useEffect } from 'react';
import { ButtonLink } from '../../../../components/controls/buttons';
import { translate } from '../../../../helpers/l10n';
-import { ExtendedSettingDefinition, SettingType } from '../../../../types/settings';
+import { DefinitionV2, ExtendedSettingDefinition, SettingType } from '../../../../types/settings';
import { isSecuredDefinition } from '../../utils';
interface SamlToggleFieldProps {
onFieldChange: (key: string, value: string) => void;
settingValue?: string;
- definition: ExtendedSettingDefinition;
+ definition: ExtendedSettingDefinition | DefinitionV2;
optional?: boolean;
isNotSet: boolean;
}
*/
import React from 'react';
import Toggle from '../../../../components/controls/Toggle';
-import { ExtendedSettingDefinition } from '../../../../types/settings';
+import { DefinitionV2, ExtendedSettingDefinition } from '../../../../types/settings';
interface SamlToggleFieldProps {
onChange: (value: boolean) => void;
settingValue?: string | boolean;
- definition: ExtendedSettingDefinition;
+ definition: ExtendedSettingDefinition | DefinitionV2;
}
export default function AuthenticationToggleField(props: SamlToggleFieldProps) {
--- /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 { isEqual, omitBy } from 'lodash';
+import React, { FormEvent, useContext } from 'react';
+import { FormattedMessage } from 'react-intl';
+import {
+ GITLAB_SETTING_ALLOW_SIGNUP,
+ GITLAB_SETTING_GROUPS,
+ GITLAB_SETTING_GROUP_TOKEN,
+} from '../../../../api/provisioning';
+import GitLabSynchronisationWarning from '../../../../app/components/GitLabSynchronisationWarning';
+import { AvailableFeaturesContext } from '../../../../app/components/available-features/AvailableFeaturesContext';
+import DocLink from '../../../../components/common/DocLink';
+import ConfirmModal from '../../../../components/controls/ConfirmModal';
+import RadioCard from '../../../../components/controls/RadioCard';
+import Tooltip from '../../../../components/controls/Tooltip';
+import { Button, ResetButtonLink, SubmitButton } from '../../../../components/controls/buttons';
+import DeleteIcon from '../../../../components/icons/DeleteIcon';
+import EditIcon from '../../../../components/icons/EditIcon';
+import { Alert } from '../../../../components/ui/Alert';
+import Spinner from '../../../../components/ui/Spinner';
+import { translate } from '../../../../helpers/l10n';
+import {
+ useDeleteGitLabConfigurationMutation,
+ useGitLabConfigurationsQuery,
+ useIdentityProviderQuery,
+ useUpdateGitLabConfigurationMutation,
+} from '../../../../queries/identity-provider';
+import { AlmKeys } from '../../../../types/alm-settings';
+import { Feature } from '../../../../types/features';
+import { GitLabConfigurationUpdateBody, ProvisioningType } from '../../../../types/provisioning';
+import { ExtendedSettingDefinition } from '../../../../types/settings';
+import { Provider } from '../../../../types/types';
+import { DOCUMENTATION_LINK_SUFFIXES } from './Authentication';
+import AuthenticationFormField from './AuthenticationFormField';
+import GitLabConfigurationForm from './GitLabConfigurationForm';
+
+interface GitLabAuthenticationTab {
+ definitions: ExtendedSettingDefinition[];
+}
+
+interface ChangesForm {
+ type?: GitLabConfigurationUpdateBody['type'];
+ allowUsersToSignUp?: GitLabConfigurationUpdateBody['allowUsersToSignUp'];
+ provisioningToken?: GitLabConfigurationUpdateBody['provisioningToken'];
+ groups?: GitLabConfigurationUpdateBody['groups'];
+}
+
+export default function GitLabAuthenticationTab(props: Readonly<GitLabAuthenticationTab>) {
+ const { definitions } = props;
+
+ const [openForm, setOpenForm] = React.useState(false);
+ const [changes, setChanges] = React.useState<ChangesForm | undefined>(undefined);
+ const [tokenKey, setTokenKey] = React.useState<number>(0);
+ const [showConfirmProvisioningModal, setShowConfirmProvisioningModal] = React.useState(false);
+
+ const hasGitlabProvisioningFeature = useContext(AvailableFeaturesContext).includes(
+ Feature.GitlabProvisioning,
+ );
+
+ const { data: identityProvider } = useIdentityProviderQuery();
+ const { data: list, isLoading: isLoadingList } = useGitLabConfigurationsQuery();
+ const configuration = list?.configurations[0];
+
+ const { mutate: updateConfig, isLoading: isUpdating } = useUpdateGitLabConfigurationMutation();
+ const { mutate: deleteConfig, isLoading: isDeleting } = useDeleteGitLabConfigurationMutation();
+
+ const toggleEnable = () => {
+ if (!configuration) {
+ return;
+ }
+ updateConfig({ id: configuration.id, data: { enabled: !configuration.enabled } });
+ };
+
+ const deleteConfiguration = () => {
+ if (!configuration) {
+ return;
+ }
+ deleteConfig(configuration.id);
+ };
+
+ const handleSubmit = (e: FormEvent) => {
+ e.preventDefault();
+ if (changes?.type !== undefined) {
+ setShowConfirmProvisioningModal(true);
+ } else {
+ updateProvisioning();
+ }
+ };
+
+ const updateProvisioning = () => {
+ if (!changes || !configuration) {
+ return;
+ }
+
+ updateConfig(
+ { id: configuration.id, data: omitBy(changes, (value) => value === undefined) },
+ {
+ onSuccess: () => {
+ setChanges(undefined);
+ setTokenKey(tokenKey + 1);
+ },
+ },
+ );
+ };
+
+ const setJIT = () =>
+ setChangesWithCheck({
+ type: ProvisioningType.jit,
+ provisioningToken: undefined,
+ groups: undefined,
+ });
+
+ const setAuto = () =>
+ setChangesWithCheck({
+ type: ProvisioningType.auto,
+ allowUsersToSignUp: undefined,
+ });
+
+ const hasDifferentProvider =
+ identityProvider?.provider !== undefined && identityProvider.provider !== Provider.Gitlab;
+ const allowUsersToSignUpDefinition = definitions.find(
+ (d) => d.key === GITLAB_SETTING_ALLOW_SIGNUP,
+ );
+ const provisioningTokenDefinition = definitions.find((d) => d.key === GITLAB_SETTING_GROUP_TOKEN);
+ const provisioningGroupDefinition = definitions.find((d) => d.key === GITLAB_SETTING_GROUPS);
+
+ const provisioningType = changes?.type ?? configuration?.type;
+ const allowUsersToSignUp = changes?.allowUsersToSignUp ?? configuration?.allowUsersToSignUp;
+ const provisioningToken = changes?.provisioningToken;
+ const groups = changes?.groups ?? configuration?.groups;
+
+ const canSave = () => {
+ if (!configuration || changes === undefined) {
+ return false;
+ }
+ const type = changes.type ?? configuration.type;
+ if (type === ProvisioningType.auto) {
+ const hasConfigGroups = configuration.groups && configuration.groups.length > 0;
+ const hasGroups = changes.groups ? changes.groups.length > 0 : hasConfigGroups;
+ const hasToken = hasConfigGroups
+ ? changes.provisioningToken !== ''
+ : !!changes.provisioningToken;
+ return hasGroups && hasToken;
+ }
+ return true;
+ };
+
+ const setChangesWithCheck = (newChanges: ChangesForm) => {
+ const newValue = {
+ type: configuration?.type === newChanges.type ? undefined : newChanges.type,
+ allowUsersToSignUp:
+ configuration?.allowUsersToSignUp === newChanges.allowUsersToSignUp
+ ? undefined
+ : newChanges.allowUsersToSignUp,
+ provisioningToken: newChanges.provisioningToken,
+ groups: isEqual(configuration?.groups, newChanges.groups) ? undefined : newChanges.groups,
+ };
+ if (Object.values(newValue).some((v) => v !== undefined)) {
+ setChanges(newValue);
+ } else {
+ setChanges(undefined);
+ }
+ };
+
+ return (
+ <Spinner loading={isLoadingList}>
+ <div className="authentication-configuration">
+ <div className="spacer-bottom display-flex-space-between display-flex-center">
+ <h4>{translate('settings.authentication.gitlab.configuration')}</h4>
+ {!configuration && (
+ <div>
+ <Button onClick={() => setOpenForm(true)}>
+ {translate('settings.authentication.form.create')}
+ </Button>
+ </div>
+ )}
+ </div>
+ {!configuration && (
+ <div className="big-padded text-center huge-spacer-bottom authentication-no-config">
+ {translate('settings.authentication.gitlab.form.not_configured')}
+ </div>
+ )}
+ {configuration && (
+ <div className="spacer-bottom big-padded bordered display-flex-space-between">
+ <div>
+ <p>{configuration.url}</p>
+ <Tooltip
+ overlay={
+ configuration.type === ProvisioningType.auto
+ ? translate('settings.authentication.form.disable.tooltip')
+ : null
+ }
+ >
+ <Button
+ className="spacer-top"
+ onClick={toggleEnable}
+ disabled={isUpdating || configuration.type === ProvisioningType.auto}
+ >
+ {configuration.enabled
+ ? translate('settings.authentication.form.disable')
+ : translate('settings.authentication.form.enable')}
+ </Button>
+ </Tooltip>
+ </div>
+ <div>
+ <Button className="spacer-right" onClick={() => setOpenForm(true)}>
+ <EditIcon />
+ {translate('settings.authentication.form.edit')}
+ </Button>
+ <Tooltip
+ overlay={
+ configuration.enabled
+ ? translate('settings.authentication.form.delete.tooltip')
+ : null
+ }
+ >
+ <Button
+ className="button-red"
+ disabled={configuration.enabled || isDeleting}
+ onClick={deleteConfiguration}
+ >
+ <DeleteIcon />
+ {translate('settings.authentication.form.delete')}
+ </Button>
+ </Tooltip>
+ </div>
+ </div>
+ )}
+ <div className="spacer-bottom big-padded bordered">
+ <form onSubmit={handleSubmit}>
+ <fieldset className="display-flex-column big-spacer-bottom">
+ <label className="h5">{translate('settings.authentication.form.provisioning')}</label>
+
+ {configuration?.enabled ? (
+ <div className="display-flex-column spacer-top">
+ <RadioCard
+ className="sw-min-h-0"
+ label={translate('settings.authentication.gitlab.provisioning_at_login')}
+ title={translate('settings.authentication.gitlab.provisioning_at_login')}
+ selected={provisioningType === ProvisioningType.jit}
+ onClick={setJIT}
+ >
+ <p className="spacer-bottom">
+ <FormattedMessage id="settings.authentication.gitlab.provisioning_at_login.description" />
+ </p>
+ <p className="spacer-bottom">
+ <FormattedMessage
+ id="settings.authentication.gitlab.description.doc"
+ values={{
+ documentation: (
+ <DocLink
+ to={`/instance-administration/authentication/${
+ DOCUMENTATION_LINK_SUFFIXES[AlmKeys.GitLab]
+ }/`}
+ >
+ {translate('documentation')}
+ </DocLink>
+ ),
+ }}
+ />
+ </p>
+ {provisioningType === ProvisioningType.jit &&
+ allowUsersToSignUpDefinition !== undefined && (
+ <AuthenticationFormField
+ settingValue={allowUsersToSignUp}
+ definition={allowUsersToSignUpDefinition}
+ mandatory
+ onFieldChange={(_, value) =>
+ setChangesWithCheck({
+ ...changes,
+ allowUsersToSignUp: value as boolean,
+ })
+ }
+ isNotSet={configuration.type !== ProvisioningType.auto}
+ />
+ )}
+ </RadioCard>
+ <RadioCard
+ className="spacer-top sw-min-h-0"
+ label={translate(
+ 'settings.authentication.gitlab.form.provisioning_with_gitlab',
+ )}
+ title={translate(
+ 'settings.authentication.gitlab.form.provisioning_with_gitlab',
+ )}
+ selected={provisioningType === ProvisioningType.auto}
+ onClick={setAuto}
+ disabled={!hasGitlabProvisioningFeature || hasDifferentProvider}
+ >
+ {hasGitlabProvisioningFeature ? (
+ <>
+ {hasDifferentProvider && (
+ <p className="spacer-bottom text-bold ">
+ {translate('settings.authentication.form.other_provisioning_enabled')}
+ </p>
+ )}
+ <p className="spacer-bottom">
+ {translate(
+ 'settings.authentication.gitlab.form.provisioning_with_gitlab.description',
+ )}
+ </p>
+ <p className="spacer-bottom">
+ <FormattedMessage
+ id="settings.authentication.gitlab.description.doc"
+ values={{
+ documentation: (
+ <DocLink
+ to={`/instance-administration/authentication/${
+ DOCUMENTATION_LINK_SUFFIXES[AlmKeys.GitLab]
+ }/`}
+ >
+ {translate('documentation')}
+ </DocLink>
+ ),
+ }}
+ />
+ </p>
+
+ {configuration?.type === ProvisioningType.auto && (
+ <>
+ <GitLabSynchronisationWarning />
+ <hr className="spacer-top" />
+ </>
+ )}
+
+ {provisioningType === ProvisioningType.auto &&
+ provisioningTokenDefinition !== undefined &&
+ provisioningGroupDefinition !== undefined && (
+ <>
+ <AuthenticationFormField
+ settingValue={provisioningToken}
+ key={tokenKey}
+ definition={provisioningTokenDefinition}
+ mandatory
+ onFieldChange={(_, value) =>
+ setChangesWithCheck({
+ ...changes,
+ provisioningToken: value as string,
+ })
+ }
+ isNotSet={
+ configuration.type !== ProvisioningType.auto &&
+ configuration.groups?.length === 0
+ }
+ />
+ <AuthenticationFormField
+ settingValue={groups}
+ definition={provisioningGroupDefinition}
+ mandatory
+ onFieldChange={(_, values) =>
+ setChangesWithCheck({ ...changes, groups: values as string[] })
+ }
+ isNotSet={configuration.type !== ProvisioningType.auto}
+ />
+ </>
+ )}
+ </>
+ ) : (
+ <p>
+ <FormattedMessage
+ id="settings.authentication.gitlab.form.provisioning.disabled"
+ defaultMessage={translate(
+ 'settings.authentication.gitlab.form.provisioning.disabled',
+ )}
+ values={{
+ documentation: (
+ <DocLink to="/instance-administration/authentication/gitlab">
+ {translate('documentation')}
+ </DocLink>
+ ),
+ }}
+ />
+ </p>
+ )}
+ </RadioCard>
+ </div>
+ ) : (
+ <Alert className="big-spacer-top" variant="info">
+ {translate('settings.authentication.github.enable_first')}
+ </Alert>
+ )}
+ </fieldset>
+ {configuration?.enabled && (
+ <div className="sw-flex sw-gap-2 sw-h-8 sw-items-center">
+ <SubmitButton disabled={!canSave()}>{translate('save')}</SubmitButton>
+ <ResetButtonLink
+ onClick={() => {
+ setChanges(undefined);
+ setTokenKey(tokenKey + 1);
+ }}
+ disabled={false}
+ >
+ {translate('cancel')}
+ </ResetButtonLink>
+ <Alert variant="warning" className="sw-mb-0">
+ {canSave() &&
+ translate('settings.authentication.gitlab.configuration.unsaved_changes')}
+ </Alert>
+ </div>
+ )}
+ {showConfirmProvisioningModal && provisioningType && (
+ <ConfirmModal
+ onConfirm={updateProvisioning}
+ header={translate('settings.authentication.gitlab.confirm', provisioningType)}
+ onClose={() => setShowConfirmProvisioningModal(false)}
+ confirmButtonText={translate(
+ 'settings.authentication.gitlab.provisioning_change.confirm_changes',
+ )}
+ >
+ {translate(
+ 'settings.authentication.gitlab.confirm',
+ provisioningType,
+ 'description',
+ )}
+ </ConfirmModal>
+ )}
+ </form>
+ </div>
+ </div>
+ {openForm && (
+ <GitLabConfigurationForm data={configuration ?? null} onClose={() => setOpenForm(false)} />
+ )}
+ </Spinner>
+ );
+}
--- /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 { keyBy } from 'lodash';
+import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+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 Spinner from '../../../../components/ui/Spinner';
+import { translate } from '../../../../helpers/l10n';
+import {
+ useCreateGitLabConfigurationMutation,
+ useUpdateGitLabConfigurationMutation,
+} from '../../../../queries/identity-provider';
+import { GitLabConfigurationCreateBody, GitlabConfiguration } from '../../../../types/provisioning';
+import { DefinitionV2, SettingType } from '../../../../types/settings';
+import { DOCUMENTATION_LINK_SUFFIXES } from './Authentication';
+import AuthenticationFormField from './AuthenticationFormField';
+
+interface Props {
+ data: GitlabConfiguration | null;
+ onClose: () => void;
+}
+
+interface ErrorValue {
+ key: string;
+ message: string;
+}
+
+interface FormData {
+ value: string | boolean;
+ required: boolean;
+ definition: DefinitionV2;
+}
+
+const DEFAULT_URL = 'https://gitlab.com';
+
+export default function GitLabConfigurationForm(props: Readonly<Props>) {
+ const { data } = props;
+ const isCreate = data === null;
+ const [errors, setErrors] = React.useState<Record<string, ErrorValue>>({});
+ const { mutate: createConfig, isLoading: createLoading } = useCreateGitLabConfigurationMutation();
+ const { mutate: updateConfig, isLoading: updateLoading } = useUpdateGitLabConfigurationMutation();
+
+ const [formData, setFormData] = React.useState<
+ Record<keyof GitLabConfigurationCreateBody, FormData>
+ >({
+ applicationId: {
+ value: '',
+ required: true,
+ definition: {
+ name: translate('settings.authentication.gitlab.form.applicationId.name'),
+ key: 'applicationId',
+ description: translate('settings.authentication.gitlab.form.applicationId.description'),
+ secured: true,
+ },
+ },
+ url: {
+ value: data?.url ?? DEFAULT_URL,
+ required: true,
+ definition: {
+ name: translate('settings.authentication.gitlab.form.url.name'),
+ secured: false,
+ key: 'url',
+ description: translate('settings.authentication.gitlab.form.url.description'),
+ },
+ },
+ clientSecret: {
+ value: '',
+ required: true,
+ definition: {
+ name: translate('settings.authentication.gitlab.form.clientSecret.name'),
+ secured: true,
+ key: 'clientSecret',
+ description: translate('settings.authentication.gitlab.form.clientSecret.description'),
+ },
+ },
+ synchronizeUserGroups: {
+ value: data?.synchronizeUserGroups ?? false,
+ required: false,
+ definition: {
+ name: translate('settings.authentication.gitlab.form.synchronizeUserGroups.name'),
+ secured: false,
+ key: 'synchronizeUserGroups',
+ description: translate(
+ 'settings.authentication.gitlab.form.synchronizeUserGroups.description',
+ ),
+ type: SettingType.BOOLEAN,
+ },
+ },
+ });
+
+ const headerLabel = translate(
+ 'settings.authentication.gitlab.form',
+ isCreate ? 'create' : 'edit',
+ );
+
+ const canBeSaved = Object.values(formData).every(
+ (v) => (!isCreate && v.definition.secured) || !v.required || v.value !== '',
+ );
+
+ const handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
+ event.preventDefault();
+
+ if (canBeSaved) {
+ const submitData = Object.entries(formData).reduce<GitLabConfigurationCreateBody>(
+ (acc, [key, { value }]: [keyof GitLabConfigurationCreateBody, FormData]) => {
+ if (value === '') {
+ return acc;
+ }
+ return {
+ ...acc,
+ [key]: value,
+ };
+ },
+ {} as GitLabConfigurationCreateBody,
+ );
+ if (data) {
+ updateConfig({ id: data.id, data: submitData }, { onSuccess: props.onClose });
+ } else {
+ createConfig(submitData, { onSuccess: props.onClose });
+ }
+ } else {
+ const errors = Object.entries(formData)
+ .filter(([_, v]) => v.required && !v.value)
+ .map(([key]) => ({ key, message: translate('field_required') }));
+ setErrors(keyBy(errors, 'key'));
+ }
+ };
+
+ return (
+ <Modal
+ contentLabel={headerLabel}
+ onRequestClose={props.onClose}
+ shouldCloseOnOverlayClick={false}
+ shouldCloseOnEsc
+ size="medium"
+ >
+ <form onSubmit={handleSubmit}>
+ <div className="modal-head">
+ <h2>{headerLabel}</h2>
+ </div>
+ <div className="modal-body modal-container">
+ <Alert variant="info">
+ <FormattedMessage
+ id="settings.authentication.help"
+ values={{
+ link: (
+ <DocLink
+ to={`/instance-administration/authentication/${DOCUMENTATION_LINK_SUFFIXES.gitlab}/`}
+ >
+ {translate('settings.authentication.help.link')}
+ </DocLink>
+ ),
+ }}
+ />
+ </Alert>
+ {Object.entries(formData).map(
+ ([key, { value, required, definition }]: [
+ key: keyof GitLabConfigurationCreateBody,
+ FormData,
+ ]) => (
+ <div key={key}>
+ <AuthenticationFormField
+ settingValue={value}
+ definition={definition}
+ mandatory={required}
+ onFieldChange={(_, value) => {
+ setFormData((prev) => ({ ...prev, [key]: { ...prev[key], value } }));
+ }}
+ isNotSet={isCreate}
+ error={errors[key]?.message}
+ />
+ </div>
+ ),
+ )}
+ </div>
+
+ <div className="modal-foot">
+ <SubmitButton disabled={!canBeSaved}>
+ {translate('settings.almintegration.form.save')}
+ <Spinner className="spacer-left" loading={createLoading || updateLoading} />
+ </SubmitButton>
+ <ResetButtonLink onClick={props.onClose}>{translate('cancel')}</ResetButtonLink>
+ </div>
+ </form>
+ </Modal>
+ );
+}
import RadioCard from '../../../../components/controls/RadioCard';
import Tooltip from '../../../../components/controls/Tooltip';
import { Button, ResetButtonLink, SubmitButton } from '../../../../components/controls/buttons';
-import { Provider } from '../../../../components/hooks/useManageProvider';
import DeleteIcon from '../../../../components/icons/DeleteIcon';
import EditIcon from '../../../../components/icons/EditIcon';
import { Alert } from '../../../../components/ui/Alert';
} from '../../../../queries/identity-provider';
import { AlmKeys } from '../../../../types/alm-settings';
import { ExtendedSettingDefinition } from '../../../../types/settings';
+import { Provider } from '../../../../types/types';
import { AuthenticationTabs, DOCUMENTATION_LINK_SUFFIXES } from './Authentication';
import AuthenticationFormField from './AuthenticationFormField';
import AuthenticationFormFieldWrapper from './AuthenticationFormFieldWrapper';
<p className="spacer-bottom">
<FormattedMessage
id="settings.authentication.github.form.description.doc"
- tagName="p"
values={{
documentation: (
<DocLink
import ConfirmModal from '../../../../components/controls/ConfirmModal';
import RadioCard from '../../../../components/controls/RadioCard';
import { Button, ResetButtonLink, SubmitButton } from '../../../../components/controls/buttons';
-import { Provider } from '../../../../components/hooks/useManageProvider';
import CheckIcon from '../../../../components/icons/CheckIcon';
import DeleteIcon from '../../../../components/icons/DeleteIcon';
import EditIcon from '../../../../components/icons/EditIcon';
} from '../../../../queries/identity-provider';
import { useSaveValueMutation } from '../../../../queries/settings';
import { ExtendedSettingDefinition } from '../../../../types/settings';
+import { Provider } from '../../../../types/types';
import ConfigurationForm from './ConfigurationForm';
import useSamlConfiguration, {
SAML_ENABLED_FIELD,
import SettingsServiceMock from '../../../../../api/mocks/SettingsServiceMock';
import SystemServiceMock from '../../../../../api/mocks/SystemServiceMock';
import { AvailableFeaturesContext } from '../../../../../app/components/available-features/AvailableFeaturesContext';
+import { mockGitlabConfiguration } from '../../../../../helpers/mocks/alm-integrations';
import { definitions } from '../../../../../helpers/mocks/definitions-list';
import { renderComponent } from '../../../../../helpers/testReactTestingUtils';
import { byRole, byText } from '../../../../../helpers/testSelector';
import { Feature } from '../../../../../types/features';
-import { GitHubProvisioningStatus } from '../../../../../types/provisioning';
-import { TaskStatuses } from '../../../../../types/tasks';
+import { GitHubProvisioningStatus, ProvisioningType } from '../../../../../types/provisioning';
+import { TaskStatuses, TaskTypes } from '../../../../../types/tasks';
import Authentication from '../Authentication';
let handler: AuthenticationServiceMock;
computeEngineHandler.reset();
});
+const ghContainer = byRole('tabpanel', { name: 'github GitHub' });
+const glContainer = byRole('tabpanel', { name: 'gitlab GitLab' });
+const samlContainer = byRole('tabpanel', { name: 'SAML' });
+
const ui = {
saveButton: byRole('button', { name: 'settings.authentication.saml.form.save' }),
customMessageInformation: byText('settings.authentication.custom_message_information'),
textbox2: byRole('textbox', { name: 'test2' }),
saml: {
noSamlConfiguration: byText('settings.authentication.saml.form.not_configured'),
- createConfigButton: byRole('button', { name: 'settings.authentication.form.create' }),
+ createConfigButton: samlContainer.byRole('button', {
+ name: 'settings.authentication.form.create',
+ }),
providerName: byRole('textbox', { name: 'property.sonar.auth.saml.providerName.name' }),
providerId: byRole('textbox', { name: 'property.sonar.auth.saml.providerId.name' }),
providerCertificate: byRole('textbox', {
confirmProvisioningButton: byRole('button', {
name: 'yes',
}),
- saveScim: byRole('button', { name: 'save' }),
- enableConfigButton: byRole('button', { name: 'settings.authentication.form.enable' }),
- disableConfigButton: byRole('button', { name: 'settings.authentication.form.disable' }),
- editConfigButton: byRole('button', { name: 'settings.authentication.form.edit' }),
+ saveScim: samlContainer.byRole('button', { name: 'save' }),
+ enableConfigButton: samlContainer.byRole('button', {
+ name: 'settings.authentication.form.enable',
+ }),
+ disableConfigButton: samlContainer.byRole('button', {
+ name: 'settings.authentication.form.disable',
+ }),
+ editConfigButton: samlContainer.byRole('button', {
+ name: 'settings.authentication.form.edit',
+ }),
enableFirstMessage: byText('settings.authentication.saml.enable_first'),
jitProvisioningButton: byRole('radio', {
name: 'settings.authentication.saml.form.provisioning_at_login',
createConfiguration: async (user: UserEvent) => {
const { saml } = ui;
- await user.click((await saml.createConfigButton.findAll())[0]);
+ await user.click(await saml.createConfigButton.find());
await saml.fillForm(user);
await user.click(saml.saveConfigButton.get());
},
github: {
tab: byRole('tab', { name: 'github GitHub' }),
noGithubConfiguration: byText('settings.authentication.github.form.not_configured'),
- createConfigButton: byRole('button', { name: 'settings.authentication.form.create' }),
- clientId: byRole('textbox', { name: 'property.sonar.auth.github.clientId.secured.name' }),
+ createConfigButton: ghContainer.byRole('button', {
+ name: 'settings.authentication.form.create',
+ }),
+ clientId: byRole('textbox', {
+ name: 'property.sonar.auth.github.clientId.secured.name',
+ }),
appId: byRole('textbox', { name: 'property.sonar.auth.github.appId.name' }),
- privateKey: byRole('textbox', { name: 'property.sonar.auth.github.privateKey.secured.name' }),
+ privateKey: byRole('textbox', {
+ name: 'property.sonar.auth.github.privateKey.secured.name',
+ }),
clientSecret: byRole('textbox', {
name: 'property.sonar.auth.github.clientSecret.secured.name',
}),
allowUserToSignUp: byRole('switch', {
name: 'sonar.auth.github.allowUsersToSignUp',
}),
- organizations: byRole('textbox', { name: 'property.sonar.auth.github.organizations.name' }),
+ organizations: byRole('textbox', {
+ name: 'property.sonar.auth.github.organizations.name',
+ }),
saveConfigButton: byRole('button', { name: 'settings.almintegration.form.save' }),
confirmProvisioningButton: byRole('button', {
name: 'settings.authentication.github.provisioning_change.confirm_changes',
}),
- saveGithubProvisioning: byRole('button', { name: 'save' }),
- groupAttribute: byRole('textbox', { name: 'property.sonar.auth.github.group.name.name' }),
- enableConfigButton: byRole('button', { name: 'settings.authentication.form.enable' }),
- disableConfigButton: byRole('button', { name: 'settings.authentication.form.disable' }),
- editConfigButton: byRole('button', { name: 'settings.authentication.form.edit' }),
- editMappingButton: byRole('button', {
+ saveGithubProvisioning: ghContainer.byRole('button', { name: 'save' }),
+ groupAttribute: byRole('textbox', {
+ name: 'property.sonar.auth.github.group.name.name',
+ }),
+ enableConfigButton: ghContainer.byRole('button', {
+ name: 'settings.authentication.form.enable',
+ }),
+ disableConfigButton: ghContainer.byRole('button', {
+ name: 'settings.authentication.form.disable',
+ }),
+ editConfigButton: ghContainer.byRole('button', {
+ name: 'settings.authentication.form.edit',
+ }),
+ editMappingButton: ghContainer.byRole('button', {
name: 'settings.authentication.github.configuration.roles_mapping.button_label',
}),
mappingRow: byRole('dialog', {
byRole('button', {
name: `settings.definition.delete_value.property.sonar.auth.github.organizations.name.${org}`,
}),
- enableFirstMessage: byText('settings.authentication.github.enable_first'),
- jitProvisioningButton: byRole('radio', {
+ enableFirstMessage: ghContainer.byText('settings.authentication.github.enable_first'),
+ jitProvisioningButton: ghContainer.byRole('radio', {
name: 'settings.authentication.form.provisioning_at_login',
}),
- githubProvisioningButton: byRole('radio', {
+ githubProvisioningButton: ghContainer.byRole('radio', {
name: 'settings.authentication.github.form.provisioning_with_github',
}),
- githubProvisioningPending: byText(/synchronization_pending/),
- githubProvisioningInProgress: byText(/synchronization_in_progress/),
- githubProvisioningSuccess: byText(/synchronization_successful/),
- githubProvisioningAlert: byText(/synchronization_failed/),
- configurationValidityLoading: byRole('status', {
+ githubProvisioningPending: ghContainer.byText(/synchronization_pending/),
+ githubProvisioningInProgress: ghContainer.byText(/synchronization_in_progress/),
+ githubProvisioningSuccess: ghContainer.byText(/synchronization_successful/),
+ githubProvisioningAlert: ghContainer.byText(/synchronization_failed/),
+ configurationValidityLoading: ghContainer.byRole('status', {
name: /github.configuration.validation.loading/,
}),
- configurationValiditySuccess: byRole('status', {
+ configurationValiditySuccess: ghContainer.byRole('status', {
name: /github.configuration.validation.valid/,
}),
- configurationValidityError: byRole('status', {
+ configurationValidityError: ghContainer.byRole('status', {
name: /github.configuration.validation.invalid/,
}),
- syncWarning: byText(/Warning/),
- syncSummary: byText(/Test summary/),
- configurationValidityWarning: byRole('status', {
+ syncWarning: ghContainer.byText(/Warning/),
+ syncSummary: ghContainer.byText(/Test summary/),
+ configurationValidityWarning: ghContainer.byRole('status', {
name: /github.configuration.validation.valid.short/,
}),
- checkConfigButton: byRole('button', {
+ checkConfigButton: ghContainer.byRole('button', {
name: 'settings.authentication.github.configuration.validation.test',
}),
- viewConfigValidityDetailsButton: byRole('button', {
+ viewConfigValidityDetailsButton: ghContainer.byRole('button', {
name: 'settings.authentication.github.configuration.validation.details',
}),
configDetailsDialog: byRole('dialog', {
createConfiguration: async (user: UserEvent) => {
const { github } = ui;
- await user.click((await github.createConfigButton.findAll())[1]);
+ await user.click(await github.createConfigButton.find());
await github.fillForm(user);
await user.click(github.saveConfigButton.get());
await user.click(github.confirmProvisioningButton.get());
},
},
+ gitlab: {
+ tab: byRole('tab', { name: 'gitlab GitLab' }),
+ noGitlabConfiguration: glContainer.byText('settings.authentication.gitlab.form.not_configured'),
+ createConfigButton: glContainer.byRole('button', {
+ name: 'settings.authentication.form.create',
+ }),
+ editConfigButton: glContainer.byRole('button', {
+ name: 'settings.authentication.form.edit',
+ }),
+ deleteConfigButton: glContainer.byRole('button', {
+ name: 'settings.authentication.form.delete',
+ }),
+ enableConfigButton: glContainer.byRole('button', {
+ name: 'settings.authentication.form.enable',
+ }),
+ disableConfigButton: glContainer.byRole('button', {
+ name: 'settings.authentication.form.disable',
+ }),
+ createDialog: byRole('dialog', {
+ name: 'settings.authentication.gitlab.form.create',
+ }),
+ editDialog: byRole('dialog', {
+ name: 'settings.authentication.gitlab.form.edit',
+ }),
+ applicationId: byRole('textbox', {
+ name: 'property.applicationId.name',
+ }),
+ url: byRole('textbox', { name: 'property.url.name' }),
+ clientSecret: byRole('textbox', {
+ name: 'property.clientSecret.name',
+ }),
+ synchronizeUserGroups: byRole('switch', {
+ name: 'synchronizeUserGroups',
+ }),
+ saveConfigButton: byRole('button', { name: 'settings.almintegration.form.save' }),
+ jitProvisioningRadioButton: glContainer.byRole('radio', {
+ name: 'settings.authentication.gitlab.provisioning_at_login',
+ }),
+ autoProvisioningRadioButton: glContainer.byRole('radio', {
+ name: 'settings.authentication.gitlab.form.provisioning_with_gitlab',
+ }),
+ jitAllowUsersToSignUpToggle: byRole('switch', { name: 'sonar.auth.gitlab.allowUsersToSignUp' }),
+ autoProvisioningToken: byRole('textbox', {
+ name: 'property.provisioning.gitlab.token.secured.name',
+ }),
+ autoProvisioningUpdateTokenButton: byRole('button', {
+ name: 'settings.almintegration.form.secret.update_field',
+ }),
+ autoProvisioningGroupsInput: byRole('textbox', {
+ name: 'property.provisioning.gitlab.groups.name',
+ }),
+ removeProvisioniongGroup: byRole('button', {
+ name: /settings.definition.delete_value.property.provisioning.gitlab.groups.name./,
+ }),
+ saveProvisioning: glContainer.byRole('button', { name: 'save' }),
+ cancelProvisioningChanges: glContainer.byRole('button', { name: 'cancel' }),
+ confirmAutoProvisioningDialog: byRole('dialog', {
+ name: 'settings.authentication.gitlab.confirm.Auto',
+ }),
+ confirmJitProvisioningDialog: byRole('dialog', {
+ name: 'settings.authentication.gitlab.confirm.JIT',
+ }),
+ confirmProvisioningChange: byRole('button', {
+ name: 'settings.authentication.gitlab.provisioning_change.confirm_changes',
+ }),
+ syncSummary: glContainer.byText(/Test summary/),
+ syncWarning: glContainer.byText(/Warning/),
+ gitlabProvisioningPending: glContainer.byText(/synchronization_pending/),
+ gitlabProvisioningInProgress: glContainer.byText(/synchronization_in_progress/),
+ gitlabProvisioningSuccess: glContainer.byText(/synchronization_successful/),
+ gitlabProvisioningAlert: glContainer.byText(/synchronization_failed/),
+ },
};
it('should render tabs and allow navigation', async () => {
const user = userEvent.setup();
renderAuthentication();
- await user.click((await saml.createConfigButton.findAll())[0]);
+ await user.click(await saml.createConfigButton.find());
expect(saml.saveConfigButton.get()).toBeDisabled();
await saml.fillForm(user);
renderAuthentication();
await user.click(await github.tab.find());
- await user.click((await github.createConfigButton.findAll())[1]);
+ await user.click(await github.createConfigButton.find());
expect(github.saveConfigButton.get()).toBeDisabled();
});
});
+describe('GitLab', () => {
+ const { gitlab } = ui;
+
+ it('should create a Gitlab configuration and disable it', async () => {
+ handler.setGitlabConfigurations([]);
+ renderAuthentication();
+ const user = userEvent.setup();
+ await user.click(await gitlab.tab.find());
+
+ expect(await gitlab.noGitlabConfiguration.find()).toBeInTheDocument();
+ expect(gitlab.createConfigButton.get()).toBeInTheDocument();
+
+ await user.click(gitlab.createConfigButton.get());
+ expect(await gitlab.createDialog.find()).toBeInTheDocument();
+ await user.type(gitlab.applicationId.get(), '123');
+ await user.type(gitlab.url.get(), 'https://company.gitlab.com');
+ await user.type(gitlab.clientSecret.get(), '123');
+ await user.click(gitlab.synchronizeUserGroups.get());
+ await user.click(gitlab.saveConfigButton.get());
+
+ expect(await gitlab.editConfigButton.find()).toBeInTheDocument();
+ expect(gitlab.noGitlabConfiguration.query()).not.toBeInTheDocument();
+ expect(glContainer.get()).toHaveTextContent('https://company.gitlab.com');
+
+ expect(gitlab.disableConfigButton.get()).toBeInTheDocument();
+ await user.click(gitlab.disableConfigButton.get());
+ expect(gitlab.enableConfigButton.get()).toBeInTheDocument();
+ expect(gitlab.disableConfigButton.query()).not.toBeInTheDocument();
+ });
+
+ it('should edit/delete configuration', async () => {
+ const user = userEvent.setup();
+ renderAuthentication();
+ await user.click(await gitlab.tab.find());
+
+ expect(await gitlab.editConfigButton.find()).toBeInTheDocument();
+ expect(glContainer.get()).toHaveTextContent('URL');
+ expect(gitlab.disableConfigButton.get()).toBeInTheDocument();
+ expect(gitlab.deleteConfigButton.get()).toBeInTheDocument();
+ expect(gitlab.deleteConfigButton.get()).toBeDisabled();
+
+ await user.click(gitlab.editConfigButton.get());
+ expect(await gitlab.editDialog.find()).toBeInTheDocument();
+ expect(gitlab.url.get()).toHaveValue('URL');
+ expect(gitlab.applicationId.query()).not.toBeInTheDocument();
+ expect(gitlab.clientSecret.query()).not.toBeInTheDocument();
+ expect(gitlab.synchronizeUserGroups.get()).toBeChecked();
+ await user.clear(gitlab.url.get());
+ await user.type(gitlab.url.get(), 'https://company.gitlab.com');
+ await user.click(gitlab.saveConfigButton.get());
+
+ expect(glContainer.get()).not.toHaveTextContent('URL');
+ expect(glContainer.get()).toHaveTextContent('https://company.gitlab.com');
+
+ expect(gitlab.disableConfigButton.get()).toBeInTheDocument();
+ await user.click(gitlab.disableConfigButton.get());
+ expect(await gitlab.enableConfigButton.find()).toBeInTheDocument();
+ expect(gitlab.deleteConfigButton.get()).toBeEnabled();
+ await user.click(gitlab.deleteConfigButton.get());
+ expect(await gitlab.noGitlabConfiguration.find()).toBeInTheDocument();
+ expect(gitlab.editConfigButton.query()).not.toBeInTheDocument();
+ });
+
+ it('should change from just-in-time to Auto Provisioning with proper validation', async () => {
+ const user = userEvent.setup();
+ renderAuthentication([Feature.GitlabProvisioning]);
+ await user.click(await gitlab.tab.find());
+
+ expect(await gitlab.editConfigButton.find()).toBeInTheDocument();
+ expect(gitlab.jitProvisioningRadioButton.get()).toBeChecked();
+
+ user.click(gitlab.autoProvisioningRadioButton.get());
+ expect(await gitlab.autoProvisioningRadioButton.find()).toBeEnabled();
+ expect(gitlab.saveProvisioning.get()).toBeDisabled();
+
+ await user.type(gitlab.autoProvisioningToken.get(), 'JRR Tolkien');
+ expect(await gitlab.saveProvisioning.find()).toBeDisabled();
+
+ await user.type(gitlab.autoProvisioningGroupsInput.get(), 'NWA');
+ user.click(gitlab.autoProvisioningRadioButton.get());
+ expect(await gitlab.saveProvisioning.find()).toBeEnabled();
+
+ await user.click(gitlab.removeProvisioniongGroup.get());
+ expect(await gitlab.saveProvisioning.find()).toBeDisabled();
+ await user.type(gitlab.autoProvisioningGroupsInput.get(), 'Wu-Tang Clan');
+ expect(await gitlab.saveProvisioning.find()).toBeEnabled();
+
+ await user.clear(gitlab.autoProvisioningToken.get());
+ expect(await gitlab.saveProvisioning.find()).toBeDisabled();
+ await user.type(gitlab.autoProvisioningToken.get(), 'tiktoken');
+ expect(await gitlab.saveProvisioning.find()).toBeEnabled();
+
+ await user.click(gitlab.saveProvisioning.get());
+ expect(gitlab.confirmAutoProvisioningDialog.get()).toBeInTheDocument();
+ await user.click(gitlab.confirmProvisioningChange.get());
+ expect(gitlab.confirmAutoProvisioningDialog.query()).not.toBeInTheDocument();
+
+ expect(gitlab.autoProvisioningRadioButton.get()).toBeChecked();
+ expect(await gitlab.saveProvisioning.find()).toBeDisabled();
+ });
+
+ it('should change from auto provisioning to JIT with proper validation', async () => {
+ handler.setGitlabConfigurations([
+ mockGitlabConfiguration({
+ allowUsersToSignUp: false,
+ enabled: true,
+ type: ProvisioningType.auto,
+ groups: ['D12'],
+ }),
+ ]);
+ const user = userEvent.setup();
+ renderAuthentication([Feature.GitlabProvisioning]);
+ await user.click(await gitlab.tab.find());
+
+ expect(await gitlab.editConfigButton.find()).toBeInTheDocument();
+
+ expect(gitlab.jitProvisioningRadioButton.get()).not.toBeChecked();
+ expect(gitlab.autoProvisioningRadioButton.get()).toBeChecked();
+ expect(gitlab.autoProvisioningGroupsInput.get()).toHaveValue('D12');
+
+ expect(gitlab.autoProvisioningToken.query()).not.toBeInTheDocument();
+ expect(gitlab.autoProvisioningUpdateTokenButton.get()).toBeInTheDocument();
+
+ await user.click(gitlab.jitProvisioningRadioButton.get());
+ expect(await gitlab.jitProvisioningRadioButton.find()).toBeChecked();
+
+ expect(await gitlab.saveProvisioning.find()).toBeEnabled();
+
+ expect(gitlab.jitAllowUsersToSignUpToggle.get()).toBeInTheDocument();
+
+ await user.click(gitlab.saveProvisioning.get());
+ expect(gitlab.confirmJitProvisioningDialog.get()).toBeInTheDocument();
+ await user.click(gitlab.confirmProvisioningChange.get());
+ expect(gitlab.confirmJitProvisioningDialog.query()).not.toBeInTheDocument();
+
+ expect(gitlab.jitProvisioningRadioButton.get()).toBeChecked();
+ expect(await gitlab.saveProvisioning.find()).toBeDisabled();
+ });
+
+ it('should be able to allow user to sign up for JIT with proper validation', async () => {
+ handler.setGitlabConfigurations([
+ mockGitlabConfiguration({
+ allowUsersToSignUp: false,
+ enabled: true,
+ type: ProvisioningType.jit,
+ }),
+ ]);
+ const user = userEvent.setup();
+ renderAuthentication([Feature.GitlabProvisioning]);
+ await user.click(await gitlab.tab.find());
+
+ expect(await gitlab.editConfigButton.find()).toBeInTheDocument();
+
+ expect(gitlab.jitProvisioningRadioButton.get()).toBeChecked();
+ expect(gitlab.autoProvisioningRadioButton.get()).not.toBeChecked();
+
+ expect(gitlab.jitAllowUsersToSignUpToggle.get()).not.toBeChecked();
+
+ expect(gitlab.saveProvisioning.get()).toBeDisabled();
+ await user.click(gitlab.jitAllowUsersToSignUpToggle.get());
+ expect(gitlab.saveProvisioning.get()).toBeEnabled();
+ await user.click(gitlab.jitAllowUsersToSignUpToggle.get());
+ expect(gitlab.saveProvisioning.get()).toBeDisabled();
+ await user.click(gitlab.jitAllowUsersToSignUpToggle.get());
+
+ await user.click(gitlab.saveProvisioning.get());
+
+ expect(gitlab.jitProvisioningRadioButton.get()).toBeChecked();
+ expect(gitlab.jitAllowUsersToSignUpToggle.get()).toBeChecked();
+ expect(await gitlab.saveProvisioning.find()).toBeDisabled();
+ });
+
+ it('should be able to edit groups and token for Auto provisioning with proper validation', async () => {
+ handler.setGitlabConfigurations([
+ mockGitlabConfiguration({
+ allowUsersToSignUp: false,
+ enabled: true,
+ type: ProvisioningType.auto,
+ groups: ['Cypress Hill', 'Public Enemy'],
+ }),
+ ]);
+ const user = userEvent.setup();
+ renderAuthentication([Feature.GitlabProvisioning]);
+ await user.click(await gitlab.tab.find());
+
+ expect(gitlab.autoProvisioningRadioButton.get()).toBeChecked();
+ expect(gitlab.autoProvisioningUpdateTokenButton.get()).toBeInTheDocument();
+ expect(gitlab.autoProvisioningGroupsInput.get()).toHaveValue('Cypress Hill');
+
+ expect(gitlab.saveProvisioning.get()).toBeDisabled();
+
+ // Changing the Provisioning token should enable save
+ await user.click(gitlab.autoProvisioningUpdateTokenButton.get());
+ await user.type(gitlab.autoProvisioningGroupsInput.get(), 'Tok Token!');
+ expect(gitlab.saveProvisioning.get()).toBeEnabled();
+ await user.click(gitlab.cancelProvisioningChanges.get());
+ expect(gitlab.saveProvisioning.get()).toBeDisabled();
+
+ // Adding a group should enable save
+ await user.click(gitlab.autoProvisioningGroupsInput.get());
+ await user.tab();
+ await user.tab();
+ await user.tab();
+ await user.tab();
+ await user.keyboard('Run DMC');
+ expect(gitlab.saveProvisioning.get()).toBeEnabled();
+ await user.tab();
+ await user.keyboard('{Enter}');
+ expect(gitlab.saveProvisioning.get()).toBeDisabled();
+
+ // Removing a group should enable save
+ await user.click(gitlab.autoProvisioningGroupsInput.get());
+ await user.tab();
+ await user.keyboard('{Enter}');
+ expect(gitlab.saveProvisioning.get()).toBeEnabled();
+
+ // Removing all groups should disable save
+ await user.click(gitlab.autoProvisioningGroupsInput.get());
+ await user.tab();
+ await user.keyboard('{Enter}');
+ expect(gitlab.saveProvisioning.get()).toBeDisabled();
+ });
+
+ it('should be able to reset Auto Provisioning changes', async () => {
+ handler.setGitlabConfigurations([
+ mockGitlabConfiguration({
+ allowUsersToSignUp: false,
+ enabled: true,
+ type: ProvisioningType.auto,
+ groups: ['Cypress Hill', 'Public Enemy'],
+ }),
+ ]);
+ const user = userEvent.setup();
+ renderAuthentication([Feature.GitlabProvisioning]);
+ await user.click(await gitlab.tab.find());
+
+ expect(gitlab.autoProvisioningRadioButton.get()).toBeChecked();
+
+ // Cancel doesn't fully work yet as the AuthenticationFormField needs to be worked on
+ await user.click(gitlab.autoProvisioningGroupsInput.get());
+ await user.tab();
+ await user.tab();
+ await user.tab();
+ await user.tab();
+ await user.keyboard('A Tribe Called Quest');
+ await user.click(gitlab.autoProvisioningGroupsInput.get());
+ await user.tab();
+ await user.keyboard('{Enter}');
+ await user.click(gitlab.autoProvisioningUpdateTokenButton.get());
+ await user.type(gitlab.autoProvisioningGroupsInput.get(), 'ToToken!');
+ expect(gitlab.saveProvisioning.get()).toBeEnabled();
+ await user.click(gitlab.cancelProvisioningChanges.get());
+ // expect(gitlab.autoProvisioningUpdateTokenButton.get()).toBeInTheDocument();
+ expect(gitlab.autoProvisioningGroupsInput.get()).toHaveValue('Cypress Hill');
+ });
+
+ describe('Gitlab Provisioning', () => {
+ beforeEach(() => {
+ jest.useFakeTimers({
+ advanceTimers: true,
+ now: new Date('2022-02-04T12:00:59Z'),
+ });
+ handler.setGitlabConfigurations([
+ mockGitlabConfiguration({
+ id: '1',
+ enabled: true,
+ type: ProvisioningType.auto,
+ groups: ['Test'],
+ }),
+ ]);
+ });
+
+ afterEach(() => {
+ jest.runOnlyPendingTimers();
+ jest.useRealTimers();
+ });
+
+ it('should display a success status when the synchronisation is a success', async () => {
+ computeEngineHandler.addTask({
+ status: TaskStatuses.Success,
+ executedAt: '2022-02-03T11:45:35+0200',
+ infoMessages: ['Test summary'],
+ type: TaskTypes.GitlabProvisioning,
+ });
+
+ renderAuthentication([Feature.GitlabProvisioning]);
+ expect(await gitlab.gitlabProvisioningSuccess.find()).toBeInTheDocument();
+ expect(gitlab.syncSummary.get()).toBeInTheDocument();
+ });
+
+ it('should display a success status even when another task is pending', async () => {
+ computeEngineHandler.addTask({
+ status: TaskStatuses.Pending,
+ executedAt: '2022-02-03T11:55:35+0200',
+ type: TaskTypes.GitlabProvisioning,
+ });
+ computeEngineHandler.addTask({
+ status: TaskStatuses.Success,
+ executedAt: '2022-02-03T11:45:35+0200',
+ type: TaskTypes.GitlabProvisioning,
+ });
+ renderAuthentication([Feature.GitlabProvisioning]);
+ expect(await gitlab.gitlabProvisioningSuccess.find()).toBeInTheDocument();
+ expect(gitlab.gitlabProvisioningPending.get()).toBeInTheDocument();
+ });
+
+ it('should display an error alert when the synchronisation failed', async () => {
+ computeEngineHandler.addTask({
+ status: TaskStatuses.Failed,
+ executedAt: '2022-02-03T11:45:35+0200',
+ errorMessage: "T'es mauvais Jacques",
+ type: TaskTypes.GitlabProvisioning,
+ });
+ renderAuthentication([Feature.GitlabProvisioning]);
+ expect(await gitlab.gitlabProvisioningAlert.find()).toBeInTheDocument();
+ expect(gitlab.autoProvisioningRadioButton.get()).toHaveTextContent("T'es mauvais Jacques");
+ expect(gitlab.gitlabProvisioningSuccess.query()).not.toBeInTheDocument();
+ });
+
+ it('should display an error alert even when another task is in progress', async () => {
+ computeEngineHandler.addTask({
+ status: TaskStatuses.InProgress,
+ executedAt: '2022-02-03T11:55:35+0200',
+ type: TaskTypes.GitlabProvisioning,
+ });
+ computeEngineHandler.addTask({
+ status: TaskStatuses.Failed,
+ executedAt: '2022-02-03T11:45:35+0200',
+ errorMessage: "T'es mauvais Jacques",
+ type: TaskTypes.GitlabProvisioning,
+ });
+ renderAuthentication([Feature.GitlabProvisioning]);
+ expect(await gitlab.gitlabProvisioningAlert.find()).toBeInTheDocument();
+ expect(gitlab.autoProvisioningRadioButton.get()).toHaveTextContent("T'es mauvais Jacques");
+ expect(gitlab.gitlabProvisioningSuccess.query()).not.toBeInTheDocument();
+ expect(gitlab.gitlabProvisioningInProgress.get()).toBeInTheDocument();
+ });
+
+ it('should show warning', async () => {
+ computeEngineHandler.addTask({
+ status: TaskStatuses.Success,
+ warnings: ['Warning'],
+ infoMessages: ['Test summary'],
+ type: TaskTypes.GitlabProvisioning,
+ });
+ renderAuthentication([Feature.GitlabProvisioning]);
+
+ expect(await gitlab.syncWarning.find()).toBeInTheDocument();
+ expect(gitlab.syncSummary.get()).toBeInTheDocument();
+ });
+ });
+});
+
const appLoaded = async () => {
await waitFor(async () => {
expect(await screen.findByText('loading')).not.toBeInTheDocument();
import { getGlobalSettingsUrl, getProjectSettingsUrl } from '../../helpers/urls';
import { AlmKeys } from '../../types/alm-settings';
import {
+ DefinitionV2,
ExtendedSettingDefinition,
Setting,
SettingDefinition,
value: any;
}
-export function getPropertyName(definition: SettingDefinition) {
+export function getPropertyName(definition: SettingDefinition | DefinitionV2) {
const key = `property.${definition.key}.name`;
if (hasMessage(key)) {
return translate(key);
return definition.name ?? definition.key;
}
-export function getPropertyDescription(definition: SettingDefinition) {
+export function getPropertyDescription(definition: SettingDefinition | DefinitionV2) {
const key = `property.${definition.key}.description`;
return hasMessage(key) ? translate(key) : definition.description;
}
].includes(definition.key);
}
-export function isSecuredDefinition(item: SettingDefinition): boolean {
- return item.key.endsWith('.secured');
+export function isSecuredDefinition(item: SettingDefinition | DefinitionV2): boolean {
+ return 'secured' in item ? item.secured : item.key.endsWith('.secured');
}
export function isCategoryDefinition(item: SettingDefinition): item is ExtendedSettingDefinition {
import { Helmet } from 'react-helmet-async';
import { getIdentityProviders } from '../../api/users';
import GitHubSynchronisationWarning from '../../app/components/GitHubSynchronisationWarning';
+import GitLabSynchronisationWarning from '../../app/components/GitLabSynchronisationWarning';
import HelpTooltip from '../../components/controls/HelpTooltip';
import ListFooter from '../../components/controls/ListFooter';
import { ManagedFilter } from '../../components/controls/ManagedFilter';
import SearchBox from '../../components/controls/SearchBox';
import Select, { LabelValueSelectOption } from '../../components/controls/Select';
import Suggestions from '../../components/embed-docs-modal/Suggestions';
-import { Provider, useManageProvider } from '../../components/hooks/useManageProvider';
import Spinner from '../../components/ui/Spinner';
import { now, toISO8601WithOffsetString } from '../../helpers/dates';
import { translate } from '../../helpers/l10n';
+import { useIdentityProviderQuery } from '../../queries/identity-provider';
import { useUsersQueries } from '../../queries/users';
-import { IdentityProvider } from '../../types/types';
+import { IdentityProvider, Provider } from '../../types/types';
import { RestUserDetailed } from '../../types/users';
import Header from './Header';
import UsersList from './UsersList';
const [usersActivity, setUsersActivity] = useState<UserActivity>(UserActivity.AnyActivity);
const [managed, setManaged] = useState<boolean | undefined>(undefined);
+ const { data: manageProvider } = useIdentityProviderQuery();
+
const usersActivityParams = useMemo(() => {
const nowDate = now();
const nowDateMinus30Days = subDays(nowDate, USER_INACTIVITY_DAYS_THRESHOLD);
const users = data?.pages.flatMap((page) => page.users) ?? [];
- const manageProvider = useManageProvider();
-
useEffect(() => {
(async () => {
const { identityProviders } = await getIdentityProviders();
<main className="page page-limited" id="users-page">
<Suggestions suggestions="users" />
<Helmet defer={false} title={translate('users.page')} />
- <Header manageProvider={manageProvider} />
- {manageProvider === Provider.Github && <GitHubSynchronisationWarning short />}
+ <Header manageProvider={manageProvider?.provider} />
+ {manageProvider?.provider === Provider.Github && <GitHubSynchronisationWarning short />}
+ {manageProvider?.provider === Provider.Gitlab && <GitLabSynchronisationWarning short />}
<div className="display-flex-justify-start big-spacer-bottom big-spacer-top">
<ManagedFilter
- manageProvider={manageProvider}
+ manageProvider={manageProvider?.provider}
loading={isLoading}
managed={managed}
setManaged={(m) => setManaged(m)}
<UsersList
identityProviders={identityProviders}
users={users}
- manageProvider={manageProvider}
+ manageProvider={manageProvider?.provider}
/>
</Spinner>
import * as React from 'react';
import HelpTooltip from '../../components/controls/HelpTooltip';
import { translate } from '../../helpers/l10n';
-import { IdentityProvider } from '../../types/types';
+import { IdentityProvider, Provider } from '../../types/types';
import { RestUserDetailed } from '../../types/users';
import UserListItem from './components/UserListItem';
interface Props {
identityProviders: IdentityProvider[];
users: RestUserDetailed[];
- manageProvider: string | undefined;
+ manageProvider: Provider | undefined;
}
export default function UsersList({ identityProviders, users, manageProvider }: Props) {
import SystemServiceMock from '../../../api/mocks/SystemServiceMock';
import UserTokensMock from '../../../api/mocks/UserTokensMock';
import UsersServiceMock from '../../../api/mocks/UsersServiceMock';
-import { Provider } from '../../../components/hooks/useManageProvider';
import { mockCurrentUser, mockLoggedInUser, mockRestUser } from '../../../helpers/testMocks';
import { renderApp } from '../../../helpers/testReactTestingUtils';
import { byLabelText, byRole, byText } from '../../../helpers/testSelector';
import { Feature } from '../../../types/features';
import { TaskStatuses } from '../../../types/tasks';
+import { Provider } from '../../../types/types';
import { ChangePasswordResults, CurrentUser } from '../../../types/users';
import UsersApp from '../UsersApp';
ActionsDropdownItem,
} from '../../../components/controls/ActionsDropdown';
import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { Provider } from '../../../types/types';
import { RestUserDetailed, isUserActive } from '../../../types/users';
import DeactivateForm from './DeactivateForm';
import PasswordForm from './PasswordForm';
interface Props {
user: RestUserDetailed;
- manageProvider: string | undefined;
+ manageProvider: Provider | undefined;
}
export default function UserActions(props: Props) {
import Spinner from '../../../components/ui/Spinner';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { useUserGroupsCountQuery, useUserTokensQuery } from '../../../queries/users';
-import { IdentityProvider } from '../../../types/types';
+import { IdentityProvider, Provider } from '../../../types/types';
import { RestUserDetailed } from '../../../types/users';
import GroupsForm from './GroupsForm';
import TokensFormModal from './TokensFormModal';
export interface UserListItemProps {
identityProvider?: IdentityProvider;
user: RestUserDetailed;
- manageProvider: string | undefined;
+ manageProvider: Provider | undefined;
}
export default function UserListItem(props: UserListItemProps) {
import { colors } from '../../../app/theme';
import { translate } from '../../../helpers/l10n';
import { getBaseUrl } from '../../../helpers/system';
-import { IdentityProvider } from '../../../types/types';
+import { IdentityProvider, Provider } from '../../../types/types';
import { RestUserDetailed } from '../../../types/users';
export interface Props {
identityProvider?: IdentityProvider;
user: RestUserDetailed;
- manageProvider?: string;
+ manageProvider: Provider | undefined;
}
export default function UserListItemIdentity({ identityProvider, user, manageProvider }: Props) {
*/
import * as React from 'react';
import { translate } from '../../helpers/l10n';
+import { Provider } from '../../types/types';
import ButtonToggle from './ButtonToggle';
interface ManagedFilterProps {
- manageProvider: string | undefined;
+ manageProvider: Provider | undefined;
loading: boolean;
managed: boolean | undefined;
setManaged: (managed: boolean | undefined) => void;
+++ /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 * as React from 'react';
-import { useEffect } from 'react';
-import { getSystemInfo } from '../../api/system';
-import { SysInfoCluster } from '../../types/types';
-
-export enum Provider {
- Github = 'GitHub',
- Scim = 'SCIM',
-}
-
-export function useManageProvider(): string | undefined {
- const [manageProvider, setManageProvider] = React.useState<Provider | undefined>();
-
- useEffect(() => {
- (async () => {
- const info = (await getSystemInfo()) as SysInfoCluster;
- setManageProvider(info.System['External Users and Groups Provisioning'] as Provider);
- })();
- }, []);
-
- return manageProvider;
-}
import { translate } from '../../helpers/l10n';
import { isPermissionDefinitionGroup } from '../../helpers/permissions';
import { getBaseUrl } from '../../helpers/system';
+import { useIdentityProviderQuery } from '../../queries/identity-provider';
import { Permissions } from '../../types/permissions';
-import { PermissionDefinitions, PermissionGroup } from '../../types/types';
+import { PermissionDefinitions, PermissionGroup, Provider } from '../../types/types';
import GroupIcon from '../icons/GroupIcon';
import PermissionCell from './PermissionCell';
import usePermissionChange from './usePermissionChange';
permissions,
removeOnly,
});
+ const { data: identityProvider } = useIdentityProviderQuery();
const description =
group.name === ANYONE ? translate('user_groups.anyone.description') : group.description;
<div className="sw-flex-1 sw-text-ellipsis sw-whitespace-nowrap sw-overflow-hidden sw-min-w-0">
<strong>{group.name}</strong>
</div>
- {isGitHubProject && group.managed && (
- <img
- alt="github"
- className="sw-my-2"
- aria-label={translate('project_permission.github_managed')}
- height={16}
- src={`${getBaseUrl()}/images/alm/github.svg`}
- />
- )}
+ {isGitHubProject &&
+ identityProvider?.provider === Provider.Github &&
+ group.managed && (
+ <img
+ alt="github"
+ className="sw-ml-2"
+ aria-label={translate('project_permission.github_managed')}
+ height={16}
+ src={`${getBaseUrl()}/images/alm/github.svg`}
+ />
+ )}
{group.name === ANYONE && (
<Badge className="sw-ml-2" variant="deleted">
{translate('deprecated')}
import { translate } from '../../helpers/l10n';
import { isPermissionDefinitionGroup } from '../../helpers/permissions';
import { getBaseUrl } from '../../helpers/system';
-import { PermissionDefinitions, PermissionUser } from '../../types/types';
+import { useIdentityProviderQuery } from '../../queries/identity-provider';
+import { PermissionDefinitions, PermissionUser, Provider } from '../../types/types';
import PermissionCell from './PermissionCell';
import usePermissionChange from './usePermissionChange';
permissions,
removeOnly,
});
+ const { data: identityProvider } = useIdentityProviderQuery();
const permissionCells = permissions.map((permission) => (
<PermissionCell
<strong>{user.name}</strong>
<Note className="sw-ml-2">{user.login}</Note>
</div>
- {isGitHubProject && user.managed && (
- <img
- alt="github"
- className="sw-my-2"
- height={16}
- aria-label={translate('project_permission.github_managed')}
- src={`${getBaseUrl()}/images/alm/github.svg`}
- />
- )}
+ {isGitHubProject &&
+ identityProvider?.provider === Provider.Github &&
+ user.managed && (
+ <img
+ alt="github"
+ className="sw-ml-2"
+ height={16}
+ aria-label={translate('project_permission.github_managed')}
+ src={`${getBaseUrl()}/images/alm/github.svg`}
+ />
+ )}
</div>
{user.email && (
<div className="sw-mt-2 sw-max-w-100 sw-text-ellipsis sw-whitespace-nowrap sw-overflow-hidden">
GithubRepository,
GitlabProject,
} from '../../types/alm-integration';
+import { GitlabConfiguration, ProvisioningType } from '../../types/provisioning';
export function mockAzureProject(overrides: Partial<AzureProject> = {}): AzureProject {
return {
...overrides,
};
}
+
+export function mockGitlabConfiguration(
+ overrides: Partial<GitlabConfiguration> = {},
+): GitlabConfiguration {
+ return {
+ id: Math.random().toString(),
+ enabled: false,
+ url: 'URL',
+ allowUsersToSignUp: false,
+ synchronizeUserGroups: true,
+ type: ProvisioningType.jit,
+ groups: [],
+ ...overrides,
+ };
+}
options: [],
fields: [],
},
+ {
+ key: 'provisioning.gitlab.token.secured',
+ name: 'Provisioning token',
+ description:
+ 'Token used for provisioning users. Both a group or a personal access token can be used as soon as it has visibility on desired groups.',
+ type: SettingType.PASSWORD,
+ category: 'authentication',
+ subCategory: 'gitlab',
+ options: [],
+ fields: [],
+ },
+ {
+ key: 'provisioning.gitlab.groups',
+ name: 'Groups',
+ description:
+ 'Only members of these groups (and sub-groups) will be able to authenticate to the server.',
+ category: 'authentication',
+ subCategory: 'gitlab',
+ multiValues: true,
+ options: [],
+ fields: [],
+ },
{
key: 'sonar.auth.saml.loginUrl',
name: 'SAML login url',
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { isEqual, keyBy, partition, pick, unionBy } from 'lodash';
import { useContext } from 'react';
+import { getActivity } from '../api/ce';
import {
activateGithubProvisioning,
activateScim,
addGithubRolesMapping,
checkConfigurationValidity,
+ createGitLabConfiguration,
deactivateGithubProvisioning,
deactivateScim,
+ deleteGitLabConfiguration,
deleteGithubRolesMapping,
+ fetchGitLabConfigurations,
fetchGithubProvisioningStatus,
fetchGithubRolesMapping,
fetchIsScimEnabled,
syncNowGithubProvisioning,
+ updateGitLabConfiguration,
updateGithubRolesMapping,
} from '../api/provisioning';
import { getSystemInfo } from '../api/system';
import { translate } from '../helpers/l10n';
import { mapReactQueryResult } from '../helpers/react-query';
import { Feature } from '../types/features';
-import { GitHubMapping } from '../types/provisioning';
+import { AlmSyncStatus, GitHubMapping } from '../types/provisioning';
+import { TaskStatuses, TaskTypes } from '../types/tasks';
import { SysInfoCluster } from '../types/types';
const MAPPING_STALE_TIME = 60_000;
},
});
}
+
+export function useGitLabConfigurationsQuery() {
+ return useQuery(['identity_provider', 'gitlab_config', 'list'], fetchGitLabConfigurations);
+}
+
+export function useCreateGitLabConfigurationMutation() {
+ const client = useQueryClient();
+ return useMutation({
+ mutationFn: (data: Parameters<typeof createGitLabConfiguration>[0]) =>
+ createGitLabConfiguration(data),
+ onSuccess(data) {
+ client.setQueryData(['identity_provider', 'gitlab_config', 'list'], {
+ configurations: [data],
+ page: {
+ pageIndex: 1,
+ pageSize: 1,
+ total: 1,
+ },
+ });
+ },
+ });
+}
+
+export function useUpdateGitLabConfigurationMutation() {
+ const client = useQueryClient();
+ return useMutation({
+ mutationFn: ({
+ id,
+ data,
+ }: {
+ id: Parameters<typeof updateGitLabConfiguration>[0];
+ data: Parameters<typeof updateGitLabConfiguration>[1];
+ }) => updateGitLabConfiguration(id, data),
+ onSuccess(data) {
+ client.invalidateQueries({ queryKey: ['identity_provider'] });
+ client.setQueryData(['identity_provider', 'gitlab_config', 'list'], {
+ configurations: [data],
+ page: {
+ pageIndex: 1,
+ pageSize: 1,
+ total: 1,
+ },
+ });
+ },
+ });
+}
+
+export function useDeleteGitLabConfigurationMutation() {
+ const client = useQueryClient();
+ return useMutation({
+ mutationFn: (id: Parameters<typeof deleteGitLabConfiguration>[0]) =>
+ deleteGitLabConfiguration(id),
+ onSuccess() {
+ client.setQueryData(['identity_provider', 'gitlab_config', 'list'], {
+ configurations: [],
+ page: {
+ pageIndex: 1,
+ pageSize: 1,
+ total: 0,
+ },
+ });
+ },
+ });
+}
+
+export function useGitLabSyncStatusQuery() {
+ const getLastSync = async () => {
+ const lastSyncTasks = await getActivity({
+ type: TaskTypes.GitlabProvisioning,
+ p: 1,
+ ps: 1,
+ status: [TaskStatuses.Success, TaskStatuses.Failed, TaskStatuses.Canceled].join(','),
+ });
+ const lastSync = lastSyncTasks?.tasks[0];
+ if (!lastSync) {
+ return undefined;
+ }
+ const summary = lastSync.infoMessages ? lastSync.infoMessages?.join(', ') : '';
+ const errorMessage = lastSync.errorMessage ?? '';
+ return {
+ executionTimeMs: lastSync?.executionTimeMs ?? 0,
+ startedAt: +new Date(lastSync?.startedAt ?? 0),
+ finishedAt: +new Date(lastSync?.startedAt ?? 0) + (lastSync?.executionTimeMs ?? 0),
+ warningMessage:
+ lastSync.warnings && lastSync.warnings.length > 0
+ ? lastSync.warnings?.join(', ')
+ : undefined,
+ status: lastSync?.status as
+ | TaskStatuses.Success
+ | TaskStatuses.Failed
+ | TaskStatuses.Canceled,
+ ...(lastSync.status === TaskStatuses.Success ? { summary } : {}),
+ ...(lastSync.status !== TaskStatuses.Success ? { errorMessage } : {}),
+ };
+ };
+
+ const getNextSync = async () => {
+ const nextSyncTasks = await getActivity({
+ type: TaskTypes.GitlabProvisioning,
+ p: 1,
+ ps: 1,
+ status: [TaskStatuses.Pending, TaskStatuses.InProgress].join(','),
+ });
+ const nextSync = nextSyncTasks?.tasks[0];
+ if (!nextSync) {
+ return undefined;
+ }
+ return { status: nextSync.status as TaskStatuses.Pending | TaskStatuses.InProgress };
+ };
+
+ return useQuery(
+ ['identity_provider', 'gitlab_sync', 'status'],
+ async () => {
+ const [lastSync, nextSync] = await Promise.all([getLastSync(), getNextSync()]);
+ return {
+ lastSync,
+ nextSync,
+ } as AlmSyncStatus;
+ },
+ {
+ refetchInterval: 10_000,
+ },
+ );
+}
RegulatoryReport = 'regulatory-reports',
Scim = 'scim',
GithubProvisioning = 'github-provisioning',
+ GitlabProvisioning = 'gitlab-provisioning',
}
nextSync?: never;
lastSync?: never;
};
-export type GithubStatusEnabled = {
+export interface GithubStatusEnabled extends AlmSyncStatus {
enabled: true;
+}
+
+export interface AlmSyncStatus {
nextSync?: { status: TaskStatuses.Pending | TaskStatuses.InProgress };
lastSync?: {
executionTimeMs: number;
errorMessage?: string;
}
);
-};
+}
export type GithubStatus = GithubStatusDisabled | GithubStatusEnabled;
scan: boolean;
};
}
+
+export interface GitLabConfigurationCreateBody {
+ applicationId: string;
+ url: string;
+ clientSecret: string;
+ synchronizeUserGroups: boolean;
+}
+
+export type GitLabConfigurationUpdateBody = {
+ applicationId?: string;
+ url?: string;
+ clientSecret?: string;
+ synchronizeUserGroups?: boolean;
+ enabled?: boolean;
+ type?: ProvisioningType;
+ provisioningToken?: string;
+ groups?: string[];
+ allowUsersToSignUp?: boolean;
+};
+
+export type GitlabConfiguration = {
+ id: string;
+ enabled: boolean;
+ synchronizeUserGroups: boolean;
+ url: string;
+ type: ProvisioningType;
+ groups: string[];
+ allowUsersToSignUp: boolean;
+};
+
+export enum ProvisioningType {
+ jit = 'JIT',
+ auto = 'Auto',
+}
subCategory: string;
}
+export interface DefinitionV2 {
+ name: string;
+ key: string;
+ description?: string;
+ secured: boolean;
+ multiValues?: boolean;
+ type?: SettingType;
+}
+
export interface SettingValueResponse {
settings: SettingValue[];
setSecuredSettings: string[];
IssueSync = 'ISSUE_SYNC',
GithubProvisioning = 'GITHUB_AUTH_PROVISIONING',
GithubProjectPermissionsProvisioning = 'GITHUB_PROJECT_PERMISSIONS_PROVISIONING',
+ GitlabProvisioning = 'GITLAB_AUTH_PROVISIONING',
AppRefresh = 'APP_REFRESH',
ViewRefresh = 'VIEW_REFRESH',
ProjectExport = 'PROJECT_EXPORT',
type: TaskTypes;
warningCount?: number;
warnings?: string[];
+ infoMessages?: string[];
}
export interface TaskWarning {
};
}
+export enum Provider {
+ Github = 'github',
+ Gitlab = 'gitlab',
+ Scim = 'SCIM',
+}
+
export interface SysInfoCluster extends SysInfoBase {
'Application Nodes': SysInfoAppNode[];
'Search Nodes': SysInfoSearchNode[];
'High Availability': true;
'Server ID': string;
Version: string;
- 'External Users and Groups Provisioning'?: string;
+ 'External Users and Groups Provisioning'?: Provider;
};
}
settings.authentication.github.configuration.roles_mapping.save_success=GitHub roles mapping saved successfully.
settings.authentication.github.configuration.unsaved_changes=You have unsaved changes.
+# GITLAB
+settings.authentication.gitlab.configuration=GitLab Configuration
+settings.authentication.gitlab.form.not_configured=GitLab App is not configured
+settings.authentication.gitlab.form.create=New GitLab Configuration
+settings.authentication.gitlab.form.edit=Edit GitLab Configuration
+settings.authentication.gitlab.form.applicationId.name=Application ID
+settings.authentication.gitlab.form.applicationId.description=Application ID provided by GitLab when registering the application.
+settings.authentication.gitlab.form.url.name=GitLab URL
+settings.authentication.gitlab.form.url.description=URL to access GitLab.
+settings.authentication.gitlab.form.clientSecret.name=Secret
+settings.authentication.gitlab.form.clientSecret.description=Secret provided by GitLab when registering the application.
+settings.authentication.gitlab.form.synchronizeUserGroups.name=Synchronize user groups
+settings.authentication.gitlab.form.synchronizeUserGroups.description=For each GitLab group they belong to, the user will be associated to a group with the same name (if it exists) in SonarQube. If enabled, the GitLab Oauth2 application will need to provide the api scope.
+settings.authentication.gitlab.provisioning_at_login=Just-in-Time user provisioning (default)
+settings.authentication.gitlab.provisioning_at_login.description=Users are synchronized only when users log in to SonarQube.
+settings.authentication.gitlab.description.doc=For more details, see {documentation}.
+settings.authentication.gitlab.confirm.Auto=Switch to automatic provisioning
+settings.authentication.gitlab.confirm.JIT=Switch to Just-in-Time provisioning
+settings.authentication.gitlab.confirm.Auto.description=Once you transition to automatic provisioning users on GitLab projects will be inherited from GitLab. You will no longer have the ability to edit them within SonarQube. Do you want to proceed with this change?
+settings.authentication.gitlab.confirm.JIT.description=Switching to Just-in-Time provisioning removes the automatic synchronization of users. Users are provisioned and updated only at user login. Are you sure?
+settings.authentication.gitlab.provisioning_change.confirm_changes=Confirm Changes
+settings.authentication.gitlab.form.provisioning_with_gitlab=Automatic user provisioning
+settings.authentication.gitlab.form.provisioning_with_gitlab.description=Users are automatically provisioned from your GitLab organizations. Once activated, users can only be created and modified from your GitLab groups. Existing local users will be kept and can only be deactivated.
+settings.authentication.gitlab.form.provisioning.disabled=Your current edition does not support provisioning with GitLab. See the {documentation} for more information.
+settings.authentication.gitlab.configuration.unsaved_changes=You have unsaved changes.
+
# SAML
settings.authentication.form.create.saml=New SAML configuration
settings.authentication.form.edit.saml=Edit SAML configuration
background_task.type.PROJECT_IMPORT=Project Import
background_task.type.AUDIT_PURGE=Audit Log Purge
background_task.type.REPORT_SUBMIT=Report Email Submit
-background_task.type.GITHUB_AUTH_PROVISIONING=Github Provisioning
-background_task.type.GITHUB_PROJECT_PERMISSIONS_PROVISIONING=Github Project Permission Sync
+background_task.type.GITHUB_AUTH_PROVISIONING=GitHub Provisioning
+background_task.type.GITHUB_PROJECT_PERMISSIONS_PROVISIONING=GitHub Project Permission Sync
+background_task.type.GITLAB_AUTH_PROVISIONING=GitLab Provisioning
background_tasks.page=Background Tasks
background_tasks.page.description=This page allows monitoring of the queue of tasks running asynchronously on the server. It also gives access to the history of finished tasks and their status. Analysis report processing is the most common kind of background task.