From 67fe5bc9a93a7817434a7540fa5bbc2ee71d98de Mon Sep 17 00:00:00 2001 From: Phil Kuang Date: Tue, 18 Oct 2022 18:10:13 -0400 Subject: [PATCH] fix(GithubLocationAnalyzer): support github app auth & authenticated backends Signed-off-by: Phil Kuang --- .changeset/eight-pears-attack.md | 17 +++++++ .changeset/popular-bulldogs-lie.md | 5 ++ .../api-report.md | 2 + .../analyzers/GithubLocationAnalyzer.test.ts | 11 +++- .../src/analyzers/GithubLocationAnalyzer.ts | 51 +++++++++++++------ 5 files changed, 70 insertions(+), 16 deletions(-) create mode 100644 .changeset/eight-pears-attack.md create mode 100644 .changeset/popular-bulldogs-lie.md diff --git a/.changeset/eight-pears-attack.md b/.changeset/eight-pears-attack.md new file mode 100644 index 0000000000..ca18abb2db --- /dev/null +++ b/.changeset/eight-pears-attack.md @@ -0,0 +1,17 @@ +--- +'@backstage/plugin-catalog-backend-module-github': minor +--- + +BREAKING: Support authenticated backends by including a server token for catalog requests. The constructor of `GithubLocationAnalyzer` now requires an instance of `TokenManager` to be supplied: + +```diff +... + builder.addLocationAnalyzers( + new GitHubLocationAnalyzer({ + discovery: env.discovery, + config: env.config, ++ tokenManager: env.tokenManager, + }), + ); +... +``` diff --git a/.changeset/popular-bulldogs-lie.md b/.changeset/popular-bulldogs-lie.md new file mode 100644 index 0000000000..3bc83338c1 --- /dev/null +++ b/.changeset/popular-bulldogs-lie.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-github': patch +--- + +Properly derive Github credentials when making requests in `GithubLocationAnalyzer` to support Github App authentication diff --git a/plugins/catalog-backend-module-github/api-report.md b/plugins/catalog-backend-module-github/api-report.md index 195b150dea..a9587e172b 100644 --- a/plugins/catalog-backend-module-github/api-report.md +++ b/plugins/catalog-backend-module-github/api-report.md @@ -20,6 +20,7 @@ import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { ScmLocationAnalyzer } from '@backstage/plugin-catalog-backend'; import { TaskRunner } from '@backstage/backend-tasks'; +import { TokenManager } from '@backstage/backend-common'; // @public export class GithubDiscoveryProcessor implements CatalogProcessor { @@ -111,6 +112,7 @@ export class GithubLocationAnalyzer implements ScmLocationAnalyzer { export type GithubLocationAnalyzerOptions = { config: Config; discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; }; // @public diff --git a/plugins/catalog-backend-module-github/src/analyzers/GithubLocationAnalyzer.test.ts b/plugins/catalog-backend-module-github/src/analyzers/GithubLocationAnalyzer.test.ts index 936f8a3576..549ca385e8 100644 --- a/plugins/catalog-backend-module-github/src/analyzers/GithubLocationAnalyzer.test.ts +++ b/plugins/catalog-backend-module-github/src/analyzers/GithubLocationAnalyzer.test.ts @@ -32,7 +32,10 @@ jest.mock('@octokit/rest', () => { return { Octokit }; }); -import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { + PluginEndpointDiscovery, + TokenManager, +} from '@backstage/backend-common'; import { GithubLocationAnalyzer } from './GithubLocationAnalyzer'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { setupServer } from 'msw/node'; @@ -46,6 +49,10 @@ describe('GithubLocationAnalyzer', () => { getBaseUrl: jest.fn().mockResolvedValue('http://localhost:7007'), getExternalBaseUrl: jest.fn(), }; + const mockTokenManager: jest.Mocked = { + authenticate: jest.fn(), + getToken: jest.fn().mockResolvedValue('abc123'), + }; const config = new ConfigReader({ integrations: { github: [ @@ -117,6 +124,7 @@ describe('GithubLocationAnalyzer', () => { const analyzer = new GithubLocationAnalyzer({ discovery: mockDiscoveryApi, config, + tokenManager: mockTokenManager, }); const result = await analyzer.analyze({ url: 'https://github.com/foo/bar', @@ -142,6 +150,7 @@ describe('GithubLocationAnalyzer', () => { const analyzer = new GithubLocationAnalyzer({ discovery: mockDiscoveryApi, config, + tokenManager: mockTokenManager, }); const result = await analyzer.analyze({ url: 'https://github.com/foo/bar', diff --git a/plugins/catalog-backend-module-github/src/analyzers/GithubLocationAnalyzer.ts b/plugins/catalog-backend-module-github/src/analyzers/GithubLocationAnalyzer.ts index 32ef8bf946..a740fd7706 100644 --- a/plugins/catalog-backend-module-github/src/analyzers/GithubLocationAnalyzer.ts +++ b/plugins/catalog-backend-module-github/src/analyzers/GithubLocationAnalyzer.ts @@ -15,7 +15,12 @@ */ import { CatalogApi, CatalogClient } from '@backstage/catalog-client'; -import { ScmIntegrations } from '@backstage/integration'; +import { + DefaultGithubCredentialsProvider, + GithubCredentialsProvider, + ScmIntegrationRegistry, + ScmIntegrations, +} from '@backstage/integration'; import { Octokit } from '@octokit/rest'; import { trimEnd } from 'lodash'; import parseGitUrl from 'git-url-parse'; @@ -23,28 +28,36 @@ import { AnalyzeOptions, ScmLocationAnalyzer, } from '@backstage/plugin-catalog-backend'; -import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { + PluginEndpointDiscovery, + TokenManager, +} from '@backstage/backend-common'; import { Config } from '@backstage/config'; /** @public */ export type GithubLocationAnalyzerOptions = { config: Config; discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; }; /** @public */ export class GithubLocationAnalyzer implements ScmLocationAnalyzer { private readonly catalogClient: CatalogApi; - private readonly config: Config; + private readonly githubCredentialsProvider: GithubCredentialsProvider; + private readonly integrations: ScmIntegrationRegistry; + private readonly tokenManager: TokenManager; constructor(options: GithubLocationAnalyzerOptions) { - this.config = options.config; this.catalogClient = new CatalogClient({ discoveryApi: options.discovery }); + this.integrations = ScmIntegrations.fromConfig(options.config); + this.githubCredentialsProvider = + DefaultGithubCredentialsProvider.fromIntegrations(this.integrations); + this.tokenManager = options.tokenManager; } supports(url: string) { - const integrations = ScmIntegrations.fromConfig(this.config); - const integration = integrations.byUrl(url); + const integration = this.integrations.byUrl(url); return integration?.type === 'github'; } @@ -55,15 +68,18 @@ export class GithubLocationAnalyzer implements ScmLocationAnalyzer { const query = `filename:${catalogFile} repo:${owner}/${repo}`; - const integration = ScmIntegrations.fromConfig(this.config).github.byUrl( - url, - ); + const integration = this.integrations.github.byUrl(url); if (!integration) { throw new Error('Make sure you have a GitHub integration configured'); } + const { token: githubToken } = + await this.githubCredentialsProvider.getCredentials({ + url, + }); + const octokitClient = new Octokit({ - auth: integration.config.token, + auth: githubToken, baseUrl: integration.config.apiBaseUrl, }); @@ -82,15 +98,20 @@ export class GithubLocationAnalyzer implements ScmLocationAnalyzer { }); const defaultBranch = repoInformation.data.default_branch; + const { token: serviceToken } = await this.tokenManager.getToken(); + const result = await Promise.all( searchResult.data.items .map(i => `${trimEnd(url, '/')}/blob/${defaultBranch}/${i.path}`) .map(async target => { - const addLocationResult = await this.catalogClient.addLocation({ - type: 'url', - target, - dryRun: true, - }); + const addLocationResult = await this.catalogClient.addLocation( + { + type: 'url', + target, + dryRun: true, + }, + { token: serviceToken }, + ); return addLocationResult.entities.map(e => ({ location: { type: 'url', target }, isRegistered: !!addLocationResult.exists,