Browse Source

SONAR-13427 Add renew token badge for admin

tags/9.2.0.49834
Mathieu Suen 2 years ago
parent
commit
be28c6d7d7

+ 5
- 1
server/sonar-web/src/main/js/api/project-badges.ts View File

@@ -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);
}

+ 1
- 6
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/ProjectInformation.tsx View File

@@ -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 && (

+ 89
- 8
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/__snapshots__/ProjectInformation-test.tsx.snap View File

@@ -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>

+ 65
- 14
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/ProjectBadges.tsx View File

@@ -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>
);
}

+ 29
- 3
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/ProjectBadges-test.tsx View File

@@ -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}
/>
);

+ 8
- 5
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/__snapshots__/ProjectBadges-test.tsx.snap View File

@@ -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>
`;

+ 4
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -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:


#------------------------------------------------------------------------------
#

Loading…
Cancel
Save