feat(backend-common): exposed onAuth callback

Exposed the `onAuth` callback from isomorphic git to allow callers to specify a callback to resolve credentials based on the URL of the remote instead of providing static credentials. For example, the [DefaultAzureDevOpsCredentialsProvider](https://github.com/backstage/backstage/blob/master/packages/integration/src/azure/DefaultAzureDevOpsCredentialsProvider.ts) can be used to resolve Azure DevOps credentials based on the request URL.

Signed-off-by: Sander Aernouts <sander.aernouts@gmail.com>
This commit is contained in:
Sander Aernouts
2023-11-02 14:19:35 +01:00
parent c21fa418d7
commit d1e00aa17a
5 changed files with 83 additions and 20 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-common': patch
---
Expose an `onAuth` handler for `git` actions to provide custom credentials
+16 -6
View File
@@ -7,6 +7,7 @@
/// <reference types="webpack-env" />
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<void>;
fetch(options: { dir: string; remote?: string }): Promise<void>;
// (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<void>;
log(options: { dir: string; ref?: string }): Promise<ReadCommitResult[]>;
@@ -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<any>;
+20 -1
View File
@@ -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';
+41 -13
View File
@@ -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 });
};
}
+1
View File
@@ -15,3 +15,4 @@
*/
export { Git } from './git';
export type { StaticAuthOptions, AuthCallbackOptions } from './git';