diff --git a/.changeset/tiny-cobras-look.md b/.changeset/tiny-cobras-look.md new file mode 100644 index 0000000000..1c5acc6c4a --- /dev/null +++ b/.changeset/tiny-cobras-look.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Expose an `onAuth` handler for `git` actions to provide custom credentials diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index f0fa15efb0..ef8094eb84 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -7,6 +7,7 @@ /// import { AppConfig } from '@backstage/config'; +import { AuthCallback } from 'isomorphic-git'; import { AwsCredentialsManager } from '@backstage/integration-aws-node'; import { AwsS3Integration } from '@backstage/integration'; import { AzureDevOpsCredentialsProvider } from '@backstage/integration'; @@ -66,6 +67,12 @@ import { V1PodTemplateSpec } from '@kubernetes/client-node'; import * as winston from 'winston'; import { Writable } from 'stream'; +// @public +export type AuthCallbackOptions = { + onAuth: AuthCallback; + logger?: LoggerService; +}; + // @public export class AwsS3UrlReader implements UrlReader { constructor( @@ -379,12 +386,7 @@ export class Git { deleteRemote(options: { dir: string; remote: string }): Promise; fetch(options: { dir: string; remote?: string }): Promise; // (undocumented) - static fromAuth: (options: { - username?: string; - password?: string; - token?: string; - logger?: LoggerService; - }) => Git; + static fromAuth: (options: StaticAuthOptions | AuthCallbackOptions) => Git; // (undocumented) init(options: { dir: string; defaultBranch?: string }): Promise; log(options: { dir: string; ref?: string }): Promise; @@ -749,6 +751,14 @@ export function setRootLogger(newLogger: winston.Logger): void; // @public @deprecated export const SingleHostDiscovery: typeof HostDiscovery; +// @public +export type StaticAuthOptions = { + username?: string; + password?: string; + token?: string; + logger?: LoggerService; +}; + // @public export type StatusCheck = () => Promise; diff --git a/packages/backend-common/src/scm/git.test.ts b/packages/backend-common/src/scm/git.test.ts index 629173f96e..654336e920 100644 --- a/packages/backend-common/src/scm/git.test.ts +++ b/packages/backend-common/src/scm/git.test.ts @@ -190,7 +190,7 @@ describe('Git', () => { }); }); - it('should pass a function that returns the authorization as the onAuth handler', async () => { + it('should pass a function that returns the authorization as the onAuth handler when username and password are specified', async () => { const url = 'http://github.com/some/repo'; const dir = '/some/mock/dir'; const auth = { @@ -208,6 +208,25 @@ describe('Git', () => { expect(onAuth()).toEqual(auth); }); + it('should pass the provided callback as the onAuth handler when on auth is specified', async () => { + const url = 'http://github.com/some/repo'; + const dir = '/some/mock/dir'; + const auth = { + username: 'from', + password: 'callback', + }; + + const git = Git.fromAuth({ onAuth: () => auth }); + + await git.clone({ url, dir }); + + const { onAuth } = ( + isomorphic.clone as unknown as jest.Mock<(typeof isomorphic)['clone']> + ).mock.calls[0][0]!; + + expect(onAuth()).toEqual(auth); + }); + it('should propagate the data from the error handler', async () => { const url = 'http://github.com/some/repo'; const dir = '/some/mock/dir'; diff --git a/packages/backend-common/src/scm/git.ts b/packages/backend-common/src/scm/git.ts index 234ae57d30..deb8c111cd 100644 --- a/packages/backend-common/src/scm/git.ts +++ b/packages/backend-common/src/scm/git.ts @@ -18,11 +18,40 @@ import git, { ProgressCallback, MergeResult, ReadCommitResult, + AuthCallback, } from 'isomorphic-git'; import http from 'isomorphic-git/http/node'; import fs from 'fs-extra'; import { LoggerService } from '@backstage/backend-plugin-api'; +function isAuthCallbackOptions( + options: StaticAuthOptions | AuthCallbackOptions, +): options is AuthCallbackOptions { + return 'onAuth' in options; +} + +/** + * Configure static credential for authentication + * + * @public + */ +export type StaticAuthOptions = { + username?: string; + password?: string; + token?: string; + logger?: LoggerService; +}; + +/** + * Configure an authentication callback that can provide credentials on demand + * + * @public + */ +export type AuthCallbackOptions = { + onAuth: AuthCallback; + logger?: LoggerService; +}; + /* provider username password Azure 'notempty' token @@ -42,6 +71,7 @@ instead of Basic Auth (e.g., Bitbucket Server). * * @public */ + export class Git { private readonly headers: { [x: string]: string; @@ -49,12 +79,13 @@ export class Git { private constructor( private readonly config: { - username?: string; - password?: string; + onAuth: AuthCallback; token?: string; logger?: LoggerService; }, ) { + this.onAuth = config.onAuth; + this.headers = { 'user-agent': 'git/@isomorphic-git', ...(config.token ? { Authorization: `Bearer ${config.token}` } : {}), @@ -283,10 +314,7 @@ export class Git { }); } - private onAuth = () => ({ - username: this.config.username, - password: this.config.password, - }); + private onAuth: AuthCallback; private onProgressHandler = (): ProgressCallback => { let currentPhase = ''; @@ -303,13 +331,13 @@ export class Git { }; }; - static fromAuth = (options: { - username?: string; - password?: string; - token?: string; - logger?: LoggerService; - }) => { + static fromAuth = (options: StaticAuthOptions | AuthCallbackOptions) => { + if (isAuthCallbackOptions(options)) { + const { onAuth, logger } = options; + return new Git({ onAuth, logger }); + } + const { username, password, token, logger } = options; - return new Git({ username, password, token, logger }); + return new Git({ onAuth: () => ({ username, password }), token, logger }); }; } diff --git a/packages/backend-common/src/scm/index.ts b/packages/backend-common/src/scm/index.ts index ceba752c9e..f9c59e99eb 100644 --- a/packages/backend-common/src/scm/index.ts +++ b/packages/backend-common/src/scm/index.ts @@ -15,3 +15,4 @@ */ export { Git } from './git'; +export type { StaticAuthOptions, AuthCallbackOptions } from './git';