TaskTypes.ProjectImport,
TaskTypes.ViewRefresh,
TaskTypes.ReportSubmit,
+ TaskTypes.GithubProvisioning,
];
const DEFAULT_TASKS: Task[] = [mockTask()];
System: {
'High Availability': true,
'Server ID': 'asd564-asd54a-5dsfg45',
- 'External Users and Groups Provisioning': 'Okta',
+ 'External Users and Groups Provisioning': '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 { isEmpty } from 'lodash';
+import * as React from 'react';
+import { useContext, useEffect, useState } from 'react';
+import { WrappedComponentProps, injectIntl } from 'react-intl';
+import { getActivity } from '../../api/ce';
+import { formatterOption } from '../../components/intl/DateTimeFormatter';
+import { Alert, AlertVariant } from '../../components/ui/Alert';
+import { translateWithParameters } from '../../helpers/l10n';
+import { Feature } from '../../types/features';
+import { ActivityRequestParameters, TaskStatuses, TaskTypes } from '../../types/tasks';
+import './SystemAnnouncement.css';
+import { AvailableFeaturesContext } from './available-features/AvailableFeaturesContext';
+
+function GitHubSynchronisationWarning(props: WrappedComponentProps) {
+ const { formatDate } = props.intl;
+ const [displayMessage, setDisplayMessage] = useState(false);
+ const [activityStatus, setActivityStatus] = useState<AlertVariant>('info');
+ const [message, setMessage] = useState('');
+ const hasGithubProvisioning = useContext(AvailableFeaturesContext).includes(
+ Feature.GithubProvisioning
+ );
+
+ useEffect(() => {
+ (async () => {
+ const lastActivity = await getLatestGithubSynchronisationTask();
+
+ if (lastActivity === undefined) {
+ return;
+ }
+ const { status, errorMessage, executedAt } = lastActivity;
+
+ if (executedAt === undefined) {
+ return;
+ }
+ const formattedDate = formatDate(new Date(executedAt), formatterOption);
+
+ switch (status) {
+ case TaskStatuses.Failed:
+ setMessage(
+ translateWithParameters(
+ 'settings.authentication.github.background_task.synchronisation_failed',
+ formattedDate,
+ errorMessage ?? ''
+ )
+ );
+ setActivityStatus('error');
+ break;
+ case TaskStatuses.Success:
+ setMessage(
+ translateWithParameters(
+ 'settings.authentication.github.background_task.synchronisation_successful',
+ formattedDate
+ )
+ );
+ setActivityStatus('success');
+ break;
+ case TaskStatuses.InProgress:
+ setMessage(
+ translateWithParameters(
+ 'settings.authentication.github.background_task.synchronisation_in_progress',
+ formattedDate
+ )
+ );
+ setActivityStatus('loading');
+ break;
+ default:
+ return;
+ }
+ setDisplayMessage(true);
+ })();
+ }, []);
+
+ if (!displayMessage || !hasGithubProvisioning) {
+ return null;
+ }
+
+ return (
+ <Alert title={message} variant={activityStatus}>
+ {message}
+ </Alert>
+ );
+}
+
+const getLatestGithubSynchronisationTask = async () => {
+ const data: ActivityRequestParameters = {
+ type: TaskTypes.GithubProvisioning,
+ onlyCurrents: true,
+ status: [TaskStatuses.InProgress, TaskStatuses.Success, TaskStatuses.Failed].join(','),
+ };
+ const activity = await getActivity(data);
+
+ if (isEmpty(activity.tasks)) {
+ return undefined;
+ }
+
+ return activity.tasks[0];
+};
+
+export default injectIntl(GitHubSynchronisationWarning);
resetSettingValue,
setSettingValue,
} from '../../../../api/settings';
+import GitHubSynchronisationWarning from '../../../../app/components/GitHubSynchronisationWarning';
import DocLink from '../../../../components/common/DocLink';
import ConfirmModal from '../../../../components/controls/ConfirmModal';
import RadioCard from '../../../../components/controls/RadioCard';
import { Button, ResetButtonLink, SubmitButton } from '../../../../components/controls/buttons';
+import { Provider } from '../../../../components/hooks/useManageProvider';
import CheckIcon from '../../../../components/icons/CheckIcon';
import DeleteIcon from '../../../../components/icons/DeleteIcon';
import EditIcon from '../../../../components/icons/EditIcon';
resetJitSetting,
} = useGithubConfiguration(definitions, props.onReload);
- const hasDifferentProvider = provider !== undefined && provider !== 'github';
+ const hasDifferentProvider = provider !== undefined && provider !== Provider.Github;
const handleCreateConfiguration = () => {
setShowEditModal(true);
'settings.authentication.github.form.provisioning_with_github.description'
)}
</p>
- <p>
+ <p className="spacer-bottom">
<FormattedMessage
id="settings.authentication.github.form.provisioning_with_github.description.doc"
defaultMessage={translate(
}}
/>
</p>
+ {githubProvisioningStatus && <GitHubSynchronisationWarning />}
</>
) : (
<p>
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';
deleteConfiguration,
} = useSamlConfiguration(definitions, onReload);
- const hasDifferentProvider = provider !== undefined && provider !== name;
+ const hasDifferentProvider = provider !== undefined && provider !== Provider.Scim;
const handleCreateConfiguration = () => {
setShowEditModal(true);
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Helmet } from 'react-helmet-async';
import { getIdentityProviders, searchUsers } from '../../api/users';
+import GitHubSynchronisationWarning from '../../app/components/GitHubSynchronisationWarning';
import HelpTooltip from '../../components/controls/HelpTooltip';
import ListFooter from '../../components/controls/ListFooter';
import { ManagedFilter } from '../../components/controls/ManagedFilter';
import SearchBox from '../../components/controls/SearchBox';
import Select, { LabelValueSelectOption } from '../../components/controls/Select';
import Suggestions from '../../components/embed-docs-modal/Suggestions';
-import { useManageProvider } from '../../components/hooks/useManageProvider';
+import { Provider, useManageProvider } from '../../components/hooks/useManageProvider';
import DeferredSpinner from '../../components/ui/DeferredSpinner';
import { now, toISO8601WithOffsetString } from '../../helpers/dates';
import { translate } from '../../helpers/l10n';
<Suggestions suggestions="users" />
<Helmet defer={false} title={translate('users.page')} />
<Header onUpdateUsers={fetchUsers} manageProvider={manageProvider} />
+ {manageProvider === Provider.Github && <GitHubSynchronisationWarning />}
<div className="display-flex-justify-start big-spacer-bottom big-spacer-top">
<ManagedFilter
manageProvider={manageProvider}
import selectEvent from 'react-select-event';
import { byLabelText, byRole, byText } from 'testing-library-selector';
import ComponentsServiceMock from '../../../api/mocks/ComponentsServiceMock';
+import ComputeEngineServiceMock from '../../../api/mocks/ComputeEngineServiceMock';
import SettingsServiceMock from '../../../api/mocks/SettingsServiceMock';
import UserTokensMock from '../../../api/mocks/UserTokensMock';
import UsersServiceMock from '../../../api/mocks/UsersServiceMock';
import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks';
import { renderApp } from '../../../helpers/testReactTestingUtils';
+import { Feature } from '../../../types/features';
+import { TaskStatuses, TaskTypes } from '../../../types/tasks';
import { ChangePasswordResults, CurrentUser } from '../../../types/users';
import UsersApp from '../UsersApp';
jest.mock('../../../api/user-tokens');
jest.mock('../../../api/components');
jest.mock('../../../api/settings');
+jest.mock('../../../api/ce');
const userHandler = new UsersServiceMock();
const tokenHandler = new UserTokensMock();
const componentsHandler = new ComponentsServiceMock();
const settingsHandler = new SettingsServiceMock();
+const computeEngineHandler = new ComputeEngineServiceMock();
const ui = {
createUserButton: byRole('button', { name: 'users.create_user' }),
byRole('button', {
name: `remove_x.users.create_user.scm_account_${value ? `x.${value}` : 'new'}`,
}),
-
userRows: byRole('row', {
name: (accessibleName) => /^[A-Z]+ /.test(accessibleName),
}),
confirmPassword: byLabelText('my_profile.password.confirm', { selector: 'input', exact: false }),
tokenNameInput: byRole('textbox', { name: 'users.tokens.name' }),
deleteUserCheckbox: byRole('checkbox', { name: 'users.delete_user' }),
+ githubProvisioningInProgress: byRole('status', { name: /synchronisation_in_progress/ }),
+ githubProvisioningSuccess: byRole('status', { name: /synchronisation_successful/ }),
+ githubProvisioningAlert: byRole('alert', { name: /synchronisation_failed/ }),
};
beforeEach(() => {
it('should change a password', async () => {
const user = userEvent.setup();
const currentUser = mockLoggedInUser({ login: 'alice.merveille' });
- renderUsersApp(currentUser);
+ renderUsersApp([], currentUser);
await act(async () =>
user.click(
userHandler.setIsManaged(true);
});
+ afterEach(() => {
+ computeEngineHandler.clearTasks();
+ });
+
it('should not be able to create a user"', async () => {
renderUsersApp();
await user.click(ui.doneButton.get());
expect(ui.dialogTokens.query()).not.toBeInTheDocument();
});
+
+ describe('Github Provisioning', () => {
+ it('should display an info status when the synchronisation is in Progress', async () => {
+ computeEngineHandler.addTask({
+ status: TaskStatuses.InProgress,
+ type: TaskTypes.GithubProvisioning,
+ executedAt: '2022-02-03T11:45:35+0200',
+ });
+ renderUsersApp([Feature.GithubProvisioning]);
+ expect(await ui.githubProvisioningInProgress.find()).toBeInTheDocument();
+ });
+
+ it('should display a success status when the synchronisation is a success', async () => {
+ computeEngineHandler.addTask({
+ status: TaskStatuses.Success,
+ type: TaskTypes.GithubProvisioning,
+ executedAt: '2022-02-03T11:45:35+0200',
+ });
+ renderUsersApp([Feature.GithubProvisioning]);
+ expect(await ui.githubProvisioningSuccess.find()).toBeInTheDocument();
+ });
+
+ it('should display an error alert when the synchronisation failed', async () => {
+ computeEngineHandler.addTask({
+ status: TaskStatuses.Failed,
+ type: TaskTypes.GithubProvisioning,
+ executedAt: '2022-02-03T11:45:35+0200',
+ errorMessage: "T'es mauvais Jacques",
+ });
+ renderUsersApp([Feature.GithubProvisioning]);
+ expect(await ui.githubProvisioningAlert.find()).toBeInTheDocument();
+ });
+ });
});
it('should render external identity Providers', async () => {
expect(await ui.denisRow.find()).toHaveTextContent(/test2: UnknownExternalProvider/);
});
-function renderUsersApp(currentUser?: CurrentUser) {
+function renderUsersApp(featureList: Feature[] = [], currentUser?: CurrentUser) {
// eslint-disable-next-line testing-library/no-unnecessary-act
- return renderApp('admin/users', <UsersApp />, { currentUser: mockCurrentUser(currentUser) });
+ renderApp('admin/users', <UsersApp />, {
+ currentUser: mockCurrentUser(currentUser),
+ featureList,
+ });
}
import { getSystemInfo } from '../../api/system';
import { SysInfoCluster } from '../../types/types';
+export enum Provider {
+ Github = 'GitHub',
+ Scim = 'SCIM',
+}
+
export function useManageProvider(): string | undefined {
const [manageProvider, setManageProvider] = React.useState<string | undefined>();
export enum TaskTypes {
Report = 'REPORT',
IssueSync = 'ISSUE_SYNC',
+ GithubProvisioning = 'GITHUB_AUTH_PROVISIONING',
AppRefresh = 'APP_REFRESH',
ViewRefresh = 'VIEW_REFRESH',
ProjectExport = 'PROJECT_EXPORT',
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.
+settings.authentication.github.background_task.synchronisation_in_progress=Synchronisation is in progress. Started on {0}.
+settings.authentication.github.background_task.synchronisation_successful=Successful synchronisation on {0}.
+settings.authentication.github.background_task.synchronisation_failed=Synchronisation failed on {0}. {1}
# SAML
settings.authentication.form.create.saml=New SAML configuration
background_task.type.PROJECT_IMPORT=Project Import
background_task.type.AUDIT_PURGE=Audit Log Purge
background_task.type.REPORT_SUBMIT=Report Email Submit
+background_task.type.GITHUB_AUTH_PROVISIONING=Github Provisioning
background_tasks.page=Background Tasks
background_tasks.page.description=This page allows monitoring of the queue of tasks running asynchronously on the server. It also gives access to the history of finished tasks and their status. Analysis report processing is the most common kind of background task.