--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import axios from 'axios';
+import { throwGlobalError } from '../helpers/error';
+import { getJSON, post, postJSON } from '../helpers/request';
+import { GitHubConfigurationStatus, GitHubMapping, GithubStatus } from '../types/provisioning';
+
+const GITHUB_PERMISSION_MAPPINGS = '/api/v2/dop-translation/github-permission-mappings';
+
+export function fetchGithubProvisioningStatus(): Promise<GithubStatus> {
+ return getJSON('/api/github_provisioning/status').catch(throwGlobalError);
+}
+
+export function activateGithubProvisioning(): Promise<void> {
+ return post('/api/github_provisioning/enable').catch(throwGlobalError);
+}
+
+export function deactivateGithubProvisioning(): Promise<void> {
+ return post('/api/github_provisioning/disable').catch(throwGlobalError);
+}
+
+export function checkConfigurationValidity(): Promise<GitHubConfigurationStatus> {
+ return postJSON('/api/github_provisioning/check').catch(throwGlobalError);
+}
+
+export function syncNowGithubProvisioning(): Promise<void> {
+ return post('/api/github_provisioning/sync').catch(throwGlobalError);
+}
+
+export function fetchGithubRolesMapping() {
+ return axios
+ .get<{ githubPermissionsMappings: GitHubMapping[] }>(GITHUB_PERMISSION_MAPPINGS)
+ .then((data) => data.githubPermissionsMappings);
+}
+
+export function updateGithubRolesMapping(
+ role: string,
+ data: Partial<Pick<GitHubMapping, 'permissions'>>,
+) {
+ return axios.patch<GitHubMapping>(
+ `${GITHUB_PERMISSION_MAPPINGS}/${encodeURIComponent(role)}`,
+ data,
+ );
+}
+
+export function addGithubRolesMapping(data: Omit<GitHubMapping, 'id'>) {
+ return axios.post<GitHubMapping>(GITHUB_PERMISSION_MAPPINGS, data);
+}
+
+export function deleteGithubRolesMapping(role: string) {
+ return axios.delete(`${GITHUB_PERMISSION_MAPPINGS}/${encodeURIComponent(role)}`);
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import axios from 'axios';
+import {
+ GitLabConfigurationCreateBody,
+ GitLabConfigurationUpdateBody,
+ GitlabConfiguration,
+ ProvisioningType,
+} from '../types/provisioning';
+import { Paging } from '../types/types';
+
+const GITLAB_CONFIGURATIONS = '/api/v2/dop-translation/gitlab-configurations';
+
+export function fetchGitLabConfigurations() {
+ return axios.get<{ page: Paging; gitlabConfigurations: GitlabConfiguration[] }>(
+ GITLAB_CONFIGURATIONS,
+ );
+}
+
+export function fetchGitLabConfiguration(id: string): Promise<GitlabConfiguration> {
+ return axios.get<GitlabConfiguration>(`${GITLAB_CONFIGURATIONS}/${id}`);
+}
+
+export function createGitLabConfiguration(
+ data: GitLabConfigurationCreateBody,
+): Promise<GitlabConfiguration> {
+ return axios.post(GITLAB_CONFIGURATIONS, {
+ ...data,
+ synchronizationType: ProvisioningType.jit,
+ allowUsersToSignUp: false,
+ enabled: true,
+ });
+}
+
+export function updateGitLabConfiguration(
+ id: string,
+ data: Partial<GitLabConfigurationUpdateBody>,
+) {
+ return axios.patch<GitlabConfiguration>(`${GITLAB_CONFIGURATIONS}/${id}`, data);
+}
+
+export function deleteGitLabConfiguration(id: string): Promise<void> {
+ return axios.delete(`${GITLAB_CONFIGURATIONS}/${id}`);
+}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { 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 {
- activateGithubProvisioning,
- activateScim,
- addGithubRolesMapping,
- checkConfigurationValidity,
- createGitLabConfiguration,
- deactivateGithubProvisioning,
- deactivateScim,
- deleteGitLabConfiguration,
- deleteGithubRolesMapping,
- fetchGitLabConfiguration,
- fetchGitLabConfigurations,
- fetchGithubProvisioningStatus,
- fetchGithubRolesMapping,
- fetchIsScimEnabled,
- updateGitLabConfiguration,
- updateGithubRolesMapping,
-} from '../provisioning';
-
-jest.mock('../provisioning');
-
-const defaultConfigurationStatus: GitHubConfigurationStatus = {
- application: {
- jit: {
- status: GitHubProvisioningStatus.Success,
- },
- autoProvisioning: {
- status: GitHubProvisioningStatus.Success,
- },
- },
- installations: [
- {
- organization: 'testOrg',
- autoProvisioning: {
- status: GitHubProvisioningStatus.Success,
- },
- jit: {
- status: GitHubProvisioningStatus.Success,
- },
- },
- ],
-};
-
-const defaultGitlabConfiguration: GitlabConfiguration[] = [
- mockGitlabConfiguration({ id: '1', enabled: true }),
-];
-
-const githubMappingMock = (
- id: string,
- permissions: (keyof GitHubMapping['permissions'])[],
- isBaseRole = false,
-) => ({
- id,
- githubRole: id,
- isBaseRole,
- permissions: {
- user: permissions.includes('user'),
- codeViewer: permissions.includes('codeViewer'),
- issueAdmin: permissions.includes('issueAdmin'),
- securityHotspotAdmin: permissions.includes('securityHotspotAdmin'),
- admin: permissions.includes('admin'),
- scan: permissions.includes('scan'),
- },
-});
-
-const defaultMapping: GitHubMapping[] = [
- githubMappingMock('read', ['user', 'codeViewer'], true),
- githubMappingMock(
- 'write',
- ['user', 'codeViewer', 'issueAdmin', 'securityHotspotAdmin', 'scan'],
- true,
- ),
- githubMappingMock('triage', ['user', 'codeViewer'], true),
- githubMappingMock(
- 'maintain',
- ['user', 'codeViewer', 'issueAdmin', 'securityHotspotAdmin', 'scan'],
- true,
- ),
- githubMappingMock(
- 'admin',
- ['user', 'codeViewer', 'issueAdmin', 'securityHotspotAdmin', 'admin', 'scan'],
- true,
- ),
-];
-
-export default class AuthenticationServiceMock {
- scimStatus: boolean;
- githubProvisioningStatus: boolean;
- githubConfigurationStatus: GitHubConfigurationStatus;
- githubMapping: GitHubMapping[];
- tasks: Task[];
- gitlabConfigurations: GitlabConfiguration[];
-
- constructor() {
- this.scimStatus = false;
- this.githubProvisioningStatus = false;
- this.githubConfigurationStatus = cloneDeep(defaultConfigurationStatus);
- this.githubMapping = cloneDeep(defaultMapping);
- this.tasks = [];
- this.gitlabConfigurations = cloneDeep(defaultGitlabConfiguration);
- jest.mocked(activateScim).mockImplementation(this.handleActivateScim);
- jest.mocked(deactivateScim).mockImplementation(this.handleDeactivateScim);
- jest.mocked(fetchIsScimEnabled).mockImplementation(this.handleFetchIsScimEnabled);
- jest
- .mocked(activateGithubProvisioning)
- .mockImplementation(this.handleActivateGithubProvisioning);
- jest
- .mocked(deactivateGithubProvisioning)
- .mockImplementation(this.handleDeactivateGithubProvisioning);
- jest
- .mocked(fetchGithubProvisioningStatus)
- .mockImplementation(this.handleFetchGithubProvisioningStatus);
- jest
- .mocked(checkConfigurationValidity)
- .mockImplementation(this.handleCheckConfigurationValidity);
- jest.mocked(fetchGithubRolesMapping).mockImplementation(this.handleFetchGithubRolesMapping);
- jest.mocked(updateGithubRolesMapping).mockImplementation(this.handleUpdateGithubRolesMapping);
- jest.mocked(addGithubRolesMapping).mockImplementation(this.handleAddGithubRolesMapping);
- jest.mocked(deleteGithubRolesMapping).mockImplementation(this.handleDeleteGithubRolesMapping);
- jest.mocked(fetchGitLabConfigurations).mockImplementation(this.handleFetchGitLabConfigurations);
- jest.mocked(fetchGitLabConfiguration).mockImplementation(this.handleFetchGitLabConfiguration);
- jest.mocked(createGitLabConfiguration).mockImplementation(this.handleCreateGitLabConfiguration);
- jest.mocked(updateGitLabConfiguration).mockImplementation(this.handleUpdateGitLabConfiguration);
- jest.mocked(deleteGitLabConfiguration).mockImplementation(this.handleDeleteGitLabConfiguration);
- }
-
- addProvisioningTask = (overrides: Partial<Omit<Task, 'type'>> = {}) => {
- this.tasks.push(
- mockTask({
- id: Math.random().toString(),
- type: TaskTypes.GithubProvisioning,
- ...overrides,
- }),
- );
- };
-
- setConfigurationValidity = (overrides: Partial<GitHubConfigurationStatus> = {}) => {
- this.githubConfigurationStatus = {
- ...this.githubConfigurationStatus,
- ...overrides,
- };
- };
-
- enableGithubProvisioning = () => {
- this.scimStatus = false;
- this.githubProvisioningStatus = true;
- };
-
- handleActivateScim = () => {
- this.scimStatus = true;
- return Promise.resolve();
- };
-
- handleDeactivateScim = () => {
- this.scimStatus = false;
- return Promise.resolve();
- };
-
- handleFetchIsScimEnabled = () => {
- return Promise.resolve(this.scimStatus);
- };
-
- handleActivateGithubProvisioning = () => {
- this.githubProvisioningStatus = true;
- return Promise.resolve();
- };
-
- handleDeactivateGithubProvisioning = () => {
- this.githubProvisioningStatus = false;
- return Promise.resolve();
- };
-
- handleFetchGithubProvisioningStatus = () => {
- if (!this.githubProvisioningStatus) {
- return Promise.resolve({ enabled: false });
- }
-
- const nextSync = this.tasks.find((t: Task) =>
- [TaskStatuses.InProgress, TaskStatuses.Pending].includes(t.status),
- );
- const lastSync = this.tasks.find(
- (t: Task) => ![TaskStatuses.InProgress, TaskStatuses.Pending].includes(t.status),
- );
-
- return Promise.resolve({
- enabled: true,
- nextSync: nextSync ? { status: nextSync.status } : undefined,
- lastSync: lastSync
- ? {
- status: lastSync.status,
- finishedAt: lastSync.executedAt,
- startedAt: lastSync.startedAt,
- executionTimeMs: lastSync.executionTimeMs,
- summary: lastSync.status === TaskStatuses.Success ? 'Test summary' : undefined,
- errorMessage: lastSync.errorMessage,
- warningMessage: lastSync.warnings?.join() ?? undefined,
- }
- : undefined,
- });
- };
-
- handleCheckConfigurationValidity = () => {
- return Promise.resolve(this.githubConfigurationStatus);
- };
-
- handleFetchGithubRolesMapping: typeof fetchGithubRolesMapping = () => {
- return Promise.resolve(this.githubMapping);
- };
-
- handleUpdateGithubRolesMapping: typeof updateGithubRolesMapping = (id, data) => {
- this.githubMapping = this.githubMapping.map((mapping) =>
- mapping.id === id ? { ...mapping, ...data } : mapping,
- );
-
- return Promise.resolve(
- this.githubMapping.find((mapping) => mapping.id === id) as GitHubMapping,
- );
- };
-
- handleAddGithubRolesMapping: typeof addGithubRolesMapping = (data) => {
- const newRole = { ...data, id: data.githubRole };
- this.githubMapping = [...this.githubMapping, newRole];
-
- return Promise.resolve(newRole);
- };
-
- handleDeleteGithubRolesMapping: typeof deleteGithubRolesMapping = (id) => {
- this.githubMapping = this.githubMapping.filter((el) => el.id !== id);
- return Promise.resolve();
- };
-
- addGitHubCustomRole = (id: string, permissions: (keyof GitHubMapping['permissions'])[]) => {
- 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);
- };
-}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { cloneDeep } from 'lodash';
+import { mockTask } from '../../helpers/mocks/tasks';
+import {
+ GitHubConfigurationStatus,
+ GitHubMapping,
+ GitHubProvisioningStatus,
+} from '../../types/provisioning';
+import { Task, TaskStatuses, TaskTypes } from '../../types/tasks';
+import {
+ activateGithubProvisioning,
+ addGithubRolesMapping,
+ checkConfigurationValidity,
+ deactivateGithubProvisioning,
+ deleteGithubRolesMapping,
+ fetchGithubProvisioningStatus,
+ fetchGithubRolesMapping,
+ updateGithubRolesMapping,
+} from '../github-provisioning';
+
+jest.mock('../github-provisioning');
+
+const defaultConfigurationStatus: GitHubConfigurationStatus = {
+ application: {
+ jit: {
+ status: GitHubProvisioningStatus.Success,
+ },
+ autoProvisioning: {
+ status: GitHubProvisioningStatus.Success,
+ },
+ },
+ installations: [
+ {
+ organization: 'testOrg',
+ autoProvisioning: {
+ status: GitHubProvisioningStatus.Success,
+ },
+ jit: {
+ status: GitHubProvisioningStatus.Success,
+ },
+ },
+ ],
+};
+
+const githubMappingMock = (
+ id: string,
+ permissions: (keyof GitHubMapping['permissions'])[],
+ isBaseRole = false,
+) => ({
+ id,
+ githubRole: id,
+ isBaseRole,
+ permissions: {
+ user: permissions.includes('user'),
+ codeViewer: permissions.includes('codeViewer'),
+ issueAdmin: permissions.includes('issueAdmin'),
+ securityHotspotAdmin: permissions.includes('securityHotspotAdmin'),
+ admin: permissions.includes('admin'),
+ scan: permissions.includes('scan'),
+ },
+});
+
+const defaultMapping: GitHubMapping[] = [
+ githubMappingMock('read', ['user', 'codeViewer'], true),
+ githubMappingMock(
+ 'write',
+ ['user', 'codeViewer', 'issueAdmin', 'securityHotspotAdmin', 'scan'],
+ true,
+ ),
+ githubMappingMock('triage', ['user', 'codeViewer'], true),
+ githubMappingMock(
+ 'maintain',
+ ['user', 'codeViewer', 'issueAdmin', 'securityHotspotAdmin', 'scan'],
+ true,
+ ),
+ githubMappingMock(
+ 'admin',
+ ['user', 'codeViewer', 'issueAdmin', 'securityHotspotAdmin', 'admin', 'scan'],
+ true,
+ ),
+];
+
+export default class GithubProvisioningServiceMock {
+ githubProvisioningStatus: boolean;
+ githubConfigurationStatus: GitHubConfigurationStatus;
+ githubMapping: GitHubMapping[];
+ tasks: Task[];
+
+ constructor() {
+ this.githubProvisioningStatus = false;
+ this.githubConfigurationStatus = cloneDeep(defaultConfigurationStatus);
+ this.githubMapping = cloneDeep(defaultMapping);
+ this.tasks = [];
+ jest
+ .mocked(activateGithubProvisioning)
+ .mockImplementation(this.handleActivateGithubProvisioning);
+ jest
+ .mocked(deactivateGithubProvisioning)
+ .mockImplementation(this.handleDeactivateGithubProvisioning);
+ jest
+ .mocked(fetchGithubProvisioningStatus)
+ .mockImplementation(this.handleFetchGithubProvisioningStatus);
+ jest
+ .mocked(checkConfigurationValidity)
+ .mockImplementation(this.handleCheckConfigurationValidity);
+ jest.mocked(fetchGithubRolesMapping).mockImplementation(this.handleFetchGithubRolesMapping);
+ jest.mocked(updateGithubRolesMapping).mockImplementation(this.handleUpdateGithubRolesMapping);
+ jest.mocked(addGithubRolesMapping).mockImplementation(this.handleAddGithubRolesMapping);
+ jest.mocked(deleteGithubRolesMapping).mockImplementation(this.handleDeleteGithubRolesMapping);
+ }
+
+ addProvisioningTask = (overrides: Partial<Omit<Task, 'type'>> = {}) => {
+ this.tasks.push(
+ mockTask({
+ id: Math.random().toString(),
+ type: TaskTypes.GithubProvisioning,
+ ...overrides,
+ }),
+ );
+ };
+
+ setConfigurationValidity = (overrides: Partial<GitHubConfigurationStatus> = {}) => {
+ this.githubConfigurationStatus = {
+ ...this.githubConfigurationStatus,
+ ...overrides,
+ };
+ };
+
+ enableGithubProvisioning = () => {
+ this.githubProvisioningStatus = true;
+ };
+
+ handleActivateGithubProvisioning = () => {
+ this.githubProvisioningStatus = true;
+ return Promise.resolve();
+ };
+
+ handleDeactivateGithubProvisioning = () => {
+ this.githubProvisioningStatus = false;
+ return Promise.resolve();
+ };
+
+ handleFetchGithubProvisioningStatus = () => {
+ if (!this.githubProvisioningStatus) {
+ return Promise.resolve({ enabled: false });
+ }
+
+ const nextSync = this.tasks.find((t: Task) =>
+ [TaskStatuses.InProgress, TaskStatuses.Pending].includes(t.status),
+ );
+ const lastSync = this.tasks.find(
+ (t: Task) => ![TaskStatuses.InProgress, TaskStatuses.Pending].includes(t.status),
+ );
+
+ return Promise.resolve({
+ enabled: true,
+ nextSync: nextSync ? { status: nextSync.status } : undefined,
+ lastSync: lastSync
+ ? {
+ status: lastSync.status,
+ finishedAt: lastSync.executedAt,
+ startedAt: lastSync.startedAt,
+ executionTimeMs: lastSync.executionTimeMs,
+ summary: lastSync.status === TaskStatuses.Success ? 'Test summary' : undefined,
+ errorMessage: lastSync.errorMessage,
+ warningMessage: lastSync.warnings?.join() ?? undefined,
+ }
+ : undefined,
+ });
+ };
+
+ handleCheckConfigurationValidity = () => {
+ return Promise.resolve(this.githubConfigurationStatus);
+ };
+
+ handleFetchGithubRolesMapping: typeof fetchGithubRolesMapping = () => {
+ return Promise.resolve(this.githubMapping);
+ };
+
+ handleUpdateGithubRolesMapping: typeof updateGithubRolesMapping = (id, data) => {
+ this.githubMapping = this.githubMapping.map((mapping) =>
+ mapping.id === id ? { ...mapping, ...data } : mapping,
+ );
+
+ return Promise.resolve(
+ this.githubMapping.find((mapping) => mapping.id === id) as GitHubMapping,
+ );
+ };
+
+ handleAddGithubRolesMapping: typeof addGithubRolesMapping = (data) => {
+ const newRole = { ...data, id: data.githubRole };
+ this.githubMapping = [...this.githubMapping, newRole];
+
+ return Promise.resolve(newRole);
+ };
+
+ handleDeleteGithubRolesMapping: typeof deleteGithubRolesMapping = (id) => {
+ this.githubMapping = this.githubMapping.filter((el) => el.id !== id);
+ return Promise.resolve();
+ };
+
+ addGitHubCustomRole = (id: string, permissions: (keyof GitHubMapping['permissions'])[]) => {
+ this.githubMapping = [...this.githubMapping, githubMappingMock(id, permissions)];
+ };
+
+ reset = () => {
+ this.githubProvisioningStatus = false;
+ this.githubConfigurationStatus = cloneDeep(defaultConfigurationStatus);
+ this.githubMapping = cloneDeep(defaultMapping);
+ this.tasks = [];
+ };
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { cloneDeep, omit } from 'lodash';
+import { mockGitlabConfiguration } from '../../helpers/mocks/alm-integrations';
+import { mockPaging } from '../../helpers/testMocks';
+import { GitlabConfiguration } from '../../types/provisioning';
+import {
+ createGitLabConfiguration,
+ deleteGitLabConfiguration,
+ fetchGitLabConfiguration,
+ fetchGitLabConfigurations,
+ updateGitLabConfiguration,
+} from '../gitlab-provisioning';
+
+jest.mock('../gitlab-provisioning');
+
+const defaultGitlabConfiguration: GitlabConfiguration[] = [
+ mockGitlabConfiguration({ id: '1', enabled: true }),
+];
+
+export default class GitlabProvisioningServiceMock {
+ gitlabConfigurations: GitlabConfiguration[];
+
+ constructor() {
+ this.gitlabConfigurations = cloneDeep(defaultGitlabConfiguration);
+ 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);
+ }
+
+ handleFetchGitLabConfigurations: typeof fetchGitLabConfigurations = () => {
+ return Promise.resolve({
+ gitlabConfigurations: 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.gitlabConfigurations = cloneDeep(defaultGitlabConfiguration);
+ };
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { activateScim, deactivateScim, fetchIsScimEnabled } from '../scim-provisioning';
+
+jest.mock('../scim-provisioning');
+
+export default class ScimProvisioningServiceMock {
+ scimStatus: boolean;
+
+ constructor() {
+ this.scimStatus = false;
+ jest.mocked(activateScim).mockImplementation(this.handleActivateScim);
+ jest.mocked(deactivateScim).mockImplementation(this.handleDeactivateScim);
+ jest.mocked(fetchIsScimEnabled).mockImplementation(this.handleFetchIsScimEnabled);
+ }
+
+ handleActivateScim = () => {
+ this.scimStatus = true;
+ return Promise.resolve();
+ };
+
+ handleDeactivateScim = () => {
+ this.scimStatus = false;
+ return Promise.resolve();
+ };
+
+ handleFetchIsScimEnabled = () => {
+ return Promise.resolve(this.scimStatus);
+ };
+
+ reset = () => {
+ this.scimStatus = false;
+ };
+}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import axios from 'axios';
-import { keyBy } from 'lodash';
-import { throwGlobalError } from '../helpers/error';
-import { getJSON, post, postJSON } from '../helpers/request';
-import {
- GitHubConfigurationStatus,
- GitHubMapping,
- GitLabConfigurationCreateBody,
- GitLabConfigurationUpdateBody,
- GithubStatus,
- GitlabConfiguration,
- ProvisioningType,
-} from '../types/provisioning';
-import { Paging } from '../types/types';
-import { getValues, resetSettingValue, setSimpleSettingValue } from './settings';
-
-const GITHUB_PERMISSION_MAPPINGS = '/api/v2/dop-translation/github-permission-mappings';
-
-export function fetchIsScimEnabled(): Promise<boolean> {
- return getJSON('/api/scim_management/status')
- .then((r) => r.enabled)
- .catch(throwGlobalError);
-}
-
-export function activateScim(): Promise<void> {
- return post('/api/scim_management/enable').catch(throwGlobalError);
-}
-
-export function deactivateScim(): Promise<void> {
- return post('/api/scim_management/disable').catch(throwGlobalError);
-}
-
-export function fetchGithubProvisioningStatus(): Promise<GithubStatus> {
- return getJSON('/api/github_provisioning/status').catch(throwGlobalError);
-}
-
-export function activateGithubProvisioning(): Promise<void> {
- return post('/api/github_provisioning/enable').catch(throwGlobalError);
-}
-
-export function deactivateGithubProvisioning(): Promise<void> {
- return post('/api/github_provisioning/disable').catch(throwGlobalError);
-}
-
-export function checkConfigurationValidity(): Promise<GitHubConfigurationStatus> {
- return postJSON('/api/github_provisioning/check').catch(throwGlobalError);
-}
-
-export function syncNowGithubProvisioning(): Promise<void> {
- return post('/api/github_provisioning/sync').catch(throwGlobalError);
-}
-
-export function fetchGithubRolesMapping() {
- return axios
- .get<{ githubPermissionsMappings: GitHubMapping[] }>(GITHUB_PERMISSION_MAPPINGS)
- .then((data) => data.githubPermissionsMappings);
-}
-
-export function updateGithubRolesMapping(
- role: string,
- data: Partial<Pick<GitHubMapping, 'permissions'>>,
-) {
- return axios.patch<GitHubMapping>(
- `${GITHUB_PERMISSION_MAPPINGS}/${encodeURIComponent(role)}`,
- data,
- );
-}
-
-export function addGithubRolesMapping(data: Omit<GitHubMapping, 'id'>) {
- return axios.post<GitHubMapping>(GITHUB_PERMISSION_MAPPINGS, data);
-}
-
-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(',') });
-}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { throwGlobalError } from '../helpers/error';
+import { getJSON, post } from '../helpers/request';
+
+export function fetchIsScimEnabled(): Promise<boolean> {
+ return getJSON('/api/scim_management/status')
+ .then((r) => r.enabled)
+ .catch(throwGlobalError);
+}
+
+export function activateScim(): Promise<void> {
+ return post('/api/scim_management/enable').catch(throwGlobalError);
+}
+
+export function deactivateScim(): Promise<void> {
+ return post('/api/scim_management/disable').catch(throwGlobalError);
+}
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { useGitHubSyncStatusQuery } from '../../queries/identity-provider';
+import { useGitHubSyncStatusQuery } from '../../queries/identity-provider/github';
import AlmSynchronisationWarning from './AlmSynchronisationWarning';
import './SystemAnnouncement.css';
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { useGitLabSyncStatusQuery } from '../../queries/identity-provider';
+import { useGitLabSyncStatusQuery } from '../../queries/identity-provider/gitlab';
import AlmSynchronisationWarning from './AlmSynchronisationWarning';
import './SystemAnnouncement.css';
import Suggestions from '../../components/embed-docs-modal/Suggestions';
import { translate } from '../../helpers/l10n';
import { useGroupsQueries } from '../../queries/groups';
-import { useIdentityProviderQuery } from '../../queries/identity-provider';
+import { useIdentityProviderQuery } from '../../queries/identity-provider/common';
import { Provider } from '../../types/types';
import Header from './components/Header';
import List from './components/List';
import { screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';
-import AuthenticationServiceMock from '../../../api/mocks/AuthenticationServiceMock';
+import GithubProvisioningServiceMock from '../../../api/mocks/GithubProvisioningServiceMock';
import GroupMembershipsServiceMock from '../../../api/mocks/GroupMembersipsServiceMock';
import GroupsServiceMock from '../../../api/mocks/GroupsServiceMock';
import SystemServiceMock from '../../../api/mocks/SystemServiceMock';
const handler = new GroupsServiceMock();
const groupMembershipsHandler = new GroupMembershipsServiceMock();
const userHandler = new UsersServiceMock(groupMembershipsHandler);
-const authenticationHandler = new AuthenticationServiceMock();
+const githubHandler = new GithubProvisioningServiceMock();
const ui = {
createGroupButton: byRole('button', { name: 'groups.create_group' }),
beforeEach(() => {
handler.reset();
systemHandler.reset();
- authenticationHandler.reset();
+ githubHandler.reset();
userHandler.reset();
groupMembershipsHandler.reset();
groupMembershipsHandler.memberships = [
describe('Github Provisioning', () => {
beforeEach(() => {
- authenticationHandler.handleActivateGithubProvisioning();
+ githubHandler.handleActivateGithubProvisioning();
systemHandler.setProvider(Provider.Github);
});
it('should display a success status when the synchronisation is a success', async () => {
- authenticationHandler.addProvisioningTask({
+ githubHandler.addProvisioningTask({
status: TaskStatuses.Success,
executedAt: '2022-02-03T11:45:35+0200',
});
});
it('should display a success status even when another task is pending', async () => {
- authenticationHandler.addProvisioningTask({
+ githubHandler.addProvisioningTask({
status: TaskStatuses.Pending,
executedAt: '2022-02-03T11:55:35+0200',
});
- authenticationHandler.addProvisioningTask({
+ githubHandler.addProvisioningTask({
status: TaskStatuses.Success,
executedAt: '2022-02-03T11:45:35+0200',
});
});
it('should display an error alert when the synchronisation failed', async () => {
- authenticationHandler.addProvisioningTask({
+ githubHandler.addProvisioningTask({
status: TaskStatuses.Failed,
executedAt: '2022-02-03T11:45:35+0200',
errorMessage: "T'es mauvais Jacques",
});
it('should display an error alert even when another task is in progress', async () => {
- authenticationHandler.addProvisioningTask({
+ githubHandler.addProvisioningTask({
status: TaskStatuses.InProgress,
executedAt: '2022-02-03T11:55:35+0200',
});
- authenticationHandler.addProvisioningTask({
+ githubHandler.addProvisioningTask({
status: TaskStatuses.Failed,
executedAt: '2022-02-03T11:45:35+0200',
errorMessage: "T'es mauvais Jacques",
import Spinner from '../../../components/ui/Spinner';
import { throwGlobalError } from '../../../helpers/error';
import { translate } from '../../../helpers/l10n';
-import { useGithubProvisioningEnabledQuery } from '../../../queries/identity-provider';
+import { useGithubProvisioningEnabledQuery } from '../../../queries/identity-provider/github';
import { PERMISSION_TEMPLATES_PATH } from '../utils';
import Form from './Form';
PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE,
convertToPermissionDefinitions,
} from '../../../helpers/permissions';
-import { useGithubProvisioningEnabledQuery } from '../../../queries/identity-provider';
+import { useGithubProvisioningEnabledQuery } from '../../../queries/identity-provider/github';
import { Paging, PermissionGroup, PermissionTemplate, PermissionUser } from '../../../types/types';
import TemplateDetails from './TemplateDetails';
import TemplateHeader from './TemplateHeader';
import userEvent from '@testing-library/user-event';
import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
import { uniq } from 'lodash';
-import AuthenticationServiceMock from '../../../../api/mocks/AuthenticationServiceMock';
+import GithubProvisioningServiceMock from '../../../../api/mocks/GithubProvisioningServiceMock';
import PermissionsServiceMock from '../../../../api/mocks/PermissionsServiceMock';
import { mockPermissionGroup, mockPermissionUser } from '../../../../helpers/mocks/permissions';
import { PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE } from '../../../../helpers/permissions';
import routes from '../../routes';
const serviceMock = new PermissionsServiceMock();
-const authServiceMock = new AuthenticationServiceMock();
+const githubHandler = new GithubProvisioningServiceMock();
beforeEach(() => {
serviceMock.reset();
- authServiceMock.reset();
+ githubHandler.reset();
});
describe('rendering', () => {
it('should show github warning', async () => {
const user = userEvent.setup();
const ui = getPageObject(user);
- authServiceMock.githubProvisioningStatus = true;
+ githubHandler.githubProvisioningStatus = true;
renderPermissionTemplatesApp(undefined, [Feature.GithubProvisioning]);
expect(await ui.githubWarning.find()).toBeInTheDocument();
import GitHubSynchronisationWarning from '../../../../app/components/GitHubSynchronisationWarning';
import { translate } from '../../../../helpers/l10n';
import { getBaseUrl } from '../../../../helpers/system';
-import { useGithubProvisioningEnabledQuery } from '../../../../queries/identity-provider';
+import { useGithubProvisioningEnabledQuery } from '../../../../queries/identity-provider/github';
import { isApplication, isPortfolioLike, isProject } from '../../../../types/component';
import { Component } from '../../../../types/types';
import ApplyTemplate from './ApplyTemplate';
convertToPermissionDefinitions,
} from '../../../../helpers/permissions';
import { useIsGitHubProjectQuery } from '../../../../queries/devops-integration';
-import { useGithubProvisioningEnabledQuery } from '../../../../queries/identity-provider';
+import { useGithubProvisioningEnabledQuery } from '../../../../queries/identity-provider/github';
import { ComponentContextShape, Visibility } from '../../../../types/component';
import { Permissions } from '../../../../types/permissions';
import { Component, Paging, PermissionGroup, PermissionUser } from '../../../../types/types';
import { act, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import AlmSettingsServiceMock from '../../../../../api/mocks/AlmSettingsServiceMock';
-import AuthenticationServiceMock from '../../../../../api/mocks/AuthenticationServiceMock';
+import GithubProvisioningServiceMock from '../../../../../api/mocks/GithubProvisioningServiceMock';
import PermissionsServiceMock from '../../../../../api/mocks/PermissionsServiceMock';
import SystemServiceMock from '../../../../../api/mocks/SystemServiceMock';
import { mockComponent } from '../../../../../helpers/mocks/component';
import { getPageObject } from '../../../test-utils';
let serviceMock: PermissionsServiceMock;
-let authHandler: AuthenticationServiceMock;
+let githubHandler: GithubProvisioningServiceMock;
let almHandler: AlmSettingsServiceMock;
let systemHandler: SystemServiceMock;
beforeAll(() => {
serviceMock = new PermissionsServiceMock();
- authHandler = new AuthenticationServiceMock();
+ githubHandler = new GithubProvisioningServiceMock();
almHandler = new AlmSettingsServiceMock();
systemHandler = new SystemServiceMock();
});
afterEach(() => {
serviceMock.reset();
- authHandler.reset();
+ githubHandler.reset();
almHandler.reset();
});
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;
+ githubHandler.githubProvisioningStatus = true;
almHandler.handleSetProjectBinding(AlmKeys.GitHub, {
almSetting: 'test',
repository: 'test',
it('should allow to change visibility for non-GH Project', async () => {
const user = userEvent.setup();
const ui = getPageObject(user);
- authHandler.githubProvisioningStatus = true;
+ githubHandler.githubProvisioningStatus = true;
almHandler.handleSetProjectBinding(AlmKeys.Azure, {
almSetting: 'test',
repository: 'test',
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;
+ githubHandler.githubProvisioningStatus = false;
almHandler.handleSetProjectBinding(AlmKeys.GitHub, {
almSetting: 'test',
repository: 'test',
it('should have disabled permissions for GH Project', async () => {
const user = userEvent.setup();
const ui = getPageObject(user);
- authHandler.githubProvisioningStatus = true;
+ githubHandler.githubProvisioningStatus = true;
almHandler.handleSetProjectBinding(AlmKeys.GitHub, {
almSetting: 'test',
repository: 'test',
it('should allow to change permissions for GH Project without auto-provisioning', async () => {
const user = userEvent.setup();
const ui = getPageObject(user);
- authHandler.githubProvisioningStatus = false;
+ githubHandler.githubProvisioningStatus = false;
almHandler.handleSetProjectBinding(AlmKeys.GitHub, {
almSetting: 'test',
repository: 'test',
it('should allow to change permissions for non-GH Project', async () => {
const user = userEvent.setup();
const ui = getPageObject(user);
- authHandler.githubProvisioningStatus = true;
+ githubHandler.githubProvisioningStatus = true;
renderPermissionsProjectApp({}, { featureList: [Feature.GithubProvisioning] });
await ui.appLoaded();
import { Button, ResetButtonLink } from '../../components/controls/buttons';
import { Alert } from '../../components/ui/Alert';
import { translate } from '../../helpers/l10n';
-import { useGithubProvisioningEnabledQuery } from '../../queries/identity-provider';
+import { useGithubProvisioningEnabledQuery } from '../../queries/identity-provider/github';
import { Visibility } from '../../types/component';
export interface Props {
import DateFormatter from '../../components/intl/DateFormatter';
import { translate, translateWithParameters } from '../../helpers/l10n';
import { getComponentOverviewUrl } from '../../helpers/urls';
-import { useGithubProvisioningEnabledQuery } from '../../queries/identity-provider';
+import { useGithubProvisioningEnabledQuery } from '../../queries/identity-provider/github';
import { ComponentQualifier } from '../../types/component';
import { LoggedInUser } from '../../types/users';
import './ProjectRow.css';
import { throwGlobalError } from '../../helpers/error';
import { translate, translateWithParameters } from '../../helpers/l10n';
import { getComponentPermissionsUrl } from '../../helpers/urls';
-import { useGithubProvisioningEnabledQuery } from '../../queries/identity-provider';
+import { useGithubProvisioningEnabledQuery } from '../../queries/identity-provider/github';
import { LoggedInUser } from '../../types/users';
import ApplyTemplate from '../permissions/project/components/ApplyTemplate';
import RestoreAccessModal from './RestoreAccessModal';
import { screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import selectEvent from 'react-select-event';
-import AuthenticationServiceMock from '../../../api/mocks/AuthenticationServiceMock';
+import GithubProvisioningServiceMock from '../../../api/mocks/GithubProvisioningServiceMock';
import PermissionsServiceMock from '../../../api/mocks/PermissionsServiceMock';
import ProjectManagementServiceMock from '../../../api/mocks/ProjectsManagementServiceMock';
import SettingsServiceMock from '../../../api/mocks/SettingsServiceMock';
const permissionsHandler = new PermissionsServiceMock();
const settingsHandler = new SettingsServiceMock();
-const authHandler = new AuthenticationServiceMock();
+const githubHandler = new GithubProvisioningServiceMock();
const handler = new ProjectManagementServiceMock(settingsHandler);
jest.mock('../../../api/navigation', () => ({
permissionsHandler.reset();
settingsHandler.reset();
- authHandler.reset();
+ githubHandler.reset();
handler.reset();
});
it('should restore access for github project', async () => {
const user = userEvent.setup();
- authHandler.enableGithubProvisioning();
+ githubHandler.enableGithubProvisioning();
renderProjectManagementApp(
{},
{ login: 'gooduser2', local: true },
it('should not allow to restore access on github project for GH user', async () => {
const user = userEvent.setup();
- authHandler.githubProvisioningStatus = true;
+ githubHandler.githubProvisioningStatus = true;
renderProjectManagementApp(
{},
{ login: 'gooduser2', local: false },
it('should show github warning on changing default visibility to admin', async () => {
const user = userEvent.setup();
- authHandler.githubProvisioningStatus = true;
+ githubHandler.githubProvisioningStatus = true;
renderProjectManagementApp({}, {}, { featureList: [Feature.GithubProvisioning] });
await user.click(ui.editDefaultVisibility.get());
expect(await ui.changeDefaultVisibilityDialog.find()).toBeInTheDocument();
});
it('should not show local badge for applications and portfolios', async () => {
- authHandler.enableGithubProvisioning();
+ githubHandler.enableGithubProvisioning();
renderProjectManagementApp({}, {}, { featureList: [Feature.GithubProvisioning] });
await waitFor(() => expect(screen.getAllByText('local')).toHaveLength(3));
const [query, setSearchParams] = useSearchParams();
- const currentTab = (query.get('tab') || SAML) as AuthenticationTabs;
+ const currentTab = (query.get('tab') ?? SAML) as AuthenticationTabs;
const tabs = [
{
},
] as const;
- const [samlDefinitions, githubDefinitions, gitlabDefinitions] = React.useMemo(
+ const [samlDefinitions, githubDefinitions] = React.useMemo(
() => [
definitions.filter((def) => def.subCategory === SAML),
definitions.filter((def) => def.subCategory === AlmKeys.GitHub),
- definitions.filter((def) => def.subCategory === AlmKeys.GitLab),
],
[definitions],
);
aria-labelledby={getTabId(tab.key)}
id={getTabPanelId(tab.key)}
>
- <div className="big-padded-top big-padded-left big-padded-right">
- {tab.key === SAML && <SamlAuthenticationTab definitions={samlDefinitions} />}
-
- {tab.key === AlmKeys.GitHub && (
- <GithubAuthenticationTab
- currentTab={currentTab}
- definitions={githubDefinitions}
- />
- )}
-
- {tab.key === AlmKeys.GitLab && (
- <GitLabAuthenticationTab definitions={gitlabDefinitions} />
- )}
-
- {tab.key === AlmKeys.BitbucketServer && (
- <>
- <Alert variant="info">
- <FormattedMessage
- id="settings.authentication.help"
- defaultMessage={translate('settings.authentication.help')}
- values={{
- link: (
- <DocLink
- to={`/instance-administration/authentication/${
- DOCUMENTATION_LINK_SUFFIXES[tab.key]
- }/`}
- >
- {translate('settings.authentication.help.link')}
- </DocLink>
- ),
- }}
- />
- </Alert>
- <CategoryDefinitionsList
- category={AUTHENTICATION_CATEGORY}
- definitions={definitions}
- subCategory={tab.key}
- displaySubCategoryTitle={false}
+ {currentTab === tab.key && (
+ <div className="big-padded-top big-padded-left big-padded-right">
+ {tab.key === SAML && <SamlAuthenticationTab definitions={samlDefinitions} />}
+
+ {tab.key === AlmKeys.GitHub && (
+ <GithubAuthenticationTab
+ currentTab={currentTab}
+ definitions={githubDefinitions}
/>
- </>
- )}
- </div>
+ )}
+
+ {tab.key === AlmKeys.GitLab && <GitLabAuthenticationTab />}
+
+ {tab.key === AlmKeys.BitbucketServer && (
+ <>
+ <Alert variant="info">
+ <FormattedMessage
+ id="settings.authentication.help"
+ defaultMessage={translate('settings.authentication.help')}
+ values={{
+ link: (
+ <DocLink
+ to={`/instance-administration/authentication/${
+ DOCUMENTATION_LINK_SUFFIXES[tab.key]
+ }/`}
+ >
+ {translate('settings.authentication.help.link')}
+ </DocLink>
+ ),
+ }}
+ />
+ </Alert>
+ <CategoryDefinitionsList
+ category={AUTHENTICATION_CATEGORY}
+ definitions={definitions}
+ subCategory={tab.key}
+ displaySubCategoryTitle={false}
+ />
+ </>
+ )}
+ </div>
+ )}
</div>
))}
</>
import Modal from '../../../../components/controls/Modal';
import { Button } from '../../../../components/controls/buttons';
import { translate } from '../../../../helpers/l10n';
-import { useToggleGithubProvisioningMutation } from '../../../../queries/identity-provider';
+import { useToggleGithubProvisioningMutation } from '../../../../queries/identity-provider/github';
import { useGetValueQuery, useResetSettingsMutation } from '../../../../queries/settings';
const GITHUB_PERMISSION_USER_CONSENT =
import HelpIcon from '../../../../components/icons/HelpIcon';
import { Alert, AlertVariant } from '../../../../components/ui/Alert';
import { translate, translateWithParameters } from '../../../../helpers/l10n';
-import { useCheckGitHubConfigQuery } from '../../../../queries/identity-provider';
+import { useCheckGitHubConfigQuery } from '../../../../queries/identity-provider/github';
import { GitHubProvisioningStatus } from '../../../../types/provisioning';
const intlPrefix = 'settings.authentication.github.configuration.validation';
convertToPermissionDefinitions,
isPermissionDefinitionGroup,
} from '../../../../helpers/permissions';
-import { useGithubRolesMappingQuery } from '../../../../queries/identity-provider';
+import { useGithubRolesMappingQuery } from '../../../../queries/identity-provider/github';
import { GitHubMapping } from '../../../../types/provisioning';
interface Props {
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 { Alert } from '../../../../components/ui/Alert';
import Spinner from '../../../../components/ui/Spinner';
import { translate } from '../../../../helpers/l10n';
+import { useIdentityProviderQuery } from '../../../../queries/identity-provider/common';
import {
useDeleteGitLabConfigurationMutation,
useGitLabConfigurationsQuery,
- useIdentityProviderQuery,
useUpdateGitLabConfigurationMutation,
-} from '../../../../queries/identity-provider';
+} from '../../../../queries/identity-provider/gitlab';
import { AlmKeys } from '../../../../types/alm-settings';
import { Feature } from '../../../../types/features';
import { GitLabConfigurationUpdateBody, ProvisioningType } from '../../../../types/provisioning';
-import { ExtendedSettingDefinition } from '../../../../types/settings';
+import { DefinitionV2, SettingType } 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'];
+ synchronizationType?: GitLabConfigurationUpdateBody['synchronizationType'];
allowUsersToSignUp?: GitLabConfigurationUpdateBody['allowUsersToSignUp'];
provisioningToken?: GitLabConfigurationUpdateBody['provisioningToken'];
- groups?: GitLabConfigurationUpdateBody['groups'];
+ provisioningGroups?: GitLabConfigurationUpdateBody['provisioningGroups'];
}
-export default function GitLabAuthenticationTab(props: Readonly<GitLabAuthenticationTab>) {
- const { definitions } = props;
+const definitions: Record<keyof Omit<ChangesForm, 'synchronizationType'>, DefinitionV2> = {
+ provisioningGroups: {
+ name: translate('settings.authentication.gitlab.form.provisioningGroups.name'),
+ key: 'provisioningGroups',
+ description: translate('settings.authentication.gitlab.form.provisioningGroups.description'),
+ secured: false,
+ multiValues: true,
+ },
+ allowUsersToSignUp: {
+ name: translate('settings.authentication.gitlab.form.allowUsersToSignUp.name'),
+ secured: false,
+ key: 'allowUsersToSignUp',
+ description: translate('settings.authentication.gitlab.form.allowUsersToSignUp.description'),
+ type: SettingType.BOOLEAN,
+ },
+ provisioningToken: {
+ name: translate('settings.authentication.gitlab.form.provisioningToken.name'),
+ secured: true,
+ key: 'provisioningToken',
+ description: translate('settings.authentication.gitlab.form.provisioningToken.description'),
+ },
+};
+export default function GitLabAuthenticationTab() {
const [openForm, setOpenForm] = React.useState(false);
const [changes, setChanges] = React.useState<ChangesForm | undefined>(undefined);
const [tokenKey, setTokenKey] = React.useState<number>(0);
const { data: identityProvider } = useIdentityProviderQuery();
const { data: list, isLoading: isLoadingList } = useGitLabConfigurationsQuery();
- const configuration = list?.configurations[0];
+ const configuration = list?.gitlabConfigurations[0];
const { mutate: updateConfig, isLoading: isUpdating } = useUpdateGitLabConfigurationMutation();
const { mutate: deleteConfig, isLoading: isDeleting } = useDeleteGitLabConfigurationMutation();
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
- if (changes?.type !== undefined) {
+ if (changes?.synchronizationType !== undefined) {
setShowConfirmProvisioningModal(true);
} else {
updateProvisioning();
const setJIT = () =>
setChangesWithCheck({
- type: ProvisioningType.jit,
+ synchronizationType: ProvisioningType.jit,
provisioningToken: undefined,
- groups: undefined,
+ provisioningGroups: undefined,
});
const setAuto = () =>
setChangesWithCheck({
- type: ProvisioningType.auto,
+ synchronizationType: 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 allowUsersToSignUpDefinition = definitions.allowUsersToSignUp;
+ const provisioningTokenDefinition = definitions.provisioningToken;
+ const provisioningGroupDefinition = definitions.provisioningGroups;
- const provisioningType = changes?.type ?? configuration?.type;
+ const provisioningType = changes?.synchronizationType ?? configuration?.synchronizationType;
const allowUsersToSignUp = changes?.allowUsersToSignUp ?? configuration?.allowUsersToSignUp;
const provisioningToken = changes?.provisioningToken;
- const groups = changes?.groups ?? configuration?.groups;
+ const groups = changes?.provisioningGroups ?? configuration?.provisioningGroups;
const canSave = () => {
if (!configuration || changes === undefined) {
return false;
}
- const type = changes.type ?? configuration.type;
+ const type = changes.synchronizationType ?? configuration.synchronizationType;
if (type === ProvisioningType.auto) {
- const hasConfigGroups = configuration.groups && configuration.groups.length > 0;
- const hasGroups = changes.groups ? changes.groups.length > 0 : hasConfigGroups;
+ const hasConfigGroups =
+ configuration.provisioningGroups && configuration.provisioningGroups.length > 0;
+ const hasGroups = changes.provisioningGroups
+ ? changes.provisioningGroups.length > 0
+ : hasConfigGroups;
const hasToken = hasConfigGroups
? changes.provisioningToken !== ''
: !!changes.provisioningToken;
const setChangesWithCheck = (newChanges: ChangesForm) => {
const newValue = {
- type: configuration?.type === newChanges.type ? undefined : newChanges.type,
+ synchronizationType:
+ configuration?.synchronizationType === newChanges.synchronizationType
+ ? undefined
+ : newChanges.synchronizationType,
allowUsersToSignUp:
configuration?.allowUsersToSignUp === newChanges.allowUsersToSignUp
? undefined
: newChanges.allowUsersToSignUp,
provisioningToken: newChanges.provisioningToken,
- groups: isEqual(configuration?.groups, newChanges.groups) ? undefined : newChanges.groups,
+ provisioningGroups: isEqual(configuration?.provisioningGroups, newChanges.provisioningGroups)
+ ? undefined
+ : newChanges.provisioningGroups,
};
if (Object.values(newValue).some((v) => v !== undefined)) {
setChanges(newValue);
<p>{configuration.url}</p>
<Tooltip
overlay={
- configuration.type === ProvisioningType.auto
+ configuration.synchronizationType === ProvisioningType.auto
? translate('settings.authentication.form.disable.tooltip')
: null
}
<Button
className="spacer-top"
onClick={toggleEnable}
- disabled={isUpdating || configuration.type === ProvisioningType.auto}
+ disabled={
+ isUpdating || configuration.synchronizationType === ProvisioningType.auto
+ }
>
{configuration.enabled
? translate('settings.authentication.form.disable')
allowUsersToSignUp: value as boolean,
})
}
- isNotSet={configuration.type !== ProvisioningType.auto}
+ isNotSet={configuration.synchronizationType !== ProvisioningType.auto}
/>
)}
</RadioCard>
/>
</p>
- {configuration?.type === ProvisioningType.auto && (
+ {configuration?.synchronizationType === 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}
- />
- </>
- )}
+ {provisioningType === ProvisioningType.auto && (
+ <>
+ <AuthenticationFormField
+ settingValue={provisioningToken}
+ key={tokenKey}
+ definition={provisioningTokenDefinition}
+ mandatory
+ onFieldChange={(_, value) =>
+ setChangesWithCheck({
+ ...changes,
+ provisioningToken: value as string,
+ })
+ }
+ isNotSet={
+ configuration.synchronizationType !== ProvisioningType.auto &&
+ configuration.provisioningGroups?.length === 0
+ }
+ />
+ <AuthenticationFormField
+ settingValue={groups}
+ definition={provisioningGroupDefinition}
+ mandatory
+ onFieldChange={(_, values) =>
+ setChangesWithCheck({
+ ...changes,
+ provisioningGroups: values as string[],
+ })
+ }
+ isNotSet={configuration.synchronizationType !== ProvisioningType.auto}
+ />
+ </>
+ )}
</>
) : (
<p>
import {
useCreateGitLabConfigurationMutation,
useUpdateGitLabConfigurationMutation,
-} from '../../../../queries/identity-provider';
+} from '../../../../queries/identity-provider/gitlab';
import { GitLabConfigurationCreateBody, GitlabConfiguration } from '../../../../types/provisioning';
import { DefinitionV2, SettingType } from '../../../../types/settings';
import { DOCUMENTATION_LINK_SUFFIXES } from './Authentication';
Record<keyof GitLabConfigurationCreateBody, FormData>
>({
applicationId: {
- value: '',
+ value: data?.applicationId ?? '',
required: true,
definition: {
name: translate('settings.authentication.gitlab.form.applicationId.name'),
key: 'applicationId',
description: translate('settings.authentication.gitlab.form.applicationId.description'),
- secured: true,
+ secured: false,
},
},
url: {
description: translate('settings.authentication.gitlab.form.url.description'),
},
},
- clientSecret: {
+ secret: {
value: '',
required: true,
definition: {
- name: translate('settings.authentication.gitlab.form.clientSecret.name'),
+ name: translate('settings.authentication.gitlab.form.secret.name'),
secured: true,
- key: 'clientSecret',
- description: translate('settings.authentication.gitlab.form.clientSecret.description'),
+ key: 'secret',
+ description: translate('settings.authentication.gitlab.form.secret.description'),
},
},
- synchronizeUserGroups: {
- value: data?.synchronizeUserGroups ?? false,
+ synchronizeGroups: {
+ value: data?.synchronizeGroups ?? false,
required: false,
definition: {
- name: translate('settings.authentication.gitlab.form.synchronizeUserGroups.name'),
+ name: translate('settings.authentication.gitlab.form.synchronizeGroups.name'),
secured: false,
- key: 'synchronizeUserGroups',
- description: translate(
- 'settings.authentication.gitlab.form.synchronizeUserGroups.description',
- ),
+ key: 'synchronizeGroups',
+ description: translate('settings.authentication.gitlab.form.synchronizeGroups.description'),
type: SettingType.BOOLEAN,
},
},
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, translateWithParameters } from '../../../../helpers/l10n';
+import { useIdentityProviderQuery } from '../../../../queries/identity-provider/common';
import {
useCheckGitHubConfigQuery,
- useIdentityProviderQuery,
useSyncWithGitHubNow,
-} from '../../../../queries/identity-provider';
+} from '../../../../queries/identity-provider/github';
import { AlmKeys } from '../../../../types/alm-settings';
import { ExtendedSettingDefinition } from '../../../../types/settings';
import { Provider } from '../../../../types/types';
};
return (
- <div className="authentication-configuration">
- <div className="spacer-bottom display-flex-space-between display-flex-center">
- <h4>{translate('settings.authentication.github.configuration')}</h4>
+ <Spinner loading={isLoading}>
+ <div className="authentication-configuration">
+ <div className="spacer-bottom display-flex-space-between display-flex-center">
+ <h4>{translate('settings.authentication.github.configuration')}</h4>
- {!hasConfiguration && (
- <div>
- <Button onClick={handleCreateConfiguration}>
- {translate('settings.authentication.form.create')}
- </Button>
+ {!hasConfiguration && (
+ <div>
+ <Button onClick={handleCreateConfiguration}>
+ {translate('settings.authentication.form.create')}
+ </Button>
+ </div>
+ )}
+ </div>
+ {enabled && !hasLegacyConfiguration && (
+ <GitHubConfigurationValidity
+ selectedOrganizations={
+ (values['sonar.auth.github.organizations']?.value as string[]) ?? []
+ }
+ isAutoProvisioning={!!(newGithubProvisioningStatus ?? githubProvisioningStatus)}
+ />
+ )}
+ {!hasConfiguration && !hasLegacyConfiguration && (
+ <div className="big-padded text-center huge-spacer-bottom authentication-no-config">
+ {translate('settings.authentication.github.form.not_configured')}
</div>
)}
- </div>
- {enabled && !hasLegacyConfiguration && (
- <GitHubConfigurationValidity
- selectedOrganizations={
- (values['sonar.auth.github.organizations']?.value as string[]) ?? []
- }
- isAutoProvisioning={!!(newGithubProvisioningStatus ?? githubProvisioningStatus)}
- />
- )}
- {!hasConfiguration && !hasLegacyConfiguration && (
- <div className="big-padded text-center huge-spacer-bottom authentication-no-config">
- {translate('settings.authentication.github.form.not_configured')}
- </div>
- )}
- {!hasConfiguration && hasLegacyConfiguration && (
- <div className="big-padded">
- <Alert variant="warning">
- <FormattedMessage
- id="settings.authentication.github.form.legacy_configured"
- defaultMessage={translate('settings.authentication.github.form.legacy_configured')}
- values={{
- documentation: (
- <DocLink to="/instance-administration/authentication/github">
- {translate('settings.authentication.github.form.legacy_configured.link')}
- </DocLink>
- ),
- }}
- />
- </Alert>
- </div>
- )}
- {hasConfiguration && (
- <>
- <div className="spacer-bottom big-padded bordered display-flex-space-between">
- <div>
- <h5>{translateWithParameters('settings.authentication.github.appid_x', appId)}</h5>
- <p>{url}</p>
- <Tooltip
- overlay={
- githubProvisioningStatus
- ? translate('settings.authentication.form.disable.tooltip')
- : null
- }
- >
- <Button
- className="spacer-top"
- onClick={toggleEnable}
- disabled={githubProvisioningStatus}
+ {!hasConfiguration && hasLegacyConfiguration && (
+ <div className="big-padded">
+ <Alert variant="warning">
+ <FormattedMessage
+ id="settings.authentication.github.form.legacy_configured"
+ defaultMessage={translate('settings.authentication.github.form.legacy_configured')}
+ values={{
+ documentation: (
+ <DocLink to="/instance-administration/authentication/github">
+ {translate('settings.authentication.github.form.legacy_configured.link')}
+ </DocLink>
+ ),
+ }}
+ />
+ </Alert>
+ </div>
+ )}
+ {hasConfiguration && (
+ <>
+ <div className="spacer-bottom big-padded bordered display-flex-space-between">
+ <div>
+ <h5>{translateWithParameters('settings.authentication.github.appid_x', appId)}</h5>
+ <p>{url}</p>
+ <Tooltip
+ overlay={
+ githubProvisioningStatus
+ ? translate('settings.authentication.form.disable.tooltip')
+ : null
+ }
>
- {enabled
- ? translate('settings.authentication.form.disable')
- : translate('settings.authentication.form.enable')}
+ <Button
+ className="spacer-top"
+ onClick={toggleEnable}
+ disabled={githubProvisioningStatus}
+ >
+ {enabled
+ ? translate('settings.authentication.form.disable')
+ : translate('settings.authentication.form.enable')}
+ </Button>
+ </Tooltip>
+ </div>
+ <div>
+ <Button className="spacer-right" onClick={handleCreateConfiguration}>
+ <EditIcon />
+ {translate('settings.authentication.form.edit')}
</Button>
- </Tooltip>
- </div>
- <div>
- <Button className="spacer-right" onClick={handleCreateConfiguration}>
- <EditIcon />
- {translate('settings.authentication.form.edit')}
- </Button>
- <Tooltip
- overlay={
- enabled || isDeleting
- ? translate('settings.authentication.form.delete.tooltip')
- : null
- }
- >
- <Button
- className="button-red"
- disabled={enabled || isDeleting}
- onClick={deleteConfiguration}
+ <Tooltip
+ overlay={
+ enabled || isDeleting
+ ? translate('settings.authentication.form.delete.tooltip')
+ : null
+ }
>
- <DeleteIcon />
- {translate('settings.authentication.form.delete')}
- </Button>
- </Tooltip>
+ <Button
+ className="button-red"
+ disabled={enabled || isDeleting}
+ onClick={deleteConfiguration}
+ >
+ <DeleteIcon />
+ {translate('settings.authentication.form.delete')}
+ </Button>
+ </Tooltip>
+ </div>
</div>
- </div>
- <div className="spacer-bottom big-padded bordered display-flex-space-between">
- <form onSubmit={handleSubmit}>
- <fieldset className="display-flex-column big-spacer-bottom">
- <label className="h5">
- {translate('settings.authentication.form.provisioning')}
- </label>
+ <div className="spacer-bottom big-padded bordered display-flex-space-between">
+ <form onSubmit={handleSubmit}>
+ <fieldset className="display-flex-column big-spacer-bottom">
+ <label className="h5">
+ {translate('settings.authentication.form.provisioning')}
+ </label>
- {enabled ? (
- <div className="display-flex-column spacer-top">
- <RadioCard
- className="sw-min-h-0"
- label={translate('settings.authentication.form.provisioning_at_login')}
- title={translate('settings.authentication.form.provisioning_at_login')}
- selected={!(newGithubProvisioningStatus ?? githubProvisioningStatus)}
- onClick={() => setProvisioningType(false)}
- >
- <p className="spacer-bottom">
- <FormattedMessage id="settings.authentication.github.form.provisioning_at_login.description" />
- </p>
- <p className="spacer-bottom">
- <FormattedMessage
- id="settings.authentication.github.form.description.doc"
- values={{
- documentation: (
- <DocLink
- to={`/instance-administration/authentication/${
- DOCUMENTATION_LINK_SUFFIXES[AlmKeys.GitHub]
- }/`}
- >
- {translate('documentation')}
- </DocLink>
- ),
- }}
- />
- </p>
+ {enabled ? (
+ <div className="display-flex-column spacer-top">
+ <RadioCard
+ className="sw-min-h-0"
+ label={translate('settings.authentication.form.provisioning_at_login')}
+ title={translate('settings.authentication.form.provisioning_at_login')}
+ selected={!(newGithubProvisioningStatus ?? githubProvisioningStatus)}
+ onClick={() => setProvisioningType(false)}
+ >
+ <p className="spacer-bottom">
+ <FormattedMessage id="settings.authentication.github.form.provisioning_at_login.description" />
+ </p>
+ <p className="spacer-bottom">
+ <FormattedMessage
+ id="settings.authentication.github.form.description.doc"
+ values={{
+ documentation: (
+ <DocLink
+ to={`/instance-administration/authentication/${
+ DOCUMENTATION_LINK_SUFFIXES[AlmKeys.GitHub]
+ }/`}
+ >
+ {translate('documentation')}
+ </DocLink>
+ ),
+ }}
+ />
+ </p>
- {!(newGithubProvisioningStatus ?? githubProvisioningStatus) && (
- <>
- <hr />
- {Object.values(values).map((val) => {
- if (!GITHUB_JIT_FIELDS.includes(val.key)) {
- return null;
- }
- return (
- <div key={val.key}>
- <AuthenticationFormField
- settingValue={values[val.key]?.newValue ?? values[val.key]?.value}
- definition={val.definition}
- mandatory={val.mandatory}
- onFieldChange={setNewValue}
- isNotSet={val.isNotSet}
- />
- </div>
- );
- })}
- </>
- )}
- </RadioCard>
- <RadioCard
- className="spacer-top sw-min-h-0"
- label={translate(
- 'settings.authentication.github.form.provisioning_with_github',
- )}
- title={translate(
- 'settings.authentication.github.form.provisioning_with_github',
- )}
- selected={newGithubProvisioningStatus ?? githubProvisioningStatus}
- onClick={() => setProvisioningType(true)}
- disabled={!hasGithubProvisioning || hasDifferentProvider}
- >
- {hasGithubProvisioning ? (
- <>
- {hasDifferentProvider && (
- <p className="spacer-bottom text-bold ">
- {translate('settings.authentication.form.other_provisioning_enabled')}
+ {!(newGithubProvisioningStatus ?? githubProvisioningStatus) && (
+ <>
+ <hr />
+ {Object.values(values).map((val) => {
+ if (!GITHUB_JIT_FIELDS.includes(val.key)) {
+ return null;
+ }
+ return (
+ <div key={val.key}>
+ <AuthenticationFormField
+ settingValue={
+ values[val.key]?.newValue ?? values[val.key]?.value
+ }
+ definition={val.definition}
+ mandatory={val.mandatory}
+ onFieldChange={setNewValue}
+ isNotSet={val.isNotSet}
+ />
+ </div>
+ );
+ })}
+ </>
+ )}
+ </RadioCard>
+ <RadioCard
+ className="spacer-top sw-min-h-0"
+ label={translate(
+ 'settings.authentication.github.form.provisioning_with_github',
+ )}
+ title={translate(
+ 'settings.authentication.github.form.provisioning_with_github',
+ )}
+ selected={newGithubProvisioningStatus ?? githubProvisioningStatus}
+ onClick={() => setProvisioningType(true)}
+ disabled={!hasGithubProvisioning || hasDifferentProvider}
+ >
+ {hasGithubProvisioning ? (
+ <>
+ {hasDifferentProvider && (
+ <p className="spacer-bottom text-bold ">
+ {translate(
+ 'settings.authentication.form.other_provisioning_enabled',
+ )}
+ </p>
+ )}
+ <p className="spacer-bottom">
+ {translate(
+ 'settings.authentication.github.form.provisioning_with_github.description',
+ )}
+ </p>
+ <p className="spacer-bottom">
+ <FormattedMessage
+ id="settings.authentication.github.form.description.doc"
+ values={{
+ documentation: (
+ <DocLink
+ to={`/instance-administration/authentication/${
+ DOCUMENTATION_LINK_SUFFIXES[AlmKeys.GitHub]
+ }/`}
+ >
+ {translate('documentation')}
+ </DocLink>
+ ),
+ }}
+ />
</p>
- )}
- <p className="spacer-bottom">
- {translate(
- 'settings.authentication.github.form.provisioning_with_github.description',
+
+ {githubProvisioningStatus && <GitHubSynchronisationWarning />}
+ {(newGithubProvisioningStatus ?? githubProvisioningStatus) && (
+ <>
+ <div className="sw-flex sw-flex-1 spacer-bottom">
+ <Button
+ className="spacer-top width-30"
+ onClick={synchronizeNow}
+ disabled={!canSyncNow}
+ >
+ {translate('settings.authentication.github.synchronize_now')}
+ </Button>
+ </div>
+ <hr />
+ {Object.values(values).map((val) => {
+ if (!GITHUB_PROVISIONING_FIELDS.includes(val.key)) {
+ return null;
+ }
+ return (
+ <div key={val.key}>
+ <AuthenticationFormField
+ settingValue={
+ values[val.key]?.newValue ?? values[val.key]?.value
+ }
+ definition={val.definition}
+ mandatory={val.mandatory}
+ onFieldChange={setNewValue}
+ isNotSet={val.isNotSet}
+ />
+ </div>
+ );
+ })}
+ <AuthenticationFormFieldWrapper
+ title={translate(
+ 'settings.authentication.github.configuration.roles_mapping.title',
+ )}
+ description={translate(
+ 'settings.authentication.github.configuration.roles_mapping.description',
+ )}
+ >
+ <Button
+ className="spacer-top"
+ onClick={() => setShowMappingModal(true)}
+ >
+ {translate(
+ 'settings.authentication.github.configuration.roles_mapping.button_label',
+ )}
+ </Button>
+ </AuthenticationFormFieldWrapper>
+ </>
)}
- </p>
- <p className="spacer-bottom">
+ </>
+ ) : (
+ <p>
<FormattedMessage
- id="settings.authentication.github.form.description.doc"
+ id="settings.authentication.github.form.provisioning.disabled"
+ defaultMessage={translate(
+ 'settings.authentication.github.form.provisioning.disabled',
+ )}
values={{
documentation: (
- <DocLink
- to={`/instance-administration/authentication/${
- DOCUMENTATION_LINK_SUFFIXES[AlmKeys.GitHub]
- }/`}
- >
+ // Documentation page not ready yet.
+ <DocLink to="/instance-administration/authentication/github">
{translate('documentation')}
</DocLink>
),
}}
/>
</p>
-
- {githubProvisioningStatus && <GitHubSynchronisationWarning />}
- {(newGithubProvisioningStatus ?? githubProvisioningStatus) && (
- <>
- <div className="sw-flex sw-flex-1 spacer-bottom">
- <Button
- className="spacer-top width-30"
- onClick={synchronizeNow}
- disabled={!canSyncNow}
- >
- {translate('settings.authentication.github.synchronize_now')}
- </Button>
- </div>
- <hr />
- {Object.values(values).map((val) => {
- if (!GITHUB_PROVISIONING_FIELDS.includes(val.key)) {
- return null;
- }
- return (
- <div key={val.key}>
- <AuthenticationFormField
- settingValue={
- values[val.key]?.newValue ?? values[val.key]?.value
- }
- definition={val.definition}
- mandatory={val.mandatory}
- onFieldChange={setNewValue}
- isNotSet={val.isNotSet}
- />
- </div>
- );
- })}
- <AuthenticationFormFieldWrapper
- title={translate(
- 'settings.authentication.github.configuration.roles_mapping.title',
- )}
- description={translate(
- 'settings.authentication.github.configuration.roles_mapping.description',
- )}
- >
- <Button
- className="spacer-top"
- onClick={() => setShowMappingModal(true)}
- >
- {translate(
- 'settings.authentication.github.configuration.roles_mapping.button_label',
- )}
- </Button>
- </AuthenticationFormFieldWrapper>
- </>
- )}
- </>
- ) : (
- <p>
- <FormattedMessage
- id="settings.authentication.github.form.provisioning.disabled"
- defaultMessage={translate(
- 'settings.authentication.github.form.provisioning.disabled',
- )}
- values={{
- documentation: (
- // Documentation page not ready yet.
- <DocLink to="/instance-administration/authentication/github">
- {translate('documentation')}
- </DocLink>
- ),
- }}
- />
- </p>
- )}
- </RadioCard>
+ )}
+ </RadioCard>
+ </div>
+ ) : (
+ <Alert className="big-spacer-top" variant="info">
+ {translate('settings.authentication.github.enable_first')}
+ </Alert>
+ )}
+ </fieldset>
+ {enabled && (
+ <div className="sw-flex sw-gap-2 sw-h-8 sw-items-center">
+ <SubmitButton disabled={!hasGithubProvisioningConfigChange}>
+ {translate('save')}
+ </SubmitButton>
+ <ResetButtonLink
+ onClick={() => {
+ setProvisioningType(undefined);
+ resetJitSetting();
+ }}
+ disabled={!hasGithubProvisioningConfigChange}
+ >
+ {translate('cancel')}
+ </ResetButtonLink>
+ <Alert variant="warning" className="sw-mb-0">
+ {hasGithubProvisioningConfigChange &&
+ translate('settings.authentication.github.configuration.unsaved_changes')}
+ </Alert>
</div>
- ) : (
- <Alert className="big-spacer-top" variant="info">
- {translate('settings.authentication.github.enable_first')}
- </Alert>
)}
- </fieldset>
- {enabled && (
- <div className="sw-flex sw-gap-2 sw-h-8 sw-items-center">
- <SubmitButton disabled={!hasGithubProvisioningConfigChange}>
- {translate('save')}
- </SubmitButton>
- <ResetButtonLink
- onClick={() => {
- setProvisioningType(undefined);
- resetJitSetting();
- }}
- disabled={!hasGithubProvisioningConfigChange}
+ {showConfirmProvisioningModal && (
+ <ConfirmModal
+ onConfirm={() => changeProvisioning()}
+ header={translate(
+ 'settings.authentication.github.confirm',
+ newGithubProvisioningStatus ? 'auto' : 'jit',
+ )}
+ onClose={() => setShowConfirmProvisioningModal(false)}
+ confirmButtonText={translate(
+ 'settings.authentication.github.provisioning_change.confirm_changes',
+ )}
>
- {translate('cancel')}
- </ResetButtonLink>
- <Alert variant="warning" className="sw-mb-0">
- {hasGithubProvisioningConfigChange &&
- translate('settings.authentication.github.configuration.unsaved_changes')}
- </Alert>
- </div>
- )}
- {showConfirmProvisioningModal && (
- <ConfirmModal
- onConfirm={() => changeProvisioning()}
- header={translate(
- 'settings.authentication.github.confirm',
- newGithubProvisioningStatus ? 'auto' : 'jit',
- )}
- onClose={() => setShowConfirmProvisioningModal(false)}
- confirmButtonText={translate(
- 'settings.authentication.github.provisioning_change.confirm_changes',
- )}
- >
- {translate(
- 'settings.authentication.github.confirm',
- newGithubProvisioningStatus ? 'auto' : 'jit',
- 'description',
- )}
- </ConfirmModal>
+ {translate(
+ 'settings.authentication.github.confirm',
+ newGithubProvisioningStatus ? 'auto' : 'jit',
+ 'description',
+ )}
+ </ConfirmModal>
+ )}
+ </form>
+ {showMappingModal && (
+ <GitHubMappingModal
+ mapping={rolesMapping}
+ setMapping={setRolesMapping}
+ onClose={() => setShowMappingModal(false)}
+ />
)}
- </form>
- {showMappingModal && (
- <GitHubMappingModal
- mapping={rolesMapping}
- setMapping={setRolesMapping}
- onClose={() => setShowMappingModal(false)}
- />
- )}
- </div>
- </>
- )}
+ </div>
+ </>
+ )}
- {showEditModal && (
- <ConfigurationForm
- tab={AlmKeys.GitHub}
- excludedField={GITHUB_EXCLUDED_FIELD}
- loading={isLoading}
- values={values}
- setNewValue={setNewValue}
- canBeSave={canBeSave}
- onClose={handleCloseConfiguration}
- create={!hasConfiguration}
- hasLegacyConfiguration={hasLegacyConfiguration}
- />
- )}
+ {showEditModal && (
+ <ConfigurationForm
+ tab={AlmKeys.GitHub}
+ excludedField={GITHUB_EXCLUDED_FIELD}
+ loading={isLoading}
+ values={values}
+ setNewValue={setNewValue}
+ canBeSave={canBeSave}
+ onClose={handleCloseConfiguration}
+ create={!hasConfiguration}
+ hasLegacyConfiguration={hasLegacyConfiguration}
+ />
+ )}
- {currentTab === AlmKeys.GitHub && <AutoProvisioningConsent />}
- </div>
+ {currentTab === AlmKeys.GitHub && <AutoProvisioningConsent />}
+ </div>
+ </Spinner>
);
}
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 {
- useIdentityProviderQuery,
- useToggleScimMutation,
-} from '../../../../queries/identity-provider';
+import { useIdentityProviderQuery } from '../../../../queries/identity-provider/common';
+import { useToggleScimMutation } from '../../../../queries/identity-provider/scim';
import { useSaveValueMutation } from '../../../../queries/settings';
import { ExtendedSettingDefinition } from '../../../../types/settings';
import { Provider } from '../../../../types/types';
};
return (
- <div className="authentication-configuration">
- <div className="spacer-bottom display-flex-space-between display-flex-center">
- <h4>{translate('settings.authentication.saml.configuration')}</h4>
+ <Spinner loading={isLoading}>
+ <div className="authentication-configuration">
+ <div className="spacer-bottom display-flex-space-between display-flex-center">
+ <h4>{translate('settings.authentication.saml.configuration')}</h4>
+ {!hasConfiguration && (
+ <div>
+ <Button onClick={handleCreateConfiguration}>
+ {translate('settings.authentication.form.create')}
+ </Button>
+ </div>
+ )}
+ </div>
{!hasConfiguration && (
- <div>
- <Button onClick={handleCreateConfiguration}>
- {translate('settings.authentication.form.create')}
- </Button>
+ <div className="big-padded text-center huge-spacer-bottom authentication-no-config">
+ {translate('settings.authentication.saml.form.not_configured')}
</div>
)}
- </div>
- {!hasConfiguration && (
- <div className="big-padded text-center huge-spacer-bottom authentication-no-config">
- {translate('settings.authentication.saml.form.not_configured')}
- </div>
- )}
- {hasConfiguration && (
- <>
- <div className="spacer-bottom big-padded bordered display-flex-space-between">
- <div>
- <h5>{name}</h5>
- <p>{url}</p>
- <p className="big-spacer-top big-spacer-bottom">
- {samlEnabled ? (
- <span className="authentication-enabled spacer-left">
- <CheckIcon className="spacer-right" />
- {translate('settings.authentication.form.enabled')}
- </span>
- ) : (
- translate('settings.authentication.form.not_enabled')
- )}
- </p>
- <Button className="spacer-top" disabled={scimStatus} onClick={handleToggleEnable}>
- {samlEnabled
- ? translate('settings.authentication.form.disable')
- : translate('settings.authentication.form.enable')}
- </Button>
+ {hasConfiguration && (
+ <>
+ <div className="spacer-bottom big-padded bordered display-flex-space-between">
+ <div>
+ <h5>{name}</h5>
+ <p>{url}</p>
+ <p className="big-spacer-top big-spacer-bottom">
+ {samlEnabled ? (
+ <span className="authentication-enabled spacer-left">
+ <CheckIcon className="spacer-right" />
+ {translate('settings.authentication.form.enabled')}
+ </span>
+ ) : (
+ translate('settings.authentication.form.not_enabled')
+ )}
+ </p>
+ <Button className="spacer-top" disabled={scimStatus} onClick={handleToggleEnable}>
+ {samlEnabled
+ ? translate('settings.authentication.form.disable')
+ : translate('settings.authentication.form.enable')}
+ </Button>
+ </div>
+ <div>
+ <Link className="button spacer-right" target="_blank" to={CONFIG_TEST_PATH}>
+ {translate('settings.authentication.saml.form.test')}
+ </Link>
+ <Button className="spacer-right" onClick={handleCreateConfiguration}>
+ <EditIcon />
+ {translate('settings.authentication.form.edit')}
+ </Button>
+ <Button
+ className="button-red"
+ disabled={samlEnabled || isDeleting}
+ onClick={deleteConfiguration}
+ >
+ <DeleteIcon />
+ {translate('settings.authentication.form.delete')}
+ </Button>
+ </div>
</div>
- <div>
- <Link className="button spacer-right" target="_blank" to={CONFIG_TEST_PATH}>
- {translate('settings.authentication.saml.form.test')}
- </Link>
- <Button className="spacer-right" onClick={handleCreateConfiguration}>
- <EditIcon />
- {translate('settings.authentication.form.edit')}
- </Button>
- <Button
- className="button-red"
- disabled={samlEnabled || isDeleting}
- onClick={deleteConfiguration}
+ <div className="spacer-bottom big-padded bordered display-flex-space-between">
+ <form
+ onSubmit={(e) => {
+ e.preventDefault();
+ if (hasScimTypeChange) {
+ setShowConfirmProvisioningModal(true);
+ } else {
+ handleSaveGroup();
+ }
+ }}
>
- <DeleteIcon />
- {translate('settings.authentication.form.delete')}
- </Button>
- </div>
- </div>
- <div className="spacer-bottom big-padded bordered display-flex-space-between">
- <form
- onSubmit={(e) => {
- e.preventDefault();
- if (hasScimTypeChange) {
- setShowConfirmProvisioningModal(true);
- } else {
- handleSaveGroup();
- }
- }}
- >
- <fieldset className="display-flex-column big-spacer-bottom">
- <label className="h5">
- {translate('settings.authentication.form.provisioning')}
- </label>
- {samlEnabled ? (
- <div className="display-flex-column spacer-top">
- <RadioCard
- className="sw-min-h-0"
- label={translate('settings.authentication.saml.form.provisioning_at_login')}
- title={translate('settings.authentication.saml.form.provisioning_at_login')}
- selected={!(newScimStatus ?? scimStatus)}
- onClick={() => setNewScimStatus(false)}
- >
- <p>
- {translate('settings.authentication.saml.form.provisioning_at_login.sub')}
- </p>
- </RadioCard>
- <RadioCard
- className="spacer-top sw-min-h-0"
- label={translate('settings.authentication.saml.form.provisioning_with_scim')}
- title={translate('settings.authentication.saml.form.provisioning_with_scim')}
- selected={newScimStatus ?? scimStatus}
- onClick={() => setNewScimStatus(true)}
- disabled={!hasScim || hasDifferentProvider}
- >
- {!hasScim ? (
+ <fieldset className="display-flex-column big-spacer-bottom">
+ <label className="h5">
+ {translate('settings.authentication.form.provisioning')}
+ </label>
+ {samlEnabled ? (
+ <div className="display-flex-column spacer-top">
+ <RadioCard
+ className="sw-min-h-0"
+ label={translate('settings.authentication.saml.form.provisioning_at_login')}
+ title={translate('settings.authentication.saml.form.provisioning_at_login')}
+ selected={!(newScimStatus ?? scimStatus)}
+ onClick={() => setNewScimStatus(false)}
+ >
<p>
- <FormattedMessage
- id="settings.authentication.saml.form.provisioning.disabled"
- values={{
- documentation: (
- <DocLink to="/instance-administration/authentication/saml/scim/overview">
- {translate('documentation')}
- </DocLink>
- ),
- }}
- />
+ {translate('settings.authentication.saml.form.provisioning_at_login.sub')}
</p>
- ) : (
- <>
- {hasDifferentProvider && (
- <p className="spacer-bottom text-bold">
- {translate('settings.authentication.form.other_provisioning_enabled')}
- </p>
- )}
- <p className="spacer-bottom ">
- {translate(
- 'settings.authentication.saml.form.provisioning_with_scim.sub',
- )}
- </p>
- <p className="spacer-bottom ">
- {translate(
- 'settings.authentication.saml.form.provisioning_with_scim.description',
- )}
- </p>
+ </RadioCard>
+ <RadioCard
+ className="spacer-top sw-min-h-0"
+ label={translate(
+ 'settings.authentication.saml.form.provisioning_with_scim',
+ )}
+ title={translate(
+ 'settings.authentication.saml.form.provisioning_with_scim',
+ )}
+ selected={newScimStatus ?? scimStatus}
+ onClick={() => setNewScimStatus(true)}
+ disabled={!hasScim || hasDifferentProvider}
+ >
+ {!hasScim ? (
<p>
<FormattedMessage
- id="settings.authentication.saml.form.provisioning_with_scim.description.doc"
- defaultMessage={translate(
- 'settings.authentication.saml.form.provisioning_with_scim.description.doc',
- )}
+ id="settings.authentication.saml.form.provisioning.disabled"
values={{
documentation: (
<DocLink to="/instance-administration/authentication/saml/scim/overview">
}}
/>
</p>
- </>
- )}
- </RadioCard>
- </div>
- ) : (
- <Alert className="big-spacer-top" variant="info">
- {translate('settings.authentication.saml.enable_first')}
- </Alert>
+ ) : (
+ <>
+ {hasDifferentProvider && (
+ <p className="spacer-bottom text-bold">
+ {translate(
+ 'settings.authentication.form.other_provisioning_enabled',
+ )}
+ </p>
+ )}
+ <p className="spacer-bottom ">
+ {translate(
+ 'settings.authentication.saml.form.provisioning_with_scim.sub',
+ )}
+ </p>
+ <p className="spacer-bottom ">
+ {translate(
+ 'settings.authentication.saml.form.provisioning_with_scim.description',
+ )}
+ </p>
+ <p>
+ <FormattedMessage
+ id="settings.authentication.saml.form.provisioning_with_scim.description.doc"
+ defaultMessage={translate(
+ 'settings.authentication.saml.form.provisioning_with_scim.description.doc',
+ )}
+ values={{
+ documentation: (
+ <DocLink to="/instance-administration/authentication/saml/scim/overview">
+ {translate('documentation')}
+ </DocLink>
+ ),
+ }}
+ />
+ </p>
+ </>
+ )}
+ </RadioCard>
+ </div>
+ ) : (
+ <Alert className="big-spacer-top" variant="info">
+ {translate('settings.authentication.saml.enable_first')}
+ </Alert>
+ )}
+ </fieldset>
+ {samlEnabled && (
+ <>
+ <SubmitButton disabled={!hasScimConfigChange}>{translate('save')}</SubmitButton>
+ <ResetButtonLink
+ className="spacer-left"
+ onClick={() => {
+ setNewScimStatus(undefined);
+ setNewGroupSetting();
+ }}
+ disabled={!hasScimConfigChange}
+ >
+ {translate('cancel')}
+ </ResetButtonLink>
+ </>
)}
- </fieldset>
- {samlEnabled && (
- <>
- <SubmitButton disabled={!hasScimConfigChange}>{translate('save')}</SubmitButton>
- <ResetButtonLink
- className="spacer-left"
- onClick={() => {
- setNewScimStatus(undefined);
- setNewGroupSetting();
- }}
- disabled={!hasScimConfigChange}
+ {showConfirmProvisioningModal && (
+ <ConfirmModal
+ onConfirm={() => handleConfirmChangeProvisioning()}
+ header={translate(
+ 'settings.authentication.saml.confirm',
+ newScimStatus ? 'scim' : 'jit',
+ )}
+ onClose={() => setShowConfirmProvisioningModal(false)}
+ isDestructive={!newScimStatus}
+ confirmButtonText={translate('yes')}
>
- {translate('cancel')}
- </ResetButtonLink>
- </>
- )}
- {showConfirmProvisioningModal && (
- <ConfirmModal
- onConfirm={() => handleConfirmChangeProvisioning()}
- header={translate(
- 'settings.authentication.saml.confirm',
- newScimStatus ? 'scim' : 'jit',
- )}
- onClose={() => setShowConfirmProvisioningModal(false)}
- isDestructive={!newScimStatus}
- confirmButtonText={translate('yes')}
- >
- {translate(
- 'settings.authentication.saml.confirm',
- newScimStatus ? 'scim' : 'jit',
- 'description',
- )}
- </ConfirmModal>
- )}
- </form>
- </div>
- </>
- )}
- {showEditModal && (
- <ConfigurationForm
- tab={SAML}
- excludedField={SAML_EXCLUDED_FIELD}
- loading={isLoading}
- values={values}
- setNewValue={setNewValue}
- canBeSave={canBeSave}
- onClose={handleCancelConfiguration}
- create={!hasConfiguration}
- />
- )}
- </div>
+ {translate(
+ 'settings.authentication.saml.confirm',
+ newScimStatus ? 'scim' : 'jit',
+ 'description',
+ )}
+ </ConfirmModal>
+ )}
+ </form>
+ </div>
+ </>
+ )}
+ {showEditModal && (
+ <ConfigurationForm
+ tab={SAML}
+ excludedField={SAML_EXCLUDED_FIELD}
+ loading={isLoading}
+ values={values}
+ setNewValue={setNewValue}
+ canBeSave={canBeSave}
+ onClose={handleCancelConfiguration}
+ create={!hasConfiguration}
+ />
+ )}
+ </div>
+ </Spinner>
);
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { screen, waitFor, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
+import React from 'react';
+import ComputeEngineServiceMock from '../../../../../api/mocks/ComputeEngineServiceMock';
+import GithubProvisioningServiceMock from '../../../../../api/mocks/GithubProvisioningServiceMock';
+import SettingsServiceMock from '../../../../../api/mocks/SettingsServiceMock';
+import SystemServiceMock from '../../../../../api/mocks/SystemServiceMock';
+import { AvailableFeaturesContext } from '../../../../../app/components/available-features/AvailableFeaturesContext';
+import { definitions } from '../../../../../helpers/mocks/definitions-list';
+import { renderComponent } from '../../../../../helpers/testReactTestingUtils';
+import { byRole, byText } from '../../../../../helpers/testSelector';
+import { AlmKeys } from '../../../../../types/alm-settings';
+import { Feature } from '../../../../../types/features';
+import { GitHubProvisioningStatus } from '../../../../../types/provisioning';
+import { TaskStatuses } from '../../../../../types/tasks';
+import Authentication from '../Authentication';
+
+let handler: GithubProvisioningServiceMock;
+let system: SystemServiceMock;
+let settingsHandler: SettingsServiceMock;
+let computeEngineHandler: ComputeEngineServiceMock;
+
+beforeEach(() => {
+ handler = new GithubProvisioningServiceMock();
+ system = new SystemServiceMock();
+ settingsHandler = new SettingsServiceMock();
+ computeEngineHandler = new ComputeEngineServiceMock();
+});
+
+afterEach(() => {
+ handler.reset();
+ settingsHandler.reset();
+ system.reset();
+ computeEngineHandler.reset();
+});
+
+const ghContainer = byRole('tabpanel', { name: 'github GitHub' });
+
+const ui = {
+ saveButton: byRole('button', { name: 'settings.authentication.saml.form.save' }),
+ customMessageInformation: byText('settings.authentication.custom_message_information'),
+ enabledToggle: byRole('switch'),
+ testButton: byText('settings.authentication.saml.form.test'),
+ textbox1: byRole('textbox', { name: 'test1' }),
+ textbox2: byRole('textbox', { name: 'test2' }),
+ tab: byRole('tab', { name: 'github GitHub' }),
+ noGithubConfiguration: byText('settings.authentication.github.form.not_configured'),
+ 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',
+ }),
+ clientSecret: byRole('textbox', {
+ name: 'property.sonar.auth.github.clientSecret.secured.name',
+ }),
+ githubApiUrl: byRole('textbox', { name: 'property.sonar.auth.github.apiUrl.name' }),
+ githubWebUrl: byRole('textbox', { name: 'property.sonar.auth.github.webUrl.name' }),
+ allowUsersToSignUp: byRole('switch', {
+ name: 'sonar.auth.github.allowUsersToSignUp',
+ }),
+ 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: 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', {
+ name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
+ }).byRole('row'),
+ customRoleInput: byRole('textbox', {
+ name: 'settings.authentication.github.configuration.roles_mapping.dialog.add_custom_role',
+ }),
+ customRoleAddBtn: byRole('dialog', {
+ name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
+ }).byRole('button', { name: 'add_verb' }),
+ roleExistsError: byRole('dialog', {
+ name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
+ }).byText('settings.authentication.github.configuration.roles_mapping.role_exists'),
+ emptyRoleError: byRole('dialog', {
+ name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
+ }).byText('settings.authentication.github.configuration.roles_mapping.empty_custom_role'),
+ deleteCustomRoleCustom2: byRole('button', {
+ name: 'settings.authentication.github.configuration.roles_mapping.dialog.delete_custom_role.custom2',
+ }),
+ getMappingRowByRole: (text: string) =>
+ ui.mappingRow.getAll().find((row) => within(row).queryByText(text) !== null),
+ mappingCheckbox: byRole('checkbox'),
+ mappingDialogClose: byRole('dialog', {
+ name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
+ }).byRole('button', {
+ name: 'close',
+ }),
+ deleteOrg: (org: string) =>
+ byRole('button', {
+ name: `settings.definition.delete_value.property.sonar.auth.github.organizations.name.${org}`,
+ }),
+ enableFirstMessage: ghContainer.byText('settings.authentication.github.enable_first'),
+ jitProvisioningButton: ghContainer.byRole('radio', {
+ name: 'settings.authentication.form.provisioning_at_login',
+ }),
+ githubProvisioningButton: ghContainer.byRole('radio', {
+ name: 'settings.authentication.github.form.provisioning_with_github',
+ }),
+ 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: ghContainer.byRole('status', {
+ name: /github.configuration.validation.valid/,
+ }),
+ configurationValidityError: ghContainer.byRole('status', {
+ name: /github.configuration.validation.invalid/,
+ }),
+ syncWarning: ghContainer.byText(/Warning/),
+ syncSummary: ghContainer.byText(/Test summary/),
+ configurationValidityWarning: ghContainer.byRole('status', {
+ name: /github.configuration.validation.valid.short/,
+ }),
+ checkConfigButton: ghContainer.byRole('button', {
+ name: 'settings.authentication.github.configuration.validation.test',
+ }),
+ viewConfigValidityDetailsButton: ghContainer.byRole('button', {
+ name: 'settings.authentication.github.configuration.validation.details',
+ }),
+ configDetailsDialog: byRole('dialog', {
+ name: 'settings.authentication.github.configuration.validation.details.title',
+ }),
+ continueAutoButton: byRole('button', {
+ name: 'settings.authentication.github.confirm_auto_provisioning.continue',
+ }),
+ switchJitButton: byRole('button', {
+ name: 'settings.authentication.github.confirm_auto_provisioning.switch_jit',
+ }),
+ consentDialog: byRole('dialog', {
+ name: 'settings.authentication.github.confirm_auto_provisioning.header',
+ }),
+ getConfigDetailsTitle: () => within(ui.configDetailsDialog.get()).getByRole('heading'),
+ getOrgs: () => within(ui.configDetailsDialog.get()).getAllByRole('listitem'),
+ fillForm: async (user: UserEvent) => {
+ await user.type(await ui.clientId.find(), 'Awsome GITHUB config');
+ await user.type(ui.clientSecret.get(), 'Client shut');
+ await user.type(ui.appId.get(), 'App id');
+ await user.type(ui.privateKey.get(), 'Private Key');
+ await user.type(ui.githubApiUrl.get(), 'API Url');
+ await user.type(ui.githubWebUrl.get(), 'WEb Url');
+ await user.type(ui.organizations.get(), 'organization1');
+ },
+ createConfiguration: async (user: UserEvent) => {
+ await user.click(await ui.createConfigButton.find());
+ await ui.fillForm(user);
+
+ await user.click(ui.saveConfigButton.get());
+ },
+ enableConfiguration: async (user: UserEvent) => {
+ await user.click(await ui.tab.find());
+ await ui.createConfiguration(user);
+ await user.click(await ui.enableConfigButton.find());
+ },
+ enableProvisioning: async (user: UserEvent) => {
+ await user.click(await ui.tab.find());
+
+ await ui.createConfiguration(user);
+
+ await user.click(await ui.enableConfigButton.find());
+ await user.click(await ui.githubProvisioningButton.find());
+ await user.click(ui.saveGithubProvisioning.get());
+ await user.click(ui.confirmProvisioningButton.get());
+ },
+};
+
+describe('Github tab', () => {
+ it('should render an empty Github configuration', async () => {
+ renderAuthentication();
+ const user = userEvent.setup();
+ await user.click(await ui.tab.find());
+ expect(await ui.noGithubConfiguration.find()).toBeInTheDocument();
+ });
+
+ it('should be able to create a configuration', async () => {
+ const user = userEvent.setup();
+ renderAuthentication();
+
+ await user.click(await ui.tab.find());
+ await user.click(await ui.createConfigButton.find());
+
+ expect(ui.saveConfigButton.get()).toBeDisabled();
+
+ await ui.fillForm(user);
+ expect(ui.saveConfigButton.get()).toBeEnabled();
+
+ await user.click(ui.saveConfigButton.get());
+
+ expect(await ui.editConfigButton.find()).toBeInTheDocument();
+ });
+
+ it('should be able to edit configuration', async () => {
+ const user = userEvent.setup();
+ renderAuthentication();
+ await user.click(await ui.tab.find());
+
+ await ui.createConfiguration(user);
+
+ await user.click(ui.editConfigButton.get());
+ await user.click(ui.deleteOrg('organization1').get());
+
+ await user.click(ui.saveConfigButton.get());
+
+ await user.click(await ui.editConfigButton.find());
+
+ expect(ui.organizations.get()).toHaveValue('');
+ });
+
+ it('should be able to enable/disable configuration', async () => {
+ const user = userEvent.setup();
+ renderAuthentication();
+ await user.click(await ui.tab.find());
+
+ await ui.createConfiguration(user);
+
+ await user.click(await ui.enableConfigButton.find());
+
+ expect(await ui.disableConfigButton.find()).toBeInTheDocument();
+ await user.click(ui.disableConfigButton.get());
+ await waitFor(() => expect(ui.disableConfigButton.query()).not.toBeInTheDocument());
+
+ expect(await ui.enableConfigButton.find()).toBeInTheDocument();
+ });
+
+ it('should not allow edtion below Enterprise to select Github provisioning', async () => {
+ const user = userEvent.setup();
+
+ renderAuthentication();
+ await user.click(await ui.tab.find());
+
+ await ui.createConfiguration(user);
+ await user.click(await ui.enableConfigButton.find());
+
+ expect(await ui.jitProvisioningButton.find()).toBeChecked();
+ expect(ui.githubProvisioningButton.get()).toHaveAttribute('aria-disabled', 'true');
+ });
+
+ it('should be able to choose provisioning', async () => {
+ const user = userEvent.setup();
+
+ renderAuthentication([Feature.GithubProvisioning]);
+ await user.click(await ui.tab.find());
+
+ await ui.createConfiguration(user);
+
+ expect(await ui.enableFirstMessage.find()).toBeInTheDocument();
+ await user.click(await ui.enableConfigButton.find());
+
+ expect(await ui.jitProvisioningButton.find()).toBeChecked();
+
+ expect(ui.saveGithubProvisioning.get()).toBeDisabled();
+ await user.click(ui.allowUsersToSignUp.get());
+
+ expect(ui.saveGithubProvisioning.get()).toBeEnabled();
+ await user.click(ui.saveGithubProvisioning.get());
+
+ await waitFor(() => expect(ui.saveGithubProvisioning.query()).toBeDisabled());
+
+ await user.click(ui.githubProvisioningButton.get());
+
+ expect(ui.saveGithubProvisioning.get()).toBeEnabled();
+ await user.click(ui.saveGithubProvisioning.get());
+ await user.click(ui.confirmProvisioningButton.get());
+
+ expect(await ui.githubProvisioningButton.find()).toBeChecked();
+ expect(ui.disableConfigButton.get()).toBeDisabled();
+ expect(ui.saveGithubProvisioning.get()).toBeDisabled();
+ });
+
+ describe('Github Provisioning', () => {
+ let user: UserEvent;
+
+ beforeEach(() => {
+ jest.useFakeTimers({
+ advanceTimers: true,
+ now: new Date('2022-02-04T12:00:59Z'),
+ });
+ user = userEvent.setup();
+ });
+
+ afterEach(() => {
+ jest.runOnlyPendingTimers();
+ jest.useRealTimers();
+ });
+
+ it('should display a success status when the synchronisation is a success', async () => {
+ handler.addProvisioningTask({
+ status: TaskStatuses.Success,
+ executedAt: '2022-02-03T11:45:35+0200',
+ });
+
+ renderAuthentication([Feature.GithubProvisioning]);
+ await ui.enableProvisioning(user);
+ expect(ui.githubProvisioningSuccess.get()).toBeInTheDocument();
+ expect(ui.syncSummary.get()).toBeInTheDocument();
+ });
+
+ it('should display a success status even when another task is pending', async () => {
+ handler.addProvisioningTask({
+ status: TaskStatuses.Pending,
+ executedAt: '2022-02-03T11:55:35+0200',
+ });
+ handler.addProvisioningTask({
+ status: TaskStatuses.Success,
+ executedAt: '2022-02-03T11:45:35+0200',
+ });
+ renderAuthentication([Feature.GithubProvisioning]);
+ await ui.enableProvisioning(user);
+ expect(ui.githubProvisioningSuccess.get()).toBeInTheDocument();
+ expect(ui.githubProvisioningPending.get()).toBeInTheDocument();
+ });
+
+ it('should display an error alert when the synchronisation failed', async () => {
+ handler.addProvisioningTask({
+ status: TaskStatuses.Failed,
+ executedAt: '2022-02-03T11:45:35+0200',
+ errorMessage: "T'es mauvais Jacques",
+ });
+ renderAuthentication([Feature.GithubProvisioning]);
+ await ui.enableProvisioning(user);
+ expect(ui.githubProvisioningAlert.get()).toBeInTheDocument();
+ expect(ui.githubProvisioningButton.get()).toHaveTextContent("T'es mauvais Jacques");
+ expect(ui.githubProvisioningSuccess.query()).not.toBeInTheDocument();
+ });
+
+ it('should display an error alert even when another task is in progress', async () => {
+ handler.addProvisioningTask({
+ status: TaskStatuses.InProgress,
+ executedAt: '2022-02-03T11:55:35+0200',
+ });
+ handler.addProvisioningTask({
+ status: TaskStatuses.Failed,
+ executedAt: '2022-02-03T11:45:35+0200',
+ errorMessage: "T'es mauvais Jacques",
+ });
+ renderAuthentication([Feature.GithubProvisioning]);
+ await ui.enableProvisioning(user);
+ expect(ui.githubProvisioningAlert.get()).toBeInTheDocument();
+ expect(ui.githubProvisioningButton.get()).toHaveTextContent("T'es mauvais Jacques");
+ expect(ui.githubProvisioningSuccess.query()).not.toBeInTheDocument();
+ expect(ui.githubProvisioningInProgress.get()).toBeInTheDocument();
+ });
+
+ it('should display that config is valid for both provisioning with 1 org', async () => {
+ renderAuthentication([Feature.GithubProvisioning]);
+ await ui.enableConfiguration(user);
+
+ await appLoaded();
+
+ await waitFor(() => expect(ui.configurationValiditySuccess.query()).toBeInTheDocument());
+ });
+
+ it('should display that config is valid for both provisioning with multiple orgs', async () => {
+ handler.setConfigurationValidity({
+ installations: [
+ {
+ organization: 'org1',
+ autoProvisioning: { status: GitHubProvisioningStatus.Success },
+ jit: { status: GitHubProvisioningStatus.Success },
+ },
+ {
+ organization: 'org2',
+ autoProvisioning: { status: GitHubProvisioningStatus.Success },
+ jit: { status: GitHubProvisioningStatus.Success },
+ },
+ ],
+ });
+ renderAuthentication([Feature.GithubProvisioning]);
+ await ui.enableConfiguration(user);
+
+ await appLoaded();
+
+ await waitFor(() => expect(ui.configurationValiditySuccess.query()).toBeInTheDocument());
+ expect(ui.configurationValiditySuccess.get()).toHaveTextContent('2');
+
+ await user.click(ui.viewConfigValidityDetailsButton.get());
+ expect(ui.getConfigDetailsTitle()).toHaveTextContent(
+ 'settings.authentication.github.configuration.validation.details.valid_label',
+ );
+ expect(ui.getOrgs()[0]).toHaveTextContent(
+ 'settings.authentication.github.configuration.validation.details.valid_labelorg1',
+ );
+ expect(ui.getOrgs()[1]).toHaveTextContent(
+ 'settings.authentication.github.configuration.validation.details.valid_labelorg2',
+ );
+ });
+
+ it('should display that config is invalid for autoprovisioning if some apps are suspended but valid for jit', async () => {
+ const errorMessage = 'Installation suspended';
+ handler.setConfigurationValidity({
+ installations: [
+ {
+ organization: 'org1',
+ autoProvisioning: {
+ status: GitHubProvisioningStatus.Failed,
+ errorMessage,
+ },
+ jit: {
+ status: GitHubProvisioningStatus.Failed,
+ errorMessage,
+ },
+ },
+ ],
+ });
+
+ renderAuthentication([Feature.GithubProvisioning]);
+ await ui.enableConfiguration(user);
+
+ await appLoaded();
+
+ await waitFor(() => expect(ui.configurationValidityWarning.get()).toBeInTheDocument());
+ expect(ui.configurationValidityWarning.get()).toHaveTextContent(errorMessage);
+
+ await user.click(ui.viewConfigValidityDetailsButton.get());
+ expect(ui.getConfigDetailsTitle()).toHaveTextContent(
+ 'settings.authentication.github.configuration.validation.details.valid_label',
+ );
+ expect(ui.getOrgs()[0]).toHaveTextContent(
+ 'settings.authentication.github.configuration.validation.details.invalid_labelorg1 - Installation suspended',
+ );
+
+ await user.click(ui.configDetailsDialog.byRole('button', { name: 'close' }).get());
+
+ await user.click(ui.githubProvisioningButton.get());
+ await waitFor(() => expect(ui.configurationValidityError.get()).toBeInTheDocument());
+ expect(ui.configurationValidityError.get()).toHaveTextContent(errorMessage);
+ });
+
+ it('should display that config is valid but some organizations were not found', async () => {
+ handler.setConfigurationValidity({
+ installations: [
+ {
+ organization: 'org1',
+ autoProvisioning: { status: GitHubProvisioningStatus.Success },
+ jit: { status: GitHubProvisioningStatus.Success },
+ },
+ ],
+ });
+
+ renderAuthentication([Feature.GithubProvisioning]);
+ await ui.enableConfiguration(user);
+
+ await appLoaded();
+
+ await waitFor(() => expect(ui.configurationValiditySuccess.get()).toBeInTheDocument());
+ expect(ui.configurationValiditySuccess.get()).toHaveTextContent('1');
+
+ await user.click(ui.viewConfigValidityDetailsButton.get());
+ expect(ui.getConfigDetailsTitle()).toHaveTextContent(
+ 'settings.authentication.github.configuration.validation.details.valid_label',
+ );
+ expect(ui.getOrgs()[0]).toHaveTextContent(
+ 'settings.authentication.github.configuration.validation.details.valid_labelorg1',
+ );
+ expect(ui.getOrgs()[1]).toHaveTextContent(
+ 'settings.authentication.github.configuration.validation.details.org_not_found.organization1',
+ );
+ });
+
+ it('should display that config is invalid', async () => {
+ const errorMessage = 'Test error';
+ handler.setConfigurationValidity({
+ application: {
+ jit: {
+ status: GitHubProvisioningStatus.Failed,
+ errorMessage,
+ },
+ autoProvisioning: {
+ status: GitHubProvisioningStatus.Failed,
+ errorMessage,
+ },
+ },
+ });
+ renderAuthentication([Feature.GithubProvisioning]);
+ await ui.enableConfiguration(user);
+
+ await appLoaded();
+
+ await waitFor(() => expect(ui.configurationValidityError.query()).toBeInTheDocument());
+ expect(ui.configurationValidityError.get()).toHaveTextContent(errorMessage);
+
+ await user.click(ui.viewConfigValidityDetailsButton.get());
+ expect(ui.getConfigDetailsTitle()).toHaveTextContent(
+ 'settings.authentication.github.configuration.validation.details.invalid_label',
+ );
+ expect(ui.configDetailsDialog.get()).toHaveTextContent(errorMessage);
+ });
+
+ it('should display that config is valid for jit, but not for auto', async () => {
+ const errorMessage = 'Test error';
+ handler.setConfigurationValidity({
+ application: {
+ jit: {
+ status: GitHubProvisioningStatus.Success,
+ },
+ autoProvisioning: {
+ status: GitHubProvisioningStatus.Failed,
+ errorMessage,
+ },
+ },
+ });
+ renderAuthentication([Feature.GithubProvisioning]);
+ await ui.enableConfiguration(user);
+
+ await appLoaded();
+
+ await waitFor(() => expect(ui.configurationValiditySuccess.query()).toBeInTheDocument());
+ expect(ui.configurationValiditySuccess.get()).not.toHaveTextContent(errorMessage);
+
+ await user.click(ui.viewConfigValidityDetailsButton.get());
+ expect(ui.getConfigDetailsTitle()).toHaveTextContent(
+ 'settings.authentication.github.configuration.validation.details.valid_label',
+ );
+ await user.click(ui.configDetailsDialog.byRole('button', { name: 'close' }).get());
+
+ await user.click(ui.githubProvisioningButton.get());
+
+ expect(ui.configurationValidityError.get()).toBeInTheDocument();
+ expect(ui.configurationValidityError.get()).toHaveTextContent(errorMessage);
+
+ await user.click(ui.viewConfigValidityDetailsButton.get());
+ expect(ui.getConfigDetailsTitle()).toHaveTextContent(
+ 'settings.authentication.github.configuration.validation.details.invalid_label',
+ );
+ });
+
+ it('should display that config is invalid because of orgs', async () => {
+ const errorMessage = 'Test error';
+ handler.setConfigurationValidity({
+ installations: [
+ {
+ organization: 'org1',
+ autoProvisioning: { status: GitHubProvisioningStatus.Success },
+ jit: { status: GitHubProvisioningStatus.Success },
+ },
+ {
+ organization: 'org2',
+ jit: { status: GitHubProvisioningStatus.Failed, errorMessage },
+ autoProvisioning: { status: GitHubProvisioningStatus.Failed, errorMessage },
+ },
+ ],
+ });
+ renderAuthentication([Feature.GithubProvisioning]);
+ await ui.enableConfiguration(user);
+
+ await appLoaded();
+
+ await waitFor(() => expect(ui.configurationValiditySuccess.query()).toBeInTheDocument());
+
+ await user.click(ui.viewConfigValidityDetailsButton.get());
+
+ expect(ui.getOrgs()[0]).toHaveTextContent(
+ 'settings.authentication.github.configuration.validation.details.valid_labelorg1',
+ );
+ expect(ui.getOrgs()[1]).toHaveTextContent(
+ 'settings.authentication.github.configuration.validation.details.invalid_labelorg2 - Test error',
+ );
+
+ await user.click(ui.configDetailsDialog.byRole('button', { name: 'close' }).get());
+
+ await user.click(ui.githubProvisioningButton.get());
+
+ expect(ui.configurationValidityError.get()).toBeInTheDocument();
+ expect(ui.configurationValidityError.get()).toHaveTextContent(
+ `settings.authentication.github.configuration.validation.invalid_org.org2.${errorMessage}`,
+ );
+ await user.click(ui.viewConfigValidityDetailsButton.get());
+ expect(ui.getOrgs()[1]).toHaveTextContent(
+ `settings.authentication.github.configuration.validation.details.invalid_labelorg2 - ${errorMessage}`,
+ );
+ });
+
+ it('should update provisioning validity after clicking Test Configuration', async () => {
+ const errorMessage = 'Test error';
+ handler.setConfigurationValidity({
+ application: {
+ jit: {
+ status: GitHubProvisioningStatus.Failed,
+ errorMessage,
+ },
+ autoProvisioning: {
+ status: GitHubProvisioningStatus.Failed,
+ errorMessage,
+ },
+ },
+ });
+ renderAuthentication([Feature.GithubProvisioning]);
+ await ui.enableConfiguration(user);
+ handler.setConfigurationValidity({
+ application: {
+ jit: {
+ status: GitHubProvisioningStatus.Success,
+ },
+ autoProvisioning: {
+ status: GitHubProvisioningStatus.Success,
+ },
+ },
+ });
+
+ await appLoaded();
+
+ expect(await ui.configurationValidityError.find()).toBeInTheDocument();
+
+ await user.click(ui.checkConfigButton.get());
+
+ expect(ui.configurationValiditySuccess.get()).toBeInTheDocument();
+ expect(ui.configurationValidityError.query()).not.toBeInTheDocument();
+ });
+
+ it('should show warning', async () => {
+ handler.addProvisioningTask({
+ status: TaskStatuses.Success,
+ warnings: ['Warning'],
+ });
+ renderAuthentication([Feature.GithubProvisioning]);
+ await ui.enableProvisioning(user);
+
+ expect(await ui.syncWarning.find()).toBeInTheDocument();
+ expect(ui.syncSummary.get()).toBeInTheDocument();
+ });
+
+ it('should display a modal if user was already using auto and continue using auto provisioning', async () => {
+ const user = userEvent.setup();
+ settingsHandler.presetGithubAutoProvisioning();
+ handler.enableGithubProvisioning();
+ settingsHandler.set('sonar.auth.github.userConsentForPermissionProvisioningRequired', '');
+ renderAuthentication([Feature.GithubProvisioning]);
+
+ await user.click(await ui.tab.find());
+
+ expect(await ui.consentDialog.find()).toBeInTheDocument();
+ await user.click(ui.continueAutoButton.get());
+
+ expect(await ui.githubProvisioningButton.find()).toBeChecked();
+ expect(ui.consentDialog.query()).not.toBeInTheDocument();
+ });
+
+ it('should display a modal if user was already using auto and switch to JIT', async () => {
+ const user = userEvent.setup();
+ settingsHandler.presetGithubAutoProvisioning();
+ handler.enableGithubProvisioning();
+ settingsHandler.set('sonar.auth.github.userConsentForPermissionProvisioningRequired', '');
+ renderAuthentication([Feature.GithubProvisioning]);
+
+ await user.click(await ui.tab.find());
+
+ expect(await ui.consentDialog.find()).toBeInTheDocument();
+ await user.click(ui.switchJitButton.get());
+
+ expect(await ui.jitProvisioningButton.find()).toBeChecked();
+ expect(ui.consentDialog.query()).not.toBeInTheDocument();
+ });
+
+ it('should sort mapping rows', async () => {
+ const user = userEvent.setup();
+ settingsHandler.presetGithubAutoProvisioning();
+ handler.enableGithubProvisioning();
+ renderAuthentication([Feature.GithubProvisioning]);
+ await user.click(await ui.tab.find());
+
+ expect(await ui.editMappingButton.find()).toBeInTheDocument();
+ await user.click(ui.editMappingButton.get());
+
+ const rows = (await ui.mappingRow.findAll()).filter(
+ (row) => within(row).queryAllByRole('checkbox').length > 0,
+ );
+
+ expect(rows).toHaveLength(5);
+
+ expect(rows[0]).toHaveTextContent('read');
+ expect(rows[1]).toHaveTextContent('triage');
+ expect(rows[2]).toHaveTextContent('write');
+ expect(rows[3]).toHaveTextContent('maintain');
+ expect(rows[4]).toHaveTextContent('admin');
+ });
+
+ it('should apply new mapping and new provisioning type at the same time', async () => {
+ const user = userEvent.setup();
+ renderAuthentication([Feature.GithubProvisioning]);
+ await user.click(await ui.tab.find());
+
+ await ui.createConfiguration(user);
+ await user.click(await ui.enableConfigButton.find());
+
+ expect(await ui.jitProvisioningButton.find()).toBeChecked();
+ expect(ui.editMappingButton.query()).not.toBeInTheDocument();
+ await user.click(ui.githubProvisioningButton.get());
+ expect(await ui.editMappingButton.find()).toBeInTheDocument();
+ await user.click(ui.editMappingButton.get());
+
+ expect(await ui.mappingRow.findAll()).toHaveLength(7);
+
+ let readCheckboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('read'));
+ let adminCheckboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('admin'));
+
+ expect(readCheckboxes[0]).toBeChecked();
+ expect(readCheckboxes[5]).not.toBeChecked();
+ expect(adminCheckboxes[5]).toBeChecked();
+
+ await user.click(readCheckboxes[0]);
+ await user.click(readCheckboxes[5]);
+ await user.click(adminCheckboxes[5]);
+ await user.click(ui.mappingDialogClose.get());
+
+ await user.click(ui.saveGithubProvisioning.get());
+ await user.click(ui.confirmProvisioningButton.get());
+
+ // Clean local mapping state
+ await user.click(ui.jitProvisioningButton.get());
+ await user.click(ui.githubProvisioningButton.get());
+
+ await user.click(ui.editMappingButton.get());
+ readCheckboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('read'));
+ adminCheckboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('admin'));
+
+ expect(readCheckboxes[0]).not.toBeChecked();
+ expect(readCheckboxes[5]).toBeChecked();
+ expect(adminCheckboxes[5]).not.toBeChecked();
+ await user.click(ui.mappingDialogClose.get());
+ });
+
+ it('should apply new mapping on auto-provisioning', async () => {
+ const user = userEvent.setup();
+ settingsHandler.presetGithubAutoProvisioning();
+ handler.enableGithubProvisioning();
+ renderAuthentication([Feature.GithubProvisioning]);
+ await user.click(await ui.tab.find());
+
+ expect(await ui.saveGithubProvisioning.find()).toBeDisabled();
+ await user.click(ui.editMappingButton.get());
+
+ expect(await ui.mappingRow.findAll()).toHaveLength(7);
+
+ let readCheckboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('read'))[0];
+
+ expect(readCheckboxes).toBeChecked();
+
+ await user.click(readCheckboxes);
+ await user.click(ui.mappingDialogClose.get());
+
+ expect(await ui.saveGithubProvisioning.find()).toBeEnabled();
+
+ await user.click(ui.saveGithubProvisioning.get());
+
+ // Clean local mapping state
+ await user.click(ui.jitProvisioningButton.get());
+ await user.click(ui.githubProvisioningButton.get());
+
+ await user.click(ui.editMappingButton.get());
+ readCheckboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('read'))[0];
+
+ expect(readCheckboxes).not.toBeChecked();
+ await user.click(ui.mappingDialogClose.get());
+ });
+
+ it('should add/remove/update custom roles', async () => {
+ const user = userEvent.setup();
+ settingsHandler.presetGithubAutoProvisioning();
+ handler.enableGithubProvisioning();
+ handler.addGitHubCustomRole('custom1', ['user', 'codeViewer', 'scan']);
+ handler.addGitHubCustomRole('custom2', ['user', 'codeViewer', 'issueAdmin', 'scan']);
+ renderAuthentication([Feature.GithubProvisioning]);
+ await user.click(await ui.tab.find());
+
+ expect(await ui.saveGithubProvisioning.find()).toBeDisabled();
+ await user.click(ui.editMappingButton.get());
+
+ const rows = (await ui.mappingRow.findAll()).filter(
+ (row) => within(row).queryAllByRole('checkbox').length > 0,
+ );
+
+ expect(rows).toHaveLength(7);
+
+ let custom1Checkboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('custom1'));
+
+ expect(custom1Checkboxes[0]).toBeChecked();
+ expect(custom1Checkboxes[1]).toBeChecked();
+ expect(custom1Checkboxes[2]).not.toBeChecked();
+ expect(custom1Checkboxes[3]).not.toBeChecked();
+ expect(custom1Checkboxes[4]).not.toBeChecked();
+ expect(custom1Checkboxes[5]).toBeChecked();
+
+ await user.click(custom1Checkboxes[1]);
+ await user.click(custom1Checkboxes[2]);
+
+ await user.click(ui.deleteCustomRoleCustom2.get());
+
+ expect(ui.customRoleInput.get()).toHaveValue('');
+ await user.type(ui.customRoleInput.get(), 'read');
+ await user.click(ui.customRoleAddBtn.get());
+ expect(await ui.roleExistsError.find()).toBeInTheDocument();
+ expect(ui.customRoleAddBtn.get()).toBeDisabled();
+ await user.clear(ui.customRoleInput.get());
+ expect(ui.roleExistsError.query()).not.toBeInTheDocument();
+ await user.type(ui.customRoleInput.get(), 'custom1');
+ await user.click(ui.customRoleAddBtn.get());
+ expect(await ui.roleExistsError.find()).toBeInTheDocument();
+ expect(ui.customRoleAddBtn.get()).toBeDisabled();
+ await user.clear(ui.customRoleInput.get());
+ await user.type(ui.customRoleInput.get(), 'custom3');
+ expect(ui.roleExistsError.query()).not.toBeInTheDocument();
+ expect(ui.customRoleAddBtn.get()).toBeEnabled();
+ await user.click(ui.customRoleAddBtn.get());
+
+ let custom3Checkboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('custom3'));
+ expect(custom3Checkboxes[0]).toBeChecked();
+ expect(custom3Checkboxes[1]).not.toBeChecked();
+ expect(custom3Checkboxes[2]).not.toBeChecked();
+ expect(custom3Checkboxes[3]).not.toBeChecked();
+ expect(custom3Checkboxes[4]).not.toBeChecked();
+ expect(custom3Checkboxes[5]).not.toBeChecked();
+ await user.click(custom3Checkboxes[0]);
+ expect(await ui.emptyRoleError.find()).toBeInTheDocument();
+ expect(ui.mappingDialogClose.get()).toBeDisabled();
+ await user.click(custom3Checkboxes[1]);
+ expect(ui.emptyRoleError.query()).not.toBeInTheDocument();
+ expect(ui.mappingDialogClose.get()).toBeEnabled();
+ await user.click(ui.mappingDialogClose.get());
+
+ expect(await ui.saveGithubProvisioning.find()).toBeEnabled();
+ await user.click(ui.saveGithubProvisioning.get());
+
+ // Clean local mapping state
+ await user.click(ui.jitProvisioningButton.get());
+ await user.click(ui.githubProvisioningButton.get());
+
+ await user.click(ui.editMappingButton.get());
+
+ expect(
+ (await ui.mappingRow.findAll()).filter(
+ (row) => within(row).queryAllByRole('checkbox').length > 0,
+ ),
+ ).toHaveLength(7);
+ custom1Checkboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('custom1'));
+ custom3Checkboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('custom3'));
+ expect(ui.getMappingRowByRole('custom2')).toBeUndefined();
+ expect(custom1Checkboxes[0]).toBeChecked();
+ expect(custom1Checkboxes[1]).not.toBeChecked();
+ expect(custom1Checkboxes[2]).toBeChecked();
+ expect(custom1Checkboxes[3]).not.toBeChecked();
+ expect(custom1Checkboxes[4]).not.toBeChecked();
+ expect(custom1Checkboxes[5]).toBeChecked();
+ expect(custom3Checkboxes[0]).not.toBeChecked();
+ expect(custom3Checkboxes[1]).toBeChecked();
+ expect(custom3Checkboxes[2]).not.toBeChecked();
+ expect(custom3Checkboxes[3]).not.toBeChecked();
+ expect(custom3Checkboxes[4]).not.toBeChecked();
+ expect(custom3Checkboxes[5]).not.toBeChecked();
+ await user.click(ui.mappingDialogClose.get());
+ });
+ });
+});
+
+const appLoaded = async () => {
+ await waitFor(async () => {
+ expect(await screen.findByText('loading')).not.toBeInTheDocument();
+ });
+};
+
+function renderAuthentication(features: Feature[] = []) {
+ renderComponent(
+ <AvailableFeaturesContext.Provider value={features}>
+ <Authentication definitions={definitions} />
+ </AvailableFeaturesContext.Provider>,
+ `?tab=${AlmKeys.GitHub}`,
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import userEvent from '@testing-library/user-event';
+import React from 'react';
+import ComputeEngineServiceMock from '../../../../../api/mocks/ComputeEngineServiceMock';
+import GitlabProvisioningServiceMock from '../../../../../api/mocks/GitlabProvisioningServiceMock';
+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 } from '../../../../../helpers/testSelector';
+import { AlmKeys } from '../../../../../types/alm-settings';
+import { Feature } from '../../../../../types/features';
+import { ProvisioningType } from '../../../../../types/provisioning';
+import { TaskStatuses, TaskTypes } from '../../../../../types/tasks';
+import Authentication from '../Authentication';
+
+let handler: GitlabProvisioningServiceMock;
+let system: SystemServiceMock;
+let settingsHandler: SettingsServiceMock;
+let computeEngineHandler: ComputeEngineServiceMock;
+
+beforeEach(() => {
+ handler = new GitlabProvisioningServiceMock();
+ system = new SystemServiceMock();
+ settingsHandler = new SettingsServiceMock();
+ computeEngineHandler = new ComputeEngineServiceMock();
+});
+
+afterEach(() => {
+ handler.reset();
+ settingsHandler.reset();
+ system.reset();
+ computeEngineHandler.reset();
+});
+
+const glContainer = byRole('tabpanel', { name: 'gitlab GitLab' });
+
+const ui = {
+ 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' }),
+ secret: byRole('textbox', {
+ name: 'property.secret.name',
+ }),
+ synchronizeGroups: byRole('switch', {
+ name: 'synchronizeGroups',
+ }),
+ 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: 'allowUsersToSignUp' }),
+ autoProvisioningToken: byRole('textbox', {
+ name: 'property.provisioningToken.name',
+ }),
+ autoProvisioningUpdateTokenButton: byRole('button', {
+ name: 'settings.almintegration.form.secret.update_field',
+ }),
+ autoProvisioningGroupsInput: byRole('textbox', {
+ name: 'property.provisioningGroups.name',
+ }),
+ removeProvisioniongGroup: byRole('button', {
+ name: /settings.definition.delete_value.property.provisioningGroups.name./,
+ }),
+ saveProvisioning: glContainer.byRole('button', { name: 'save' }),
+ cancelProvisioningChanges: glContainer.byRole('button', { name: 'cancel' }),
+ confirmAutoProvisioningDialog: byRole('dialog', {
+ name: 'settings.authentication.gitlab.confirm.AUTO_PROVISIONING',
+ }),
+ 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 create a Gitlab configuration and disable it', async () => {
+ handler.setGitlabConfigurations([]);
+ renderAuthentication();
+ const user = userEvent.setup();
+
+ expect(await ui.noGitlabConfiguration.find()).toBeInTheDocument();
+ expect(ui.createConfigButton.get()).toBeInTheDocument();
+
+ await user.click(ui.createConfigButton.get());
+ expect(await ui.createDialog.find()).toBeInTheDocument();
+ await user.type(ui.applicationId.get(), '123');
+ await user.type(ui.url.get(), 'https://company.ui.com');
+ await user.type(ui.secret.get(), '123');
+ await user.click(ui.synchronizeGroups.get());
+ await user.click(ui.saveConfigButton.get());
+
+ expect(await ui.editConfigButton.find()).toBeInTheDocument();
+ expect(ui.noGitlabConfiguration.query()).not.toBeInTheDocument();
+ expect(glContainer.get()).toHaveTextContent('https://company.ui.com');
+
+ expect(ui.disableConfigButton.get()).toBeInTheDocument();
+ await user.click(ui.disableConfigButton.get());
+ expect(ui.enableConfigButton.get()).toBeInTheDocument();
+ expect(ui.disableConfigButton.query()).not.toBeInTheDocument();
+});
+
+it('should edit/delete configuration', async () => {
+ const user = userEvent.setup();
+ renderAuthentication();
+
+ expect(await ui.editConfigButton.find()).toBeInTheDocument();
+ expect(glContainer.get()).toHaveTextContent('URL');
+ expect(ui.disableConfigButton.get()).toBeInTheDocument();
+ expect(ui.deleteConfigButton.get()).toBeInTheDocument();
+ expect(ui.deleteConfigButton.get()).toBeDisabled();
+
+ await user.click(ui.editConfigButton.get());
+ expect(await ui.editDialog.find()).toBeInTheDocument();
+ expect(ui.url.get()).toHaveValue('URL');
+ expect(ui.applicationId.get()).toBeInTheDocument();
+ expect(ui.secret.query()).not.toBeInTheDocument();
+ expect(ui.synchronizeGroups.get()).toBeChecked();
+ await user.clear(ui.url.get());
+ await user.type(ui.url.get(), 'https://company.ui.com');
+ await user.click(ui.saveConfigButton.get());
+
+ expect(glContainer.get()).not.toHaveTextContent('URL');
+ expect(glContainer.get()).toHaveTextContent('https://company.ui.com');
+
+ expect(ui.disableConfigButton.get()).toBeInTheDocument();
+ await user.click(ui.disableConfigButton.get());
+ expect(await ui.enableConfigButton.find()).toBeInTheDocument();
+ expect(ui.deleteConfigButton.get()).toBeEnabled();
+ await user.click(ui.deleteConfigButton.get());
+ expect(await ui.noGitlabConfiguration.find()).toBeInTheDocument();
+ expect(ui.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]);
+
+ expect(await ui.editConfigButton.find()).toBeInTheDocument();
+ expect(ui.jitProvisioningRadioButton.get()).toBeChecked();
+
+ user.click(ui.autoProvisioningRadioButton.get());
+ expect(await ui.autoProvisioningRadioButton.find()).toBeEnabled();
+ expect(ui.saveProvisioning.get()).toBeDisabled();
+
+ await user.type(ui.autoProvisioningToken.get(), 'JRR Tolkien');
+ expect(await ui.saveProvisioning.find()).toBeDisabled();
+
+ await user.type(ui.autoProvisioningGroupsInput.get(), 'NWA');
+ user.click(ui.autoProvisioningRadioButton.get());
+ expect(await ui.saveProvisioning.find()).toBeEnabled();
+
+ await user.click(ui.removeProvisioniongGroup.get());
+ expect(await ui.saveProvisioning.find()).toBeDisabled();
+ await user.type(ui.autoProvisioningGroupsInput.get(), 'Wu-Tang Clan');
+ expect(await ui.saveProvisioning.find()).toBeEnabled();
+
+ await user.clear(ui.autoProvisioningToken.get());
+ expect(await ui.saveProvisioning.find()).toBeDisabled();
+ await user.type(ui.autoProvisioningToken.get(), 'tiktoken');
+ expect(await ui.saveProvisioning.find()).toBeEnabled();
+
+ await user.click(ui.saveProvisioning.get());
+ expect(ui.confirmAutoProvisioningDialog.get()).toBeInTheDocument();
+ await user.click(ui.confirmProvisioningChange.get());
+ expect(ui.confirmAutoProvisioningDialog.query()).not.toBeInTheDocument();
+
+ expect(ui.autoProvisioningRadioButton.get()).toBeChecked();
+ expect(await ui.saveProvisioning.find()).toBeDisabled();
+});
+
+it('should change from auto provisioning to JIT with proper validation', async () => {
+ handler.setGitlabConfigurations([
+ mockGitlabConfiguration({
+ allowUsersToSignUp: false,
+ enabled: true,
+ synchronizationType: ProvisioningType.auto,
+ provisioningGroups: ['D12'],
+ }),
+ ]);
+ const user = userEvent.setup();
+ renderAuthentication([Feature.GitlabProvisioning]);
+
+ expect(await ui.editConfigButton.find()).toBeInTheDocument();
+
+ expect(ui.jitProvisioningRadioButton.get()).not.toBeChecked();
+ expect(ui.autoProvisioningRadioButton.get()).toBeChecked();
+ expect(ui.autoProvisioningGroupsInput.get()).toHaveValue('D12');
+
+ expect(ui.autoProvisioningToken.query()).not.toBeInTheDocument();
+ expect(ui.autoProvisioningUpdateTokenButton.get()).toBeInTheDocument();
+
+ await user.click(ui.jitProvisioningRadioButton.get());
+ expect(await ui.jitProvisioningRadioButton.find()).toBeChecked();
+
+ expect(await ui.saveProvisioning.find()).toBeEnabled();
+
+ expect(ui.jitAllowUsersToSignUpToggle.get()).toBeInTheDocument();
+
+ await user.click(ui.saveProvisioning.get());
+ expect(ui.confirmJitProvisioningDialog.get()).toBeInTheDocument();
+ await user.click(ui.confirmProvisioningChange.get());
+ expect(ui.confirmJitProvisioningDialog.query()).not.toBeInTheDocument();
+
+ expect(ui.jitProvisioningRadioButton.get()).toBeChecked();
+ expect(await ui.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,
+ synchronizationType: ProvisioningType.jit,
+ }),
+ ]);
+ const user = userEvent.setup();
+ renderAuthentication([Feature.GitlabProvisioning]);
+
+ expect(await ui.editConfigButton.find()).toBeInTheDocument();
+
+ expect(ui.jitProvisioningRadioButton.get()).toBeChecked();
+ expect(ui.autoProvisioningRadioButton.get()).not.toBeChecked();
+
+ expect(ui.jitAllowUsersToSignUpToggle.get()).not.toBeChecked();
+
+ expect(ui.saveProvisioning.get()).toBeDisabled();
+ await user.click(ui.jitAllowUsersToSignUpToggle.get());
+ expect(ui.saveProvisioning.get()).toBeEnabled();
+ await user.click(ui.jitAllowUsersToSignUpToggle.get());
+ expect(ui.saveProvisioning.get()).toBeDisabled();
+ await user.click(ui.jitAllowUsersToSignUpToggle.get());
+
+ await user.click(ui.saveProvisioning.get());
+
+ expect(ui.jitProvisioningRadioButton.get()).toBeChecked();
+ expect(ui.jitAllowUsersToSignUpToggle.get()).toBeChecked();
+ expect(await ui.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,
+ synchronizationType: ProvisioningType.auto,
+ provisioningGroups: ['Cypress Hill', 'Public Enemy'],
+ }),
+ ]);
+ const user = userEvent.setup();
+ renderAuthentication([Feature.GitlabProvisioning]);
+
+ expect(await ui.autoProvisioningRadioButton.find()).toBeChecked();
+ expect(ui.autoProvisioningUpdateTokenButton.get()).toBeInTheDocument();
+ expect(ui.autoProvisioningGroupsInput.get()).toHaveValue('Cypress Hill');
+
+ expect(ui.saveProvisioning.get()).toBeDisabled();
+
+ // Changing the Provisioning token should enable save
+ await user.click(ui.autoProvisioningUpdateTokenButton.get());
+ await user.type(ui.autoProvisioningGroupsInput.get(), 'Tok Token!');
+ expect(ui.saveProvisioning.get()).toBeEnabled();
+ await user.click(ui.cancelProvisioningChanges.get());
+ expect(ui.saveProvisioning.get()).toBeDisabled();
+
+ // Adding a group should enable save
+ await user.click(ui.autoProvisioningGroupsInput.get());
+ await user.tab();
+ await user.tab();
+ await user.tab();
+ await user.tab();
+ await user.keyboard('Run DMC');
+ expect(ui.saveProvisioning.get()).toBeEnabled();
+ await user.tab();
+ await user.keyboard('{Enter}');
+ expect(ui.saveProvisioning.get()).toBeDisabled();
+
+ // Removing a group should enable save
+ await user.click(ui.autoProvisioningGroupsInput.get());
+ await user.tab();
+ await user.keyboard('{Enter}');
+ expect(ui.saveProvisioning.get()).toBeEnabled();
+
+ // Removing all groups should disable save
+ await user.click(ui.autoProvisioningGroupsInput.get());
+ await user.tab();
+ await user.keyboard('{Enter}');
+ expect(ui.saveProvisioning.get()).toBeDisabled();
+});
+
+it('should be able to reset Auto Provisioning changes', async () => {
+ handler.setGitlabConfigurations([
+ mockGitlabConfiguration({
+ allowUsersToSignUp: false,
+ enabled: true,
+ synchronizationType: ProvisioningType.auto,
+ provisioningGroups: ['Cypress Hill', 'Public Enemy'],
+ }),
+ ]);
+ const user = userEvent.setup();
+ renderAuthentication([Feature.GitlabProvisioning]);
+
+ expect(await ui.autoProvisioningRadioButton.find()).toBeChecked();
+
+ // Cancel doesn't fully work yet as the AuthenticationFormField needs to be worked on
+ await user.click(ui.autoProvisioningGroupsInput.get());
+ await user.tab();
+ await user.tab();
+ await user.tab();
+ await user.tab();
+ await user.keyboard('A Tribe Called Quest');
+ await user.click(ui.autoProvisioningGroupsInput.get());
+ await user.tab();
+ await user.keyboard('{Enter}');
+ await user.click(ui.autoProvisioningUpdateTokenButton.get());
+ await user.type(ui.autoProvisioningGroupsInput.get(), 'ToToken!');
+ expect(ui.saveProvisioning.get()).toBeEnabled();
+ await user.click(ui.cancelProvisioningChanges.get());
+ // expect(ui.autoProvisioningUpdateTokenButton.get()).toBeInTheDocument();
+ expect(ui.autoProvisioningGroupsInput.get()).toHaveValue('Cypress Hill');
+});
+
+describe('Gitlab Provisioning', () => {
+ beforeEach(() => {
+ jest.useFakeTimers({
+ advanceTimers: true,
+ now: new Date('2022-02-04T12:00:59Z'),
+ });
+ handler.setGitlabConfigurations([
+ mockGitlabConfiguration({
+ enabled: true,
+ synchronizationType: ProvisioningType.auto,
+ provisioningGroups: ['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 ui.gitlabProvisioningSuccess.find()).toBeInTheDocument();
+ expect(ui.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 ui.gitlabProvisioningSuccess.find()).toBeInTheDocument();
+ expect(ui.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 ui.gitlabProvisioningAlert.find()).toBeInTheDocument();
+ expect(ui.autoProvisioningRadioButton.get()).toHaveTextContent("T'es mauvais Jacques");
+ expect(ui.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 ui.gitlabProvisioningAlert.find()).toBeInTheDocument();
+ expect(ui.autoProvisioningRadioButton.get()).toHaveTextContent("T'es mauvais Jacques");
+ expect(ui.gitlabProvisioningSuccess.query()).not.toBeInTheDocument();
+ expect(ui.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 ui.syncWarning.find()).toBeInTheDocument();
+ expect(ui.syncSummary.get()).toBeInTheDocument();
+ });
+});
+
+function renderAuthentication(features: Feature[] = []) {
+ renderComponent(
+ <AvailableFeaturesContext.Provider value={features}>
+ <Authentication definitions={definitions} />
+ </AvailableFeaturesContext.Provider>,
+ `?tab=${AlmKeys.GitLab}`,
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
+import React from 'react';
+import ScimProvisioningServiceMock from '../../../../../api/mocks/ScimProvisioningServiceMock';
+import SettingsServiceMock from '../../../../../api/mocks/SettingsServiceMock';
+import SystemServiceMock from '../../../../../api/mocks/SystemServiceMock';
+import { AvailableFeaturesContext } from '../../../../../app/components/available-features/AvailableFeaturesContext';
+import { definitions } from '../../../../../helpers/mocks/definitions-list';
+import { renderComponent } from '../../../../../helpers/testReactTestingUtils';
+import { byRole, byText } from '../../../../../helpers/testSelector';
+import { Feature } from '../../../../../types/features';
+import Authentication from '../Authentication';
+
+let handler: ScimProvisioningServiceMock;
+let system: SystemServiceMock;
+let settingsHandler: SettingsServiceMock;
+
+beforeEach(() => {
+ handler = new ScimProvisioningServiceMock();
+ system = new SystemServiceMock();
+ settingsHandler = new SettingsServiceMock();
+ [
+ {
+ key: 'sonar.auth.saml.signature.enabled',
+ value: 'false',
+ },
+ {
+ key: 'sonar.auth.saml.enabled',
+ value: 'false',
+ },
+ {
+ key: 'sonar.auth.saml.applicationId',
+ value: 'sonarqube',
+ },
+ {
+ key: 'sonar.auth.saml.providerName',
+ value: 'SAML',
+ },
+ ].forEach((setting: any) => settingsHandler.set(setting.key, setting.value));
+});
+
+afterEach(() => {
+ handler.reset();
+ settingsHandler.reset();
+ system.reset();
+});
+
+const samlContainer = byRole('tabpanel', { name: 'SAML' });
+
+const ui = {
+ noSamlConfiguration: byText('settings.authentication.saml.form.not_configured'),
+ 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', {
+ name: 'property.sonar.auth.saml.certificate.secured.name',
+ }),
+ loginUrl: byRole('textbox', { name: 'property.sonar.auth.saml.loginUrl.name' }),
+ userLoginAttribute: byRole('textbox', { name: 'property.sonar.auth.saml.user.login.name' }),
+ userNameAttribute: byRole('textbox', { name: 'property.sonar.auth.saml.user.name.name' }),
+ saveConfigButton: byRole('button', { name: 'settings.almintegration.form.save' }),
+ confirmProvisioningButton: byRole('button', {
+ name: 'yes',
+ }),
+ 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',
+ }),
+ scimProvisioningButton: byRole('radio', {
+ name: 'settings.authentication.saml.form.provisioning_with_scim',
+ }),
+ fillForm: async (user: UserEvent) => {
+ await user.clear(ui.providerName.get());
+ await user.type(ui.providerName.get(), 'Awsome SAML config');
+ await user.type(ui.providerId.get(), 'okta-1234');
+ await user.type(ui.loginUrl.get(), 'http://test.org');
+ await user.type(ui.providerCertificate.get(), '-secret-');
+ await user.type(ui.userLoginAttribute.get(), 'login');
+ await user.type(ui.userNameAttribute.get(), 'name');
+ },
+ createConfiguration: async (user: UserEvent) => {
+ await user.click(await ui.createConfigButton.find());
+ await ui.fillForm(user);
+ await user.click(ui.saveConfigButton.get());
+ },
+};
+
+it('should render an empty SAML configuration', async () => {
+ renderAuthentication();
+ expect(await ui.noSamlConfiguration.find()).toBeInTheDocument();
+});
+
+it('should be able to create a configuration', async () => {
+ const user = userEvent.setup();
+ renderAuthentication();
+
+ await user.click(await ui.createConfigButton.find());
+
+ expect(ui.saveConfigButton.get()).toBeDisabled();
+ await ui.fillForm(user);
+ expect(ui.saveConfigButton.get()).toBeEnabled();
+
+ await user.click(ui.saveConfigButton.get());
+
+ expect(await ui.editConfigButton.find()).toBeInTheDocument();
+});
+
+it('should be able to enable/disable configuration', async () => {
+ const user = userEvent.setup();
+ renderAuthentication();
+
+ await ui.createConfiguration(user);
+ await user.click(await ui.enableConfigButton.find());
+
+ expect(await ui.disableConfigButton.find()).toBeInTheDocument();
+ await user.click(ui.disableConfigButton.get());
+ await waitFor(() => expect(ui.disableConfigButton.query()).not.toBeInTheDocument());
+
+ expect(await ui.enableConfigButton.find()).toBeInTheDocument();
+});
+
+it('should be able to choose provisioning', async () => {
+ const user = userEvent.setup();
+
+ renderAuthentication([Feature.Scim]);
+
+ await ui.createConfiguration(user);
+
+ expect(await ui.enableFirstMessage.find()).toBeInTheDocument();
+ await user.click(await ui.enableConfigButton.find());
+
+ expect(await ui.jitProvisioningButton.find()).toBeChecked();
+ expect(ui.saveScim.get()).toBeDisabled();
+
+ await user.click(ui.scimProvisioningButton.get());
+ expect(ui.saveScim.get()).toBeEnabled();
+ await user.click(ui.saveScim.get());
+ await user.click(ui.confirmProvisioningButton.get());
+
+ expect(await ui.scimProvisioningButton.find()).toBeChecked();
+ expect(await ui.saveScim.find()).toBeDisabled();
+});
+
+it('should not allow editions below Enterprise to select SCIM provisioning', async () => {
+ const user = userEvent.setup();
+
+ renderAuthentication();
+
+ await ui.createConfiguration(user);
+ await user.click(await ui.enableConfigButton.find());
+
+ expect(await ui.jitProvisioningButton.find()).toBeChecked();
+ expect(ui.scimProvisioningButton.get()).toHaveAttribute('aria-disabled', 'true');
+});
+
+function renderAuthentication(features: Feature[] = []) {
+ renderComponent(
+ <AvailableFeaturesContext.Provider value={features}>
+ <Authentication definitions={definitions} />
+ </AvailableFeaturesContext.Provider>,
+ );
+}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { screen, waitFor, within } from '@testing-library/react';
+import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
-import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
import React from 'react';
-import AuthenticationServiceMock from '../../../../../api/mocks/AuthenticationServiceMock';
-import ComputeEngineServiceMock from '../../../../../api/mocks/ComputeEngineServiceMock';
+import GithubProvisioningServiceMock from '../../../../../api/mocks/GithubProvisioningServiceMock';
+import GitlabProvisioningServiceMock from '../../../../../api/mocks/GitlabProvisioningServiceMock';
+import ScimProvisioningServiceMock from '../../../../../api/mocks/ScimProvisioningServiceMock';
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, ProvisioningType } from '../../../../../types/provisioning';
-import { TaskStatuses, TaskTypes } from '../../../../../types/tasks';
import Authentication from '../Authentication';
-let handler: AuthenticationServiceMock;
+let scimHandler: ScimProvisioningServiceMock;
+let githubHandler: GithubProvisioningServiceMock;
+let gitlabHandler: GitlabProvisioningServiceMock;
let system: SystemServiceMock;
let settingsHandler: SettingsServiceMock;
-let computeEngineHandler: ComputeEngineServiceMock;
beforeEach(() => {
- handler = new AuthenticationServiceMock();
+ scimHandler = new ScimProvisioningServiceMock();
+ githubHandler = new GithubProvisioningServiceMock();
+ gitlabHandler = new GitlabProvisioningServiceMock();
system = new SystemServiceMock();
settingsHandler = new SettingsServiceMock();
- computeEngineHandler = new ComputeEngineServiceMock();
- [
- {
- key: 'sonar.auth.saml.signature.enabled',
- value: 'false',
- },
- {
- key: 'sonar.auth.saml.enabled',
- value: 'false',
- },
- {
- key: 'sonar.auth.saml.applicationId',
- value: 'sonarqube',
- },
- {
- key: 'sonar.auth.saml.providerName',
- value: 'SAML',
- },
- ].forEach((setting: any) => settingsHandler.set(setting.key, setting.value));
});
afterEach(() => {
- handler.reset();
+ scimHandler.reset();
+ githubHandler.reset();
+ gitlabHandler.reset();
settingsHandler.reset();
system.reset();
- 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'),
enabledToggle: byRole('switch'),
- testButton: byText('settings.authentication.saml.form.test'),
- textbox1: byRole('textbox', { name: 'test1' }),
- textbox2: byRole('textbox', { name: 'test2' }),
- saml: {
- noSamlConfiguration: byText('settings.authentication.saml.form.not_configured'),
- 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', {
- name: 'property.sonar.auth.saml.certificate.secured.name',
- }),
- loginUrl: byRole('textbox', { name: 'property.sonar.auth.saml.loginUrl.name' }),
- userLoginAttribute: byRole('textbox', { name: 'property.sonar.auth.saml.user.login.name' }),
- userNameAttribute: byRole('textbox', { name: 'property.sonar.auth.saml.user.name.name' }),
- saveConfigButton: byRole('button', { name: 'settings.almintegration.form.save' }),
- confirmProvisioningButton: byRole('button', {
- name: 'yes',
- }),
- 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',
- }),
- scimProvisioningButton: byRole('radio', {
- name: 'settings.authentication.saml.form.provisioning_with_scim',
- }),
- fillForm: async (user: UserEvent) => {
- const { saml } = ui;
-
- await user.clear(saml.providerName.get());
- await user.type(saml.providerName.get(), 'Awsome SAML config');
- await user.type(saml.providerId.get(), 'okta-1234');
- await user.type(saml.loginUrl.get(), 'http://test.org');
- await user.type(saml.providerCertificate.get(), '-secret-');
- await user.type(saml.userLoginAttribute.get(), 'login');
- await user.type(saml.userNameAttribute.get(), 'name');
- },
- createConfiguration: async (user: UserEvent) => {
- const { saml } = ui;
-
- await user.click(await saml.createConfigButton.find());
- await saml.fillForm(user);
- await user.click(saml.saveConfigButton.get());
- },
- },
- github: {
- tab: byRole('tab', { name: 'github GitHub' }),
- noGithubConfiguration: byText('settings.authentication.github.form.not_configured'),
- createConfigButton: 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',
- }),
- clientSecret: byRole('textbox', {
- name: 'property.sonar.auth.github.clientSecret.secured.name',
- }),
- githubApiUrl: byRole('textbox', { name: 'property.sonar.auth.github.apiUrl.name' }),
- githubWebUrl: byRole('textbox', { name: 'property.sonar.auth.github.webUrl.name' }),
- allowUserToSignUp: byRole('switch', {
- name: 'sonar.auth.github.allowUsersToSignUp',
- }),
- 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: 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', {
- name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
- }).byRole('row'),
- customRoleInput: byRole('textbox', {
- name: 'settings.authentication.github.configuration.roles_mapping.dialog.add_custom_role',
- }),
- customRoleAddBtn: byRole('dialog', {
- name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
- }).byRole('button', { name: 'add_verb' }),
- roleExistsError: byRole('dialog', {
- name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
- }).byText('settings.authentication.github.configuration.roles_mapping.role_exists'),
- emptyRoleError: byRole('dialog', {
- name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
- }).byText('settings.authentication.github.configuration.roles_mapping.empty_custom_role'),
- deleteCustomRoleCustom2: byRole('button', {
- name: 'settings.authentication.github.configuration.roles_mapping.dialog.delete_custom_role.custom2',
- }),
- getMappingRowByRole: (text: string) =>
- ui.github.mappingRow.getAll().find((row) => within(row).queryByText(text) !== null),
- mappingCheckbox: byRole('checkbox'),
- mappingDialogClose: byRole('dialog', {
- name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
- }).byRole('button', {
- name: 'close',
- }),
- deleteOrg: (org: string) =>
- byRole('button', {
- name: `settings.definition.delete_value.property.sonar.auth.github.organizations.name.${org}`,
- }),
- enableFirstMessage: ghContainer.byText('settings.authentication.github.enable_first'),
- jitProvisioningButton: ghContainer.byRole('radio', {
- name: 'settings.authentication.form.provisioning_at_login',
- }),
- githubProvisioningButton: ghContainer.byRole('radio', {
- name: 'settings.authentication.github.form.provisioning_with_github',
- }),
- 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: ghContainer.byRole('status', {
- name: /github.configuration.validation.valid/,
- }),
- configurationValidityError: ghContainer.byRole('status', {
- name: /github.configuration.validation.invalid/,
- }),
- syncWarning: ghContainer.byText(/Warning/),
- syncSummary: ghContainer.byText(/Test summary/),
- configurationValidityWarning: ghContainer.byRole('status', {
- name: /github.configuration.validation.valid.short/,
- }),
- checkConfigButton: ghContainer.byRole('button', {
- name: 'settings.authentication.github.configuration.validation.test',
- }),
- viewConfigValidityDetailsButton: ghContainer.byRole('button', {
- name: 'settings.authentication.github.configuration.validation.details',
- }),
- configDetailsDialog: byRole('dialog', {
- name: 'settings.authentication.github.configuration.validation.details.title',
- }),
- continueAutoButton: byRole('button', {
- name: 'settings.authentication.github.confirm_auto_provisioning.continue',
- }),
- switchJitButton: byRole('button', {
- name: 'settings.authentication.github.confirm_auto_provisioning.switch_jit',
- }),
- consentDialog: byRole('dialog', {
- name: 'settings.authentication.github.confirm_auto_provisioning.header',
- }),
- getConfigDetailsTitle: () => within(ui.github.configDetailsDialog.get()).getByRole('heading'),
- getOrgs: () => within(ui.github.configDetailsDialog.get()).getAllByRole('listitem'),
- fillForm: async (user: UserEvent) => {
- const { github } = ui;
-
- await user.type(await github.clientId.find(), 'Awsome GITHUB config');
- await user.type(github.clientSecret.get(), 'Client shut');
- await user.type(github.appId.get(), 'App id');
- await user.type(github.privateKey.get(), 'Private Key');
- await user.type(github.githubApiUrl.get(), 'API Url');
- await user.type(github.githubWebUrl.get(), 'WEb Url');
- await user.type(github.organizations.get(), 'organization1');
- },
- createConfiguration: async (user: UserEvent) => {
- const { github } = ui;
-
- await user.click(await github.createConfigButton.find());
- await github.fillForm(user);
-
- await user.click(github.saveConfigButton.get());
- },
- enableConfiguration: async (user: UserEvent) => {
- const { github } = ui;
- await user.click(await github.tab.find());
- await github.createConfiguration(user);
- await user.click(await github.enableConfigButton.find());
- },
- enableProvisioning: async (user: UserEvent) => {
- const { github } = ui;
- await user.click(await github.tab.find());
-
- await github.createConfiguration(user);
-
- await user.click(await github.enableConfigButton.find());
- await user.click(await github.githubProvisioningButton.find());
- await user.click(github.saveGithubProvisioning.get());
- await user.click(github.confirmProvisioningButton.get());
- },
- },
- gitlab: {
- tab: byRole('tab', { name: 'gitlab GitLab' }),
- noGitlabConfiguration: glContainer.byText('settings.authentication.gitlab.form.not_configured'),
- createConfigButton: glContainer.byRole('button', {
- name: 'settings.authentication.form.create',
- }),
- editConfigButton: glContainer.byRole('button', {
- name: 'settings.authentication.form.edit',
- }),
- deleteConfigButton: glContainer.byRole('button', {
- name: 'settings.authentication.form.delete',
- }),
- enableConfigButton: glContainer.byRole('button', {
- name: 'settings.authentication.form.enable',
- }),
- disableConfigButton: glContainer.byRole('button', {
- name: 'settings.authentication.form.disable',
- }),
- createDialog: byRole('dialog', {
- name: 'settings.authentication.gitlab.form.create',
- }),
- editDialog: byRole('dialog', {
- name: 'settings.authentication.gitlab.form.edit',
- }),
- applicationId: byRole('textbox', {
- name: 'property.applicationId.name',
- }),
- url: byRole('textbox', { name: 'property.url.name' }),
- clientSecret: byRole('textbox', {
- name: 'property.clientSecret.name',
- }),
- synchronizeUserGroups: byRole('switch', {
- name: 'synchronizeUserGroups',
- }),
- saveConfigButton: byRole('button', { name: 'settings.almintegration.form.save' }),
- jitProvisioningRadioButton: glContainer.byRole('radio', {
- name: 'settings.authentication.gitlab.provisioning_at_login',
- }),
- autoProvisioningRadioButton: glContainer.byRole('radio', {
- name: 'settings.authentication.gitlab.form.provisioning_with_gitlab',
- }),
- jitAllowUsersToSignUpToggle: byRole('switch', { name: 'sonar.auth.gitlab.allowUsersToSignUp' }),
- autoProvisioningToken: byRole('textbox', {
- name: 'property.provisioning.gitlab.token.secured.name',
- }),
- autoProvisioningUpdateTokenButton: byRole('button', {
- name: 'settings.almintegration.form.secret.update_field',
- }),
- autoProvisioningGroupsInput: byRole('textbox', {
- name: 'property.provisioning.gitlab.groups.name',
- }),
- removeProvisioniongGroup: byRole('button', {
- name: /settings.definition.delete_value.property.provisioning.gitlab.groups.name./,
- }),
- saveProvisioning: glContainer.byRole('button', { name: 'save' }),
- cancelProvisioningChanges: glContainer.byRole('button', { name: 'cancel' }),
- confirmAutoProvisioningDialog: byRole('dialog', {
- name: 'settings.authentication.gitlab.confirm.Auto',
- }),
- confirmJitProvisioningDialog: byRole('dialog', {
- name: 'settings.authentication.gitlab.confirm.JIT',
- }),
- confirmProvisioningChange: byRole('button', {
- name: 'settings.authentication.gitlab.provisioning_change.confirm_changes',
- }),
- syncSummary: glContainer.byText(/Test summary/),
- syncWarning: glContainer.byText(/Warning/),
- gitlabProvisioningPending: glContainer.byText(/synchronization_pending/),
- gitlabProvisioningInProgress: glContainer.byText(/synchronization_in_progress/),
- gitlabProvisioningSuccess: glContainer.byText(/synchronization_successful/),
- gitlabProvisioningAlert: glContainer.byText(/synchronization_failed/),
- },
};
it('should render tabs and allow navigation', async () => {
expect(ui.customMessageInformation.get()).toBeInTheDocument();
});
-describe('SAML tab', () => {
- const { saml } = ui;
-
- it('should render an empty SAML configuration', async () => {
- renderAuthentication();
- expect(await saml.noSamlConfiguration.find()).toBeInTheDocument();
- });
-
- it('should be able to create a configuration', async () => {
- const user = userEvent.setup();
- renderAuthentication();
-
- await user.click(await saml.createConfigButton.find());
-
- expect(saml.saveConfigButton.get()).toBeDisabled();
- await saml.fillForm(user);
- expect(saml.saveConfigButton.get()).toBeEnabled();
-
- await user.click(saml.saveConfigButton.get());
-
- expect(await saml.editConfigButton.find()).toBeInTheDocument();
- });
-
- it('should be able to enable/disable configuration', async () => {
- const { saml } = ui;
- const user = userEvent.setup();
- renderAuthentication();
-
- await saml.createConfiguration(user);
- await user.click(await saml.enableConfigButton.find());
-
- expect(await saml.disableConfigButton.find()).toBeInTheDocument();
- await user.click(saml.disableConfigButton.get());
- await waitFor(() => expect(saml.disableConfigButton.query()).not.toBeInTheDocument());
-
- expect(await saml.enableConfigButton.find()).toBeInTheDocument();
- });
-
- it('should be able to choose provisioning', async () => {
- const { saml } = ui;
- const user = userEvent.setup();
-
- renderAuthentication([Feature.Scim]);
-
- await saml.createConfiguration(user);
-
- expect(await saml.enableFirstMessage.find()).toBeInTheDocument();
- await user.click(await saml.enableConfigButton.find());
-
- expect(await saml.jitProvisioningButton.find()).toBeChecked();
- expect(saml.saveScim.get()).toBeDisabled();
-
- await user.click(saml.scimProvisioningButton.get());
- expect(saml.saveScim.get()).toBeEnabled();
- await user.click(saml.saveScim.get());
- await user.click(saml.confirmProvisioningButton.get());
-
- expect(await saml.scimProvisioningButton.find()).toBeChecked();
- expect(await saml.saveScim.find()).toBeDisabled();
- });
-
- it('should not allow editions below Enterprise to select SCIM provisioning', async () => {
- const { saml } = ui;
- const user = userEvent.setup();
-
- renderAuthentication();
-
- await saml.createConfiguration(user);
- await user.click(await saml.enableConfigButton.find());
-
- expect(await saml.jitProvisioningButton.find()).toBeChecked();
- expect(saml.scimProvisioningButton.get()).toHaveAttribute('aria-disabled', 'true');
- });
-});
-
-describe('Github tab', () => {
- const { github } = ui;
-
- it('should render an empty Github configuration', async () => {
- renderAuthentication();
- const user = userEvent.setup();
- await user.click(await github.tab.find());
- expect(await github.noGithubConfiguration.find()).toBeInTheDocument();
- });
-
- it('should be able to create a configuration', async () => {
- const user = userEvent.setup();
- renderAuthentication();
-
- await user.click(await github.tab.find());
- await user.click(await github.createConfigButton.find());
-
- expect(github.saveConfigButton.get()).toBeDisabled();
-
- await github.fillForm(user);
- expect(github.saveConfigButton.get()).toBeEnabled();
-
- await user.click(github.saveConfigButton.get());
-
- expect(await github.editConfigButton.find()).toBeInTheDocument();
- });
-
- it('should be able to edit configuration', async () => {
- const { github } = ui;
- const user = userEvent.setup();
- renderAuthentication();
- await user.click(await github.tab.find());
-
- await github.createConfiguration(user);
-
- await user.click(github.editConfigButton.get());
- await user.click(github.deleteOrg('organization1').get());
-
- await user.click(github.saveConfigButton.get());
-
- await user.click(await github.editConfigButton.find());
-
- expect(github.organizations.get()).toHaveValue('');
- });
-
- it('should be able to enable/disable configuration', async () => {
- const { github } = ui;
- const user = userEvent.setup();
- renderAuthentication();
- await user.click(await github.tab.find());
-
- await github.createConfiguration(user);
-
- await user.click(await github.enableConfigButton.find());
-
- expect(await github.disableConfigButton.find()).toBeInTheDocument();
- await user.click(github.disableConfigButton.get());
- await waitFor(() => expect(github.disableConfigButton.query()).not.toBeInTheDocument());
-
- expect(await github.enableConfigButton.find()).toBeInTheDocument();
- });
-
- it('should not allow edtion below Enterprise to select Github provisioning', async () => {
- const { github } = ui;
- const user = userEvent.setup();
-
- renderAuthentication();
- await user.click(await github.tab.find());
-
- await github.createConfiguration(user);
- await user.click(await github.enableConfigButton.find());
-
- expect(await github.jitProvisioningButton.find()).toBeChecked();
- expect(github.githubProvisioningButton.get()).toHaveAttribute('aria-disabled', 'true');
- });
-
- it('should be able to choose provisioning', async () => {
- const { github } = ui;
- const user = userEvent.setup();
-
- renderAuthentication([Feature.GithubProvisioning]);
- await user.click(await github.tab.find());
-
- await github.createConfiguration(user);
-
- expect(await github.enableFirstMessage.find()).toBeInTheDocument();
- await user.click(await github.enableConfigButton.find());
-
- expect(await github.jitProvisioningButton.find()).toBeChecked();
-
- expect(github.saveGithubProvisioning.get()).toBeDisabled();
- await user.click(github.allowUserToSignUp.get());
-
- expect(github.saveGithubProvisioning.get()).toBeEnabled();
- await user.click(github.saveGithubProvisioning.get());
-
- await waitFor(() => expect(github.saveGithubProvisioning.query()).toBeDisabled());
-
- await user.click(github.githubProvisioningButton.get());
-
- expect(github.saveGithubProvisioning.get()).toBeEnabled();
- await user.click(github.saveGithubProvisioning.get());
- await user.click(github.confirmProvisioningButton.get());
-
- expect(await github.githubProvisioningButton.find()).toBeChecked();
- expect(github.disableConfigButton.get()).toBeDisabled();
- expect(github.saveGithubProvisioning.get()).toBeDisabled();
- });
-
- describe('Github Provisioning', () => {
- let user: UserEvent;
-
- beforeEach(() => {
- jest.useFakeTimers({
- advanceTimers: true,
- now: new Date('2022-02-04T12:00:59Z'),
- });
- user = userEvent.setup();
- });
-
- afterEach(() => {
- jest.runOnlyPendingTimers();
- jest.useRealTimers();
- });
-
- it('should display a success status when the synchronisation is a success', async () => {
- handler.addProvisioningTask({
- status: TaskStatuses.Success,
- executedAt: '2022-02-03T11:45:35+0200',
- });
-
- renderAuthentication([Feature.GithubProvisioning]);
- await github.enableProvisioning(user);
- expect(github.githubProvisioningSuccess.get()).toBeInTheDocument();
- expect(github.syncSummary.get()).toBeInTheDocument();
- });
-
- it('should display a success status even when another task is pending', async () => {
- handler.addProvisioningTask({
- status: TaskStatuses.Pending,
- executedAt: '2022-02-03T11:55:35+0200',
- });
- handler.addProvisioningTask({
- status: TaskStatuses.Success,
- executedAt: '2022-02-03T11:45:35+0200',
- });
- renderAuthentication([Feature.GithubProvisioning]);
- await github.enableProvisioning(user);
- expect(github.githubProvisioningSuccess.get()).toBeInTheDocument();
- expect(github.githubProvisioningPending.get()).toBeInTheDocument();
- });
-
- it('should display an error alert when the synchronisation failed', async () => {
- handler.addProvisioningTask({
- status: TaskStatuses.Failed,
- executedAt: '2022-02-03T11:45:35+0200',
- errorMessage: "T'es mauvais Jacques",
- });
- renderAuthentication([Feature.GithubProvisioning]);
- await github.enableProvisioning(user);
- expect(github.githubProvisioningAlert.get()).toBeInTheDocument();
- expect(github.githubProvisioningButton.get()).toHaveTextContent("T'es mauvais Jacques");
- expect(github.githubProvisioningSuccess.query()).not.toBeInTheDocument();
- });
-
- it('should display an error alert even when another task is in progress', async () => {
- handler.addProvisioningTask({
- status: TaskStatuses.InProgress,
- executedAt: '2022-02-03T11:55:35+0200',
- });
- handler.addProvisioningTask({
- status: TaskStatuses.Failed,
- executedAt: '2022-02-03T11:45:35+0200',
- errorMessage: "T'es mauvais Jacques",
- });
- renderAuthentication([Feature.GithubProvisioning]);
- await github.enableProvisioning(user);
- expect(github.githubProvisioningAlert.get()).toBeInTheDocument();
- expect(github.githubProvisioningButton.get()).toHaveTextContent("T'es mauvais Jacques");
- expect(github.githubProvisioningSuccess.query()).not.toBeInTheDocument();
- expect(github.githubProvisioningInProgress.get()).toBeInTheDocument();
- });
-
- it('should display that config is valid for both provisioning with 1 org', async () => {
- renderAuthentication([Feature.GithubProvisioning]);
- await github.enableConfiguration(user);
-
- await appLoaded();
-
- await waitFor(() => expect(github.configurationValiditySuccess.query()).toBeInTheDocument());
- });
-
- it('should display that config is valid for both provisioning with multiple orgs', async () => {
- handler.setConfigurationValidity({
- installations: [
- {
- organization: 'org1',
- autoProvisioning: { status: GitHubProvisioningStatus.Success },
- jit: { status: GitHubProvisioningStatus.Success },
- },
- {
- organization: 'org2',
- autoProvisioning: { status: GitHubProvisioningStatus.Success },
- jit: { status: GitHubProvisioningStatus.Success },
- },
- ],
- });
- renderAuthentication([Feature.GithubProvisioning]);
- await github.enableConfiguration(user);
-
- await appLoaded();
-
- await waitFor(() => expect(github.configurationValiditySuccess.query()).toBeInTheDocument());
- expect(github.configurationValiditySuccess.get()).toHaveTextContent('2');
-
- await user.click(github.viewConfigValidityDetailsButton.get());
- expect(github.getConfigDetailsTitle()).toHaveTextContent(
- 'settings.authentication.github.configuration.validation.details.valid_label',
- );
- expect(github.getOrgs()[0]).toHaveTextContent(
- 'settings.authentication.github.configuration.validation.details.valid_labelorg1',
- );
- expect(github.getOrgs()[1]).toHaveTextContent(
- 'settings.authentication.github.configuration.validation.details.valid_labelorg2',
- );
- });
-
- it('should display that config is invalid for autoprovisioning if some apps are suspended but valid for jit', async () => {
- const errorMessage = 'Installation suspended';
- handler.setConfigurationValidity({
- installations: [
- {
- organization: 'org1',
- autoProvisioning: {
- status: GitHubProvisioningStatus.Failed,
- errorMessage,
- },
- jit: {
- status: GitHubProvisioningStatus.Failed,
- errorMessage,
- },
- },
- ],
- });
-
- renderAuthentication([Feature.GithubProvisioning]);
- await github.enableConfiguration(user);
-
- await appLoaded();
-
- await waitFor(() => expect(github.configurationValidityWarning.get()).toBeInTheDocument());
- expect(github.configurationValidityWarning.get()).toHaveTextContent(errorMessage);
-
- await user.click(github.viewConfigValidityDetailsButton.get());
- expect(github.getConfigDetailsTitle()).toHaveTextContent(
- 'settings.authentication.github.configuration.validation.details.valid_label',
- );
- expect(github.getOrgs()[0]).toHaveTextContent(
- 'settings.authentication.github.configuration.validation.details.invalid_labelorg1 - Installation suspended',
- );
-
- await user.click(github.configDetailsDialog.byRole('button', { name: 'close' }).get());
-
- await user.click(github.githubProvisioningButton.get());
- await waitFor(() => expect(github.configurationValidityError.get()).toBeInTheDocument());
- expect(github.configurationValidityError.get()).toHaveTextContent(errorMessage);
- });
-
- it('should display that config is valid but some organizations were not found', async () => {
- handler.setConfigurationValidity({
- installations: [
- {
- organization: 'org1',
- autoProvisioning: { status: GitHubProvisioningStatus.Success },
- jit: { status: GitHubProvisioningStatus.Success },
- },
- ],
- });
-
- renderAuthentication([Feature.GithubProvisioning]);
- await github.enableConfiguration(user);
-
- await appLoaded();
-
- await waitFor(() => expect(github.configurationValiditySuccess.get()).toBeInTheDocument());
- expect(github.configurationValiditySuccess.get()).toHaveTextContent('1');
-
- await user.click(github.viewConfigValidityDetailsButton.get());
- expect(github.getConfigDetailsTitle()).toHaveTextContent(
- 'settings.authentication.github.configuration.validation.details.valid_label',
- );
- expect(github.getOrgs()[0]).toHaveTextContent(
- 'settings.authentication.github.configuration.validation.details.valid_labelorg1',
- );
- expect(github.getOrgs()[1]).toHaveTextContent(
- 'settings.authentication.github.configuration.validation.details.org_not_found.organization1',
- );
- });
-
- it('should display that config is invalid', async () => {
- const errorMessage = 'Test error';
- handler.setConfigurationValidity({
- application: {
- jit: {
- status: GitHubProvisioningStatus.Failed,
- errorMessage,
- },
- autoProvisioning: {
- status: GitHubProvisioningStatus.Failed,
- errorMessage,
- },
- },
- });
- renderAuthentication([Feature.GithubProvisioning]);
- await github.enableConfiguration(user);
-
- await appLoaded();
-
- await waitFor(() => expect(github.configurationValidityError.query()).toBeInTheDocument());
- expect(github.configurationValidityError.get()).toHaveTextContent(errorMessage);
-
- await user.click(github.viewConfigValidityDetailsButton.get());
- expect(github.getConfigDetailsTitle()).toHaveTextContent(
- 'settings.authentication.github.configuration.validation.details.invalid_label',
- );
- expect(github.configDetailsDialog.get()).toHaveTextContent(errorMessage);
- });
-
- it('should display that config is valid for jit, but not for auto', async () => {
- const errorMessage = 'Test error';
- handler.setConfigurationValidity({
- application: {
- jit: {
- status: GitHubProvisioningStatus.Success,
- },
- autoProvisioning: {
- status: GitHubProvisioningStatus.Failed,
- errorMessage,
- },
- },
- });
- renderAuthentication([Feature.GithubProvisioning]);
- await github.enableConfiguration(user);
-
- await appLoaded();
-
- await waitFor(() => expect(github.configurationValiditySuccess.query()).toBeInTheDocument());
- expect(github.configurationValiditySuccess.get()).not.toHaveTextContent(errorMessage);
-
- await user.click(github.viewConfigValidityDetailsButton.get());
- expect(github.getConfigDetailsTitle()).toHaveTextContent(
- 'settings.authentication.github.configuration.validation.details.valid_label',
- );
- await user.click(github.configDetailsDialog.byRole('button', { name: 'close' }).get());
-
- await user.click(github.githubProvisioningButton.get());
-
- expect(github.configurationValidityError.get()).toBeInTheDocument();
- expect(github.configurationValidityError.get()).toHaveTextContent(errorMessage);
-
- await user.click(github.viewConfigValidityDetailsButton.get());
- expect(github.getConfigDetailsTitle()).toHaveTextContent(
- 'settings.authentication.github.configuration.validation.details.invalid_label',
- );
- });
-
- it('should display that config is invalid because of orgs', async () => {
- const errorMessage = 'Test error';
- handler.setConfigurationValidity({
- installations: [
- {
- organization: 'org1',
- autoProvisioning: { status: GitHubProvisioningStatus.Success },
- jit: { status: GitHubProvisioningStatus.Success },
- },
- {
- organization: 'org2',
- jit: { status: GitHubProvisioningStatus.Failed, errorMessage },
- autoProvisioning: { status: GitHubProvisioningStatus.Failed, errorMessage },
- },
- ],
- });
- renderAuthentication([Feature.GithubProvisioning]);
- await github.enableConfiguration(user);
-
- await appLoaded();
-
- await waitFor(() => expect(github.configurationValiditySuccess.query()).toBeInTheDocument());
-
- await user.click(github.viewConfigValidityDetailsButton.get());
-
- expect(github.getOrgs()[0]).toHaveTextContent(
- 'settings.authentication.github.configuration.validation.details.valid_labelorg1',
- );
- expect(github.getOrgs()[1]).toHaveTextContent(
- 'settings.authentication.github.configuration.validation.details.invalid_labelorg2 - Test error',
- );
-
- await user.click(github.configDetailsDialog.byRole('button', { name: 'close' }).get());
-
- await user.click(github.githubProvisioningButton.get());
-
- expect(github.configurationValidityError.get()).toBeInTheDocument();
- expect(github.configurationValidityError.get()).toHaveTextContent(
- `settings.authentication.github.configuration.validation.invalid_org.org2.${errorMessage}`,
- );
- await user.click(github.viewConfigValidityDetailsButton.get());
- expect(github.getOrgs()[1]).toHaveTextContent(
- `settings.authentication.github.configuration.validation.details.invalid_labelorg2 - ${errorMessage}`,
- );
- });
-
- it('should update provisioning validity after clicking Test Configuration', async () => {
- const errorMessage = 'Test error';
- handler.setConfigurationValidity({
- application: {
- jit: {
- status: GitHubProvisioningStatus.Failed,
- errorMessage,
- },
- autoProvisioning: {
- status: GitHubProvisioningStatus.Failed,
- errorMessage,
- },
- },
- });
- renderAuthentication([Feature.GithubProvisioning]);
- await github.enableConfiguration(user);
- handler.setConfigurationValidity({
- application: {
- jit: {
- status: GitHubProvisioningStatus.Success,
- },
- autoProvisioning: {
- status: GitHubProvisioningStatus.Success,
- },
- },
- });
-
- await appLoaded();
-
- expect(await github.configurationValidityError.find()).toBeInTheDocument();
-
- await user.click(github.checkConfigButton.get());
-
- expect(github.configurationValiditySuccess.get()).toBeInTheDocument();
- expect(github.configurationValidityError.query()).not.toBeInTheDocument();
- });
-
- it('should show warning', async () => {
- handler.addProvisioningTask({
- status: TaskStatuses.Success,
- warnings: ['Warning'],
- });
- renderAuthentication([Feature.GithubProvisioning]);
- await github.enableProvisioning(user);
-
- expect(await github.syncWarning.find()).toBeInTheDocument();
- expect(github.syncSummary.get()).toBeInTheDocument();
- });
-
- it('should display a modal if user was already using auto and continue using auto provisioning', async () => {
- const user = userEvent.setup();
- settingsHandler.presetGithubAutoProvisioning();
- handler.enableGithubProvisioning();
- settingsHandler.set('sonar.auth.github.userConsentForPermissionProvisioningRequired', '');
- renderAuthentication([Feature.GithubProvisioning]);
-
- await user.click(await github.tab.find());
-
- expect(await github.consentDialog.find()).toBeInTheDocument();
- await user.click(github.continueAutoButton.get());
-
- expect(await github.githubProvisioningButton.find()).toBeChecked();
- expect(github.consentDialog.query()).not.toBeInTheDocument();
- });
-
- it('should display a modal if user was already using auto and switch to JIT', async () => {
- const user = userEvent.setup();
- settingsHandler.presetGithubAutoProvisioning();
- handler.enableGithubProvisioning();
- settingsHandler.set('sonar.auth.github.userConsentForPermissionProvisioningRequired', '');
- renderAuthentication([Feature.GithubProvisioning]);
-
- await user.click(await github.tab.find());
-
- expect(await github.consentDialog.find()).toBeInTheDocument();
- await user.click(github.switchJitButton.get());
-
- expect(await github.jitProvisioningButton.find()).toBeChecked();
- expect(github.consentDialog.query()).not.toBeInTheDocument();
- });
-
- it('should sort mapping rows', async () => {
- const user = userEvent.setup();
- settingsHandler.presetGithubAutoProvisioning();
- handler.enableGithubProvisioning();
- renderAuthentication([Feature.GithubProvisioning]);
- await user.click(await github.tab.find());
-
- expect(await github.editMappingButton.find()).toBeInTheDocument();
- await user.click(github.editMappingButton.get());
-
- const rows = (await github.mappingRow.findAll()).filter(
- (row) => within(row).queryAllByRole('checkbox').length > 0,
- );
-
- expect(rows).toHaveLength(5);
-
- expect(rows[0]).toHaveTextContent('read');
- expect(rows[1]).toHaveTextContent('triage');
- expect(rows[2]).toHaveTextContent('write');
- expect(rows[3]).toHaveTextContent('maintain');
- expect(rows[4]).toHaveTextContent('admin');
- });
-
- it('should apply new mapping and new provisioning type at the same time', async () => {
- const user = userEvent.setup();
- renderAuthentication([Feature.GithubProvisioning]);
- await user.click(await github.tab.find());
-
- await github.createConfiguration(user);
- await user.click(await github.enableConfigButton.find());
-
- expect(await github.jitProvisioningButton.find()).toBeChecked();
- expect(github.editMappingButton.query()).not.toBeInTheDocument();
- await user.click(github.githubProvisioningButton.get());
- expect(await github.editMappingButton.find()).toBeInTheDocument();
- await user.click(github.editMappingButton.get());
-
- expect(await github.mappingRow.findAll()).toHaveLength(7);
-
- let readCheckboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('read'));
- let adminCheckboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('admin'));
-
- expect(readCheckboxes[0]).toBeChecked();
- expect(readCheckboxes[5]).not.toBeChecked();
- expect(adminCheckboxes[5]).toBeChecked();
-
- await user.click(readCheckboxes[0]);
- await user.click(readCheckboxes[5]);
- await user.click(adminCheckboxes[5]);
- await user.click(github.mappingDialogClose.get());
-
- await user.click(github.saveGithubProvisioning.get());
- await user.click(github.confirmProvisioningButton.get());
-
- // Clean local mapping state
- await user.click(github.jitProvisioningButton.get());
- await user.click(github.githubProvisioningButton.get());
-
- await user.click(github.editMappingButton.get());
- readCheckboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('read'));
- adminCheckboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('admin'));
-
- expect(readCheckboxes[0]).not.toBeChecked();
- expect(readCheckboxes[5]).toBeChecked();
- expect(adminCheckboxes[5]).not.toBeChecked();
- await user.click(github.mappingDialogClose.get());
- });
-
- it('should apply new mapping on auto-provisioning', async () => {
- const user = userEvent.setup();
- settingsHandler.presetGithubAutoProvisioning();
- handler.enableGithubProvisioning();
- renderAuthentication([Feature.GithubProvisioning]);
- await user.click(await github.tab.find());
-
- expect(await github.saveGithubProvisioning.find()).toBeDisabled();
- await user.click(github.editMappingButton.get());
-
- expect(await github.mappingRow.findAll()).toHaveLength(7);
-
- let readCheckboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('read'))[0];
-
- expect(readCheckboxes).toBeChecked();
-
- await user.click(readCheckboxes);
- await user.click(github.mappingDialogClose.get());
-
- expect(await github.saveGithubProvisioning.find()).toBeEnabled();
-
- await user.click(github.saveGithubProvisioning.get());
-
- // Clean local mapping state
- await user.click(github.jitProvisioningButton.get());
- await user.click(github.githubProvisioningButton.get());
-
- await user.click(github.editMappingButton.get());
- readCheckboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('read'))[0];
-
- expect(readCheckboxes).not.toBeChecked();
- await user.click(github.mappingDialogClose.get());
- });
-
- it('should add/remove/update custom roles', async () => {
- const user = userEvent.setup();
- settingsHandler.presetGithubAutoProvisioning();
- handler.enableGithubProvisioning();
- handler.addGitHubCustomRole('custom1', ['user', 'codeViewer', 'scan']);
- handler.addGitHubCustomRole('custom2', ['user', 'codeViewer', 'issueAdmin', 'scan']);
- renderAuthentication([Feature.GithubProvisioning]);
- await user.click(await github.tab.find());
-
- expect(await github.saveGithubProvisioning.find()).toBeDisabled();
- await user.click(github.editMappingButton.get());
-
- const rows = (await github.mappingRow.findAll()).filter(
- (row) => within(row).queryAllByRole('checkbox').length > 0,
- );
-
- expect(rows).toHaveLength(7);
-
- let custom1Checkboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('custom1'));
-
- expect(custom1Checkboxes[0]).toBeChecked();
- expect(custom1Checkboxes[1]).toBeChecked();
- expect(custom1Checkboxes[2]).not.toBeChecked();
- expect(custom1Checkboxes[3]).not.toBeChecked();
- expect(custom1Checkboxes[4]).not.toBeChecked();
- expect(custom1Checkboxes[5]).toBeChecked();
-
- await user.click(custom1Checkboxes[1]);
- await user.click(custom1Checkboxes[2]);
-
- await user.click(github.deleteCustomRoleCustom2.get());
-
- expect(github.customRoleInput.get()).toHaveValue('');
- await user.type(github.customRoleInput.get(), 'read');
- await user.click(github.customRoleAddBtn.get());
- expect(await github.roleExistsError.find()).toBeInTheDocument();
- expect(github.customRoleAddBtn.get()).toBeDisabled();
- await user.clear(github.customRoleInput.get());
- expect(github.roleExistsError.query()).not.toBeInTheDocument();
- await user.type(github.customRoleInput.get(), 'custom1');
- await user.click(github.customRoleAddBtn.get());
- expect(await github.roleExistsError.find()).toBeInTheDocument();
- expect(github.customRoleAddBtn.get()).toBeDisabled();
- await user.clear(github.customRoleInput.get());
- await user.type(github.customRoleInput.get(), 'custom3');
- expect(github.roleExistsError.query()).not.toBeInTheDocument();
- expect(github.customRoleAddBtn.get()).toBeEnabled();
- await user.click(github.customRoleAddBtn.get());
-
- let custom3Checkboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('custom3'));
- expect(custom3Checkboxes[0]).toBeChecked();
- expect(custom3Checkboxes[1]).not.toBeChecked();
- expect(custom3Checkboxes[2]).not.toBeChecked();
- expect(custom3Checkboxes[3]).not.toBeChecked();
- expect(custom3Checkboxes[4]).not.toBeChecked();
- expect(custom3Checkboxes[5]).not.toBeChecked();
- await user.click(custom3Checkboxes[0]);
- expect(await github.emptyRoleError.find()).toBeInTheDocument();
- expect(github.mappingDialogClose.get()).toBeDisabled();
- await user.click(custom3Checkboxes[1]);
- expect(github.emptyRoleError.query()).not.toBeInTheDocument();
- expect(github.mappingDialogClose.get()).toBeEnabled();
- await user.click(github.mappingDialogClose.get());
-
- expect(await github.saveGithubProvisioning.find()).toBeEnabled();
- await user.click(github.saveGithubProvisioning.get());
-
- // Clean local mapping state
- await user.click(github.jitProvisioningButton.get());
- await user.click(github.githubProvisioningButton.get());
-
- await user.click(github.editMappingButton.get());
-
- expect(
- (await github.mappingRow.findAll()).filter(
- (row) => within(row).queryAllByRole('checkbox').length > 0,
- ),
- ).toHaveLength(7);
- custom1Checkboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('custom1'));
- custom3Checkboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('custom3'));
- expect(github.getMappingRowByRole('custom2')).toBeUndefined();
- expect(custom1Checkboxes[0]).toBeChecked();
- expect(custom1Checkboxes[1]).not.toBeChecked();
- expect(custom1Checkboxes[2]).toBeChecked();
- expect(custom1Checkboxes[3]).not.toBeChecked();
- expect(custom1Checkboxes[4]).not.toBeChecked();
- expect(custom1Checkboxes[5]).toBeChecked();
- expect(custom3Checkboxes[0]).not.toBeChecked();
- expect(custom3Checkboxes[1]).toBeChecked();
- expect(custom3Checkboxes[2]).not.toBeChecked();
- expect(custom3Checkboxes[3]).not.toBeChecked();
- expect(custom3Checkboxes[4]).not.toBeChecked();
- expect(custom3Checkboxes[5]).not.toBeChecked();
- await user.click(github.mappingDialogClose.get());
- });
- });
-});
-
-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();
- });
-};
-
function renderAuthentication(features: Feature[] = []) {
renderComponent(
<AvailableFeaturesContext.Provider value={features}>
useGithubProvisioningEnabledQuery,
useGithubRolesMappingMutation,
useToggleGithubProvisioningMutation,
-} from '../../../../../queries/identity-provider';
+} from '../../../../../queries/identity-provider/github';
import { useSaveValueMutation, useSaveValuesMutation } from '../../../../../queries/settings';
import { Feature } from '../../../../../types/features';
import { GitHubMapping } from '../../../../../types/provisioning';
*/
import React from 'react';
import { AvailableFeaturesContext } from '../../../../../app/components/available-features/AvailableFeaturesContext';
-import { useScimStatusQuery } from '../../../../../queries/identity-provider';
+import { useScimStatusQuery } from '../../../../../queries/identity-provider/scim';
import { Feature } from '../../../../../types/features';
import { ExtendedSettingDefinition } from '../../../../../types/settings';
import useConfiguration from './useConfiguration';
import Spinner from '../../components/ui/Spinner';
import { now, toISO8601WithOffsetString } from '../../helpers/dates';
import { translate } from '../../helpers/l10n';
-import { useIdentityProviderQuery } from '../../queries/identity-provider';
+import { useIdentityProviderQuery } from '../../queries/identity-provider/common';
import { useUsersQueries } from '../../queries/users';
import { IdentityProvider, Provider } from '../../types/types';
import { RestUserDetailed } from '../../types/users';
import userEvent from '@testing-library/user-event';
import * as React from 'react';
import selectEvent from 'react-select-event';
-import AuthenticationServiceMock from '../../../api/mocks/AuthenticationServiceMock';
import ComponentsServiceMock from '../../../api/mocks/ComponentsServiceMock';
+import GithubProvisioningServiceMock from '../../../api/mocks/GithubProvisioningServiceMock';
import SettingsServiceMock from '../../../api/mocks/SettingsServiceMock';
import SystemServiceMock from '../../../api/mocks/SystemServiceMock';
import UserTokensMock from '../../../api/mocks/UserTokensMock';
const systemHandler = new SystemServiceMock();
const componentsHandler = new ComponentsServiceMock();
const settingsHandler = new SettingsServiceMock();
-const authenticationHandler = new AuthenticationServiceMock();
+const githubHandler = new GithubProvisioningServiceMock();
const ui = {
createUserButton: byRole('button', { name: 'users.create_user' }),
componentsHandler.reset();
settingsHandler.reset();
systemHandler.reset();
- authenticationHandler.reset();
+ githubHandler.reset();
});
describe('different filters combinations', () => {
describe('Github Provisioning', () => {
beforeEach(() => {
- authenticationHandler.handleActivateGithubProvisioning();
+ githubHandler.handleActivateGithubProvisioning();
});
it('should display a success status when the synchronisation is a success', async () => {
- authenticationHandler.addProvisioningTask({
+ githubHandler.addProvisioningTask({
status: TaskStatuses.Success,
executedAt: '2022-02-03T11:45:35+0200',
});
});
it('should display a success status even when another task is pending', async () => {
- authenticationHandler.addProvisioningTask({
+ githubHandler.addProvisioningTask({
status: TaskStatuses.Pending,
executedAt: '2022-02-03T11:55:35+0200',
});
- authenticationHandler.addProvisioningTask({
+ githubHandler.addProvisioningTask({
status: TaskStatuses.Success,
executedAt: '2022-02-03T11:45:35+0200',
});
});
it('should display an error alert when the synchronisation failed', async () => {
- authenticationHandler.addProvisioningTask({
+ githubHandler.addProvisioningTask({
status: TaskStatuses.Failed,
executedAt: '2022-02-03T11:45:35+0200',
errorMessage: 'Error Message',
});
it('should display an error alert even when another task is in progress', async () => {
- authenticationHandler.addProvisioningTask({
+ githubHandler.addProvisioningTask({
status: TaskStatuses.InProgress,
executedAt: '2022-02-03T11:55:35+0200',
});
- authenticationHandler.addProvisioningTask({
+ githubHandler.addProvisioningTask({
status: TaskStatuses.Failed,
executedAt: '2022-02-03T11:45:35+0200',
errorMessage: 'Error Message',
it('should display an warning alert', async () => {
const warningMessage = 'Very long warning about something that user is not interested in';
- authenticationHandler.addProvisioningTask({
+ githubHandler.addProvisioningTask({
status: TaskStatuses.Success,
warnings: [warningMessage],
});
import { translate } from '../../helpers/l10n';
import { isPermissionDefinitionGroup } from '../../helpers/permissions';
import { getBaseUrl } from '../../helpers/system';
-import { useIdentityProviderQuery } from '../../queries/identity-provider';
+import { useIdentityProviderQuery } from '../../queries/identity-provider/common';
import { Permissions } from '../../types/permissions';
import { PermissionDefinitions, PermissionGroup, Provider } from '../../types/types';
import GroupIcon from '../icons/GroupIcon';
import { translate } from '../../helpers/l10n';
import { isPermissionDefinitionGroup } from '../../helpers/permissions';
import { useIsGitHubProjectQuery } from '../../queries/devops-integration';
-import { useGithubProvisioningEnabledQuery } from '../../queries/identity-provider';
+import { useGithubProvisioningEnabledQuery } from '../../queries/identity-provider/github';
import { Dict, PermissionDefinitions, PermissionGroup, PermissionUser } from '../../types/types';
import GroupHolder from './GroupHolder';
import PermissionHeader from './PermissionHeader';
import { translate } from '../../helpers/l10n';
import { isPermissionDefinitionGroup } from '../../helpers/permissions';
import { getBaseUrl } from '../../helpers/system';
-import { useIdentityProviderQuery } from '../../queries/identity-provider';
+import { useIdentityProviderQuery } from '../../queries/identity-provider/common';
import { PermissionDefinitions, PermissionUser, Provider } from '../../types/types';
import PermissionCell from './PermissionCell';
import usePermissionChange from './usePermissionChange';
id: Math.random().toString(),
enabled: false,
url: 'URL',
+ applicationId: '123',
allowUsersToSignUp: false,
- synchronizeUserGroups: true,
- type: ProvisioningType.jit,
- groups: [],
+ synchronizeGroups: true,
+ synchronizationType: ProvisioningType.jit,
+ provisioningGroups: [],
...overrides,
};
}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-
-import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
-import { isEqual, keyBy, partition, pick, unionBy } from 'lodash';
-import { useContext } from 'react';
-import { getActivity } from '../api/ce';
-import {
- activateGithubProvisioning,
- activateScim,
- addGithubRolesMapping,
- checkConfigurationValidity,
- createGitLabConfiguration,
- deactivateGithubProvisioning,
- deactivateScim,
- deleteGitLabConfiguration,
- deleteGithubRolesMapping,
- fetchGitLabConfigurations,
- fetchGithubProvisioningStatus,
- fetchGithubRolesMapping,
- fetchIsScimEnabled,
- syncNowGithubProvisioning,
- updateGitLabConfiguration,
- updateGithubRolesMapping,
-} from '../api/provisioning';
-import { getSystemInfo } from '../api/system';
-import { AvailableFeaturesContext } from '../app/components/available-features/AvailableFeaturesContext';
-import { addGlobalSuccessMessage } from '../helpers/globalMessages';
-import { translate } from '../helpers/l10n';
-import { mapReactQueryResult } from '../helpers/react-query';
-import { Feature } from '../types/features';
-import { AlmSyncStatus, GitHubMapping } from '../types/provisioning';
-import { TaskStatuses, TaskTypes } from '../types/tasks';
-import { SysInfoCluster } from '../types/types';
-
-const MAPPING_STALE_TIME = 60_000;
-
-export function useIdentityProviderQuery() {
- return useQuery(['identity_provider'], async () => {
- const info = (await getSystemInfo()) as SysInfoCluster;
- return { provider: info.System['External Users and Groups Provisioning'] };
- });
-}
-
-export function useScimStatusQuery() {
- const hasScim = useContext(AvailableFeaturesContext).includes(Feature.Scim);
-
- return useQuery(['identity_provider', 'scim_status'], () => {
- if (!hasScim) {
- return false;
- }
- return fetchIsScimEnabled();
- });
-}
-
-export function useToggleScimMutation() {
- const client = useQueryClient();
- return useMutation({
- mutationFn: (activate: boolean) => (activate ? activateScim() : deactivateScim()),
- onSuccess: () => {
- client.invalidateQueries({ queryKey: ['identity_provider'] });
- },
- });
-}
-
-export function useToggleGithubProvisioningMutation() {
- const client = useQueryClient();
- return useMutation({
- mutationFn: (activate: boolean) =>
- activate ? activateGithubProvisioning() : deactivateGithubProvisioning(),
- onSuccess: () => {
- client.invalidateQueries({ queryKey: ['identity_provider'] });
- },
- });
-}
-
-export const useCheckGitHubConfigQuery = (githubEnabled: boolean) => {
- return useQuery(['identity_provider', 'github_check'], checkConfigurationValidity, {
- enabled: githubEnabled,
- });
-};
-
-interface GithubSyncStatusOptions {
- noRefetch?: boolean;
-}
-
-export function useGitHubSyncStatusQuery(options: GithubSyncStatusOptions = {}) {
- const hasGithubProvisioning = useContext(AvailableFeaturesContext).includes(
- Feature.GithubProvisioning,
- );
- return useQuery(['identity_provider', 'github_sync', 'status'], fetchGithubProvisioningStatus, {
- enabled: hasGithubProvisioning,
- refetchInterval: options.noRefetch ? undefined : 10_000,
- });
-}
-
-export function useGithubProvisioningEnabledQuery() {
- const res = useGitHubSyncStatusQuery({ noRefetch: true });
-
- return mapReactQueryResult(res, (data) => data.enabled);
-}
-
-export function useSyncWithGitHubNow() {
- const queryClient = useQueryClient();
- const { data } = useGitHubSyncStatusQuery();
- const mutation = useMutation(syncNowGithubProvisioning, {
- onSuccess: () => {
- queryClient.invalidateQueries(['identity_provider', 'github_sync']);
- },
- });
-
- return {
- synchronizeNow: mutation.mutate,
- canSyncNow: data?.enabled && !data.nextSync && !mutation.isLoading,
- };
-}
-
-// Order is reversed to put custom roles at the end (their index is -1)
-const defaultRoleOrder = ['admin', 'maintain', 'write', 'triage', 'read'];
-
-export function useGithubRolesMappingQuery() {
- return useQuery(['identity_provider', 'github_mapping'], fetchGithubRolesMapping, {
- staleTime: MAPPING_STALE_TIME,
- select: (data) =>
- [...data].sort((a, b) => {
- if (defaultRoleOrder.includes(a.id) || defaultRoleOrder.includes(b.id)) {
- return defaultRoleOrder.indexOf(b.id) - defaultRoleOrder.indexOf(a.id);
- }
- return a.githubRole.localeCompare(b.githubRole);
- }),
- });
-}
-
-export function useGithubRolesMappingMutation() {
- const client = useQueryClient();
- const queryKey = ['identity_provider', 'github_mapping'];
- return useMutation({
- mutationFn: async (mapping: GitHubMapping[]) => {
- const state = keyBy(client.getQueryData<GitHubMapping[]>(queryKey), (m) => m.id);
-
- const [maybeChangedRoles, newRoles] = partition(mapping, (m) => state[m.id]);
- const changedRoles = maybeChangedRoles.filter((item) => !isEqual(item, state[item.id]));
- const deletedRoles = Object.values(state).filter(
- (m) => !m.isBaseRole && !mapping.some((cm) => m.id === cm.id),
- );
-
- return {
- addedOrChanged: await Promise.all([
- ...changedRoles.map((data) =>
- updateGithubRolesMapping(data.id, pick(data, 'permissions')),
- ),
- ...newRoles.map((m) => addGithubRolesMapping(m)),
- ]),
- deleted: await Promise.all([
- deletedRoles.map((dm) => deleteGithubRolesMapping(dm.id)),
- ]).then(() => deletedRoles.map((dm) => dm.id)),
- };
- },
- onSuccess: ({ addedOrChanged, deleted }) => {
- const state = client.getQueryData<GitHubMapping[]>(queryKey);
- if (state) {
- const newData = unionBy(
- addedOrChanged,
- state.filter((s) => !deleted.find((id) => id === s.id)),
- (el) => el.id,
- );
- client.setQueryData(queryKey, newData);
- }
- addGlobalSuccessMessage(
- translate('settings.authentication.github.configuration.roles_mapping.save_success'),
- );
- },
- });
-}
-
-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,
- },
- );
-}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import { useQuery } from '@tanstack/react-query';
+import { getSystemInfo } from '../../api/system';
+import { SysInfoCluster } from '../../types/types';
+
+export function useIdentityProviderQuery() {
+ return useQuery(['identity_provider', 'users_and_groups_provider'], async () => {
+ const info = (await getSystemInfo()) as SysInfoCluster;
+ return { provider: info.System['External Users and Groups Provisioning'] };
+ });
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { isEqual, keyBy, partition, pick, unionBy } from 'lodash';
+import { useContext } from 'react';
+import {
+ activateGithubProvisioning,
+ addGithubRolesMapping,
+ checkConfigurationValidity,
+ deactivateGithubProvisioning,
+ deleteGithubRolesMapping,
+ fetchGithubProvisioningStatus,
+ fetchGithubRolesMapping,
+ syncNowGithubProvisioning,
+ updateGithubRolesMapping,
+} from '../../api/github-provisioning';
+import { AvailableFeaturesContext } from '../../app/components/available-features/AvailableFeaturesContext';
+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';
+
+const MAPPING_STALE_TIME = 60_000;
+
+export function useToggleGithubProvisioningMutation() {
+ const client = useQueryClient();
+ return useMutation({
+ mutationFn: (activate: boolean) =>
+ activate ? activateGithubProvisioning() : deactivateGithubProvisioning(),
+ onSuccess: () => {
+ client.invalidateQueries({ queryKey: ['identity_provider'] });
+ },
+ });
+}
+
+export const useCheckGitHubConfigQuery = (githubEnabled: boolean) => {
+ return useQuery(['identity_provider', 'github_check'], checkConfigurationValidity, {
+ enabled: githubEnabled,
+ });
+};
+
+interface GithubSyncStatusOptions {
+ noRefetch?: boolean;
+}
+
+export function useGitHubSyncStatusQuery(options: GithubSyncStatusOptions = {}) {
+ const hasGithubProvisioning = useContext(AvailableFeaturesContext).includes(
+ Feature.GithubProvisioning,
+ );
+ return useQuery(['identity_provider', 'github_sync', 'status'], fetchGithubProvisioningStatus, {
+ enabled: hasGithubProvisioning,
+ refetchInterval: options.noRefetch ? undefined : 10_000,
+ });
+}
+
+export function useGithubProvisioningEnabledQuery() {
+ const res = useGitHubSyncStatusQuery({ noRefetch: true });
+
+ return mapReactQueryResult(res, (data) => data.enabled);
+}
+
+export function useSyncWithGitHubNow() {
+ const queryClient = useQueryClient();
+ const { data } = useGitHubSyncStatusQuery();
+ const mutation = useMutation(syncNowGithubProvisioning, {
+ onSuccess: () => {
+ queryClient.invalidateQueries(['identity_provider', 'github_sync']);
+ },
+ });
+
+ return {
+ synchronizeNow: mutation.mutate,
+ canSyncNow: data?.enabled && !data.nextSync && !mutation.isLoading,
+ };
+}
+
+// Order is reversed to put custom roles at the end (their index is -1)
+const defaultRoleOrder = ['admin', 'maintain', 'write', 'triage', 'read'];
+
+export function useGithubRolesMappingQuery() {
+ return useQuery(['identity_provider', 'github_mapping'], fetchGithubRolesMapping, {
+ staleTime: MAPPING_STALE_TIME,
+ select: (data) =>
+ [...data].sort((a, b) => {
+ if (defaultRoleOrder.includes(a.id) || defaultRoleOrder.includes(b.id)) {
+ return defaultRoleOrder.indexOf(b.id) - defaultRoleOrder.indexOf(a.id);
+ }
+ return a.githubRole.localeCompare(b.githubRole);
+ }),
+ });
+}
+
+export function useGithubRolesMappingMutation() {
+ const client = useQueryClient();
+ const queryKey = ['identity_provider', 'github_mapping'];
+ return useMutation({
+ mutationFn: async (mapping: GitHubMapping[]) => {
+ const state = keyBy(client.getQueryData<GitHubMapping[]>(queryKey), (m) => m.id);
+
+ const [maybeChangedRoles, newRoles] = partition(mapping, (m) => state[m.id]);
+ const changedRoles = maybeChangedRoles.filter((item) => !isEqual(item, state[item.id]));
+ const deletedRoles = Object.values(state).filter(
+ (m) => !m.isBaseRole && !mapping.some((cm) => m.id === cm.id),
+ );
+
+ return {
+ addedOrChanged: await Promise.all([
+ ...changedRoles.map((data) =>
+ updateGithubRolesMapping(data.id, pick(data, 'permissions')),
+ ),
+ ...newRoles.map((m) => addGithubRolesMapping(m)),
+ ]),
+ deleted: await Promise.all([
+ deletedRoles.map((dm) => deleteGithubRolesMapping(dm.id)),
+ ]).then(() => deletedRoles.map((dm) => dm.id)),
+ };
+ },
+ onSuccess: ({ addedOrChanged, deleted }) => {
+ const state = client.getQueryData<GitHubMapping[]>(queryKey);
+ if (state) {
+ const newData = unionBy(
+ addedOrChanged,
+ state.filter((s) => !deleted.find((id) => id === s.id)),
+ (el) => el.id,
+ );
+ client.setQueryData(queryKey, newData);
+ }
+ addGlobalSuccessMessage(
+ translate('settings.authentication.github.configuration.roles_mapping.save_success'),
+ );
+ },
+ });
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { getActivity } from '../../api/ce';
+import {
+ createGitLabConfiguration,
+ deleteGitLabConfiguration,
+ fetchGitLabConfigurations,
+ updateGitLabConfiguration,
+} from '../../api/gitlab-provisioning';
+import { AlmSyncStatus } from '../../types/provisioning';
+import { TaskStatuses, TaskTypes } from '../../types/tasks';
+
+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'], {
+ gitlabConfigurations: [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', 'users_and_groups_provider'] });
+ client.setQueryData(['identity_provider', 'gitlab_config', 'list'], {
+ gitlabConfigurations: [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'], {
+ gitlabConfigurations: [],
+ 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,
+ },
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { useContext } from 'react';
+import { activateScim, deactivateScim, fetchIsScimEnabled } from '../../api/scim-provisioning';
+import { AvailableFeaturesContext } from '../../app/components/available-features/AvailableFeaturesContext';
+import { Feature } from '../../types/features';
+
+export function useScimStatusQuery() {
+ const hasScim = useContext(AvailableFeaturesContext).includes(Feature.Scim);
+
+ return useQuery(['identity_provider', 'scim_status'], () => {
+ if (!hasScim) {
+ return false;
+ }
+ return fetchIsScimEnabled();
+ });
+}
+
+export function useToggleScimMutation() {
+ const client = useQueryClient();
+ return useMutation({
+ mutationFn: (activate: boolean) => (activate ? activateScim() : deactivateScim()),
+ onSuccess: () => {
+ client.invalidateQueries({ queryKey: ['identity_provider'] });
+ },
+ });
+}
export interface GitLabConfigurationCreateBody {
applicationId: string;
url: string;
- clientSecret: string;
- synchronizeUserGroups: boolean;
+ secret: string;
+ synchronizeGroups: boolean;
}
export type GitLabConfigurationUpdateBody = {
applicationId?: string;
url?: string;
- clientSecret?: string;
- synchronizeUserGroups?: boolean;
+ secret?: string;
+ synchronizeGroups?: boolean;
enabled?: boolean;
- type?: ProvisioningType;
+ synchronizationType?: ProvisioningType;
provisioningToken?: string;
- groups?: string[];
+ provisioningGroups?: string[];
allowUsersToSignUp?: boolean;
};
export type GitlabConfiguration = {
id: string;
enabled: boolean;
- synchronizeUserGroups: boolean;
+ applicationId: string;
+ synchronizeGroups: boolean;
url: string;
- type: ProvisioningType;
- groups: string[];
+ synchronizationType: ProvisioningType;
+ provisioningGroups: string[];
allowUsersToSignUp: boolean;
};
export enum ProvisioningType {
jit = 'JIT',
- auto = 'Auto',
+ auto = 'AUTO_PROVISIONING',
}
settings.authentication.gitlab.form.applicationId.description=Application ID provided by GitLab when registering the application.
settings.authentication.gitlab.form.url.name=GitLab URL
settings.authentication.gitlab.form.url.description=URL to access GitLab.
-settings.authentication.gitlab.form.clientSecret.name=Secret
-settings.authentication.gitlab.form.clientSecret.description=Secret provided by GitLab when registering the application.
-settings.authentication.gitlab.form.synchronizeUserGroups.name=Synchronize user groups
-settings.authentication.gitlab.form.synchronizeUserGroups.description=For each GitLab group they belong to, the user will be associated to a group with the same name (if it exists) in SonarQube. If enabled, the GitLab Oauth2 application will need to provide the api scope.
+settings.authentication.gitlab.form.secret.name=Secret
+settings.authentication.gitlab.form.secret.description=Secret provided by GitLab when registering the application.
+settings.authentication.gitlab.form.synchronizeGroups.name=Synchronize user groups
+settings.authentication.gitlab.form.synchronizeGroups.description=For each GitLab group they belong to, the user will be associated to a group with the same name (if it exists) in SonarQube. If enabled, the GitLab Oauth2 application will need to provide the api scope.
+settings.authentication.gitlab.form.provisioningGroups.name=Groups
+settings.authentication.gitlab.form.provisioningGroups.description=Only members of these groups (and sub-groups) will be provisioned. Please enter the group slug as it appears in GitLab URL, for instance `my-gitlab-group`.
+settings.authentication.gitlab.form.allowUsersToSignUp.name=Allow users to sign up
+settings.authentication.gitlab.form.allowUsersToSignUp.description=Allow new users to authenticate. When set to 'false', only existing users will be able to authenticate to the server.
+settings.authentication.gitlab.form.provisioningToken.name=Provisioning token
+settings.authentication.gitlab.form.provisioningToken.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.
settings.authentication.gitlab.provisioning_at_login=Just-in-Time user provisioning (default)
settings.authentication.gitlab.provisioning_at_login.description=Users are synchronized only when users log in to SonarQube.
settings.authentication.gitlab.description.doc=For more details, see {documentation}.
-settings.authentication.gitlab.confirm.Auto=Switch to automatic provisioning
+settings.authentication.gitlab.confirm.AUTO_PROVISIONING=Switch to automatic provisioning
settings.authentication.gitlab.confirm.JIT=Switch to Just-in-Time provisioning
-settings.authentication.gitlab.confirm.Auto.description=Once you transition to automatic provisioning users on GitLab projects will be inherited from GitLab. You will no longer have the ability to edit them within SonarQube. Do you want to proceed with this change?
-settings.authentication.gitlab.confirm.JIT.description=Switching to Just-in-Time provisioning removes the automatic synchronization of users. Users are provisioned and updated only at user login. Are you sure?
+settings.authentication.gitlab.confirm.AUTO_PROVISIONING.description=Once you transition to automatic provisioning users and groups on GitLab projects will be inherited from GitLab. You will no longer have the ability to edit them within SonarQube. Do you want to proceed with this change?
+settings.authentication.gitlab.confirm.JIT.description=Switching to Just-in-Time provisioning removes the automatic synchronization of users and groups. Users are provisioned and updated only at user login. Are you sure?
settings.authentication.gitlab.provisioning_change.confirm_changes=Confirm Changes
-settings.authentication.gitlab.form.provisioning_with_gitlab=Automatic user provisioning
-settings.authentication.gitlab.form.provisioning_with_gitlab.description=Users are automatically provisioned from your GitLab organizations. Once activated, users can only be created and modified from your GitLab groups. Existing local users will be kept and can only be deactivated.
+settings.authentication.gitlab.form.provisioning_with_gitlab=Automatic user and group provisioning
+settings.authentication.gitlab.form.provisioning_with_gitlab.description=Users and groups are automatically provisioned from GitLab. Once activated, users and groups can only be created and modified from GitLab. Existing local users will be kept and can only be deactivated.
settings.authentication.gitlab.form.provisioning.disabled=Your current edition does not support provisioning with GitLab. See the {documentation} for more information.
settings.authentication.gitlab.configuration.unsaved_changes=You have unsaved changes.