remove code accidently added in
Signed-off-by: Brian Fletcher <brian@roadie.io>
This commit is contained in:
@@ -1,214 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { GithubAppConfig, GitHubIntegrationConfig } from './config';
|
||||
import { Octokit, RestEndpointMethodTypes } from '@octokit/rest';
|
||||
import { createAppAuth } from '@octokit/auth-app';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
/**
|
||||
* This accept header is required when calling App APIs in GitHub Enterprise.
|
||||
* It has no effect on calls to github.com and can probably be removed entirely
|
||||
* once GitHub Apps is out of preview.
|
||||
*/
|
||||
const HEADERS = {
|
||||
Accept: 'application/vnd.github.machine-man-preview+json',
|
||||
};
|
||||
|
||||
type InstallationData = {
|
||||
installationId: number;
|
||||
suspended: boolean;
|
||||
};
|
||||
|
||||
class Cache {
|
||||
private readonly tokenCache = new Map<
|
||||
string,
|
||||
{ token: string; expiresAt: DateTime }
|
||||
>();
|
||||
|
||||
async getOrCreateToken(
|
||||
key: string,
|
||||
supplier: () => Promise<{ token: string; expiresAt: DateTime }>,
|
||||
): Promise<{ accessToken: string }> {
|
||||
const item = this.tokenCache.get(key);
|
||||
if (item && this.isNotExpired(item.expiresAt)) {
|
||||
return { accessToken: item.token };
|
||||
}
|
||||
|
||||
const result = await supplier();
|
||||
this.tokenCache.set(key, result);
|
||||
return { accessToken: result.token };
|
||||
}
|
||||
|
||||
// consider timestamps older than 50 minutes to be expired.
|
||||
private isNotExpired = (date: DateTime) =>
|
||||
date.diff(DateTime.local(), 'minutes').minutes > 50;
|
||||
}
|
||||
|
||||
/**
|
||||
* GithubAppManager issues and caches tokens for a specific GitHub App.
|
||||
*/
|
||||
class GithubAppManager {
|
||||
private readonly appClient: Octokit;
|
||||
private readonly baseUrl?: string;
|
||||
private readonly baseAuthConfig: { appId: number; privateKey: string };
|
||||
private readonly cache = new Cache();
|
||||
private readonly allowedInstallationOwners: string[] | undefined; // undefined allows all installations
|
||||
|
||||
constructor(config: GithubAppConfig, baseUrl?: string) {
|
||||
this.allowedInstallationOwners = config.allowedInstallationOwners;
|
||||
this.baseUrl = baseUrl;
|
||||
this.baseAuthConfig = {
|
||||
appId: config.appId,
|
||||
privateKey: config.privateKey.replace(/\\n/gm, '\n'),
|
||||
};
|
||||
this.appClient = new Octokit({
|
||||
baseUrl,
|
||||
headers: HEADERS,
|
||||
authStrategy: createAppAuth,
|
||||
auth: this.baseAuthConfig,
|
||||
});
|
||||
}
|
||||
|
||||
async getInstallationCredentials(
|
||||
owner: string,
|
||||
repo?: string,
|
||||
): Promise<{ accessToken: string }> {
|
||||
const { installationId, suspended } = await this.getInstallationData(owner);
|
||||
if (this.allowedInstallationOwners) {
|
||||
if (!this.allowedInstallationOwners?.includes(owner)) {
|
||||
throw new Error(
|
||||
`The GitHub application for ${owner} is not included in the allowed installation list (${installationId}).`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (suspended) {
|
||||
throw new Error(`The GitHub application for ${owner} is suspended`);
|
||||
}
|
||||
|
||||
const cacheKey = repo ? `${owner}/${repo}` : owner;
|
||||
|
||||
// Go and grab an access token for the app scoped to a repository if provided, if not use the organisation installation.
|
||||
return this.cache.getOrCreateToken(cacheKey, async () => {
|
||||
const result = await this.appClient.apps.createInstallationAccessToken({
|
||||
installation_id: installationId,
|
||||
headers: HEADERS,
|
||||
});
|
||||
if (repo && result.data.repository_selection === 'selected') {
|
||||
const installationClient = new Octokit({
|
||||
baseUrl: this.baseUrl,
|
||||
auth: result.data.token,
|
||||
});
|
||||
const repos = await installationClient.paginate(
|
||||
installationClient.apps.listReposAccessibleToInstallation,
|
||||
);
|
||||
const hasRepo = repos.some(repository => {
|
||||
return repository.name === repo;
|
||||
});
|
||||
if (!hasRepo) {
|
||||
throw new Error(
|
||||
`The Backstage GitHub application used in the ${owner} organization does not have access to a repository with the name ${repo}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return {
|
||||
token: result.data.token,
|
||||
expiresAt: DateTime.fromISO(result.data.expires_at),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
getInstallations(): Promise<
|
||||
RestEndpointMethodTypes['apps']['listInstallations']['response']['data']
|
||||
> {
|
||||
return this.appClient.paginate(this.appClient.apps.listInstallations);
|
||||
}
|
||||
|
||||
private async getInstallationData(owner: string): Promise<InstallationData> {
|
||||
const allInstallations = await this.getInstallations();
|
||||
const installation = allInstallations.find(
|
||||
inst =>
|
||||
inst.account?.login?.toLocaleLowerCase('en-US') ===
|
||||
owner.toLocaleLowerCase('en-US'),
|
||||
);
|
||||
if (installation) {
|
||||
return {
|
||||
installationId: installation.id,
|
||||
suspended: Boolean(installation.suspended_by),
|
||||
};
|
||||
}
|
||||
const notFoundError = new Error(
|
||||
`No app installation found for ${owner} in ${this.baseAuthConfig.appId}`,
|
||||
);
|
||||
notFoundError.name = 'NotFoundError';
|
||||
throw notFoundError;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Corresponds to a Github installation which internally could hold several GitHub Apps.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class GithubAppCredentialsMux {
|
||||
private readonly apps: GithubAppManager[];
|
||||
|
||||
constructor(config: GitHubIntegrationConfig) {
|
||||
this.apps =
|
||||
config.apps?.map(ac => new GithubAppManager(ac, config.apiBaseUrl)) ?? [];
|
||||
}
|
||||
|
||||
async getAllInstallations(): Promise<
|
||||
RestEndpointMethodTypes['apps']['listInstallations']['response']['data']
|
||||
> {
|
||||
if (!this.apps.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const installs = await Promise.all(
|
||||
this.apps.map(app => app.getInstallations()),
|
||||
);
|
||||
|
||||
return installs.flat();
|
||||
}
|
||||
|
||||
async getAppToken(owner: string, repo?: string): Promise<string | undefined> {
|
||||
if (this.apps.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const results = await Promise.all(
|
||||
this.apps.map(app =>
|
||||
app.getInstallationCredentials(owner, repo).then(
|
||||
credentials => ({ credentials, error: undefined }),
|
||||
error => ({ credentials: undefined, error }),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const result = results.find(resultItem => resultItem.credentials);
|
||||
if (result) {
|
||||
return result.credentials!.accessToken;
|
||||
}
|
||||
|
||||
const errors = results.map(r => r.error);
|
||||
const notNotFoundError = errors.find(err => err.name !== 'NotFoundError');
|
||||
if (notNotFoundError) {
|
||||
throw notNotFoundError;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -140,9 +140,3 @@ export class UrlReaderProcessor implements CatalogProcessor {
|
||||
return { response: [{ url: location, data }] };
|
||||
}
|
||||
}
|
||||
|
||||
export class RoadieDemoDataReaderProcessor extends UrlReaderProcessor {
|
||||
getProcessorName() {
|
||||
return 'roadie-demo-data-reader';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user