From be28c6d7d70b85472a1213e2384a499432de2e99 Mon Sep 17 00:00:00 2001 From: Mathieu Suen Date: Thu, 11 Nov 2021 18:04:34 +0100 Subject: [PATCH] SONAR-13427 Add renew token badge for admin --- .../src/main/js/api/project-badges.ts | 6 +- .../projectInformation/ProjectInformation.tsx | 7 +- .../ProjectInformation-test.tsx.snap | 97 +++++++++++++++++-- .../badges/ProjectBadges.tsx | 79 ++++++++++++--- .../badges/__tests__/ProjectBadges-test.tsx | 32 +++++- .../__snapshots__/ProjectBadges-test.tsx.snap | 13 ++- .../resources/org/sonar/l10n/core.properties | 5 +- 7 files changed, 201 insertions(+), 38 deletions(-) diff --git a/server/sonar-web/src/main/js/api/project-badges.ts b/server/sonar-web/src/main/js/api/project-badges.ts index 692b8d793c0..b8783f72f9e 100644 --- a/server/sonar-web/src/main/js/api/project-badges.ts +++ b/server/sonar-web/src/main/js/api/project-badges.ts @@ -18,10 +18,14 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import throwGlobalError from '../app/utils/throwGlobalError'; -import { getJSON } from '../helpers/request'; +import { getJSON, postJSON } from '../helpers/request'; export function getProjectBadgesToken(project: string) { return getJSON('/api/project_badges/token', { project }) .then(({ token }) => token) .catch(throwGlobalError); } + +export function renewProjectBadgesToken(project: string) { + return postJSON('/api/project_badges/renew_token', { project }).catch(throwGlobalError); +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/ProjectInformation.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/ProjectInformation.tsx index 0b2072f2d11..3df9c2b97f0 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/ProjectInformation.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/ProjectInformation.tsx @@ -107,12 +107,7 @@ export class ProjectInformation extends React.PureComponent { - + )} {canConfigureNotifications && ( diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/__snapshots__/ProjectInformation-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/__snapshots__/ProjectInformation-test.tsx.snap index fe5bdc421ce..91b4f6e28fc 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/__snapshots__/ProjectInformation-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/__snapshots__/ProjectInformation-test.tsx.snap @@ -35,6 +35,28 @@ exports[`should render correctly: default 1`] = ` onPageChange={[Function]} > @@ -87,6 +107,28 @@ exports[`should render correctly: logged in user 1`] = ` onPageChange={[Function]} > @@ -235,6 +295,29 @@ exports[`should render correctly: private 1`] = ` onPageChange={[Function]} > diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/ProjectBadges.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/ProjectBadges.tsx index 73bdf3fb9a1..b219a077bae 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/ProjectBadges.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/ProjectBadges.tsx @@ -18,9 +18,14 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { getProjectBadgesToken } from '../../../../../../api/project-badges'; +import { + getProjectBadgesToken, + renewProjectBadgesToken +} from '../../../../../../api/project-badges'; import CodeSnippet from '../../../../../../components/common/CodeSnippet'; +import { Button } from '../../../../../../components/controls/buttons'; import { Alert } from '../../../../../../components/ui/Alert'; +import DeferredSpinner from '../../../../../../components/ui/DeferredSpinner'; import { getBranchLikeQuery } from '../../../../../../helpers/branch-like'; import { translate } from '../../../../../../helpers/l10n'; import { BranchLike } from '../../../../../../types/branch-like'; @@ -33,11 +38,11 @@ import { BadgeOptions, BadgeType, getBadgeSnippet, getBadgeUrl } from './utils'; interface Props { branchLike?: BranchLike; metrics: T.Dict; - project: string; - qualifier: string; + component: T.Component; } interface State { + isRenewing: boolean; token: string; selectedType: BadgeType; badgeOptions: BadgeOptions; @@ -46,6 +51,7 @@ interface State { export default class ProjectBadges extends React.PureComponent { mounted = false; state: State = { + isRenewing: false, token: '', selectedType: BadgeType.measure, badgeOptions: { metric: MetricKey.alert_status } @@ -61,8 +67,10 @@ export default class ProjectBadges extends React.PureComponent { } async fetchToken() { - const { project } = this.props; - const token = await getProjectBadgesToken(project); + const { + component: { key } + } = this.props; + const token = await getProjectBadgesToken(key).catch(() => ''); if (this.mounted) { this.setState({ token }); } @@ -73,13 +81,36 @@ export default class ProjectBadges extends React.PureComponent { }; handleUpdateOptions = (options: Partial) => { - this.setState(state => ({ badgeOptions: { ...state.badgeOptions, ...options } })); + this.setState(state => ({ + badgeOptions: { ...state.badgeOptions, ...options } + })); + }; + + handleRenew = async () => { + const { + component: { key } + } = this.props; + + this.setState({ isRenewing: true }); + await renewProjectBadgesToken(key).catch(() => {}); + await this.fetchToken(); + if (this.mounted) { + this.setState({ isRenewing: false }); + } }; render() { - const { branchLike, project, qualifier } = this.props; - const { selectedType, badgeOptions, token } = this.state; - const fullBadgeOptions = { project, ...badgeOptions, ...getBranchLikeQuery(branchLike) }; + const { + branchLike, + component: { key: project, qualifier, configuration } + } = this.props; + const { isRenewing, selectedType, badgeOptions, token } = this.state; + const fullBadgeOptions = { + project, + ...badgeOptions, + ...getBranchLikeQuery(branchLike) + }; + const canRenew = configuration?.showSettings; return (
@@ -110,11 +141,31 @@ export default class ProjectBadges extends React.PureComponent { type={selectedType} updateOptions={this.handleUpdateOptions} /> - {translate('overview.badges.leak_warning')} - + {isRenewing ? ( +
+ +
+ ) : ( + + )} + + +

+ {translate('overview.badges.leak_warning')}{' '} + {canRenew && translate('overview.badges.renew.description')} +

+ {canRenew && ( + + )} +
); } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/ProjectBadges-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/ProjectBadges-test.tsx index 8b0868d437c..da888216e20 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/ProjectBadges-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/ProjectBadges-test.tsx @@ -19,11 +19,16 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { getProjectBadgesToken } from '../../../../../../../api/project-badges'; +import CodeSnippet from '../../../../../../../components/common/CodeSnippet'; import { mockBranch } from '../../../../../../../helpers/mocks/branch-like'; +import { mockComponent } from '../../../../../../../helpers/mocks/component'; import { mockMetric } from '../../../../../../../helpers/testMocks'; import { waitAndUpdate } from '../../../../../../../helpers/testUtils'; import { Location } from '../../../../../../../helpers/urls'; +import { ComponentQualifier } from '../../../../../../../types/component'; import { MetricKey } from '../../../../../../../types/metrics'; +import BadgeButton from '../BadgeButton'; import ProjectBadges from '../ProjectBadges'; jest.mock('../../../../../../../helpers/urls', () => ({ @@ -33,7 +38,8 @@ jest.mock('../../../../../../../helpers/urls', () => ({ })); jest.mock('../../../../../../../api/project-badges', () => ({ - getProjectBadgesToken: jest.fn().mockResolvedValue('foo') + getProjectBadgesToken: jest.fn().mockResolvedValue('foo'), + renewProjectBadgesToken: jest.fn().mockResolvedValue({}) })); it('should display correctly', async () => { @@ -42,6 +48,27 @@ it('should display correctly', async () => { expect(wrapper).toMatchSnapshot(); }); +it('should renew token', async () => { + (getProjectBadgesToken as jest.Mock).mockResolvedValueOnce('foo').mockResolvedValueOnce('bar'); + const wrapper = shallowRender({ + component: mockComponent({ configuration: { showSettings: true } }) + }); + await waitAndUpdate(wrapper); + wrapper.find('.it__project-info-renew-badge').simulate('click'); + + // it shoud be loading + expect(wrapper.find('.it__project-info-renew-badge').props().disabled).toBe(true); + + await waitAndUpdate(wrapper); + const buttons = wrapper.find(BadgeButton); + expect(buttons.at(0).props().url).toMatch('token=bar'); + expect(buttons.at(1).props().url).toMatch('token=bar'); + expect(wrapper.find(CodeSnippet).props().snippet).toMatch('token=bar'); + + // let's check that the loading has correclty ends. + expect(wrapper.find('.it__project-info-renew-badge').props().disabled).toBe(false); +}); + function shallowRender(overrides = {}) { return shallow( ); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/__snapshots__/ProjectBadges-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/__snapshots__/ProjectBadges-test.tsx.snap index 4c7f8b850db..ea5049ad896 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/__snapshots__/ProjectBadges-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/__snapshots__/ProjectBadges-test.tsx.snap @@ -60,14 +60,17 @@ exports[`should display correctly 1`] = ` type="measure" updateOptions={[Function]} /> - - overview.badges.leak_warning - + +

+ overview.badges.leak_warning + +

+
`; 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 423495025c3..4d4c5557a0c 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -2997,7 +2997,7 @@ project_dump.pending_import=Import was scheduled on {0}, waiting to be processed project_dump.in_progress_import=Import is in progress, started {0}. project_dump.failed_import=The last import has failed. Please try once again. project_dump.import_form_description=A dump has been found on the file system for this project. You can import it by clicking on the button below. -project_dump.import_form_description_disabled=Projects cannot be imported. This feature is only available starting from Enterprise Edition +project_dump.import_form_description_disabled=Projects cannot be imported. This feature is only available starting from Enterprise Edition. #------------------------------------------------------------------------------ # @@ -3170,6 +3170,9 @@ overview.badges.quality_gate.description.APP=Displays the current quality gate s overview.badges.quality_gate.description.TRK=Displays the current quality gate status of your project. overview.badges.quality_gate.description.VW=Displays the current quality gate status of your portfolio. overview.badges.leak_warning=Project badges can expose your security rating and other measures. Only use project badges in trusted environments. +overview.badges.renew=Renew Token +overview.badges.renew.description=If your project badge security token has leaked to an unsafe environment, you can renew it: + #------------------------------------------------------------------------------ # -- 2.39.5