@@ -35,6 +35,8 @@ auth:
|
||||
clientSecret: ${AUTH_AUTH0_CLIENT_SECRET}
|
||||
domain: ${AUTH_AUTH0_DOMAIN_ID}
|
||||
audience: ${AUTH_AUTH0_AUDIENCE}
|
||||
connection: ${AUTH_AUTH0_CONNECTION}
|
||||
connectionScope: ${AUTH_AUTH0_CONNECTION_SCOPE}
|
||||
```
|
||||
|
||||
The Auth0 provider is a structure with three configuration keys:
|
||||
@@ -44,6 +46,12 @@ The Auth0 provider is a structure with three configuration keys:
|
||||
page
|
||||
- `domain`: The Application domain, found on the Auth0 Application page
|
||||
|
||||
## Optional Configuration
|
||||
|
||||
- `audience`: The intended recipients of the token
|
||||
- `connection`: Social identity provider name. To check the available social connections, please visit [Auth0 Social Connections](https://marketplace.auth0.com/features/social-connections).
|
||||
- `connectionScope`: Additional scopes in the interactive token request. It should always be used in combination with the `connection` parameter
|
||||
|
||||
## Adding the provider to the Backstage frontend
|
||||
|
||||
To add the provider to the frontend, add the `auth0AuthApi` reference and
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
---
|
||||
id: service-to-service-auth
|
||||
title: Service to Service Auth
|
||||
# prettier-ignore
|
||||
description: This section describes how to use service to service authentication, both internally within Backstage plugins and towards external services.
|
||||
---
|
||||
|
||||
This article describes the steps needed to introduce _backend-to-backend auth_.
|
||||
This allows plugin backends to determine whether a given request originates from
|
||||
a legitimate Backstage plugin (or other external caller), by requiring a special
|
||||
type of service-to-service token which is signed with a shared secret.
|
||||
|
||||
When enabling this protection on your Backstage backend plugins, for example the
|
||||
catalog, other callers in the ecosystem such as the search indexer and
|
||||
scaffolder would need to present a valid token to the catalog to be able to
|
||||
request its contents.
|
||||
|
||||
## Setup
|
||||
|
||||
In a newly created Backstage app, the backend is setup up to not require any
|
||||
auth at all. This means that generated service-to-service tokens are empty, and
|
||||
that incoming requests are not validated. If you want to enable
|
||||
service-to-service auth, the first step is to switch out the following line in
|
||||
your backend setup at `packages/backend/src/index.ts`:
|
||||
|
||||
```diff
|
||||
- const tokenManager = ServerTokenManager.noop();
|
||||
+ const tokenManager = ServerTokenManager.fromConfig(config, { logger: root });
|
||||
```
|
||||
|
||||
By switching from the no-op `ServiceTokenManager` to one created from config,
|
||||
you enable service-to-service auth for any plugin that implements it. The local
|
||||
development setup will generally not be impacted by this, as temporary keys are
|
||||
generated under the hood. But for the production setup, this means you must now
|
||||
provide a shared secret that enables your backend plugins to communicate with
|
||||
each other.
|
||||
|
||||
Backstage service-to-service tokens are currently always signed with a single
|
||||
secret key. It needs to be shared across all backend plugins and services that
|
||||
ones wishes to communicate across. The key can be any base64 encoded secret.
|
||||
The following command can be used to generate such a key in a terminal:
|
||||
|
||||
```bash
|
||||
node -p 'require("crypto").randomBytes(24).toString("base64")'
|
||||
```
|
||||
|
||||
Then place it in the backend configuration, either as a direct value or
|
||||
injected as an env variable.
|
||||
|
||||
```yaml
|
||||
# commonly in your app-config.production.yaml
|
||||
backend:
|
||||
auth:
|
||||
keys:
|
||||
- secret: <the string returned by the above crypto command>
|
||||
# - secret: ${BACKEND_SECRET} - if you want to use an env variable instead
|
||||
```
|
||||
|
||||
**NOTE**: For ease of development, we auto-generate a key for you if you haven't
|
||||
configured a secret in dev mode. You _must set your own secret_ in order for
|
||||
backend-to-backend auth to work in production; the `ServiceTokenManager` will
|
||||
throw an exception in production if it has no keys to work with, which will lead
|
||||
to the backend failing to start up.
|
||||
|
||||
## Usage in Backend Plugins
|
||||
|
||||
There are a few steps if you want to make use of the service-to-service auth in
|
||||
your own backend plugin. First you need to add the `TokenManager` dependency to
|
||||
the `createRouter` options. Typically as `tokenManager: TokenManager`. Along
|
||||
with this you'll need to ask users to start providing this new dependency in
|
||||
their backend setup code.
|
||||
|
||||
Once the `TokenManager` is available, you use the `.getToken()` method to generate
|
||||
a new token for any outgoing requests towards other Backstage backend plugins.
|
||||
This method should be called for every request that you make; do not store the
|
||||
token for later use. The `TokenManager` implementations should already cache
|
||||
tokens as needed. The returned token should then be added as a `Bearer` token
|
||||
for the upstream request, for example:
|
||||
|
||||
```ts
|
||||
const { token } = await this.tokenManager.getToken();
|
||||
|
||||
const response = await fetch(pluginBackendApiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
...headers,
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
To authenticate an incoming request you use the `.authenticate(token)` method.
|
||||
At the time of writing this method doesn't return anything, it will simply
|
||||
throw if the token is invalid.
|
||||
|
||||
```ts
|
||||
await tokenManager.authenticate(token); // throws if token is invalid
|
||||
```
|
||||
|
||||
## Usage in External Callers
|
||||
|
||||
If you have enabled server-to-server auth, you may be interested in generating
|
||||
tokens in code that is external to Backstage itself. External callers may even
|
||||
be written in other languages than Node.js. This section explains how to generate
|
||||
a valid token yourself.
|
||||
|
||||
The token must be a JWT with a `HS256` signature, using the raw base64 decoded
|
||||
value of the configured key as the secret. It must also have the following payload:
|
||||
|
||||
- `sub`: "backstage-server" (only this value supported currently)
|
||||
- `exp`: one hour from the time it was generated, in epoch seconds
|
||||
|
||||
## Granular Access Control
|
||||
|
||||
We plan to build out the service-to-service auth to be much more powerful in the
|
||||
future, but before that is done there are a few tricks you can use with the
|
||||
current system to harden your deployments. This section assumes that you have
|
||||
already split your backend plugins into more than one backend deployment, in
|
||||
order to scale or isolate them.
|
||||
|
||||
The backend auth configuration has support for providing multiple keys, for
|
||||
example:
|
||||
|
||||
```yaml
|
||||
backend:
|
||||
auth:
|
||||
keys:
|
||||
- secret: my-secret-key-1
|
||||
- secret: my-secret-key-2
|
||||
- secret: my-secret-key-3
|
||||
```
|
||||
|
||||
The first key will be used for signing requests, while all of the keys will be
|
||||
used for validation. This means that you can set up an asymmetric configuration
|
||||
where some backend deployments do not have access to each other.
|
||||
|
||||
For example, consider the case where we have split up the catalog, scaffolder,
|
||||
and search plugin into three separate backend deployments. We can use the
|
||||
following configurations to allow both the scaffolder and search plugin to speak
|
||||
to the
|
||||
catalog, but not the other way around, and to not allow any communication between
|
||||
the scaffolder and search plugins.
|
||||
|
||||
```yaml
|
||||
# catalog config
|
||||
backend:
|
||||
auth:
|
||||
keys:
|
||||
- secret: my-secret-key-catalog
|
||||
- secret: my-secret-key-scaffolder
|
||||
- secret: my-secret-key-search
|
||||
|
||||
# scaffolder config
|
||||
backend:
|
||||
auth:
|
||||
keys:
|
||||
- secret: my-secret-key-scaffolder
|
||||
|
||||
# search config
|
||||
backend:
|
||||
auth:
|
||||
keys:
|
||||
- secret: my-secret-key-search
|
||||
```
|
||||
@@ -101,14 +101,14 @@ array. Users will see this value in the Software Catalog Kubernetes plugin.
|
||||
This determines how the Kubernetes client authenticates with the Kubernetes
|
||||
cluster. Valid values are:
|
||||
|
||||
| Value | Description |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `serviceAccount` | This will use a Kubernetes [service account](https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/) to access the Kubernetes API. When this is used the `serviceAccountToken` field should also be set. |
|
||||
| `google` | This will use a user's Google auth token from the [Google auth plugin](https://backstage.io/docs/auth/) to access the Kubernetes API. |
|
||||
| `aws` | This will use AWS credentials to access resources in EKS clusters |
|
||||
| `googleServiceAccount` | This will use the Google Cloud service account credentials to access resources in clusters |
|
||||
| `azure` | This will use [Azure Identity](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview) to access resources in clusters |
|
||||
| `oidc` | This will use [Oidc Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens) to authenticate to the Kubernetes API. When this is used the `oidcTokenProvider` field should also be set. |
|
||||
| Value | Description |
|
||||
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `serviceAccount` | This will use a Kubernetes [service account](https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/) to access the Kubernetes API. When this is used the `serviceAccountToken` field should also be set. |
|
||||
| `google` | This will use a user's Google auth token from the [Google auth plugin](https://backstage.io/docs/auth/) to access the Kubernetes API. |
|
||||
| `aws` | This will use AWS credentials to access resources in EKS clusters |
|
||||
| `googleServiceAccount` | This will use the Google Cloud service account credentials to access resources in clusters |
|
||||
| `azure` | This will use [Azure Identity](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview) to access resources in clusters |
|
||||
| `oidc` | This will use [Oidc Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens) to authenticate to the Kubernetes API. When this is used the `oidcTokenProvider` field should also be set. Please note the cluster must support OIDC, at the time of writing AKS clusters do not support OIDC. |
|
||||
|
||||
Check the [Kubernetes Authentication][4] section for additional explanation.
|
||||
|
||||
@@ -159,7 +159,12 @@ auth:
|
||||
audience: ${AUTH_OKTA_AUDIENCE}
|
||||
```
|
||||
|
||||
The following values are supported out-of-the-box by the frontend: `google`, `microsoft`, `okta`, `onelogin`.
|
||||
The following values are supported out-of-the-box by the frontend: `google`, `microsoft`,
|
||||
`okta`, `onelogin`.
|
||||
|
||||
Take note that `oidcTokenProvider` is just the issuer for the token, you can use any
|
||||
of these with an OIDC enabled cluster, like using `microsoft` as the issuer for a EKS
|
||||
cluster.
|
||||
|
||||
##### `clusters.\*.dashboardUrl` (optional)
|
||||
|
||||
@@ -413,6 +418,7 @@ rules:
|
||||
- ingresses
|
||||
- statefulsets
|
||||
- limitranges
|
||||
- daemonsets
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
|
||||
@@ -101,7 +101,7 @@ Similarly to Lunr above, ElasticSearch can be set up like this:
|
||||
|
||||
```typescript
|
||||
// app/backend/src/plugins/search.ts
|
||||
const searchEngine = await ElasticSearchSearchEngine.initialize({
|
||||
const searchEngine = await ElasticSearchSearchEngine.fromConfig({
|
||||
logger: env.logger,
|
||||
config: env.config,
|
||||
});
|
||||
|
||||
@@ -156,16 +156,17 @@ export default async function createPlugin(
|
||||
Here is a list of Open Source custom actions that you can add to your Backstage
|
||||
scaffolder backend:
|
||||
|
||||
| Name | Package | Owner |
|
||||
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
|
||||
| Yeoman | [plugin-scaffolder-backend-module-yeoman](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend-module-yeoman) | [Backstage](https://backstage.io) |
|
||||
| Cookiecutter | [plugin-scaffolder-backend-module-cookiecutter](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend-module-cookiecutter) | [Backstage](https://backstage.io) |
|
||||
| Rails | [plugin-scaffolder-backend-module-rails](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend-module-rails) | [Backstage](https://backstage.io) |
|
||||
| HTTP requests | [scaffolder-backend-module-http-request](https://www.npmjs.com/package/@roadiehq/scaffolder-backend-module-http-request) | [Roadie](https://roadie.io) |
|
||||
| Utility actions | [scaffolder-backend-module-utils](https://www.npmjs.com/package/@roadiehq/scaffolder-backend-module-utils) | [Roadie](https://roadie.io) |
|
||||
| AWS cli actions | [scaffolder-backend-module-aws](https://www.npmjs.com/package/@roadiehq/scaffolder-backend-module-aws) | [Roadie](https://roadie.io) |
|
||||
| Scaffolder .NET Actions | [plugin-scaffolder-dotnet-backend](https://www.npmjs.com/package/@plusultra/plugin-scaffolder-dotnet-backend) | [Alef Carlos](https://github.com/alefcarlos) |
|
||||
| Scaffolder Git Actions | [plugin-scaffolder-git-actions](https://www.npmjs.com/package/@mdude2314/backstage-plugin-scaffolder-git-actions) | [Drew Hill](https://github.com/arhill05) |
|
||||
| Azure Pipeline Actions | [scaffolder-backend-module-azure-pipelines](https://www.npmjs.com/package/@parfuemerie-douglas/scaffolder-backend-module-azure-pipelines) | [Parfümerie Douglas](https://github.com/Parfuemerie-Douglas) |
|
||||
| Name | Package | Owner |
|
||||
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
|
||||
| Yeoman | [plugin-scaffolder-backend-module-yeoman](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend-module-yeoman) | [Backstage](https://backstage.io) |
|
||||
| Cookiecutter | [plugin-scaffolder-backend-module-cookiecutter](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend-module-cookiecutter) | [Backstage](https://backstage.io) |
|
||||
| Rails | [plugin-scaffolder-backend-module-rails](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend-module-rails) | [Backstage](https://backstage.io) |
|
||||
| HTTP requests | [scaffolder-backend-module-http-request](https://www.npmjs.com/package/@roadiehq/scaffolder-backend-module-http-request) | [Roadie](https://roadie.io) |
|
||||
| Utility actions | [scaffolder-backend-module-utils](https://www.npmjs.com/package/@roadiehq/scaffolder-backend-module-utils) | [Roadie](https://roadie.io) |
|
||||
| AWS cli actions | [scaffolder-backend-module-aws](https://www.npmjs.com/package/@roadiehq/scaffolder-backend-module-aws) | [Roadie](https://roadie.io) |
|
||||
| Scaffolder .NET Actions | [plugin-scaffolder-dotnet-backend](https://www.npmjs.com/package/@plusultra/plugin-scaffolder-dotnet-backend) | [Alef Carlos](https://github.com/alefcarlos) |
|
||||
| Scaffolder Git Actions | [plugin-scaffolder-git-actions](https://www.npmjs.com/package/@mdude2314/backstage-plugin-scaffolder-git-actions) | [Drew Hill](https://github.com/arhill05) |
|
||||
| Azure Pipeline Actions | [scaffolder-backend-module-azure-pipelines](https://www.npmjs.com/package/@parfuemerie-douglas/scaffolder-backend-module-azure-pipelines) | [Parfümerie Douglas](https://github.com/Parfuemerie-Douglas) |
|
||||
| Azure Repository Actions | [scaffolder-backend-module-azure-repositories](https://www.npmjs.com/package/@parfuemerie-douglas/scaffolder-backend-module-azure-repositories) | [Parfümerie Douglas](https://github.com/Parfuemerie-Douglas) |
|
||||
|
||||
Have fun! 🚀
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
id: writing-custom-step-layouts
|
||||
title: Writing custom step layouts
|
||||
description: How to override the default step form layout
|
||||
---
|
||||
|
||||
Every form in each step rendered in the frontend uses the default form layout from [react-json-schema-form](https://react-jsonschema-form.readthedocs.io/). It is possible to override this behaviour by supplying a `ui:ObjectFieldTemplate` property for a particular step:
|
||||
|
||||
```yaml
|
||||
parameters:
|
||||
- title: Fill in some steps
|
||||
ui:ObjectFieldTemplate: TwoColumn
|
||||
```
|
||||
|
||||
This is the same [field](https://react-jsonschema-form.readthedocs.io/en/latest/advanced-customization/custom-templates/#objectfieldtemplate) used by [react-json-schema-form](https://react-jsonschema-form.readthedocs.io/) but we need to add a couple of steps to ensure that the string value of `TwoColumn` above is resolved to a react component.
|
||||
|
||||
## Registering a React component as a custom step layout
|
||||
|
||||
The [createScaffolderLayout](https://backstage.io/docs/reference/plugin-scaffolder.createscaffolderlayout) function is used to mark a component as a custom step layout:
|
||||
|
||||
```ts
|
||||
import React from 'react';
|
||||
import {
|
||||
createScaffolderLayout,
|
||||
LayoutTemplate,
|
||||
scaffolderPlugin,
|
||||
} from '@backstage/plugin-scaffolder';
|
||||
import { Grid } from '@material-ui/core';
|
||||
|
||||
const TwoColumn: LayoutTemplate = ({ properties, description, title }) => {
|
||||
const mid = Math.ceil(properties.length / 2);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>{title}</h1>
|
||||
<h2>In two column layout!!</h2>
|
||||
<Grid container justifyContent="flex-end">
|
||||
{properties.slice(0, mid).map(prop => (
|
||||
<Grid item xs={6} key={prop.content.key}>
|
||||
{prop.content}
|
||||
</Grid>
|
||||
))}
|
||||
{properties.slice(mid).map(prop => (
|
||||
<Grid item xs={6} key={prop.content.key}>
|
||||
{prop.content}
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
{description}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const TwoColumnLayout = scaffolderPlugin.provide(
|
||||
createScaffolderLayout({
|
||||
name: 'TwoColumn',
|
||||
component: TwoColumn,
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
After you have registered your component as a custom layout then you need to provide the `layouts` to the `ScaffolderPage`:
|
||||
|
||||
```tsx
|
||||
import { MyCustomFieldExtension } from './scaffolder/MyCustomExtension';
|
||||
import { TwoColumnLayout } from './components/scaffolder/customScaffolderLayouts';
|
||||
|
||||
const routes = (
|
||||
<FlatRoutes>
|
||||
...
|
||||
<Route path="/create" element={<ScaffolderPage />}>
|
||||
<ScaffolderLayouts>
|
||||
<TwoColumnLayout />
|
||||
</ScaffolderLayouts>
|
||||
</Route>
|
||||
...
|
||||
</FlatRoutes>
|
||||
);
|
||||
```
|
||||
|
||||
## Using the custom step layout
|
||||
|
||||
Any component that has been passed to the `ScaffolderPage` as children of the `ScaffolderLayouts` component can be used as a `ui:ObjectFieldTemplate` in your template file:
|
||||
|
||||
```yaml
|
||||
parameters:
|
||||
- title: Fill in some steps
|
||||
ui:ObjectFieldTemplate: TwoColumn
|
||||
```
|
||||
@@ -258,6 +258,10 @@ use `ui:widget: password` or set some properties of `ui:backstage`:
|
||||
show: false # won't print any info about 'hidden' property on Review Step
|
||||
```
|
||||
|
||||
### Custom step layouts
|
||||
|
||||
If you find that the default layout of the form used in a particular step does not meet your needs then you can supply your own [custom step layout](./writing-custom-step-layouts.md).
|
||||
|
||||
### Remove sections or fields based on feature flags
|
||||
|
||||
Based on feature flags you can hide sections or even only fields of your
|
||||
|
||||
@@ -96,6 +96,10 @@ catalog:
|
||||
topic:
|
||||
include: ['backstage-include'] # optional array of strings
|
||||
exclude: ['experiments'] # optional array of strings
|
||||
enterpriseProviderId:
|
||||
host: ghe.example.net
|
||||
organization: 'backstage' # string
|
||||
catalogPath: '/catalog-info.yaml' # string
|
||||
```
|
||||
|
||||
This provider supports multiple organizations via unique provider IDs.
|
||||
@@ -125,6 +129,8 @@ This provider supports multiple organizations via unique provider IDs.
|
||||
- **organization**:
|
||||
Name of your organization account/workspace.
|
||||
If you want to add multiple organizations, you need to add one provider config each.
|
||||
- **host** _(optional)_:
|
||||
The hostname of your GitHub Enterprise instance. It must match a host defined in [integrations.github](locations.md).
|
||||
|
||||
## GitHub API Rate Limits
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ description: Support and Community Details and Links
|
||||
|
||||
- [Discord chatroom](https://discord.gg/MUpMjP2) - Get support or discuss the
|
||||
project.
|
||||
- [Stack Overflow](https://stackoverflow.com/questions/tagged/backstage) - Browse or ask questions on Stack Overflow.
|
||||
- [Good First Issues](https://github.com/backstage/backstage/contribute) - Start
|
||||
here if you want to contribute.
|
||||
- [RFCs](https://github.com/backstage/backstage/labels/rfc) - Help shape the
|
||||
|
||||
@@ -17,7 +17,7 @@ If your Backstage permission policy may return a `DENY` for users requesting the
|
||||
|
||||
...
|
||||
|
||||
+ import { PermissionedRoute } from '@backstage/plugin-permission-react';
|
||||
+ import { RequirePermission } from '@backstage/plugin-permission-react';
|
||||
+ import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common';
|
||||
|
||||
...
|
||||
|
||||
@@ -20,15 +20,18 @@ Let's navigate to the file `plugins/todo-list-common/src/permissions.ts` and add
|
||||
- export const tempExamplePermission = createPermission({
|
||||
- name: 'temp.example.noop',
|
||||
- attributes: {},
|
||||
+ export const todoListCreate = createPermission({
|
||||
+ export const todoListCreatePermission = createPermission({
|
||||
+ name: 'todo.list.create',
|
||||
+ attributes: { action: 'create' },
|
||||
});
|
||||
|
||||
- export const todoListPermissions = [tempExamplePermission];
|
||||
+ export const todoListPermissions = [todoListCreatePermission];
|
||||
```
|
||||
|
||||
For this tutorial, we've automatically exported all permissions from this file (see `plugins/todo-list-common/src/index.ts`).
|
||||
|
||||
> Note: All permissions authorized by your plugin should be exported from a ["common-library" package](https://backstage.io/docs/local-dev/cli-build-system#package-roles). This allows Backstage integrators to reference them in frontend components and permission policies.
|
||||
> Note: We use a separate `todo-list-common` package since all permissions authorized by your plugin should be exported from a ["common-library" package](https://backstage.io/docs/local-dev/cli-build-system#package-roles). This allows Backstage integrators to reference them in frontend components and permission policies.
|
||||
|
||||
## Authorizing using the new permission
|
||||
|
||||
@@ -47,7 +50,7 @@ Edit `plugins/todo-list-backend/src/service/router.ts`:
|
||||
- import { InputError } from '@backstage/errors';
|
||||
+ import { InputError, NotAllowedError } from '@backstage/errors';
|
||||
+ import { PermissionEvaluator, AuthorizeResult } from '@backstage/plugin-permission-common';
|
||||
+ import { todoListCreate } from '@internal/plugin-todo-list-common';
|
||||
+ import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
|
||||
...
|
||||
|
||||
@@ -72,7 +75,7 @@ Edit `plugins/todo-list-backend/src/service/router.ts`:
|
||||
const user = token ? await identity.authenticate(token) : undefined;
|
||||
author = user?.identity.userEntityRef;
|
||||
+ const decision = (
|
||||
+ await permissions.authorize([{ permission: todoListCreate }], {
|
||||
+ await permissions.authorize([{ permission: todoListCreatePermission }], {
|
||||
+ token,
|
||||
+ })
|
||||
+ )[0];
|
||||
@@ -135,7 +138,7 @@ In order to test the logic above, the integrators of your backstage instance nee
|
||||
+ PolicyQuery,
|
||||
} from '@backstage/plugin-permission-node';
|
||||
+ import { isPermission } from '@backstage/plugin-permission-common';
|
||||
+ import { todoListCreate } from '@internal/plugin-todo-list-common';
|
||||
+ import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
- async handle(): Promise<PolicyDecision> {
|
||||
@@ -143,7 +146,7 @@ In order to test the logic above, the integrators of your backstage instance nee
|
||||
+ request: PolicyQuery,
|
||||
+ user?: BackstageIdentityResponse,
|
||||
+ ): Promise<PolicyDecision> {
|
||||
+ if (isPermission(request.permission, todoListCreate)) {
|
||||
+ if (isPermission(request.permission, todoListCreatePermission)) {
|
||||
+ return {
|
||||
+ result: AuthorizeResult.DENY,
|
||||
+ };
|
||||
@@ -160,7 +163,7 @@ Now the frontend should show an error whenever you try to create a new Todo item
|
||||
Let's flip the result back to `ALLOW` before moving on.
|
||||
|
||||
```diff
|
||||
if (isPermission(request.permission, todoListCreate)) {
|
||||
if (isPermission(request.permission, todoListCreatePermission)) {
|
||||
return {
|
||||
- result: AuthorizeResult.DENY,
|
||||
+ result: AuthorizeResult.ALLOW,
|
||||
|
||||
@@ -15,27 +15,30 @@ Let's add a new permission to the file `plugins/todo-list-common/src/permissions
|
||||
|
||||
+ export const TODO_LIST_RESOURCE_TYPE = 'todo-item';
|
||||
+
|
||||
export const todoListCreate = createPermission({
|
||||
export const todoListCreatePermission = createPermission({
|
||||
name: 'todo.list.create',
|
||||
attributes: { action: 'create' },
|
||||
});
|
||||
+
|
||||
+ export const todoListUpdate = createPermission({
|
||||
+ export const todoListUpdatePermission = createPermission({
|
||||
+ name: 'todo.list.update',
|
||||
+ attributes: { action: 'update' },
|
||||
+ resourceType: TODO_LIST_RESOURCE_TYPE,
|
||||
+ });
|
||||
|
||||
- export const todoListPermissions = [todoListCreatePermission];
|
||||
+ export const todoListPermissions = [todoListCreatePermission, todoListUpdatePermission];
|
||||
```
|
||||
|
||||
Notice that unlike `todoListCreate`, the `todoListUpdate` permission contains a `resourceType` field. This field indicates to the permission framework that this permission is intended to be authorized in the context of a resource with type `'todo-item'`. You can use whatever string you like as the resource type, as long as you use the same value consistently for each type of resource.
|
||||
Notice that unlike `todoListCreatePermission`, the `todoListUpdatePermission` permission contains a `resourceType` field. This field indicates to the permission framework that this permission is intended to be authorized in the context of a resource with type `'todo-item'`. You can use whatever string you like as the resource type, as long as you use the same value consistently for each type of resource.
|
||||
|
||||
## Setting up authorization for the update permission
|
||||
|
||||
To start, let's edit `plugins/todo-list-backend/src/service/router.ts` in the same manner as we did in the previous section:
|
||||
|
||||
```diff
|
||||
- import { todoListCreate } from '@internal/plugin-todo-list-common';
|
||||
+ import { todoListCreate, todoListUpdate } from '@internal/plugin-todo-list-common';
|
||||
- import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
+ import { todoListCreatePermission, todoListUpdatePermission } from '@internal/plugin-todo-list-common';
|
||||
|
||||
...
|
||||
|
||||
@@ -49,7 +52,7 @@ To start, let's edit `plugins/todo-list-backend/src/service/router.ts` in the sa
|
||||
}
|
||||
+ const decision = (
|
||||
+ await permissions.authorize(
|
||||
+ [{ permission: todoListUpdate, resourceRef: req.body.id }],
|
||||
+ [{ permission: todoListUpdatePermission, resourceRef: req.body.id }],
|
||||
+ {
|
||||
+ token,
|
||||
+ },
|
||||
@@ -119,6 +122,7 @@ Now, let's create the new endpoint by editing `plugins/todo-list-backend/src/ser
|
||||
|
||||
- `getResources`: a function that accepts an array of `resourceRefs` in the same format you expect to be passed to `authorize`, and returns an array of the corresponding resources.
|
||||
- `resourceType`: the same value used in the permission rule above.
|
||||
- `permissions`: the list of permissions that your plugin accepts.
|
||||
- `rules`: an array of all the permission rules you want to support in conditional decisions.
|
||||
|
||||
```diff
|
||||
@@ -127,7 +131,7 @@ Now, let's create the new endpoint by editing `plugins/todo-list-backend/src/ser
|
||||
- import { add, getAll, update } from './todos';
|
||||
+ import { add, getAll, getTodo, update } from './todos';
|
||||
+ import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';
|
||||
+ import { TODO_LIST_RESOURCE_TYPE } from '@internal/plugin-todo-list-common';
|
||||
+ import { TODO_LIST_RESOURCE_TYPE, todoListPermissions } from '@internal/plugin-todo-list-common';
|
||||
+ import { rules } from './rules';
|
||||
|
||||
export async function createRouter(
|
||||
@@ -140,6 +144,7 @@ Now, let's create the new endpoint by editing `plugins/todo-list-backend/src/ser
|
||||
+ return resourceRefs.map(getTodo);
|
||||
+ },
|
||||
+ resourceType: TODO_LIST_RESOURCE_TYPE,
|
||||
+ permissions: todoListPermissions,
|
||||
+ rules: Object.values(rules),
|
||||
+ });
|
||||
|
||||
@@ -196,10 +201,10 @@ Let's go back to the permission policy's handle function and try to authorize ou
|
||||
PolicyQuery,
|
||||
} from '@backstage/plugin-permission-node';
|
||||
import { isPermission } from '@backstage/plugin-permission-common';
|
||||
- import { todoListCreate } from '@internal/plugin-todo-list-common';
|
||||
- import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
+ import {
|
||||
+ todoListCreate,
|
||||
+ todoListUpdate,
|
||||
+ todoListCreatePermission,
|
||||
+ todoListUpdatePermission,
|
||||
+ TODO_LIST_RESOURCE_TYPE,
|
||||
+ } from '@internal/plugin-todo-list-common';
|
||||
+ import {
|
||||
@@ -209,13 +214,13 @@ Let's go back to the permission policy's handle function and try to authorize ou
|
||||
|
||||
...
|
||||
|
||||
if (isPermission(request.permission, todoListCreate)) {
|
||||
if (isPermission(request.permission, todoListCreatePermission)) {
|
||||
return {
|
||||
result: AuthorizeResult.ALLOW,
|
||||
};
|
||||
}
|
||||
|
||||
+ if (isPermission(request.permission, todoListUpdate)) {
|
||||
+ if (isPermission(request.permission, todoListUpdatePermission)) {
|
||||
+ return createTodoListConditionalDecision(
|
||||
+ request.permission,
|
||||
+ todoListConditions.isOwner(user?.identity.userEntityRef),
|
||||
|
||||
@@ -42,12 +42,12 @@ Let's add another permission to the plugin.
|
||||
|
||||
export const TODO_LIST_RESOURCE_TYPE = 'todo-item';
|
||||
|
||||
export const todoListCreate = createPermission({
|
||||
export const todoListCreatePermission = createPermission({
|
||||
name: 'todo.list.create',
|
||||
attributes: { action: 'create' },
|
||||
});
|
||||
|
||||
export const todoListUpdate = createPermission({
|
||||
export const todoListUpdatePermission = createPermission({
|
||||
name: 'todo.list.update',
|
||||
attributes: { action: 'update' },
|
||||
resourceType: TODO_LIST_RESOURCE_TYPE,
|
||||
@@ -58,6 +58,9 @@ Let's add another permission to the plugin.
|
||||
+ attributes: { action: 'read' },
|
||||
+ resourceType: TODO_LIST_RESOURCE_TYPE,
|
||||
+ });
|
||||
|
||||
- export const todoListPermissions = [todoListCreatePermission, todoListUpdatePermission];
|
||||
+ export const todoListPermissions = [todoListCreatePermission, todoListUpdatePermission, todoListReadPermission];
|
||||
```
|
||||
|
||||
## Using conditional policy decisions
|
||||
@@ -126,18 +129,18 @@ Let's update our permission policy to return a conditional result whenever a `to
|
||||
...
|
||||
|
||||
import {
|
||||
todoListCreate,
|
||||
todoListUpdate,
|
||||
+ todoListRead,
|
||||
todoListCreatePermission,
|
||||
todoListUpdatePermission,
|
||||
+ todoListReadPermission,
|
||||
TODO_LIST_RESOURCE_TYPE,
|
||||
} from '@internal/plugin-todo-list-common';
|
||||
|
||||
...
|
||||
|
||||
- if (isPermission(request.permission, todoListUpdate)) {
|
||||
- if (isPermission(request.permission, todoListUpdatePermission)) {
|
||||
+ if (
|
||||
+ isPermission(request.permission, todoListUpdate) ||
|
||||
+ isPermission(request.permission, todoListRead)
|
||||
+ isPermission(request.permission, todoListUpdatePermission) ||
|
||||
+ isPermission(request.permission, todoListReadPermission)
|
||||
+ ) {
|
||||
return createTodoListConditionalDecision(
|
||||
request.permission,
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
---
|
||||
id: 05-frontend-authorization
|
||||
title: 5. Frontend Components with Authorization
|
||||
description: Placing frontend components behind authorization
|
||||
---
|
||||
|
||||
In the previous sections, we learned how to protect our plugin's backend API routes with the permission framework. Most routes that return some data to be displayed (such as our `GET /todos` route) need no additional changes on the frontend, as the backend will simply return an empty list or a `404`. However, for UI elements that trigger a mutative action, it's common practice to hide or disable them when a user doesn't have permission.
|
||||
|
||||
Take, for example, the "Add" button in our todo list application. When a user clicks this button, the frontend makes a `POST` request to the `/todos` route of our backend. If a user tries to add a todo but is not authorized, they will have no way of knowing this until they perform the action and are faced with an error. This is a poor user experience. We can do better by disabling the add button.
|
||||
|
||||
> Note: Placing frontend components behind authorization cannot take the place of placing your backend routes behind authorization. Authorization checks on the frontend should be used in _addition_ to the corresponding backend authorization, as an improvement to the user experience. If you do not place your backend route behind authorization, a malicious actor can still send a request to the route even if you disabled the corresponding frontend component.
|
||||
|
||||
## Using `usePermission`
|
||||
|
||||
Let's start by adding the packages we will need:
|
||||
|
||||
```
|
||||
$ yarn workspace @internal/plugin-todo-list \
|
||||
add @backstage/plugin-permission-react @internal/plugin-todo-list-common
|
||||
```
|
||||
|
||||
Let's make the following changes in `plugins/todo-list/src/components/TodoListPage/TodoListPage.tsx`:
|
||||
|
||||
```diff
|
||||
...
|
||||
|
||||
import {
|
||||
alertApiRef,
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
useApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
+ import { usePermission } from '@backstage/plugin-permission-react';
|
||||
+ import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
|
||||
...
|
||||
|
||||
function AddTodo({ onAdd }: { onAdd: (title: string) => any }) {
|
||||
const title = useRef('');
|
||||
+ const { loading: loadingPermission, allowed: canAddTodo } = usePermission({ permission: todoListCreatePermission });
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="body1">Add todo</Typography>
|
||||
<Box
|
||||
component="span"
|
||||
alignItems="flex-end"
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
>
|
||||
<TextField
|
||||
placeholder="Write something here..."
|
||||
onChange={e => (title.current = e.target.value)}
|
||||
/>
|
||||
- <Button variant="contained" onClick={handleAdd}>
|
||||
- Add
|
||||
- </Button>
|
||||
+ {!loadingPermission && (
|
||||
+ <Button disabled={!canAddTodo} variant="contained" onClick={handleAdd}>
|
||||
+ Add
|
||||
+ </Button>
|
||||
+ )}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
Here we are using the [`usePermission` hook](https://backstage.io/docs/reference/plugin-permission-react.usepermission) to communicate with the permission policy and receive a decision on whether this user is authorized to create a todo list item.
|
||||
|
||||
It's really that simple! Let's change our policy to test the disabled button:
|
||||
|
||||
```diff
|
||||
// packages/backend/src/plugins/permission.ts
|
||||
|
||||
...
|
||||
|
||||
if (isPermission(request.permission, todoListCreatePermission)) {
|
||||
return {
|
||||
- result: AuthorizeResult.ALLOW,
|
||||
+ result: AuthorizeResult.DENY,
|
||||
};
|
||||
}
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
And now you should see that you are not able to create a todo item from the frontend!
|
||||
|
||||
## Using `RequirePermission`
|
||||
|
||||
Providing a disabled state can be a helpful signal to users, but there may be cases where hiding the element is preferred. For such cases, you can use the provided [`RequirePermission` component](https://backstage.io/docs/reference/plugin-permission-react.requirepermission):
|
||||
|
||||
```diff
|
||||
// plugins/todo-list/src/components/TodoListPage/TodoListPage.tsx
|
||||
|
||||
...
|
||||
|
||||
import {
|
||||
alertApiRef,
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
useApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
- import { usePermission } from '@backstage/plugin-permission-react';
|
||||
+ import { RequirePermission } from '@backstage/plugin-permission-react';
|
||||
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
|
||||
...
|
||||
|
||||
export const TodoListPage = () => {
|
||||
|
||||
...
|
||||
|
||||
<Grid container spacing={3} direction="column">
|
||||
- <Grid item>
|
||||
- <AddTodo onAdd={handleAdd} />
|
||||
- </Grid>
|
||||
+ <RequirePermission permission={todoListCreatePermission}>
|
||||
+ <Grid item>
|
||||
+ <AddTodo onAdd={handleAdd} />
|
||||
+ </Grid>
|
||||
+ </RequirePermission>
|
||||
<Grid item>
|
||||
<TodoList key={key} onEdit={setEdit} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
...
|
||||
|
||||
|
||||
function AddTodo({ onAdd }: { onAdd: (title: string) => any }) {
|
||||
const title = useRef('');
|
||||
- const { loading: loadingPermission, allowed: canAddTodo } = usePermission({ permission: todoListCreatePermission });
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="body1">Add todo</Typography>
|
||||
<Box
|
||||
component="span"
|
||||
alignItems="flex-end"
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
>
|
||||
<TextField
|
||||
placeholder="Write something here..."
|
||||
onChange={e => (title.current = e.target.value)}
|
||||
/>
|
||||
- {!loadingPermission && (
|
||||
- <Button disabled={!canAddTodo} variant="contained" onClick={handleAdd}>
|
||||
- Add
|
||||
- </Button>
|
||||
- )}
|
||||
+ <Button variant="contained" onClick={handleAdd}>
|
||||
+ Add
|
||||
+ </Button>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
Now you should find that the component for adding a todo list item does not render at all. Success!
|
||||
@@ -13,10 +13,10 @@ A new, bare-bones backend plugin package can be created by issuing the following
|
||||
command in your Backstage repository root:
|
||||
|
||||
```sh
|
||||
yarn create-plugin --backend
|
||||
yarn new --select backend-plugin
|
||||
```
|
||||
|
||||
Please also see the `--help` flag for the `create-plugin` command for some
|
||||
Please also see the `--help` flag for the `new` command for some
|
||||
further options that are available, notably the `--scope` and `--no-private`
|
||||
flags that control naming and publishing of the newly created package. Your repo
|
||||
root `package.json` will probably also have some default values already set up
|
||||
|
||||
@@ -15,7 +15,7 @@ invoking the
|
||||
from the root of your project.
|
||||
|
||||
```bash
|
||||
yarn create-plugin
|
||||
yarn new --select plugin
|
||||
```
|
||||
|
||||

|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -4,68 +4,4 @@ title: Backend-to-Backend Authentication
|
||||
description: Guide for authenticating API requests between Backstage plugin backends
|
||||
---
|
||||
|
||||
This tutorial describes the steps needed to handle _backend-to-backend
|
||||
authentication_, which allows plugin backends to determine whether a given
|
||||
request originates from a legitimate Backstage backend by verifying a token
|
||||
signed with a shared secret. This system has limited use for now, but will be
|
||||
needed to support the upcoming framework for permissions and authorization (see
|
||||
[the PRFC on the topic](https://github.com/backstage/backstage/pull/7761) for
|
||||
more details).
|
||||
|
||||
Backends have no concept of a Backstage identity, so instead they use a token
|
||||
generated using a shared key stored in config. You can generate a unique key for
|
||||
your app in a terminal, and set the `BACKEND_SECRET` environment variable to the
|
||||
resulting value.
|
||||
|
||||
```bash
|
||||
node -p 'require("crypto").randomBytes(24).toString("base64")'
|
||||
```
|
||||
|
||||
**NOTE**: For ease of development, we auto-generate a key for you if you haven't
|
||||
configured a secret in dev mode. You _must set your own secret_ in order for
|
||||
backend-to-backend authentication to work in production.
|
||||
|
||||
Requests originating from a backend plugin can be authenticated by decorating
|
||||
them with a backend token. Backend tokens can be generated using a
|
||||
`TokenManager`, which can be passed to plugin backends via the
|
||||
`PluginEnvironment`. The `TokenManager` provided in new Backstage instances
|
||||
generated by `create-app` is a stub, which returns empty tokens and accepts any
|
||||
input string as valid. To enable backend-to-backend authentication, you'll need
|
||||
to instantiate a new one using the secret from your config instead:
|
||||
|
||||
```diff
|
||||
// packages/backend/src/index.ts
|
||||
|
||||
function makeCreateEnv(config: Config) {
|
||||
const root = getRootLogger();
|
||||
const reader = UrlReaders.default({ logger: root, config });
|
||||
const discovery = SingleHostDiscovery.fromConfig(config);
|
||||
|
||||
root.info(`Created UrlReader ${reader}`);
|
||||
|
||||
const cacheManager = CacheManager.fromConfig(config);
|
||||
const databaseManager = DatabaseManager.fromConfig(config);
|
||||
- const tokenManager = ServerTokenManager.noop();
|
||||
+ const tokenManager = ServerTokenManager.fromConfig(config, { logger: root });
|
||||
```
|
||||
|
||||
With this `tokenManager`, you can then generate a server token for requests:
|
||||
|
||||
```typescript
|
||||
const { token } = await this.tokenManager.getToken();
|
||||
|
||||
const response = await fetch(pluginBackendApiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
...headers,
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
You can use the same `tokenManager` to authenticate tokens supplied on incoming
|
||||
requests:
|
||||
|
||||
```typescript
|
||||
await tokenManager.authenticate(token); // throws if token is invalid
|
||||
```
|
||||
See [new docs](../auth/service-to-service-auth.md)
|
||||
|
||||
Reference in New Issue
Block a user