Add GHA option to auth flow

Signed-off-by: Philipp Hugenroth <philipph@spotify.com>
This commit is contained in:
Philipp Hugenroth
2023-02-10 13:43:16 +01:00
parent 29534d2453
commit 250fb2af40
4 changed files with 141 additions and 54 deletions
+23 -3
View File
@@ -19,6 +19,7 @@ import inquirer from 'inquirer';
import { Task } from '../../lib/tasks';
import { github } from './github';
import { updateConfigFile, updateEnvFile } from './file';
import ghAuth from './gh-auth';
export async function command(): Promise<void> {
const answers = await inquirer.prompt<{
@@ -44,7 +45,7 @@ export async function command(): Promise<void> {
type: 'list',
name: 'provider',
message: 'Please select a provider:',
choices: ['github'],
choices: ['GitHub OAuth', 'GitHub App'],
when: ({ shouldSetupAuth }) => shouldSetupAuth,
},
]);
@@ -59,9 +60,10 @@ export async function command(): Promise<void> {
process.exit(1);
}
const { useEnvForSecrets } = answers;
switch (answers.provider) {
case 'github': {
const { useEnvForSecrets } = answers;
case 'GitHub OAuth': {
const config = await github(useEnvForSecrets);
await updateConfigFile(config);
if (useEnvForSecrets) {
@@ -69,6 +71,24 @@ export async function command(): Promise<void> {
}
break;
}
case 'GitHub App': {
const config = await ghAuth();
// TODO(tudi2d): Also change integrations
if (useEnvForSecrets) {
await updateEnvFile({
auth: config.auth,
});
config.auth.providers.github.clientId = '${AUTH_GITHUB_CLIENT_ID}';
config.auth.providers.github.clientSecret =
'${AUTH_GITHUB_CLIENT_SECRET}';
await updateConfigFile({
auth: config.auth,
});
} else {
await updateConfigFile({ auth: config.auth });
}
break;
}
default:
throw new Error(`Provider ${answers.provider} not implemented yet.`);
}
+4 -3
View File
@@ -15,10 +15,11 @@
*/
import chalk from 'chalk';
import inquirer from 'inquirer';
import createGithubApp from '../create-github-app';
import { adminCli } from '../create-github-app';
// TODO(tudi2d): Wrapper for admin CLI around `create-github-app` - potentially to be removed
export default async () => {
// TODO: Make the GitHub Org optional
// TODO(tudi2d): Make the GitHub Org optional
const input = await inquirer.prompt<{ org: string }>([
{
type: 'input',
@@ -26,5 +27,5 @@ export default async () => {
message: chalk.blue('Enter a GitHub Org [required]'),
},
]);
await createGithubApp(input.org);
return await adminCli(input.org);
};
@@ -33,7 +33,7 @@ const FORM_PAGE = `
</html>
`;
type GithubAppConfig = {
export type GithubAppConfig = {
appId: number;
slug?: string;
name?: string;
@@ -14,62 +14,26 @@
* limitations under the License.
*/
import fs from 'fs-extra';
import chalk from 'chalk';
import { stringify as stringifyYaml } from 'yaml';
import inquirer, { Question, Answers } from 'inquirer';
import { paths } from '../../lib/paths';
import { GithubCreateAppServer } from './GithubCreateAppServer';
import fs from 'fs-extra';
import inquirer, { Question } from 'inquirer';
import fetch from 'node-fetch';
import openBrowser from 'react-dev-utils/openBrowser';
import { stringify as stringifyYaml } from 'yaml';
import { GithubAppConfig } from '@backstage/integration';
import { paths } from '../../lib/paths';
import { GithubCreateAppServer } from './GithubCreateAppServer';
// This is an experimental command that at this point does not support GitHub Enterprise
// due to lacking support for creating apps from manifests.
// https://docs.github.com/en/free-pro-team@latest/developers/apps/creating-a-github-app-from-a-manifest
export default async (org: string) => {
// Why is the Org check not done here?
const answers: Answers = await inquirer.prompt({
name: 'appType',
type: 'checkbox',
message:
'Select permissions [required] (these can be changed later but then require approvals in all installations)',
choices: [
{
name: 'Read access to content (required by Software Catalog to ingest data from repositories)',
value: 'read',
checked: true,
},
{
name: 'Read access to members (required by Software Catalog to ingest GitHub teams)',
value: 'members',
checked: true,
},
{
name: 'Read and Write to content and actions (required by Software Templates to create new repositories)',
value: 'write',
},
],
});
if (answers.appType.length === 0) {
console.log(chalk.red('You must select at least one permission'));
process.exit(1);
}
// TODO(tudi2d): Why is the Org check not done here?
const answers = await permissionAnswers();
await verifyGithubOrg(org);
const { slug, name, ...config } = await GithubCreateAppServer.run({
org,
permissions: answers.appType,
});
const { fileName } = await createGHAYaml(org, answers.appType);
const fileName = `github-app-${slug}-credentials.yaml`;
const content = `# Name: ${name}\n${stringifyYaml(config)}`;
await fs.writeFile(paths.resolveTargetRoot(fileName), content);
console.log(`GitHub App configuration written to ${chalk.cyan(fileName)}`);
console.log(
chalk.yellow(
'This file contains sensitive credentials, it should not be committed to version control and handled with care!',
),
);
console.log(
"Here's an example on how to update the integrations section in app-config.yaml",
);
@@ -83,6 +47,42 @@ integrations:
);
};
/**
*
* @param org string: GitHub Organisation
* @returns Promise<config>: Partial App Config for Admin CLI
*/
export async function adminCli(org: string) {
await verifyGithubOrg(org);
const answers = await permissionAnswers({
read: true,
members: true,
write: true,
});
const { fileName, config } = await createGHAYaml(org, answers.appType);
const auth = {
providers: {
github: {
clientId: config.clientId,
clientSecret: config.clientSecret,
},
},
};
const integration = {
github: {
host: 'github.com',
apps: {
$include: `${fileName}`,
},
},
};
return {
auth,
integration,
};
}
async function verifyGithubOrg(org: string): Promise<void> {
let response;
@@ -121,6 +121,7 @@ async function verifyGithubOrg(org: string): Promise<void> {
openBrowser('https://github.com/account/organizations/new');
// TODO(tudi2d): Could we maybe re-run the command rather than exit?
console.log(
chalk.yellow(
'Please re-run this command when you have created your new organization',
@@ -130,3 +131,68 @@ async function verifyGithubOrg(org: string): Promise<void> {
process.exit(0);
}
}
async function permissionAnswers(
checked: { read: boolean; members: boolean; write: boolean } = {
read: true,
members: true,
write: false,
},
) {
const answers = await inquirer.prompt<{ appType: string[] }>({
name: 'appType',
type: 'checkbox',
message:
'Select permissions [required] (these can be changed later but then require approvals in all installations)',
choices: [
{
name: 'Read access to content (required by Software Catalog to ingest data from repositories)',
value: 'read',
checked: checked.read,
},
{
name: 'Read access to members (required by Software Catalog to ingest GitHub teams)',
value: 'members',
checked: checked.members,
},
{
name: 'Read and Write to content and actions (required by Software Templates to create new repositories)',
value: 'write',
checked: checked.write,
},
],
});
if (answers.appType.length === 0) {
console.log(chalk.red('You must select at least one permission'));
// TODO(tudi2d): Maybe re-do input rather than exit?
process.exit(1);
}
return answers;
}
async function createGHAYaml(
org: string,
permissions: string[],
): Promise<{ fileName: string; config: GithubAppConfig }> {
const { slug, name, ...config } = await GithubCreateAppServer.run({
org,
permissions,
});
const fileName = `github-app-${slug}-credentials.yaml`;
const content = `# Name: ${name}\n${stringifyYaml(config)}`;
await fs.writeFile(paths.resolveTargetRoot(fileName), content);
console.log(`GitHub App configuration written to ${chalk.cyan(fileName)}`);
console.log(
chalk.yellow(
'This file contains sensitive credentials, it should not be committed to version control and handled with care!',
),
);
return {
fileName,
config,
};
}