aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorVik Vorona <viktor.vorona@sonarsource.com>2023-05-31 11:09:54 +0200
committersonartech <sonartech@sonarsource.com>2023-06-01 20:02:58 +0000
commit902bbef4744bfbc116f57bef25dec6f262895163 (patch)
tree72477fb5788fb1b88ec3c3818a55a21a411ca8cb
parentd0dcdf86c351884f4ee896b61b8ff85dbd311e93 (diff)
downloadsonarqube-902bbef4744bfbc116f57bef25dec6f262895163.tar.gz
sonarqube-902bbef4744bfbc116f57bef25dec6f262895163.zip
SONAR-19337 Display configuration validity status
-rw-r--r--server/sonar-web/src/main/js/api/mocks/AuthenticationServiceMock.ts39
-rw-r--r--server/sonar-web/src/main/js/api/provisioning.ts8
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubConfigurationValidity.tsx159
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx19
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx213
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useGithubConfiguration.ts2
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useSamlConfiguration.ts2
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/queries/identity-provider.ts (renamed from server/sonar-web/src/main/js/apps/settings/components/authentication/queries/IdentityProvider.ts)5
-rw-r--r--server/sonar-web/src/main/js/types/provisioning.ts26
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties12
11 files changed, 461 insertions, 26 deletions
diff --git a/server/sonar-web/src/main/js/api/mocks/AuthenticationServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/AuthenticationServiceMock.ts
index 6adf7f8010a..54510a6285a 100644
--- a/server/sonar-web/src/main/js/api/mocks/AuthenticationServiceMock.ts
+++ b/server/sonar-web/src/main/js/api/mocks/AuthenticationServiceMock.ts
@@ -17,11 +17,14 @@
* 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, GitHubProvisioningStatus } from '../../types/provisioning';
import { Task, TaskStatuses, TaskTypes } from '../../types/tasks';
import {
activateGithubProvisioning,
activateScim,
+ checkConfigurationValidity,
deactivateGithubProvisioning,
deactivateScim,
fetchGithubProvisioningStatus,
@@ -30,14 +33,35 @@ import {
jest.mock('../provisioning');
+const defaultConfigurationStatus: GitHubConfigurationStatus = {
+ application: {
+ jit: {
+ status: GitHubProvisioningStatus.Success,
+ },
+ autoProvisioning: {
+ status: GitHubProvisioningStatus.Success,
+ },
+ },
+ installations: [
+ {
+ organization: 'testOrg',
+ autoProvisioning: {
+ status: GitHubProvisioningStatus.Success,
+ },
+ },
+ ],
+};
+
export default class AuthenticationServiceMock {
scimStatus: boolean;
githubProvisioningStatus: boolean;
+ githubConfigurationStatus: GitHubConfigurationStatus;
tasks: Task[];
constructor() {
this.scimStatus = false;
this.githubProvisioningStatus = false;
+ this.githubConfigurationStatus = cloneDeep(defaultConfigurationStatus);
this.tasks = [];
jest.mocked(activateScim).mockImplementation(this.handleActivateScim);
jest.mocked(deactivateScim).mockImplementation(this.handleDeactivateScim);
@@ -51,6 +75,9 @@ export default class AuthenticationServiceMock {
jest
.mocked(fetchGithubProvisioningStatus)
.mockImplementation(this.handleFetchGithubProvisioningStatus);
+ jest
+ .mocked(checkConfigurationValidity)
+ .mockImplementation(this.handleCheckConfigurationValidity);
}
addProvisioningTask = (overrides: Partial<Omit<Task, 'type'>> = {}) => {
@@ -63,6 +90,13 @@ export default class AuthenticationServiceMock {
);
};
+ setConfigurationValidity = (overrides: Partial<GitHubConfigurationStatus> = {}) => {
+ this.githubConfigurationStatus = {
+ ...this.githubConfigurationStatus,
+ ...overrides,
+ };
+ };
+
handleActivateScim = () => {
this.scimStatus = true;
return Promise.resolve();
@@ -115,9 +149,14 @@ export default class AuthenticationServiceMock {
});
};
+ handleCheckConfigurationValidity = () => {
+ return Promise.resolve(this.githubConfigurationStatus);
+ };
+
reset = () => {
this.scimStatus = false;
this.githubProvisioningStatus = false;
+ this.githubConfigurationStatus = cloneDeep(defaultConfigurationStatus);
this.tasks = [];
};
}
diff --git a/server/sonar-web/src/main/js/api/provisioning.ts b/server/sonar-web/src/main/js/api/provisioning.ts
index fbadc9dee85..00c9232ff49 100644
--- a/server/sonar-web/src/main/js/api/provisioning.ts
+++ b/server/sonar-web/src/main/js/api/provisioning.ts
@@ -18,8 +18,8 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { throwGlobalError } from '../helpers/error';
-import { getJSON, post } from '../helpers/request';
-import { GithubStatus } from '../types/provisioning';
+import { getJSON, post, postJSON } from '../helpers/request';
+import { GitHubConfigurationStatus, GithubStatus } from '../types/provisioning';
export function fetchIsScimEnabled(): Promise<boolean> {
return getJSON('/api/scim_management/status')
@@ -47,6 +47,10 @@ 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);
}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubConfigurationValidity.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubConfigurationValidity.tsx
new file mode 100644
index 00000000000..b3e07c92cec
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubConfigurationValidity.tsx
@@ -0,0 +1,159 @@
+/*
+ * 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 React, { useEffect, useState } from 'react';
+import theme from '../../../../app/theme';
+import Modal from '../../../../components/controls/Modal';
+import { Button } from '../../../../components/controls/buttons';
+import CheckIcon from '../../../../components/icons/CheckIcon';
+import ClearIcon from '../../../../components/icons/ClearIcon';
+import { Alert, AlertVariant } from '../../../../components/ui/Alert';
+import { translate, translateWithParameters } from '../../../../helpers/l10n';
+import { GitHubProvisioningStatus } from '../../../../types/provisioning';
+import { useCheckGitHubConfigQuery } from './queries/identity-provider';
+
+const intlPrefix = 'settings.authentication.github.configuration.validation';
+
+function ValidityIcon({ valid }: { valid: boolean }) {
+ const color = valid ? theme.colors.success500 : theme.colors.error500;
+
+ return valid ? (
+ <CheckIcon fill={color} label={translate(`${intlPrefix}.details.valid_label`)} />
+ ) : (
+ <ClearIcon fill={color} label={translate(`${intlPrefix}.details.invalid_label`)} />
+ );
+}
+
+interface Props {
+ isAutoProvisioning: boolean;
+}
+
+function GitHubConfigurationValidity({ isAutoProvisioning }: Props) {
+ const [openDetails, setOpenDetails] = useState(false);
+ const [messages, setMessages] = useState<string[]>([]);
+ const [alertVariant, setAlertVariant] = useState<AlertVariant>('loading');
+ const { data, isFetching, refetch } = useCheckGitHubConfigQuery();
+ const modalHeader = translate(`${intlPrefix}.details.title`);
+
+ const applicationField = isAutoProvisioning ? 'autoProvisioning' : 'jit';
+
+ const isValidApp =
+ data?.application[applicationField].status === GitHubProvisioningStatus.Success;
+
+ useEffect(() => {
+ if (isFetching) {
+ setMessages([translate(`${intlPrefix}.loading`)]);
+ setAlertVariant('loading');
+ return;
+ }
+
+ const invalidOrgs =
+ isValidApp && isAutoProvisioning && data
+ ? data.installations.filter(
+ (org) => org.autoProvisioning.status === GitHubProvisioningStatus.Error
+ )
+ : [];
+
+ if (isValidApp && invalidOrgs.length === 0) {
+ setMessages([
+ translateWithParameters(
+ `${intlPrefix}.valid${data.installations.length > 1 ? '.multiple_orgs' : ''}`,
+ isAutoProvisioning
+ ? translate('settings.authentication.github.form.provisioning_with_github_short')
+ : translate('settings.authentication.form.provisioning_at_login_short'),
+ data.installations.length
+ ),
+ ]);
+ setAlertVariant('success');
+ } else {
+ setMessages([
+ translateWithParameters(
+ `${intlPrefix}.invalid`,
+ data?.application[applicationField].errorMessage ?? ''
+ ),
+ ...invalidOrgs.map((org) =>
+ translateWithParameters(
+ `${intlPrefix}.invalid_org`,
+ org.organization,
+ org.autoProvisioning.errorMessage ?? ''
+ )
+ ),
+ ]);
+ setAlertVariant('error');
+ }
+ }, [isFetching, isValidApp, isAutoProvisioning, applicationField, data]);
+
+ return (
+ <>
+ <Alert title={messages[0]} variant={alertVariant}>
+ <div className="sw-flex sw-justify-between sw-items-center">
+ <div>
+ {messages.map((msg) => (
+ <div key={msg}>{msg}</div>
+ ))}
+ </div>
+ <div>
+ <Button onClick={() => setOpenDetails(true)} disabled={isFetching} className="sw-mr-2">
+ {translate(`${intlPrefix}.details`)}
+ </Button>
+ <Button onClick={() => refetch()} disabled={isFetching}>
+ {translate(`${intlPrefix}.test`)}
+ </Button>
+ </div>
+ </div>
+ </Alert>
+ {openDetails && (
+ <Modal size="small" contentLabel={modalHeader} onRequestClose={() => setOpenDetails(false)}>
+ <header className="modal-head">
+ <h2>
+ {modalHeader} <ValidityIcon valid={isValidApp} />
+ </h2>
+ </header>
+ <div className="modal-body modal-container">
+ {!isValidApp && (
+ <Alert variant="error">{data?.application[applicationField].errorMessage}</Alert>
+ )}
+ <ul className="sw-pl-5">
+ {data?.installations.map((inst) => (
+ <li key={inst.organization}>
+ <ValidityIcon
+ valid={
+ !isAutoProvisioning ||
+ inst.autoProvisioning.status === GitHubProvisioningStatus.Success
+ }
+ />
+ <span className="sw-ml-2">{inst.organization}</span>
+ {isAutoProvisioning &&
+ inst.autoProvisioning.status === GitHubProvisioningStatus.Error && (
+ <span> - {inst.autoProvisioning.errorMessage}</span>
+ )}
+ </li>
+ ))}
+ </ul>
+ </div>
+ <footer className="modal-foot">
+ <Button onClick={() => setOpenDetails(false)}>{translate('close')}</Button>
+ </footer>
+ </Modal>
+ )}
+ </>
+ );
+}
+
+export default GitHubConfigurationValidity;
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx
index 1f2348a1f62..214e89f16dd 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx
@@ -25,7 +25,6 @@ import ConfirmModal from '../../../../components/controls/ConfirmModal';
import RadioCard from '../../../../components/controls/RadioCard';
import { Button, ResetButtonLink, SubmitButton } from '../../../../components/controls/buttons';
import { Provider } from '../../../../components/hooks/useManageProvider';
-import CheckIcon from '../../../../components/icons/CheckIcon';
import DeleteIcon from '../../../../components/icons/DeleteIcon';
import EditIcon from '../../../../components/icons/EditIcon';
import { Alert } from '../../../../components/ui/Alert';
@@ -36,8 +35,9 @@ import { ExtendedSettingDefinition } from '../../../../types/settings';
import { DOCUMENTATION_LINK_SUFFIXES } from './Authentication';
import AuthenticationFormField from './AuthenticationFormField';
import ConfigurationForm from './ConfigurationForm';
+import GitHubConfigurationValidity from './GitHubConfigurationValidity';
import useGithubConfiguration, { GITHUB_JIT_FIELDS } from './hook/useGithubConfiguration';
-import { useIdentityProvierQuery } from './queries/IdentityProvider';
+import { useIdentityProvierQuery } from './queries/identity-provider';
interface GithubAuthenticationProps {
definitions: ExtendedSettingDefinition[];
@@ -102,6 +102,11 @@ export default function GithubAuthenticationTab(props: GithubAuthenticationProps
</div>
)}
</div>
+ {enabled && (
+ <GitHubConfigurationValidity
+ isAutoProvisioning={!!(newGithubProvisioningStatus ?? githubProvisioningStatus)}
+ />
+ )}
{!hasConfiguration && !hasLegacyConfiguration && (
<div className="big-padded text-center huge-spacer-bottom authentication-no-config">
{translate('settings.authentication.github.form.not_configured')}
@@ -130,16 +135,6 @@ export default function GithubAuthenticationTab(props: GithubAuthenticationProps
<div>
<h5>{translateWithParameters('settings.authentication.github.appid_x', appId)}</h5>
<p>{url}</p>
- <p className="big-spacer-top big-spacer-bottom">
- {enabled ? (
- <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"
onClick={toggleEnable}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx
index 092ae89ede2..f5672920161 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx
@@ -42,7 +42,7 @@ import useSamlConfiguration, {
SAML_GROUP_NAME,
SAML_SCIM_DEPRECATED,
} from './hook/useSamlConfiguration';
-import { useIdentityProvierQuery, useToggleScimMutation } from './queries/IdentityProvider';
+import { useIdentityProvierQuery, useToggleScimMutation } from './queries/identity-provider';
interface SamlAuthenticationProps {
definitions: ExtendedSettingDefinition[];
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx
index 64e43df9627..fd56ab5477f 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx
@@ -17,7 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { act, screen } from '@testing-library/react';
+import { act, screen, 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';
@@ -30,6 +30,7 @@ import { AvailableFeaturesContext } from '../../../../../app/components/availabl
import { definitions } from '../../../../../helpers/mocks/definitions-list';
import { renderComponent } from '../../../../../helpers/testReactTestingUtils';
import { Feature } from '../../../../../types/features';
+import { GitHubProvisioningStatus } from '../../../../../types/provisioning';
import { TaskStatuses } from '../../../../../types/tasks';
import Authentication from '../Authentication';
@@ -163,6 +164,26 @@ const ui = {
githubProvisioningInProgress: byText(/synchronization_in_progress/),
githubProvisioningSuccess: byText(/synchronization_successful/),
githubProvisioningAlert: byText(/synchronization_failed/),
+ configurationValidityLoading: byRole('status', {
+ name: /github.configuration.validation.loading/,
+ }),
+ configurationValiditySuccess: byRole('status', {
+ name: /github.configuration.validation.valid/,
+ }),
+ configurationValidityError: byRole('alert', {
+ name: /github.configuration.validation.invalid/,
+ }),
+ checkConfigButton: byRole('button', {
+ name: 'settings.authentication.github.configuration.validation.test',
+ }),
+ viewConfigValidityDetailsButton: byRole('button', {
+ name: 'settings.authentication.github.configuration.validation.details',
+ }),
+ configDetailsDialog: byRole('dialog', {
+ name: 'settings.authentication.github.configuration.validation.details.title',
+ }),
+ getConfigDetailsTitle: () => within(ui.github.configDetailsDialog.get()).getByRole('heading'),
+ getOrgs: () => within(ui.github.configDetailsDialog.get()).getAllByRole('listitem'),
fillForm: async (user: UserEvent) => {
const { github } = ui;
await act(async () => {
@@ -183,6 +204,12 @@ const ui = {
await user.click(github.saveConfigButton.get());
});
},
+ enableConfiguration: async (user: UserEvent) => {
+ const { github } = ui;
+ await act(async () => user.click(await github.tab.find()));
+ await github.createConfiguration(user);
+ await act(async () => user.click(await github.enableConfigButton.find()));
+ },
enableProvisioning: async (user: UserEvent) => {
const { github } = ui;
await act(async () => user.click(await github.tab.find()));
@@ -418,14 +445,15 @@ describe('Github tab', () => {
});
describe('Github Provisioning', () => {
+ let user: UserEvent;
beforeEach(() => {
jest.useFakeTimers({
advanceTimers: true,
now: new Date('2022-02-04T12:00:59Z'),
});
+ user = userEvent.setup();
});
it('should display a success status when the synchronisation is a success', async () => {
- const user = userEvent.setup();
handler.addProvisioningTask({
status: TaskStatuses.Success,
executedAt: '2022-02-03T11:45:35+0200',
@@ -438,7 +466,6 @@ describe('Github tab', () => {
});
it('should display a success status even when another task is pending', async () => {
- const user = userEvent.setup();
handler.addProvisioningTask({
status: TaskStatuses.Pending,
executedAt: '2022-02-03T11:55:35+0200',
@@ -449,12 +476,11 @@ describe('Github tab', () => {
});
renderAuthentication([Feature.GithubProvisioning]);
await github.enableProvisioning(user);
- expect(await github.githubProvisioningSuccess.find()).toBeInTheDocument();
- expect(await github.githubProvisioningPending.find()).toBeInTheDocument();
+ expect(github.githubProvisioningSuccess.get()).toBeInTheDocument();
+ expect(github.githubProvisioningPending.get()).toBeInTheDocument();
});
it('should display an error alert when the synchronisation failed', async () => {
- const user = userEvent.setup();
handler.addProvisioningTask({
status: TaskStatuses.Failed,
executedAt: '2022-02-03T11:45:35+0200',
@@ -462,13 +488,12 @@ describe('Github tab', () => {
});
renderAuthentication([Feature.GithubProvisioning]);
await github.enableProvisioning(user);
- expect(await github.githubProvisioningAlert.find()).toBeInTheDocument();
+ 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 () => {
- const user = userEvent.setup();
handler.addProvisioningTask({
status: TaskStatuses.InProgress,
executedAt: '2022-02-03T11:55:35+0200',
@@ -480,11 +505,181 @@ describe('Github tab', () => {
});
renderAuthentication([Feature.GithubProvisioning]);
await github.enableProvisioning(user);
- expect(await github.githubProvisioningAlert.find()).toBeInTheDocument();
+ 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);
+
+ expect(github.configurationValiditySuccess.get()).toBeInTheDocument();
+ });
+
+ it('should display that config is valid for both provisioning with multiple orgs', async () => {
+ handler.setConfigurationValidity({
+ installations: [
+ { organization: 'org1', autoProvisioning: { status: GitHubProvisioningStatus.Success } },
+ { organization: 'org2', autoProvisioning: { status: GitHubProvisioningStatus.Success } },
+ ],
+ });
+ renderAuthentication([Feature.GithubProvisioning]);
+ await github.enableConfiguration(user);
+
+ expect(github.configurationValiditySuccess.get()).toBeInTheDocument();
+ expect(github.configurationValiditySuccess.get()).toHaveTextContent('2');
+
+ await act(() => 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', async () => {
+ const errorMessage = 'Test error';
+ handler.setConfigurationValidity({
+ application: {
+ jit: {
+ status: GitHubProvisioningStatus.Error,
+ errorMessage,
+ },
+ autoProvisioning: {
+ status: GitHubProvisioningStatus.Error,
+ errorMessage,
+ },
+ },
+ });
+ renderAuthentication([Feature.GithubProvisioning]);
+ await github.enableConfiguration(user);
+
+ expect(github.configurationValidityError.get()).toBeInTheDocument();
+ expect(github.configurationValidityError.get()).toHaveTextContent(errorMessage);
+
+ await act(() => 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.Error,
+ errorMessage,
+ },
+ },
+ });
+ renderAuthentication([Feature.GithubProvisioning]);
+ await github.enableConfiguration(user);
+
+ expect(github.configurationValiditySuccess.get()).toBeInTheDocument();
+ expect(github.configurationValiditySuccess.get()).not.toHaveTextContent(errorMessage);
+
+ await act(() => user.click(github.viewConfigValidityDetailsButton.get()));
+ expect(github.getConfigDetailsTitle()).toHaveTextContent(
+ 'settings.authentication.github.configuration.validation.details.valid_label'
+ );
+ await act(() =>
+ user.click(within(github.configDetailsDialog.get()).getByRole('button', { name: 'close' }))
+ );
+
+ await act(() => user.click(github.githubProvisioningButton.get()));
+
+ expect(github.configurationValidityError.get()).toBeInTheDocument();
+ expect(github.configurationValidityError.get()).toHaveTextContent(errorMessage);
+
+ await act(() => 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 } },
+ {
+ organization: 'org2',
+ autoProvisioning: { status: GitHubProvisioningStatus.Error, errorMessage },
+ },
+ ],
+ });
+ renderAuthentication([Feature.GithubProvisioning]);
+ await github.enableConfiguration(user);
+
+ expect(github.configurationValiditySuccess.get()).toBeInTheDocument();
+
+ await act(() => user.click(github.viewConfigValidityDetailsButton.get()));
+ github.getOrgs().forEach((org) => {
+ expect(org).toHaveTextContent(
+ 'settings.authentication.github.configuration.validation.details.valid_label'
+ );
+ });
+ await act(() =>
+ user.click(within(github.configDetailsDialog.get()).getByRole('button', { name: 'close' }))
+ );
+
+ await act(() => 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 act(() => 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.Error,
+ errorMessage,
+ },
+ autoProvisioning: {
+ status: GitHubProvisioningStatus.Error,
+ errorMessage,
+ },
+ },
+ });
+ renderAuthentication([Feature.GithubProvisioning]);
+ await github.enableConfiguration(user);
+ handler.setConfigurationValidity({
+ application: {
+ jit: {
+ status: GitHubProvisioningStatus.Success,
+ },
+ autoProvisioning: {
+ status: GitHubProvisioningStatus.Success,
+ },
+ },
+ });
+
+ expect(await github.configurationValidityError.find()).toBeInTheDocument();
+
+ await act(() => user.click(github.checkConfigButton.get()));
+
+ expect(github.configurationValiditySuccess.get()).toBeInTheDocument();
+ expect(github.configurationValidityError.query()).not.toBeInTheDocument();
+ });
});
});
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useGithubConfiguration.ts b/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useGithubConfiguration.ts
index d8efbe74788..a99702ca1ae 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useGithubConfiguration.ts
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useGithubConfiguration.ts
@@ -26,7 +26,7 @@ import { ExtendedSettingDefinition } from '../../../../../types/settings';
import {
useGithubStatusQuery,
useToggleGithubProvisioningMutation,
-} from '../queries/IdentityProvider';
+} from '../queries/identity-provider';
import useConfiguration from './useConfiguration';
export const GITHUB_ENABLED_FIELD = 'sonar.auth.github.enabled';
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useSamlConfiguration.ts b/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useSamlConfiguration.ts
index aed3fa84031..2204d44ec83 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useSamlConfiguration.ts
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useSamlConfiguration.ts
@@ -21,7 +21,7 @@ import React from 'react';
import { AvailableFeaturesContext } from '../../../../../app/components/available-features/AvailableFeaturesContext';
import { Feature } from '../../../../../types/features';
import { ExtendedSettingDefinition } from '../../../../../types/settings';
-import { useScimStatusQuery } from '../queries/IdentityProvider';
+import { useScimStatusQuery } from '../queries/identity-provider';
import useConfiguration from './useConfiguration';
export const SAML_ENABLED_FIELD = 'sonar.auth.saml.enabled';
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/queries/IdentityProvider.ts b/server/sonar-web/src/main/js/apps/settings/components/authentication/queries/identity-provider.ts
index 1396ec4dc1f..832a6c54035 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/authentication/queries/IdentityProvider.ts
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/queries/identity-provider.ts
@@ -23,6 +23,7 @@ import { useContext } from 'react';
import {
activateGithubProvisioning,
activateScim,
+ checkConfigurationValidity,
deactivateGithubProvisioning,
deactivateScim,
fetchIsScimEnabled,
@@ -78,3 +79,7 @@ export function useToggleGithubProvisioningMutation() {
},
});
}
+
+export const useCheckGitHubConfigQuery = () => {
+ return useQuery(['identity_provider', 'github_check'], checkConfigurationValidity);
+};
diff --git a/server/sonar-web/src/main/js/types/provisioning.ts b/server/sonar-web/src/main/js/types/provisioning.ts
index 6d6f05af192..0ee96064316 100644
--- a/server/sonar-web/src/main/js/types/provisioning.ts
+++ b/server/sonar-web/src/main/js/types/provisioning.ts
@@ -47,3 +47,29 @@ export type GithubStatusEnabled = {
};
export type GithubStatus = GithubStatusDisabled | GithubStatusEnabled;
+
+export enum GitHubProvisioningStatus {
+ Success = 'SUCCESS',
+ Error = 'ERROR',
+}
+
+type GitHubProvisioning =
+ | {
+ status: GitHubProvisioningStatus.Success;
+ errorMessage?: never;
+ }
+ | {
+ status: GitHubProvisioningStatus.Error;
+ errorMessage: string;
+ };
+
+export interface GitHubConfigurationStatus {
+ application: {
+ jit: GitHubProvisioning;
+ autoProvisioning: GitHubProvisioning;
+ };
+ installations: {
+ organization: string;
+ autoProvisioning: GitHubProvisioning;
+ }[];
+}
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
index 4989a041d78..2f0bc0fe6ef 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -1368,6 +1368,7 @@ settings.authentication.form.enable=Enable configuration
settings.authentication.form.disable=Disable configuration
settings.authentication.form.provisioning=Provisioning
settings.authentication.form.provisioning_at_login=Just-in-Time user and group provisioning (default)
+settings.authentication.form.provisioning_at_login_short=Just-in-Time provisioning
settings.authentication.form.other_provisioning_enabled=Already enabled for another provider. Only one identity provider can have automatic users and groups provisioning enabled.
# GITHUB
@@ -1383,6 +1384,7 @@ settings.authentication.github.form.not_configured=GitHub App is not configured
settings.authentication.github.form.legacy_configured=Compatibility with GitHub OAuth App is deprecated and will be removed in a future release. Your configuration will continue to work but with limited support. We recommend using GitHub Apps. Check out the {documentation} for more information.
settings.authentication.github.enable_first=Enable your GitHub configuration for more provisioning options.
settings.authentication.github.form.provisioning_with_github=Automatic user and group provisioning
+settings.authentication.github.form.provisioning_with_github_short=Automatic provisioning
settings.authentication.github.form.provisioning_with_github.description=Users and groups are automatically provisioned from your GitHub organizations. Once activated, managed users and groups can only be modified from your GitHub organizations/teams. Existing local users and groups will be kept.
settings.authentication.github.form.provisioning_with_github.description.doc=For more details, see {documentation}.
settings.authentication.github.form.provisioning.disabled=Your current edition does not support provisioning with GitHub. See the {documentation} for more information.
@@ -1393,6 +1395,16 @@ settings.authentication.github.synchronization_successful=Last synchronization w
settings.authentication.github.synchronization_failed=Last synchronization failed {0} ago.
settings.authentication.github.synchronization_failed_short=Last synchronization failed. {details}
settings.authentication.github.synchronization_failed_link=More details
+settings.authentication.github.configuration.validation.details=View details
+settings.authentication.github.configuration.validation.test=Test configuration
+settings.authentication.github.configuration.validation.loading=Checking the configuration
+settings.authentication.github.configuration.validation.valid=Configuration is valid for {0}.
+settings.authentication.github.configuration.validation.valid.multiple_orgs=Configuration is valid for {0}. {1} organizations will be synced.
+settings.authentication.github.configuration.validation.invalid=Configuration is invalid. {0}
+settings.authentication.github.configuration.validation.invalid_org=Organization "{0}" has the following error: {1}
+settings.authentication.github.configuration.validation.details.title=Configuration validity:
+settings.authentication.github.configuration.validation.details.valid_label=Valid
+settings.authentication.github.configuration.validation.details.invalid_label=Invalid
# SAML
settings.authentication.form.create.saml=New SAML configuration