From 24c509b9e2df5a6382929fa93824e91432a833a4 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Tue, 14 Jul 2020 07:16:20 +0200 Subject: [PATCH 01/34] Created gcp-projects plugin --- .github/CODEOWNERS | 4 +- package.json | 3 + packages/app/package.json | 1 + packages/app/src/apis.ts | 2 + packages/app/src/plugins.ts | 1 + plugins/gcp-projects/.eslintrc.js | 3 + plugins/gcp-projects/README.md | 13 ++ plugins/gcp-projects/dev/index.tsx | 22 +++ plugins/gcp-projects/package.json | 47 ++++++ plugins/gcp-projects/src/api/GCPApi.ts | 34 ++++ plugins/gcp-projects/src/api/GCPClient.ts | 125 ++++++++++++++ plugins/gcp-projects/src/api/index.ts | 19 +++ plugins/gcp-projects/src/api/types.ts | 41 +++++ .../NewProjectPage/NewProjectPage.tsx | 119 ++++++++++++++ .../src/components/NewProjectPage/index.ts | 17 ++ .../ProjectDetailsPage/ProjectDetailsPage.tsx | 153 +++++++++++++++++ .../components/ProjectDetailsPage/index.ts | 17 ++ .../ProjectListPage/ProjectListPage.tsx | 155 ++++++++++++++++++ .../src/components/ProjectListPage/index.ts | 17 ++ plugins/gcp-projects/src/index.ts | 18 ++ plugins/gcp-projects/src/plugin.test.ts | 23 +++ plugins/gcp-projects/src/plugin.ts | 42 +++++ plugins/gcp-projects/src/setupTests.ts | 19 +++ yarn.lock | 83 +++++++++- 24 files changed, 970 insertions(+), 8 deletions(-) create mode 100644 plugins/gcp-projects/.eslintrc.js create mode 100644 plugins/gcp-projects/README.md create mode 100644 plugins/gcp-projects/dev/index.tsx create mode 100644 plugins/gcp-projects/package.json create mode 100644 plugins/gcp-projects/src/api/GCPApi.ts create mode 100644 plugins/gcp-projects/src/api/GCPClient.ts create mode 100644 plugins/gcp-projects/src/api/index.ts create mode 100644 plugins/gcp-projects/src/api/types.ts create mode 100644 plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx create mode 100644 plugins/gcp-projects/src/components/NewProjectPage/index.ts create mode 100644 plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx create mode 100644 plugins/gcp-projects/src/components/ProjectDetailsPage/index.ts create mode 100644 plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx create mode 100644 plugins/gcp-projects/src/components/ProjectListPage/index.ts create mode 100644 plugins/gcp-projects/src/index.ts create mode 100644 plugins/gcp-projects/src/plugin.test.ts create mode 100644 plugins/gcp-projects/src/plugin.ts create mode 100644 plugins/gcp-projects/src/setupTests.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 751ec4914d..7d941588c7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,6 +4,6 @@ # The last matching pattern takes precedence. # https://help.github.com/articles/about-codeowners/ -* @spotify/backstage-core -/plugins/techdocs @spotify/techdocs-core +* @spotify/backstage-core /packages/techdocs-cli @spotify/techdocs-core +/plugins/techdocs @spotify/techdocs-core diff --git a/package.json b/package.json index cc45d541d4..125dc285aa 100644 --- a/package.json +++ b/package.json @@ -53,5 +53,8 @@ "*.{json,md}": [ "prettier --write" ] + }, + "dependencies": { + "ts-node": "^8.10.2" } } diff --git a/packages/app/package.json b/packages/app/package.json index ed256e6c2d..88e2e630a7 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -8,6 +8,7 @@ "@backstage/plugin-catalog": "^0.1.1-alpha.13", "@backstage/plugin-circleci": "^0.1.1-alpha.13", "@backstage/plugin-explore": "^0.1.1-alpha.13", + "@backstage/plugin-gcp-projects": "^0.1.1-alpha.13", "@backstage/plugin-github-actions": "^0.1.1-alpha.13", "@backstage/plugin-gitops-profiles": "^0.1.1-alpha.13", "@backstage/plugin-graphiql": "^0.1.1-alpha.13", diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index abc490b658..f99a88b345 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -58,6 +58,7 @@ import { import { scaffolderApiRef, ScaffolderApi } from '@backstage/plugin-scaffolder'; import { rollbarApiRef, RollbarClient } from '@backstage/plugin-rollbar'; +import { GCPClient, GCPApiRef } from '@backstage/plugin-gcp-projects'; export const apis = (config: ConfigApi) => { // eslint-disable-next-line no-console @@ -75,6 +76,7 @@ export const apis = (config: ConfigApi) => { builder.add(storageApiRef, WebStorage.create({ errorApi })); builder.add(circleCIApiRef, new CircleCIApi()); + builder.add(GCPApiRef, new GCPClient()); builder.add(featureFlagsApiRef, new FeatureFlags()); builder.add(lighthouseApiRef, new LighthouseRestApi('http://localhost:3003')); diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index 46c8f23ef0..19a359043b 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -27,3 +27,4 @@ export { plugin as TechDocs } from '@backstage/plugin-techdocs'; export { plugin as GraphiQL } from '@backstage/plugin-graphiql'; export { plugin as GithubActions } from '@backstage/plugin-github-actions'; export { plugin as Rollbar } from '@backstage/plugin-rollbar'; +export { plugin as GcpProjects } from '@backstage/plugin-gcp-projects'; diff --git a/plugins/gcp-projects/.eslintrc.js b/plugins/gcp-projects/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/gcp-projects/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/gcp-projects/README.md b/plugins/gcp-projects/README.md new file mode 100644 index 0000000000..6a2ab63c96 --- /dev/null +++ b/plugins/gcp-projects/README.md @@ -0,0 +1,13 @@ +# gcp-projects + +Welcome to the gcp-projects plugin! + +_This plugin was created through the Backstage CLI_ + +## Getting started + +Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/gcp-projects](http://localhost:3000/gcp-projects). + +You can also serve the plugin in isolation by running `yarn start` in the plugin directory. +This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. +It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory. diff --git a/plugins/gcp-projects/dev/index.tsx b/plugins/gcp-projects/dev/index.tsx new file mode 100644 index 0000000000..d97643057b --- /dev/null +++ b/plugins/gcp-projects/dev/index.tsx @@ -0,0 +1,22 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { createDevApp } from '@backstage/dev-utils'; +import { plugin } from '../src/plugin'; + +createDevApp() + .registerPlugin(plugin) + .render(); diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json new file mode 100644 index 0000000000..2bef7feef4 --- /dev/null +++ b/plugins/gcp-projects/package.json @@ -0,0 +1,47 @@ +{ + "name": "@backstage/plugin-gcp-projects", + "version": "0.1.1-alpha.13", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": true, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/core": "^0.1.1-alpha.13", + "@backstage/theme": "^0.1.1-alpha.13", + "@material-ui/core": "^4.9.1", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-router-dom": "^5.2.0", + "react-use": "^14.2.0" + }, + "devDependencies": { + "@backstage/cli": "^0.1.1-alpha.13", + "@backstage/dev-utils": "^0.1.1-alpha.13", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", + "@types/jest": "^25.2.2", + "@types/node": "^12.0.0", + "jest-fetch-mock": "^3.0.3" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/gcp-projects/src/api/GCPApi.ts b/plugins/gcp-projects/src/api/GCPApi.ts new file mode 100644 index 0000000000..95b686815e --- /dev/null +++ b/plugins/gcp-projects/src/api/GCPApi.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { createApiRef } from '@backstage/core'; +import { Project, Operation } from './types'; + +export const GCPApiRef = createApiRef({ + id: 'plugin.gcpprojects.service', + description: 'Used by the GCP Projects plugin to make requests', +}); + +export type GCPApi = { + listProjects: ({ token }: { token: string }) => Promise; + getProject: (projectId: string, token: Promise) => Promise; + createProject: ( + projectName: string, + projectId: string, + owner: string, + token: string, + ) => Promise; +}; diff --git a/plugins/gcp-projects/src/api/GCPClient.ts b/plugins/gcp-projects/src/api/GCPClient.ts new file mode 100644 index 0000000000..fd8b708377 --- /dev/null +++ b/plugins/gcp-projects/src/api/GCPClient.ts @@ -0,0 +1,125 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { GCPApi } from './GCPApi'; +import { Project, Operation, Status } from './types'; + +export class GCPClient implements GCPApi { + async listProjects({ token }: { token: string }): Promise { + const url = `https://content-cloudresourcemanager.googleapis.com/v1/projects`; + + const response = await fetch(url, { + headers: new Headers({ + Accept: '*/*', + Authorization: `Bearer ${token}`, + }), + }); + + if (!response.ok) { + return [ + { + name: 'Error', + projectNumber: 'Response status is not OK', + projectId: 'Error', + lifecycleState: 'error', + createTime: 'Error', + }, + ]; + } + + const data = await response.json(); + + return data.projects; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async getProject( + projectId: string, + token: Promise, + ): Promise { + const url = `https://content-cloudresourcemanager.googleapis.com/v1/projects/${projectId}`; + const response = await fetch(url, { + headers: new Headers({ + Authorization: `Bearer ${await token}`, + }), + }); + + const dataBlank: Project = { + name: 'Error', + projectNumber: `Response status is${response.status}`, + projectId: 'Error', + lifecycleState: 'error', + createTime: 'Error', + }; + + if (!response.ok) { + return dataBlank; + } + + const data = await response.json(); + + const newData: Project = data; + + return newData; + } + + async createProject( + projectName: string, + projectId: string, + token: string, + ): Promise { + const status: Status = { + code: 0, + message: '', + details: [], + }; + + const op: Operation = { + name: '', + metadata: '', + done: true, + error: status, + response: '', + }; + + const newProject: Project = { + name: projectName, + projectId: projectId, + }; + + const body = JSON.stringify(newProject); + + const url = `https://content-cloudresourcemanager.googleapis.com/v1/projects`; + + const response = await fetch(url, { + headers: new Headers({ + Accept: '*/*', + Authorization: `Bearer ${token}`, + }), + body: body, + method: 'POST', + }); + + if (!response.ok) { + status.code = response.status; + return op; + } + + const data = await response.json(); + + return data; + } +} diff --git a/plugins/gcp-projects/src/api/index.ts b/plugins/gcp-projects/src/api/index.ts new file mode 100644 index 0000000000..51f617f19c --- /dev/null +++ b/plugins/gcp-projects/src/api/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2020 Spotify AB + * + * 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. + */ + +export * from './GCPApi'; +export * from './GCPClient'; +export * from './types'; diff --git a/plugins/gcp-projects/src/api/types.ts b/plugins/gcp-projects/src/api/types.ts new file mode 100644 index 0000000000..8dccaec5e2 --- /dev/null +++ b/plugins/gcp-projects/src/api/types.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2020 Spotify AB + * + * 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. + */ + +export type Project = { + name: string; + projectNumber?: string; + projectId: string; + lifecycleState?: string; + createTime?: string; +}; + +export type ProjectDetails = { + details: string; +}; + +export type Operation = { + name: string; + metadata: string; + done: boolean; + error: Status; + response: string; +}; + +export type Status = { + code: number; + message: string; + details: string[]; +}; diff --git a/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx b/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx new file mode 100644 index 0000000000..bfa2f158ce --- /dev/null +++ b/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx @@ -0,0 +1,119 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 React, { FC, useState } from 'react'; +import { Grid, Button, TextField } from '@material-ui/core'; + +import { + InfoCard, + Header, + Page, + pageTheme, + Content, + ContentHeader, + HeaderLabel, + SupportButton, + SimpleStepper, + SimpleStepperStep, + StructuredMetadataTable, +} from '@backstage/core'; + +export const NewProjectPage: FC<{}> = () => { + const [projectName, setProjectName] = useState(''); + const [projectId, setProjectId] = useState(''); + const [done, setDone] = useState(false); + + const metadata = { + ProjectName: projectName, + ProjectId: projectId, + }; + + return ( + +
+ + +
+ + + + This plugin will help you create a project in gcp. + + + + + + + + setProjectName(e.target.value)} + value={projectName} + fullWidth + /> + + + setProjectId(e.target.value)} + value={projectId} + fullWidth + /> + + + + + + + + {done === true ? ( + + + +
+
+
+ +
+
+ ) : ( +
+ )} +
+
+
+ ); +}; diff --git a/plugins/gcp-projects/src/components/NewProjectPage/index.ts b/plugins/gcp-projects/src/components/NewProjectPage/index.ts new file mode 100644 index 0000000000..1d2f023887 --- /dev/null +++ b/plugins/gcp-projects/src/components/NewProjectPage/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * 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. + */ + +export { NewProjectPage } from './NewProjectPage'; diff --git a/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx b/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx new file mode 100644 index 0000000000..d9721bf549 --- /dev/null +++ b/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx @@ -0,0 +1,153 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { + Button, + ButtonGroup, + LinearProgress, + makeStyles, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableRow, + Theme, + Typography, +} from '@material-ui/core'; +import React from 'react'; +import { useLocation } from 'react-router-dom'; +import { useAsync } from 'react-use'; +import { Link, useApi, googleAuthApiRef } from '@backstage/core'; +import { GCPApiRef } from '../../api'; + +const useStyles = makeStyles(theme => ({ + root: { + maxWidth: 720, + margin: theme.spacing(2), + }, + title: { + padding: theme.spacing(1, 0, 2, 0), + }, + table: { + padding: theme.spacing(1), + }, +})); + +export const ProjectDetailsPage = () => { + const api = useApi(GCPApiRef); + const googleApi = useApi(googleAuthApiRef); + const token = googleApi.getAccessToken( + 'https://www.googleapis.com/auth/cloud-platform.read-only', + ); + + const classes = useStyles(); + const location = useLocation(); + const status = useAsync( + () => + api.getProject( + decodeURIComponent(location.search.split('projectId=')[1]), + token, + ), + [location.search], + ); + + if (status.loading) { + return ; + } else if (status.error) { + return ( + + Failed to load build, {status.error.message} + + ); + } + + const details = status.value; + + return ( +
+ + + + < + + + Build Details + + + + + + + Name + + {details?.name} + + + + Project Number + + {details?.projectNumber} + + + + Project ID + + {details?.projectId} + + + + State + + + {details?.lifecycleState} + + + + + Creation Time + + {details?.createTime} + + + + Links + + + + {details?.name && ( + + )} + {details?.name && ( + + )} + + + + +
+
+
+ ); +}; diff --git a/plugins/gcp-projects/src/components/ProjectDetailsPage/index.ts b/plugins/gcp-projects/src/components/ProjectDetailsPage/index.ts new file mode 100644 index 0000000000..e9b6eb095a --- /dev/null +++ b/plugins/gcp-projects/src/components/ProjectDetailsPage/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * 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. + */ + +export { ProjectDetailsPage } from './ProjectDetailsPage'; diff --git a/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx b/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx new file mode 100644 index 0000000000..8be1b47857 --- /dev/null +++ b/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx @@ -0,0 +1,155 @@ +/* + * Copyright 2020 Spotify AB + * + * 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. + */ + +// NEEDS WORK + +import { Link, useApi, googleAuthApiRef, InfoCard } from '@backstage/core'; +import { + LinearProgress, + makeStyles, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Theme, + Tooltip, + Typography, + Button, +} from '@material-ui/core'; +import React from 'react'; +import { useAsync } from 'react-use'; +import { GCPApiRef, Project } from '../../api'; + +const LongText = ({ text, max }: { text: string; max: number }) => { + if (text.length < max) { + return {text}; + } + return ( + + {text.slice(0, max)}... + + ); +}; + +const useStyles = makeStyles(theme => ({ + root: { + padding: theme.spacing(2), + }, + title: { + padding: theme.spacing(1, 0, 2, 0), + }, +})); + +const PageContents = () => { + const api = useApi(GCPApiRef); + const googleApi = useApi(googleAuthApiRef); + + const { loading, error, value } = useAsync(async () => { + const token = await googleApi.getAccessToken( + 'https://www.googleapis.com/auth/cloud-platform.read-only', + ); + + const projects = api.listProjects({ token }); + return projects; + }); + + if (loading) { + return ; + } + + if (error) { + return ( + + Failed to load projects, {error.message}{' '} + + ); + } + + return ( + + + + + Name + Project Number + Project ID + State + Creation Time + + + + {value?.map((project: Project) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ))} + +
+
+ ); +}; + +export const ProjectListPage = () => { + const classes = useStyles(); + + return ( +
+ + GCP Projects + + + + + + +
+ ); +}; diff --git a/plugins/gcp-projects/src/components/ProjectListPage/index.ts b/plugins/gcp-projects/src/components/ProjectListPage/index.ts new file mode 100644 index 0000000000..c2b0479cef --- /dev/null +++ b/plugins/gcp-projects/src/components/ProjectListPage/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * 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. + */ + +export { ProjectListPage } from './ProjectListPage'; diff --git a/plugins/gcp-projects/src/index.ts b/plugins/gcp-projects/src/index.ts new file mode 100644 index 0000000000..d67bc6a864 --- /dev/null +++ b/plugins/gcp-projects/src/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 Spotify AB + * + * 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. + */ + +export { plugin } from './plugin'; +export * from './api'; diff --git a/plugins/gcp-projects/src/plugin.test.ts b/plugins/gcp-projects/src/plugin.test.ts new file mode 100644 index 0000000000..86e909995f --- /dev/null +++ b/plugins/gcp-projects/src/plugin.test.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { plugin } from './plugin'; + +describe('gcp-projects', () => { + it('should export plugin', () => { + expect(plugin).toBeDefined(); + }); +}); diff --git a/plugins/gcp-projects/src/plugin.ts b/plugins/gcp-projects/src/plugin.ts new file mode 100644 index 0000000000..29c7d74434 --- /dev/null +++ b/plugins/gcp-projects/src/plugin.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { createPlugin, createRouteRef } from '@backstage/core'; +import { ProjectListPage } from './components/ProjectListPage'; +import { ProjectDetailsPage } from './components/ProjectDetailsPage'; +import { NewProjectPage } from './components/NewProjectPage'; + +export const rootRouteRef = createRouteRef({ + path: '/gcp-projects', + title: 'GCP Projects', +}); +export const ProjectRouteRef = createRouteRef({ + path: '/gcp-projects/project', + title: 'GCP Project Page', +}); +export const NewProjectRouteRef = createRouteRef({ + path: '/gcp-projects/new', + title: 'GCP Project Page', +}); + +export const plugin = createPlugin({ + id: 'gcp-projects', + register({ router }) { + router.addRoute(rootRouteRef, ProjectListPage); + router.addRoute(ProjectRouteRef, ProjectDetailsPage); + router.addRoute(NewProjectRouteRef, NewProjectPage); + }, +}); diff --git a/plugins/gcp-projects/src/setupTests.ts b/plugins/gcp-projects/src/setupTests.ts new file mode 100644 index 0000000000..8553642152 --- /dev/null +++ b/plugins/gcp-projects/src/setupTests.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 '@testing-library/jest-dom'; + +require('jest-fetch-mock').enableMocks(); diff --git a/yarn.lock b/yarn.lock index 0fb5368314..7620f8832d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9942,6 +9942,18 @@ highlight.js@~9.15.0, highlight.js@~9.15.1: resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-9.15.10.tgz#7b18ed75c90348c045eef9ed08ca1319a2219ad2" integrity sha512-RoV7OkQm0T3os3Dd2VHLNMoaoDVx77Wygln3n9l5YV172XonWG6rgQD3XnF/BuFFZw9A0TJgmMSO8FEWQgvcXw== +history@^4.9.0: + version "4.10.1" + resolved "https://registry.npmjs.org/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" + integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== + dependencies: + "@babel/runtime" "^7.1.2" + loose-envify "^1.2.0" + resolve-pathname "^3.0.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + value-equal "^1.0.1" + history@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/history/-/history-5.0.0.tgz#0cabbb6c4bbf835addb874f8259f6d25101efd08" @@ -9958,7 +9970,7 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -11090,6 +11102,11 @@ is-yarn-global@^0.3.0: resolved "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -12430,7 +12447,7 @@ loglevel@^1.6.8: resolved "https://registry.npmjs.org/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171" integrity sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA== -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.0, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -12889,6 +12906,14 @@ min-indent@^1.0.0: resolved "https://registry.npmjs.org/min-indent/-/min-indent-1.0.0.tgz#cfc45c37e9ec0d8f0a0ec3dd4ef7f7c3abe39256" integrity sha1-z8RcN+nsDY8KDsPdTvf3w6vjklY= +mini-create-react-context@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.0.tgz#df60501c83151db69e28eac0ef08b4002efab040" + integrity sha512-b0TytUgFSbgFJGzJqXPKCFCBWigAjpjo+Fl7Vf7ZbKRDptszpppKxXH6DRXEABZ/gcEQczeb0iZ7JvL8e8jjCA== + dependencies: + "@babel/runtime" "^7.5.5" + tiny-warning "^1.0.3" + mini-css-extract-plugin@^0.7.0: version "0.7.0" resolved "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.7.0.tgz#5ba8290fbb4179a43dd27cca444ba150bee743a0" @@ -14369,6 +14394,13 @@ path-to-regexp@2.2.1: resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.2.1.tgz#90b617025a16381a879bc82a38d4e8bdeb2bcf45" integrity sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ== +path-to-regexp@^1.7.0: + version "1.8.0" + resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" + integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + dependencies: + isarray "0.0.1" + path-type@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -15646,7 +15678,7 @@ react-inspector@^4.0.0: is-dom "^1.0.9" prop-types "^15.6.1" -react-is@^16.12.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0: +react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0: version "16.13.1" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -15725,6 +15757,35 @@ react-router-dom@6.0.0-beta.0: prop-types "^15.7.2" react-router "6.0.0-beta.0" +react-router-dom@^5.2.0: + version "5.2.0" + resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662" + integrity sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA== + dependencies: + "@babel/runtime" "^7.1.2" + history "^4.9.0" + loose-envify "^1.3.1" + prop-types "^15.6.2" + react-router "5.2.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + +react-router@5.2.0: + version "5.2.0" + resolved "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz#424e75641ca8747fbf76e5ecca69781aa37ea293" + integrity sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw== + dependencies: + "@babel/runtime" "^7.1.2" + history "^4.9.0" + hoist-non-react-statics "^3.1.0" + loose-envify "^1.3.1" + mini-create-react-context "^0.4.0" + path-to-regexp "^1.7.0" + prop-types "^15.6.2" + react-is "^16.6.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + react-router@6.0.0-beta.0: version "6.0.0-beta.0" resolved "https://registry.npmjs.org/react-router/-/react-router-6.0.0-beta.0.tgz#3e11f39b6ded4412c2fed9e4f989dd4c8156724d" @@ -16342,6 +16403,11 @@ resolve-from@^5.0.0: resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== +resolve-pathname@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" + integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== + resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" @@ -18185,12 +18251,12 @@ tiny-emitter@^2.0.0: resolved "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== -tiny-invariant@^1.0.6: +tiny-invariant@^1.0.2, tiny-invariant@^1.0.6: version "1.1.0" resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw== -tiny-warning@^1.0.2: +tiny-warning@^1.0.0, tiny-warning@^1.0.2, tiny-warning@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== @@ -18406,7 +18472,7 @@ ts-loader@^7.0.4: micromatch "^4.0.0" semver "^6.0.0" -ts-node@^8.6.2: +ts-node@^8.10.2, ts-node@^8.6.2: version "8.10.2" resolved "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz#eee03764633b1234ddd37f8db9ec10b75ec7fb8d" integrity sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA== @@ -18987,6 +19053,11 @@ validate.io-number@^1.0.3: resolved "https://registry.npmjs.org/validate.io-number/-/validate.io-number-1.0.3.tgz#f63ffeda248bf28a67a8d48e0e3b461a1665baf8" integrity sha1-9j/+2iSL8opnqNSODjtGGhZluvg= +value-equal@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" + integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== + vary@^1, vary@~1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" From 91dfb7d749dbbf43869098f17d59a8f1d7b9a28b Mon Sep 17 00:00:00 2001 From: ebarrios Date: Tue, 14 Jul 2020 07:19:04 +0200 Subject: [PATCH 02/34] Reverted order change in CODEOWNERS --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7d941588c7..1622f2dfcf 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,5 +5,5 @@ # https://help.github.com/articles/about-codeowners/ * @spotify/backstage-core -/packages/techdocs-cli @spotify/techdocs-core /plugins/techdocs @spotify/techdocs-core +/packages/techdocs-cli @spotify/techdocs-core From e42c1ae02b7c368320aee28bd4e157a487312cef Mon Sep 17 00:00:00 2001 From: ebarrios Date: Tue, 14 Jul 2020 07:19:52 +0200 Subject: [PATCH 03/34] Removed space on CODEOWNERS file --- .github/CODEOWNERS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1622f2dfcf..751ec4914d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,6 +4,6 @@ # The last matching pattern takes precedence. # https://help.github.com/articles/about-codeowners/ -* @spotify/backstage-core -/plugins/techdocs @spotify/techdocs-core +* @spotify/backstage-core +/plugins/techdocs @spotify/techdocs-core /packages/techdocs-cli @spotify/techdocs-core From 461f16250a348623257b55c6bdd91f4b9385e870 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Tue, 14 Jul 2020 07:26:15 +0200 Subject: [PATCH 04/34] Removed trivago name on the title for creating a new GCP Project --- .../src/components/NewProjectPage/NewProjectPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx b/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx index bfa2f158ce..5588884a2a 100644 --- a/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx +++ b/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx @@ -48,7 +48,7 @@ export const NewProjectPage: FC<{}> = () => { - + This plugin will help you create a project in gcp. From 07128d499b4a47076693365ba890cb8bd43c2d96 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Tue, 14 Jul 2020 07:40:08 +0200 Subject: [PATCH 05/34] Added or to not mandatory values in the project type for printing if they are not set on the response --- .../src/components/ProjectListPage/ProjectListPage.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx b/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx index 8be1b47857..00dac5c70e 100644 --- a/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx +++ b/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx @@ -103,7 +103,7 @@ const PageContents = () => { - + @@ -119,12 +119,15 @@ const PageContents = () => { - + - + From 8fbe0d32f4d2803f9105e3fb0af74ff1cbae9689 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Tue, 14 Jul 2020 08:11:00 +0200 Subject: [PATCH 06/34] Added baseURL per recomendation from @stefanalund --- packages/backend/package.json | 2 +- plugins/gcp-projects/src/api/GCPClient.ts | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index 2844f17744..9e42f6c4cb 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -18,7 +18,6 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/plugin-proxy-backend": "^0.1.1-alpha.13", "@backstage/backend-common": "^0.1.1-alpha.13", "@backstage/catalog-model": "^0.1.1-alpha.13", "@backstage/config": "^0.1.1-alpha.13", @@ -26,6 +25,7 @@ "@backstage/plugin-auth-backend": "^0.1.1-alpha.13", "@backstage/plugin-catalog-backend": "^0.1.1-alpha.13", "@backstage/plugin-identity-backend": "^0.1.1-alpha.13", + "@backstage/plugin-proxy-backend": "^0.1.1-alpha.13", "@backstage/plugin-rollbar-backend": "^0.1.1-alpha.13", "@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.13", "@backstage/plugin-sentry-backend": "^0.1.1-alpha.13", diff --git a/plugins/gcp-projects/src/api/GCPClient.ts b/plugins/gcp-projects/src/api/GCPClient.ts index fd8b708377..dc4fe21132 100644 --- a/plugins/gcp-projects/src/api/GCPClient.ts +++ b/plugins/gcp-projects/src/api/GCPClient.ts @@ -17,11 +17,12 @@ import { GCPApi } from './GCPApi'; import { Project, Operation, Status } from './types'; +const BaseURL = + 'https://content-cloudresourcemanager.googleapis.com/v1/projects'; + export class GCPClient implements GCPApi { async listProjects({ token }: { token: string }): Promise { - const url = `https://content-cloudresourcemanager.googleapis.com/v1/projects`; - - const response = await fetch(url, { + const response = await fetch(BaseURL, { headers: new Headers({ Accept: '*/*', Authorization: `Bearer ${token}`, @@ -50,7 +51,7 @@ export class GCPClient implements GCPApi { projectId: string, token: Promise, ): Promise { - const url = `https://content-cloudresourcemanager.googleapis.com/v1/projects/${projectId}`; + const url = `${BaseURL}/${projectId}`; const response = await fetch(url, { headers: new Headers({ Authorization: `Bearer ${await token}`, @@ -59,7 +60,7 @@ export class GCPClient implements GCPApi { const dataBlank: Project = { name: 'Error', - projectNumber: `Response status is${response.status}`, + projectNumber: `Response status is ${response.status}`, projectId: 'Error', lifecycleState: 'error', createTime: 'Error', @@ -102,9 +103,7 @@ export class GCPClient implements GCPApi { const body = JSON.stringify(newProject); - const url = `https://content-cloudresourcemanager.googleapis.com/v1/projects`; - - const response = await fetch(url, { + const response = await fetch(BaseURL, { headers: new Headers({ Accept: '*/*', Authorization: `Bearer ${token}`, From 18fc7c99f617d55a59b32c34b44f77175bf5ab54 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Tue, 14 Jul 2020 08:27:22 +0200 Subject: [PATCH 07/34] Removed some extra header on the create new GCP Project --- .../NewProjectPage/NewProjectPage.tsx | 124 ++++++++---------- 1 file changed, 57 insertions(+), 67 deletions(-) diff --git a/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx b/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx index 5588884a2a..57695dd6bc 100644 --- a/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx +++ b/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx @@ -42,78 +42,68 @@ export const NewProjectPage: FC<{}> = () => { }; return ( - -
- - -
- - - - This plugin will help you create a project in gcp. - - - - - - - - setProjectName(e.target.value)} - value={projectName} - fullWidth - /> - - - setProjectId(e.target.value)} - value={projectId} - fullWidth - /> - - - - - - - - {done === true ? ( - - - -
-
-
+ + + + + + + + setProjectName(e.target.value)} + value={projectName} + fullWidth + /> + + + setProjectId(e.target.value)} + value={projectId} + fullWidth + /> + + - - - ) : ( -
- )} + + +
-
-
+ {done === true ? ( + + + +
+
+
+ +
+
+ ) : ( +
+ )} + +
); }; From f9b4a2bd35c028f596673dde656a0ddd88d24dc2 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Tue, 14 Jul 2020 09:37:38 +0200 Subject: [PATCH 08/34] Removed unsused imports from NewProjectPage --- .../src/components/NewProjectPage/NewProjectPage.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx b/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx index 57695dd6bc..0eddcf09e5 100644 --- a/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx +++ b/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx @@ -19,13 +19,8 @@ import { Grid, Button, TextField } from '@material-ui/core'; import { InfoCard, - Header, - Page, - pageTheme, Content, ContentHeader, - HeaderLabel, - SupportButton, SimpleStepper, SimpleStepperStep, StructuredMetadataTable, From 2c13894037705b392cb3d2c31f45c5b8ed1eb5ae Mon Sep 17 00:00:00 2001 From: ebarrios Date: Tue, 28 Jul 2020 15:48:14 +0200 Subject: [PATCH 09/34] Modify the UI per request of the backstage team --- .../NewProjectPage/NewProjectPage.tsx | 73 +++++--- .../ProjectDetailsPage/ProjectDetailsPage.tsx | 169 ++++++++++-------- .../ProjectListPage/ProjectListPage.tsx | 58 +++--- 3 files changed, 173 insertions(+), 127 deletions(-) diff --git a/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx b/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx index 0eddcf09e5..f09044f3ab 100644 --- a/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx +++ b/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { FC, useState } from 'react'; +import React, { FC, useState, Fragment } from 'react'; import { Grid, Button, TextField } from '@material-ui/core'; import { @@ -24,9 +24,14 @@ import { SimpleStepper, SimpleStepperStep, StructuredMetadataTable, + HeaderLabel, + Page, + Header, + pageTheme, + SupportButton, } from '@backstage/core'; -export const NewProjectPage: FC<{}> = () => { +export const Project: FC<{}> = () => { const [projectName, setProjectName] = useState(''); const [projectId, setProjectId] = useState(''); const [done, setDone] = useState(false); @@ -38,10 +43,9 @@ export const NewProjectPage: FC<{}> = () => { return ( - - - + + = () => { /> + + - {done === true ? ( - - - -
-
-
- -
-
- ) : ( -
- )}
); }; + +const labels = ( + <> + + + +); + +export const NewProjectPage = () => { + return ( + +
+ {labels} +
+ + + Support Button + + + +
+ ); +}; diff --git a/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx b/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx index d9721bf549..713918d8a2 100644 --- a/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx +++ b/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx @@ -28,10 +28,19 @@ import { Theme, Typography, } from '@material-ui/core'; +import { + useApi, + googleAuthApiRef, + HeaderLabel, + Page, + Header, + pageTheme, + SupportButton, + Content, + ContentHeader, +} from '@backstage/core'; import React from 'react'; -import { useLocation } from 'react-router-dom'; import { useAsync } from 'react-use'; -import { Link, useApi, googleAuthApiRef } from '@backstage/core'; import { GCPApiRef } from '../../api'; const useStyles = makeStyles(theme => ({ @@ -47,7 +56,7 @@ const useStyles = makeStyles(theme => ({ }, })); -export const ProjectDetailsPage = () => { +const DetailsPage = () => { const api = useApi(GCPApiRef); const googleApi = useApi(googleAuthApiRef); const token = googleApi.getAccessToken( @@ -55,7 +64,6 @@ export const ProjectDetailsPage = () => { ); const classes = useStyles(); - const location = useLocation(); const status = useAsync( () => api.getProject( @@ -78,76 +86,87 @@ export const ProjectDetailsPage = () => { const details = status.value; return ( -
- - - - < - - - Build Details - - - - - - - Name - - {details?.name} - - - - Project Number - - {details?.projectNumber} - - - - Project ID - - {details?.projectId} - - - - State - - - {details?.lifecycleState} - - - - - Creation Time - - {details?.createTime} - - - - Links - - - - {details?.name && ( - - )} - {details?.name && ( - - )} - - - - -
-
-
+ + + + + + Name + + {details?.name} + + + + Project Number + + {details?.projectNumber} + + + + Project ID + + {details?.projectId} + + + + State + + {details?.lifecycleState} + + + + Creation Time + + {details?.createTime} + + + + Links + + + + {details?.name && ( + + )} + {details?.name && ( + + )} + + + + +
+
+ ); +}; + +const labels = ( + <> + + + +); + +export const ProjectDetailsPage = () => { + return ( + +
+ {labels} +
+ + + Support Button + + + +
); }; diff --git a/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx b/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx index 00dac5c70e..163d8a54d8 100644 --- a/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx +++ b/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx @@ -16,7 +16,19 @@ // NEEDS WORK -import { Link, useApi, googleAuthApiRef, InfoCard } from '@backstage/core'; +import { + Link, + useApi, + googleAuthApiRef, + InfoCard, + HeaderLabel, + Page, + Header, + pageTheme, + SupportButton, + Content, + ContentHeader, +} from '@backstage/core'; import { LinearProgress, makeStyles, @@ -47,14 +59,12 @@ const LongText = ({ text, max }: { text: string; max: number }) => { ); }; -const useStyles = makeStyles(theme => ({ - root: { - padding: theme.spacing(2), - }, - title: { - padding: theme.spacing(1, 0, 2, 0), - }, -})); +const labels = ( + <> + + + +); const PageContents = () => { const api = useApi(GCPApiRef); @@ -76,7 +86,7 @@ const PageContents = () => { if (error) { return ( - Failed to load projects, {error.message}{' '} + {error.message}{' '} ); } @@ -139,20 +149,20 @@ const PageContents = () => { }; export const ProjectListPage = () => { - const classes = useStyles(); - return ( -
- - GCP Projects - - - - - - -
+ +
+ {labels} +
+ + + + All your software catalog entities + + + +
); }; From 082d667854ed763d3377980541638382ab67fcfd Mon Sep 17 00:00:00 2001 From: ebarrios Date: Tue, 28 Jul 2020 17:12:54 +0200 Subject: [PATCH 10/34] Clean up for yarn tsc and fix the simple settep for project creation --- .../NewProjectPage/NewProjectPage.tsx | 32 ++++++++++++------- .../ProjectListPage/ProjectListPage.tsx | 3 -- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx b/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx index f09044f3ab..4d439bbc0d 100644 --- a/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx +++ b/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { FC, useState, Fragment } from 'react'; +import React, { FC, useState } from 'react'; import { Grid, Button, TextField } from '@material-ui/core'; import { @@ -34,7 +34,7 @@ import { export const Project: FC<{}> = () => { const [projectName, setProjectName] = useState(''); const [projectId, setProjectId] = useState(''); - const [done, setDone] = useState(false); + const [disabled, setDisabled] = useState(true); const metadata = { ProjectName: projectName, @@ -69,17 +69,15 @@ export const Project: FC<{}> = () => { fullWidth /> - + + setDisabled(false), + }} + > - + diff --git a/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx b/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx index 163d8a54d8..8d612f75f8 100644 --- a/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx +++ b/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx @@ -20,7 +20,6 @@ import { Link, useApi, googleAuthApiRef, - InfoCard, HeaderLabel, Page, Header, @@ -31,7 +30,6 @@ import { } from '@backstage/core'; import { LinearProgress, - makeStyles, Paper, Table, TableBody, @@ -39,7 +37,6 @@ import { TableContainer, TableHead, TableRow, - Theme, Tooltip, Typography, Button, From 019b9b6dbc1fade2762a21ff89646fda9c4765a9 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Wed, 29 Jul 2020 12:09:33 +0200 Subject: [PATCH 11/34] Readded package gcp-projects to packages.app.package.json --- packages/app/package.json | 1 + yarn.lock | 21 +++++++++------------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index 168506d498..164865cead 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -8,6 +8,7 @@ "@backstage/plugin-catalog": "^0.1.1-alpha.16", "@backstage/plugin-circleci": "^0.1.1-alpha.16", "@backstage/plugin-explore": "^0.1.1-alpha.16", + "@backstage/plugin-gcp-projects": "^0.1.1-alpha.16", "@backstage/plugin-github-actions": "^0.1.1-alpha.16", "@backstage/plugin-gitops-profiles": "^0.1.1-alpha.16", "@backstage/plugin-graphiql": "^0.1.1-alpha.16", diff --git a/yarn.lock b/yarn.lock index cb354e71d4..f1bcff10a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3933,6 +3933,14 @@ jest-diff "^25.2.1" pretty-format "^25.2.1" +"@types/jest@^25.2.2": + version "25.2.3" + resolved "https://registry.npmjs.org/@types/jest/-/jest-25.2.3.tgz#33d27e4c4716caae4eced355097a47ad363fdcaf" + integrity sha512-JXc1nK/tXHiDhV55dvfzqtmP4S3sy3T3ouV2tkViZgxY/zeUkcpQcQPGRlgF4KmWzWW5oiWYSZwtCB+2RsE4Fw== + dependencies: + jest-diff "^25.2.1" + pretty-format "^25.2.1" + "@types/jquery@^3.3.34": version "3.3.38" resolved "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.38.tgz#6385f1e1b30bd2bff55ae8ee75ea42a999cc3608" @@ -13220,13 +13228,12 @@ loglevel@^1.6.7, loglevel@^1.6.8: resolved "https://registry.npmjs.org/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171" integrity sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA== - long@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.0, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -17358,11 +17365,6 @@ resolve-from@^4.0.0: resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== -resolve-from@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" - integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== - resolve-pathname@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" @@ -20372,11 +20374,6 @@ whatwg-fetch@^2.0.0, whatwg-fetch@^2.0.4: resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" integrity sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng== -whatwg-fetch@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.2.0.tgz#8e134f701f0a4ab5fda82626f113e2b647fd16dc" - integrity sha512-SdGPoQMMnzVYThUbSrEvqTlkvC1Ux27NehaJ/GUHBfNrh5Mjg+1/uRyFMwVnxO2MrikMWvWAqUGgQOfVU4hT7w== - whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" From 840e708bf7a472d55a2daa7f3a79b30fb77b1e4a Mon Sep 17 00:00:00 2001 From: ebarrios Date: Wed, 29 Jul 2020 12:14:11 +0200 Subject: [PATCH 12/34] Rolled back plugin version to 0.1.1-alpha.13 --- packages/app/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/package.json b/packages/app/package.json index 164865cead..495a375bd8 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -8,7 +8,7 @@ "@backstage/plugin-catalog": "^0.1.1-alpha.16", "@backstage/plugin-circleci": "^0.1.1-alpha.16", "@backstage/plugin-explore": "^0.1.1-alpha.16", - "@backstage/plugin-gcp-projects": "^0.1.1-alpha.16", + "@backstage/plugin-gcp-projects": "^0.1.1-alpha.13", "@backstage/plugin-github-actions": "^0.1.1-alpha.16", "@backstage/plugin-gitops-profiles": "^0.1.1-alpha.16", "@backstage/plugin-graphiql": "^0.1.1-alpha.16", From c28527a469133141d94de71770a9da2cb6fdd8d5 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Wed, 29 Jul 2020 12:42:25 +0200 Subject: [PATCH 13/34] Using version ^0.1.1-alpha.16 --- packages/app/package.json | 2 +- plugins/gcp-projects/package.json | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index 495a375bd8..164865cead 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -8,7 +8,7 @@ "@backstage/plugin-catalog": "^0.1.1-alpha.16", "@backstage/plugin-circleci": "^0.1.1-alpha.16", "@backstage/plugin-explore": "^0.1.1-alpha.16", - "@backstage/plugin-gcp-projects": "^0.1.1-alpha.13", + "@backstage/plugin-gcp-projects": "^0.1.1-alpha.16", "@backstage/plugin-github-actions": "^0.1.1-alpha.16", "@backstage/plugin-gitops-profiles": "^0.1.1-alpha.16", "@backstage/plugin-graphiql": "^0.1.1-alpha.16", diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index 2bef7feef4..1b4a031aac 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-gcp-projects", - "version": "0.1.1-alpha.13", + "version": "0.1.1-alpha.16", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.13", - "@backstage/theme": "^0.1.1-alpha.13", + "@backstage/core": "^0.1.1-alpha.16", + "@backstage/theme": "^0.1.1-alpha.16", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -32,12 +32,12 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.13", - "@backstage/dev-utils": "^0.1.1-alpha.13", + "@backstage/cli": "^0.1.1-alpha.16", + "@backstage/dev-utils": "^0.1.1-alpha.16", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", - "@types/jest": "^25.2.2", + "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "jest-fetch-mock": "^3.0.3" }, From c4b98146ac7cd3689f8adcf80edf05b287288384 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Fri, 28 Aug 2020 15:46:01 +0200 Subject: [PATCH 14/34] Added missing . on gcp-projects plugin Readme.md file --- plugins/gcp-projects/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/gcp-projects/README.md b/plugins/gcp-projects/README.md index 6a2ab63c96..586cd7b603 100644 --- a/plugins/gcp-projects/README.md +++ b/plugins/gcp-projects/README.md @@ -10,4 +10,4 @@ Your plugin has been added to the example app in this repository, meaning you'll You can also serve the plugin in isolation by running `yarn start` in the plugin directory. This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. -It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory. +It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. From 73d28cd52a629c9bea42d225ed369e8f3adf8363 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Fri, 28 Aug 2020 15:48:31 +0200 Subject: [PATCH 15/34] Added description to the support button --- .../src/components/NewProjectPage/NewProjectPage.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx b/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx index 4d439bbc0d..0149f8ac92 100644 --- a/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx +++ b/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx @@ -120,7 +120,9 @@ export const NewProjectPage = () => { - Support Button + + This plugin allows you to view and interact with your gcp projects. + From 67017fbd003b4199ef3467049ee2d1ff83073fbd Mon Sep 17 00:00:00 2001 From: ebarrios Date: Fri, 28 Aug 2020 16:24:07 +0200 Subject: [PATCH 16/34] Updated package.json react-use to version 15.3.3 --- plugins/gcp-projects/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index 5249cc3b36..8a19584a52 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -29,7 +29,7 @@ "react": "^16.13.1", "react-dom": "^16.13.1", "react-router-dom": "^5.2.0", - "react-use": "^14.2.0" + "react-use": "^15.3.3" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.20", From 53a947bd5a7fcb6c91da5c15cec51efc8f312b93 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 2 Sep 2020 17:24:41 +0200 Subject: [PATCH 17/34] auth-backend: split OAuthProvider into lib/oauth and lib/flow --- .../src/lib/flow/authFlowHelpers.test.ts | 103 ++++++++++++ .../src/lib/flow/authFlowHelpers.ts | 56 +++++++ plugins/auth-backend/src/lib/flow/index.ts | 17 ++ .../src/lib/{ => oauth}/OAuthProvider.test.ts | 148 +----------------- .../src/lib/{ => oauth}/OAuthProvider.ts | 100 +----------- .../src/lib/oauth/helpers.test.ts | 77 +++++++++ plugins/auth-backend/src/lib/oauth/helpers.ts | 60 +++++++ plugins/auth-backend/src/lib/oauth/index.ts | 17 ++ .../src/providers/auth0/provider.ts | 2 +- .../src/providers/github/provider.ts | 2 +- .../src/providers/gitlab/provider.ts | 2 +- .../src/providers/google/provider.ts | 2 +- .../src/providers/microsoft/provider.ts | 2 +- .../src/providers/oauth2/provider.ts | 2 +- .../src/providers/okta/provider.ts | 2 +- .../src/providers/saml/provider.ts | 2 +- 16 files changed, 344 insertions(+), 250 deletions(-) create mode 100644 plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts create mode 100644 plugins/auth-backend/src/lib/flow/authFlowHelpers.ts create mode 100644 plugins/auth-backend/src/lib/flow/index.ts rename plugins/auth-backend/src/lib/{ => oauth}/OAuthProvider.test.ts (60%) rename plugins/auth-backend/src/lib/{ => oauth}/OAuthProvider.ts (75%) create mode 100644 plugins/auth-backend/src/lib/oauth/helpers.test.ts create mode 100644 plugins/auth-backend/src/lib/oauth/helpers.ts create mode 100644 plugins/auth-backend/src/lib/oauth/index.ts diff --git a/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts b/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts new file mode 100644 index 0000000000..10e99c0fe1 --- /dev/null +++ b/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts @@ -0,0 +1,103 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 express from 'express'; +import { ensuresXRequestedWith, postMessageResponse } from './authFlowHelpers'; +import { WebMessageResponse } from '../../providers/types'; + +describe('OAuthProvider Utils', () => { + describe('postMessageResponse', () => { + const appOrigin = 'http://localhost:3000'; + it('should post a message back with payload success', () => { + const mockResponse = ({ + end: jest.fn().mockReturnThis(), + setHeader: jest.fn().mockReturnThis(), + } as unknown) as express.Response; + + const data: WebMessageResponse = { + type: 'authorization_response', + response: { + providerInfo: { + accessToken: 'ACCESS_TOKEN', + idToken: 'ID_TOKEN', + expiresInSeconds: 10, + scope: 'email', + }, + profile: { + email: 'foo@bar.com', + }, + backstageIdentity: { + id: 'a', + idToken: 'a.b.c', + }, + }, + }; + const jsonData = JSON.stringify(data); + const base64Data = Buffer.from(jsonData, 'utf8').toString('base64'); + + postMessageResponse(mockResponse, appOrigin, data); + expect(mockResponse.setHeader).toBeCalledTimes(3); + expect(mockResponse.end).toBeCalledTimes(1); + expect(mockResponse.end).toBeCalledWith( + expect.stringContaining(base64Data), + ); + }); + + it('should post a message back with payload error', () => { + const mockResponse = ({ + end: jest.fn().mockReturnThis(), + setHeader: jest.fn().mockReturnThis(), + } as unknown) as express.Response; + + const data: WebMessageResponse = { + type: 'authorization_response', + error: new Error('Unknown error occured'), + }; + const jsonData = JSON.stringify(data); + const base64Data = Buffer.from(jsonData, 'utf8').toString('base64'); + + postMessageResponse(mockResponse, appOrigin, data); + expect(mockResponse.setHeader).toBeCalledTimes(3); + expect(mockResponse.end).toBeCalledTimes(1); + expect(mockResponse.end).toBeCalledWith( + expect.stringContaining(base64Data), + ); + }); + }); + + describe('ensuresXRequestedWith', () => { + it('should return false if no header present', () => { + const mockRequest = ({ + header: () => jest.fn(), + } as unknown) as express.Request; + expect(ensuresXRequestedWith(mockRequest)).toBe(false); + }); + + it('should return false if header present with incorrect value', () => { + const mockRequest = ({ + header: () => 'INVALID', + } as unknown) as express.Request; + expect(ensuresXRequestedWith(mockRequest)).toBe(false); + }); + + it('should return true if header present with correct value', () => { + const mockRequest = ({ + header: () => 'XMLHttpRequest', + } as unknown) as express.Request; + expect(ensuresXRequestedWith(mockRequest)).toBe(true); + }); + }); +}); diff --git a/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts b/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts new file mode 100644 index 0000000000..500a6cec8e --- /dev/null +++ b/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 express from 'express'; +import crypto from 'crypto'; +import { WebMessageResponse } from '../../providers/types'; + +export const postMessageResponse = ( + res: express.Response, + appOrigin: string, + response: WebMessageResponse, +) => { + const jsonData = JSON.stringify(response); + const base64Data = Buffer.from(jsonData, 'utf8').toString('base64'); + + res.setHeader('Content-Type', 'text/html'); + res.setHeader('X-Frame-Options', 'sameorigin'); + + // TODO: Make target app origin configurable globally + const script = ` + (window.opener || window.parent).postMessage(JSON.parse(atob('${base64Data}')), '${appOrigin}') + window.close() + `; + const hash = crypto.createHash('sha256').update(script).digest('base64'); + res.setHeader('Content-Security-Policy', `script-src 'sha256-${hash}'`); + + res.end(` + + + + + + `); +}; + +export const ensuresXRequestedWith = (req: express.Request) => { + const requiredHeader = req.header('X-Requested-With'); + + if (!requiredHeader || requiredHeader !== 'XMLHttpRequest') { + return false; + } + return true; +}; diff --git a/plugins/auth-backend/src/lib/flow/index.ts b/plugins/auth-backend/src/lib/flow/index.ts new file mode 100644 index 0000000000..a5f2f7a3ac --- /dev/null +++ b/plugins/auth-backend/src/lib/flow/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * 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. + */ + +export { ensuresXRequestedWith, postMessageResponse } from './authFlowHelpers'; diff --git a/plugins/auth-backend/src/lib/OAuthProvider.test.ts b/plugins/auth-backend/src/lib/oauth/OAuthProvider.test.ts similarity index 60% rename from plugins/auth-backend/src/lib/OAuthProvider.test.ts rename to plugins/auth-backend/src/lib/oauth/OAuthProvider.test.ts index 06bbd15c51..124ace1013 100644 --- a/plugins/auth-backend/src/lib/OAuthProvider.test.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthProvider.test.ts @@ -16,15 +16,12 @@ import express from 'express'; import { - ensuresXRequestedWith, - postMessageResponse, THOUSAND_DAYS_MS, TEN_MINUTES_MS, - verifyNonce, - encodeState, OAuthProvider, } from './OAuthProvider'; -import { WebMessageResponse, OAuthProviderHandlers } from '../providers/types'; +import { OAuthProviderHandlers } from '../../providers/types'; +import { encodeState } from './helpers'; const mockResponseData = { providerInfo: { @@ -41,147 +38,6 @@ const mockResponseData = { }, }; -describe('OAuthProvider Utils', () => { - describe('verifyNonce', () => { - it('should throw error if cookie nonce missing', () => { - const state = { nonce: 'NONCE', env: 'development' }; - const mockRequest = ({ - cookies: {}, - query: { - state: encodeState(state), - }, - } as unknown) as express.Request; - expect(() => { - verifyNonce(mockRequest, 'providera'); - }).toThrowError('Auth response is missing cookie nonce'); - }); - - it('should throw error if state nonce missing', () => { - const mockRequest = ({ - cookies: { - 'providera-nonce': 'NONCE', - }, - query: {}, - } as unknown) as express.Request; - expect(() => { - verifyNonce(mockRequest, 'providera'); - }).toThrowError('Invalid state passed via request'); - }); - - it('should throw error if nonce mismatch', () => { - const state = { nonce: 'NONCEB', env: 'development' }; - const mockRequest = ({ - cookies: { - 'providera-nonce': 'NONCEA', - }, - query: { - state: encodeState(state), - }, - } as unknown) as express.Request; - expect(() => { - verifyNonce(mockRequest, 'providera'); - }).toThrowError('Invalid nonce'); - }); - - it('should not throw any error if nonce matches', () => { - const state = { nonce: 'NONCE', env: 'development' }; - const mockRequest = ({ - cookies: { - 'providera-nonce': 'NONCE', - }, - query: { - state: encodeState(state), - }, - } as unknown) as express.Request; - expect(() => { - verifyNonce(mockRequest, 'providera'); - }).not.toThrow(); - }); - }); - - describe('postMessageResponse', () => { - const appOrigin = 'http://localhost:3000'; - it('should post a message back with payload success', () => { - const mockResponse = ({ - end: jest.fn().mockReturnThis(), - setHeader: jest.fn().mockReturnThis(), - } as unknown) as express.Response; - - const data: WebMessageResponse = { - type: 'authorization_response', - response: { - providerInfo: { - accessToken: 'ACCESS_TOKEN', - idToken: 'ID_TOKEN', - expiresInSeconds: 10, - scope: 'email', - }, - profile: { - email: 'foo@bar.com', - }, - backstageIdentity: { - id: 'a', - idToken: 'a.b.c', - }, - }, - }; - const jsonData = JSON.stringify(data); - const base64Data = Buffer.from(jsonData, 'utf8').toString('base64'); - - postMessageResponse(mockResponse, appOrigin, data); - expect(mockResponse.setHeader).toBeCalledTimes(3); - expect(mockResponse.end).toBeCalledTimes(1); - expect(mockResponse.end).toBeCalledWith( - expect.stringContaining(base64Data), - ); - }); - - it('should post a message back with payload error', () => { - const mockResponse = ({ - end: jest.fn().mockReturnThis(), - setHeader: jest.fn().mockReturnThis(), - } as unknown) as express.Response; - - const data: WebMessageResponse = { - type: 'authorization_response', - error: new Error('Unknown error occured'), - }; - const jsonData = JSON.stringify(data); - const base64Data = Buffer.from(jsonData, 'utf8').toString('base64'); - - postMessageResponse(mockResponse, appOrigin, data); - expect(mockResponse.setHeader).toBeCalledTimes(3); - expect(mockResponse.end).toBeCalledTimes(1); - expect(mockResponse.end).toBeCalledWith( - expect.stringContaining(base64Data), - ); - }); - }); - - describe('ensuresXRequestedWith', () => { - it('should return false if no header present', () => { - const mockRequest = ({ - header: () => jest.fn(), - } as unknown) as express.Request; - expect(ensuresXRequestedWith(mockRequest)).toBe(false); - }); - - it('should return false if header present with incorrect value', () => { - const mockRequest = ({ - header: () => 'INVALID', - } as unknown) as express.Request; - expect(ensuresXRequestedWith(mockRequest)).toBe(false); - }); - - it('should return true if header present with correct value', () => { - const mockRequest = ({ - header: () => 'XMLHttpRequest', - } as unknown) as express.Request; - expect(ensuresXRequestedWith(mockRequest)).toBe(true); - }); - }); -}); - describe('OAuthProvider', () => { class MyAuthProvider implements OAuthProviderHandlers { async start() { diff --git a/plugins/auth-backend/src/lib/OAuthProvider.ts b/plugins/auth-backend/src/lib/oauth/OAuthProvider.ts similarity index 75% rename from plugins/auth-backend/src/lib/OAuthProvider.ts rename to plugins/auth-backend/src/lib/oauth/OAuthProvider.ts index dac31b442d..95948ff7cc 100644 --- a/plugins/auth-backend/src/lib/OAuthProvider.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthProvider.ts @@ -20,13 +20,13 @@ import { URL } from 'url'; import { AuthProviderRouteHandlers, OAuthProviderHandlers, - WebMessageResponse, BackstageIdentity, - OAuthState, AuthProviderConfig, -} from '../providers/types'; +} from '../../providers/types'; import { InputError } from '@backstage/backend-common'; -import { TokenIssuer } from '../identity'; +import { TokenIssuer } from '../../identity'; +import { verifyNonce, encodeState } from './helpers'; +import { postMessageResponse, ensuresXRequestedWith } from '../flow'; export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; export const TEN_MINUTES_MS = 600 * 1000; @@ -42,85 +42,6 @@ export type Options = { tokenIssuer: TokenIssuer; }; -const readState = (stateString: string): OAuthState => { - const state = Object.fromEntries( - new URLSearchParams(decodeURIComponent(stateString)), - ); - if ( - !state.nonce || - !state.env || - state.nonce?.length === 0 || - state.env?.length === 0 - ) { - throw Error(`Invalid state passed via request`); - } - return { - nonce: state.nonce, - env: state.env, - }; -}; - -export const encodeState = (state: OAuthState): string => { - const searchParams = new URLSearchParams(); - searchParams.append('nonce', state.nonce); - searchParams.append('env', state.env); - - return encodeURIComponent(searchParams.toString()); -}; - -export const verifyNonce = (req: express.Request, providerId: string) => { - const cookieNonce = req.cookies[`${providerId}-nonce`]; - const state: OAuthState = readState(req.query.state?.toString() ?? ''); - const stateNonce = state.nonce; - - if (!cookieNonce) { - throw new Error('Auth response is missing cookie nonce'); - } - if (stateNonce.length === 0) { - throw new Error('Auth response is missing state nonce'); - } - if (cookieNonce !== stateNonce) { - throw new Error('Invalid nonce'); - } -}; - -export const postMessageResponse = ( - res: express.Response, - appOrigin: string, - response: WebMessageResponse, -) => { - const jsonData = JSON.stringify(response); - const base64Data = Buffer.from(jsonData, 'utf8').toString('base64'); - - res.setHeader('Content-Type', 'text/html'); - res.setHeader('X-Frame-Options', 'sameorigin'); - - // TODO: Make target app origin configurable globally - const script = ` - (window.opener || window.parent).postMessage(JSON.parse(atob('${base64Data}')), '${appOrigin}') - window.close() - `; - const hash = crypto.createHash('sha256').update(script).digest('base64'); - res.setHeader('Content-Security-Policy', `script-src 'sha256-${hash}'`); - - res.end(` - - - - - - `); -}; - -export const ensuresXRequestedWith = (req: express.Request) => { - const requiredHeader = req.header('X-Requested-With'); - - if (!requiredHeader || requiredHeader !== 'XMLHttpRequest') { - return false; - } - return true; -}; - export class OAuthProvider implements AuthProviderRouteHandlers { static fromConfig( config: AuthProviderConfig, @@ -287,19 +208,6 @@ export class OAuthProvider implements AuthProviderRouteHandlers { } } - identifyEnv(req: express.Request): string | undefined { - const reqEnv = req.query.env?.toString(); - if (reqEnv) { - return reqEnv; - } - const stateParams = req.query.state?.toString(); - if (!stateParams) { - return undefined; - } - const env = readState(stateParams).env; - return env; - } - /** * If the response from the OAuth provider includes a Backstage identity, we * make sure it's populated with all the information we can derive from the user ID. diff --git a/plugins/auth-backend/src/lib/oauth/helpers.test.ts b/plugins/auth-backend/src/lib/oauth/helpers.test.ts new file mode 100644 index 0000000000..fcf56705d2 --- /dev/null +++ b/plugins/auth-backend/src/lib/oauth/helpers.test.ts @@ -0,0 +1,77 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 express from 'express'; +import { verifyNonce, encodeState } from './helpers'; + +describe('OAuthProvider Utils', () => { + describe('verifyNonce', () => { + it('should throw error if cookie nonce missing', () => { + const state = { nonce: 'NONCE', env: 'development' }; + const mockRequest = ({ + cookies: {}, + query: { + state: encodeState(state), + }, + } as unknown) as express.Request; + expect(() => { + verifyNonce(mockRequest, 'providera'); + }).toThrowError('Auth response is missing cookie nonce'); + }); + + it('should throw error if state nonce missing', () => { + const mockRequest = ({ + cookies: { + 'providera-nonce': 'NONCE', + }, + query: {}, + } as unknown) as express.Request; + expect(() => { + verifyNonce(mockRequest, 'providera'); + }).toThrowError('Invalid state passed via request'); + }); + + it('should throw error if nonce mismatch', () => { + const state = { nonce: 'NONCEB', env: 'development' }; + const mockRequest = ({ + cookies: { + 'providera-nonce': 'NONCEA', + }, + query: { + state: encodeState(state), + }, + } as unknown) as express.Request; + expect(() => { + verifyNonce(mockRequest, 'providera'); + }).toThrowError('Invalid nonce'); + }); + + it('should not throw any error if nonce matches', () => { + const state = { nonce: 'NONCE', env: 'development' }; + const mockRequest = ({ + cookies: { + 'providera-nonce': 'NONCE', + }, + query: { + state: encodeState(state), + }, + } as unknown) as express.Request; + expect(() => { + verifyNonce(mockRequest, 'providera'); + }).not.toThrow(); + }); + }); +}); diff --git a/plugins/auth-backend/src/lib/oauth/helpers.ts b/plugins/auth-backend/src/lib/oauth/helpers.ts new file mode 100644 index 0000000000..34f12b8d22 --- /dev/null +++ b/plugins/auth-backend/src/lib/oauth/helpers.ts @@ -0,0 +1,60 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 express from 'express'; +import { OAuthState } from '../../providers/types'; + +const readState = (stateString: string): OAuthState => { + const state = Object.fromEntries( + new URLSearchParams(decodeURIComponent(stateString)), + ); + if ( + !state.nonce || + !state.env || + state.nonce?.length === 0 || + state.env?.length === 0 + ) { + throw Error(`Invalid state passed via request`); + } + return { + nonce: state.nonce, + env: state.env, + }; +}; + +export const encodeState = (state: OAuthState): string => { + const searchParams = new URLSearchParams(); + searchParams.append('nonce', state.nonce); + searchParams.append('env', state.env); + + return encodeURIComponent(searchParams.toString()); +}; + +export const verifyNonce = (req: express.Request, providerId: string) => { + const cookieNonce = req.cookies[`${providerId}-nonce`]; + const state: OAuthState = readState(req.query.state?.toString() ?? ''); + const stateNonce = state.nonce; + + if (!cookieNonce) { + throw new Error('Auth response is missing cookie nonce'); + } + if (stateNonce.length === 0) { + throw new Error('Auth response is missing state nonce'); + } + if (cookieNonce !== stateNonce) { + throw new Error('Invalid nonce'); + } +}; diff --git a/plugins/auth-backend/src/lib/oauth/index.ts b/plugins/auth-backend/src/lib/oauth/index.ts new file mode 100644 index 0000000000..4c120799e7 --- /dev/null +++ b/plugins/auth-backend/src/lib/oauth/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * 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. + */ + +export { OAuthProvider } from './OAuthProvider'; diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index 7eeca59028..f785d1e147 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -19,7 +19,7 @@ import passport from 'passport'; import Auth0Strategy from './strategy'; import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; -import { OAuthProvider } from '../../lib/OAuthProvider'; +import { OAuthProvider } from '../../lib/oauth'; import { executeFetchUserProfileStrategy, executeFrameHandlerStrategy, diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index b63acdfb20..8e9bf1aa4a 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -29,7 +29,7 @@ import { OAuthResponse, PassportDoneCallback, } from '../types'; -import { OAuthProvider } from '../../lib/OAuthProvider'; +import { OAuthProvider } from '../../lib/oauth'; import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; import passport from 'passport'; diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index 1c2a822d82..2100bc753e 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -29,7 +29,7 @@ import { OAuthResponse, PassportDoneCallback, } from '../types'; -import { OAuthProvider } from '../../lib/OAuthProvider'; +import { OAuthProvider } from '../../lib/oauth'; import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; import passport from 'passport'; diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index d4455ac4d3..366f9cd96c 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -31,7 +31,7 @@ import { OAuthResponse, PassportDoneCallback, } from '../types'; -import { OAuthProvider } from '../../lib/OAuthProvider'; +import { OAuthProvider } from '../../lib/oauth'; import passport from 'passport'; import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index 5997d8e1a8..fb96d2236f 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -35,7 +35,7 @@ import { PassportDoneCallback, } from '../types'; -import { OAuthProvider } from '../../lib/OAuthProvider'; +import { OAuthProvider } from '../../lib/oauth'; import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; import { Config } from '@backstage/config'; diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index 4b8a826d81..c08e18960c 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -19,7 +19,7 @@ import passport from 'passport'; import { Strategy as OAuth2Strategy } from 'passport-oauth2'; import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; -import { OAuthProvider } from '../../lib/OAuthProvider'; +import { OAuthProvider } from '../../lib/oauth'; import { executeFetchUserProfileStrategy, executeFrameHandlerStrategy, diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index db8e0cf313..a6ccc25d21 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import express from 'express'; -import { OAuthProvider } from '../../lib/OAuthProvider'; +import { OAuthProvider } from '../../lib/oauth'; import { Strategy as OktaStrategy } from 'passport-okta-oauth'; import passport from 'passport'; import { diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 56793d85f4..d98bcc9579 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -30,7 +30,7 @@ import { PassportDoneCallback, ProfileInfo, } from '../types'; -import { postMessageResponse } from '../../lib/OAuthProvider'; +import { postMessageResponse } from '../../lib/flow'; import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; import { Config } from '@backstage/config'; From b8a3f851cdb111e291611eb1c8f95abcd325fd8b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 3 Sep 2020 09:25:44 +0200 Subject: [PATCH 18/34] auth-backend: cleanup types and move them closer to home --- .../src/lib/EnvironmentHandler.ts | 9 +- .../src/lib/PassportStrategyHelper.ts | 40 ++++- .../src/lib/flow/authFlowHelpers.test.ts | 2 +- .../src/lib/flow/authFlowHelpers.ts | 2 +- plugins/auth-backend/src/lib/flow/types.ts | 31 ++++ .../src/lib/oauth/OAuthProvider.test.ts | 2 +- .../src/lib/oauth/OAuthProvider.ts | 2 +- plugins/auth-backend/src/lib/oauth/helpers.ts | 4 +- plugins/auth-backend/src/lib/oauth/index.ts | 8 + plugins/auth-backend/src/lib/oauth/types.ts | 112 +++++++++++++ .../src/providers/auth0/provider.ts | 16 +- .../auth-backend/src/providers/factories.ts | 7 +- .../src/providers/github/provider.ts | 18 +- .../src/providers/gitlab/provider.ts | 18 +- .../src/providers/google/provider.ts | 14 +- .../src/providers/microsoft/provider.ts | 18 +- .../src/providers/oauth2/provider.ts | 16 +- .../src/providers/okta/provider.ts | 18 +- .../src/providers/saml/provider.ts | 2 +- plugins/auth-backend/src/providers/types.ts | 157 ------------------ 20 files changed, 258 insertions(+), 238 deletions(-) create mode 100644 plugins/auth-backend/src/lib/flow/types.ts create mode 100644 plugins/auth-backend/src/lib/oauth/types.ts diff --git a/plugins/auth-backend/src/lib/EnvironmentHandler.ts b/plugins/auth-backend/src/lib/EnvironmentHandler.ts index 53e56a7420..dd9c972cb2 100644 --- a/plugins/auth-backend/src/lib/EnvironmentHandler.ts +++ b/plugins/auth-backend/src/lib/EnvironmentHandler.ts @@ -15,16 +15,17 @@ */ import express from 'express'; -import { - AuthProviderRouteHandlers, - EnvironmentIdentifierFn, -} from '../providers/types'; +import { AuthProviderRouteHandlers } from '../providers/types'; import { InputError } from '@backstage/backend-common'; export type EnvironmentHandlers = { [key: string]: AuthProviderRouteHandlers; }; +export type EnvironmentIdentifierFn = ( + req: express.Request, +) => string | undefined; + export class EnvironmentHandler implements AuthProviderRouteHandlers { constructor( private readonly providerId: string, diff --git a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts b/plugins/auth-backend/src/lib/PassportStrategyHelper.ts index 9a169f9696..3b8d0801e9 100644 --- a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts +++ b/plugins/auth-backend/src/lib/PassportStrategyHelper.ts @@ -17,12 +17,13 @@ import express from 'express'; import passport from 'passport'; import jwtDecoder from 'jwt-decode'; -import { - RedirectInfo, - RefreshTokenResponse, - ProfileInfo, - ProviderStrategy, -} from '../providers/types'; +import { ProfileInfo } from '../providers/types'; + +export type PassportDoneCallback = ( + err?: Error, + response?: Res, + privateInfo?: Private, +) => void; export const makeProfileInfo = ( profile: passport.Profile, @@ -63,6 +64,17 @@ export const makeProfileInfo = ( }; }; +export type RedirectInfo = { + /** + * URL to redirect to + */ + url: string; + /** + * Status code to use for the redirect + */ + status?: number; +}; + export const executeRedirectStrategy = async ( req: express.Request, providerStrategy: passport.Strategy, @@ -106,6 +118,18 @@ export const executeFrameHandlerStrategy = async ( ); }; +type RefreshTokenResponse = { + /** + * An access token issued for the signed in user. + */ + accessToken: string; + /** + * Optionally, the server can issue a new Refresh Token for the user + */ + refreshToken?: string; + params: any; +}; + export const executeRefreshTokenStrategy = async ( providerStrategy: passport.Strategy, refreshToken: string, @@ -156,6 +180,10 @@ export const executeRefreshTokenStrategy = async ( }); }; +type ProviderStrategy = { + userProfile(accessToken: string, callback: Function): void; +}; + export const executeFetchUserProfileStrategy = async ( providerStrategy: passport.Strategy, accessToken: string, diff --git a/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts b/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts index 10e99c0fe1..b525e98e12 100644 --- a/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts +++ b/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts @@ -16,7 +16,7 @@ import express from 'express'; import { ensuresXRequestedWith, postMessageResponse } from './authFlowHelpers'; -import { WebMessageResponse } from '../../providers/types'; +import { WebMessageResponse } from './types'; describe('OAuthProvider Utils', () => { describe('postMessageResponse', () => { diff --git a/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts b/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts index 500a6cec8e..63d7c28b50 100644 --- a/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts +++ b/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts @@ -16,7 +16,7 @@ import express from 'express'; import crypto from 'crypto'; -import { WebMessageResponse } from '../../providers/types'; +import { WebMessageResponse } from './types'; export const postMessageResponse = ( res: express.Response, diff --git a/plugins/auth-backend/src/lib/flow/types.ts b/plugins/auth-backend/src/lib/flow/types.ts new file mode 100644 index 0000000000..98bb551c2c --- /dev/null +++ b/plugins/auth-backend/src/lib/flow/types.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { AuthResponse } from '../../providers/types'; + +/** + * Payload sent as a post message after the auth request is complete. + * If successful then has a valid payload with Auth information else contains an error. + */ +export type WebMessageResponse = + | { + type: 'authorization_response'; + response: AuthResponse; + } + | { + type: 'authorization_response'; + error: Error; + }; diff --git a/plugins/auth-backend/src/lib/oauth/OAuthProvider.test.ts b/plugins/auth-backend/src/lib/oauth/OAuthProvider.test.ts index 124ace1013..ce78733211 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthProvider.test.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthProvider.test.ts @@ -20,8 +20,8 @@ import { TEN_MINUTES_MS, OAuthProvider, } from './OAuthProvider'; -import { OAuthProviderHandlers } from '../../providers/types'; import { encodeState } from './helpers'; +import { OAuthProviderHandlers } from './types'; const mockResponseData = { providerInfo: { diff --git a/plugins/auth-backend/src/lib/oauth/OAuthProvider.ts b/plugins/auth-backend/src/lib/oauth/OAuthProvider.ts index 95948ff7cc..1fe62845a0 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthProvider.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthProvider.ts @@ -19,7 +19,6 @@ import crypto from 'crypto'; import { URL } from 'url'; import { AuthProviderRouteHandlers, - OAuthProviderHandlers, BackstageIdentity, AuthProviderConfig, } from '../../providers/types'; @@ -27,6 +26,7 @@ import { InputError } from '@backstage/backend-common'; import { TokenIssuer } from '../../identity'; import { verifyNonce, encodeState } from './helpers'; import { postMessageResponse, ensuresXRequestedWith } from '../flow'; +import { OAuthProviderHandlers } from './types'; export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; export const TEN_MINUTES_MS = 600 * 1000; diff --git a/plugins/auth-backend/src/lib/oauth/helpers.ts b/plugins/auth-backend/src/lib/oauth/helpers.ts index 34f12b8d22..9f250a0285 100644 --- a/plugins/auth-backend/src/lib/oauth/helpers.ts +++ b/plugins/auth-backend/src/lib/oauth/helpers.ts @@ -15,9 +15,9 @@ */ import express from 'express'; -import { OAuthState } from '../../providers/types'; +import { OAuthState } from './types'; -const readState = (stateString: string): OAuthState => { +export const readState = (stateString: string): OAuthState => { const state = Object.fromEntries( new URLSearchParams(decodeURIComponent(stateString)), ); diff --git a/plugins/auth-backend/src/lib/oauth/index.ts b/plugins/auth-backend/src/lib/oauth/index.ts index 4c120799e7..eac43141a4 100644 --- a/plugins/auth-backend/src/lib/oauth/index.ts +++ b/plugins/auth-backend/src/lib/oauth/index.ts @@ -14,4 +14,12 @@ * limitations under the License. */ +export { OAuthEnvironmentHandler } from './OAuthEnvironmentHandler'; export { OAuthProvider } from './OAuthProvider'; +export type { + OAuthProviderHandlers, + OAuthProviderInfo, + OAuthProviderOptions, + OAuthResponse, + OAuthState, +} from './types'; diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts new file mode 100644 index 0000000000..ecc665831f --- /dev/null +++ b/plugins/auth-backend/src/lib/oauth/types.ts @@ -0,0 +1,112 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 express from 'express'; +import { AuthResponse } from '../../providers/types'; +import { RedirectInfo } from '../PassportStrategyHelper'; + +/** + * Common options for passport.js-based OAuth providers + */ +export type OAuthProviderOptions = { + /** + * Client ID of the auth provider. + */ + clientId: string; + /** + * Client Secret of the auth provider. + */ + clientSecret: string; + /** + * Callback URL to be passed to the auth provider to redirect to after the user signs in. + */ + callbackUrl: string; +}; + +export type OAuthResponse = AuthResponse; + +export type OAuthProviderInfo = { + /** + * An access token issued for the signed in user. + */ + accessToken: string; + /** + * (Optional) Id token issued for the signed in user. + */ + idToken?: string; + /** + * Expiry of the access token in seconds. + */ + expiresInSeconds?: number; + /** + * Scopes granted for the access token. + */ + scope: string; + /** + * A refresh token issued for the signed in user + */ + refreshToken?: string; +}; + +export type OAuthState = { + /* A type for the serialized value in the `state` parameter of the OAuth authorization flow + */ + nonce: string; + env: string; +}; + +/** + * Any OAuth provider needs to implement this interface which has provider specific + * handlers for different methods to perform authentication, get access tokens, + * refresh tokens and perform sign out. + */ +export interface OAuthProviderHandlers { + /** + * This method initiates a sign in request with an auth provider. + * @param {express.Request} req + * @param options + */ + start( + req: express.Request, + options: Record, + ): Promise; + + /** + * Handles the redirect from the auth provider when the user has signed in. + * @param {express.Request} req + */ + handler( + req: express.Request, + ): Promise<{ + response: AuthResponse; + refreshToken?: string; + }>; + + /** + * (Optional) Given a refresh token and scope fetches a new access token from the auth provider. + * @param {string} refreshToken + * @param {string} scope + */ + refresh?( + refreshToken: string, + scope: string, + ): Promise>; + + /** + * (Optional) Sign out of the auth provider. + */ + logout?(): Promise; +} diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index f785d1e147..ff55846302 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -19,22 +19,22 @@ import passport from 'passport'; import Auth0Strategy from './strategy'; import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; -import { OAuthProvider } from '../../lib/oauth'; +import { + OAuthProvider, + OAuthProviderOptions, + OAuthProviderHandlers, + OAuthResponse, +} from '../../lib/oauth'; import { executeFetchUserProfileStrategy, executeFrameHandlerStrategy, executeRedirectStrategy, executeRefreshTokenStrategy, makeProfileInfo, -} from '../../lib/PassportStrategyHelper'; -import { - AuthProviderConfig, - OAuthProviderHandlers, - OAuthResponse, PassportDoneCallback, RedirectInfo, - OAuthProviderOptions, -} from '../types'; +} from '../../lib/PassportStrategyHelper'; +import { AuthProviderConfig } from '../types'; import { Config } from '@backstage/config'; type PrivateInfo = { diff --git a/plugins/auth-backend/src/providers/factories.ts b/plugins/auth-backend/src/providers/factories.ts index 26989d8759..2b67510991 100644 --- a/plugins/auth-backend/src/providers/factories.ts +++ b/plugins/auth-backend/src/providers/factories.ts @@ -25,15 +25,12 @@ import { createOktaProvider } from './okta'; import { createSamlProvider } from './saml'; import { createAuth0Provider } from './auth0'; import { createMicrosoftProvider } from './microsoft'; -import { - AuthProviderConfig, - AuthProviderFactory, - EnvironmentIdentifierFn, -} from './types'; +import { AuthProviderConfig, AuthProviderFactory } from './types'; import { Config } from '@backstage/config'; import { EnvironmentHandlers, EnvironmentHandler, + EnvironmentIdentifierFn, } from '../lib/EnvironmentHandler'; const factories: { [providerId: string]: AuthProviderFactory } = { diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index 8e9bf1aa4a..22fb908ef9 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -20,16 +20,16 @@ import { executeFrameHandlerStrategy, executeRedirectStrategy, makeProfileInfo, -} from '../../lib/PassportStrategyHelper'; -import { - OAuthProviderHandlers, - AuthProviderConfig, - RedirectInfo, - OAuthProviderOptions, - OAuthResponse, PassportDoneCallback, -} from '../types'; -import { OAuthProvider } from '../../lib/oauth'; + RedirectInfo, +} from '../../lib/PassportStrategyHelper'; +import { AuthProviderConfig } from '../types'; +import { + OAuthProvider, + OAuthProviderOptions, + OAuthProviderHandlers, + OAuthResponse, +} from '../../lib/oauth'; import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; import passport from 'passport'; diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index 2100bc753e..a3bd6deef3 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -20,16 +20,16 @@ import { executeFrameHandlerStrategy, executeRedirectStrategy, makeProfileInfo, -} from '../../lib/PassportStrategyHelper'; -import { - OAuthProviderHandlers, - AuthProviderConfig, - RedirectInfo, - OAuthProviderOptions, - OAuthResponse, PassportDoneCallback, -} from '../types'; -import { OAuthProvider } from '../../lib/oauth'; + RedirectInfo, +} from '../../lib/PassportStrategyHelper'; +import { AuthProviderConfig } from '../types'; +import { + OAuthProvider, + OAuthProviderOptions, + OAuthProviderHandlers, + OAuthResponse, +} from '../../lib/oauth'; import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; import passport from 'passport'; diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 366f9cd96c..0301062474 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -22,16 +22,16 @@ import { executeRefreshTokenStrategy, makeProfileInfo, executeFetchUserProfileStrategy, -} from '../../lib/PassportStrategyHelper'; -import { - OAuthProviderHandlers, + PassportDoneCallback, RedirectInfo, - AuthProviderConfig, +} from '../../lib/PassportStrategyHelper'; +import { AuthProviderConfig } from '../types'; +import { + OAuthProvider, + OAuthProviderHandlers, OAuthProviderOptions, OAuthResponse, - PassportDoneCallback, -} from '../types'; -import { OAuthProvider } from '../../lib/oauth'; +} from '../../lib/oauth'; import passport from 'passport'; import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index fb96d2236f..5eec047b69 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -24,18 +24,18 @@ import { executeRefreshTokenStrategy, makeProfileInfo, executeFetchUserProfileStrategy, + PassportDoneCallback, + RedirectInfo, } from '../../lib/PassportStrategyHelper'; -import { - OAuthProviderHandlers, - RedirectInfo, - AuthProviderConfig, - OAuthProviderOptions, - OAuthResponse, - PassportDoneCallback, -} from '../types'; +import { AuthProviderConfig } from '../types'; -import { OAuthProvider } from '../../lib/oauth'; +import { + OAuthProvider, + OAuthProviderOptions, + OAuthProviderHandlers, + OAuthResponse, +} from '../../lib/oauth'; import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; import { Config } from '@backstage/config'; diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index c08e18960c..a1a3cc7d97 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -19,22 +19,22 @@ import passport from 'passport'; import { Strategy as OAuth2Strategy } from 'passport-oauth2'; import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; -import { OAuthProvider } from '../../lib/oauth'; +import { + OAuthProvider, + OAuthProviderOptions, + OAuthProviderHandlers, + OAuthResponse, +} from '../../lib/oauth'; import { executeFetchUserProfileStrategy, executeFrameHandlerStrategy, executeRedirectStrategy, executeRefreshTokenStrategy, makeProfileInfo, -} from '../../lib/PassportStrategyHelper'; -import { - AuthProviderConfig, - OAuthProviderOptions, - OAuthProviderHandlers, - OAuthResponse, PassportDoneCallback, RedirectInfo, -} from '../types'; +} from '../../lib/PassportStrategyHelper'; +import { AuthProviderConfig } from '../types'; import { Config } from '@backstage/config'; type PrivateInfo = { diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index a6ccc25d21..e46e396aa5 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -14,7 +14,12 @@ * limitations under the License. */ import express from 'express'; -import { OAuthProvider } from '../../lib/oauth'; +import { + OAuthProvider, + OAuthProviderOptions, + OAuthProviderHandlers, + OAuthResponse, +} from '../../lib/oauth'; import { Strategy as OktaStrategy } from 'passport-okta-oauth'; import passport from 'passport'; import { @@ -23,15 +28,10 @@ import { executeRefreshTokenStrategy, makeProfileInfo, executeFetchUserProfileStrategy, -} from '../../lib/PassportStrategyHelper'; -import { - OAuthProviderHandlers, - RedirectInfo, - AuthProviderConfig, - OAuthProviderOptions, - OAuthResponse, PassportDoneCallback, -} from '../types'; + RedirectInfo, +} from '../../lib/PassportStrategyHelper'; +import { AuthProviderConfig } from '../types'; import { Logger } from 'winston'; import { StateStore } from 'passport-oauth2'; import { TokenIssuer } from '../../identity'; diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index d98bcc9579..d6ea0520fc 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -23,11 +23,11 @@ import { import { executeFrameHandlerStrategy, executeRedirectStrategy, + PassportDoneCallback, } from '../../lib/PassportStrategyHelper'; import { AuthProviderConfig, AuthProviderRouteHandlers, - PassportDoneCallback, ProfileInfo, } from '../types'; import { postMessageResponse } from '../../lib/flow'; diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index b3b7e518cc..c7b70adbdc 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -19,21 +19,6 @@ import { Logger } from 'winston'; import { TokenIssuer } from '../identity'; import { Config } from '@backstage/config'; -export type OAuthProviderOptions = { - /** - * Client ID of the auth provider. - */ - clientId: string; - /** - * Client Secret of the auth provider. - */ - clientSecret: string; - /** - * Callback URL to be passed to the auth provider to redirect to after the user signs in. - */ - callbackUrl: string; -}; - export type AuthProviderConfig = { /** * The protocol://domain[:port] where the app is hosted. This is used to construct the @@ -47,49 +32,6 @@ export type AuthProviderConfig = { appUrl: string; }; -/** - * Any OAuth provider needs to implement this interface which has provider specific - * handlers for different methods to perform authentication, get access tokens, - * refresh tokens and perform sign out. - */ -export interface OAuthProviderHandlers { - /** - * This method initiates a sign in request with an auth provider. - * @param {express.Request} req - * @param options - */ - start( - req: express.Request, - options: Record, - ): Promise; - - /** - * Handles the redirect from the auth provider when the user has signed in. - * @param {express.Request} req - */ - handler( - req: express.Request, - ): Promise<{ - response: AuthResponse; - refreshToken?: string; - }>; - - /** - * (Optional) Given a refresh token and scope fetches a new access token from the auth provider. - * @param {string} refreshToken - * @param {string} scope - */ - refresh?( - refreshToken: string, - scope: string, - ): Promise>; - - /** - * (Optional) Sign out of the auth provider. - */ - logout?(): Promise; -} - /** * Any Auth provider needs to implement this interface which handles the routes in the * auth backend. Any auth API requests from the frontend reaches these methods. @@ -180,8 +122,6 @@ export type AuthResponse = { backstageIdentity?: BackstageIdentity; }; -export type OAuthResponse = AuthResponse; - export type BackstageIdentity = { /** * The backstage user ID. @@ -194,67 +134,6 @@ export type BackstageIdentity = { idToken?: string; }; -export type OAuthProviderInfo = { - /** - * An access token issued for the signed in user. - */ - accessToken: string; - /** - * (Optional) Id token issued for the signed in user. - */ - idToken?: string; - /** - * Expiry of the access token in seconds. - */ - expiresInSeconds?: number; - /** - * Scopes granted for the access token. - */ - scope: string; - /** - * A refresh token issued for the signed in user - */ - refreshToken?: string; -}; - -export type OAuthPrivateInfo = { - /** - * A refresh token issued for the signed in user. - */ - refreshToken: string; -}; - -/** - * Payload sent as a post message after the auth request is complete. - * If successful then has a valid payload with Auth information else contains an error. - */ -export type WebMessageResponse = - | { - type: 'authorization_response'; - response: AuthResponse; - } - | { - type: 'authorization_response'; - error: Error; - }; - -export type PassportDoneCallback = ( - err?: Error, - response?: Res, - privateInfo?: Private, -) => void; - -export type RedirectInfo = { - /** - * URL to redirect to - */ - url: string; - /** - * Status code to use for the redirect - */ - status?: number; -}; - /** * Used to display login information to user, i.e. sidebar popup. * @@ -276,39 +155,3 @@ export type ProfileInfo = { */ picture?: string; }; - -export type RefreshTokenResponse = { - /** - * An access token issued for the signed in user. - */ - accessToken: string; - /** - * Optionally, the server can issue a new Refresh Token for the user - */ - refreshToken?: string; - params: any; -}; - -export type ProviderStrategy = { - userProfile(accessToken: string, callback: Function): void; -}; - -export type SAMLProviderConfig = { - entryPoint: string; - issuer: string; -}; - -export type SAMLEnvironmentProviderConfig = { - [key: string]: SAMLProviderConfig; -}; - -export type OAuthState = { - /* A type for the serialized value in the `state` parameter of the OAuth authorization flow - */ - nonce: string; - env: string; -}; - -export type EnvironmentIdentifierFn = ( - req: express.Request, -) => string | undefined; From 88cf6da18d2f793d891c78df3ae5809548cf8536 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 3 Sep 2020 12:02:58 +0200 Subject: [PATCH 19/34] auth-backend: move passport helpers into separate lib --- plugins/auth-backend/src/lib/oauth/types.ts | 3 +-- .../PassportStrategyHelper.test.ts | 0 .../{ => passport}/PassportStrategyHelper.ts | 13 +--------- .../auth-backend/src/lib/passport/index.ts | 24 +++++++++++++++++++ .../src/providers/auth0/provider.ts | 5 ++-- .../src/providers/github/provider.ts | 5 ++-- .../src/providers/gitlab/provider.ts | 5 ++-- .../src/providers/google/provider.ts | 5 ++-- .../src/providers/microsoft/provider.ts | 5 ++-- .../src/providers/oauth2/provider.ts | 5 ++-- .../src/providers/okta/provider.ts | 5 ++-- .../src/providers/saml/provider.ts | 2 +- plugins/auth-backend/src/providers/types.ts | 11 +++++++++ 13 files changed, 52 insertions(+), 36 deletions(-) rename plugins/auth-backend/src/lib/{ => passport}/PassportStrategyHelper.test.ts (100%) rename plugins/auth-backend/src/lib/{ => passport}/PassportStrategyHelper.ts (96%) create mode 100644 plugins/auth-backend/src/lib/passport/index.ts diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts index ecc665831f..440b89ed6f 100644 --- a/plugins/auth-backend/src/lib/oauth/types.ts +++ b/plugins/auth-backend/src/lib/oauth/types.ts @@ -15,8 +15,7 @@ */ import express from 'express'; -import { AuthResponse } from '../../providers/types'; -import { RedirectInfo } from '../PassportStrategyHelper'; +import { AuthResponse, RedirectInfo } from '../../providers/types'; /** * Common options for passport.js-based OAuth providers diff --git a/plugins/auth-backend/src/lib/PassportStrategyHelper.test.ts b/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.test.ts similarity index 100% rename from plugins/auth-backend/src/lib/PassportStrategyHelper.test.ts rename to plugins/auth-backend/src/lib/passport/PassportStrategyHelper.test.ts diff --git a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts b/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts similarity index 96% rename from plugins/auth-backend/src/lib/PassportStrategyHelper.ts rename to plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts index 3b8d0801e9..7bc34186f4 100644 --- a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts +++ b/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts @@ -17,7 +17,7 @@ import express from 'express'; import passport from 'passport'; import jwtDecoder from 'jwt-decode'; -import { ProfileInfo } from '../providers/types'; +import { ProfileInfo, RedirectInfo } from '../../providers/types'; export type PassportDoneCallback = ( err?: Error, @@ -64,17 +64,6 @@ export const makeProfileInfo = ( }; }; -export type RedirectInfo = { - /** - * URL to redirect to - */ - url: string; - /** - * Status code to use for the redirect - */ - status?: number; -}; - export const executeRedirectStrategy = async ( req: express.Request, providerStrategy: passport.Strategy, diff --git a/plugins/auth-backend/src/lib/passport/index.ts b/plugins/auth-backend/src/lib/passport/index.ts new file mode 100644 index 0000000000..c307e212fa --- /dev/null +++ b/plugins/auth-backend/src/lib/passport/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2020 Spotify AB + * + * 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. + */ + +export { + executeFetchUserProfileStrategy, + executeFrameHandlerStrategy, + executeRedirectStrategy, + executeRefreshTokenStrategy, + makeProfileInfo, +} from './PassportStrategyHelper'; +export type { PassportDoneCallback } from './PassportStrategyHelper'; diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index ff55846302..19f8d5d803 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -32,9 +32,8 @@ import { executeRefreshTokenStrategy, makeProfileInfo, PassportDoneCallback, - RedirectInfo, -} from '../../lib/PassportStrategyHelper'; -import { AuthProviderConfig } from '../types'; +} from '../../lib/passport'; +import { AuthProviderConfig, RedirectInfo } from '../types'; import { Config } from '@backstage/config'; type PrivateInfo = { diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index 22fb908ef9..68810a5b1b 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -21,9 +21,8 @@ import { executeRedirectStrategy, makeProfileInfo, PassportDoneCallback, - RedirectInfo, -} from '../../lib/PassportStrategyHelper'; -import { AuthProviderConfig } from '../types'; +} from '../../lib/passport'; +import { AuthProviderConfig, RedirectInfo } from '../types'; import { OAuthProvider, OAuthProviderOptions, diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index a3bd6deef3..d076644e46 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -21,9 +21,8 @@ import { executeRedirectStrategy, makeProfileInfo, PassportDoneCallback, - RedirectInfo, -} from '../../lib/PassportStrategyHelper'; -import { AuthProviderConfig } from '../types'; +} from '../../lib/passport'; +import { AuthProviderConfig, RedirectInfo } from '../types'; import { OAuthProvider, OAuthProviderOptions, diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 0301062474..21cd76b26f 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -23,9 +23,8 @@ import { makeProfileInfo, executeFetchUserProfileStrategy, PassportDoneCallback, - RedirectInfo, -} from '../../lib/PassportStrategyHelper'; -import { AuthProviderConfig } from '../types'; +} from '../../lib/passport'; +import { AuthProviderConfig, RedirectInfo } from '../types'; import { OAuthProvider, OAuthProviderHandlers, diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index 5eec047b69..a7edbb134e 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -25,10 +25,9 @@ import { makeProfileInfo, executeFetchUserProfileStrategy, PassportDoneCallback, - RedirectInfo, -} from '../../lib/PassportStrategyHelper'; +} from '../../lib/passport'; -import { AuthProviderConfig } from '../types'; +import { AuthProviderConfig, RedirectInfo } from '../types'; import { OAuthProvider, diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index a1a3cc7d97..bc50408948 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -32,9 +32,8 @@ import { executeRefreshTokenStrategy, makeProfileInfo, PassportDoneCallback, - RedirectInfo, -} from '../../lib/PassportStrategyHelper'; -import { AuthProviderConfig } from '../types'; +} from '../../lib/passport'; +import { AuthProviderConfig, RedirectInfo } from '../types'; import { Config } from '@backstage/config'; type PrivateInfo = { diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index e46e396aa5..05a692ba40 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -29,9 +29,8 @@ import { makeProfileInfo, executeFetchUserProfileStrategy, PassportDoneCallback, - RedirectInfo, -} from '../../lib/PassportStrategyHelper'; -import { AuthProviderConfig } from '../types'; +} from '../../lib/passport'; +import { AuthProviderConfig, RedirectInfo } from '../types'; import { Logger } from 'winston'; import { StateStore } from 'passport-oauth2'; import { TokenIssuer } from '../../identity'; diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index d6ea0520fc..82a1cfa1b7 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -24,7 +24,7 @@ import { executeFrameHandlerStrategy, executeRedirectStrategy, PassportDoneCallback, -} from '../../lib/PassportStrategyHelper'; +} from '../../lib/passport'; import { AuthProviderConfig, AuthProviderRouteHandlers, diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index c7b70adbdc..bc80a3d9b5 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -32,6 +32,17 @@ export type AuthProviderConfig = { appUrl: string; }; +export type RedirectInfo = { + /** + * URL to redirect to + */ + url: string; + /** + * Status code to use for the redirect + */ + status?: number; +}; + /** * Any Auth provider needs to implement this interface which handles the routes in the * auth backend. Any auth API requests from the frontend reaches these methods. From fb2d4cf24131911f93066b448529500106b28b8b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 3 Sep 2020 12:39:01 +0200 Subject: [PATCH 20/34] auth-backend: refactor EnvironmentHandler to be OAuth-specific and move env config to be a provider concern --- app-config.yaml | 7 +- .../OAuthEnvironmentHandler.ts} | 89 ++++++++++++------- .../src/providers/auth0/provider.ts | 51 +++++------ .../auth-backend/src/providers/factories.ts | 31 +------ .../src/providers/github/provider.ts | 79 ++++++++-------- .../src/providers/gitlab/provider.ts | 53 ++++++----- .../src/providers/google/provider.ts | 47 +++++----- .../src/providers/microsoft/provider.ts | 59 ++++++------ .../src/providers/oauth2/provider.ts | 55 ++++++------ .../src/providers/okta/provider.ts | 51 +++++------ .../src/providers/saml/provider.ts | 21 ++--- plugins/auth-backend/src/providers/types.ts | 24 ++--- 12 files changed, 265 insertions(+), 302 deletions(-) rename plugins/auth-backend/src/lib/{EnvironmentHandler.ts => oauth/OAuthEnvironmentHandler.ts} (54%) diff --git a/app-config.yaml b/app-config.yaml index 7a8863495e..beb610a926 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -118,10 +118,9 @@ auth: audience: $secret: env: GITLAB_BASE_URL - # saml: - # development: - # entryPoint: "http://localhost:7001/" - # issuer: "passport-saml" + saml: + entryPoint: "http://localhost:7001/" + issuer: "passport-saml" okta: development: clientId: diff --git a/plugins/auth-backend/src/lib/EnvironmentHandler.ts b/plugins/auth-backend/src/lib/oauth/OAuthEnvironmentHandler.ts similarity index 54% rename from plugins/auth-backend/src/lib/EnvironmentHandler.ts rename to plugins/auth-backend/src/lib/oauth/OAuthEnvironmentHandler.ts index dd9c972cb2..d22fc52499 100644 --- a/plugins/auth-backend/src/lib/EnvironmentHandler.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthEnvironmentHandler.ts @@ -15,47 +15,32 @@ */ import express from 'express'; -import { AuthProviderRouteHandlers } from '../providers/types'; +import { Config } from '@backstage/config'; import { InputError } from '@backstage/backend-common'; +import { readState } from './helpers'; +import { AuthProviderRouteHandlers } from '../../providers/types'; -export type EnvironmentHandlers = { - [key: string]: AuthProviderRouteHandlers; -}; +export class OAuthEnvironmentHandler implements AuthProviderRouteHandlers { + static mapConfig( + config: Config, + factoryFunc: (envConfig: Config) => AuthProviderRouteHandlers, + ) { + const envs = config.keys(); + const handlers = new Map(); -export type EnvironmentIdentifierFn = ( - req: express.Request, -) => string | undefined; - -export class EnvironmentHandler implements AuthProviderRouteHandlers { - constructor( - private readonly providerId: string, - private readonly providers: EnvironmentHandlers, - private readonly envIdentifier: EnvironmentIdentifierFn, - ) {} - - private getProviderForEnv( - req: express.Request, - res: express.Response, - ): AuthProviderRouteHandlers | undefined { - const env: string | undefined = this.envIdentifier(req); - - if (!env) { - throw new InputError(`Must specify 'env' query to select environment`); + for (const env of envs) { + const envConfig = config.getConfig(env); + const handler = factoryFunc(envConfig); + handlers.set(env, handler); } - if (this.providers.hasOwnProperty(env)) { - return this.providers[env]; - } - - res.status(404).send( - `Missing configuration. -
-
-For this flow to work you need to supply a valid configuration for the "${env}" environment of the "${this.providerId}" provider.`, - ); - return undefined; + return new OAuthEnvironmentHandler(handlers); } + constructor( + private readonly handlers: Map, + ) {} + async start(req: express.Request, res: express.Response): Promise { const provider = this.getProviderForEnv(req, res); await provider?.start(req, res); @@ -78,4 +63,40 @@ For this flow to work you need to supply a valid configuration for the "${env}" const provider = this.getProviderForEnv(req, res); await provider?.logout?.(req, res); } + + private getRequestFromEnv(req: express.Request): string | undefined { + const reqEnv = req.query.env?.toString(); + if (reqEnv) { + return reqEnv; + } + const stateParams = req.query.state?.toString(); + if (!stateParams) { + return undefined; + } + const env = readState(stateParams).env; + return env; + } + + private getProviderForEnv( + req: express.Request, + res: express.Response, + ): AuthProviderRouteHandlers | undefined { + const env: string | undefined = this.getRequestFromEnv(req); + + if (!env) { + throw new InputError(`Must specify 'env' query to select environment`); + } + + if (!this.handlers.has(env)) { + res.status(404).send( + `Missing configuration. +
+
+ For this flow to work you need to supply a valid configuration for the "${env}" environment of provider.`, + ); + return undefined; + } + + return this.handlers.get(env); + } } diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index 19f8d5d803..a091a37e90 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -17,13 +17,12 @@ import express from 'express'; import passport from 'passport'; import Auth0Strategy from './strategy'; -import { Logger } from 'winston'; -import { TokenIssuer } from '../../identity'; import { OAuthProvider, OAuthProviderOptions, OAuthProviderHandlers, OAuthResponse, + OAuthEnvironmentHandler, } from '../../lib/oauth'; import { executeFetchUserProfileStrategy, @@ -33,8 +32,7 @@ import { makeProfileInfo, PassportDoneCallback, } from '../../lib/passport'; -import { AuthProviderConfig, RedirectInfo } from '../types'; -import { Config } from '@backstage/config'; +import { RedirectInfo, AuthProviderFactory } from '../types'; type PrivateInfo = { refreshToken: string; @@ -150,29 +148,28 @@ export class Auth0AuthProvider implements OAuthProviderHandlers { } } -export function createAuth0Provider( - config: AuthProviderConfig, - _: string, - envConfig: Config, - _logger: Logger, - tokenIssuer: TokenIssuer, -) { - const providerId = 'auth0'; - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const domain = envConfig.getString('domain'); - const callbackUrl = `${config.baseUrl}/${providerId}/handler/frame`; +export const createAuth0Provider: AuthProviderFactory = ({ + globalConfig, + config, + tokenIssuer, +}) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const providerId = 'auth0'; + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const domain = envConfig.getString('domain'); + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const provider = new Auth0AuthProvider({ - clientId, - clientSecret, - callbackUrl, - domain, - }); + const provider = new Auth0AuthProvider({ + clientId, + clientSecret, + callbackUrl, + domain, + }); - return OAuthProvider.fromConfig(config, provider, { - disableRefresh: true, - providerId, - tokenIssuer, + return OAuthProvider.fromConfig(globalConfig, provider, { + disableRefresh: true, + providerId, + tokenIssuer, + }); }); -} diff --git a/plugins/auth-backend/src/providers/factories.ts b/plugins/auth-backend/src/providers/factories.ts index 2b67510991..6c83dffda3 100644 --- a/plugins/auth-backend/src/providers/factories.ts +++ b/plugins/auth-backend/src/providers/factories.ts @@ -27,11 +27,6 @@ import { createAuth0Provider } from './auth0'; import { createMicrosoftProvider } from './microsoft'; import { AuthProviderConfig, AuthProviderFactory } from './types'; import { Config } from '@backstage/config'; -import { - EnvironmentHandlers, - EnvironmentHandler, - EnvironmentIdentifierFn, -} from '../lib/EnvironmentHandler'; const factories: { [providerId: string]: AuthProviderFactory } = { google: createGoogleProvider, @@ -47,9 +42,9 @@ const factories: { [providerId: string]: AuthProviderFactory } = { export const createAuthProviderRouter = ( providerId: string, globalConfig: AuthProviderConfig, - providerConfig: Config, + config: Config, logger: Logger, - issuer: TokenIssuer, + tokenIssuer: TokenIssuer, ) => { const factory = factories[providerId]; if (!factory) { @@ -57,28 +52,8 @@ export const createAuthProviderRouter = ( } const router = Router(); - const envs = providerConfig.keys(); - const envProviders: EnvironmentHandlers = {}; - let envIdentifier: EnvironmentIdentifierFn | undefined; - for (const env of envs) { - const envConfig = providerConfig.getConfig(env); - const provider = factory(globalConfig, env, envConfig, logger, issuer); - if (provider) { - envProviders[env] = provider; - envIdentifier = provider.identifyEnv; - } - } - - if (typeof envIdentifier === 'undefined') { - throw Error(`No envIdentifier provided for '${providerId}'`); - } - - const handler = new EnvironmentHandler( - providerId, - envProviders, - envIdentifier, - ); + const handler = factory({ globalConfig, config, logger, tokenIssuer }); router.get('/start', handler.start.bind(handler)); router.get('/handler/frame', handler.frameHandler.bind(handler)); diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index 68810a5b1b..ab52b628e7 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -22,17 +22,15 @@ import { makeProfileInfo, PassportDoneCallback, } from '../../lib/passport'; -import { AuthProviderConfig, RedirectInfo } from '../types'; +import { RedirectInfo, AuthProviderFactory } from '../types'; import { OAuthProvider, OAuthProviderOptions, OAuthProviderHandlers, OAuthResponse, + OAuthEnvironmentHandler, } from '../../lib/oauth'; -import { Logger } from 'winston'; -import { TokenIssuer } from '../../identity'; import passport from 'passport'; -import { Config } from '@backstage/config'; export type GithubAuthProviderOptions = OAuthProviderOptions & { tokenUrl?: string; @@ -136,43 +134,42 @@ export class GithubAuthProvider implements OAuthProviderHandlers { } } -export function createGithubProvider( - config: AuthProviderConfig, - _: string, - envConfig: Config, - _logger: Logger, - tokenIssuer: TokenIssuer, -) { - const providerId = 'github'; - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const enterpriseInstanceUrl = envConfig.getOptionalString( - 'enterpriseInstanceUrl', - ); - const authorizationUrl = enterpriseInstanceUrl - ? `${enterpriseInstanceUrl}/login/oauth/authorize` - : undefined; - const tokenUrl = enterpriseInstanceUrl - ? `${enterpriseInstanceUrl}/login/oauth/access_token` - : undefined; - const userProfileUrl = enterpriseInstanceUrl - ? `${enterpriseInstanceUrl}/api/v3/user` - : undefined; - const callbackUrl = `${config.baseUrl}/${providerId}/handler/frame`; +export const createGithubProvider: AuthProviderFactory = ({ + globalConfig, + config, + tokenIssuer, +}) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const providerId = 'github'; + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const enterpriseInstanceUrl = envConfig.getOptionalString( + 'enterpriseInstanceUrl', + ); + const authorizationUrl = enterpriseInstanceUrl + ? `${enterpriseInstanceUrl}/login/oauth/authorize` + : undefined; + const tokenUrl = enterpriseInstanceUrl + ? `${enterpriseInstanceUrl}/login/oauth/access_token` + : undefined; + const userProfileUrl = enterpriseInstanceUrl + ? `${enterpriseInstanceUrl}/api/v3/user` + : undefined; + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const provider = new GithubAuthProvider({ - clientId, - clientSecret, - callbackUrl, - tokenUrl, - userProfileUrl, - authorizationUrl, - }); + const provider = new GithubAuthProvider({ + clientId, + clientSecret, + callbackUrl, + tokenUrl, + userProfileUrl, + authorizationUrl, + }); - return OAuthProvider.fromConfig(config, provider, { - disableRefresh: true, - persistScopes: true, - providerId, - tokenIssuer, + return OAuthProvider.fromConfig(globalConfig, provider, { + disableRefresh: true, + persistScopes: true, + providerId, + tokenIssuer, + }); }); -} diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index d076644e46..26e849aa5c 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -22,17 +22,15 @@ import { makeProfileInfo, PassportDoneCallback, } from '../../lib/passport'; -import { AuthProviderConfig, RedirectInfo } from '../types'; +import { RedirectInfo, AuthProviderFactory } from '../types'; import { OAuthProvider, OAuthProviderOptions, OAuthProviderHandlers, OAuthResponse, + OAuthEnvironmentHandler, } from '../../lib/oauth'; -import { Logger } from 'winston'; -import { TokenIssuer } from '../../identity'; import passport from 'passport'; -import { Config } from '@backstage/config'; export type GitlabAuthProviderOptions = OAuthProviderOptions & { baseUrl: string; @@ -139,30 +137,29 @@ export class GitlabAuthProvider implements OAuthProviderHandlers { } } -export function createGitlabProvider( - config: AuthProviderConfig, - _: string, - envConfig: Config, - _logger: Logger, - tokenIssuer: TokenIssuer, -) { - const providerId = 'gitlab'; - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const audience = envConfig.getString('audience'); - const baseUrl = audience || 'https://gitlab.com'; - const callbackUrl = `${config.baseUrl}/${providerId}/handler/frame`; +export const createGitlabProvider: AuthProviderFactory = ({ + globalConfig, + config, + tokenIssuer, +}) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const providerId = 'gitlab'; + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const audience = envConfig.getString('audience'); + const baseUrl = audience || 'https://gitlab.com'; + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const provider = new GitlabAuthProvider({ - clientId, - clientSecret, - callbackUrl, - baseUrl, - }); + const provider = new GitlabAuthProvider({ + clientId, + clientSecret, + callbackUrl, + baseUrl, + }); - return OAuthProvider.fromConfig(config, provider, { - disableRefresh: true, - providerId, - tokenIssuer, + return OAuthProvider.fromConfig(globalConfig, provider, { + disableRefresh: true, + providerId, + tokenIssuer, + }); }); -} diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 21cd76b26f..62e1d90577 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -24,17 +24,15 @@ import { executeFetchUserProfileStrategy, PassportDoneCallback, } from '../../lib/passport'; -import { AuthProviderConfig, RedirectInfo } from '../types'; +import { RedirectInfo, AuthProviderFactory } from '../types'; import { OAuthProvider, OAuthProviderHandlers, OAuthProviderOptions, OAuthResponse, + OAuthEnvironmentHandler, } from '../../lib/oauth'; import passport from 'passport'; -import { Logger } from 'winston'; -import { TokenIssuer } from '../../identity'; -import { Config } from '@backstage/config'; type PrivateInfo = { refreshToken: string; @@ -147,27 +145,26 @@ export class GoogleAuthProvider implements OAuthProviderHandlers { } } -export function createGoogleProvider( - config: AuthProviderConfig, - _: string, - envConfig: Config, - _logger: Logger, - tokenIssuer: TokenIssuer, -) { - const providerId = 'google'; - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const callbackUrl = `${config.baseUrl}/${providerId}/handler/frame`; +export const createGoogleProvider: AuthProviderFactory = ({ + globalConfig, + config, + tokenIssuer, +}) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const providerId = 'google'; + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const provider = new GoogleAuthProvider({ - clientId, - clientSecret, - callbackUrl, - }); + const provider = new GoogleAuthProvider({ + clientId, + clientSecret, + callbackUrl, + }); - return OAuthProvider.fromConfig(config, provider, { - disableRefresh: false, - providerId, - tokenIssuer, + return OAuthProvider.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + }); }); -} diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index a7edbb134e..97e7dc3781 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -27,17 +27,15 @@ import { PassportDoneCallback, } from '../../lib/passport'; -import { AuthProviderConfig, RedirectInfo } from '../types'; +import { RedirectInfo, AuthProviderFactory } from '../types'; import { OAuthProvider, OAuthProviderOptions, OAuthProviderHandlers, OAuthResponse, + OAuthEnvironmentHandler, } from '../../lib/oauth'; -import { Logger } from 'winston'; -import { TokenIssuer } from '../../identity'; -import { Config } from '@backstage/config'; import got from 'got'; @@ -204,34 +202,33 @@ export class MicrosoftAuthProvider implements OAuthProviderHandlers { } } -export function createMicrosoftProvider( - config: AuthProviderConfig, - _: string, - envConfig: Config, - _logger: Logger, - tokenIssuer: TokenIssuer, -) { - const providerId = 'microsoft'; +export const createMicrosoftProvider: AuthProviderFactory = ({ + globalConfig, + config, + tokenIssuer, +}) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const providerId = 'microsoft'; - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const tenantID = envConfig.getString('tenantId'); + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const tenantID = envConfig.getString('tenantId'); - const callbackUrl = `${config.baseUrl}/${providerId}/handler/frame`; - const authorizationUrl = `https://login.microsoftonline.com/${tenantID}/oauth2/v2.0/authorize`; - const tokenUrl = `https://login.microsoftonline.com/${tenantID}/oauth2/v2.0/token`; + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const authorizationUrl = `https://login.microsoftonline.com/${tenantID}/oauth2/v2.0/authorize`; + const tokenUrl = `https://login.microsoftonline.com/${tenantID}/oauth2/v2.0/token`; - const provider = new MicrosoftAuthProvider({ - clientId, - clientSecret, - callbackUrl, - authorizationUrl, - tokenUrl, + const provider = new MicrosoftAuthProvider({ + clientId, + clientSecret, + callbackUrl, + authorizationUrl, + tokenUrl, + }); + + return OAuthProvider.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + }); }); - - return OAuthProvider.fromConfig(config, provider, { - disableRefresh: false, - providerId, - tokenIssuer, - }); -} diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index bc50408948..fea7d58e5c 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -17,13 +17,12 @@ import express from 'express'; import passport from 'passport'; import { Strategy as OAuth2Strategy } from 'passport-oauth2'; -import { Logger } from 'winston'; -import { TokenIssuer } from '../../identity'; import { OAuthProvider, OAuthProviderOptions, OAuthProviderHandlers, OAuthResponse, + OAuthEnvironmentHandler, } from '../../lib/oauth'; import { executeFetchUserProfileStrategy, @@ -33,8 +32,7 @@ import { makeProfileInfo, PassportDoneCallback, } from '../../lib/passport'; -import { AuthProviderConfig, RedirectInfo } from '../types'; -import { Config } from '@backstage/config'; +import { RedirectInfo, AuthProviderFactory } from '../types'; type PrivateInfo = { refreshToken: string; @@ -158,31 +156,30 @@ export class OAuth2AuthProvider implements OAuthProviderHandlers { } } -export function createOAuth2Provider( - config: AuthProviderConfig, - _: string, - envConfig: Config, - _logger: Logger, - tokenIssuer: TokenIssuer, -) { - const providerId = 'oauth2'; - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const callbackUrl = `${config.baseUrl}/${providerId}/handler/frame`; - const authorizationUrl = envConfig.getString('authorizationUrl'); - const tokenUrl = envConfig.getString('tokenUrl'); +export const createOAuth2Provider: AuthProviderFactory = ({ + globalConfig, + config, + tokenIssuer, +}) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const providerId = 'oauth2'; + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const authorizationUrl = envConfig.getString('authorizationUrl'); + const tokenUrl = envConfig.getString('tokenUrl'); - const provider = new OAuth2AuthProvider({ - clientId, - clientSecret, - callbackUrl, - authorizationUrl, - tokenUrl, - }); + const provider = new OAuth2AuthProvider({ + clientId, + clientSecret, + callbackUrl, + authorizationUrl, + tokenUrl, + }); - return OAuthProvider.fromConfig(config, provider, { - disableRefresh: false, - providerId, - tokenIssuer, + return OAuthProvider.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + }); }); -} diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index 05a692ba40..3d33868e84 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -19,6 +19,7 @@ import { OAuthProviderOptions, OAuthProviderHandlers, OAuthResponse, + OAuthEnvironmentHandler, } from '../../lib/oauth'; import { Strategy as OktaStrategy } from 'passport-okta-oauth'; import passport from 'passport'; @@ -30,11 +31,8 @@ import { executeFetchUserProfileStrategy, PassportDoneCallback, } from '../../lib/passport'; -import { AuthProviderConfig, RedirectInfo } from '../types'; -import { Logger } from 'winston'; +import { RedirectInfo, AuthProviderFactory } from '../types'; import { StateStore } from 'passport-oauth2'; -import { TokenIssuer } from '../../identity'; -import { Config } from '@backstage/config'; type PrivateInfo = { refreshToken: string; @@ -169,29 +167,28 @@ export class OktaAuthProvider implements OAuthProviderHandlers { } } -export function createOktaProvider( - config: AuthProviderConfig, - _: string, - envConfig: Config, - _logger: Logger, - tokenIssuer: TokenIssuer, -) { - const providerId = 'okta'; - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const audience = envConfig.getString('audience'); - const callbackUrl = `${config.baseUrl}/${providerId}/handler/frame`; +export const createOktaProvider: AuthProviderFactory = ({ + globalConfig, + config, + tokenIssuer, +}) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const providerId = 'okta'; + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const audience = envConfig.getString('audience'); + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const provider = new OktaAuthProvider({ - audience, - clientId, - clientSecret, - callbackUrl, - }); + const provider = new OktaAuthProvider({ + audience, + clientId, + clientSecret, + callbackUrl, + }); - return OAuthProvider.fromConfig(config, provider, { - disableRefresh: false, - providerId, - tokenIssuer, + return OAuthProvider.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + }); }); -} diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 82a1cfa1b7..2bd8ed0bf3 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -26,14 +26,12 @@ import { PassportDoneCallback, } from '../../lib/passport'; import { - AuthProviderConfig, AuthProviderRouteHandlers, ProfileInfo, + AuthProviderFactory, } from '../types'; import { postMessageResponse } from '../../lib/flow'; -import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; -import { Config } from '@backstage/config'; type SamlInfo = { userId: string; @@ -119,15 +117,12 @@ type SAMLProviderOptions = { tokenIssuer: TokenIssuer; }; -export function createSamlProvider( - _authProviderConfig: AuthProviderConfig, - _env: string, - envConfig: Config, - _logger: Logger, - tokenIssuer: TokenIssuer, -) { - const entryPoint = envConfig.getString('entryPoint'); - const issuer = envConfig.getString('issuer'); +export const createSamlProvider: AuthProviderFactory = ({ + config, + tokenIssuer, +}) => { + const entryPoint = config.getString('entryPoint'); + const issuer = config.getString('issuer'); const opts = { entryPoint, issuer, @@ -136,4 +131,4 @@ export function createSamlProvider( }; return new SamlAuthProvider(opts); -} +}; diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index bc80a3d9b5..5c05b02739 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -108,24 +108,18 @@ export interface AuthProviderRouteHandlers { * @param {express.Response} res */ logout?(req: express.Request, res: express.Response): Promise; - - /** - *(Optional) A method to identify the environment Context of the Request - * - *Request - *- contains the environment context information encoded in the request - * @param {express.Request} req - */ - identifyEnv?(req: express.Request): string | undefined; } +export type AuthProviderFactoryOptions = { + globalConfig: AuthProviderConfig; + config: Config; + logger: Logger; + tokenIssuer: TokenIssuer; +}; + export type AuthProviderFactory = ( - globalConfig: AuthProviderConfig, - env: string, - envConfig: Config, - logger: Logger, - issuer: TokenIssuer, -) => AuthProviderRouteHandlers | undefined; + options: AuthProviderFactoryOptions, +) => AuthProviderRouteHandlers; export type AuthResponse = { providerInfo: ProviderInfo; From fa32cee7adb2758f5c5dcec0059c74f069f21371 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 3 Sep 2020 14:51:22 +0200 Subject: [PATCH 21/34] docs/auth: update to reflect backend changes --- docs/auth/add-auth-provider.md | 151 ++++++++++++++++++++---------- docs/auth/auth-backend-classes.md | 47 +++++----- 2 files changed, 127 insertions(+), 71 deletions(-) diff --git a/docs/auth/add-auth-provider.md b/docs/auth/add-auth-provider.md index a2830dcf4f..55ef554c05 100644 --- a/docs/auth/add-auth-provider.md +++ b/docs/auth/add-auth-provider.md @@ -48,22 +48,35 @@ provider class which implements a handler for the chosen framework. #### Adding an OAuth based provider If we're adding an `OAuth` based provider we would implement the -[OAuthProviderHandlers](#OAuthProviderHandlers) interface. +[OAuthProviderHandlers](#OAuthProviderHandlers) interface. By implementing this +interface we can use the `OAuthProvider` class provided by `lib/oauth`, meaning +we don't need to implement the full +[AuthProviderRouteHandlers](#AuthProviderRouteHandlers) interface that providers +otherwise need to implement. -The provider class takes the provider's configuration as a class parameter. It -also imports the `Strategy` from the passport package. +The provider class takes the provider's options as a class parameter. It also +imports the `Strategy` from the passport package. ```ts import { Strategy as ProviderAStrategy } from 'passport-provider-a'; +export type ProviderAProviderOptions = OAuthProviderOptions & { + // extra options here +} + export class ProviderAAuthProvider implements OAuthProviderHandlers { - private readonly providerConfig: AuthProviderConfig; private readonly _strategy: ProviderAStrategy; - constructor(providerConfig: AuthProviderConfig) { - this.providerConfig = providerConfig; + constructor(options: ProviderAProviderOptions) { this._strategy = new ProviderAStrategy( - { ...providerConfig.options }, + { + clientID: options.clientId, + clientSecret: options.clientSecret, + callbackURL: options.callbackUrl, + passReqToCallback: false as true, + response_type: 'code', + /// ... etc + } verifyFunction, // See the "Verify Callback" section ); } @@ -82,14 +95,18 @@ An non-`OAuth` based provider could implement [AuthProviderRouteHandlers](#AuthProviderRouteHandlers) instead. ```ts +type ProviderAOptions = { + // ... +}; + export class ProviderAAuthProvider implements AuthProviderRouteHandlers { - private readonly providerConfig: AuthProviderConfig; private readonly _strategy: ProviderAStrategy; - constructor(providerConfig: AuthProviderConfig) { - this.providerConfig = providerConfig; + constructor(options: ProviderAOptions) { this._strategy = new ProviderAStrategy( - { ...providerConfig.options }, + { + // ... + }, verifyFunction, // See the "Verify Callback" section ); } @@ -101,31 +118,61 @@ export class ProviderAAuthProvider implements AuthProviderRouteHandlers { } ``` -#### Create method +#### Factory method -Each provider exports a create method that creates the provider instance, -optionally extending a supported authorization framework. This method exists to -allow for flexibility if additional frameworks are supported in the future. +Each provider exports a factory function that instantiates the provider. The +factory should implement [AuthProviderFactory](#AuthProviderFactory), which +passes in a object with utilities for configuration, logging, token issuing, +etc. The factory should return an implementation of +[AuthProviderRouteHandlers](#AuthProviderRouteHandlers). -Implementing OAuth by returning an instance of `OAuthProvider` based of the -provider's class: +The factory is what decides the mapping from +[static configuration](../conf/index.md) to the creation of auth providers. For +example, OAuth providers use `OAuthEnvironmentHandler` to allow for multiple +different configurations, one for each environment, which looks like this; ```ts -export function createProviderAProvider(config: AuthProviderConfig) { - const provider = new ProviderAAuthProvider(config); - const oauthProvider = new OAuthProvider(provider, config.provider, true); - return oauthProvider; -} +export const createOktaProvider: AuthProviderFactory = ({ + globalConfig, + config, + tokenIssuer, +}) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + // read options from config + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + + // instantiate our OAuthProviderHandlers implementation + const provider = new OktaAuthProvider({ + audience, + clientId, + clientSecret, + callbackUrl, + }); + + // Wrap the OAuthProviderHandlers with OAuthProvider, which implements AuthProviderRouteHandlers + return OAuthProvider.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + }); + }); ``` -Not extending with OAuth, the main difference here is that the create method is -returning a instance of the class without adding the OAuth authorization -framework to it. +The purpose of the different environment is to allow for a single auth-backend +to service as the authentication service for multiple different frontend +environments, such as local development, staging, and production. + +The factory function for other providers can be a lot simpler, as they might not +have configuration for each environment. Looking something like this: ```ts -export function createProviderAProvider(config: AuthProviderConfig) { - return new ProviderAAuthProvider(config); -} +export const createProviderAProvider: AuthProviderFactory = ({ config }) => { + const a = config.getString('a'); + const b = config.getString('b'); + + return new ProviderAAuthProvider({ a, b }); +}; ``` #### Verify Callback @@ -153,19 +200,6 @@ export { createProviderAProvider } from './provider'; ### Hook it up to the backend -**`plugins/auth-backend/src/providers/config.ts`** The provider needs to be -configured properly so you need to add it to the list of configured providers, -all of which implement [AuthProviderConfig](#AuthProviderConfig): - -```ts -export const providers = [ - { - provider: 'providerA', # used as an identifier - options: { ... }, # consult the provider documentation for which options you should provide - disableRefresh: true # if the provider lacks refresh tokens - }, -``` - **`plugins/auth-backend/src/providers/factories.ts`** When the `auth-backend` starts it sets up routing for all the available providers by calling `createAuthProviderRouter` on each provider. You need to import the create @@ -173,6 +207,7 @@ method from the provider and add it to the factory: ```ts import { createProviderAProvider } from './providerA'; + const factories: { [providerId: string]: AuthProviderFactory } = { providerA: createProviderAProvider, }; @@ -203,10 +238,21 @@ web browser and you should be able to trigger the authorization flow. ```ts export interface OAuthProviderHandlers { - start(req: express.Request, options: any): Promise; - handler(req: express.Request): Promise; - refresh?(refreshToken: string, scope: string): Promise; - logout?(): Promise; + start( + req: express.Request, + options: Record, + ): Promise; + handler( + req: express.Request, + ): Promise<{ + response: AuthResponse; + refreshToken?: string; + }>; + refresh?( + refreshToken: string, + scope: string, + ): Promise>; + logout?(): Promise; } ``` @@ -221,12 +267,17 @@ export interface AuthProviderRouteHandlers { } ``` -##### AuthProviderConfig +##### AuthProviderFactory ```ts -export type AuthProviderConfig = { - provider: string; - options: any; - disableRefresh?: boolean; +export type AuthProviderFactoryOptions = { + globalConfig: AuthProviderConfig; + config: Config; + logger: Logger; + tokenIssuer: TokenIssuer; }; + +export type AuthProviderFactory = ( + options: AuthProviderFactoryOptions, +) => AuthProviderRouteHandlers; ``` diff --git a/docs/auth/auth-backend-classes.md b/docs/auth/auth-backend-classes.md index 9034a3d054..258a732651 100644 --- a/docs/auth/auth-backend-classes.md +++ b/docs/auth/auth-backend-classes.md @@ -50,11 +50,12 @@ OAuth2) providers, you can configure them by setting the right variables in Each authentication provider (except SAML) needs five parameters: an OAuth client ID, a client secret, an authorization endpoint and a token endpoint, and an app origin. The app origin is the URL at which the frontend of the -application is hosted. This is required because the application opens a popup -window to perform the authentication, and once the flow is completed, the popup -window sends a `postMessage` to the frontend application to indicate the result -of the operation. Also this URL is used to verify that authentication requests -are coming from only this endpoint. +application is hosted, and it is read from the `app.baseUrl` config. This is +required because the application opens a popup window to perform the +authentication, and once the flow is completed, the popup window sends a +`postMessage` to the frontend application to indicate the result of the +operation. Also this URL is used to verify that authentication requests are +coming from only this endpoint. These values are configured via the `app-config.yaml` present in the root of your app folder. @@ -64,8 +65,6 @@ auth: providers: google: development: - appOrigin: "http://localhost:3000/" - secure: false clientId: $secret: env: AUTH_GOOGLE_CLIENT_ID @@ -74,8 +73,6 @@ auth: env: AUTH_GOOGLE_CLIENT_SECRET github: development: - appOrigin: "http://localhost:3000/" - secure: false clientId: $secret: env: AUTH_GITHUB_CLIENT_ID @@ -87,8 +84,6 @@ auth: env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL gitlab: development: - appOrigin: "http://localhost:3000/" - secure: false clientId: $secret: ... @@ -96,24 +91,34 @@ auth: ## Technical Notes -### EnvironmentHandler +### OAuthEnvironmentHandler The concept of an "env" is core to the way the auth backend works. It uses an `env` query parameter to identify the environment in which the application is running (`development`, `staging`, `production`, etc). Each runtime can support multiple environments at the same time and the right handler for each request is identified and dispatched to based on the `env` parameter. All -`AuthProviderRouteHandlers` are wrapped within an `EnvironmentHandler`. +`AuthProviderRouteHandlers` are wrapped within an `OAuthEnvironmentHandler`. -An `EnvironmentHandler` takes an ID for each provider that it wraps, the -handlers for each env the provider is supported in, and a function that, given a -`Request` as argument, can extract the information about the env under which it -should be processed. +To instantiate multiple OAuth providers for different environments, use +`OAuthEnvironmentHandler.mapConfig`. It's a helper to iterate over a +configuration object that is a map of environment to configurations. See one of +the existing OAuth providers for an example of how it is used. -Each provider exposes a factory function `createXProvider` (where X is the name -of the provider) that takes the global config, env and other parameters and -returns an `AuthProviderRouteHandlers` for each env, and an `envIdentifier` -function to identify the env of a request. +Given the following configuration: + +```yaml +development: + clientId: abc + clientSecret: secret +production: + clientId: xyz + clientSecret: supersecret +``` + +The `OAuthEnvironmentHandler.mapConfig(config, envConfig => ...)` call will +split the `config` by the top level `development` and `production` keys, and +pass on each block as `envConfig`. For a list of currently available providers, look in the `factories` module located in `plugins/auth-backend/src/providers/factories.ts` From c8785b9ee063fd3c76c36bdcc17e37d218c6d7db Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 3 Sep 2020 15:08:38 +0200 Subject: [PATCH 22/34] auth-backend: rename OAuthProvider to OAuthAdapter --- docs/auth/auth-backend-classes.md | 2 +- .../src/lib/flow/authFlowHelpers.test.ts | 2 +- ...hProvider.test.ts => OAuthAdapter.test.ts} | 24 ++++++++---------- .../{OAuthProvider.ts => OAuthAdapter.ts} | 25 ++++++++----------- plugins/auth-backend/src/lib/oauth/index.ts | 4 +-- plugins/auth-backend/src/lib/oauth/types.ts | 2 +- .../src/providers/auth0/provider.ts | 8 +++--- .../src/providers/github/provider.ts | 8 +++--- .../src/providers/gitlab/provider.ts | 8 +++--- .../src/providers/google/provider.ts | 8 +++--- .../src/providers/microsoft/provider.ts | 8 +++--- .../src/providers/oauth2/provider.ts | 8 +++--- .../src/providers/okta/provider.ts | 8 +++--- 13 files changed, 53 insertions(+), 62 deletions(-) rename plugins/auth-backend/src/lib/oauth/{OAuthProvider.test.ts => OAuthAdapter.test.ts} (91%) rename plugins/auth-backend/src/lib/oauth/{OAuthProvider.ts => OAuthAdapter.ts} (91%) diff --git a/docs/auth/auth-backend-classes.md b/docs/auth/auth-backend-classes.md index 258a732651..0147c66dd9 100644 --- a/docs/auth/auth-backend-classes.md +++ b/docs/auth/auth-backend-classes.md @@ -26,7 +26,7 @@ refer to the type documentation under `plugins/auth-backend/src/providers/types.ts`. There are currently two different classes for two authentication mechanisms that -implement this interface: an `OAuthProvider` for [OAuth](https://oauth.net/2/) +implement this interface: an `OAuthAdapter` for [OAuth](https://oauth.net/2/) based mechanisms and a `SAMLAuthProvider` for [SAML](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html) based mechanisms. diff --git a/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts b/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts index b525e98e12..f42e4c4270 100644 --- a/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts +++ b/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts @@ -18,7 +18,7 @@ import express from 'express'; import { ensuresXRequestedWith, postMessageResponse } from './authFlowHelpers'; import { WebMessageResponse } from './types'; -describe('OAuthProvider Utils', () => { +describe('oauth helpers', () => { describe('postMessageResponse', () => { const appOrigin = 'http://localhost:3000'; it('should post a message back with payload success', () => { diff --git a/plugins/auth-backend/src/lib/oauth/OAuthProvider.test.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts similarity index 91% rename from plugins/auth-backend/src/lib/oauth/OAuthProvider.test.ts rename to plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts index ce78733211..d2b31213f2 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthProvider.test.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts @@ -15,13 +15,9 @@ */ import express from 'express'; -import { - THOUSAND_DAYS_MS, - TEN_MINUTES_MS, - OAuthProvider, -} from './OAuthProvider'; +import { THOUSAND_DAYS_MS, TEN_MINUTES_MS, OAuthAdapter } from './OAuthAdapter'; import { encodeState } from './helpers'; -import { OAuthProviderHandlers } from './types'; +import { OAuthHandlers } from './types'; const mockResponseData = { providerInfo: { @@ -38,8 +34,8 @@ const mockResponseData = { }, }; -describe('OAuthProvider', () => { - class MyAuthProvider implements OAuthProviderHandlers { +describe('OAuthAdapter', () => { + class MyAuthProvider implements OAuthHandlers { async start() { return { url: '/url', @@ -71,7 +67,7 @@ describe('OAuthProvider', () => { }; it('sets the correct headers in start', async () => { - const oauthProvider = new OAuthProvider( + const oauthProvider = new OAuthAdapter( providerInstance, oAuthProviderOptions, ); @@ -106,7 +102,7 @@ describe('OAuthProvider', () => { }); it('sets the refresh cookie if refresh is enabled', async () => { - const oauthProvider = new OAuthProvider(providerInstance, { + const oauthProvider = new OAuthAdapter(providerInstance, { ...oAuthProviderOptions, disableRefresh: false, }); @@ -140,7 +136,7 @@ describe('OAuthProvider', () => { }); it('does not set the refresh cookie if refresh is disabled', async () => { - const oauthProvider = new OAuthProvider(providerInstance, { + const oauthProvider = new OAuthAdapter(providerInstance, { ...oAuthProviderOptions, disableRefresh: true, }); @@ -165,7 +161,7 @@ describe('OAuthProvider', () => { }); it('removes refresh cookie when logging out', async () => { - const oauthProvider = new OAuthProvider(providerInstance, { + const oauthProvider = new OAuthAdapter(providerInstance, { ...oAuthProviderOptions, disableRefresh: false, }); @@ -190,7 +186,7 @@ describe('OAuthProvider', () => { it('gets new access-token when refreshing', async () => { oAuthProviderOptions.disableRefresh = false; - const oauthProvider = new OAuthProvider(providerInstance, { + const oauthProvider = new OAuthAdapter(providerInstance, { ...oAuthProviderOptions, disableRefresh: false, }); @@ -219,7 +215,7 @@ describe('OAuthProvider', () => { }); it('handles refresh without capabilities', async () => { - const oauthProvider = new OAuthProvider(providerInstance, { + const oauthProvider = new OAuthAdapter(providerInstance, { ...oAuthProviderOptions, disableRefresh: true, }); diff --git a/plugins/auth-backend/src/lib/oauth/OAuthProvider.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts similarity index 91% rename from plugins/auth-backend/src/lib/oauth/OAuthProvider.ts rename to plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index 1fe62845a0..0f092ae780 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthProvider.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -26,7 +26,7 @@ import { InputError } from '@backstage/backend-common'; import { TokenIssuer } from '../../identity'; import { verifyNonce, encodeState } from './helpers'; import { postMessageResponse, ensuresXRequestedWith } from '../flow'; -import { OAuthProviderHandlers } from './types'; +import { OAuthHandlers } from './types'; export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; export const TEN_MINUTES_MS = 600 * 1000; @@ -42,20 +42,20 @@ export type Options = { tokenIssuer: TokenIssuer; }; -export class OAuthProvider implements AuthProviderRouteHandlers { +export class OAuthAdapter implements AuthProviderRouteHandlers { static fromConfig( config: AuthProviderConfig, - providerHandlers: OAuthProviderHandlers, + handlers: OAuthHandlers, options: Pick< Options, 'providerId' | 'persistScopes' | 'disableRefresh' | 'tokenIssuer' >, - ): OAuthProvider { + ): OAuthAdapter { const { origin: appOrigin } = new URL(config.appUrl); const secure = config.baseUrl.startsWith('https://'); const url = new URL(config.baseUrl); const cookiePath = `${url.pathname}/${options.providerId}`; - return new OAuthProvider(providerHandlers, { + return new OAuthAdapter(handlers, { ...options, appOrigin, cookieDomain: url.hostname, @@ -65,7 +65,7 @@ export class OAuthProvider implements AuthProviderRouteHandlers { } constructor( - private readonly providerHandlers: OAuthProviderHandlers, + private readonly handlers: OAuthHandlers, private readonly options: Options, ) {} @@ -94,10 +94,7 @@ export class OAuthProvider implements AuthProviderRouteHandlers { state: stateParameter, }; - const { url, status } = await this.providerHandlers.start( - req, - queryParameters, - ); + const { url, status } = await this.handlers.start(req, queryParameters); res.statusCode = status || 302; res.setHeader('Location', url); @@ -113,9 +110,7 @@ export class OAuthProvider implements AuthProviderRouteHandlers { // verify nonce cookie and state cookie on callback verifyNonce(req, this.options.providerId); - const { response, refreshToken } = await this.providerHandlers.handler( - req, - ); + const { response, refreshToken } = await this.handlers.handler(req); if (this.options.persistScopes) { const grantedScopes = this.getScopesFromCookie( @@ -172,7 +167,7 @@ export class OAuthProvider implements AuthProviderRouteHandlers { return; } - if (!this.providerHandlers.refresh || this.options.disableRefresh) { + if (!this.handlers.refresh || this.options.disableRefresh) { res.send( `Refresh token not supported for provider: ${this.options.providerId}`, ); @@ -191,7 +186,7 @@ export class OAuthProvider implements AuthProviderRouteHandlers { const scope = req.query.scope?.toString() ?? ''; // get new access_token - const response = await this.providerHandlers.refresh(refreshToken, scope); + const response = await this.handlers.refresh(refreshToken, scope); await this.populateIdentity(response.backstageIdentity); diff --git a/plugins/auth-backend/src/lib/oauth/index.ts b/plugins/auth-backend/src/lib/oauth/index.ts index eac43141a4..05c8bd9d3d 100644 --- a/plugins/auth-backend/src/lib/oauth/index.ts +++ b/plugins/auth-backend/src/lib/oauth/index.ts @@ -15,9 +15,9 @@ */ export { OAuthEnvironmentHandler } from './OAuthEnvironmentHandler'; -export { OAuthProvider } from './OAuthProvider'; +export { OAuthAdapter } from './OAuthAdapter'; export type { - OAuthProviderHandlers, + OAuthHandlers, OAuthProviderInfo, OAuthProviderOptions, OAuthResponse, diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts index 440b89ed6f..a854326bed 100644 --- a/plugins/auth-backend/src/lib/oauth/types.ts +++ b/plugins/auth-backend/src/lib/oauth/types.ts @@ -72,7 +72,7 @@ export type OAuthState = { * handlers for different methods to perform authentication, get access tokens, * refresh tokens and perform sign out. */ -export interface OAuthProviderHandlers { +export interface OAuthHandlers { /** * This method initiates a sign in request with an auth provider. * @param {express.Request} req diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index a091a37e90..3b7817ffe9 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -18,9 +18,9 @@ import express from 'express'; import passport from 'passport'; import Auth0Strategy from './strategy'; import { - OAuthProvider, + OAuthAdapter, OAuthProviderOptions, - OAuthProviderHandlers, + OAuthHandlers, OAuthResponse, OAuthEnvironmentHandler, } from '../../lib/oauth'; @@ -42,7 +42,7 @@ export type Auth0AuthProviderOptions = OAuthProviderOptions & { domain: string; }; -export class Auth0AuthProvider implements OAuthProviderHandlers { +export class Auth0AuthProvider implements OAuthHandlers { private readonly _strategy: Auth0Strategy; constructor(options: Auth0AuthProviderOptions) { @@ -167,7 +167,7 @@ export const createAuth0Provider: AuthProviderFactory = ({ domain, }); - return OAuthProvider.fromConfig(globalConfig, provider, { + return OAuthAdapter.fromConfig(globalConfig, provider, { disableRefresh: true, providerId, tokenIssuer, diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index ab52b628e7..2205fb6794 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -24,9 +24,9 @@ import { } from '../../lib/passport'; import { RedirectInfo, AuthProviderFactory } from '../types'; import { - OAuthProvider, + OAuthAdapter, OAuthProviderOptions, - OAuthProviderHandlers, + OAuthHandlers, OAuthResponse, OAuthEnvironmentHandler, } from '../../lib/oauth'; @@ -38,7 +38,7 @@ export type GithubAuthProviderOptions = OAuthProviderOptions & { authorizationUrl?: string; }; -export class GithubAuthProvider implements OAuthProviderHandlers { +export class GithubAuthProvider implements OAuthHandlers { private readonly _strategy: GithubStrategy; static transformPassportProfile(rawProfile: any): passport.Profile { @@ -166,7 +166,7 @@ export const createGithubProvider: AuthProviderFactory = ({ authorizationUrl, }); - return OAuthProvider.fromConfig(globalConfig, provider, { + return OAuthAdapter.fromConfig(globalConfig, provider, { disableRefresh: true, persistScopes: true, providerId, diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index 26e849aa5c..97f0b2bd22 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -24,9 +24,9 @@ import { } from '../../lib/passport'; import { RedirectInfo, AuthProviderFactory } from '../types'; import { - OAuthProvider, + OAuthAdapter, OAuthProviderOptions, - OAuthProviderHandlers, + OAuthHandlers, OAuthResponse, OAuthEnvironmentHandler, } from '../../lib/oauth'; @@ -36,7 +36,7 @@ export type GitlabAuthProviderOptions = OAuthProviderOptions & { baseUrl: string; }; -export class GitlabAuthProvider implements OAuthProviderHandlers { +export class GitlabAuthProvider implements OAuthHandlers { private readonly _strategy: GitlabStrategy; static transformPassportProfile(rawProfile: any): passport.Profile { @@ -157,7 +157,7 @@ export const createGitlabProvider: AuthProviderFactory = ({ baseUrl, }); - return OAuthProvider.fromConfig(globalConfig, provider, { + return OAuthAdapter.fromConfig(globalConfig, provider, { disableRefresh: true, providerId, tokenIssuer, diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 62e1d90577..9ee2e1d680 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -26,8 +26,8 @@ import { } from '../../lib/passport'; import { RedirectInfo, AuthProviderFactory } from '../types'; import { - OAuthProvider, - OAuthProviderHandlers, + OAuthAdapter, + OAuthHandlers, OAuthProviderOptions, OAuthResponse, OAuthEnvironmentHandler, @@ -38,7 +38,7 @@ type PrivateInfo = { refreshToken: string; }; -export class GoogleAuthProvider implements OAuthProviderHandlers { +export class GoogleAuthProvider implements OAuthHandlers { private readonly _strategy: GoogleStrategy; constructor(options: OAuthProviderOptions) { @@ -162,7 +162,7 @@ export const createGoogleProvider: AuthProviderFactory = ({ callbackUrl, }); - return OAuthProvider.fromConfig(globalConfig, provider, { + return OAuthAdapter.fromConfig(globalConfig, provider, { disableRefresh: false, providerId, tokenIssuer, diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index 97e7dc3781..edc5509d84 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -30,9 +30,9 @@ import { import { RedirectInfo, AuthProviderFactory } from '../types'; import { - OAuthProvider, + OAuthAdapter, OAuthProviderOptions, - OAuthProviderHandlers, + OAuthHandlers, OAuthResponse, OAuthEnvironmentHandler, } from '../../lib/oauth'; @@ -48,7 +48,7 @@ export type MicrosoftAuthProviderOptions = OAuthProviderOptions & { tokenUrl?: string; }; -export class MicrosoftAuthProvider implements OAuthProviderHandlers { +export class MicrosoftAuthProvider implements OAuthHandlers { private readonly _strategy: MicrosoftStrategy; static transformAuthResponse( @@ -226,7 +226,7 @@ export const createMicrosoftProvider: AuthProviderFactory = ({ tokenUrl, }); - return OAuthProvider.fromConfig(globalConfig, provider, { + return OAuthAdapter.fromConfig(globalConfig, provider, { disableRefresh: false, providerId, tokenIssuer, diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index fea7d58e5c..5a4882fa6f 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -18,9 +18,9 @@ import express from 'express'; import passport from 'passport'; import { Strategy as OAuth2Strategy } from 'passport-oauth2'; import { - OAuthProvider, + OAuthAdapter, OAuthProviderOptions, - OAuthProviderHandlers, + OAuthHandlers, OAuthResponse, OAuthEnvironmentHandler, } from '../../lib/oauth'; @@ -43,7 +43,7 @@ export type OAuth2AuthProviderOptions = OAuthProviderOptions & { tokenUrl: string; }; -export class OAuth2AuthProvider implements OAuthProviderHandlers { +export class OAuth2AuthProvider implements OAuthHandlers { private readonly _strategy: OAuth2Strategy; constructor(options: OAuth2AuthProviderOptions) { @@ -177,7 +177,7 @@ export const createOAuth2Provider: AuthProviderFactory = ({ tokenUrl, }); - return OAuthProvider.fromConfig(globalConfig, provider, { + return OAuthAdapter.fromConfig(globalConfig, provider, { disableRefresh: false, providerId, tokenIssuer, diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index 3d33868e84..0368bd8415 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -15,9 +15,9 @@ */ import express from 'express'; import { - OAuthProvider, + OAuthAdapter, OAuthProviderOptions, - OAuthProviderHandlers, + OAuthHandlers, OAuthResponse, OAuthEnvironmentHandler, } from '../../lib/oauth'; @@ -42,7 +42,7 @@ export type OktaAuthProviderOptions = OAuthProviderOptions & { audience: string; }; -export class OktaAuthProvider implements OAuthProviderHandlers { +export class OktaAuthProvider implements OAuthHandlers { private readonly _strategy: any; /** @@ -186,7 +186,7 @@ export const createOktaProvider: AuthProviderFactory = ({ callbackUrl, }); - return OAuthProvider.fromConfig(globalConfig, provider, { + return OAuthAdapter.fromConfig(globalConfig, provider, { disableRefresh: false, providerId, tokenIssuer, From 39934f43b68bd6da3144d7b85a6636758ce2845a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 3 Sep 2020 15:11:17 +0200 Subject: [PATCH 23/34] auth-backend: add not found error if provider is not found --- plugins/auth-backend/src/service/router.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index b5451cad6c..19a74d47c5 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -22,6 +22,7 @@ import { Logger } from 'winston'; import { createAuthProviderRouter } from '../providers'; import { Config } from '@backstage/config'; import { DatabaseKeyStore, TokenFactory, createOidcRouter } from '../identity'; +import { NotFoundError } from '@backstage/backend-common'; export interface RouterOptions { logger: Logger; @@ -88,5 +89,10 @@ export async function createRouter( }), ); + router.use('/:provider/', req => { + const { provider } = req.params; + throw new NotFoundError(`No auth provider registered for '${provider}'`); + }); + return router; } From ee5d500624969f4669f4a3fddeba61fee8cf9869 Mon Sep 17 00:00:00 2001 From: Omer Farooq Date: Fri, 4 Sep 2020 02:03:02 +1200 Subject: [PATCH 24/34] fix azure ingestion default branch browser url --- .../AzureApiReaderProcessor.test.ts | 10 ++++++++- .../processors/AzureApiReaderProcessor.ts | 22 ++++++++++--------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.test.ts index 62259f87a2..9b234e3e75 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.test.ts @@ -41,7 +41,15 @@ describe('AzureApiReaderProcessor', () => { target: 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml&version=GBmaster', url: new URL( - 'https://dev.azure.com/org-name/project-name/_apis/sourceProviders/TfsGit/filecontents?repository=repo-name&commitOrBranch=master&path=my-template.yaml&api-version=6.0-preview.1', + 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml&version=master', + ), + err: undefined, + }, + { + target: + 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml', + url: new URL( + 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml', ), err: undefined, }, diff --git a/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.ts index 7033521da6..03ff30ea69 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.ts @@ -83,7 +83,7 @@ export class AzureApiReaderProcessor implements LocationProcessor { // Converts // from: https://dev.azure.com/{organization}/{project}/_git/reponame?path={path}&version=GB{commitOrBranch}&_a=contents - // to: https://dev.azure.com/{organization}/{project}/_apis/sourceProviders/{providerName}/filecontents?repository={repository}&commitOrBranch={commitOrBranch}&path={path}&api-version=6.0-preview.1 + // to: https://dev.azure.com/{organization}/{project}/_apis/git/repositories/reponame/items?path={path}&version={commitOrBranch} buildRawUrl(target: string): URL { try { const url = new URL(target); @@ -119,17 +119,19 @@ export class AzureApiReaderProcessor implements LocationProcessor { userOrOrg, project, '_apis', - 'sourceProviders', - 'TfsGit', - 'filecontents', + 'git', + 'repositories', + repoName, + 'items', ].join('/'); - url.search = [ - `repository=${repoName}`, - `commitOrBranch=${ref}`, - `path=${path}`, - 'api-version=6.0-preview.1', - ].join('&'); + const queryParams = [`path=${path}`]; + + if (ref) { + queryParams.push(`version=${ref}`); + } + + url.search = queryParams.join('&'); url.protocol = 'https'; From 5a772948298dc3858b4964bc688487697e219a90 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 3 Sep 2020 17:48:10 +0200 Subject: [PATCH 25/34] docs/auth: minor text fixes --- docs/auth/add-auth-provider.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/auth/add-auth-provider.md b/docs/auth/add-auth-provider.md index 55ef554c05..7e4f5c8e2f 100644 --- a/docs/auth/add-auth-provider.md +++ b/docs/auth/add-auth-provider.md @@ -118,7 +118,7 @@ export class ProviderAAuthProvider implements AuthProviderRouteHandlers { } ``` -#### Factory method +#### Factory function Each provider exports a factory function that instantiates the provider. The factory should implement [AuthProviderFactory](#AuthProviderFactory), which @@ -159,8 +159,8 @@ export const createOktaProvider: AuthProviderFactory = ({ }); ``` -The purpose of the different environment is to allow for a single auth-backend -to service as the authentication service for multiple different frontend +The purpose of the different environments is to allow for a single auth-backend +to serve as the authentication service for multiple different frontend environments, such as local development, staging, and production. The factory function for other providers can be a lot simpler, as they might not @@ -191,7 +191,7 @@ export const createProviderAProvider: AuthProviderFactory = ({ config }) => { > http://www.passportjs.org/docs/configure/ **`plugins/auth-backend/src/providers/providerA/index.ts`** is simply -re-exporting the create method to be used for hooking the provider up to the +re-exporting the factory function to be used for hooking the provider up to the backend. ```ts @@ -202,8 +202,8 @@ export { createProviderAProvider } from './provider'; **`plugins/auth-backend/src/providers/factories.ts`** When the `auth-backend` starts it sets up routing for all the available providers by calling -`createAuthProviderRouter` on each provider. You need to import the create -method from the provider and add it to the factory: +`createAuthProviderRouter` on each provider. You need to import the factory +function from the provider and add it to the factory: ```ts import { createProviderAProvider } from './providerA'; From 3f6e2591d605d5ca1075cb8d0effb9c80d6e4d95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Fri, 4 Sep 2020 09:52:36 +0200 Subject: [PATCH 26/34] Use primary color for checkbox --- plugins/catalog/src/components/ResultsFilter/ResultsFilter.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/catalog/src/components/ResultsFilter/ResultsFilter.tsx b/plugins/catalog/src/components/ResultsFilter/ResultsFilter.tsx index eb500da651..8c5737b7b6 100644 --- a/plugins/catalog/src/components/ResultsFilter/ResultsFilter.tsx +++ b/plugins/catalog/src/components/ResultsFilter/ResultsFilter.tsx @@ -104,6 +104,7 @@ export const ResultsFilter = ({ availableTags }: Props) => { > Date: Fri, 4 Sep 2020 09:59:14 +0200 Subject: [PATCH 27/34] TechDocs & Catalog: Private Github repository support (#2247) * Add private repo functionality to GhReaderProcesor * Add github helper for private github repo * Edit test * lint * Remove debugging console.log() * Less specific if statement * shorten get private token * Actual shortening * Change how current branch name is retrieved --- .../processors/GithubReaderProcessor.ts | 28 ++++++++++++- .../techdocs/stages/prepare/github.test.ts | 8 ++-- .../src/techdocs/stages/prepare/github.ts | 4 +- .../src/techdocs/stages/prepare/helpers.ts | 42 +++++++++++++++++++ 4 files changed, 75 insertions(+), 7 deletions(-) diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GithubReaderProcessor.ts index b83c9a16f3..9f38d782fe 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GithubReaderProcessor.ts @@ -15,11 +15,35 @@ */ import { LocationSpec } from '@backstage/catalog-model'; -import fetch from 'node-fetch'; +import fetch, { RequestInit, HeadersInit } from 'node-fetch'; import * as result from './results'; import { LocationProcessor, LocationProcessorEmit } from './types'; +import { Config } from '@backstage/config'; export class GithubReaderProcessor implements LocationProcessor { + private privateToken: string; + + constructor(config?: Config) { + this.privateToken = + config?.getOptionalString('catalog.processors.github.privateToken') ?? ''; + } + + getRequestOptions(): RequestInit { + const headers: HeadersInit = { + Accept: 'application/vnd.github.v3.raw', + }; + + if (this.privateToken !== '') { + headers.Authorization = `token ${this.privateToken}`; + } + + const requestOptions: RequestInit = { + headers, + }; + + return requestOptions; + } + async readLocation( location: LocationSpec, optional: boolean, @@ -34,7 +58,7 @@ export class GithubReaderProcessor implements LocationProcessor { // TODO(freben): Should "hard" errors thrown by this line be treated as // notFound instead of fatal? - const response = await fetch(url.toString()); + const response = await fetch(url.toString(), this.getRequestOptions()); if (response.ok) { const data = await response.buffer(); diff --git a/plugins/techdocs-backend/src/techdocs/stages/prepare/github.test.ts b/plugins/techdocs-backend/src/techdocs/stages/prepare/github.test.ts index 0d29d9c7be..99a7d3bb5c 100644 --- a/plugins/techdocs-backend/src/techdocs/stages/prepare/github.test.ts +++ b/plugins/techdocs-backend/src/techdocs/stages/prepare/github.test.ts @@ -16,11 +16,13 @@ import { getVoidLogger } from '@backstage/backend-common'; import { GithubPreparer } from './github'; -import { checkoutGitRepository } from './helpers'; +import { checkoutGithubRepository } from './helpers'; jest.mock('./helpers', () => ({ ...jest.requireActual<{}>('./helpers'), - checkoutGitRepository: jest.fn(() => '/tmp/backstage-repo/org/name/branch'), + checkoutGithubRepository: jest.fn( + () => '/tmp/backstage-repo/org/name/branch', + ), })); const createMockEntity = (annotations = {}) => { @@ -48,7 +50,7 @@ describe('github preparer', () => { }); const tempDocsPath = await preparer.prepare(mockEntity); - expect(checkoutGitRepository).toHaveBeenCalledTimes(1); + expect(checkoutGithubRepository).toHaveBeenCalledTimes(1); expect(tempDocsPath).toEqual( '/tmp/backstage-repo/org/name/branch/plugins/techdocs-backend/examples/documented-component', ); diff --git a/plugins/techdocs-backend/src/techdocs/stages/prepare/github.ts b/plugins/techdocs-backend/src/techdocs/stages/prepare/github.ts index 65c3e551cb..242b19c24d 100644 --- a/plugins/techdocs-backend/src/techdocs/stages/prepare/github.ts +++ b/plugins/techdocs-backend/src/techdocs/stages/prepare/github.ts @@ -18,7 +18,7 @@ import { Entity } from '@backstage/catalog-model'; import { InputError } from '@backstage/backend-common'; import { PreparerBase } from './types'; import parseGitUrl from 'git-url-parse'; -import { parseReferenceAnnotation, checkoutGitRepository } from './helpers'; +import { parseReferenceAnnotation, checkoutGithubRepository } from './helpers'; import { Logger } from 'winston'; export class GithubPreparer implements PreparerBase { @@ -39,7 +39,7 @@ export class GithubPreparer implements PreparerBase { } try { - const repoPath = await checkoutGitRepository(target); + const repoPath = await checkoutGithubRepository(target); const parsedGitLocation = parseGitUrl(target); return path.join(repoPath, parsedGitLocation.filepath); diff --git a/plugins/techdocs-backend/src/techdocs/stages/prepare/helpers.ts b/plugins/techdocs-backend/src/techdocs/stages/prepare/helpers.ts index 27e746590a..d962242262 100644 --- a/plugins/techdocs-backend/src/techdocs/stages/prepare/helpers.ts +++ b/plugins/techdocs-backend/src/techdocs/stages/prepare/helpers.ts @@ -94,3 +94,45 @@ export const checkoutGitRepository = async ( return repositoryTmpPath; }; + +// Could be merged with checkoutGitRepository +export const checkoutGithubRepository = async ( + repoUrl: string, +): Promise => { + const parsedGitLocation = parseGitUrl(repoUrl); + + // Should propably not be hardcoded names of env variables, but seems too hard to access config down here + const user = process.env.GITHUB_PRIVATE_TOKEN_USER || ''; + const token = process.env.GITHUB_PRIVATE_TOKEN || ''; + + const repositoryTmpPath = path.join( + // fs.realpathSync fixes a problem with macOS returning a path that is a symlink + fs.realpathSync(os.tmpdir()), + 'backstage-repo', + parsedGitLocation.source, + parsedGitLocation.owner, + parsedGitLocation.name, + parsedGitLocation.ref, + ); + + if (fs.existsSync(repositoryTmpPath)) { + const repository = await Repository.open(repositoryTmpPath); + const currentBranchName = (await repository.getCurrentBranch()).shorthand(); + await repository.mergeBranches( + currentBranchName, + `origin/${currentBranchName}`, + ); + return repositoryTmpPath; + } + + if (user && token) { + parsedGitLocation.token = `${user}:${token}`; + } + + const repositoryCheckoutUrl = parsedGitLocation.toString('https'); + + fs.mkdirSync(repositoryTmpPath, { recursive: true }); + await Clone.clone(repositoryCheckoutUrl, repositoryTmpPath); + + return repositoryTmpPath; +}; From 69c2fc72cf3f8cbb5e0b1aefc0fde86efc636605 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Fri, 4 Sep 2020 10:03:48 +0200 Subject: [PATCH 28/34] Bumped version to alpha21 --- packages/app/package.json | 1 + plugins/gcp-projects/package.json | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index 1affbe4d09..77f5ab4432 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -11,6 +11,7 @@ "@backstage/plugin-catalog": "^0.1.1-alpha.21", "@backstage/plugin-circleci": "^0.1.1-alpha.21", "@backstage/plugin-explore": "^0.1.1-alpha.21", + "@backstage/plugin-gcp-projects": "^0.1.1-alpha.21", "@backstage/plugin-github-actions": "^0.1.1-alpha.21", "@backstage/plugin-gitops-profiles": "^0.1.1-alpha.21", "@backstage/plugin-graphiql": "^0.1.1-alpha.21", diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index 8a19584a52..55ed362876 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-gcp-projects", - "version": "0.1.1-alpha.20", + "version": "0.1.1-alpha.21", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.20", - "@backstage/theme": "^0.1.1-alpha.20", + "@backstage/core": "^0.1.1-alpha.21", + "@backstage/theme": "^0.1.1-alpha.21", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -32,8 +32,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.20", - "@backstage/dev-utils": "^0.1.1-alpha.20", + "@backstage/cli": "^0.1.1-alpha.21", + "@backstage/dev-utils": "^0.1.1-alpha.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", From c73618c4c1e4caede04f3f7663f76939385bda98 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Fri, 4 Sep 2020 10:17:56 +0200 Subject: [PATCH 29/34] Fixed old merge conflict in api.ts and cange to Table element in ProjectDetailsPage.tsx --- packages/app/src/apis.ts | 2 -- .../ProjectDetailsPage/ProjectDetailsPage.tsx | 4 ++-- yarn.lock | 24 ++----------------- 3 files changed, 4 insertions(+), 26 deletions(-) diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index ace4ea16a2..b391e95d62 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -84,7 +84,6 @@ import { githubPullRequestsApiRef, } from '@roadiehq/backstage-plugin-github-pull-requests'; - export const apis = (config: ConfigApi) => { // eslint-disable-next-line no-console console.log(`Creating APIs for ${config.getString('app.title')}`); @@ -105,7 +104,6 @@ export const apis = (config: ConfigApi) => { ); builder.add(storageApiRef, WebStorage.create({ errorApi })); - builder.add(circleCIApiRef, new CircleCIApi()); builder.add(GCPApiRef, new GCPClient()); builder.add( circleCIApiRef, diff --git a/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx b/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx index 713918d8a2..57f1806973 100644 --- a/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx +++ b/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx @@ -86,7 +86,7 @@ const DetailsPage = () => { const details = status.value; return ( - +
@@ -144,7 +144,7 @@ const DetailsPage = () => {
-
+ ); }; diff --git a/yarn.lock b/yarn.lock index 2840e9d7a5..9175c61e60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18844,7 +18844,7 @@ react-transition-group@^4.0.0, react-transition-group@^4.3.0: loose-envify "^1.4.0" prop-types "^15.6.2" -react-universal-interface@^0.6.0, react-universal-interface@^0.6.2: +react-universal-interface@^0.6.2: version "0.6.2" resolved "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz#5e8d438a01729a4dbbcbeeceb0b86be146fe2b3b" integrity sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw== @@ -18866,26 +18866,6 @@ react-use@^12.2.0: ts-easing "^0.2.0" tslib "^1.10.0" -react-use@^14.2.0: - version "14.3.0" - resolved "https://registry.npmjs.org/react-use/-/react-use-14.3.0.tgz#aa794db42108e15363be5c04db35a57acf8ecb6b" - integrity sha512-Jx7Zl0k8dHA0UKpTVwYUThC5/V+Dt6JzCGiMHPNIhsxJGkiKuB1AQ7J7pNq4zj3l37ABd/RF+jRGThw0czrJXA== - dependencies: - "@types/js-cookie" "2.2.6" - "@xobotyi/scrollbar-width" "1.9.5" - copy-to-clipboard "^3.2.0" - fast-deep-equal "^3.1.1" - fast-shallow-equal "^1.0.0" - js-cookie "^2.2.1" - nano-css "^5.2.1" - react-universal-interface "^0.6.0" - resize-observer-polyfill "^1.5.1" - screenfull "^5.0.0" - set-harmonic-interval "^1.0.1" - throttle-debounce "^2.1.0" - ts-easing "^0.2.0" - tslib "^1.10.0" - react-use@^15.3.3: version "15.3.3" resolved "https://registry.npmjs.org/react-use/-/react-use-15.3.3.tgz#f16de7a16286c446388e8bd99680952fc3dc9a95" @@ -21882,7 +21862,7 @@ ts-loader@^7.0.4: micromatch "^4.0.0" semver "^6.0.0" -ts-node@^8.10.2, ts-node@^8.6.2: +ts-node@^8.6.2: version "8.10.2" resolved "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz#eee03764633b1234ddd37f8db9ec10b75ec7fb8d" integrity sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA== From c9558f65701157a04e812510f6eaa6270d297347 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Fri, 4 Sep 2020 10:23:51 +0200 Subject: [PATCH 30/34] Removed unused import for TableContainer --- .../src/components/ProjectDetailsPage/ProjectDetailsPage.tsx | 1 - .../src/components/ProjectListPage/ProjectListPage.tsx | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx b/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx index 57f1806973..41d0b11019 100644 --- a/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx +++ b/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx @@ -23,7 +23,6 @@ import { Table, TableBody, TableCell, - TableContainer, TableRow, Theme, Typography, diff --git a/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx b/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx index 8d612f75f8..7e09a307c9 100644 --- a/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx +++ b/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx @@ -34,7 +34,6 @@ import { Table, TableBody, TableCell, - TableContainer, TableHead, TableRow, Tooltip, @@ -89,7 +88,7 @@ const PageContents = () => { } return ( - +
@@ -141,7 +140,7 @@ const PageContents = () => { ))}
-
+ ); }; From 08374355317d114e0ac182a3676d8ba695490404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Marschollek?= Date: Thu, 3 Sep 2020 09:20:04 +0200 Subject: [PATCH 31/34] Conditionally add Sentry widget to component page This adds the Sentry widget back to the component page (from which it was removed in #2246) if the component declares its own Sentry project ID in the annotation named backstage.io/sentry-project-id. The widget is hidden otherwise. Previous to #2246, the widget was just displaying an empty list because it was trying to load data for a hardcoded project name of sample-sentry-project-id. Fixes #2257 --- plugins/catalog/package.json | 1 + .../EntityPageOverview/EntityPageOverview.tsx | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index b088e1699b..e393be9d17 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -27,6 +27,7 @@ "@backstage/plugin-github-actions": "^0.1.1-alpha.21", "@backstage/plugin-jenkins": "^0.1.1-alpha.21", "@backstage/plugin-scaffolder": "^0.1.1-alpha.21", + "@backstage/plugin-sentry": "^0.1.1-alpha.21", "@backstage/plugin-techdocs": "^0.1.1-alpha.21", "@backstage/theme": "^0.1.1-alpha.21", "@material-ui/core": "^4.9.1", diff --git a/plugins/catalog/src/components/EntityPageOverview/EntityPageOverview.tsx b/plugins/catalog/src/components/EntityPageOverview/EntityPageOverview.tsx index 9b29b205cf..f261698f7d 100644 --- a/plugins/catalog/src/components/EntityPageOverview/EntityPageOverview.tsx +++ b/plugins/catalog/src/components/EntityPageOverview/EntityPageOverview.tsx @@ -17,6 +17,7 @@ // TODO(shmidt-i): move to the app import { Entity } from '@backstage/catalog-model'; import { Content } from '@backstage/core'; +import { SentryIssuesWidget } from '@backstage/plugin-sentry'; import { LatestWorkflowRunCard } from '@backstage/plugin-github-actions'; import { JenkinsBuildsWidget, @@ -52,6 +53,16 @@ export const EntityPageOverview: FC<{ entity: Entity }> = ({ entity }) => { )} + {entity.metadata?.annotations?.['backstage.io/sentry-project-id'] && ( + + + + )} ); From eb4d54fcc4a2f756a12c56ee36fa4e71b132d6f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Marschollek?= Date: Fri, 4 Sep 2020 12:27:54 +0200 Subject: [PATCH 32/34] Adapt app-driven entity page Addresses the changes required after merging #2076. The Sentry plugin now defines a router and the widget will be displayed as a tab. --- .../app/src/components/catalog/EntityPage.tsx | 11 +++++ .../EntityPageOverview/EntityPageOverview.tsx | 11 ----- plugins/sentry/package.json | 2 + plugins/sentry/src/components/Router.tsx | 49 +++++++++++++++++++ plugins/sentry/src/index.ts | 1 + 5 files changed, 63 insertions(+), 11 deletions(-) create mode 100644 plugins/sentry/src/components/Router.tsx diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 879e2fe6f7..cc1ae8bedc 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -14,6 +14,7 @@ * limitations under the License. */ import { Router as GitHubActionsRouter } from '@backstage/plugin-github-actions'; +import { Router as SentryRouter } from '@backstage/plugin-sentry'; import React from 'react'; import { EntityPageLayout, @@ -43,6 +44,11 @@ const ServiceEntityPage = ({ entity }: { entity: Entity }) => ( title="CI/CD" element={} /> + } + /> ); @@ -58,6 +64,11 @@ const WebsiteEntityPage = ({ entity }: { entity: Entity }) => ( title="CI/CD" element={} /> + } + /> ); diff --git a/plugins/catalog/src/components/EntityPageOverview/EntityPageOverview.tsx b/plugins/catalog/src/components/EntityPageOverview/EntityPageOverview.tsx index f261698f7d..9b29b205cf 100644 --- a/plugins/catalog/src/components/EntityPageOverview/EntityPageOverview.tsx +++ b/plugins/catalog/src/components/EntityPageOverview/EntityPageOverview.tsx @@ -17,7 +17,6 @@ // TODO(shmidt-i): move to the app import { Entity } from '@backstage/catalog-model'; import { Content } from '@backstage/core'; -import { SentryIssuesWidget } from '@backstage/plugin-sentry'; import { LatestWorkflowRunCard } from '@backstage/plugin-github-actions'; import { JenkinsBuildsWidget, @@ -53,16 +52,6 @@ export const EntityPageOverview: FC<{ entity: Entity }> = ({ entity }) => { )} - {entity.metadata?.annotations?.['backstage.io/sentry-project-id'] && ( - - - - )} ); diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index d5b29cc307..294d7159bf 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -21,6 +21,7 @@ "clean": "backstage-cli clean" }, "dependencies": { + "@backstage/catalog-model": "^0.1.1-alpha.21", "@backstage/core": "^0.1.1-alpha.21", "@backstage/theme": "^0.1.1-alpha.21", "@material-ui/core": "^4.9.1", @@ -29,6 +30,7 @@ "@types/react": "^16.9", "react": "^16.13.1", "react-dom": "^16.13.1", + "react-router": "6.0.0-beta.0", "react-sparklines": "^1.7.0", "react-use": "^15.3.3", "timeago.js": "^4.0.2" diff --git a/plugins/sentry/src/components/Router.tsx b/plugins/sentry/src/components/Router.tsx new file mode 100644 index 0000000000..7c0468fda2 --- /dev/null +++ b/plugins/sentry/src/components/Router.tsx @@ -0,0 +1,49 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 React from 'react'; +import { Entity } from '@backstage/catalog-model'; +import { Routes, Route } from 'react-router'; +import { WarningPanel } from '@backstage/core'; +import { SentryPluginWidget } from './SentryPluginWidget/SentryPluginWidget'; + +const SENTRY_ANNOTATION = 'sentry.io/project-id'; + +const isPluginApplicableToEntity = (entity: Entity) => + Boolean(entity.metadata.annotations?.[SENTRY_ANNOTATION]) && + entity.metadata.annotations?.[SENTRY_ANNOTATION] !== ''; + +export const Router = ({ entity }: { entity: Entity }) => + !isPluginApplicableToEntity(entity) ? ( + + `entity.metadata.annotations[' + {SENTRY_ANNOTATION}']` key is missing on the entity.{' '} + + ) : ( + + + } + /> + ) + + ); diff --git a/plugins/sentry/src/index.ts b/plugins/sentry/src/index.ts index 50da801a40..a0d3cab1be 100644 --- a/plugins/sentry/src/index.ts +++ b/plugins/sentry/src/index.ts @@ -15,4 +15,5 @@ */ export { plugin } from './plugin'; +export { Router } from './components/Router'; export { SentryPluginWidget as SentryIssuesWidget } from './components/SentryPluginWidget/SentryPluginWidget'; From bb7430dababac68b9f4989aa5933b2157408f3d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Marschollek?= Date: Fri, 4 Sep 2020 13:56:42 +0200 Subject: [PATCH 33/34] Refactor the Sentry plugin router --- plugins/sentry/src/components/Router.tsx | 31 +++++++++++------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/plugins/sentry/src/components/Router.tsx b/plugins/sentry/src/components/Router.tsx index 7c0468fda2..47d532b4e3 100644 --- a/plugins/sentry/src/components/Router.tsx +++ b/plugins/sentry/src/components/Router.tsx @@ -19,31 +19,28 @@ import { Routes, Route } from 'react-router'; import { WarningPanel } from '@backstage/core'; import { SentryPluginWidget } from './SentryPluginWidget/SentryPluginWidget'; -const SENTRY_ANNOTATION = 'sentry.io/project-id'; +const SENTRY_ANNOTATION = 'sentry.io/project-slug'; -const isPluginApplicableToEntity = (entity: Entity) => - Boolean(entity.metadata.annotations?.[SENTRY_ANNOTATION]) && - entity.metadata.annotations?.[SENTRY_ANNOTATION] !== ''; +export const Router = ({ entity }: { entity: Entity }) => { + const projectId = entity.metadata.annotations?.[SENTRY_ANNOTATION]; -export const Router = ({ entity }: { entity: Entity }) => - !isPluginApplicableToEntity(entity) ? ( - - `entity.metadata.annotations[' - {SENTRY_ANNOTATION}']` key is missing on the entity.{' '} - - ) : ( + if (!projectId) { + return ( + +
{SENTRY_ANNOTATION}
annotation is missing on the entity. +
+ ); + } + + return ( + } /> ) ); +}; From 0f74167e824cfe83f9c3f3d11c352e158e00876a Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Fri, 4 Sep 2020 14:17:01 +0200 Subject: [PATCH 34/34] Example Plugin with Grid for Storybook (#2271) * Add an example plugin with a grid of information and data * Adjust types & imports * Abstract Header & ContentHeader for both plugin examples * Fix linting by removing unused import * Remove React.FC --- .../core/src/layout/Page/Page.stories.tsx | 149 ++++++++++++++++-- 1 file changed, 133 insertions(+), 16 deletions(-) diff --git a/packages/core/src/layout/Page/Page.stories.tsx b/packages/core/src/layout/Page/Page.stories.tsx index 786b4740fd..aec99967dc 100644 --- a/packages/core/src/layout/Page/Page.stories.tsx +++ b/packages/core/src/layout/Page/Page.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React from 'react'; +import React, { useState } from 'react'; import { Header, Page, @@ -22,9 +22,18 @@ import { ContentHeader, Content, pageTheme, + InfoCard, + HeaderTabs, } from '../'; -import { SupportButton, Table, StatusOK, TableColumn } from '../../components'; -import { Box, Typography, Link, Chip, Button } from '@material-ui/core'; +import { + SupportButton, + Table, + StatusOK, + TableColumn, + ProgressCard, + TrendLine, +} from '../../components'; +import { Box, Typography, Link, Chip, Grid } from '@material-ui/core'; export default { title: 'Example Plugin', @@ -85,23 +94,131 @@ const columns: TableColumn[] = [ }, ]; +const tabs = [ + { label: 'Overview' }, + { label: 'CI/CD' }, + { label: 'Cost Efficency' }, + { label: 'Code Coverage' }, + { label: 'Test' }, + { label: 'Compliance Advisor' }, +]; + +const DataGrid = () => ( + + + + + + + + + + + + + + + + + + + Rightsize GKE deployment +

+ Services are considered underutilized in GKE when the average usage of + requested cores is less than 80%. +

+ What can I do? +

+ Review requested core and limit settings. Check HPA target scaling + settings in hpa.yaml. The recommended value for + targetCPUUtilizationPercentage is 80. +

+

+ For single pods, there is of course no HPA. But it can also be useful + to think about a single pod out of a larger deployment, then modify + based on HPA requirements. Within a pod, each container has its own + CPU and memory requests and limits. +

+ Definitions +

+ A request is a minimum reserved value; a container will never have + less than this amount allocated to it, even if it doesn't actually use + it. Requests are used for determining what nodes to schedule pods on + (bin-packing). The tension here is between not allocating resources we + don't need, and having easy-enough access to enough resources to be + able to function. +

+ + Contact #cost-awareness for information and support. + +
+
+
+); + +const ExampleHeader = () => ( +
+ + +
+); + +const ExampleContentHeader = ({ selectedTab }: { selectedTab?: number }) => ( + + + This Plugin is an example. This text could provide usefull information for + the user. + + +); + +export const PluginWithData = () => { + const [selectedTab, setSelectedTab] = useState(2); + return ( + + + setSelectedTab(index)} + tabs={tabs.map(({ label }, index) => ({ + id: index.toString(), + label, + }))} + /> + + + + + + ); +}; + export const PluginWithTable = () => { return ( -
- - -
+ - - - - This Plugin is an example. This text could provide usefull - information for the user. - - +