aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorWouter Admiraal <wouter.admiraal@sonarsource.com>2022-07-08 10:13:41 +0200
committersonartech <sonartech@sonarsource.com>2022-07-08 20:02:47 +0000
commit231b27662db5d7525d24704e6c53981662e3403f (patch)
treefa2cabaa4e740e84759e1f57f2c12d1dac431bd5 /server
parentbcf7cdd7569465198d9d0252b6aedcfc51c83474 (diff)
downloadsonarqube-231b27662db5d7525d24704e6c53981662e3403f.tar.gz
sonarqube-231b27662db5d7525d24704e6c53981662e3403f.zip
SONAR-16565 Move max token lifetime logic to its own function
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/api/settings.ts9
-rw-r--r--server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx136
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx61
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/tokens-test.ts107
-rw-r--r--server/sonar-web/src/main/js/helpers/tokens.ts72
5 files changed, 199 insertions, 186 deletions
diff --git a/server/sonar-web/src/main/js/api/settings.ts b/server/sonar-web/src/main/js/api/settings.ts
index e322511bce8..c00cc81078b 100644
--- a/server/sonar-web/src/main/js/api/settings.ts
+++ b/server/sonar-web/src/main/js/api/settings.ts
@@ -45,6 +45,15 @@ export function getValues(
]);
}
+export function getAllValues(
+ data: { component?: string } & BranchParameters = {}
+): Promise<SettingValue[]> {
+ return getJSON('/api/settings/values', data).then((r: SettingValueResponse) => [
+ ...r.settings,
+ ...r.setSecuredSettings.map(key => ({ key }))
+ ]);
+}
+
export function setSettingValue(
definition: SettingDefinition,
value: any,
diff --git a/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx b/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx
index 3afaf428006..7abb80758a5 100644
--- a/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx
+++ b/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx
@@ -20,19 +20,15 @@
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';
-import { format } from 'date-fns';
import selectEvent from 'react-select-event';
import { getMyProjects, getScannableProjects } from '../../../api/components';
import NotificationsMock from '../../../api/mocks/NotificationsMock';
import UserTokensMock from '../../../api/mocks/UserTokensMock';
-import { getValues } from '../../../api/settings';
-import { generateToken } from '../../../api/user-tokens';
import { mockUserToken } from '../../../helpers/mocks/token';
import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks';
import { renderApp } from '../../../helpers/testReactTestingUtils';
import { Permissions } from '../../../types/permissions';
-import { SettingsKey } from '../../../types/settings';
-import { TokenExpiration, TokenType } from '../../../types/token';
+import { TokenType } from '../../../types/token';
import { CurrentUser } from '../../../types/users';
import routes from '../routes';
@@ -50,7 +46,7 @@ jest.mock('../../../api/settings', () => {
const { SettingsKey } = jest.requireActual('../../../types/settings');
return {
...jest.requireActual('../../../api/settings'),
- getValues: jest.fn().mockResolvedValue([
+ getAllValues: jest.fn().mockResolvedValue([
{
key: SettingsKey.TokenMaxAllowedLifetime,
value: 'No expiration'
@@ -234,7 +230,6 @@ describe('security page', () => {
beforeAll(() => {
tokenMock = new UserTokensMock();
- (getValues as jest.Mock).mockClear();
});
afterEach(() => {
@@ -244,133 +239,6 @@ describe('security page', () => {
const securityPagePath = 'account/security';
it.each([
- [
- '30 days',
- [TokenExpiration.OneMonth],
- [TokenExpiration.ThreeMonths, TokenExpiration.OneYear, TokenExpiration.NoExpiration]
- ],
- [
- '90 days',
- [TokenExpiration.OneMonth, TokenExpiration.ThreeMonths],
- [TokenExpiration.OneYear, TokenExpiration.NoExpiration]
- ],
- [
- '1 year',
- [TokenExpiration.OneMonth, TokenExpiration.ThreeMonths, TokenExpiration.OneYear],
- [TokenExpiration.NoExpiration]
- ],
- [
- 'No expiration',
- [
- TokenExpiration.OneMonth,
- TokenExpiration.ThreeMonths,
- TokenExpiration.OneYear,
- TokenExpiration.NoExpiration
- ],
- []
- ]
- ])(
- 'should display expiration date inferior or equal to the settings limit %s',
- async (settingMaxLifetime, expectedTime, notExpectedTime) => {
- (getValues as jest.Mock).mockImplementationOnce(() =>
- Promise.resolve([{ key: SettingsKey.TokenMaxAllowedLifetime, value: settingMaxLifetime }])
- );
-
- renderAccountApp(
- mockLoggedInUser({ permissions: { global: [Permissions.Scan] } }),
- securityPagePath
- );
-
- await selectEvent.openMenu(screen.getAllByRole('textbox')[2]);
-
- expectedTime.forEach(time => {
- // TokenExpiration.OneMonth is expected twice has it is the default value.
- expect(screen.getAllByText(`users.tokens.expiration.${time}`).length).toBe(
- time === TokenExpiration.OneMonth ? 2 : 1
- );
- });
-
- notExpectedTime.forEach(time => {
- expect(screen.queryByText(`users.tokens.expiration.${time}`)).not.toBeInTheDocument();
- });
- }
- );
-
- it('should handle absent setting', async () => {
- (getValues as jest.Mock).mockImplementationOnce(() => Promise.resolve([]));
-
- renderAccountApp(
- mockLoggedInUser({ permissions: { global: [Permissions.Scan] } }),
- securityPagePath
- );
-
- await selectEvent.openMenu(screen.getAllByRole('textbox')[2]);
-
- [
- TokenExpiration.OneMonth,
- TokenExpiration.ThreeMonths,
- TokenExpiration.OneYear,
- TokenExpiration.NoExpiration
- ].forEach(time => {
- // TokenExpiration.OneMonth is expected twice has it is the default value.
- expect(screen.getAllByText(`users.tokens.expiration.${time}`).length).toBe(
- time === TokenExpiration.OneMonth ? 2 : 1
- );
- });
- });
-
- it.each([
- [TokenExpiration.OneMonth, '2022-07-01'],
- [TokenExpiration.ThreeMonths, '2022-08-30'],
- [TokenExpiration.OneYear, '2023-06-01'],
- [TokenExpiration.NoExpiration, undefined]
- ])(
- 'should allow token creation with proper expiration date for %s days',
- async (tokenExpirationTime, expectedTime) => {
- const user = userEvent.setup();
-
- renderAccountApp(
- mockLoggedInUser({ permissions: { global: [Permissions.Scan] } }),
- securityPagePath
- );
-
- // Add the token
- const newTokenName = 'importantToken' + tokenExpirationTime;
- const input = screen.getByPlaceholderText('users.tokens.enter_name');
- const generateButton = screen.getByRole('button', { name: 'users.generate' });
- expect(input).toBeInTheDocument();
- await user.click(input);
- await user.keyboard(newTokenName);
-
- expect(generateButton).toBeDisabled();
-
- const tokenTypeLabel = `users.tokens.${TokenType.User}`;
- await selectEvent.select(screen.getAllByRole('textbox')[1], [tokenTypeLabel]);
-
- const tokenExpirationLabel = `users.tokens.expiration.${tokenExpirationTime}`;
- await selectEvent.select(screen.getAllByRole('textbox')[2], [tokenExpirationLabel]);
-
- await user.click(generateButton);
-
- expect(generateToken).toHaveBeenLastCalledWith({
- name: newTokenName,
- login: 'luke',
- type: TokenType.User,
- expirationDate: expectedTime
- });
-
- // ensure the list of tokens is updated
- const rows = await screen.findAllByRole('row');
- expect(rows).toHaveLength(4);
- expect(rows.pop()).toHaveTextContent(
- `${newTokenName}users.tokens.USER_TOKEN.shortneverApril 4, 2022${
- expectedTime ? format(expectedTime, 'MMMM D, YYYY') : '–'
- }users.tokens.revoke`
- );
- }
- );
-
- it.each([
['user', TokenType.User],
['global', TokenType.Global],
['project analysis', TokenType.Project]
diff --git a/server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx b/server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx
index cff1307a7c2..ec07f19b5e4 100644
--- a/server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx
+++ b/server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx
@@ -20,17 +20,19 @@
import { isEmpty } from 'lodash';
import * as React from 'react';
import { getScannableProjects } from '../../../api/components';
-import { getValues } from '../../../api/settings';
import { generateToken, getTokens } from '../../../api/user-tokens';
import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
import { SubmitButton } from '../../../components/controls/buttons';
import Select, { BasicSelectOption } from '../../../components/controls/Select';
import DeferredSpinner from '../../../components/ui/DeferredSpinner';
-import { now, toShortNotSoISOString } from '../../../helpers/dates';
import { translate } from '../../../helpers/l10n';
+import {
+ computeTokenExpirationDate,
+ EXPIRATION_OPTIONS,
+ getAvailableExpirationOptions
+} from '../../../helpers/tokens';
import { hasGlobalPermission } from '../../../helpers/users';
import { Permissions } from '../../../types/permissions';
-import { SettingsKey } from '../../../types/settings';
import { TokenExpiration, TokenType, UserToken } from '../../../types/token';
import { CurrentUser } from '../../../types/users';
import TokensFormItem, { TokenDeleteConfirmation } from './TokensFormItem';
@@ -57,25 +59,6 @@ interface State {
tokenExpirationOptions: { value: TokenExpiration; label: string }[];
}
-const EXPIRATION_OPTIONS = [
- TokenExpiration.OneMonth,
- TokenExpiration.ThreeMonths,
- TokenExpiration.OneYear,
- TokenExpiration.NoExpiration
-].map(value => {
- return {
- value,
- label: translate('users.tokens.expiration', value.toString())
- };
-});
-
-const SETTINGS_EXPIRATION_MAP: { [key: string]: TokenExpiration } = {
- '30 days': TokenExpiration.OneMonth,
- '90 days': TokenExpiration.ThreeMonths,
- '1 year': TokenExpiration.OneYear,
- 'No expiration': TokenExpiration.NoExpiration
-};
-
export class TokensForm extends React.PureComponent<Props, State> {
mounted = false;
state: State = {
@@ -120,29 +103,9 @@ export class TokensForm extends React.PureComponent<Props, State> {
};
fetchTokenSettings = async () => {
- /*
- * We intentionally fetch all settings, because fetching a specific setting will
- * return it from the DB as a fallback, even if the setting is not defined at startup.
- */
- const settings = await getValues({ keys: '' });
- const maxTokenLifetime = settings.find(
- ({ key }) => key === SettingsKey.TokenMaxAllowedLifetime
- );
-
- if (maxTokenLifetime === undefined || maxTokenLifetime.value === undefined) {
- return;
- }
-
- const maxTokenExpirationOption = SETTINGS_EXPIRATION_MAP[maxTokenLifetime.value];
-
- if (maxTokenExpirationOption !== TokenExpiration.NoExpiration) {
- const tokenExpirationOptions = EXPIRATION_OPTIONS.filter(
- option =>
- option.value <= maxTokenExpirationOption && option.value !== TokenExpiration.NoExpiration
- );
- if (this.mounted) {
- this.setState({ tokenExpirationOptions });
- }
+ const tokenExpirationOptions = await getAvailableExpirationOptions();
+ if (this.mounted) {
+ this.setState({ tokenExpirationOptions });
}
};
@@ -160,12 +123,6 @@ export class TokensForm extends React.PureComponent<Props, State> {
}
};
- getExpirationDate = (days: number): string => {
- const expirationDate = now();
- expirationDate.setDate(expirationDate.getDate() + days);
- return toShortNotSoISOString(expirationDate);
- };
-
handleGenerateToken = async (event: React.SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();
const { login } = this.props;
@@ -184,7 +141,7 @@ export class TokensForm extends React.PureComponent<Props, State> {
type: newTokenType,
...(newTokenType === TokenType.Project && { projectKey: selectedProject.key }),
...(newTokenExpiration !== TokenExpiration.NoExpiration && {
- expirationDate: this.getExpirationDate(newTokenExpiration)
+ expirationDate: computeTokenExpirationDate(newTokenExpiration)
})
});
diff --git a/server/sonar-web/src/main/js/helpers/__tests__/tokens-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/tokens-test.ts
new file mode 100644
index 00000000000..7f35313ce4d
--- /dev/null
+++ b/server/sonar-web/src/main/js/helpers/__tests__/tokens-test.ts
@@ -0,0 +1,107 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { getAllValues } from '../../api/settings';
+import { SettingsKey } from '../../types/settings';
+import { TokenExpiration } from '../../types/token';
+import { mockSettingValue } from '../mocks/settings';
+import {
+ computeTokenExpirationDate,
+ EXPIRATION_OPTIONS,
+ getAvailableExpirationOptions
+} from '../tokens';
+
+jest.mock('../../api/settings', () => {
+ return {
+ getAllValues: jest.fn().mockResolvedValue([])
+ };
+});
+
+jest.mock('../dates', () => {
+ return {
+ ...jest.requireActual('../dates'),
+ now: jest.fn(() => new Date('2022-06-01T12:00:00Z'))
+ };
+});
+
+describe('getAvailableExpirationOptions', () => {
+ it('should correctly return all options if no setting', async () => {
+ expect(await getAvailableExpirationOptions()).toEqual(EXPIRATION_OPTIONS);
+ });
+
+ it('should correctly return all options if the max setting is no expiration', async () => {
+ (getAllValues as jest.Mock).mockResolvedValueOnce([
+ mockSettingValue({ key: SettingsKey.TokenMaxAllowedLifetime, value: 'No expiration' })
+ ]);
+ expect(await getAvailableExpirationOptions()).toEqual(EXPIRATION_OPTIONS);
+ });
+
+ it('should correctly limit options if the max setting is 1 year', async () => {
+ (getAllValues as jest.Mock).mockResolvedValueOnce([
+ mockSettingValue({ key: SettingsKey.TokenMaxAllowedLifetime, value: '1 year' })
+ ]);
+ expect(await getAvailableExpirationOptions()).toEqual(
+ [TokenExpiration.OneMonth, TokenExpiration.ThreeMonths, TokenExpiration.OneYear].map(
+ value => {
+ return {
+ value,
+ label: `users.tokens.expiration.${value.toString()}`
+ };
+ }
+ )
+ );
+ });
+
+ it('should correctly limit options if the max setting is 3 months', async () => {
+ (getAllValues as jest.Mock).mockResolvedValueOnce([
+ mockSettingValue({ key: SettingsKey.TokenMaxAllowedLifetime, value: '90 days' })
+ ]);
+ expect(await getAvailableExpirationOptions()).toEqual(
+ [TokenExpiration.OneMonth, TokenExpiration.ThreeMonths].map(value => {
+ return {
+ value,
+ label: `users.tokens.expiration.${value.toString()}`
+ };
+ })
+ );
+ });
+
+ it('should correctly limit options if the max setting is 30 days', async () => {
+ (getAllValues as jest.Mock).mockResolvedValueOnce([
+ mockSettingValue({ key: SettingsKey.TokenMaxAllowedLifetime, value: '30 days' })
+ ]);
+ expect(await getAvailableExpirationOptions()).toEqual([
+ {
+ value: TokenExpiration.OneMonth,
+ label: `users.tokens.expiration.${TokenExpiration.OneMonth.toString()}`
+ }
+ ]);
+ });
+});
+
+describe('computeTokenExpirationDate', () => {
+ it.each([
+ [TokenExpiration.OneMonth, '2022-07-01'],
+ [TokenExpiration.ThreeMonths, '2022-08-30'],
+ [TokenExpiration.OneYear, '2023-06-01']
+ ])('should correctly compute the proper expiration date for %s days', (days, expected) => {
+ expect(computeTokenExpirationDate(days)).toBe(expected);
+ });
+});
diff --git a/server/sonar-web/src/main/js/helpers/tokens.ts b/server/sonar-web/src/main/js/helpers/tokens.ts
new file mode 100644
index 00000000000..ab8f3c5c522
--- /dev/null
+++ b/server/sonar-web/src/main/js/helpers/tokens.ts
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { getAllValues } from '../api/settings';
+import { SettingsKey } from '../types/settings';
+import { TokenExpiration } from '../types/token';
+import { now, toShortNotSoISOString } from './dates';
+import { translate } from './l10n';
+
+export const EXPIRATION_OPTIONS = [
+ TokenExpiration.OneMonth,
+ TokenExpiration.ThreeMonths,
+ TokenExpiration.OneYear,
+ TokenExpiration.NoExpiration
+].map(value => {
+ return {
+ value,
+ label: translate('users.tokens.expiration', value.toString())
+ };
+});
+
+const SETTINGS_EXPIRATION_MAP: { [key: string]: TokenExpiration } = {
+ '30 days': TokenExpiration.OneMonth,
+ '90 days': TokenExpiration.ThreeMonths,
+ '1 year': TokenExpiration.OneYear,
+ 'No expiration': TokenExpiration.NoExpiration
+};
+
+export async function getAvailableExpirationOptions() {
+ /*
+ * We intentionally fetch all settings, because fetching a specific setting will
+ * return it from the DB as a fallback, even if the setting is not defined at startup.
+ */
+ const setting = (await getAllValues()).find(v => v.key === SettingsKey.TokenMaxAllowedLifetime);
+ if (setting === undefined || setting.value === undefined) {
+ return EXPIRATION_OPTIONS;
+ }
+
+ const maxTokenLifetime = setting.value;
+ if (SETTINGS_EXPIRATION_MAP[maxTokenLifetime] !== TokenExpiration.NoExpiration) {
+ return EXPIRATION_OPTIONS.filter(
+ option =>
+ option.value <= SETTINGS_EXPIRATION_MAP[maxTokenLifetime] &&
+ option.value !== TokenExpiration.NoExpiration
+ );
+ }
+
+ return EXPIRATION_OPTIONS;
+}
+
+export function computeTokenExpirationDate(days: number) {
+ const expirationDate = now();
+ expirationDate.setDate(expirationDate.getDate() + days);
+ return toShortNotSoISOString(expirationDate);
+}