Quellcode durchsuchen

SONAR-16263 Update onboarding token generation

tags/9.5.0.56709
Wouter Admiraal vor 2 Jahren
Ursprung
Commit
445764544c

+ 24
- 2
server/sonar-web/src/main/js/components/tutorials/TutorialSelection.tsx Datei anzeigen

@@ -20,9 +20,12 @@
import * as React from 'react';
import { WithRouterProps } from 'react-router';
import { getAlmSettingsNoCatch } from '../../api/alm-settings';
import { getScannableProjects } from '../../api/components';
import { getValues } from '../../api/settings';
import { getHostUrl } from '../../helpers/urls';
import { hasGlobalPermission } from '../../helpers/users';
import { AlmSettingsInstance, ProjectAlmBindingResponse } from '../../types/alm-settings';
import { Permissions } from '../../types/permissions';
import { SettingsKey } from '../../types/settings';
import { Component } from '../../types/types';
import { LoggedInUser } from '../../types/users';
@@ -39,6 +42,7 @@ interface Props extends Pick<WithRouterProps, 'router' | 'location'> {

interface State {
almBinding?: AlmSettingsInstance;
currentUserCanScanProject: boolean;
baseUrl: string;
loading: boolean;
}
@@ -46,6 +50,7 @@ interface State {
export class TutorialSelection extends React.PureComponent<Props, State> {
mounted = false;
state: State = {
currentUserCanScanProject: false,
baseUrl: getHostUrl(),
loading: true
};
@@ -53,7 +58,7 @@ export class TutorialSelection extends React.PureComponent<Props, State> {
async componentDidMount() {
this.mounted = true;

await Promise.all([this.fetchAlmBindings(), this.fetchBaseUrl()]);
await Promise.all([this.fetchAlmBindings(), this.fetchBaseUrl(), this.checkUserPermissions()]);

if (this.mounted) {
this.setState({ loading: false });
@@ -64,6 +69,22 @@ export class TutorialSelection extends React.PureComponent<Props, State> {
this.mounted = false;
}

checkUserPermissions = async () => {
const { component, currentUser } = this.props;

if (hasGlobalPermission(currentUser, Permissions.Scan)) {
this.setState({ currentUserCanScanProject: true });
return Promise.resolve();
}

const { projects } = await getScannableProjects();
this.setState({
currentUserCanScanProject: projects.find(p => p.key === component.key) !== undefined
});

return Promise.resolve();
};

fetchAlmBindings = async () => {
const { component, projectBinding } = this.props;

@@ -107,7 +128,7 @@ export class TutorialSelection extends React.PureComponent<Props, State> {
projectBinding,
willRefreshAutomatically
} = this.props;
const { almBinding, baseUrl, loading } = this.state;
const { almBinding, baseUrl, currentUserCanScanProject, loading } = this.state;

const selectedTutorial: TutorialModes | undefined = location.query?.selectedTutorial;

@@ -117,6 +138,7 @@ export class TutorialSelection extends React.PureComponent<Props, State> {
baseUrl={baseUrl}
component={component}
currentUser={currentUser}
currentUserCanScanProject={currentUserCanScanProject}
loading={loading}
onSelectTutorial={this.handleSelectTutorial}
projectBinding={projectBinding}

+ 7
- 0
server/sonar-web/src/main/js/components/tutorials/TutorialSelectionRenderer.tsx Datei anzeigen

@@ -24,6 +24,7 @@ import { getBaseUrl } from '../../helpers/system';
import { AlmKeys, AlmSettingsInstance, ProjectAlmBindingResponse } from '../../types/alm-settings';
import { Component } from '../../types/types';
import { LoggedInUser } from '../../types/users';
import { Alert } from '../ui/Alert';
import AzurePipelinesTutorial from './azure-pipelines/AzurePipelinesTutorial';
import BitbucketPipelinesTutorial from './bitbucket-pipelines/BitbucketPipelinesTutorial';
import GitHubActionTutorial from './github-action/GitHubActionTutorial';
@@ -37,6 +38,7 @@ export interface TutorialSelectionRendererProps {
baseUrl: string;
component: Component;
currentUser: LoggedInUser;
currentUserCanScanProject: boolean;
loading: boolean;
onSelectTutorial: (mode: TutorialModes) => void;
projectBinding?: ProjectAlmBindingResponse;
@@ -73,6 +75,7 @@ export default function TutorialSelectionRenderer(props: TutorialSelectionRender
baseUrl,
component,
currentUser,
currentUserCanScanProject,
loading,
projectBinding,
selectedTutorial,
@@ -83,6 +86,10 @@ export default function TutorialSelectionRenderer(props: TutorialSelectionRender
return <i className="spinner" />;
}

if (!currentUserCanScanProject) {
return <Alert variant="warning">{translate('onboarding.tutorial.no_scan_rights')}</Alert>;
}

let showGitHubActions = true;
let showGitLabCICD = true;
let showBitbucketPipelines = true;

+ 39
- 0
server/sonar-web/src/main/js/components/tutorials/__tests__/TutorialSelection-test.tsx Datei anzeigen

@@ -20,6 +20,7 @@
import { shallow } from 'enzyme';
import * as React from 'react';
import { getAlmSettingsNoCatch } from '../../../api/alm-settings';
import { getScannableProjects } from '../../../api/components';
import { getValues } from '../../../api/settings';
import {
mockAlmSettingsInstance,
@@ -29,6 +30,7 @@ import { mockComponent } from '../../../helpers/mocks/component';
import { mockLocation, mockLoggedInUser, mockRouter } from '../../../helpers/testMocks';
import { waitAndUpdate } from '../../../helpers/testUtils';
import { getHostUrl } from '../../../helpers/urls';
import { Permissions } from '../../../types/permissions';
import { SettingsKey } from '../../../types/settings';
import { TutorialSelection } from '../TutorialSelection';
import { TutorialModes } from '../types';
@@ -45,6 +47,10 @@ jest.mock('../../../api/settings', () => ({
getValues: jest.fn().mockResolvedValue([])
}));

jest.mock('../../../api/components', () => ({
getScannableProjects: jest.fn().mockResolvedValue({ projects: [] })
}));

beforeEach(jest.clearAllMocks);

it('should render correctly', () => {
@@ -109,6 +115,39 @@ it('should fetch the correct baseUrl', async () => {
expect(wrapper.state().baseUrl).toBe('http://host.url');
});

it("should correctly determine the user's permission", async () => {
const component = mockComponent({ key: 'bar', name: 'Bar' });
(getScannableProjects as jest.Mock)
.mockResolvedValueOnce({
projects: [
{ key: 'foo', name: 'Foo' },
{ key: component.key, name: component.name }
]
})
.mockResolvedValueOnce({ projects: [{ key: 'foo', name: 'Foo' }] });

// Global scan permission.
let wrapper = shallowRender({
component,
currentUser: mockLoggedInUser({ permissions: { global: [Permissions.Scan] } })
});
await waitAndUpdate(wrapper);
expect(wrapper.state().currentUserCanScanProject).toBe(true);
expect(getScannableProjects).not.toBeCalled();

// Project scan permission.
wrapper = shallowRender({ component });
await waitAndUpdate(wrapper);
expect(getScannableProjects).toBeCalled();
expect(wrapper.state().currentUserCanScanProject).toBe(true);

// No scan permission.
wrapper = shallowRender({ component });
await waitAndUpdate(wrapper);
expect(getScannableProjects).toBeCalledTimes(2);
expect(wrapper.state().currentUserCanScanProject).toBe(false);
});

function shallowRender(props: Partial<TutorialSelection['props']> = {}) {
return shallow<TutorialSelection>(
<TutorialSelection

+ 4
- 0
server/sonar-web/src/main/js/components/tutorials/__tests__/TutorialSelectionRenderer-test.tsx Datei anzeigen

@@ -74,6 +74,9 @@ it('should render correctly', () => {
projectBinding: mockProjectAzureBindingResponse()
})
).toMatchSnapshot('azure pipelines tutorial');
expect(shallowRender({ currentUserCanScanProject: false })).toMatchSnapshot(
'user has no scan permission'
);
});

it('should allow mode selection for Bitbucket', () => {
@@ -165,6 +168,7 @@ function shallowRender(props: Partial<TutorialSelectionRendererProps> = {}) {
baseUrl="http://localhost:9000"
component={mockComponent()}
currentUser={mockLoggedInUser()}
currentUserCanScanProject={true}
loading={false}
onSelectTutorial={jest.fn()}
{...props}

+ 1
- 0
server/sonar-web/src/main/js/components/tutorials/__tests__/__snapshots__/TutorialSelection-test.tsx.snap Datei anzeigen

@@ -34,6 +34,7 @@ exports[`should render correctly 1`] = `
"scmAccounts": Array [],
}
}
currentUserCanScanProject={false}
loading={true}
onSelectTutorial={[Function]}
/>

+ 8
- 0
server/sonar-web/src/main/js/components/tutorials/__tests__/__snapshots__/TutorialSelectionRenderer-test.tsx.snap Datei anzeigen

@@ -714,3 +714,11 @@ exports[`should render correctly: selection 1`] = `
</div>
</Fragment>
`;

exports[`should render correctly: user has no scan permission 1`] = `
<Alert
variant="warning"
>
onboarding.tutorial.no_scan_rights
</Alert>
`;

+ 12
- 4
server/sonar-web/src/main/js/components/tutorials/components/EditTokenModal.tsx Datei anzeigen

@@ -27,6 +27,7 @@ import SimpleModal from '../../../components/controls/SimpleModal';
import { Alert } from '../../../components/ui/Alert';
import DeferredSpinner from '../../../components/ui/DeferredSpinner';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { TokenType } from '../../../types/token';
import { Component } from '../../../types/types';
import { LoggedInUser } from '../../../types/users';
import { getUniqueTokenName } from '../utils';
@@ -73,9 +74,16 @@ export default class EditTokenModal extends React.PureComponent<Props, State> {
};

getNewToken = async () => {
const {
component: { key }
} = this.props;
const { tokenName } = this.state;

const { token } = await generateToken({ name: tokenName });
const { token } = await generateToken({
name: tokenName,
type: TokenType.Project,
projectKey: key
});

if (this.mounted) {
this.setState({
@@ -109,7 +117,7 @@ export default class EditTokenModal extends React.PureComponent<Props, State> {
render() {
const { loading, token, tokenName } = this.state;

const header = translate('onboarding.token.generate_token');
const header = translate('onboarding.token.generate_project_token');

return (
<SimpleModal header={header} onClose={this.props.onClose} onSubmit={this.props.onClose}>
@@ -122,8 +130,8 @@ export default class EditTokenModal extends React.PureComponent<Props, State> {
<div className="modal-body">
<p className="spacer-bottom">
<FormattedMessage
defaultMessage={translate('onboarding.token.text')}
id="onboarding.token.text"
defaultMessage={translate('onboarding.project_token.text')}
id="onboarding.project_token.text"
values={{
link: (
<Link target="_blank" to="/account/security">

+ 1
- 1
server/sonar-web/src/main/js/components/tutorials/components/__tests__/__snapshots__/EditTokenModal-test.tsx.snap Datei anzeigen

@@ -2,7 +2,7 @@

exports[`should render correctly 1`] = `
<SimpleModal
header="onboarding.token.generate_token"
header="onboarding.token.generate_project_token"
onClose={[MockFunction]}
onSubmit={[MockFunction]}
>

+ 1
- 0
server/sonar-web/src/main/js/components/tutorials/manual/ManualTutorial.tsx Datei anzeigen

@@ -66,6 +66,7 @@ export default class ManualTutorial extends React.PureComponent<Props, State> {

<TokenStep
currentUser={currentUser}
projectKey={component.key}
finished={Boolean(this.state.token)}
initialTokenName={`Analyze "${component.name}"`}
onContinue={this.handleTokenDone}

+ 17
- 7
server/sonar-web/src/main/js/components/tutorials/manual/TokenStep.tsx Datei anzeigen

@@ -25,7 +25,7 @@ import { Button, DeleteButton, SubmitButton } from '../../../components/controls
import Radio from '../../../components/controls/Radio';
import AlertSuccessIcon from '../../../components/icons/AlertSuccessIcon';
import { translate } from '../../../helpers/l10n';
import { UserToken } from '../../../types/token';
import { TokenType, UserToken } from '../../../types/token';
import { LoggedInUser } from '../../../types/users';
import AlertErrorIcon from '../../icons/AlertErrorIcon';
import Step from '../components/Step';
@@ -33,6 +33,7 @@ import { getUniqueTokenName } from '../utils';

interface Props {
currentUser: Pick<LoggedInUser, 'login'>;
projectKey: string;
finished: boolean;
initialTokenName?: string;
open: boolean;
@@ -101,16 +102,25 @@ export default class TokenStep extends React.PureComponent<Props, State> {
this.setState({ tokenName: event.target.value });
};

handleTokenGenerate = (event: React.FormEvent<HTMLFormElement>) => {
handleTokenGenerate = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const { tokenName } = this.state;
const { projectKey } = this.props;

if (tokenName) {
this.setState({ loading: true });
generateToken({ name: tokenName }).then(({ token }) => {
try {
const { token } = await generateToken({
name: tokenName,
type: TokenType.Project,
projectKey
});
if (this.mounted) {
this.setState({ loading: false, token });
}
}, this.stopLoading);
} catch (e) {
this.stopLoading();
}
}
};

@@ -164,10 +174,10 @@ export default class TokenStep extends React.PureComponent<Props, State> {
checked={this.state.selection === 'generate'}
onCheck={this.handleModeChange}
value="generate">
{translate('onboarding.token.generate_token')}
{translate('onboarding.token.generate_project_token')}
</Radio>
) : (
translate('onboarding.token.generate_token')
translate('onboarding.token.generate_project_token')
)}
{this.state.selection === 'generate' && (
<div className="big-spacer-top">
@@ -176,7 +186,7 @@ export default class TokenStep extends React.PureComponent<Props, State> {
autoFocus={true}
className="input-super-large spacer-right text-middle"
onChange={this.handleTokenNameChange}
placeholder={translate('onboarding.token.generate_token.placeholder')}
placeholder={translate('onboarding.token.generate_project_token.placeholder')}
required={true}
type="text"
value={this.state.tokenName || ''}

+ 18
- 41
server/sonar-web/src/main/js/components/tutorials/manual/__tests__/TokenStep-test.tsx Datei anzeigen

@@ -19,8 +19,8 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockLoggedInUser } from '../../../../helpers/testMocks';
import { change, click, submit, waitAndUpdate } from '../../../../helpers/testUtils';
import { LoggedInUser } from '../../../../types/users';
import TokenStep from '../TokenStep';

jest.mock('../../../../api/user-tokens', () => ({
@@ -29,19 +29,8 @@ jest.mock('../../../../api/user-tokens', () => ({
revokeToken: () => Promise.resolve()
}));

const currentUser: Pick<LoggedInUser, 'login'> = { login: 'user' };

it('generates token', async () => {
const wrapper = shallow(
<TokenStep
currentUser={currentUser}
finished={false}
onContinue={jest.fn()}
onOpen={jest.fn()}
open={true}
stepNumber={1}
/>
);
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
expect(wrapper.dive()).toMatchSnapshot();
change(wrapper.dive().find('input'), 'my token');
@@ -52,16 +41,7 @@ it('generates token', async () => {
});

it('revokes token', async () => {
const wrapper = shallow(
<TokenStep
currentUser={currentUser}
finished={false}
onContinue={jest.fn()}
onOpen={jest.fn()}
open={true}
stepNumber={1}
/>
);
const wrapper = shallowRender();
await new Promise(setImmediate);
wrapper.setState({ token: 'abcd1234', tokenName: 'my token' });
expect(wrapper.dive()).toMatchSnapshot();
@@ -77,16 +57,7 @@ it('revokes token', async () => {

it('continues', async () => {
const onContinue = jest.fn();
const wrapper = shallow(
<TokenStep
currentUser={currentUser}
finished={false}
onContinue={onContinue}
onOpen={jest.fn()}
open={true}
stepNumber={1}
/>
);
const wrapper = shallowRender({ onContinue });
await new Promise(setImmediate);
wrapper.setState({ token: 'abcd1234', tokenName: 'my token' });
click(wrapper.dive().find('[className="js-continue"]'));
@@ -95,18 +66,24 @@ it('continues', async () => {

it('uses existing token', async () => {
const onContinue = jest.fn();
const wrapper = shallow(
const wrapper = shallowRender({ onContinue });
await new Promise(setImmediate);
wrapper.setState({ existingToken: 'abcd1234', selection: 'use-existing' });
click(wrapper.dive().find('[className="js-continue"]'));
expect(onContinue).toBeCalledWith('abcd1234');
});

function shallowRender(props: Partial<TokenStep['props']> = {}) {
return shallow<TokenStep>(
<TokenStep
currentUser={currentUser}
currentUser={mockLoggedInUser({ login: 'user' })}
finished={false}
onContinue={onContinue}
onContinue={jest.fn()}
onOpen={jest.fn()}
open={true}
projectKey="foo"
stepNumber={1}
{...props}
/>
);
await new Promise(setImmediate);
wrapper.setState({ existingToken: 'abcd1234', selection: 'use-existing' });
click(wrapper.dive().find('[className="js-continue"]'));
expect(onContinue).toBeCalledWith('abcd1234');
});
}

+ 1
- 0
server/sonar-web/src/main/js/components/tutorials/manual/__tests__/__snapshots__/ManualTutorial-test.tsx.snap Datei anzeigen

@@ -33,6 +33,7 @@ exports[`renders correctly: default 1`] = `
onContinue={[Function]}
onOpen={[Function]}
open={true}
projectKey="my-project"
stepNumber={1}
/>
<ProjectAnalysisStep

+ 6
- 6
server/sonar-web/src/main/js/components/tutorials/manual/__tests__/__snapshots__/TokenStep-test.tsx.snap Datei anzeigen

@@ -29,7 +29,7 @@ exports[`generates token 1`] = `
onCheck={[Function]}
value="generate"
>
onboarding.token.generate_token
onboarding.token.generate_project_token
</Radio>
<div
className="big-spacer-top"
@@ -41,7 +41,7 @@ exports[`generates token 1`] = `
autoFocus={true}
className="input-super-large spacer-right text-middle"
onChange={[Function]}
placeholder="onboarding.token.generate_token.placeholder"
placeholder="onboarding.token.generate_project_token.placeholder"
required={true}
type="text"
value=""
@@ -121,7 +121,7 @@ exports[`generates token 2`] = `
onCheck={[Function]}
value="generate"
>
onboarding.token.generate_token
onboarding.token.generate_project_token
</Radio>
<div
className="big-spacer-top"
@@ -133,7 +133,7 @@ exports[`generates token 2`] = `
autoFocus={true}
className="input-super-large spacer-right text-middle"
onChange={[Function]}
placeholder="onboarding.token.generate_token.placeholder"
placeholder="onboarding.token.generate_project_token.placeholder"
required={true}
type="text"
value="my token"
@@ -437,7 +437,7 @@ exports[`revokes token 3`] = `
onCheck={[Function]}
value="generate"
>
onboarding.token.generate_token
onboarding.token.generate_project_token
</Radio>
<div
className="big-spacer-top"
@@ -449,7 +449,7 @@ exports[`revokes token 3`] = `
autoFocus={true}
className="input-super-large spacer-right text-middle"
onChange={[Function]}
placeholder="onboarding.token.generate_token.placeholder"
placeholder="onboarding.token.generate_project_token.placeholder"
required={true}
type="text"
value=""

+ 6
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties Datei anzeigen

@@ -3441,12 +3441,15 @@ onboarding.create_project.gitlab.no_projects=No projects could be fetched from G
onboarding.create_project.gitlab.link=See on GitLab

onboarding.token.header=Provide a token
onboarding.token.text=The token is used to identify you when an analysis is performed. If it has been compromised, you can revoke it at any point of time in your {link}.
onboarding.token.text=The token is used to identify you when an analysis is performed. If it has been compromised, you can revoke it at any point in time in your {link}.
onboarding.project_token.text=The project token is used to identify you when an analysis is performed. If it has been compromised, you can revoke it at any point in time in your {link}.
onboarding.token.text.user_account=user account
onboarding.token.generate=Generate
onboarding.token.placeholder=Enter a name for your token
onboarding.token.generate_token=Generate a token
onboarding.token.generate_token.placeholder=Enter a name for your token
onboarding.token.generate_project_token=Generate a project token
onboarding.token.generate_project_token.placeholder=Enter a name for your project token
onboarding.token.use_existing_token=Use existing token
onboarding.token.use_existing_token.placeholder=Enter your existing token
onboarding.token.use_existing_token.label=Existing token value
@@ -3946,6 +3949,8 @@ onboarding.tutorial.with.azure_pipelines.BranchAnalysis.continous_integration.no
onboarding.tutorial.with.azure_pipelines.BranchAnalysis.continous_integration.no_branches.sentence.continuous_integration=Enable continuous integration
onboarding.tutorial.with.azure_pipelines.BranchAnalysis.branch_protection=To make sure your Pull Requests are analyzed automatically and aren't merged when they're failing their quality gate, check out the {link}.
onboarding.tutorial.with.azure_pipelines.BranchAnalysis.branch_protection.link=documentation

onboarding.tutorial.no_scan_rights=You do not have permission to analyze this project. Please contact the project administrator.
#------------------------------------------------------------------------------
#
# BRANCHES

Laden…
Abbrechen
Speichern