From 7af6c36adfea09e08dbb9a95c307e5c2c272e7e8 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Thu, 9 Feb 2023 14:33:17 +0100 Subject: [PATCH] cli: Expand admin command to setup basic authenticaton for gihub Signed-off-by: Marcus Eide --- packages/cli/src/commands/admin/command.ts | 54 ++++++++++- packages/cli/src/commands/admin/file.ts | 48 ++++++++++ packages/cli/src/commands/admin/github.ts | 104 +++++++++++++++++++++ 3 files changed, 202 insertions(+), 4 deletions(-) create mode 100644 packages/cli/src/commands/admin/file.ts create mode 100644 packages/cli/src/commands/admin/github.ts diff --git a/packages/cli/src/commands/admin/command.ts b/packages/cli/src/commands/admin/command.ts index 3539ddc40c..5e8256c14f 100644 --- a/packages/cli/src/commands/admin/command.ts +++ b/packages/cli/src/commands/admin/command.ts @@ -17,15 +17,61 @@ import chalk from 'chalk'; import inquirer from 'inquirer'; import { Task } from '../../lib/tasks'; +import { github } from './github'; +import { updateConfigFile, updateEnvFile } from './file'; export async function command(): Promise { - const answers = await inquirer.prompt<{ github: boolean }>([ + const answers = await inquirer.prompt<{ + shouldSetupAuth: boolean; + useEnvForSecrets?: boolean; + provider?: string; + }>([ { type: 'confirm', - name: 'github', - message: chalk.blue('Do you want to set up GitHub Authentication?'), + name: 'shouldSetupAuth', + message: chalk.blue( + 'Do you want to set up Authentication for this project?', + ), + }, + { + type: 'confirm', + name: 'useEnvForSecrets', + message: + 'Would you like to store sensitive configuration details such as secrets as environment variables? (recommended)', + when: ({ shouldSetupAuth }) => shouldSetupAuth, + }, + { + type: 'list', + name: 'provider', + message: 'Please select a provider:', + choices: ['github'], + when: ({ shouldSetupAuth }) => shouldSetupAuth, }, ]); - Task.log(answers.github ? 'Ok!' : 'Maybe next time'); + if (!answers.shouldSetupAuth) { + // TODO(eide): Can we add a Task.warning() method? + console.log( + chalk.yellow( + 'If you change your mind, feel free to re-run this command.', + ), + ); + process.exit(1); + } + + switch (answers.provider) { + case 'github': { + const { useEnvForSecrets } = answers; + const config = await github(useEnvForSecrets); + await updateConfigFile(config); + if (useEnvForSecrets) { + await updateEnvFile(config); + } + break; + } + default: + throw new Error(`Provider ${answers.provider} not implemented yet.`); + } + + Task.log(`Done setting up ${answers.provider}!`); } diff --git a/packages/cli/src/commands/admin/file.ts b/packages/cli/src/commands/admin/file.ts new file mode 100644 index 0000000000..52a55c3776 --- /dev/null +++ b/packages/cli/src/commands/admin/file.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2023 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 * as path from 'path'; +import * as fs from 'fs-extra'; +import yaml from 'yaml'; +import { findPaths } from '@backstage/cli-common'; +import { GithubAuthConfig } from './github'; + +/* eslint-disable-next-line no-restricted-syntax */ +const { targetRoot } = findPaths(__dirname); +const APP_CONFIG_FILE = path.join(targetRoot, 'app-config.development.yaml'); +const ENV_CONFIG_FILE = path.join(targetRoot, '.env.development'); + +// export const readAppConfig = async (file: string) => { +// return yaml.parse(await fs.readFile(file, 'utf8')); +// }; + +export const updateConfigFile = async (config: GithubAuthConfig) => { + return await fs.writeFile( + APP_CONFIG_FILE, + yaml.stringify(config, { + indent: 2, + }), + 'utf8', + ); +}; + +export const updateEnvFile = async (config: GithubAuthConfig) => { + const content = ` +AUTH_GITHUB_CLIENT_ID=${config.auth.providers.github.clientId} +AUTH_GITHUB_CLIENT_SECRET=${config.auth.providers.github.clientId}`; + + return await fs.writeFile(ENV_CONFIG_FILE, content, 'utf8'); +}; diff --git a/packages/cli/src/commands/admin/github.ts b/packages/cli/src/commands/admin/github.ts new file mode 100644 index 0000000000..5b359ecb33 --- /dev/null +++ b/packages/cli/src/commands/admin/github.ts @@ -0,0 +1,104 @@ +/* + * Copyright 2023 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 chalk from 'chalk'; +import inquirer from 'inquirer'; +import { Task } from '../../lib/tasks'; + +export type GithubAuthConfig = { + auth: { + providers: { + github: { + clientId: string; + clientSecret: string; + enterpriseInstanceUrl?: string; + }; + }; + }; +}; + +export const github = async ( + useEnvForSecrets?: boolean, +): Promise => { + Task.log(` + To add GitHub authentication, you must create an OAuth App from the GitHub developer settings: ${chalk.blue( + 'https://github.com/settings/developers', + )} + The Homepage URL should point to Backstage's frontend, while the Authorization callback URL will point to the auth backend. + + You can find the full documentation page here: ${chalk.blue( + 'https://backstage.io/docs/auth/github/provider', + )} + `); + + const answers = await inquirer.prompt<{ + clientSecret: string; + clientId: string; + hasGithubEnterprise: boolean; + enterpriseInstanceUrl?: string; + }>([ + { + type: 'input', + name: 'clientSecret', + message: 'What is your Client Secret?', + // TODO(eide): Is there another way to validate? + // validate(input) { + // if (/([a-f0-9]{40})/g.test(input)) { + // return true; + // } + + // throw Error('Please provide a valid client secret.'); + // }, + }, + { + type: 'input', + name: 'clientId', + message: 'What is your Client Id?', + }, + { + type: 'confirm', + name: 'hasGithubEnterprise', + message: 'Are you using Github Enterprise?', + }, + { + type: 'input', + name: 'enterpriseInstanceUrl', + message: 'What is your URL for Github Enterprise?', + when: ({ hasGithubEnterprise }) => hasGithubEnterprise, + validate(input: string) { + return Boolean(new URL(input)); + }, + }, + ]); + + return { + auth: { + providers: { + github: { + clientId: useEnvForSecrets + ? '${AUTH_GITHUB_CLIENT_ID}' + : answers.clientId, + clientSecret: useEnvForSecrets + ? '${AUTH_GITHUB_CLIENT_SECRET}' + : answers.clientSecret, + ...(answers.hasGithubEnterprise && { + enterpriseInstanceUrl: answers.enterpriseInstanceUrl, + }), + }, + }, + }, + }; +};