diff --git a/.changeset/fresh-hounds-argue.md b/.changeset/fresh-hounds-argue.md new file mode 100644 index 0000000000..9291a99659 --- /dev/null +++ b/.changeset/fresh-hounds-argue.md @@ -0,0 +1,6 @@ +--- +'@backstage/integration': minor +'@backstage/plugin-scaffolder-backend': minor +--- + +Add support for Basic Auth for Bitbucket Server. diff --git a/.changeset/metal-points-itch.md b/.changeset/metal-points-itch.md new file mode 100644 index 0000000000..752f52c269 --- /dev/null +++ b/.changeset/metal-points-itch.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +'@backstage/backend-common': patch +--- + +Add support for Bearer Authorization header / token-based auth at Git commands. diff --git a/.changeset/short-trains-roll.md b/.changeset/short-trains-roll.md new file mode 100644 index 0000000000..709d534d57 --- /dev/null +++ b/.changeset/short-trains-roll.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +User Bearer Authorization header at Git commands with token-based auth at Bitbucket Server. diff --git a/docs/integrations/bitbucket/locations.md b/docs/integrations/bitbucket/locations.md index 878030a29c..4fbb96bb94 100644 --- a/docs/integrations/bitbucket/locations.md +++ b/docs/integrations/bitbucket/locations.md @@ -26,6 +26,16 @@ integrations: token: ${BITBUCKET_SERVER_TOKEN} ``` +or with Basic Auth + +```yaml +integrations: + bitbucketServer: + - host: bitbucket.company.com + username: ${BITBUCKET_SERVER_USERNAME} + password: ${BITBUCKET_SERVER_PASSWORD} +``` + Directly under the `bitbucketServer` key is a list of provider configurations, where you can list the Bitbucket Server providers you want to fetch data from. Each entry is a structure with the following elements: @@ -34,5 +44,9 @@ a structure with the following elements: - `token` (optional): An [personal access token](https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html) as expected by Bitbucket Server. +- `username` (optional): + use for [Basic Auth](https://developer.atlassian.com/server/bitbucket/how-tos/command-line-rest/#authentication) for Bitbucket Server. +- `password` (optional): + use for [Basic Auth](https://developer.atlassian.com/server/bitbucket/how-tos/command-line-rest/#authentication) for Bitbucket Server. - `apiBaseUrl` (optional): The URL of the Bitbucket Server API. For self-hosted installations, it is commonly at `https:///rest/api/1.0`. diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index f2b60ae19e..87bc3fca87 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -373,6 +373,7 @@ export class Git { static fromAuth: (options: { username?: string; password?: string; + token?: string; logger?: Logger; }) => Git; // (undocumented) diff --git a/packages/backend-common/src/scm/git.test.ts b/packages/backend-common/src/scm/git.test.ts index 40814607a9..e9c66a896e 100644 --- a/packages/backend-common/src/scm/git.test.ts +++ b/packages/backend-common/src/scm/git.test.ts @@ -26,6 +26,7 @@ describe('Git', () => { beforeEach(() => { jest.resetAllMocks(); }); + describe('add', () => { it('should call isomorphic-git add with the correct arguments', async () => { const git = Git.fromAuth({}); @@ -146,6 +147,33 @@ describe('Git', () => { onAuth: expect.any(Function), }); }); + + it('should call isomorphic-git with the correct arguments (Bearer)', async () => { + const url = 'http://github.com/some/repo'; + const dir = '/some/mock/dir'; + const auth = { + token: 'test', + }; + const git = Git.fromAuth(auth); + + await git.clone({ url, dir }); + + expect(isomorphic.clone).toHaveBeenCalledWith({ + fs, + http, + url, + dir, + singleBranch: true, + depth: 1, + onProgress: expect.any(Function), + headers: { + Authorization: 'Bearer test', + 'user-agent': 'git/@isomorphic-git', + }, + onAuth: expect.any(Function), + }); + }); + it('should pass a function that returns the authorization as the onAuth handler', async () => { const url = 'http://github.com/some/repo'; const dir = '/some/mock/dir'; @@ -164,7 +192,7 @@ describe('Git', () => { expect(onAuth()).toEqual(auth); }); - it('should propogate the data from the error handler', async () => { + it('should propagate the data from the error handler', async () => { const url = 'http://github.com/some/repo'; const dir = '/some/mock/dir'; const auth = { @@ -234,6 +262,31 @@ describe('Git', () => { onAuth: expect.any(Function), }); }); + + it('should call isomorphic-git with the correct arguments (Bearer)', async () => { + const remote = 'http://github.com/some/repo'; + const dir = '/some/mock/dir'; + const auth = { + token: 'test', + }; + const git = Git.fromAuth(auth); + + await git.fetch({ remote, dir }); + + expect(isomorphic.fetch).toHaveBeenCalledWith({ + fs, + http, + remote, + dir, + onProgress: expect.any(Function), + headers: { + Authorization: 'Bearer test', + 'user-agent': 'git/@isomorphic-git', + }, + onAuth: expect.any(Function), + }); + }); + it('should pass a function that returns the authorization as the onAuth handler', async () => { const remote = 'http://github.com/some/repo'; const dir = '/some/mock/dir'; @@ -252,7 +305,7 @@ describe('Git', () => { expect(onAuth()).toEqual(auth); }); - it('should propogate the data from the error handler', async () => { + it('should propagate the data from the error handler', async () => { const remote = 'http://github.com/some/repo'; const dir = '/some/mock/dir'; const auth = { @@ -348,6 +401,35 @@ describe('Git', () => { onAuth: expect.any(Function), }); }); + + it('should call isomorphic-git with the correct arguments (Bearer)', async () => { + const remote = 'origin'; + const dir = '/some/mock/dir'; + const auth = { + token: 'test', + }; + const git = Git.fromAuth(auth); + const remoteRef = 'master'; + const force = true; + + await git.push({ dir, remote, remoteRef, force }); + + expect(isomorphic.push).toHaveBeenCalledWith({ + fs, + http, + remote, + dir, + remoteRef, + force, + onProgress: expect.any(Function), + headers: { + Authorization: 'Bearer test', + 'user-agent': 'git/@isomorphic-git', + }, + onAuth: expect.any(Function), + }); + }); + it('should call isomorphic-git with remoteRef parameter', async () => { const remote = 'origin'; const remoteRef = 'refs/for/master'; @@ -373,6 +455,7 @@ describe('Git', () => { onAuth: expect.any(Function), }); }); + it('should pass a function that returns the authorization as the onAuth handler', async () => { const remote = 'origin'; const dir = '/some/mock/dir'; @@ -393,7 +476,7 @@ describe('Git', () => { expect(onAuth()).toEqual(auth); }); - it('should propogate the data from the error handler', async () => { + it('should propagate the data from the error handler', async () => { const remote = 'origin'; const dir = '/some/mock/dir'; const auth = { diff --git a/packages/backend-common/src/scm/git.ts b/packages/backend-common/src/scm/git.ts index c8e26420ec..8a72c7af89 100644 --- a/packages/backend-common/src/scm/git.ts +++ b/packages/backend-common/src/scm/git.ts @@ -24,13 +24,17 @@ import fs from 'fs-extra'; import { Logger } from 'winston'; /* -provider username password -GitHub 'x-access-token' token -BitBucket 'x-token-auth' token -GitLab 'oauth2' token +provider username password +Azure 'notempty' token +Bitbucket Cloud 'x-token-auth' token +Bitbucket Server username password or token +GitHub 'x-access-token' token +GitLab 'oauth2' token + From : https://isomorphic-git.org/docs/en/onAuth with fix for GitHub -Azure 'notempty' token +Or token provided as `token` for Bearer auth header +instead of Basic Auth (e.g., Bitbucket Server). */ /** @@ -39,13 +43,23 @@ Azure 'notempty' token * @public */ export class Git { + private readonly headers: { + [x: string]: string; + }; + private constructor( private readonly config: { username?: string; password?: string; + token?: string; logger?: Logger; }, - ) {} + ) { + this.headers = { + 'user-agent': 'git/@isomorphic-git', + ...(config.token ? { Authorization: `Bearer ${config.token}` } : {}), + }; + } async add(options: { dir: string; filepath: string }): Promise { const { dir, filepath } = options; @@ -116,9 +130,7 @@ export class Git { depth: depth ?? 1, noCheckout, onProgress: this.onProgressHandler(), - headers: { - 'user-agent': 'git/@isomorphic-git', - }, + headers: this.headers, onAuth: this.onAuth, }); } catch (ex) { @@ -155,7 +167,7 @@ export class Git { dir, remote, onProgress: this.onProgressHandler(), - headers: { 'user-agent': 'git/@isomorphic-git' }, + headers: this.headers, onAuth: this.onAuth, }); } catch (ex) { @@ -222,9 +234,7 @@ export class Git { onProgress: this.onProgressHandler(), remoteRef, force, - headers: { - 'user-agent': 'git/@isomorphic-git', - }, + headers: this.headers, remote, onAuth: this.onAuth, }); @@ -290,9 +300,10 @@ export class Git { static fromAuth = (options: { username?: string; password?: string; + token?: string; logger?: Logger; }) => { - const { username, password, logger } = options; - return new Git({ username, password, logger }); + const { username, password, token, logger } = options; + return new Git({ username, password, token, logger }); }; } diff --git a/packages/integration/api-report.md b/packages/integration/api-report.md index 8d8d017095..cf52bec65b 100644 --- a/packages/integration/api-report.md +++ b/packages/integration/api-report.md @@ -150,6 +150,8 @@ export type BitbucketServerIntegrationConfig = { host: string; apiBaseUrl: string; token?: string; + username?: string; + password?: string; }; // @public diff --git a/packages/integration/config.d.ts b/packages/integration/config.d.ts index 22acc5cd70..7c897d637a 100644 --- a/packages/integration/config.d.ts +++ b/packages/integration/config.d.ts @@ -92,6 +92,16 @@ export interface Config { * @visibility secret */ token?: string; + /** + * Username used to authenticate requests with Basic Auth. + * @visibility secret + */ + username?: string; + /** + * Password (or token as password) used to authenticate requests with Basic Auth. + * @visibility secret + */ + password?: string; /** * The base url for the Bitbucket Server API, for example https:///rest/api/1.0 * @visibility frontend diff --git a/packages/integration/src/bitbucketServer/config.test.ts b/packages/integration/src/bitbucketServer/config.test.ts index 83c28f5e9d..985165f23f 100644 --- a/packages/integration/src/bitbucketServer/config.test.ts +++ b/packages/integration/src/bitbucketServer/config.test.ts @@ -55,7 +55,7 @@ describe('readBitbucketServerIntegrationConfig', () => { ); } - it('reads all values', () => { + it('reads all values, token', () => { const output = readBitbucketServerIntegrationConfig( buildConfig({ host: 'a.com', @@ -70,6 +70,23 @@ describe('readBitbucketServerIntegrationConfig', () => { }); }); + it('reads all values, basic auth', () => { + const output = readBitbucketServerIntegrationConfig( + buildConfig({ + host: 'a.com', + apiBaseUrl: 'https://a.com/api', + username: 'u', + password: 'p', + }), + ); + expect(output).toEqual({ + host: 'a.com', + apiBaseUrl: 'https://a.com/api', + username: 'u', + password: 'p', + }); + }); + it('rejects funky configs', () => { const valid: any = { host: 'a.com', diff --git a/packages/integration/src/bitbucketServer/config.ts b/packages/integration/src/bitbucketServer/config.ts index ec7930616c..2f93bdeb07 100644 --- a/packages/integration/src/bitbucketServer/config.ts +++ b/packages/integration/src/bitbucketServer/config.ts @@ -46,6 +46,24 @@ export type BitbucketServerIntegrationConfig = { * If no token is specified, anonymous access is used. */ token?: string; + + /** + * The credentials for Basic Authentication for requests to a Bitbucket Server provider. + * + * If `token` was provided, it will be preferred. + * + * See https://developer.atlassian.com/server/bitbucket/how-tos/command-line-rest/#authentication + */ + username?: string; + + /** + * The credentials for Basic Authentication for requests to a Bitbucket Server provider. + * + * If `token` was provided, it will be preferred. + * + * See https://developer.atlassian.com/server/bitbucket/how-tos/command-line-rest/#authentication + */ + password?: string; }; /** @@ -60,6 +78,8 @@ export function readBitbucketServerIntegrationConfig( const host = config.getString('host'); let apiBaseUrl = config.getOptionalString('apiBaseUrl'); const token = config.getOptionalString('token'); + const username = config.getOptionalString('username'); + const password = config.getOptionalString('password'); if (!isValidHost(host)) { throw new Error( @@ -77,6 +97,8 @@ export function readBitbucketServerIntegrationConfig( host, apiBaseUrl, token, + username, + password, }; } diff --git a/packages/integration/src/bitbucketServer/core.test.ts b/packages/integration/src/bitbucketServer/core.test.ts index 076a2c7a4f..de18dcd821 100644 --- a/packages/integration/src/bitbucketServer/core.test.ts +++ b/packages/integration/src/bitbucketServer/core.test.ts @@ -36,7 +36,13 @@ describe('bitbucketServer core', () => { apiBaseUrl: '', token: 'A', }; - const withoutToken: BitbucketServerIntegrationConfig = { + const withBasicAuth: BitbucketServerIntegrationConfig = { + host: '', + apiBaseUrl: '', + username: 'u', + password: 'p', + }; + const withoutCredentials: BitbucketServerIntegrationConfig = { host: '', apiBaseUrl: '', }; @@ -45,7 +51,11 @@ describe('bitbucketServer core', () => { .Authorization, ).toEqual('Bearer A'); expect( - (getBitbucketServerRequestOptions(withoutToken).headers as any) + (getBitbucketServerRequestOptions(withBasicAuth).headers as any) + .Authorization, + ).toEqual('Basic dTpw'); + expect( + (getBitbucketServerRequestOptions(withoutCredentials).headers as any) .Authorization, ).toBeUndefined(); }); diff --git a/packages/integration/src/bitbucketServer/core.ts b/packages/integration/src/bitbucketServer/core.ts index 0c57b0d3ed..5fca2aba68 100644 --- a/packages/integration/src/bitbucketServer/core.ts +++ b/packages/integration/src/bitbucketServer/core.ts @@ -140,6 +140,10 @@ export function getBitbucketServerRequestOptions( if (config.token) { headers.Authorization = `Bearer ${config.token}`; } + if (config.username && config.password) { + const buffer = Buffer.from(`${config.username}:${config.password}`, 'utf8'); + headers.Authorization = `Basic ${buffer.toString('base64')}`; + } return { headers, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.test.ts index efcf51c49c..b5a375d83d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.test.ts @@ -33,8 +33,6 @@ jest.mock('@backstage/backend-common', () => ({ })); const mockedGit = Git.fromAuth({ - username: 'test-user', - password: 'test-password', logger: getVoidLogger(), }); @@ -101,6 +99,22 @@ describe('initRepoAndPush', () => { }); }); + it('with token', async () => { + await initRepoAndPush({ + dir: '/test/repo/dir/', + remoteUrl: 'git@github.com:test/repo.git', + auth: { + token: 'test-token', + }, + logger: getVoidLogger(), + }); + + expect(mockedGit.init).toHaveBeenCalledWith({ + dir: '/test/repo/dir/', + defaultBranch: 'master', + }); + }); + it('allows overriding the default branch', async () => { await initRepoAndPush({ dir: '/test/repo/dir/', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts index 7a9127a206..d0cdcd627a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts @@ -83,15 +83,17 @@ export async function initRepoAndPush({ }: { dir: string; remoteUrl: string; - auth: { username: string; password: string }; + // For use cases where token has to be used with Basic Auth + // it has to be provided as password together with a username + // which may be a fixed value defined by the provider. + auth: { username: string; password: string } | { token: string }; logger: Logger; defaultBranch?: string; commitMessage?: string; gitAuthorInfo?: { name?: string; email?: string }; }): Promise { const git = Git.fromAuth({ - username: auth.username, - password: auth.password, + ...auth, logger, }); @@ -137,7 +139,10 @@ export async function commitAndPushRepo({ remoteRef, }: { dir: string; - auth: { username: string; password: string }; + // For use cases where token has to be used with Basic Auth + // it has to be provided as password together with a username + // which may be a fixed value defined by the provider. + auth: { username: string; password: string } | { token: string }; logger: Logger; commitMessage: string; gitAuthorInfo?: { name?: string; email?: string }; @@ -145,8 +150,7 @@ export async function commitAndPushRepo({ remoteRef?: string; }): Promise { const git = Git.fromAuth({ - username: auth.username, - password: auth.password, + ...auth, logger, }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServer.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServer.test.ts index 338231ea55..06d3dfb735 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServer.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServer.test.ts @@ -36,7 +36,13 @@ describe('publish:bitbucketServer', () => { apiBaseUrl: 'https://hosted.bitbucket.com/rest/api/1.0', }, { - host: 'notoken.bitbucket.com', + host: 'basic-auth.bitbucket.com', + username: 'test-user', + password: 'test-password', + apiBaseUrl: 'https://basic-auth.bitbucket.com/rest/api/1.0', + }, + { + host: 'no-credentials.bitbucket.com', }, ], }, @@ -96,21 +102,21 @@ describe('publish:bitbucketServer', () => { ).rejects.toThrow(/No matching integration configuration/); }); - it('should throw if there is no token in the integration config that is returned', async () => { + it('should throw if there no credentials in the integration config that is returned', async () => { await expect( action.handler({ ...mockContext, input: { ...mockContext.input, - repoUrl: 'notoken.bitbucket.com?project=project&repo=repo', + repoUrl: 'no-credentials.bitbucket.com?project=project&repo=repo', }, }), ).rejects.toThrow( - /Authorization has not been provided for notoken.bitbucket.com/, + /Authorization has not been provided for no-credentials.bitbucket.com/, ); }); - it('should call the correct APIs', async () => { + it('should call the correct APIs with token', async () => { expect.assertions(2); server.use( rest.post( @@ -150,12 +156,54 @@ describe('publish:bitbucketServer', () => { }); }); + it('should call the correct APIs with basic auth', async () => { + expect.assertions(2); + server.use( + rest.post( + 'https://basic-auth.bitbucket.com/rest/api/1.0/projects/project/repos', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe( + 'Basic dGVzdC11c2VyOnRlc3QtcGFzc3dvcmQ=', + ); + expect(req.body).toEqual({ public: false, name: 'repo' }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + ); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoUrl: 'basic-auth.bitbucket.com?project=project&repo=repo', + }, + }); + }); + it('should work if the token is provided through ctx.input', async () => { expect.assertions(2); const token = 'user-token'; server.use( rest.post( - 'https://notoken.bitbucket.com/rest/api/1.0/projects/project/repos', + 'https://no-credentials.bitbucket.com/rest/api/1.0/projects/project/repos', (req, res, ctx) => { expect(req.headers.get('Authorization')).toBe(`Bearer ${token}`); expect(req.body).toEqual({ public: false, name: 'repo' }); @@ -185,7 +233,7 @@ describe('publish:bitbucketServer', () => { ...mockContext, input: { ...mockContext.input, - repoUrl: 'notoken.bitbucket.com?project=project&repo=repo', + repoUrl: 'no-credentials.bitbucket.com?project=project&repo=repo', token: token, }, }); @@ -273,7 +321,7 @@ describe('publish:bitbucketServer', () => { }); }); - it('should call initAndPush with the correct values', async () => { + it('should call initAndPush with the correct values with token', async () => { server.use( rest.post( 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos', @@ -309,7 +357,57 @@ describe('publish:bitbucketServer', () => { dir: mockContext.workspacePath, remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo', defaultBranch: 'master', - auth: { username: 'x-token-auth', password: 'thing' }, + auth: { token: 'thing' }, + logger: mockContext.logger, + gitAuthorInfo: {}, + }); + }); + + it('should call initAndPush with the correct values with basic auth', async () => { + server.use( + rest.post( + 'https://basic-auth.bitbucket.com/rest/api/1.0/projects/project/repos', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe( + 'Basic dGVzdC11c2VyOnRlc3QtcGFzc3dvcmQ=', + ); + expect(req.body).toEqual({ public: false, name: 'repo' }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + ); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoUrl: 'basic-auth.bitbucket.com?project=project&repo=repo', + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo', + defaultBranch: 'master', + auth: { username: 'test-user', password: 'test-password' }, logger: mockContext.logger, gitAuthorInfo: {}, }); @@ -357,7 +455,7 @@ describe('publish:bitbucketServer', () => { dir: mockContext.workspacePath, remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo', defaultBranch: 'main', - auth: { username: 'x-token-auth', password: 'thing' }, + auth: { token: 'thing' }, logger: mockContext.logger, gitAuthorInfo: {}, }); @@ -373,7 +471,7 @@ describe('publish:bitbucketServer', () => { apiBaseUrl: 'https://hosted.bitbucket.com/rest/api/1.0', }, { - host: 'notoken.bitbucket.com', + host: 'no-credentials.bitbucket.com', }, ], }, @@ -426,7 +524,7 @@ describe('publish:bitbucketServer', () => { expect(initRepoAndPush).toHaveBeenCalledWith({ dir: mockContext.workspacePath, remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo', - auth: { username: 'x-token-auth', password: 'thing' }, + auth: { token: 'thing' }, logger: mockContext.logger, defaultBranch: 'master', gitAuthorInfo: { name: 'Test', email: 'example@example.com' }, @@ -443,7 +541,7 @@ describe('publish:bitbucketServer', () => { apiBaseUrl: 'https://hosted.bitbucket.com/rest/api/1.0', }, { - host: 'notoken.bitbucket.com', + host: 'no-credentials.bitbucket.com', }, ], }, @@ -493,7 +591,7 @@ describe('publish:bitbucketServer', () => { expect(initRepoAndPush).toHaveBeenCalledWith({ dir: mockContext.workspacePath, remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo', - auth: { username: 'x-token-auth', password: 'thing' }, + auth: { token: 'thing' }, logger: mockContext.logger, defaultBranch: 'master', commitMessage: 'Test commit message', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServer.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServer.ts index 208403d6f4..712a17de70 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServer.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServer.ts @@ -15,7 +15,10 @@ */ import { InputError } from '@backstage/errors'; -import { ScmIntegrationRegistry } from '@backstage/integration'; +import { + getBitbucketServerRequestOptions, + ScmIntegrationRegistry, +} from '@backstage/integration'; import fetch, { Response, RequestInit } from 'node-fetch'; import { initRepoAndPush } from '../helpers'; import { createTemplateAction } from '../../createTemplateAction'; @@ -79,10 +82,6 @@ const createRepository = async (opts: { return { remoteUrl, repoContentsUrl }; }; -const getAuthorizationHeader = (config: { token: string }) => { - return `Bearer ${config.token}`; -}; - const performEnableLFS = async (opts: { authorization: string; host: string; @@ -213,14 +212,19 @@ export function createPublishBitbucketServerAction(options: { } const token = ctx.input.token ?? integrationConfig.config.token; - if (!token) { + + const authConfig = { + ...integrationConfig.config, + ...{ token }, + }; + const reqOpts = getBitbucketServerRequestOptions(authConfig); + const authorization = reqOpts.headers.Authorization; + if (!authorization) { throw new Error( - `Authorization has not been provided for ${integrationConfig.config.host}. Please add either token to the Integrations config or a user login auth token`, + `Authorization has not been provided for ${integrationConfig.config.host}. Please add either (a) a user login auth token, or (b) a token or (c) username + password to the integration config.`, ); } - const authorization = getAuthorizationHeader({ token }); - const apiBaseUrl = integrationConfig.config.apiBaseUrl; const { remoteUrl, repoContentsUrl } = await createRepository({ @@ -237,10 +241,14 @@ export function createPublishBitbucketServerAction(options: { email: config.getOptionalString('scaffolder.defaultAuthor.email'), }; - const auth = { - username: 'x-token-auth', - password: token, - }; + const auth = authConfig.token + ? { + token: token!, + } + : { + username: authConfig.username!, + password: authConfig.password!, + }; await initRepoAndPush({ dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath),