aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorguillaume-peoch-sonarsource <guillaume.peoch@sonarsource.com>2023-11-30 12:09:06 +0100
committersonartech <sonartech@sonarsource.com>2023-12-22 20:03:01 +0000
commite4e087dd7877352e2d77f9531535258224820e9f (patch)
tree1ac7d7cc6a7645b796a1702500e2829f8dc63d09 /server
parent3cbfd8163ffdc42f89631382031dd115c0df65b6 (diff)
downloadsonarqube-e4e087dd7877352e2d77f9531535258224820e9f.tar.gz
sonarqube-e4e087dd7877352e2d77f9531535258224820e9f.zip
SONAR-21119 UI for GitLab Authentication tab with users and groups provisioning
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/api/mocks/AuthenticationServiceMock.ts62
-rw-r--r--server/sonar-web/src/main/js/api/mocks/GroupsServiceMock.ts3
-rw-r--r--server/sonar-web/src/main/js/api/mocks/SystemServiceMock.ts3
-rw-r--r--server/sonar-web/src/main/js/api/provisioning.ts135
-rw-r--r--server/sonar-web/src/main/js/api/settings.ts2
-rw-r--r--server/sonar-web/src/main/js/app/components/AlmSynchronisationWarning.tsx167
-rw-r--r--server/sonar-web/src/main/js/app/components/GitHubSynchronisationWarning.tsx141
-rw-r--r--server/sonar-web/src/main/js/app/components/GitLabSynchronisationWarning.tsx (renamed from server/sonar-web/src/main/js/components/hooks/useManageProvider.ts)29
-rw-r--r--server/sonar-web/src/main/js/apps/groups/GroupsApp.tsx13
-rw-r--r--server/sonar-web/src/main/js/apps/groups/__tests__/GroupsApp-it.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/Header.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/List.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/PermissionsProject-it.tsx353
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx12
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationFormField.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationMultiValuesField.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationSecuredField.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationToggleField.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/GitLabAuthenticationTab.tsx443
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/GitLabConfigurationForm.tsx207
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx526
-rw-r--r--server/sonar-web/src/main/js/apps/settings/utils.ts9
-rw-r--r--server/sonar-web/src/main/js/apps/users/UsersApp.tsx18
-rw-r--r--server/sonar-web/src/main/js/apps/users/UsersList.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/UserActions.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/UserListItem.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx4
-rw-r--r--server/sonar-web/src/main/js/components/controls/ManagedFilter.tsx3
-rw-r--r--server/sonar-web/src/main/js/components/permissions/GroupHolder.tsx24
-rw-r--r--server/sonar-web/src/main/js/components/permissions/UserHolder.tsx24
-rw-r--r--server/sonar-web/src/main/js/helpers/mocks/alm-integrations.ts16
-rw-r--r--server/sonar-web/src/main/js/helpers/mocks/definitions-list.ts22
-rw-r--r--server/sonar-web/src/main/js/queries/identity-provider.ts132
-rw-r--r--server/sonar-web/src/main/js/types/features.ts1
-rw-r--r--server/sonar-web/src/main/js/types/provisioning.ts41
-rw-r--r--server/sonar-web/src/main/js/types/settings.ts9
-rw-r--r--server/sonar-web/src/main/js/types/tasks.ts2
-rw-r--r--server/sonar-web/src/main/js/types/types.ts8
42 files changed, 2023 insertions, 442 deletions
diff --git a/server/sonar-web/src/main/js/api/mocks/AuthenticationServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/AuthenticationServiceMock.ts
index c4664fc7ddc..d382f32c511 100644
--- a/server/sonar-web/src/main/js/api/mocks/AuthenticationServiceMock.ts
+++ b/server/sonar-web/src/main/js/api/mocks/AuthenticationServiceMock.ts
@@ -17,12 +17,15 @@
* 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 {
@@ -30,12 +33,17 @@ import {
activateScim,
addGithubRolesMapping,
checkConfigurationValidity,
+ createGitLabConfiguration,
deactivateGithubProvisioning,
deactivateScim,
+ deleteGitLabConfiguration,
deleteGithubRolesMapping,
+ fetchGitLabConfiguration,
+ fetchGitLabConfigurations,
fetchGithubProvisioningStatus,
fetchGithubRolesMapping,
fetchIsScimEnabled,
+ updateGitLabConfiguration,
updateGithubRolesMapping,
} from '../provisioning';
@@ -63,6 +71,10 @@ const defaultConfigurationStatus: GitHubConfigurationStatus = {
],
};
+const defaultGitlabConfiguration: GitlabConfiguration[] = [
+ mockGitlabConfiguration({ id: '1', enabled: true }),
+];
+
const githubMappingMock = (
id: string,
permissions: (keyof GitHubMapping['permissions'])[],
@@ -107,6 +119,7 @@ export default class AuthenticationServiceMock {
githubConfigurationStatus: GitHubConfigurationStatus;
githubMapping: GitHubMapping[];
tasks: Task[];
+ gitlabConfigurations: GitlabConfiguration[];
constructor() {
this.scimStatus = false;
@@ -114,6 +127,7 @@ export default class AuthenticationServiceMock {
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);
@@ -133,6 +147,11 @@ export default class AuthenticationServiceMock {
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'>> = {}) => {
@@ -244,11 +263,52 @@ export default class AuthenticationServiceMock {
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);
};
}
diff --git a/server/sonar-web/src/main/js/api/mocks/GroupsServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/GroupsServiceMock.ts
index fbd14e24eb6..46a2d5e1063 100644
--- a/server/sonar-web/src/main/js/api/mocks/GroupsServiceMock.ts
+++ b/server/sonar-web/src/main/js/api/mocks/GroupsServiceMock.ts
@@ -19,14 +19,13 @@
*/
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');
diff --git a/server/sonar-web/src/main/js/api/mocks/SystemServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/SystemServiceMock.ts
index ca31714c1dc..6c9b3c67324 100644
--- a/server/sonar-web/src/main/js/api/mocks/SystemServiceMock.ts
+++ b/server/sonar-web/src/main/js/api/mocks/SystemServiceMock.ts
@@ -18,10 +18,9 @@
* 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';
diff --git a/server/sonar-web/src/main/js/api/provisioning.ts b/server/sonar-web/src/main/js/api/provisioning.ts
index e8c7ea90dc1..27f5a31ca01 100644
--- a/server/sonar-web/src/main/js/api/provisioning.ts
+++ b/server/sonar-web/src/main/js/api/provisioning.ts
@@ -18,9 +18,20 @@
* 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';
@@ -81,3 +92,125 @@ export function addGithubRolesMapping(data: Omit<GitHubMapping, 'id'>) {
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(',') });
+}
diff --git a/server/sonar-web/src/main/js/api/settings.ts b/server/sonar-web/src/main/js/api/settings.ts
index 2c2f5111526..53580197b9b 100644
--- a/server/sonar-web/src/main/js/api/settings.ts
+++ b/server/sonar-web/src/main/js/api/settings.ts
@@ -86,7 +86,7 @@ export function setSettingValue(
}
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);
}
diff --git a/server/sonar-web/src/main/js/app/components/AlmSynchronisationWarning.tsx b/server/sonar-web/src/main/js/app/components/AlmSynchronisationWarning.tsx
new file mode 100644
index 00000000000..83e933e4858
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/AlmSynchronisationWarning.tsx
@@ -0,0 +1,167 @@
+/*
+ * 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} />
+ </>
+ );
+}
diff --git a/server/sonar-web/src/main/js/app/components/GitHubSynchronisationWarning.tsx b/server/sonar-web/src/main/js/app/components/GitHubSynchronisationWarning.tsx
index 09860d1d703..22dcf68356e 100644
--- a/server/sonar-web/src/main/js/app/components/GitHubSynchronisationWarning.tsx
+++ b/server/sonar-web/src/main/js/app/components/GitHubSynchronisationWarning.tsx
@@ -17,156 +17,23 @@
* 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;
diff --git a/server/sonar-web/src/main/js/components/hooks/useManageProvider.ts b/server/sonar-web/src/main/js/app/components/GitLabSynchronisationWarning.tsx
index e91145365b1..227e2e55129 100644
--- a/server/sonar-web/src/main/js/components/hooks/useManageProvider.ts
+++ b/server/sonar-web/src/main/js/app/components/GitLabSynchronisationWarning.tsx
@@ -17,26 +17,23 @@
* 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';
+import { useGitLabSyncStatusQuery } from '../../queries/identity-provider';
+import AlmSynchronisationWarning from './AlmSynchronisationWarning';
+import './SystemAnnouncement.css';
-export enum Provider {
- Github = 'GitHub',
- Scim = 'SCIM',
+interface Props {
+ short?: boolean;
}
-export function useManageProvider(): string | undefined {
- const [manageProvider, setManageProvider] = React.useState<Provider | undefined>();
+function GitLabSynchronisationWarning({ short }: Readonly<Props>) {
+ const { data } = useGitLabSyncStatusQuery();
- useEffect(() => {
- (async () => {
- const info = (await getSystemInfo()) as SysInfoCluster;
- setManageProvider(info.System['External Users and Groups Provisioning'] as Provider);
- })();
- }, []);
+ if (!data) {
+ return null;
+ }
- return manageProvider;
+ return <AlmSynchronisationWarning short={short} data={data} />;
}
+
+export default GitLabSynchronisationWarning;
diff --git a/server/sonar-web/src/main/js/apps/groups/GroupsApp.tsx b/server/sonar-web/src/main/js/apps/groups/GroupsApp.tsx
index 6aef407edc8..4927e76985c 100644
--- a/server/sonar-web/src/main/js/apps/groups/GroupsApp.tsx
+++ b/server/sonar-web/src/main/js/apps/groups/GroupsApp.tsx
@@ -25,9 +25,10 @@ import ListFooter from '../../components/controls/ListFooter';
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';
@@ -35,7 +36,7 @@ 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,
@@ -49,12 +50,12 @@ export default function GroupsApp() {
<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}
@@ -68,7 +69,7 @@ export default function GroupsApp() {
/>
</div>
- <List groups={groups} manageProvider={manageProvider} />
+ <List groups={groups} manageProvider={manageProvider?.provider} />
<div id="groups-list-footer">
<ListFooter
diff --git a/server/sonar-web/src/main/js/apps/groups/__tests__/GroupsApp-it.tsx b/server/sonar-web/src/main/js/apps/groups/__tests__/GroupsApp-it.tsx
index 265da70be5f..f1fbe9bdaae 100644
--- a/server/sonar-web/src/main/js/apps/groups/__tests__/GroupsApp-it.tsx
+++ b/server/sonar-web/src/main/js/apps/groups/__tests__/GroupsApp-it.tsx
@@ -26,12 +26,12 @@ import GroupMembershipsServiceMock from '../../../api/mocks/GroupMembersipsServi
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();
@@ -275,6 +275,7 @@ describe('in manage mode', () => {
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();
});
diff --git a/server/sonar-web/src/main/js/apps/groups/components/Header.tsx b/server/sonar-web/src/main/js/apps/groups/components/Header.tsx
index f285ff37c38..70f64ed4912 100644
--- a/server/sonar-web/src/main/js/apps/groups/components/Header.tsx
+++ b/server/sonar-web/src/main/js/apps/groups/components/Header.tsx
@@ -23,14 +23,14 @@ import DocLink from '../../../components/common/DocLink';
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 (
diff --git a/server/sonar-web/src/main/js/apps/groups/components/List.tsx b/server/sonar-web/src/main/js/apps/groups/components/List.tsx
index b699b7eb70a..e90e3cf1cf0 100644
--- a/server/sonar-web/src/main/js/apps/groups/components/List.tsx
+++ b/server/sonar-web/src/main/js/apps/groups/components/List.tsx
@@ -20,12 +20,12 @@
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) {
diff --git a/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx b/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx
index 57452ea1138..8b35206f5a6 100644
--- a/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx
+++ b/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx
@@ -24,18 +24,17 @@ import ActionsDropdown, {
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) {
diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/PermissionsProject-it.tsx b/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/PermissionsProject-it.tsx
index 5b95545450d..a36e2ca6b33 100644
--- a/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/PermissionsProject-it.tsx
+++ b/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/PermissionsProject-it.tsx
@@ -23,6 +23,7 @@ import userEvent from '@testing-library/user-event';
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 {
@@ -41,17 +42,19 @@ 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(() => {
@@ -237,195 +240,205 @@ it('should correctly handle pagination', async () => {
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(
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx
index a095444c20b..12d5d723408 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx
@@ -37,6 +37,7 @@ import { Feature } from '../../../../types/features';
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';
@@ -112,10 +113,11 @@ export function Authentication(props: Props & WithAvailableFeaturesProps) {
},
] 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],
);
@@ -161,7 +163,7 @@ export function Authentication(props: Props & WithAvailableFeaturesProps) {
<div
style={{
maxHeight:
- tab.key !== SAML && tab.key !== AlmKeys.GitHub
+ tab.key === AlmKeys.BitbucketServer
? `calc(100vh - ${top + HEIGHT_ADJUSTMENT}px)`
: '',
}}
@@ -183,7 +185,11 @@ export function Authentication(props: Props & WithAvailableFeaturesProps) {
/>
)}
- {tab.key !== SAML && tab.key !== AlmKeys.GitHub && (
+ {tab.key === AlmKeys.GitLab && (
+ <GitLabAuthenticationTab definitions={gitlabDefinitions} />
+ )}
+
+ {tab.key === AlmKeys.BitbucketServer && (
<>
<Alert variant="info">
<FormattedMessage
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationFormField.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationFormField.tsx
index 26d50fe903d..8a8ca9d5501 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationFormField.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationFormField.tsx
@@ -21,23 +21,23 @@ import React from 'react';
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);
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationMultiValuesField.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationMultiValuesField.tsx
index 74caf05ff9d..e818928729c 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationMultiValuesField.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationMultiValuesField.tsx
@@ -20,13 +20,13 @@
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) {
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationSecuredField.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationSecuredField.tsx
index 1b2f2baa811..7074bb257e9 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationSecuredField.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationSecuredField.tsx
@@ -20,13 +20,13 @@
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;
}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationToggleField.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationToggleField.tsx
index cb82310df92..df75a345fbc 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationToggleField.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationToggleField.tsx
@@ -19,12 +19,12 @@
*/
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) {
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/GitLabAuthenticationTab.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/GitLabAuthenticationTab.tsx
new file mode 100644
index 00000000000..03fa8f27e11
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/GitLabAuthenticationTab.tsx
@@ -0,0 +1,443 @@
+/*
+ * 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>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/GitLabConfigurationForm.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/GitLabConfigurationForm.tsx
new file mode 100644
index 00000000000..0aa4379247c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/GitLabConfigurationForm.tsx
@@ -0,0 +1,207 @@
+/*
+ * 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>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx
index 665e5c027e6..4c2e271de26 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx
@@ -25,7 +25,6 @@ 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 { Provider } from '../../../../components/hooks/useManageProvider';
import DeleteIcon from '../../../../components/icons/DeleteIcon';
import EditIcon from '../../../../components/icons/EditIcon';
import { Alert } from '../../../../components/ui/Alert';
@@ -37,6 +36,7 @@ import {
} 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';
@@ -223,7 +223,6 @@ export default function GithubAuthenticationTab(props: GithubAuthenticationProps
<p className="spacer-bottom">
<FormattedMessage
id="settings.authentication.github.form.description.doc"
- tagName="p"
values={{
documentation: (
<DocLink
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx
index c0d63db413e..a191a859e52 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx
@@ -24,7 +24,6 @@ import Link from '../../../../components/common/Link';
import ConfirmModal from '../../../../components/controls/ConfirmModal';
import RadioCard from '../../../../components/controls/RadioCard';
import { Button, ResetButtonLink, SubmitButton } from '../../../../components/controls/buttons';
-import { Provider } from '../../../../components/hooks/useManageProvider';
import CheckIcon from '../../../../components/icons/CheckIcon';
import DeleteIcon from '../../../../components/icons/DeleteIcon';
import EditIcon from '../../../../components/icons/EditIcon';
@@ -36,6 +35,7 @@ import {
} 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,
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx
index 639d12b5545..f8d1a8684d5 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx
@@ -26,12 +26,13 @@ import ComputeEngineServiceMock from '../../../../../api/mocks/ComputeEngineServ
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;
@@ -71,6 +72,10 @@ afterEach(() => {
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'),
@@ -80,7 +85,9 @@ const ui = {
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', {
@@ -93,10 +100,16 @@ const ui = {
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',
@@ -118,7 +131,7 @@ const ui = {
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());
},
@@ -126,10 +139,16 @@ const ui = {
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',
}),
@@ -138,17 +157,27 @@ const ui = {
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', {
@@ -181,35 +210,35 @@ const ui = {
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', {
@@ -240,7 +269,7 @@ const ui = {
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());
@@ -263,6 +292,78 @@ const ui = {
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 () => {
@@ -306,7 +407,7 @@ describe('SAML tab', () => {
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);
@@ -384,7 +485,7 @@ describe('Github tab', () => {
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();
@@ -1062,6 +1163,359 @@ describe('Github tab', () => {
});
});
+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();
diff --git a/server/sonar-web/src/main/js/apps/settings/utils.ts b/server/sonar-web/src/main/js/apps/settings/utils.ts
index 7b02ab120e5..dd3a9ec98b6 100644
--- a/server/sonar-web/src/main/js/apps/settings/utils.ts
+++ b/server/sonar-web/src/main/js/apps/settings/utils.ts
@@ -24,6 +24,7 @@ import { hasMessage, translate } from '../../helpers/l10n';
import { getGlobalSettingsUrl, getProjectSettingsUrl } from '../../helpers/urls';
import { AlmKeys } from '../../types/alm-settings';
import {
+ DefinitionV2,
ExtendedSettingDefinition,
Setting,
SettingDefinition,
@@ -57,7 +58,7 @@ export interface DefaultInputProps {
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);
@@ -66,7 +67,7 @@ export function getPropertyName(definition: SettingDefinition) {
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;
}
@@ -148,8 +149,8 @@ export function isURLKind(definition: SettingDefinition) {
].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 {
diff --git a/server/sonar-web/src/main/js/apps/users/UsersApp.tsx b/server/sonar-web/src/main/js/apps/users/UsersApp.tsx
index f2a4f0d1538..70b7cfc9ca6 100644
--- a/server/sonar-web/src/main/js/apps/users/UsersApp.tsx
+++ b/server/sonar-web/src/main/js/apps/users/UsersApp.tsx
@@ -22,18 +22,19 @@ import React, { useEffect, useMemo, useState } from 'react';
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';
@@ -46,6 +47,8 @@ export default function UsersApp() {
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);
@@ -78,8 +81,6 @@ export default function UsersApp() {
const users = data?.pages.flatMap((page) => page.users) ?? [];
- const manageProvider = useManageProvider();
-
useEffect(() => {
(async () => {
const { identityProviders } = await getIdentityProviders();
@@ -91,11 +92,12 @@ export default function UsersApp() {
<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)}
@@ -136,7 +138,7 @@ export default function UsersApp() {
<UsersList
identityProviders={identityProviders}
users={users}
- manageProvider={manageProvider}
+ manageProvider={manageProvider?.provider}
/>
</Spinner>
diff --git a/server/sonar-web/src/main/js/apps/users/UsersList.tsx b/server/sonar-web/src/main/js/apps/users/UsersList.tsx
index 5420e2c3a80..4899bdef2bb 100644
--- a/server/sonar-web/src/main/js/apps/users/UsersList.tsx
+++ b/server/sonar-web/src/main/js/apps/users/UsersList.tsx
@@ -20,14 +20,14 @@
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) {
diff --git a/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx b/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx
index a9e090eaf1a..6fb3a709866 100644
--- a/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx
+++ b/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx
@@ -28,12 +28,12 @@ import SettingsServiceMock from '../../../api/mocks/SettingsServiceMock';
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';
diff --git a/server/sonar-web/src/main/js/apps/users/components/UserActions.tsx b/server/sonar-web/src/main/js/apps/users/components/UserActions.tsx
index e71502ae7b9..a46bf37d7be 100644
--- a/server/sonar-web/src/main/js/apps/users/components/UserActions.tsx
+++ b/server/sonar-web/src/main/js/apps/users/components/UserActions.tsx
@@ -23,6 +23,7 @@ import ActionsDropdown, {
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';
@@ -30,7 +31,7 @@ import UserForm from './UserForm';
interface Props {
user: RestUserDetailed;
- manageProvider: string | undefined;
+ manageProvider: Provider | undefined;
}
export default function UserActions(props: Props) {
diff --git a/server/sonar-web/src/main/js/apps/users/components/UserListItem.tsx b/server/sonar-web/src/main/js/apps/users/components/UserListItem.tsx
index 59cddecdc25..9ea6301a7b2 100644
--- a/server/sonar-web/src/main/js/apps/users/components/UserListItem.tsx
+++ b/server/sonar-web/src/main/js/apps/users/components/UserListItem.tsx
@@ -25,7 +25,7 @@ import LegacyAvatar from '../../../components/ui/LegacyAvatar';
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';
@@ -36,7 +36,7 @@ import UserScmAccounts from './UserScmAccounts';
export interface UserListItemProps {
identityProvider?: IdentityProvider;
user: RestUserDetailed;
- manageProvider: string | undefined;
+ manageProvider: Provider | undefined;
}
export default function UserListItem(props: UserListItemProps) {
diff --git a/server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx b/server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx
index c432e59b623..8a873882854 100644
--- a/server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx
+++ b/server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx
@@ -23,13 +23,13 @@ import * as React from 'react';
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) {
diff --git a/server/sonar-web/src/main/js/components/controls/ManagedFilter.tsx b/server/sonar-web/src/main/js/components/controls/ManagedFilter.tsx
index 4f25eeb57e1..c328bbd3fbe 100644
--- a/server/sonar-web/src/main/js/components/controls/ManagedFilter.tsx
+++ b/server/sonar-web/src/main/js/components/controls/ManagedFilter.tsx
@@ -19,10 +19,11 @@
*/
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;
diff --git a/server/sonar-web/src/main/js/components/permissions/GroupHolder.tsx b/server/sonar-web/src/main/js/components/permissions/GroupHolder.tsx
index f8d07b0ae40..434258ca1aa 100644
--- a/server/sonar-web/src/main/js/components/permissions/GroupHolder.tsx
+++ b/server/sonar-web/src/main/js/components/permissions/GroupHolder.tsx
@@ -22,8 +22,9 @@ import * as React from 'react';
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';
@@ -57,6 +58,7 @@ export default function GroupHolder(props: Props) {
permissions,
removeOnly,
});
+ const { data: identityProvider } = useIdentityProviderQuery();
const description =
group.name === ANYONE ? translate('user_groups.anyone.description') : group.description;
@@ -71,15 +73,17 @@ export default function GroupHolder(props: Props) {
<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')}
diff --git a/server/sonar-web/src/main/js/components/permissions/UserHolder.tsx b/server/sonar-web/src/main/js/components/permissions/UserHolder.tsx
index 0b11501bd10..acbb275b506 100644
--- a/server/sonar-web/src/main/js/components/permissions/UserHolder.tsx
+++ b/server/sonar-web/src/main/js/components/permissions/UserHolder.tsx
@@ -22,7 +22,8 @@ import * as React from 'react';
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';
@@ -44,6 +45,7 @@ export default function UserHolder(props: Props) {
permissions,
removeOnly,
});
+ const { data: identityProvider } = useIdentityProviderQuery();
const permissionCells = permissions.map((permission) => (
<PermissionCell
@@ -90,15 +92,17 @@ export default function UserHolder(props: Props) {
<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">
diff --git a/server/sonar-web/src/main/js/helpers/mocks/alm-integrations.ts b/server/sonar-web/src/main/js/helpers/mocks/alm-integrations.ts
index 2e5bec68af0..fb3e7206f2d 100644
--- a/server/sonar-web/src/main/js/helpers/mocks/alm-integrations.ts
+++ b/server/sonar-web/src/main/js/helpers/mocks/alm-integrations.ts
@@ -26,6 +26,7 @@ import {
GithubRepository,
GitlabProject,
} from '../../types/alm-integration';
+import { GitlabConfiguration, ProvisioningType } from '../../types/provisioning';
export function mockAzureProject(overrides: Partial<AzureProject> = {}): AzureProject {
return {
@@ -98,3 +99,18 @@ export function mockGitlabProject(overrides: Partial<GitlabProject> = {}): Gitla
...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,
+ };
+}
diff --git a/server/sonar-web/src/main/js/helpers/mocks/definitions-list.ts b/server/sonar-web/src/main/js/helpers/mocks/definitions-list.ts
index b981aeb97ac..05a75dff539 100644
--- a/server/sonar-web/src/main/js/helpers/mocks/definitions-list.ts
+++ b/server/sonar-web/src/main/js/helpers/mocks/definitions-list.ts
@@ -255,6 +255,28 @@ export const definitions: ExtendedSettingDefinition[] = [
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',
description: 'The URL where the Identity Provider expects to receive SAML requests.',
diff --git a/server/sonar-web/src/main/js/queries/identity-provider.ts b/server/sonar-web/src/main/js/queries/identity-provider.ts
index c37a6248494..68bddee21bc 100644
--- a/server/sonar-web/src/main/js/queries/identity-provider.ts
+++ b/server/sonar-web/src/main/js/queries/identity-provider.ts
@@ -21,18 +21,23 @@
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';
@@ -41,7 +46,8 @@ import { addGlobalSuccessMessage } from '../helpers/globalMessages';
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;
@@ -183,3 +189,127 @@ export function useGithubRolesMappingMutation() {
},
});
}
+
+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,
+ },
+ );
+}
diff --git a/server/sonar-web/src/main/js/types/features.ts b/server/sonar-web/src/main/js/types/features.ts
index fdeb6ab31a5..0b4758da867 100644
--- a/server/sonar-web/src/main/js/types/features.ts
+++ b/server/sonar-web/src/main/js/types/features.ts
@@ -27,4 +27,5 @@ export enum Feature {
RegulatoryReport = 'regulatory-reports',
Scim = 'scim',
GithubProvisioning = 'github-provisioning',
+ GitlabProvisioning = 'gitlab-provisioning',
}
diff --git a/server/sonar-web/src/main/js/types/provisioning.ts b/server/sonar-web/src/main/js/types/provisioning.ts
index e3ff30b3b9c..9a7700dd840 100644
--- a/server/sonar-web/src/main/js/types/provisioning.ts
+++ b/server/sonar-web/src/main/js/types/provisioning.ts
@@ -25,8 +25,11 @@ export type GithubStatusDisabled = {
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;
@@ -45,7 +48,7 @@ export type GithubStatusEnabled = {
errorMessage?: string;
}
);
-};
+}
export type GithubStatus = GithubStatusDisabled | GithubStatusEnabled;
@@ -89,3 +92,37 @@ export interface GitHubMapping {
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',
+}
diff --git a/server/sonar-web/src/main/js/types/settings.ts b/server/sonar-web/src/main/js/types/settings.ts
index 7eef37418c3..93cea170308 100644
--- a/server/sonar-web/src/main/js/types/settings.ts
+++ b/server/sonar-web/src/main/js/types/settings.ts
@@ -89,6 +89,15 @@ export interface ExtendedSettingDefinition extends SettingDefinition {
subCategory: string;
}
+export interface DefinitionV2 {
+ name: string;
+ key: string;
+ description?: string;
+ secured: boolean;
+ multiValues?: boolean;
+ type?: SettingType;
+}
+
export interface SettingValueResponse {
settings: SettingValue[];
setSecuredSettings: string[];
diff --git a/server/sonar-web/src/main/js/types/tasks.ts b/server/sonar-web/src/main/js/types/tasks.ts
index 1b8998f0b35..a6fa74513ff 100644
--- a/server/sonar-web/src/main/js/types/tasks.ts
+++ b/server/sonar-web/src/main/js/types/tasks.ts
@@ -22,6 +22,7 @@ export enum TaskTypes {
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',
@@ -63,6 +64,7 @@ export interface Task {
type: TaskTypes;
warningCount?: number;
warnings?: string[];
+ infoMessages?: string[];
}
export interface TaskWarning {
diff --git a/server/sonar-web/src/main/js/types/types.ts b/server/sonar-web/src/main/js/types/types.ts
index eb2e4f8bc03..3ad82365053 100644
--- a/server/sonar-web/src/main/js/types/types.ts
+++ b/server/sonar-web/src/main/js/types/types.ts
@@ -741,6 +741,12 @@ export interface SysInfoBase extends SysInfoValueObject {
};
}
+export enum Provider {
+ Github = 'github',
+ Gitlab = 'gitlab',
+ Scim = 'SCIM',
+}
+
export interface SysInfoCluster extends SysInfoBase {
'Application Nodes': SysInfoAppNode[];
'Search Nodes': SysInfoSearchNode[];
@@ -752,7 +758,7 @@ export interface SysInfoCluster extends SysInfoBase {
'High Availability': true;
'Server ID': string;
Version: string;
- 'External Users and Groups Provisioning'?: string;
+ 'External Users and Groups Provisioning'?: Provider;
};
}