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';
interface State {
almBinding?: AlmSettingsInstance;
+ currentUserCanScanProject: boolean;
baseUrl: string;
loading: boolean;
}
export class TutorialSelection extends React.PureComponent<Props, State> {
mounted = false;
state: State = {
+ currentUserCanScanProject: false,
baseUrl: getHostUrl(),
loading: true
};
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 });
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;
projectBinding,
willRefreshAutomatically
} = this.props;
- const { almBinding, baseUrl, loading } = this.state;
+ const { almBinding, baseUrl, currentUserCanScanProject, loading } = this.state;
const selectedTutorial: TutorialModes | undefined = location.query?.selectedTutorial;
baseUrl={baseUrl}
component={component}
currentUser={currentUser}
+ currentUserCanScanProject={currentUserCanScanProject}
loading={loading}
onSelectTutorial={this.handleSelectTutorial}
projectBinding={projectBinding}
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';
baseUrl: string;
component: Component;
currentUser: LoggedInUser;
+ currentUserCanScanProject: boolean;
loading: boolean;
onSelectTutorial: (mode: TutorialModes) => void;
projectBinding?: ProjectAlmBindingResponse;
baseUrl,
component,
currentUser,
+ currentUserCanScanProject,
loading,
projectBinding,
selectedTutorial,
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;
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,
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';
getValues: jest.fn().mockResolvedValue([])
}));
+jest.mock('../../../api/components', () => ({
+ getScannableProjects: jest.fn().mockResolvedValue({ projects: [] })
+}));
+
beforeEach(jest.clearAllMocks);
it('should render correctly', () => {
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
projectBinding: mockProjectAzureBindingResponse()
})
).toMatchSnapshot('azure pipelines tutorial');
+ expect(shallowRender({ currentUserCanScanProject: false })).toMatchSnapshot(
+ 'user has no scan permission'
+ );
});
it('should allow mode selection for Bitbucket', () => {
baseUrl="http://localhost:9000"
component={mockComponent()}
currentUser={mockLoggedInUser()}
+ currentUserCanScanProject={true}
loading={false}
onSelectTutorial={jest.fn()}
{...props}
"scmAccounts": Array [],
}
}
+ currentUserCanScanProject={false}
loading={true}
onSelectTutorial={[Function]}
/>
</div>
</Fragment>
`;
+
+exports[`should render correctly: user has no scan permission 1`] = `
+<Alert
+ variant="warning"
+>
+ onboarding.tutorial.no_scan_rights
+</Alert>
+`;
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';
};
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({
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}>
<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">
exports[`should render correctly 1`] = `
<SimpleModal
- header="onboarding.token.generate_token"
+ header="onboarding.token.generate_project_token"
onClose={[MockFunction]}
onSubmit={[MockFunction]}
>
<TokenStep
currentUser={currentUser}
+ projectKey={component.key}
finished={Boolean(this.state.token)}
initialTokenName={`Analyze "${component.name}"`}
onContinue={this.handleTokenDone}
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';
interface Props {
currentUser: Pick<LoggedInUser, 'login'>;
+ projectKey: string;
finished: boolean;
initialTokenName?: string;
open: boolean;
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();
+ }
}
};
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">
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 || ''}
*/
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', () => ({
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');
});
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();
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"]'));
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');
-});
+}
onContinue={[Function]}
onOpen={[Function]}
open={true}
+ projectKey="my-project"
stepNumber={1}
/>
<ProjectAnalysisStep
onCheck={[Function]}
value="generate"
>
- onboarding.token.generate_token
+ onboarding.token.generate_project_token
</Radio>
<div
className="big-spacer-top"
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=""
onCheck={[Function]}
value="generate"
>
- onboarding.token.generate_token
+ onboarding.token.generate_project_token
</Radio>
<div
className="big-spacer-top"
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"
onCheck={[Function]}
value="generate"
>
- onboarding.token.generate_token
+ onboarding.token.generate_project_token
</Radio>
<div
className="big-spacer-top"
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=""
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
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