diff --git a/Dockerfile b/Dockerfile index 86595ea5a2..fa89debcee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,8 @@ FROM nginx:mainline # The safest way to build this image is to use `yarn docker-build` +RUN apt-get update && apt-get -y install jq && rm -rf /var/lib/apt/lists/* + COPY packages/app/dist /usr/share/nginx/html COPY docker/default.conf.template /etc/nginx/conf.d/default.conf.template COPY docker/run.sh /usr/local/bin/run.sh diff --git a/docker/run.sh b/docker/run.sh index 7bc5f5fa39..ac120f6597 100755 --- a/docker/run.sh +++ b/docker/run.sh @@ -1,9 +1,41 @@ #!/usr/bin/env bash +set -Eeuo pipefail + # Run nginx as root sed -i 's/user nginx.*$//' /etc/nginx/nginx.conf # Write selected env vars to nginx config envsubst '$PORT' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf +# Inject runtime config into the client +function inject_config() { + # Read runtime config from env in the same way as the @backstage/config-loader package + local config + config="$(jq -n 'env | + with_entries(select(.key | startswith("APP_CONFIG_")) | .key |= sub("APP_CONFIG_"; "")) | + to_entries | + reduce .[] as $item ( + {}; setpath($item.key | split("_"); $item.value | fromjson) + )')" + + >&2 echo "Runtime app config: $config" + + local main_js + main_js="$(ls /usr/share/nginx/html/main.*.chunk.js)" + echo "Writing runtime config to ${main_js}" + + # escape ' and " twice, for both sed and json + local config_escaped_1 + config_escaped_1="$(echo "$config" | jq -cM . | sed -e 's/[\\"\x27]/\\&/g')" # \x27 = ' + # escape / and & for sed + local config_escaped_2 + config_escaped_2="$(echo "$config_escaped_1" | sed -e 's/[\/&]/\\&/g')" + + # Replace __APP_INJECTED_RUNTIME_CONFIG__ in the main chunk with the runtime config + sed -e "s/__APP_INJECTED_RUNTIME_CONFIG__/$config_escaped_2/" -i "$main_js" +} + +inject_config + exec nginx -g 'daemon off;' diff --git a/docs/FAQ.md b/docs/FAQ.md index f33b4b9dac..26f4fc4f45 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -1,52 +1,167 @@ # FAQ -## Do I have to write plugins in TypeScript? +## Product FAQ: +### Can we call Backstage something different? So that it fits our company better? + +Yes, Backstage is just a platform for building your own developer portal. We happen to call our internal version Backstage, as well, as a reference to our music roots. You can call your version whatever suits your team, company, or brand. + +### Is Backstage a monitoring platform? + +No, but it can be! Backstage is designed to be a developer portal for all your infrastructure tooling, services, and documentation. So, it's not a monitoring platform — but that doesn't mean you can't integrate a monitoring tool into Backstage by writing [a plugin](https://github.com/spotify/faq#what-is-a-plugin-in-backstage). + +### How is Backstage licensed? + +Backstage was released as free and open software by Spotify and is licensed under [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0). + +### Why did we open source Backstage? + +We hope to see Backstage become the infrastructure standard everywhere. When we saw how much Backstage improved developer experience and productivity internally, we wanted to share those gains. After all, if Backstage can create order in an engineering environment as open and diverse as ours, then we're pretty sure it can create order (and boost productivity) anywhere. To learn more, read our blog post, "[What the heck is Backstage anyway?](https://backstage.io/blog/2020/03/18/what-is-backstage)" + +### Will Spotify's internal plugins be open sourced, too? + +Yes, we've already started releasing open source versions of some of the plugins we use here, and we'll continue to do so. [Plugins](https://github.com/spotify/faq#what-is-a-plugin-in-backstage) are the building blocks of functionality in Backstage. We have over 120 plugins inside Spotify — many of those are specialized for our use, so will remain internal and proprietary to us. But we estimate that about a third of our existing plugins make good open source candidates. (And we'll probably end up writing some brand new ones, too.) +​ + +### What's the roadmap for Backstage? + +​ +We envision three phases, which you can learn about in [our project roadmap](https://github.com/spotify/backstage#project-roadmap). Even though the open source version of Backstage is relatively new compared to our internal version, we have already begun work on various aspects of all three phases. Looking at the [milestones for active issues](https://github.com/spotify/backstage/milestones) will also give you a sense of our progress. + +### My company doesn't have thousands of developers or services. Is Backstage overkill? + +Not at all! A core reason to adopt Backstage is to standardize how software is built at your company. It's easier to decide on those standards as a small company, and grows in importance as the company grows. Backstage sets a foundation, and an early investment in your infrastructure becomes even more valuable as you grow. + +### Our company has a strong design language system/brand that we want to incorporate. Does Backstage support this? + +Yes! The Backstage UI is built using Material-UI. With the theming capabilities of Material-UI, you are able to adapt the interface to your brand guidelines. + +## Technical FAQ: + +### Why Material-UI? + +The short answer is that's what we've been using in Backstage internally. + +The original decision was based on Google's Material Design being a thorough, well thought out and complete design system, with many mature and powerful libraries implemented in both the system itself and auxiliary components that we knew that we would like to use. + +It strikes a good balance between power, customizability, and ease of use. A core focus of Backstage is to make plugin developers productive with as few hurdles as possible. Material-UI lets plugin makers get going easily with both well-known tech and a large flora of components. +​ + +### What technology does Backstage use? + +​ +The code base is a large-scale React application that uses TypeScript. For [Phase 2](https://github.com/spotify/backstage#project-roadmap), we plan to use Node.js and GraphQL. +​ + +### What is the end-to-end user flow? The happy path story. + +​ +There are three main user profiles for Backstage: the integrator, the contributor, and the software engineer. +​ +The **integrator** hosts the Backstage app and configures which plugins are available to use in the app. +​ +The **contributor** adds functionality to the app by writing plugins. +​ +The **software engineer** uses the app's functionality and interacts with its plugins. +​ + +### What is a "plugin" in Backstage? + +​ +Plugins are what provide the feature functionality in Backstage. They are used to integrate different systems into Backstage's frontend, so that the developer gets a consistent UX, no matter what tool or service is being accessed on the other side. +​ +Each plugin is treated as a self-contained web app and can include almost any type of content. Plugins all use a common set of platform APIs and reusable UI components. Plugins can fetch data either from the backend or an API exposed through the proxy. +​ +Learn more about [the different components](https://github.com/spotify/backstage#overview) that make up Backstage. +​ + +### Do I have to write plugins in TypeScript? + +​ No, you can use JavaScript if you prefer. +​ +We want to keep the Backstage core APIs in TypeScript, but aren't forcing it on individual plugins. +​ -We want to keep the Backstage core APIs in TypeScript, but don't force it on individual plugins. +### How do I find out if a plugin already exists? -## Q: Why Material-UI? +​ +Before you write a plugin, [search the plugin issues](https://github.com/spotify/backstage/issues?q=is%3Aissue+label%3Aplugin+) to see if it already exists or is in the works. If no one's thought of it yet, great! Open a new issue as [a plugin suggestion](https://github.com/spotify/backstage/issues/new/choose) and describe what your plugin will do. This will help coordinate our contributors' efforts and avoid duplicating existing functionality. +​ +In the future, we will create [a plugin gallery](https://github.com/spotify/backstage/issues/260) where people can browse and search for all available plugins. +​ -The short answer is that it's what we've been using in Backstage internally. +### Which plugin is used the most at Spotify? -The original choice is based on Google's material design being a thorough and well -thought out full design system, with many mature and powerful libraries implementing -both the system itself and auxiliary components that we knew that we would like to use. +​ +By far, our most-used plugin is our TechDocs plugin, which we use for creating technical documentation. Our philosophy at Spotify is to treat "docs like code", where you write documentation using the same workflow as you write your code. This makes it easier to create, find, and update documentation. We hope to release [the open source version](https://github.com/spotify/backstage/issues/687) in the future. (See also: "[Will Spotify's internal plugins be open sourced, too?](https://github.com/spotify/faq#will-spotifys-internal-plugins-be-open-sourced-too)" above) +​ -It strikes a good balance between power, customisability, and ease of use. Since a core -focus of Backstage is to make plugin developers productive with as few hurdles as -possible, material-ui lets plugin makers both get going easily with well-known tech -and a large flora of components. +### Are you planning to have plugins baked into the repo? Or should they be developed in separate repos? -## Q: Are you planning on having plugins cooked into the repo or should they be developed in separate repos? +​ +Contributors can add open source plugins to the plugins directory in [this monorepo](https://github.com/spotify/backstage). Integrators can then configure which open source plugins are available to use in their instance of the app. Open source plugins are downloaded as npm packages published in the open source repository. +​ +While we encourage using the open source model, we know there are cases where contributors might want to experiment internally or keep their plugins closed source. Contributors writing closed source plugins should develop them in the plugins directory in their own Backstage repository. Integrators also configure closed source plugins locally from the monorepo. +​ -Additional open sourced plugins would be added to the `plugins` directory in this monorepo. +### Any plans for integrating with other repository managers, such as GitLab or Bitbucket? -While we encourage using the open source model, integrators that want to experiment with -Backstage internally may also choose to develop closed source plugins in a manner that suits -them best, for example in their respective Backstage source repository. +​ +We chose GitHub because it is the tool that we are most familiar with, so that will naturally lead to integrations for GitHub being developed at an early stage. +​ +Hosting this project on GitHub does not exclude integrations with alternatives, such as GitLab or Bitbucket. We believe that in time there will be plugins that will provide functionality for these tools as well. Hopefully, contributed by the community! +​ +Also note, implementations of Backstage can be hosted wherever you feel suits your needs best. +​ -## Q: Any plans for integrating with other repository managers such as Gitlab or Bitbucket? +### Who maintains Backstage? -We chose Github by the fact that it is the tool that we are most familiar with and that will naturally -lead to integrations for Github specifically being developed in an early stage. +​ +Spotify will maintain the open source core, but we envision different parts of the project being maintained by various companies and contributors. We also envision a large, diverse ecosystem of open source plugins, which would be maintained by their original authors/contributors or by the community. +​ +When it comes to [deployment](https://github.com/spotify/backstage/blob/master/DEPLOYMENT.md), the system integrator (typically, the infrastructure team in your organization) maintains Backstage in your own environment. +​ -Hosting this project on Github does not exclude integrations with other alternatives such as Gitlab or -Bitbucket. We believe that in time there will be plugins that will provide functionality for these tools -as well. Hopefully contributed by the community. +### Does Spotify provide a managed version of Backstage? -And note that implementations of Backstage can be hosted wherever you feel suits your needs best. +​ +No, this is not a service offering. We build the piece of software, and someone in your infrastructure team is responsible for [deploying](https://github.com/spotify/backstage/blob/master/DEPLOYMENT.md) and maintaining it. +​ -## Q: Can Backstage by used for other things than developer portals? +### How secure is Backstage? -Yes. +​ +We take security seriously. When it comes to packages and code we scan our repositories periodically and update our packages to the latest versions. When it comes to deployment of Backstage within an organisation it depends on the deployment and security setup in your organisation. Reach out to us on [Discord](https://discord.gg/MUpMjP2) if you have specific queries. -The core frontend framework could be used for building any large-scale web application where multiple teams are building separate parts of the app, but you want the overall experience to be consistent. +Please report sensitive security issues via Spotify's [bug-bounty program](https://hackerone.com/spotify) rather than GitHub. +​ +### Does Backstage collect any information that is shared with Spotify? + +​ +No. Backstage does not collect any telemetry from any third party using the platform. Spotify, and the open source community, does have access to [GitHub Insights](https://github.com/features/insights), which contains information such as contributors, commits, traffic, and dependencies. +​ +Backstage is an open platform, but you are in control of your own data. You control who has access to any data you provide to your version of Backstage and who that data is shared with. +​ + +### Can Backstage be used to build something other than a developer portal? + +​ +Yes. The core frontend framework could be used for building any large-scale web application where (1) multiple teams are building separate parts of the app, and (2) you want the overall experience to be consistent. +​ That being said, in [Phase 2](https://github.com/spotify/backstage#project-roadmap) of the project we will add features that are needed for developer portals and systems for managing software ecosystems. Our ambition will be to keep Backstage modular. +​ -## Q: My company doesn't have thousands of developers. Is Backstage overkill? +### How can I get involved? -Not really. Sure, having something like Backstage gets more important as the number of developers in your company grows. One of the core reasons to adopt Backstage is to help standardise how software is built at your company. Setting guidelines and deciding on standards is easier when your company is smaller. +​ +Jump right in! Come help us fix some of the [early bugs and first issues](https://github.com/spotify/backstage/labels/good%20first%20issue) or reach [a new milestone](https://github.com/spotify/backstage/milestones). Or write an open source plugin for Backstage, like this [Lighthouse plugin](https://github.com/spotify/backstage/tree/master/plugins/lighthouse). +​ +See all the ways you can [contribute here](https://github.com/spotify/backstage/blob/master/CONTRIBUTING.md). We'd love to have you as part of the community. +​ +### Can I join the Backstage team? + +​ +If you're interested in being part of the Backstage team, reach out to [fossopportunities@spotify.com](mailto:fossopportunities@spotify.com) diff --git a/docs/README.md b/docs/README.md index 28d1047942..89160ce9e5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,3 +7,4 @@ Check out or see the table of content below. - [References](reference/README.md) - [Publishing](publishing.md) - [Designing for Backstage](design.md) +- [How to add an auth provider](auth/add-auth-provider.md) diff --git a/docs/auth/add-auth-provider.md b/docs/auth/add-auth-provider.md new file mode 100644 index 0000000000..4bbbec88bb --- /dev/null +++ b/docs/auth/add-auth-provider.md @@ -0,0 +1,198 @@ +# Adding authentication providers + +## Passport + +We chose [Passport](http://www.passportjs.org/) as our authentication platform due to its comprehensive set of supported authentication [strategies](http://www.passportjs.org/packages/). + +## How to add a new strategy provider + +### Quick guide + +[1.](#installing-the-dependencies) Install the passport-js based provider package. + +[2.](#create-implementation) Create a new folder structure for the provider. + +[3.](#adding-an-oauth-based-provider) Implement the provider, extending the suitable framework if needed. + +[4.](#hook-it-up-to-the-backend) Add the provider to the backend. + +### Installing the dependencies: + +```bash +cd plugins/auth-backend +yarn add passport-provider-a +yarn add @types/passport-provider-a +``` + +### Create implementation + +Make a new folder with the name of the provider following the below file structure: + +```bash +plugins/auth-backend/src/providers/providerA +├── index.ts +└── provider.ts +``` + +**`plugins/auth-backend/src/providers/providerA/provider.ts`** defines the 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. + +The provider class takes the provider's configuration as a class parameter. It also imports the `Strategy` from the passport package. + +```ts +import { Strategy as ProviderAStrategy } from 'passport-provider-a'; + +export class ProviderAAuthProvider implements OAuthProviderHandlers { + private readonly providerConfig: AuthProviderConfig; + private readonly _strategy: ProviderAStrategy; + + constructor(providerConfig: AuthProviderConfig) { + this.providerConfig = providerConfig; + this._strategy = new ProviderAStrategy( + { ...providerConfig.options }, + verifyFunction, // See the "Verify Callback" section + ); + } + + async start() {} + async handler() {} +} +``` + +#### Adding an non-OAuth based provider + +_**Note**: We have prioritized OAuth-based providers and non-OAuth providers should be considered experimental._ + +An non-`OAuth` based provider could implement [AuthProviderRouteHandlers](#AuthProviderRouteHandlers) instead. + +```ts +export class ProviderAAuthProvider implements AuthProviderRouteHandlers { + private readonly providerConfig: AuthProviderConfig; + private readonly _strategy: ProviderAStrategy; + + constructor(providerConfig: AuthProviderConfig) { + this.providerConfig = providerConfig; + this._strategy = new ProviderAStrategy( + { ...providerConfig.options }, + verifyFunction, // See the "Verify Callback" section + ); + } + + async start() {} + async frameHandler() {} + async logout() {} + async refresh() {} // If supported +} +``` + +#### Create 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. + +Implementing OAuth by returning an instance of `OAuthProvider` based of the provider's class: + +```ts +export function createProviderAProvider(config: AuthProviderConfig) { + const provider = new ProviderAAuthProvider(config); + const oauthProvider = new OAuthProvider(provider, config.provider, true); + return oauthProvider; +} +``` + +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. + +```ts +export function createProviderAProvider(config: AuthProviderConfig) { + return new ProviderAAuthProvider(config); +} +``` + +#### Verify Callback + +> Strategies require what is known as a verify callback. The purpose of a verify callback is to find the user that possesses a set of credentials. +> When Passport authenticates a request, it parses the credentials contained in the request. It then invokes the verify callback with those credentials as arguments [...]. If the credentials are valid, the verify callback invokes done to supply Passport with the user that authenticated. +> +> If the credentials are not valid (for example, if the password is incorrect), done should be invoked with false instead of a user to indicate an authentication failure. +> +> 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 backend. + +```ts +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 method from the provider and add it to the factory: + +```ts +import { createProviderAProvider } from './providerA'; +const factories: { [providerId: string]: AuthProviderFactory } = { + providerA: createProviderAProvider, +}; +``` + +By doing this `auth-backend` automatically adds these endpoints: + +```ts +router.get('/auth/providerA/start'); +router.get('/auth/providerA/handler/frame'); +router.post('/auth/providerA/handler/frame'); +router.post('/auth/providerA/logout'); +router.get('/auth/providerA/refresh'); // if supported +``` + +As you can see each endpoint is prefixed with both `/auth` and its provider name. + +### Test the new provider + +You can `curl -i localhost:7000/auth/providerA/start` and which should provide a `302` redirect with a `Location` header. Paste the url from that header into a web browser and you should be able to trigger the authorization flow. + +--- + +##### OAuthProviderHandlers + +```ts +export interface OAuthProviderHandlers { + start(req: express.Request, options: any): Promise; + handler(req: express.Request): Promise; + refresh?(refreshToken: string, scope: string): Promise; + logout?(): Promise; +} +``` + +##### AuthProviderRouteHandlers + +```ts +export interface AuthProviderRouteHandlers { + start(req: express.Request, res: express.Response): Promise; + frameHandler(req: express.Request, res: express.Response): Promise; + refresh?(req: express.Request, res: express.Response): Promise; + logout(req: express.Request, res: express.Response): Promise; +} +``` + +##### AuthProviderConfig + +```ts +export type AuthProviderConfig = { + provider: string; + options: any; + disableRefresh?: boolean; +}; +``` diff --git a/lerna.json b/lerna.json index 5be143e0ef..2feea0137f 100644 --- a/lerna.json +++ b/lerna.json @@ -2,5 +2,5 @@ "packages": ["packages/*", "plugins/*"], "npmClient": "yarn", "useWorkspaces": true, - "version": "0.1.1-alpha.6" + "version": "0.1.1-alpha.7" } diff --git a/packages/app/package.json b/packages/app/package.json index 15681d0548..6e314c3fe0 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,21 +1,21 @@ { "name": "example-app", - "version": "0.1.1-alpha.6", + "version": "0.1.1-alpha.7", "private": true, "dependencies": { - "@backstage/cli": "^0.1.1-alpha.6", - "@backstage/core": "^0.1.1-alpha.6", - "@backstage/plugin-catalog": "^0.1.1-alpha.6", - "@backstage/plugin-circleci": "^0.1.1-alpha.6", - "@backstage/plugin-explore": "^0.1.1-alpha.6", - "@backstage/plugin-home-page": "^0.1.1-alpha.6", - "@backstage/plugin-lighthouse": "^0.1.1-alpha.6", - "@backstage/plugin-register-component": "^0.1.1-alpha.6", - "@backstage/plugin-scaffolder": "^0.1.1-alpha.6", - "@backstage/plugin-sentry": "^0.1.1-alpha.6", - "@backstage/plugin-tech-radar": "^0.1.1-alpha.6", - "@backstage/plugin-welcome": "^0.1.1-alpha.6", - "@backstage/theme": "^0.1.1-alpha.6", + "@backstage/cli": "^0.1.1-alpha.7", + "@backstage/core": "^0.1.1-alpha.7", + "@backstage/plugin-catalog": "^0.1.1-alpha.7", + "@backstage/plugin-circleci": "^0.1.1-alpha.7", + "@backstage/plugin-explore": "^0.1.1-alpha.7", + "@backstage/plugin-home-page": "^0.1.1-alpha.7", + "@backstage/plugin-lighthouse": "^0.1.1-alpha.7", + "@backstage/plugin-register-component": "^0.1.1-alpha.7", + "@backstage/plugin-scaffolder": "^0.1.1-alpha.7", + "@backstage/plugin-sentry": "^0.1.1-alpha.7", + "@backstage/plugin-tech-radar": "^0.1.1-alpha.7", + "@backstage/plugin-welcome": "^0.1.1-alpha.7", + "@backstage/theme": "^0.1.1-alpha.7", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "prop-types": "^15.7.2", diff --git a/packages/app/public/index.html b/packages/app/public/index.html index 3d01107696..ea9208ca57 100644 --- a/packages/app/public/index.html +++ b/packages/app/public/index.html @@ -8,47 +8,38 @@ name="description" content="Backstage is an open platform for building developer portals" /> - + - - - + + - Backstage + <%= app.title %> - +