From: Wouter Admiraal Date: Thu, 13 Feb 2020 07:56:40 +0000 (+0100) Subject: SONAR-13004 Validate Bitbucket personal access token when saving X-Git-Tag: 8.2.0.32929~53 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=6585cb549530ef4ecfbefc28670799cf7517341a;p=sonarqube.git SONAR-13004 Validate Bitbucket personal access token when saving --- diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketImportRepositoryForm.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketImportRepositoryForm.tsx index 017c2acbb46..2f999613558 100644 --- a/server/sonar-web/src/main/js/apps/create/project/BitbucketImportRepositoryForm.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/BitbucketImportRepositoryForm.tsx @@ -18,6 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Link } from 'react-router'; import SearchBox from 'sonar-ui-common/components/controls/SearchBox'; import { Alert } from 'sonar-ui-common/components/ui/Alert'; import { translate } from 'sonar-ui-common/helpers/l10n'; @@ -28,6 +30,7 @@ import { } from '../../../types/alm-integration'; import BitbucketRepositories from './BitbucketRepositories'; import BitbucketSearchResults from './BitbucketSearchResults'; +import { CreateProjectModes } from './types'; export interface BitbucketImportRepositoryFormProps { disableRepositories: boolean; @@ -53,7 +56,21 @@ export default function BitbucketImportRepositoryForm(props: BitbucketImportRepo if (projects.length === 0) { return ( - {translate('onboarding.create_project.no_bbs_projects')} + + {translate('onboarding.create_project.update_your_token')} + + ) + }} + /> ); } diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketPersonalAccessTokenForm.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketPersonalAccessTokenForm.tsx index 8d5a07e3161..4f6826be68a 100644 --- a/server/sonar-web/src/main/js/apps/create/project/BitbucketPersonalAccessTokenForm.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/BitbucketPersonalAccessTokenForm.tsx @@ -32,6 +32,7 @@ export interface BitbucketPersonalAccessTokenFormProps { bitbucketSetting: AlmSettingsInstance; onPersonalAccessTokenCreate: (token: string) => void; submitting?: boolean; + validationFailed: boolean; } export default function BitbucketPersonalAccessTokenForm( @@ -39,17 +40,24 @@ export default function BitbucketPersonalAccessTokenForm( ) { const { bitbucketSetting: { url }, - submitting = false + submitting = false, + validationFailed } = props; - const [personalAccessToken, setPersonalAccessToken] = React.useState(''); - const isValid = personalAccessToken.length > 0; + const [touched, setTouched] = React.useState(false); + + React.useEffect(() => { + setTouched(false); + }, [submitting]); + + const isInvalid = validationFailed && !touched; return (
) => { e.preventDefault(); - props.onPersonalAccessTokenCreate(personalAccessToken); + const value = new FormData(e.currentTarget).get('personal_access_token') as string; + props.onPersonalAccessTokenCreate(value); }}>

{translate('onboarding.create_project.grant_access_to_bbs.title')}

@@ -57,28 +65,30 @@ export default function BitbucketPersonalAccessTokenForm(

) => - setPersonalAccessToken(e.currentTarget.value) - } + name="personal_access_token" + onChange={() => { + setTouched(true); + }} type="text" - value={personalAccessToken} /> - {translate('save')} + + {translate('save')} + diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectAccordion.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectAccordion.tsx index 66a13c5c350..9d9ce31adf4 100644 --- a/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectAccordion.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectAccordion.tsx @@ -19,16 +19,17 @@ */ import * as classNames from 'classnames'; import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; import { Link } from 'react-router'; import BoxedGroupAccordion from 'sonar-ui-common/components/controls/BoxedGroupAccordion'; import Radio from 'sonar-ui-common/components/controls/Radio'; -import Tooltip from 'sonar-ui-common/components/controls/Tooltip'; import CheckIcon from 'sonar-ui-common/components/icons/CheckIcon'; import { Alert } from 'sonar-ui-common/components/ui/Alert'; import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; import { colors } from '../../../app/theme'; import { getProjectUrl } from '../../../helpers/urls'; import { BitbucketProject, BitbucketRepository } from '../../../types/alm-integration'; +import { CreateProjectModes } from './types'; export interface BitbucketProjectAccordionProps { disableRepositories: boolean; @@ -73,7 +74,23 @@ export default function BitbucketProjectAccordion(props: BitbucketProjectAccordi {open && (
{repositoryCount === 0 && ( - {translate('onboarding.create_project.no_bbs_repos')} + + + {translate('onboarding.create_project.update_your_token')} + + ) + }} + /> + )} {repositories.map(repo => @@ -84,11 +101,9 @@ export default function BitbucketProjectAccordion(props: BitbucketProjectAccordi
- - - {repo.name} - - + + {repo.name} +
{translate('onboarding.create_project.repository_imported')}
@@ -107,9 +122,9 @@ export default function BitbucketProjectAccordion(props: BitbucketProjectAccordi key={repo.id} onCheck={() => props.onSelectRepository(repo)} value={String(repo.id)}> - - {repo.name} - + + {repo.name} + ) )} diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreate.tsx index e4f489c0eae..ef6d825005a 100644 --- a/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreate.tsx @@ -55,6 +55,7 @@ interface State { searchResults?: BitbucketRepository[]; selectedRepository?: BitbucketRepository; submittingToken?: boolean; + tokenValidationFailed: boolean; } export class BitbucketProjectCreate extends React.PureComponent { @@ -68,7 +69,8 @@ export class BitbucketProjectCreate extends React.PureComponent { bitbucketSetting: props.bitbucketSettings[0], importing: false, loading: false, - searching: false + searching: false, + tokenValidationFailed: false }; } @@ -79,8 +81,9 @@ export class BitbucketProjectCreate extends React.PureComponent { componentDidUpdate(prevProps: Props) { if (prevProps.bitbucketSettings.length === 0 && this.props.bitbucketSettings.length > 0) { - this.setState({ bitbucketSetting: this.props.bitbucketSettings[0] }); - this.fetchInitialData(); + this.setState({ bitbucketSetting: this.props.bitbucketSettings[0] }, () => + this.fetchInitialData() + ); } } @@ -169,12 +172,15 @@ export class BitbucketProjectCreate extends React.PureComponent { return; } - this.setState({ submittingToken: true }); + this.setState({ submittingToken: true, tokenValidationFailed: false }); setAlmPersonalAccessToken(bitbucketSetting.key, token) - .then(() => { + .then(this.checkPersonalAccessToken) + .then(patIsValid => { if (this.mounted) { - this.setState({ submittingToken: false }); - this.fetchInitialData(); + this.setState({ submittingToken: false, patIsValid, tokenValidationFailed: !patIsValid }); + if (patIsValid) { + this.fetchInitialData(); + } } }) .catch(() => { @@ -241,7 +247,7 @@ export class BitbucketProjectCreate extends React.PureComponent { }; render() { - const { canAdmin, loadingBindings } = this.props; + const { canAdmin, loadingBindings, location } = this.props; const { bitbucketSetting, importing, @@ -252,7 +258,8 @@ export class BitbucketProjectCreate extends React.PureComponent { searching, searchResults, selectedRepository, - submittingToken + submittingToken, + tokenValidationFailed } = this.state; return ( @@ -271,8 +278,9 @@ export class BitbucketProjectCreate extends React.PureComponent { searchResults={searchResults} searching={searching} selectedRepository={selectedRepository} - showPersonalAccessTokenForm={!patIsValid} + showPersonalAccessTokenForm={!patIsValid || Boolean(location.query.resetPat)} submittingToken={submittingToken} + tokenValidationFailed={tokenValidationFailed} /> ); } diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreateRenderer.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreateRenderer.tsx index 8b9092d484e..94afa8a8d68 100644 --- a/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreateRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreateRenderer.tsx @@ -53,6 +53,7 @@ export interface BitbucketProjectCreateRendererProps { selectedRepository?: BitbucketRepository; showPersonalAccessTokenForm?: boolean; submittingToken?: boolean; + tokenValidationFailed: boolean; } export default function BitbucketProjectCreateRenderer(props: BitbucketProjectCreateRendererProps) { @@ -67,7 +68,8 @@ export default function BitbucketProjectCreateRenderer(props: BitbucketProjectCr searching, searchResults, showPersonalAccessTokenForm, - submittingToken + submittingToken, + tokenValidationFailed } = props; return ( @@ -133,6 +135,7 @@ export default function BitbucketProjectCreateRenderer(props: BitbucketProjectCr bitbucketSetting={bitbucketSetting} onPersonalAccessTokenCreate={props.onPersonalAccessTokenCreate} submitting={submittingToken} + validationFailed={tokenValidationFailed} /> ) : ( { expect(shallowRender()).toMatchSnapshot('default'); expect(shallowRender({ submitting: true })).toMatchSnapshot('submitting'); + expect(shallowRender({ validationFailed: true })).toMatchSnapshot('validation failed'); }); -it('should correctly handle form interactions', async () => { +it('should correctly handle form interactions', () => { const onPersonalAccessTokenCreate = jest.fn(); const wrapper = shallowRender({ onPersonalAccessTokenCreate }); @@ -46,8 +47,14 @@ it('should correctly handle form interactions', async () => { // Expect correct calls to be made when submitting. submit(wrapper.find('form')); - await waitAndUpdate(wrapper); expect(onPersonalAccessTokenCreate).toBeCalled(); + + // If validation fails, we toggle the submitting flag and call useEffect() + // to set the `touched` flag to false again. Trigger a re-render, and mock + // useEffect(). This should de-activate the submit button again. + jest.spyOn(React, 'useEffect').mockImplementationOnce(f => f()); + wrapper.setProps({ submitting: false }); + expect(wrapper.find(SubmitButton).prop('disabled')).toBe(true); }); function shallowRender(props: Partial = {}) { @@ -58,6 +65,7 @@ function shallowRender(props: Partial = { url: 'http://www.example.com' })} onPersonalAccessTokenCreate={jest.fn()} + validationFailed={false} {...props} /> ); diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreate-test.tsx index 2f999c1d91b..6d1405bb939 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreate-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreate-test.tsx @@ -92,10 +92,17 @@ it('should correctly handle an invalid PAT', async () => { expect(wrapper.state().patIsValid).toBe(false); }); -it('should correctly handle setting a new PAT', () => { +it('should correctly handle setting a new PAT', async () => { const wrapper = shallowRender(); wrapper.instance().handlePersonalAccessTokenCreate('token'); expect(setAlmPersonalAccessToken).toBeCalledWith('foo', 'token'); + expect(wrapper.state().submittingToken).toBe(true); + + (checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce(false); + await waitAndUpdate(wrapper); + expect(checkPersonalAccessTokenIsValid).toBeCalled(); + expect(wrapper.state().submittingToken).toBe(false); + expect(wrapper.state().tokenValidationFailed).toBe(true); }); it('should correctly fetch projects and repos', async () => { diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreateRenderer-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreateRenderer-test.tsx index 9ef88e6a9ef..964e30b4d9a 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreateRenderer-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreateRenderer-test.tsx @@ -60,6 +60,7 @@ function shallowRender(props: Partial = {}) projectRepositories={{ foo: { allShown: true, repositories: [mockBitbucketRepository()] } }} projects={[mockBitbucketProject({ key: 'foo' })]} searching={false} + tokenValidationFailed={false} {...props} /> ); diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketImportRepositoryForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketImportRepositoryForm-test.tsx.snap index cedc3c4e22d..6ba025c784e 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketImportRepositoryForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketImportRepositoryForm-test.tsx.snap @@ -56,7 +56,29 @@ exports[`should render correctly: no projects 1`] = ` className="spacer-top" variant="warning" > - onboarding.create_project.no_bbs_projects + + onboarding.create_project.update_your_token + , + } + } + /> `; diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketPersonalAccessTokenForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketPersonalAccessTokenForm-test.tsx.snap index f7dd160502f..18a3bf5e37b 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketPersonalAccessTokenForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketPersonalAccessTokenForm-test.tsx.snap @@ -29,9 +29,9 @@ exports[`should render correctly: default 1`] = ` className="input-super-large" id="personal_access_token" minLength={1} + name="personal_access_token" onChange={[Function]} type="text" - value="" />
`; + +exports[`should render correctly: validation failed 1`] = ` +
+
+

+ onboarding.create_project.grant_access_to_bbs.title +

+

+ onboarding.create_project.grant_access_to_bbs.help +

+ + + + + save + + + + +

+ onboarding.create_project.pat_help.title +

+

+ onboarding.create_project.pat_help.bbs_help_1 +

+ +

+ onboarding.create_project.pat_help.bbs_help_2 +

+
    +
  • + + onboarding.create_project.pat_help.read_permission + , + } + } + /> +
  • +
  • + + onboarding.create_project.pat_help.read_permission + , + } + } + /> +
  • +
+
+
+`; diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectAccordion-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectAccordion-test.tsx.snap index f1fc7af0d46..ced072128a2 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectAccordion-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectAccordion-test.tsx.snap @@ -36,15 +36,12 @@ exports[`should render correctly: default 1`] = ` onCheck={[Function]} value="1" > - - - Repo - - + Repo +
- - - - Bar - - - + } + > + Bar + +
onboarding.create_project.repository_imported @@ -114,15 +109,12 @@ exports[`should render correctly: disable options 1`] = ` onCheck={[Function]} value="1" > - - - Repo - - + Repo +
- - - - Bar - - - + } + > + Bar + +
onboarding.create_project.repository_imported @@ -192,15 +182,12 @@ exports[`should render correctly: no click handler 1`] = ` onCheck={[Function]} value="1" > - - - Repo - - + Repo +
- - - - Bar - - - + } + > + Bar + +
onboarding.create_project.repository_imported @@ -266,7 +251,29 @@ exports[`should render correctly: no repos 1`] = ` - onboarding.create_project.no_bbs_repos + + onboarding.create_project.update_your_token + , + } + } + />
@@ -294,15 +301,12 @@ exports[`should render correctly: not showing all repos 1`] = ` onCheck={[Function]} value="1" > - - - Repo - - + Repo +
- - - - Bar - - - + } + > + Bar + +
onboarding.create_project.repository_imported @@ -377,15 +379,12 @@ exports[`should render correctly: selected repo 1`] = ` onCheck={[Function]} value="1" > - - - Repo - - + Repo +
- - - - Bar - - - + } + > + Bar + +
onboarding.create_project.repository_imported diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreate-test.tsx.snap index 1b5c6ad18e4..a8e54b76f9a 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreate-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreate-test.tsx.snap @@ -17,5 +17,6 @@ exports[`should render correctly 1`] = ` onSelectRepository={[Function]} searching={false} showPersonalAccessTokenForm={true} + tokenValidationFailed={false} /> `; diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreateRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreateRenderer-test.tsx.snap index 895cf6c3de4..57c13175014 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreateRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreateRenderer-test.tsx.snap @@ -316,6 +316,7 @@ exports[`should render correctly: pat form 1`] = ` } } onPersonalAccessTokenCreate={[MockFunction]} + validationFailed={false} /> `; 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 1dd1d65c54a..2fc4cb6a01f 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -3114,6 +3114,7 @@ onboarding.create_project.bbs_not_configured=This feature isn't available onboarding.create_project.no_bbs_binding=You must have exactly at least 1 Bitbucket Server instance configured in order to use this method, but none were found. Either create the project manually, or contact your system administrator. onboarding.create_project.no_bbs_binding.admin=You must have exactly at least 1 Bitbucket Server instance configured in order to use this method. You can configure instances under {url}. onboarding.create_project.enter_pat=Enter personal access token +onboarding.create_project.pat_incorrect=Your personal access token failed to validate. onboarding.create_project.pat_help.title=How to create a personal access token? onboarding.create_project.pat_help.bbs_help_1=Click the following link to generate a token in Bitbucket Server, and copy-paste it into the personal access token field. onboarding.create_project.pat_help.bbs_help_2=Set a name, for example "SonarQube", and select the following permissions: @@ -3121,10 +3122,9 @@ onboarding.create_project.pat_help.link=Create personal access token onboarding.create_project.pat_help.bbs_permission_projects=Projects: {perm} onboarding.create_project.pat_help.bbs_permission_repos=Repositories: {perm} onboarding.create_project.pat_help.read_permission=Read -onboarding.create_project.error_fetching_bbs_projects=There was an error fetching the projects from Bitbucket Server. Contact your system administrator, or check your personal access token. -onboarding.create_project.error_fetching_bbs_repos=There was an error fetching the repositories from Bitbucket Server. Contact your system administrator, or check your personal access token. -onboarding.create_project.no_bbs_projects=No projects could be fetched from Bitbucket Server. Contact your system administrator, or check your personal access token. -onboarding.create_project.no_bbs_repos=No repositories were found for this project. Contact your system administrator, or check your personal access token. +onboarding.create_project.no_bbs_projects=No projects could be fetched from Bitbucket Server. Contact your system administrator, or {link}. +onboarding.create_project.no_bbs_repos=No repositories were found for this project. Contact your system administrator, or {link}. +onboarding.create_project.update_your_token=update your personal access token onboarding.create_project.no_bbs_repos.filter=No repositories match your filter. onboarding.create_project.only_showing_X_first_repos=We're only displaying the first {0} repositories. If you're looking for a repository that's not in this list, use the search above. onboarding.create_project.import_selected_repo=Set up selected repository