merged master

Signed-off-by: Hao Luo <howlowck@gmail.com>
This commit is contained in:
Hao Luo
2022-10-04 00:45:20 -05:00
614 changed files with 18761 additions and 5696 deletions
+8
View File
@@ -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
+164
View File
@@ -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
```
+15 -9
View File
@@ -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
+1 -1
View File
@@ -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
+6
View File
@@ -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
+1
View File
@@ -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
+1 -1
View File
@@ -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!
+2 -2
View File
@@ -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
+1 -1
View File
@@ -15,7 +15,7 @@ invoking the
from the root of your project.
```bash
yarn create-plugin
yarn new --select plugin
```
![](../assets/getting-started/create-plugin_output.png)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -65
View File
@@ -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)