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