fix(GithubLocationAnalyzer): support github app auth & authenticated backends

Signed-off-by: Phil Kuang <pkuang@factset.com>
This commit is contained in:
Phil Kuang
2022-10-18 18:10:13 -04:00
parent c2ee8f8cfc
commit 67fe5bc9a9
5 changed files with 70 additions and 16 deletions
+17
View File
@@ -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,
}),
);
...
```
+5
View File
@@ -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
@@ -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
@@ -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<TokenManager> = {
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',
@@ -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,