diff options
author | Vik Vorona <viktor.vorona@sonarsource.com> | 2023-05-31 11:09:54 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-06-01 20:02:58 +0000 |
commit | 902bbef4744bfbc116f57bef25dec6f262895163 (patch) | |
tree | 72477fb5788fb1b88ec3c3818a55a21a411ca8cb | |
parent | d0dcdf86c351884f4ee896b61b8ff85dbd311e93 (diff) | |
download | sonarqube-902bbef4744bfbc116f57bef25dec6f262895163.tar.gz sonarqube-902bbef4744bfbc116f57bef25dec6f262895163.zip |
SONAR-19337 Display configuration validity status
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 |