diff options
author | Mathieu Suen <mathieu.suen@sonarsource.com> | 2021-11-11 18:04:34 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2021-11-16 20:03:55 +0000 |
commit | be28c6d7d70b85472a1213e2384a499432de2e99 (patch) | |
tree | baac261376d6f4338a67d2cfe630a37e9fb7ba7f | |
parent | 4a478729ea3ba1c7903760253253d38a5b8cb6de (diff) | |
download | sonarqube-be28c6d7d70b85472a1213e2384a499432de2e99.tar.gz sonarqube-be28c6d7d70b85472a1213e2384a499432de2e99.zip |
SONAR-13427 Add renew token badge for admin
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<Props, State> { <InfoDrawerPage displayed={page === ProjectInformationPages.badges} onPageChange={this.setPage}> - <ProjectBadges - branchLike={branchLike} - metrics={metrics} - project={component.key} - qualifier={component.qualifier} - /> + <ProjectBadges branchLike={branchLike} metrics={metrics} component={component} /> </InfoDrawerPage> )} {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]} > <ProjectBadges + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } metrics={ Object { "coverage": Object { @@ -45,8 +67,6 @@ exports[`should render correctly: default 1`] = ` }, } } - project="my-project" - qualifier="TRK" /> </InfoDrawerPage> </Fragment> @@ -87,6 +107,28 @@ exports[`should render correctly: logged in user 1`] = ` onPageChange={[Function]} > <ProjectBadges + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } metrics={ Object { "coverage": Object { @@ -97,8 +139,6 @@ exports[`should render correctly: logged in user 1`] = ` }, } } - project="my-project" - qualifier="TRK" /> </InfoDrawerPage> <InfoDrawerPage @@ -182,6 +222,28 @@ exports[`should render correctly: measures loaded 1`] = ` onPageChange={[Function]} > <ProjectBadges + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } metrics={ Object { "coverage": Object { @@ -192,8 +254,6 @@ exports[`should render correctly: measures loaded 1`] = ` }, } } - project="my-project" - qualifier="TRK" /> </InfoDrawerPage> </Fragment> @@ -235,6 +295,29 @@ exports[`should render correctly: private 1`] = ` onPageChange={[Function]} > <ProjectBadges + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + "visibility": "private", + } + } metrics={ Object { "coverage": Object { @@ -245,8 +328,6 @@ exports[`should render correctly: private 1`] = ` }, } } - project="my-project" - qualifier="TRK" /> </InfoDrawerPage> </Fragment> 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<T.Metric>; - 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<Props, State> { 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<Props, State> { } 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<Props, State> { }; handleUpdateOptions = (options: Partial<BadgeOptions>) => { - 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 ( <div className="display-flex-column"> @@ -110,11 +141,31 @@ export default class ProjectBadges extends React.PureComponent<Props, State> { type={selectedType} updateOptions={this.handleUpdateOptions} /> - <Alert variant="warning">{translate('overview.badges.leak_warning')}</Alert> - <CodeSnippet - isOneLine={true} - snippet={getBadgeSnippet(selectedType, fullBadgeOptions, token)} - /> + {isRenewing ? ( + <div className="spacer-top spacer-bottom display-flex-row display-flex-justify-center"> + <DeferredSpinner className="spacer-top spacer-bottom" loading={isRenewing} /> + </div> + ) : ( + <CodeSnippet + isOneLine={true} + snippet={getBadgeSnippet(selectedType, fullBadgeOptions, token)} + /> + )} + + <Alert variant="warning"> + <p> + {translate('overview.badges.leak_warning')}{' '} + {canRenew && translate('overview.badges.renew.description')} + </p> + {canRenew && ( + <Button + disabled={isRenewing} + className="spacer-top it__project-info-renew-badge" + onClick={this.handleRenew}> + {translate('overview.badges.renew')} + </Button> + )} + </Alert> </div> ); } 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( <ProjectBadges @@ -50,8 +77,7 @@ function shallowRender(overrides = {}) { [MetricKey.coverage]: mockMetric({ key: MetricKey.coverage }), [MetricKey.new_code_smells]: mockMetric({ key: MetricKey.new_code_smells }) }} - project="foo" - qualifier="TRK" + component={mockComponent({ key: 'foo', qualifier: ComponentQualifier.Project })} {...overrides} /> ); 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]} /> - <Alert - variant="warning" - > - overview.badges.leak_warning - </Alert> <CodeSnippet isOneLine={true} snippet="[![alert_status](host/api/project_badges/measure?branch=branch-6.7&project=foo&metric=alert_status&token=foo)](/dashboard)" /> + <Alert + variant="warning" + > + <p> + overview.badges.leak_warning + + </p> + </Alert> </div> `; 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: + #------------------------------------------------------------------------------ # |