From c24703bd5aeda4feb606fe753ab37e621d7c914d Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Fri, 24 Feb 2023 15:28:02 +0100 Subject: [PATCH] cli: Add GitLab option to setting up auth Signed-off-by: Marcus Eide --- .../cli/src/commands/onboard/auth/config.ts | 43 +++--- .../src/commands/onboard/auth/github/index.ts | 16 +-- .../src/commands/onboard/auth/gitlab/index.ts | 126 ++++++++++++++++++ .../cli/src/commands/onboard/auth/index.ts | 7 +- .../patches/01-github.Add-SignInPage.patch | 1 - .../patches/03-gitlab.Add-SignInPage.patch | 25 ++++ .../04-gitlab.Use-signIn-resolver.patch | 27 ++++ 7 files changed, 215 insertions(+), 30 deletions(-) create mode 100644 packages/cli/src/commands/onboard/auth/gitlab/index.ts create mode 100644 packages/cli/src/commands/onboard/auth/patches/03-gitlab.Add-SignInPage.patch create mode 100644 packages/cli/src/commands/onboard/auth/patches/04-gitlab.Use-signIn-resolver.patch diff --git a/packages/cli/src/commands/onboard/auth/config.ts b/packages/cli/src/commands/onboard/auth/config.ts index 54c103cdf8..01afc5f96c 100644 --- a/packages/cli/src/commands/onboard/auth/config.ts +++ b/packages/cli/src/commands/onboard/auth/config.ts @@ -14,28 +14,35 @@ * limitations under the License. */ +import { UserEntity } from '@backstage/catalog-model'; import * as fs from 'fs-extra'; import yaml from 'yaml'; -type GithubAuthConfig = { +type AuthConfig = { auth: { providers: { - github: { + [key: string]: { development: { clientId: string; clientSecret: string; enterpriseInstanceUrl?: string; + audience?: string; }; }; }; }; - catalog?: { - locations: Array<{ - type: string; - target: string; - rules: Array<{ allow: Array }>; - }>; - }; +}; + +const catalogUserLocation = { + catalog: { + locations: [ + { + type: 'file', + target: '../../user-info.yaml', + rules: [{ allow: ['User'] }], + }, + ], + }, }; const readYaml = async (file: string) => { @@ -44,11 +51,11 @@ const readYaml = async (file: string) => { export const updateConfigFile = async ( file: string, - config: GithubAuthConfig, + authConfig: AuthConfig, ) => { const content = fs.existsSync(file) - ? { ...(await readYaml(file)), ...config } - : config; + ? { ...(await readYaml(file)), ...authConfig, ...catalogUserLocation } + : { ...authConfig, ...catalogUserLocation }; return await fs.writeFile( file, @@ -59,15 +66,17 @@ export const updateConfigFile = async ( ); }; -export const addUserEntity = async (file: string, username: string) => { - const content = { +export const addUserEntity = async ( + file: string, + username: string, + annotations?: Record, +) => { + const content: UserEntity = { apiVersion: 'backstage.io/v1alpha1', kind: 'User', metadata: { name: username, - annotations: { - 'github.com/user-login': username, - }, + annotations: { ...annotations }, }, spec: { memberOf: [], diff --git a/packages/cli/src/commands/onboard/auth/github/index.ts b/packages/cli/src/commands/onboard/auth/github/index.ts index def97bf619..c1789fa64d 100644 --- a/packages/cli/src/commands/onboard/auth/github/index.ts +++ b/packages/cli/src/commands/onboard/auth/github/index.ts @@ -67,15 +67,6 @@ const getConfig = (answers: Answers) => { }, }, }, - catalog: { - locations: [ - { - type: 'file', - target: '../../user-info.yaml', - rules: [{ allow: ['User'] }], - }, - ], - }, }; }; @@ -161,11 +152,14 @@ export const github = async () => { await Task.forItem( 'Creating', USER_ENTITY_FILE, - async () => await addUserEntity(USER_ENTITY_FILE, username), + async () => + await addUserEntity(USER_ENTITY_FILE, username, { + 'github.com/user-login': username, + }), ); const patches = await fs.readdir(PATCH_FOLDER); - for (const patchFile of patches) { + for (const patchFile of patches.filter(p => p.includes('github'))) { await Task.forItem('Patching', patchFile, async () => { await patch(patchFile); }); diff --git a/packages/cli/src/commands/onboard/auth/gitlab/index.ts b/packages/cli/src/commands/onboard/auth/gitlab/index.ts new file mode 100644 index 0000000000..4fa77f6b02 --- /dev/null +++ b/packages/cli/src/commands/onboard/auth/gitlab/index.ts @@ -0,0 +1,126 @@ +/* + * 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 * as fs from 'fs-extra'; +import inquirer from 'inquirer'; +import { Task } from '../../../../lib/tasks'; +import { addUserEntity, updateConfigFile } from '../config'; +import { APP_CONFIG_FILE, PATCH_FOLDER, USER_ENTITY_FILE } from '../files'; +import { patch } from '../patch'; + +const getConfig = (answers: Answers) => { + const { clientId, clientSecret, hasAudience, audience } = answers; + + return { + auth: { + providers: { + gitlab: { + development: { + clientId, + clientSecret, + ...(hasAudience && { + audience, + }), + }, + }, + }, + }, + }; +}; + +type Answers = { + username: string; + clientSecret: string; + clientId: string; + hasAudience: boolean; + audience?: string; +}; + +export const gitlab = async () => { + Task.log(` + To add GitLub authentication, you must create an Application from the GitLab Settings: ${chalk.blue( + 'https://gitlab.com/-/profile/applications', + )} + The Redirect URI should point to your Backstage backend auth handler. + + Settings for local development: + ${chalk.cyan(` + Name: Backstage (or your custom app name) + Redirect URI: http://localhost:7007/api/auth/gitlab/handler/frame + Scopes: read_api and read_user`)} + + You can find the full documentation page here: ${chalk.blue( + 'https://backstage.io/docs/auth/gitlab/provider', + )} + `); + + const answers = await inquirer.prompt([ + { + type: 'input', + name: 'username', + message: 'What is your GitLab username?', + validate: (input: string) => (input.length ? true : false), + }, + { + type: 'input', + name: 'clientId', + message: 'What is your Application Id?', + validate: (input: string) => (input.length ? true : false), + }, + { + type: 'input', + name: 'clientSecret', + message: 'What is your Application Secret?', + validate: (input: string) => (input.length ? true : false), + }, + { + type: 'confirm', + name: 'hasAudience', + message: 'Do you have a self-hosted instance of GitLab?', + }, + { + type: 'input', + name: 'audience', + message: 'What is the URL for your GitLab instance?', + when: ({ hasAudience }) => hasAudience, + validate: (input: string) => Boolean(new URL(input)), + }, + ]); + + const { username } = answers; + const config = getConfig(answers); + + Task.log('Setting up GitLab Authentication for you...'); + + await Task.forItem( + 'Updating', + APP_CONFIG_FILE, + async () => await updateConfigFile(APP_CONFIG_FILE, config), + ); + await Task.forItem( + 'Creating', + USER_ENTITY_FILE, + async () => await addUserEntity(USER_ENTITY_FILE, username), + ); + + const patches = await fs.readdir(PATCH_FOLDER); + for (const patchFile of patches.filter(p => p.includes('gitlab'))) { + await Task.forItem('Patching', patchFile, async () => { + await patch(patchFile); + }); + } +}; diff --git a/packages/cli/src/commands/onboard/auth/index.ts b/packages/cli/src/commands/onboard/auth/index.ts index 279a8cea7d..df9471e1db 100644 --- a/packages/cli/src/commands/onboard/auth/index.ts +++ b/packages/cli/src/commands/onboard/auth/index.ts @@ -18,6 +18,7 @@ import chalk from 'chalk'; import inquirer from 'inquirer'; import { Task } from '../../../lib/tasks'; import { github } from './github'; +import { gitlab } from './gitlab'; export async function auth(): Promise { const answers = await inquirer.prompt<{ @@ -27,7 +28,7 @@ export async function auth(): Promise { type: 'list', name: 'provider', message: 'Please select a provider:', - choices: ['GitHub'], + choices: ['GitHub', 'GitLab'], }, ]); @@ -38,6 +39,10 @@ export async function auth(): Promise { await github(); break; } + case 'GitLab': { + await gitlab(); + break; + } default: throw new Error(`Provider ${provider} not implemented yet.`); } diff --git a/packages/cli/src/commands/onboard/auth/patches/01-github.Add-SignInPage.patch b/packages/cli/src/commands/onboard/auth/patches/01-github.Add-SignInPage.patch index 8a3401fea1..db9f5228e8 100644 --- a/packages/cli/src/commands/onboard/auth/patches/01-github.Add-SignInPage.patch +++ b/packages/cli/src/commands/onboard/auth/patches/01-github.Add-SignInPage.patch @@ -14,7 +14,6 @@ + SignInPage: props => ( + ( ++ ++ ), ++ }, \ No newline at end of file diff --git a/packages/cli/src/commands/onboard/auth/patches/04-gitlab.Use-signIn-resolver.patch b/packages/cli/src/commands/onboard/auth/patches/04-gitlab.Use-signIn-resolver.patch new file mode 100644 index 0000000000..04b432c179 --- /dev/null +++ b/packages/cli/src/commands/onboard/auth/patches/04-gitlab.Use-signIn-resolver.patch @@ -0,0 +1,27 @@ +--- a/packages/backend/src/plugins/auth.ts ++++ b/packages/backend/src/plugins/auth.ts +@@ -38 +38 @@ export default async function createPlugin( +- github: providers.github.create({ ++ gitlab: providers.gitlab.create({ +@@ -40,8 +40,11 @@ export default async function createPlugin( +- resolver(_, ctx) { +- const userRef = 'user:default/guest'; // Must be a full entity reference +- return ctx.issueToken({ +- claims: { +- sub: userRef, // The user's own identity +- ent: [userRef], // A list of identities that the user claims ownership through +- }, +- }); ++ async resolver(info, ctx) { ++ const { fullProfile } = info.result; ++ ++ const userId = fullProfile.username; ++ if (!userId) { ++ throw new Error( ++ `GitLab user profile does not contain a username`, ++ ); ++ } ++ ++ return ctx.signInWithCatalogUser({ entityRef: { name: userId } }); +@@ -49 +51,0 @@ export default async function createPlugin( +- // resolver: providers.github.resolvers.usernameMatchingUserEntityName(), \ No newline at end of file