diff --git a/.changeset/ninety-worms-kick.md b/.changeset/ninety-worms-kick.md new file mode 100644 index 0000000000..5170dbbdce --- /dev/null +++ b/.changeset/ninety-worms-kick.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': patch +--- + +adds an allow list of github installations diff --git a/docs/plugins/github-apps.md b/docs/plugins/github-apps.md index 87d23b45d5..5e69ae8294 100644 --- a/docs/plugins/github-apps.md +++ b/docs/plugins/github-apps.md @@ -84,3 +84,23 @@ integrations: apps: - $include: example-backstage-app-credentials.yaml ``` + +### Limiting the Github App installations + +If you want to limit the Github app installations visible to backstage you +may optionally include the `allowedInstallationIds` option. + +```yaml +appId: 1 +allowedInstallationIds: [1234] +clientId: client id +clientSecret: client secret +webhookSecret: webhook secret +privateKey: | + -----BEGIN RSA PRIVATE KEY----- + ...Key content... + -----END RSA PRIVATE KEY----- +``` + +This will result in backstage preventing the use of any installation that is not within the +allow list. diff --git a/packages/integration/src/github/GithubCredentialsProvider.ts b/packages/integration/src/github/GithubCredentialsProvider.ts index 9bc09deab3..8dbc3e7a69 100644 --- a/packages/integration/src/github/GithubCredentialsProvider.ts +++ b/packages/integration/src/github/GithubCredentialsProvider.ts @@ -68,8 +68,10 @@ class GithubAppManager { private readonly baseAuthConfig: { appId: number; privateKey: string }; private installations?: RestEndpointMethodTypes['apps']['listInstallations']['response']; private readonly cache = new Cache(); + private readonly allowedInstallations: number[] | undefined; // undefined allows all installations constructor(config: GithubAppConfig, baseUrl?: string) { + this.allowedInstallations = config.allowedInstallations; this.baseAuthConfig = { appId: config.appId, privateKey: config.privateKey, @@ -91,6 +93,17 @@ class GithubAppManager { suspended, repositorySelection, } = await this.getInstallationData(owner); + if (this.allowedInstallations) { + if (!this.allowedInstallations?.includes(installationId)) { + throw new Error( + `The GitHub application for ${[owner, repo] + .filter(Boolean) + .join( + '/', + )} is not included in the allowed installation list (${installationId}).`, + ); + } + } if (suspended) { throw new Error( `The GitHub application for ${[owner, repo] diff --git a/packages/integration/src/github/config.ts b/packages/integration/src/github/config.ts index 94ed00731e..370aed9c8b 100644 --- a/packages/integration/src/github/config.ts +++ b/packages/integration/src/github/config.ts @@ -93,6 +93,10 @@ export type GithubAppConfig = { * Client secrets can be generated at https://github.com/organizations/$org/settings/apps/$AppName */ clientSecret: string; + /** + * List of installations allowed to be used by this backstage https://github.com/app/installations/$InstallationId + */ + allowedInstallations?: number[]; }; /** @@ -113,6 +117,9 @@ export function readGitHubIntegrationConfig( clientSecret: c.getString('clientSecret'), webhookSecret: c.getString('webhookSecret'), privateKey: c.getString('privateKey'), + allowedInstallations: c + .getOptionalStringArray('allowedInstallations') + ?.map(allowedInstallation => Number(allowedInstallation)), })); if (!isValidHost(host)) {