Merge pull request #6539 from RoadieHQ/remove-github-restriction

remove repo filtering on the github credentials
This commit is contained in:
Johan Haals
2021-08-09 15:21:34 +02:00
committed by GitHub
5 changed files with 44 additions and 27 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/integration': patch
---
Remove repo restriction from GitHub credentials provider
@@ -133,7 +133,7 @@ describe('GithubCredentialsProvider tests', () => {
expect(token).toEqual('secret_token');
});
it('should fail to issue tokens for an organization when the app is installed for a single repo', async () => {
it('should not fail to issue tokens for an organization when the app is installed for a single repo', async () => {
octokit.apps.listInstallations.mockResolvedValue({
headers: {
etag: '123',
@@ -156,13 +156,12 @@ describe('GithubCredentialsProvider tests', () => {
},
} as RestEndpointMethodTypes['apps']['createInstallationAccessToken']['response']);
await expect(
github.getCredentials({
url: 'https://github.com/backstage',
}),
).rejects.toThrow(
'The Backstage GitHub application used in the backstage organization must be installed for the entire organization to be able to issue credentials without a specified repository.',
);
const { token, headers } = await github.getCredentials({
url: 'https://github.com/backstage',
});
const expectedToken = 'secret_token';
expect(headers).toEqual({ Authorization: `Bearer ${expectedToken}` });
expect(token).toEqual('secret_token');
});
it('should throw if the app is suspended', async () => {
@@ -23,7 +23,6 @@ import { DateTime } from 'luxon';
type InstallationData = {
installationId: number;
suspended: boolean;
repositorySelection: 'selected' | 'all';
};
class Cache {
@@ -85,31 +84,34 @@ class GithubAppManager {
owner: string,
repo?: string,
): Promise<{ accessToken: string }> {
const { installationId, suspended, repositorySelection } =
await this.getInstallationData(owner);
const { installationId, suspended } = await this.getInstallationData(owner);
if (suspended) {
throw new Error(
`The GitHub application for ${[owner, repo]
.filter(Boolean)
.join('/')} is suspended`,
);
}
if (repositorySelection !== 'all' && !repo) {
throw new Error(
`The Backstage GitHub application used in the ${owner} organization must be installed for the entire organization to be able to issue credentials without a specified repository.`,
);
throw new Error(`The GitHub application for ${owner} is suspended`);
}
const cacheKey = !repo ? owner : `${owner}/${repo}`;
const repositories = repositorySelection !== 'all' ? [repo!] : undefined;
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,
repositories,
});
if (repo && result.data.repository_selection === 'selected') {
const installationClient = new Octokit({
auth: result.data.token,
});
const repos =
await installationClient.apps.listReposAccessibleToInstallation();
const hasRepo = repos.data.repositories.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),
@@ -132,7 +134,6 @@ class GithubAppManager {
return {
installationId: installation.id,
suspended: Boolean(installation.suspended_by),
repositorySelection: installation.repository_selection,
};
}
const notFoundError = new Error(
@@ -33,6 +33,7 @@ describe('GithubDiscoveryProcessor', () => {
parseUrl('https://github.com/foo/proj/blob/master/catalog.yaml'),
).toEqual({
org: 'foo',
host: 'github.com',
repoSearchPath: /^proj$/,
catalogPath: '/blob/master/catalog.yaml',
});
@@ -40,6 +41,7 @@ describe('GithubDiscoveryProcessor', () => {
parseUrl('https://github.com/foo/proj*/blob/master/catalog.yaml'),
).toEqual({
org: 'foo',
host: 'github.com',
repoSearchPath: /^proj.*$/,
catalogPath: '/blob/master/catalog.yaml',
});
@@ -64,10 +64,18 @@ export class GithubDiscoveryProcessor implements CatalogProcessor {
`There is no GitHub integration that matches ${location.target}. Please add a configuration entry for it under integrations.github`,
);
}
const { org, repoSearchPath, catalogPath, host } = parseUrl(
location.target,
);
// Building the org url here so that the github creds provider doesn't need to know
// about how to handle the wild card which is special for this processor.
const orgUrl = `https://${host}/${org}`;
const { headers } = await GithubCredentialsProvider.create(
gitHubConfig,
).getCredentials({ url: location.target });
const { org, repoSearchPath, catalogPath } = parseUrl(location.target);
).getCredentials({ url: orgUrl });
const client = graphql.defaults({
baseUrl: gitHubConfig.apiBaseUrl,
@@ -115,6 +123,7 @@ export function parseUrl(urlString: string): {
org: string;
repoSearchPath: RegExp;
catalogPath: string;
host: string;
} {
const url = new URL(urlString);
const path = url.pathname.substr(1).split('/');
@@ -125,6 +134,7 @@ export function parseUrl(urlString: string): {
org: decodeURIComponent(path[0]),
repoSearchPath: escapeRegExp(decodeURIComponent(path[1])),
catalogPath: `/${decodeURIComponent(path.slice(2).join('/'))}`,
host: url.host,
};
}