Merge branch 'master' into config-auth-cookie-exp-time

Signed-off-by: Jessica He <jhe@redhat.com>
This commit is contained in:
Jessica He
2025-01-30 10:05:14 -05:00
committed by GitHub
1307 changed files with 59750 additions and 16682 deletions
+7 -7
View File
@@ -59,22 +59,22 @@ The Bitbucket provider is a structure with two configuration keys:
This provider includes several resolvers out of the box that you can use:
- `emailMatchingUserEntityProfileEmail`: Matches the email address from the auth provider with the User entity that has a matching `spec.profile.email`. If no match is found it will throw a `NotFoundError`.
- `emailLocalPartMatchingUserEntityName`: Matches the [local part](https://en.wikipedia.org/wiki/Email_address#Local-part) of the email address from the auth provider with the User entity that has a matching `name`. If no match is found it will throw a `NotFoundError`.
- `userIdMatchingUserEntityAnnotation`: Matches the `userId` from the auth provider with the User entity that has a matching `bitbucket.org/user-id` annotation. If no match is found it will throw a `NotFoundError`.
- `usernameMatchingUserEntityAnnotation`: Matches the `username` from the auth provider with the User entity that has a matching `bitbucket.org/username` annotation. If no match is found it will throw a `NotFoundError`.
- `emailMatchingUserEntityProfileEmail`: Matches the email address from the auth provider with the User entity that has a matching `spec.profile.email`. If no match is found, it will throw a `NotFoundError`.
- `emailLocalPartMatchingUserEntityName`: Matches the [local part](https://en.wikipedia.org/wiki/Email_address#Local-part) of the email address from the auth provider with the User entity that has a matching `name`. If no match is found, it will throw a `NotFoundError`.
- `userIdMatchingUserEntityAnnotation`: Matches the `userId` from the auth provider with the User entity that has a matching `bitbucket.org/user-id` annotation. If no match is found, it will throw a `NotFoundError`.
- `usernameMatchingUserEntityAnnotation`: Matches the `username` from the auth provider with the User entity that has a matching `bitbucket.org/username` annotation. If no match is found, it will throw a `NotFoundError`.
:::note Note
The resolvers will be tried in order, but will only be skipped if they throw a `NotFoundError`.
The resolvers will be tried in order but will only be skipped if they throw a `NotFoundError`.
:::
If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation.
If these resolvers do not fit your needs, you can build a custom resolver; this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation.
## Backend Installation
To add the provider to the backend we will first need to install the package by running this command:
To add the provider to the backend, we will first need to install the package by running this command:
```bash title="from your Backstage root directory"
yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-bitbucket-provider
+6 -6
View File
@@ -51,20 +51,20 @@ The Bitbucket Server provider is a structure with two configuration keys:
This provider includes several resolvers out of the box that you can use:
- `emailMatchingUserEntityProfileEmail`: Matches the email address from the auth provider with the User entity that has a matching `spec.profile.email`. If no match is found it will throw a `NotFoundError`.
- `emailLocalPartMatchingUserEntityName`: Matches the [local part](https://en.wikipedia.org/wiki/Email_address#Local-part) of the email address from the auth provider with the User entity that has a matching `name`. If no match is found it will throw a `NotFoundError`.
- `emailMatchingUserEntityProfileEmail`: Matches the email address from the auth provider with the User entity that has a matching `spec.profile.email`. If no match is found, it will throw a `NotFoundError`.
- `emailLocalPartMatchingUserEntityName`: Matches the [local part](https://en.wikipedia.org/wiki/Email_address#Local-part) of the email address from the auth provider with the User entity that has a matching `name`. If no match is found, it will throw a `NotFoundError`.
:::note Note
The resolvers will be tried in order, but will only be skipped if they throw a `NotFoundError`.
The resolvers will be tried in order but will only be skipped if they throw a `NotFoundError`.
:::
If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation.
If these resolvers do not fit your needs, you can build a custom resolver; this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation.
## Backend Installation
To add the provider to the backend we will first need to install the package by running this command:
To add the provider to the backend, we will first need to install the package by running this command:
```bash title="from your Backstage root directory"
yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-bitbucket-server-provider
@@ -93,4 +93,4 @@ To add the provider to the frontend, add the `bitbucketServerAuthApiRef` referen
### ScmAuth
For backstage to be able to use the oauth token of the logged in user to access the Bitbucket Server API, you need to add it to list of ScmAuth providers as shown in [Custom ScmAuthApi Implementation](../index.md#custom-scmauthapi-implementation) using the `ScmAuth.forBitbucketServer` method.
For Backstage to be able to use the OAuth token of the logged-in user to access the Bitbucket Server API, you need to add it to the list of ScmAuth providers as shown in [Custom ScmAuthApi Implementation](../index.md#custom-scmauthapi-implementation) using the `ScmAuth.forBitbucketServer` method.
+7 -7
View File
@@ -57,20 +57,20 @@ This config section must be in place for the provider to load at all.
This provider includes several resolvers out of the box that you can use:
- `emailMatchingUserEntityProfileEmail`: Matches the email address from the auth provider with the User entity that has a matching `spec.profile.email`. If no match is found it will throw a `NotFoundError`.
- `emailLocalPartMatchingUserEntityName`: Matches the [local part](https://en.wikipedia.org/wiki/Email_address#Local-part) of the email address from the auth provider with the User entity that has a matching `name`. If no match is found it will throw a `NotFoundError`.
- `emailMatchingUserEntityProfileEmail`: Matches the email address from the auth provider with the User entity that has a matching `spec.profile.email`. If no match is found, it will throw a `NotFoundError`.
- `emailLocalPartMatchingUserEntityName`: Matches the [local part](https://en.wikipedia.org/wiki/Email_address#Local-part) of the email address from the auth provider with the User entity that has a matching `name`. If no match is found, it will throw a `NotFoundError`.
:::note Note
The resolvers will be tried in order, but will only be skipped if they throw a `NotFoundError`.
The resolvers will be tried in order but will only be skipped if they throw a `NotFoundError`.
:::
If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation.
If these resolvers do not fit your needs, you can build a custom resolver; this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation.
## Backend Installation
To add the provider to the backend we will first need to install the package by running this command:
To add the provider to the backend, we will first need to install the package by running this command:
```bash title="from your Backstage root directory"
yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-cloudflare-access-provider
@@ -89,6 +89,6 @@ backend.add(
## Adding the provider to the Backstage frontend
See [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) for pointers on how to set up the sign-in page, and to also make it work smoothly for local development. You'll use `cfaccess` as the provider name.
See [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) for pointers on how to set up the sign-in page and also make it work smoothly for local development. You'll use `cfaccess` as the provider name.
If you [provide a custom sign in resolver](https://backstage.io/docs/auth/identity-resolver#building-custom-resolvers), you can skip the `signIn` block entirely.
If you [provide a custom sign-in resolver](https://backstage.io/docs/auth/identity-resolver#building-custom-resolvers), you can skip the `signIn` block entirely.
+10 -10
View File
@@ -10,7 +10,7 @@ provider that can authenticate users using GitHub or GitHub Enterprise OAuth.
## Create an OAuth App on GitHub
To add GitHub authentication, you must create either a GitHub App, or an OAuth
To add GitHub authentication, you must create either a GitHub App or an OAuth
App from the GitHub
[developer settings](https://github.com/settings/developers). The `Homepage URL`
should point to Backstage's frontend, while the `Authorization callback URL`
@@ -59,15 +59,15 @@ auth:
The GitHub provider is a structure with these configuration keys:
- `clientId`: The client ID that you generated on GitHub, e.g.
- `clientId`: The client ID that you generated on GitHub, e.g.,
`b59241722e3c3b4816e2`
- `clientSecret`: The client secret tied to the generated client ID.
- `enterpriseInstanceUrl` (optional): The base URL for a GitHub Enterprise
instance, e.g. `https://ghe.<company>.com`. Only needed for GitHub Enterprise.
instance, e.g., `https://ghe.<company>.com`. Only needed for GitHub Enterprise.
- `callbackUrl` (optional): The callback URL that GitHub will use when
initiating an OAuth flow, e.g.
initiating an OAuth flow, e.g.,
`https://your-intermediate-service.com/handler`. Only needed if Backstage is
not the immediate receiver (e.g. one OAuth app for many backstage instances).
not the immediate receiver (e.g., one OAuth app for many backstage instances).
- `sessionDuration` (optional): Lifespan of the user session.
- `signIn`: The configuration for the sign-in process, including the **resolvers**
that should be used to match the user from the auth provider with the user
@@ -77,17 +77,17 @@ The GitHub provider is a structure with these configuration keys:
This provider includes several resolvers out of the box that you can use:
- `emailMatchingUserEntityProfileEmail`: Matches the email address from the auth provider with the User entity that has a matching `spec.profile.email`. If no match is found it will throw a `NotFoundError`.
- `emailLocalPartMatchingUserEntityName`: Matches the [local part](https://en.wikipedia.org/wiki/Email_address#Local-part) of the email address from the auth provider with the User entity that has a matching `name`. If no match is found it will throw a `NotFoundError`.
- `usernameMatchingUserEntityName`: Matches the username from the auth provider with the User entity that has a matching `name`. If no match is found it will throw a `NotFoundError`.
- `emailMatchingUserEntityProfileEmail`: Matches the email address from the auth provider with the User entity that has a matching `spec.profile.email`. If no match is found, it will throw a `NotFoundError`.
- `emailLocalPartMatchingUserEntityName`: Matches the [local part](https://en.wikipedia.org/wiki/Email_address#Local-part) of the email address from the auth provider with the User entity that has a matching `name`. If no match is found, it will throw a `NotFoundError`.
- `usernameMatchingUserEntityName`: Matches the username from the auth provider with the User entity that has a matching `name`. If no match is found, it will throw a `NotFoundError`.
:::note Note
The resolvers will be tried in order, but will only be skipped if they throw a `NotFoundError`.
The resolvers will be tried in order but will only be skipped if they throw a `NotFoundError`.
:::
If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation.
If these resolvers do not fit your needs, you can build a custom resolver; this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation.
## Backend Installation
+7 -6
View File
@@ -20,6 +20,7 @@ should point to your Backstage backend auth handler.
2. Set this to `http://{APP_FQDN}:{APP_BACKEND_PORT}/api/auth/gitlab/handler/frame` for non-local deployments.
3. Select the following scopes from the list:
- [x] `api` Grants full read-write access to the api. This is only required if users need to be able to create merge requests with their own permissions.
- [x] `read_api` Grants read access to the API, including all groups and projects, the container registry, and the package registry.
- [x] `read_user` Grants read-only access to the authenticated user's profile through the /user API endpoint, which includes username, public email, and full name. Also grants access to read-only API endpoints under /users.
- [x] `read_repository` Grants read-only access to repositories on private projects using Git-over-HTTP (not using the API).
- [x] `write_repository` Grants read-write access to repositories on private projects using Git-over-HTTP (not using the API).
@@ -68,21 +69,21 @@ The GitLab provider is a structure with three configuration keys:
This provider includes several resolvers out of the box that you can use:
- `emailMatchingUserEntityProfileEmail`: Matches the email address from the auth provider with the User entity that has a matching `spec.profile.email`. If no match is found it will throw a `NotFoundError`.
- `emailLocalPartMatchingUserEntityName`: Matches the [local part](https://en.wikipedia.org/wiki/Email_address#Local-part) of the email address from the auth provider with the User entity that has a matching `name`. If no match is found it will throw a `NotFoundError`.
- `usernameMatchingUserEntityName`: Matches the username from the auth provider with the User entity that has a matching `name`. If no match is found it will throw a `NotFoundError`.
- `emailMatchingUserEntityProfileEmail`: Matches the email address from the auth provider with the User entity that has a matching `spec.profile.email`. If no match is found, it will throw a `NotFoundError`.
- `emailLocalPartMatchingUserEntityName`: Matches the [local part](https://en.wikipedia.org/wiki/Email_address#Local-part) of the email address from the auth provider with the User entity that has a matching `name`. If no match is found, it will throw a `NotFoundError`.
- `usernameMatchingUserEntityName`: Matches the username from the auth provider with the User entity that has a matching `name`. If no match is found, it will throw a `NotFoundError`.
:::note Note
The resolvers will be tried in order, but will only be skipped if they throw a `NotFoundError`.
The resolvers will be tried in order but will only be skipped if they throw a `NotFoundError`.
:::
If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation.
If these resolvers do not fit your needs, you can build a custom resolver; this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation.
## Backend Installation
To add the provider to the backend we will first need to install the package by running this command:
To add the provider to the backend, we will first need to install the package by running this command:
```bash title="from your Backstage root directory"
yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-gitlab-provider
+17
View File
@@ -63,3 +63,20 @@ auth:
# highlight-add-next-line
guest: {}
```
The above is all you need to get working with the guest provider. However, you can also add additional configuration if you need to such as the following:
```yaml title="app-config.local.yaml"
auth:
providers:
guest:
userEntityRef: user:default/john-smith
ownershipEntityRefs: [group:default/smith-family]
dangerouslyAllowOutsideDevelopment: false
```
The `userEntityRef` is used as the entity reference for the guest user and the `ownershipEntityRefs` is a list of entity references to use for ownership of the guest user, if the user is not found in the catalog.
The `dangerouslyAllowOutsideDevelopment` flag is used to allow users to sign in with the guest provider outside of their development environments, e.g. in production.
You can find this information by looking at the config file [here](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-guest-provider/config.d.ts).
+3 -1
View File
@@ -275,13 +275,15 @@ The last step is to add the provider to the `SignInPage` so users can sign in wi
new provider, please follow the [Sign In Configuration][3] docs, here's where you import
and use the API reference we defined earlier.
## Note
:::note Note
These steps apply to most if not all the providers, including custom providers, the main
difference between different providers will be the contents of the API factory, the code
in the Auth Provider Factory, the resolver, and the different variables each provider
needs in the YAML config or env variables.
:::
[1]: https://backstage.io/docs/auth/identity-resolver
[2]: https://backstage.io/docs/auth/microsoft/provider#create-an-app-registration-on-azure
[3]: https://backstage.io/docs/auth/#sign-in-configuration
@@ -0,0 +1,87 @@
---
id: auditor
title: Auditor Service
sidebar_label: Auditor
description: Documentation for the Auditor service
---
## Overview
This document describes the Auditor Service, a software service designed to record and report on security-relevant events within an application. This service utilizes the `winston` library for logging and provides a flexible way to capture and format audit events.
## Key Features
- Provides a standardized way to capture security events.
- Allows categorization of events by severity level.
- Supports detailed metadata for each event.
- Offers success/failure reporting for events.
- Integrates with authentication and plugin services for enhanced context.
- Uses `winston` for flexible log formatting and transport.
- Provides a service factory for easy integration with Backstage plugins.
- Supports configurable log transports (console, file).
## How it Works
The Auditor Service defines a core class, `Auditor`, which implements the `AuditorService` interface. This class uses `winston` to log audit events with varying levels of severity and associated metadata. It also integrates with authentication and plugin services to capture actor details and plugin context.
The `auditorServiceFactory` creates an `Auditor` instance for the root context and provides a factory function for creating child loggers for individual plugins. This allows each plugin to have its own logger with inherited and additional metadata.
## Usage Guidance
The Auditor Service is designed for recording security-relevant events that require special attention or are subject to compliance regulations. These events often involve actions like:
- User session management
- Data access and modification
- System configuration changes
For general application logging that is not security-critical, you should use the standard `LoggerService` provided by Backstage. This helps to keep your audit logs focused and relevant.
## Using the Service
The Auditor Service can be accessed via dependency injection in your Backstage plugin. Here's an example of how to access the service and create an audit event within an Express route handler:
```typescript
export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
const { auditor } = options;
const router = Router();
router.use(express.json());
router.post('/my-endpoint', async (req, res) => {
const auditorEvent = await auditor.createEvent({
eventId: 'my-endpoint-call',
request: req,
meta: {
// ... metadata about the request
},
});
try {
// ... process the request
await auditorEvent.success();
res.status(200).json({ message: 'Succeeded!' });
} catch (error) {
await auditorEvent.fail({ error });
res.status(500).json({ message: 'Failed!' });
throw error;
}
});
return router;
}
```
In this example, an audit event is created for each request to `/my-endpoint`. The `success` or `fail` methods are called based on the outcome of processing the request.
## Naming Conventions
When defining `eventId` and `subEventId` for your audit events, follow these guidelines:
- Use kebab-case (e.g., `user-login`, `file-download`, `fetch`, `entity-create`, `entity-update`).
- The `eventId` represents a logical group of similar events or operations. For example, "fetch" could be used as an `eventId` encompassing various fetch methods like `by-id` or `by-location`.
- Use `subEventId` to further categorize events within a logical group. For example, if the `eventId` is "fetch", the `subEventId` could be "by-id" or "by-location" to specify the method used for fetching.
- Avoid redundant prefixes related to the plugin ID, as that context is already provided.
- Choose names that clearly and concisely describe the event being audited.
@@ -0,0 +1,80 @@
---
id: permissions
title: Permissions Registry Service
sidebar_label: Permissions Registry
description: Documentation for the Permissions Registry service
---
This service allows your plugins to register new permissions, rules, and resource types and integrate with [the permissions framework](../../permissions/overview.md).
## Using the service
For a deep dive into how to use the `permissionsRegistry` service, see the [permission guide for plugin authors](../../permissions/plugin-authors/01-setup.md).
If all you want to do is add new custom permission rules to an existing plugin, you can instead refer to the [custom permission rules guide](../../permissions/custom-rules.md).
## Migrating from `createPermissionIntegrationRouter`
Before this service was introduced, plugins would use
`createPermissionIntegrationRouter` to implement the same functionality. To
migrate a plugin, locate the `createPermissionIntegrationRouter` call for your
router and remove it, but copy all options that are passed to it, for example:
```ts
export async function createRouter() {
const router = Router();
/* highlight-remove-start */
const permissionIntegrationRouter = createPermissionIntegrationRouter({
resourceType: RESOURCE_TYPE_MY_RESOURCE,
permissions: [myResourcePermissions],
rules: [myResourceRule],
});
router.use(permissionIntegrationRouter);
/* highlight-remove-end */
// ...
}
```
Next, add a dependency on the `PermissionsRegistryService` to your plugin,
and pass it the same options:
```ts
export const examplePlugin = createBackendPlugin({
pluginId: 'example',
register(env) {
env.registerInit({
deps: {
logger: coreServices.logger,
/* highlight-add-next-line */
permissionsRegistry: coreServices.permissionsRegistry,
},
/* highlight-remove-next-line */
async init({ logger }) {
/* highlight-add-next-line */
async init({ logger, permissionsRegistry }) {
logger.log('This is a silly example plugin with no functionality');
/* highlight-add-start */
permissionsRegistry.addResourceType({
resourceType: RESOURCE_TYPE_MY_RESOURCE,
permissions: [myResourcePermissions],
rules: [myResourceRule],
});
/* highlight-add-end */
},
});
},
});
```
If you only passed the `permissions` option to
`createPermissionIntegrationRouter`, you will want to use
`permissionsRegistry.addPermissions` instead.
If you passed multiple resources types to `createPermissionIntegrationRouter`
via the `resources` option, you will want to call
`permissionsRegistry.addResourceType` multiple times for each of those
resource types.
+1 -1
View File
@@ -61,7 +61,7 @@ More details are provided in dedicated sections of the documentation.
- [Reading Configuration](./reading.md): How to read configuration in your
plugin.
- [Writing Configuration](./writing.md): How to provide configuration for your
- [Writing Configuration](./writing.md): How to write configuration for your
Backstage deployment.
- [Defining Configuration](./defining.md): How to define a configuration schema
for users of your plugin or package.
+4 -1
View File
@@ -239,4 +239,7 @@ privateKey: |
-----END RSA PRIVATE KEY-----
```
**Warning: Sensitive information, such as private keys, should not be hard coded**. We recommend that this entire file should be a secret and stored as such in a secure storage solution like Vault, to ensure they are neither exposed nor misused. This example key part only shows the format on how to use the yaml | syntax to make sure that the key is valid.
> [!WARNING]
> Sensitive information, such as private keys, should not be hard coded.
We recommend that this entire file should be a secret and stored as such in a secure storage solution like Vault, to ensure they are neither exposed nor misused. This example key part only shows the format on how to use the yaml | syntax to make sure that the key is valid.
-3
View File
@@ -221,9 +221,6 @@ USER node
WORKDIR /app
COPY --from=packages --chown=node:node /app .
COPY --from=packages --chown=node:node /app/.yarn ./.yarn
COPY --from=packages --chown=node:node /app/.yarnrc.yml ./
COPY --from=packages --chown=node:node /app/backstage.json ./
RUN --mount=type=cache,target=/home/node/.cache/yarn,sharing=locked,uid=1000,gid=1000 \
yarn install --immutable
+1 -1
View File
@@ -169,7 +169,7 @@ Kubernetes definitions in a single file and apply them at the same time.
Note the volume `type: local`; this creates a volume using local disk on
Kubernetes nodes. More likely in a production scenario, you'd want to use a more
highly available
[type of PersistentVolume](https://kubernetes.io/docs/concepts/storage/persistent-volumes/#types-of-persistent-volumes).
[types of PersistentVolume](https://kubernetes.io/docs/concepts/storage/persistent-volumes/#types-of-persistent-volumes).
Apply the storage volume and claim to the Kubernetes cluster:
+16 -16
View File
@@ -578,23 +578,22 @@ view the Kubernetes API docs for your Kubernetes version (e.g.
Overrides for the Kubernetes object types fetched from the cluster. The default object types are:
- pods
- services
- configmaps
- limitranges
- resourcequotas
- deployments
- replicasets
- horizontalpodautoscalers
- jobs
- cronjobs
- ingresses
- statefulsets
- daemonsets
- `pods`
- `services`
- `configmaps`
- `limitranges`
- `resourcequotas`
- `deployments`
- `replicasets`
- `horizontalpodautoscalers`
- `jobs`
- `cronjobs`
- `ingresses`
- `statefulsets`
- `daemonsets`
You may use this config to override the default object types if you only want a subset of
the default ones. However, it's currently not supported to fetch object types other
than the ones specified in the default types.
You may use this config to override the default object types if you only want specific ones.
However, the only additional object type to fetch at the moment is `secrets`.
Example:
@@ -608,6 +607,7 @@ kubernetes:
- pods
- services
- statefulsets
- secrets
```
### Role Based Access Control
+5 -1
View File
@@ -214,7 +214,11 @@ if `prevCursor` exists, it can be used to retrieve the previous batch of entitie
Lists entities.
**NOTE**: This endpoint is deprecated in favor of `GET /entities/by-query`, which provides a more efficient implementation and cursor based pagination.
:::note Note
This endpoint is deprecated in favor of `GET /entities/by-query`, which provides a more efficient implementation and cursor based pagination.
:::
The endpoint supports the following query parameters, described in sections
below:
@@ -375,7 +375,9 @@ Fields of a link are:
| `icon` | String | [Optional] A key representing a visual icon to be displayed in the UI. |
| `type` | String | [Optional] An optional value to categorize links into specific groups. |
_NOTE_: The `icon` field value is meant to be a semantic key that will map to a
:::note Note
The `icon` field value is meant to be a semantic key that will map to a
specific icon that may be provided by an icon library (e.g. `material-ui`
icons). These keys should be a sequence of `[a-z0-9A-Z]`, possibly separated by
one of `[-_.]`. Backstage may support some basic icons out of the box such as those [defined in app-defaults](https://github.com/backstage/backstage/blob/master/packages/app-defaults/src/defaults/icons.tsx), but the
@@ -383,6 +385,8 @@ Backstage integrator will ultimately be left to provide the appropriate icon
component mappings. A generic fallback icon would be provided if a mapping
cannot be resolved.
:::
The semantics of the `type` field are undefined. The adopter is free to define their own set of types and utilize them as they wish. Some potential use cases can be to utilize the type field to validate certain links exist on entities or to create customized UI components for specific link types.
## Common to All Kinds: Relations
+7 -1
View File
@@ -36,7 +36,13 @@ additional encoding:
The name is always required. Depending on the context, you may be able to leave
out the kind and/or namespace. If you do, it is contextual what values will be
used, and the relevant documentation should specify which rule applies where.
All strings are case insensitive.
Entity ref strings are frequently passed between systems as identifiers of
entities. In those cases the refs should always be complete (have all three
parts). The sender should ensure that the refs are always lowercased in an
`en-US` locale, preferably by using [the `stringifyEntityRef` function](https://backstage.io/docs/reference/catalog-model.stringifyentityref/)
which does this automatically. The receiver should treat incoming refs case
insensitively to avoid problems with senders who do not obey this rule.
```yaml
# Example:
@@ -220,7 +220,7 @@ const scaffolderModuleCustomExtensions = createBackendModule({
async init({ scaffolder /* ..., other dependencies */ }) {
// Here you have the opportunity to interact with the extension
// point before the plugin itself gets instantiated
scaffolder.addActions(new createNewFileAction()); // just an example
scaffolder.addActions(createNewFileAction()); // just an example
},
});
},
@@ -263,14 +263,17 @@ Idempotent action could be achieved via the usage of checkpoints.
Example:
```ts title="plugins/my-company-scaffolder-actions-plugin/src/vendor/my-custom-action.ts"
const res = await ctx.checkpoint?.('create.projects', async () => {
const projectStgId = createStagingProjectId();
const projectProId = createProductionProjectId();
const res = await ctx.checkpoint?.({
key: 'create.projects',
fn: async () => {
const projectStgId = createStagingProjectId();
const projectProId = createProductionProjectId();
return {
projectStgId,
projectProId,
};
return {
projectStgId,
projectProId,
};
},
});
```
@@ -150,6 +150,32 @@ const routes = (
);
```
### Async Validation Function
A validation function can be asyncronous and use [Utility APIs](https://backstage.io/docs/api/utility-apis/) via the `ApiHolder` in the [field validation context](https://backstage.io/docs/reference/plugin-scaffolder-react.customfieldvalidator). The example below uses the `catalogApiRef` to check if the submitted value (in this scenario an entity ref) exists in the catalog.
```tsx
import { FieldValidation } from '@rjsf/utils';
import { ApiHolder } from '@backstage/core-plugin-api';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
/*
This validation function checks if the submitted entity ref value is present in the catalog.
*/
export const customFieldExtensionValidator = async (
value: string,
validation: FieldValidation,
context: { apiHolder: ApiHolder },
) => {
const catalogApi = context.apiHolder.get(catalogApiRef);
if ((await catalogApi?.getEntityByRef(value)) === undefined) {
validation.addError('Entity not found');
}
};
```
## Using the Custom Field Extension
Once it's been passed to the `ScaffolderPage` you should now be able to use the
+1
View File
@@ -197,6 +197,7 @@ Options:
--awsS3sse <AWS SSE> Optional AWS S3 Server Side Encryption.
--awsS3ForcePathStyle Optional AWS S3 option to force path style.
--awsBucketRootPath <AWS BUCKET ROOT PATH> Optional sub-directory to store files in Amazon S3
--awsMaxAttempts <AWS MAX ATTEMPTS> Optional maximum number of retries for AWS S3 operations. If not specified, default value of 3 is used.
--osCredentialId <OPENSTACK SWIFT APPLICATION CREDENTIAL ID> (Required for OpenStack) specify when --publisher-type openStackSwift
--osSecret <OPENSTACK SWIFT APPLICATION CREDENTIAL SECRET> (Required for OpenStack) specify when --publisher-type openStackSwift
--osAuthUrl <OPENSTACK SWIFT AUTHURL> (Required for OpenStack) specify when --publisher-type openStackSwift
+41 -2
View File
@@ -137,6 +137,10 @@ Modify your `App.tsx` as follows:
import { TechDocsCustomHome } from '@backstage/plugin-techdocs';
//...
const options = { emptyRowsWhenPaging: false };
const linkDestination = (entity: Entity): string | undefined => {
return entity.metadata.annotations?.['external-docs'];
};
const techDocsTabsConfig = [
{
label: 'Recommended Documentation',
@@ -145,18 +149,53 @@ const techDocsTabsConfig = [
title: 'Golden Path',
description: 'Documentation about standards to follow',
panelType: 'DocsCardGrid',
panelProps: { CustomHeader: () => <ContentHeader title='Golden Path'/> },
filterPredicate: entity =>
entity?.metadata?.tags?.includes('golden-path') ?? false,
},
{
title: 'Recommended',
description: 'Useful documentation',
panelType: 'InfoCardGrid',
panelProps: {
CustomHeader: () => <ContentHeader title='Recommended' />
linkDestination: linkDestination,
},
filterPredicate: entity =>
entity?.metadata?.tags?.includes('recommended') ?? false,
},
],
},
{
label: 'Browse All',
panels: [
{
description: 'Browse all docs',
filterPredicate: filterEntity,
panelType: 'TechDocsIndexPage',
title: 'All',
panelProps: { PageWrapper: React.Fragment, CustomHeader: React.Fragment, options: options },
},
],
},
];
const docsFilter = {
kind: ['Location', 'Resource', 'Component'],
'metadata.annotations.featured-docs': CATALOG_FILTER_EXISTS,
}
const customPageWrapper = ({ children }: React.PropsWithChildren<{}>) =>
(<PageWithHeader title="Docs" themeId="documentation">{children}</PageWithHeader>)
const AppRoutes = () => {
<FlatRoutes>
<Route
path="/docs"
element={<TechDocsCustomHome tabsConfig={techDocsTabsConfig} />}
element={
<TechDocsCustomHome
tabsConfig={techDocsTabsConfig}
filter={docsFilter}
CustomPageWrapper={customPageWrapper}
/>
}
/>
</FlatRoutes>;
};
@@ -141,9 +141,9 @@ down the number of duplicate packages.
## Proxy
The Backstage CLI uses [global-agent](https://www.npmjs.com/package/global-agent) to configure HTTP/HTTPS proxy settings using environment variables. This allows you to route the CLIs network traffic through a proxy server, which can be useful in environments with restricted internet access.
The Backstage CLI uses [global-agent](https://www.npmjs.com/package/global-agent) and `undici` to configure HTTP/HTTPS proxy settings using environment variables. This allows you to route the CLIs network traffic through a proxy server, which can be useful in environments with restricted internet access.
Additionally, yarn needs a proxy too (sometimes), when in environments with restricted internet access. It uses different settings than the global-agent module. If you decide to use the backstage yarn plugin [mentioned above](#plugin), you will need to set additional proxy values.
Additionally, yarn needs a proxy too (sometimes), when in environments with restricted internet access. It uses different settings than the other modules. If you decide to use the backstage yarn plugin [mentioned above](#plugin), you will need to set additional proxy values.
If you will always need proxy settings in all environments and situations, you can add `httpProxy` and `httpsProxy` values to [the yarnrc.yml file](https://yarnpkg.com/configuration/yarnrc). If some environments need it (say a developer workstation) but other environments do not (perhaps a CI build server running on AWS), then you may not want to update the yarnrc.yml file but just set environment variables `YARN_HTTP_PROXY` and `YARN_HTTPS_PROXY` in the environments/situations where you need to proxy.
**If you plan to use the backstage yarn plugin, you will need these extra yarn proxy settings to both install the plugin and run the `versions:bump` command**. If you do not plan to use the backstage yarn plugin, it seems like the global agent proxy settings alone are sufficient.
@@ -151,9 +151,12 @@ If you will always need proxy settings in all environments and situations, you c
### Example Configuration
```bash
export GLOBAL_AGENT_HTTP_PROXY=http://proxy.company.com:8080
export GLOBAL_AGENT_HTTPS_PROXY=https://secure-proxy.company.com:8080
export GLOBAL_AGENT_NO_PROXY=localhost,internal.company.com
export YARN_HTTP_PROXY=http://proxy.company.com:8080 # optional
export YARN_HTTPS_PROXY=https://secure-proxy.company.com:8080 # optional
export HTTP_PROXY=http://proxy.company.com:8080
export HTTPS_PROXY=https://secure-proxy.company.com:8080
export NO_PROXY=localhost,internal.company.com
export GLOBAL_AGENT_HTTP_PROXY=${HTTP_PROXY}
export GLOBAL_AGENT_HTTPS_PROXY=${HTTPS_PROXY}
export GLOBAL_AGENT_NO_PROXY=${NO_PROXY}
export YARN_HTTP_PROXY=${HTTP_PROXY} # optional
export YARN_HTTPS_PROXY=${HTTPS_PROXY} # optional
```
+159 -23
View File
@@ -258,67 +258,203 @@ The `myUserTransformer`, `myGroupTransformer`, `myOrganizationTransformer`, and
The following provides an example of each kind of transformer. We recommend creating a `transformers.ts` file in your `packages/backend/src` folder for these.
```ts title="packages/backend/src/transformers.ts"
First, lets set up the basic structure of the file, with functions for each kind of transformer that simply passes through the default transformer unchanged.
```ts title="packages/backend/src/extensions/transformers.ts"
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
import {
defaultGroupTransformer,
defaultUserTransformer,
defaultOrganizationTransformer,
microsoftGraphOrgEntityProviderTransformExtensionPoint,
MicrosoftGraphProviderConfig,
} from '@backstage/plugin-catalog-backend-module-msgraph';
import { GroupEntity, UserEntity } from '@backstage/catalog-model';
import { createBackendModule } from '@backstage/backend-plugin-api';
// This group transformer completely replaces the built in logic with custom logic.
// The Group transformer transforms Groups that are ingested from MS Graph
export async function myGroupTransformer(
group: MicrosoftGraph.Group,
groupPhoto?: string,
): Promise<GroupEntity | undefined> {
return {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: {
name: group.id!,
annotations: {},
},
spec: {
type: 'Microsoft Entra ID',
children: [],
},
};
const backstageGroup = await defaultGroupTransformer(group, groupPhoto);
return backstageGroup;
}
// This user transformer makes use of the built in logic, but also sets the description field
// The User transformer transforms Users that are ingested from MS Graph
export async function myUserTransformer(
graphUser: MicrosoftGraph.User,
userPhoto?: string,
): Promise<UserEntity | undefined> {
const backstageUser = await defaultUserTransformer(graphUser, userPhoto);
if (backstageUser) {
backstageUser.metadata.description = 'Loaded from Microsoft Entra ID';
}
return backstageUser;
}
// Example organization transformer that removes the organization group completely
// The Organization transformer transforms the root MS Graph Organization into a Group
export async function myOrganizationTransformer(
graphOrganization: MicrosoftGraph.Organization,
): Promise<GroupEntity | undefined> {
return undefined;
const backstageOrg = await defaultOrganizationTransformer(graphOrganization);
return backstageOrg;
}
// Example config transformer that expands the group filter to also include 'azure-group-a'
// The Provider Config transformer enables modification of the plugin config
export async function myProviderConfigTransformer(
provider: MicrosoftGraphProviderConfig,
): Promise<MicrosoftGraphProviderConfig> {
return provider;
}
// Wrapping these functions in a Module allows us to inject them into the Catalog plugin easily
export default createBackendModule({
pluginId: 'catalog',
moduleId: 'msgraph-org',
register(reg) {
reg.registerInit({
deps: {
microsoftGraphTransformers:
microsoftGraphOrgEntityProviderTransformExtensionPoint,
},
async init({ microsoftGraphTransformers }) {
// Set the transformers to our custom functions
microsoftGraphTransformers.setUserTransformer(myUserTransformer);
microsoftGraphTransformers.setGroupTransformer(myGroupTransformer);
microsoftGraphTransformers.setOrganizationTransformer(
myOrganizationTransformer,
);
microsoftGraphTransformers.setProviderConfigTransformer(
myProviderConfigTransformer,
);
},
});
},
});
```
Now lets customize each of the providers to suit our needs.
This Group Transformer example will have the default logic completely removed and replaced with our custom logic:
```ts
export async function myGroupTransformer(
group: MicrosoftGraph.Group,
groupPhoto?: string,
): Promise<GroupEntity | undefined> {
// highlight-remove-start
const backstageGroup = await defaultGroupTransformer(group, groupPhoto);
return backstageGroup;
// highlight-remove-end
// highlight-add-start
// All of our groups are prefixed with the organisational unit: 'Engineering - Team A'
// We want to drop the org unit from the group name and use it for the namespace instead
const groupNameArr = group.displayName.split(' - ');
const displayName = groupNameArr[1];
// Standardise name and namespace by replacing spaces with hyphens and converting to lowercase
const namespace = groupNameArr[0].replace(' ', '-').toLowerCase();
const groupName = groupNameArr[1].replace(' ', '-').toLowerCase();
return {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: {
name: groupName,
description: group.description,
annotations: {},
},
spec: {
type: 'team',
displayName: displayName,
email: group.mail,
children: [],
},
};
// highlight-add-end
}
```
This User Transformer example makes use of the built-in logic, but also modifies the username and sets a description
```ts
export async function myUserTransformer(
graphUser: MicrosoftGraph.User,
userPhoto?: string,
): Promise<UserEntity | undefined> {
const backstageUser = await defaultUserTransformer(graphUser, userPhoto);
// highlight-add-start
// Make sure the default transformer returned an entity
if (backstageUser) {
// Update the description to make it obvious where this entity came from
backstageUser.metadata.description =
'Loaded from Microsoft Entra ID via MyCustomUserTransformer';
// The default transformer sets the username to the email address with invalid characters subbed out: 'user_domain.com'
// Set the username to the local part of the email address in lowercase without the domain
const newName = backstageUser.metadata.name.split('_')[0].toLowerCase();
backstageUser.metadata.name = newName;
return backstageUser;
}
return undefined;
// highlight-add-end
// highlight-remove-start
return backstageUser;
// highlight-remove-end
}
```
This Organization Transformer example removes the organization group completely by returning undefined
```ts
export async function myOrganizationTransformer(
graphOrganization: MicrosoftGraph.Organization,
): Promise<GroupEntity | undefined> {
// highlight-remove-start
const backstageOrg = await defaultOrganizationTransformer(graphOrganization);
return backstageOrg;
// highlight-remove-end
// highlight-add-start
// The org transformer creates a group to be used as the base of the relationship tree for groups
// We don't need this to be created, so return undefined instead of an entity
return undefined;
// highlight-add-end
}
```
This Config Transformer example expands the group filter to also include 'azure-group-a'
```ts
export async function myProviderConfigTransformer(
provider: MicrosoftGraphProviderConfig,
): Promise<MicrosoftGraphProviderConfig> {
// highlight-add-start
// The filter in our config file relies on a property that has been intermittantly causing this important group to fail ingestion
// Ensure the group is always discovered by the filter
if (!provider.groupFilter?.includes('azure-group-a')) {
provider.groupFilter = `${provider.groupFilter} or displayName eq 'azure-group-a'`;
}
// highlight-add-end
return provider;
}
```
Now we just need to add our new module to the Backend.
```ts title="packages/backend/src/index.ts"
// Your file will have more than this in it
const backend = createBackend();
...
// highlight-add-start
backend.add(import('./extensions/transformers'));
// highlight-add-end
...
backend.start();
```
## Troubleshooting
### No data
+1 -1
View File
@@ -97,7 +97,7 @@ For more information, see [the docs](./generate-client.md).
Add the following lines to your `createRouter.test.ts` or `router.test.ts` file,
```diff
+ import { wrapInOpenApiTestServer } from '@backstage/backend-openapi-utils';
+ import { wrapInOpenApiTestServer } from '@backstage/backend-openapi-utils/testUtils';
+ import { Server } from 'http';
...
+1 -1
View File
@@ -108,7 +108,7 @@ The following versioning policy applies to all packages:
- Breaking changes are noted in the changelog, and documentation is updated.
- Breaking changes are prefixed with `**BREAKING**: ` in the changelog.
- All public exports are considered stable and will have an entry in the
changelog
changelog.
- Breaking changes are recommended to document a clear upgrade path in the
changelog. This may be omitted for newly introduced or unstable packages.
+12 -6
View File
@@ -139,7 +139,13 @@ class CustomPermissionPolicy implements PermissionPolicy {
Now that we have a custom rule defined and added to our policy, we need provide it to the catalog plugin. This step is important because the catalog plugin will use the rule's `toQuery` and `apply` methods while evaluating conditional authorize results. There's no guarantee that the catalog and permission backends are running on the same server, so we must explicitly link the rule to ensure that it's available at runtime.
The api for providing custom rules may differ between plugins, but there should typically be an [extension point](../backend-system/architecture/05-extension-points.md) that you can use in your created module to add your rule. For the catalog, this extension point is exposed via `catalogPermissionExtensionPoint`. Here's the steps you'll need to take to add the `isInSystemRule` we created above to the catalog:
:::warning Warning
The `PermissionsRegistryService` is a fairly new addition and not yet supported by all plugins as they might still be using the old `createPermissionIntegrationRouter` that cannot be extended. If you encounter errors when installing custom rules for a plugin, the plugin may need to be switched to using the `PermissionsRegistryService` first.
:::
To install custom rules in a plugin, we need to use the [`PermissionsRegistryService`](../backend-system/core-services/permissionsRegistry.md). Here's the steps you'll need to take to add the `isInSystemRule` we created above to the catalog:
1. We will be using the `@backstage/plugin-catalog-node` package as it contains the extension point we need. Run this to add it:
@@ -147,10 +153,10 @@ The api for providing custom rules may differ between plugins, but there should
yarn --cwd packages/backend add @backstage/plugin-catalog-node
```
2. Next create a `catalogPermissionRules.ts` file in the `packages/backend/src/extensions` folder.
2. Next create a `catalogPermissionRules.ts` file in the `packages/backend/src/modules` folder.
3. Then add this as the contents of the new `catalogPermissionRules.ts` file:
```typescript title="packages/backend/src/extensions/catalogPermissionRules.ts"
```typescript title="packages/backend/src/modules/catalogPermissionRules.ts"
import { createBackendModule } from '@backstage/backend-plugin-api';
import { catalogPermissionExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
import { isInSystemRule } from './permissionPolicyExtension';
@@ -160,9 +166,9 @@ The api for providing custom rules may differ between plugins, but there should
moduleId: 'permission-rules',
register(reg) {
reg.registerInit({
deps: { catalog: catalogPermissionExtensionPoint },
async init({ catalog }) {
catalog.addPermissionRules(isInSystemRule);
deps: { permissionsRegistry: coreServices.permissionsRegistry },
async init({ permissionsRegistry }) {
permissionsRegistry.addPermissionRules([isInSystemRule]);
},
});
},
@@ -67,8 +67,6 @@ import { LoggerService, HttpAuthService } from '@backstage/backend-plugin-api';
import { InputError, NotAllowedError } from '@backstage/errors';
import { LoggerService, HttpAuthService, PermissionsService } from '@backstage/backend-plugin-api';
import { AuthorizeResult } from '@backstage/plugin-permission-common';
import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
/* highlight-add-end */
export interface RouterOptions {
@@ -86,12 +84,6 @@ export async function createRouter(
/* highlight-add-next-line */
const { logger, httpAuth, permissions } = options;
/* highlight-add-start */
const permissionIntegrationRouter = createPermissionIntegrationRouter({
permissions: [todoListCreatePermission],
});
/* highlight-add-end */
const router = Router();
router.use(express.json());
@@ -100,9 +92,6 @@ export async function createRouter(
response.json({ status: 'ok' });
});
/* highlight-add-next-line */
router.use(permissionIntegrationRouter);
router.get('/todos', async (_req, res) => {
res.json(getAll());
});
@@ -137,11 +126,13 @@ export async function createRouter(
// ...
```
Pass the `permissions` object to the plugin in `plugins/todo-list-backend/src/plugin.ts`:
Pass the `permissions` service and register the new permission to the plugin in `plugins/todo-list-backend/src/plugin.ts`:
```ts title="plugins/todo-list-backend/src/plugin.ts"
import { coreServices, createBackendPlugin } from '@backstage/backend-plugin-api';
import { createRouter } from './service/router';
/* highlight-add-next-line */
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
export const exampleTodoListPlugin = createBackendPlugin({
pluginId: 'todolist',
@@ -153,11 +144,16 @@ export const exampleTodoListPlugin = createBackendPlugin({
httpRouter: coreServices.httpRouter,
/* highlight-add-next-line */
permissions: coreServices.permissions,
/* highlight-add-next-line */
permissionsRegistry: coreServices.permissionsRegistry,
},
/* highlight-remove-next-line */
async init({ logger, httpAuth, httpRouter }) {
/* highlight-add-next-line */
async init({ logger, httpAuth, httpRouter, permissions }) {
async init({ httpAuth, logger, httpRouter, permissions, permissionsRegistry }) {
/* highlight-add-next-line */
permissionsRegistry.addPermissions([todoListCreatePermission]);
httpRouter.use(
await createRouter({
logger,
@@ -47,9 +47,9 @@ Notice that unlike `todoListCreatePermission`, the `todoListUpdatePermission` pe
## 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:
To start, let's edit `plugins/todo-list-backend/src/plugin.ts` to add the new permission to our plugin:
```ts title="plugins/todo-list-backend/src/service/router.ts"
```ts title="plugins/todo-list-backend/src/plugin.ts"
/* highlight-remove-next-line */
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
/* highlight-add-start */
@@ -61,14 +61,29 @@ import {
// ...
const permissionIntegrationRouter = createPermissionIntegrationRouter({
/* highlight-remove-next-line */
permissions: [todoListCreatePermission],
/* highlight-add-next-line */
permissions: [todoListCreatePermission, todoListUpdatePermission],
});
/* highlight-remove-next-line */
permissionsRegistry.addPermissions([todoListCreatePermission]);
/* highlight-add-start */
permissionsRegistry.addPermissions([
todoListCreatePermission,
todoListUpdatePermission,
]);
/* highlight-add-end */
// ...
```
Then let's edit `plugins/todo-list-backend/src/service/router.ts` in the same manner as we did in the previous section:
```ts title="plugins/todo-list-backend/src/service/router.ts"
/* highlight-remove-next-line */
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
/* highlight-add-start */
import {
todoListCreatePermission,
todoListUpdatePermission,
} from '@internal/plugin-todo-list-common';
/* highlight-add-end */
router.put('/todos', async (req, res) => {
/* highlight-add-start */
@@ -155,49 +170,50 @@ Specifically, the `apply` function is used to understand whether the passed reso
Let's skip the `toQuery` function for now, we'll come back to that in the next section.
Now, let's create the new endpoint by editing `plugins/todo-list-backend/src/service/router.ts`. This uses the `createPermissionIntegrationRouter` helper to add the APIs needed by the permission framework to your plugin. You'll need to supply:
Now, let's add the new resource type to the permissions system via the
`PermissionsRegistryService`. You'll need to supply:
- `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.
```ts title="plugins/todo-list-backend/src/service/router.ts"
```ts title="plugins/todo-list-backend/src/plugin.ts"
// ...
import {
coreServices,
createBackendPlugin,
} from '@backstage/backend-plugin-api';
import { createRouter } from './service/router';
import {
/* highlight-add-next-line */
TODO_LIST_RESOURCE_TYPE,
todoListCreatePermission,
todoListUpdatePermission,
} from '@internal/plugin-todo-list-common';
/* highlight-remove-next-line */
import { add, getAll, update } from './todos';
/* highlight-add-start */
import { add, getAll, getTodo, update } from './todos';
import { getTodo } from './todos';
import { rules } from './rules';
/* highlight-add-end */
export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
const { logger, identity, permissions } = options;
// ...
const permissionIntegrationRouter = createPermissionIntegrationRouter({
permissions: [todoListCreatePermission, todoListUpdatePermission],
/* highlight-add-start */
getResources: async resourceRefs => {
return resourceRefs.map(getTodo);
},
resourceType: TODO_LIST_RESOURCE_TYPE,
rules: Object.values(rules),
/* highlight-add-end */
});
const router = Router();
router.use(express.json());
// ...
}
/* highlight-remove-start */
permissionsRegistry.addPermissions([
todoListCreatePermission,
todoListUpdatePermission,
]);
/* highlight-remove-end */
/* highlight-add-start */
permissionsRegistry.addResourceType({
resourceType: TODO_LIST_RESOURCE_TYPE,
permissions: [todoListCreatePermission, todoListUpdatePermission],
rules: Object.values(rules),
getResources: async resourceRefs => {
return Promise.all(resourceRefs.map(getTodo));
},
});
/* highlight-add-end */
```
## Provide utilities for policy authors
@@ -4,7 +4,9 @@ title: 4. Authorizing access to paginated data
description: Explains how to authorize access to paginated data in a Backstage plugin
---
:::info
This documentation is written for [the new backend system](../../backend-system/index.md) which is the default since Backstage [version 1.24](../../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](./04-authorizing-access-to-paginated-data--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)!
:::
Authorizing `GET /todos` is similar to the update endpoint, in that it should be possible to authorize access based on the characteristics of each resource. However, we'll need to authorize a list of resources for this endpoint.
@@ -84,14 +86,41 @@ export const todoListPermissions = [
## Using conditional policy decisions
As usual, we'll start by updating the permission integration to include the new permission:
```ts title="plugins/todo-list-backend/src/plugin.ts"
import {
TODO_LIST_RESOURCE_TYPE,
todoListCreatePermission,
todoListUpdatePermission,
/* highlight-add-next-line */
todoListReadPermission,
} from '@internal/plugin-todo-list-common';
// ...
permissionsRegistry.addResourceType({
resourceType: TODO_LIST_RESOURCE_TYPE,
/* highlight-remove-next-line */
permissions: [todoListCreatePermission, todoListUpdatePermission],
/* highlight-add-next-line */
permissions: [
todoListCreatePermission,
todoListUpdatePermission,
todoListReadPermission,
],
rules: Object.values(rules),
getResources: async resourceRefs => {
return Promise.all(resourceRefs.map(getTodo));
},
});
```
So far we've only used the `PermissionsService.authorize` method, which will evaluate conditional decisions before returning a result. In this step, we want to evaluate conditional decisions within our plugin, so we'll use `PermissionsService.authorizeConditional` instead.
```ts title="plugins/todo-list-backend/src/service/router.ts"
/* highlight-remove-next-line */
import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';
/* highlight-add-start */
import {
createPermissionIntegrationRouter,
createConditionTransformer,
ConditionTransformer,
} from '@backstage/plugin-permission-node';
@@ -101,7 +130,6 @@ import { add, getAll, getTodo, update } from './todos';
/* highlight-add-next-line */
import { add, getAll, getTodo, TodoFilter, update } from './todos';
import {
TODO_LIST_RESOURCE_TYPE,
todoListCreatePermission,
todoListUpdatePermission,
/* highlight-add-next-line */
@@ -110,20 +138,6 @@ import {
// ...
const permissionIntegrationRouter = createPermissionIntegrationRouter({
/* highlight-remove-next-line */
permissions: [todoListCreatePermission, todoListUpdatePermission],
/* highlight-add-next-line */
permissions: [todoListCreatePermission, todoListUpdatePermission, todoListReadPermission],
getResources: async resourceRefs => {
return resourceRefs.map(getTodo);
},
resourceType: TODO_LIST_RESOURCE_TYPE,
rules: Object.values(rules),
});
// ...
/* highlight-add-next-line */
const transformConditions: ConditionTransformer<TodoFilter> = createConditionTransformer(Object.values(rules));
+1 -1
View File
@@ -11,7 +11,7 @@ Backstage integrator.
## Datadog RUM Events
See how to install Datadog Events in your app
[here](../integrations/datadog-rum/installation.md)
[here](../integrations/datadog-rum/installation.md).
## Logging
+39
View File
@@ -120,3 +120,42 @@ third-parties.
The same logic applies to headers that are sent from the target back to the
frontend.
### Proxy Extension Endpoint
The proxy plugin additionally supports a `proxyExtensionEndpoint` which a proxy
plugin module can utilize in order to programmatically register additional
endpoints, whose payloads are specified exactly as in app-config (described
above). Note that endpoints configured in app-config will always override those
registered in this manner.
Example:
```ts
backend.add(
createBackendModule({
pluginId: 'proxy',
moduleId: 'demo-additional-endpoints',
register: reg => {
reg.registerInit({
deps: {
proxyEndpoints: proxyEndpointsExtensionPoint,
},
init: async ({ proxyEndpoints }) => {
let largerExampleAuth: string = /* exercise for the reader */;
proxyEndpoints.addProxyEndpoints({
"/simple-example": "http://simple.example.com:8080",
"/larger-example/v1": {
target: "http://larger.example.com:8080/svc.v1",
credentials: "require",
headers: {
Authorization: largerExampleAuth
},
},
});
},
});
},
}),
);
```
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+73
View File
@@ -0,0 +1,73 @@
---
id: v1.35.0
title: v1.35.0
description: Backstage Release v1.35.0
---
These are the release notes for the v1.35.0 release of [Backstage](https://backstage.io/).
A huge thanks to the whole team of maintainers and contributors as well as the amazing Backstage Community for the hard work in getting this release developed and done.
## Highlights
### **BREAKING**: Reduced support for the old backend system
The following packages have dropped support for the old backend system:
- `@backstage/plugin-signals-backend`
- `@backstage/plugin-search-backend-module-catalog`
See the [migration guide](https://backstage.io/docs/backend-system/building-backends/migrating) for details on how to migrate your backend to use the current backend system.
### GitHub organization ingestion reliability
The GitHub org entity provider will now handle both primary and secondary GitHub rate limits, pausing work when rate limits are encountered. This particularly improves reliability when ingesting large GitHub organizations. Contributed by [@sonikro](https://github.com/sonikro) in [#28085](https://github.com/backstage/backstage/pull/28085)
### Task ID in scaffolder templates
The current task ID is now available in templates via `${{ context.task.id }}`, and in actions `ctx.task.id`. Contributed by [@fabiovincenzi](https://github.com/fabiovincenzi) in [#27592](https://github.com/backstage/backstage/pull/27592)
### Fixed `yarn dev` command when only a single CPU core is reported
Users have been reporting issues with the `yarn dev` command not properly running the `app` and `backend` packages concurrently, instead starting them one after another. This turned out to be an issue on single-core machines where `yarn workspaces foreach` defaults to running one process at a time. This is fixed by adding the `--jobs unlimited` option to the command.
### Removed experimental Vite support
The `EXPERIMENTAL_VITE` flag is no longer supported by the Backstage CLI. If you were using it to improve bundling performance, we recommend switching to `EXPERIMENTAL_RSPACK` instead.
### Proxy endpoint extensions
The proxy plugin now has a new `proxyEndpointsExtensionPoint` that lets you programmatically configure proxy endpoints. Contributed by [@mbenson](https://github.com/mbenson) in [#27257](https://github.com/backstage/backstage/pull/27257)
### Backend config schema workaround
Configuration schema from the `@backstage/backend-common` package will now be ignored if `@backstage/backend-defaults` is present, as it was causing conflicts in the backend configuration schema.
### Plugin backend error middleware
The error handler middleware has been added to the default `HttpRouterService`, this is in addition to the existing error handler middleware for the `RootHttpRouterService`. This change moves error reporting to be done within the context of a plugin, providing additional metadata. If you were using the `MiddlewareFactory` to add your own error handler middleware to the plugin router, this can be removed.
### Improved catalog `/entities` endpoint concurrency
When using the `catalog.disableRelationsCompatibility` flag to enable query streaming mode of the `/entities` endpoint, the endpoint will now fetch the next batch of entities without waiting for the current batch to be written to the response.
## Security Fixes
This release does not contain any security fixes.
## Upgrade path
We recommend that you keep your Backstage project up to date with this latest release. For more guidance on how to upgrade, check out the documentation for [keeping Backstage updated](https://backstage.io/docs/getting-started/keeping-backstage-updated).
## Links and References
Below you can find a list of links and references to help you learn about and start using this new release.
- [Backstage official website](https://backstage.io/), [documentation](https://backstage.io/docs/), and [getting started guide](https://backstage.io/docs/getting-started/)
- [GitHub repository](https://github.com/backstage/backstage)
- Backstage's [versioning and support policy](https://backstage.io/docs/overview/versioning-policy)
- [Community Discord](https://discord.gg/backstage-687207715902193673) for discussions and support
- [Changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.35.0-changelog.md)
- Backstage [Demos](https://backstage.io/demos), [Blog](https://backstage.io/blog), [Roadmap](https://backstage.io/docs/overview/roadmap) and [Plugins](https://backstage.io/plugins)
Sign up for our [newsletter](https://info.backstage.spotify.com/newsletter_subscribe) if you want to be informed about what is happening in the world of Backstage.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+53 -30
View File
@@ -483,29 +483,41 @@ of the build system, including the bundling, tests, builds, and type checking.
Loaders are always selected based on the file extension. The following is a list
of all supported file extensions:
| Extension | Exports | Purpose |
| --------- | ------------- | ------------------ |
| `.ts` | Script Module | TypeScript |
| `.tsx` | Script Module | TypeScript and XML |
| `.js` | Script Module | JavaScript |
| `.jsx` | Script Module | JavaScript and XML |
| `.mjs` | Script Module | ECMAScript Module |
| `.cjs` | Script Module | CommonJS Module |
| `.json` | JSON Data | JSON Data |
| `.yml` | JSON Data | YAML Data |
| `.yaml` | JSON Data | YAML Data |
| `.css` | classes | Style sheet |
| `.eot` | URL Path | Font |
| `.ttf` | URL Path | Font |
| `.woff2` | URL Path | Font |
| `.woff` | URL Path | Font |
| `.bmp` | URL Path | Image |
| `.gif` | URL Path | Image |
| `.jpeg` | URL Path | Image |
| `.jpg` | URL Path | Image |
| `.png` | URL Path | Image |
| `.svg` | URL Path | Image |
| `.md` | URL Path | Markdown File |
| Extension | Exports | Purpose |
| --------- | ------------- | ---------------------------- |
| `.ts` | Script Module | TypeScript |
| `.tsx` | Script Module | TypeScript and XML |
| `.mts` | Script Module | ECMAScript Module TypeScript |
| `.cts` | Script Module | CommonJS TypeScript |
| `.js` | Script Module | JavaScript |
| `.jsx` | Script Module | JavaScript and XML |
| `.mjs` | Script Module | ECMAScript Module |
| `.cjs` | Script Module | CommonJS Module |
| `.json` | JSON Data | JSON Data |
| `.yml` | JSON Data | YAML Data |
| `.yaml` | JSON Data | YAML Data |
| `.css` | classes | Style sheet |
| `.eot` | URL Path | Font |
| `.ttf` | URL Path | Font |
| `.woff2` | URL Path | Font |
| `.woff` | URL Path | Font |
| `.bmp` | URL Path | Image |
| `.gif` | URL Path | Image |
| `.jpeg` | URL Path | Image |
| `.jpg` | URL Path | Image |
| `.png` | URL Path | Image |
| `.svg` | URL Path | Image |
| `.md` | URL Path | Markdown File |
## ECMAScript Modules
The Backstage tooling supports [ECMAScript modules (ESM)](https://nodejs.org/docs/latest-v22.x/api/esm.html) in Node.js packages. This includes support for all the script module file extensions listed above during local development, in built packages, in tests, and during type checking. [Dynamic imports](https://nodejs.org/docs/latest-v22.x/api/esm.html#import-expressions) can be used to load ESM-only packages from CommonJS and vice versa. There are however a couple of limitations to be aware of:
- To enable support for native ESM in tests, you need to run the tests with the `--experimental-vm-modules` flag enabled, typically via `NODE_OPTIONS='--experimental-vm-modules'`.
- Declaring a package as `"type": "module"` in `package.json` is supported, but in tests it will cause all local transitive dependencies to also be treated as ESM, regardless of whether they declare `"type": "module"` or not.
- When running tests with coverage enabled the default `babel` coverage provider can mess with the hoisting of named exports. This can be worked around by using the `v8` provider instead by setting `"coverageProvider": "v8"` in the Jest configuration, although note that the `v8` provider is a fair bit slower than the `babel` one.
- Node.js has an [ESM interoperability layer with CommonJS](https://nodejs.org/docs/latest-v22.x/api/esm.html#interoperability-with-commonjs) that allows for imports from ESM to identify named exports in CommonJS packages. This interoperability layer is **only** enabled when importing packages with a `.cts` or `.cjs` extension. This is because the interoperability layer is not fully compatible with the NPM ecosystem, and would break package if it was enabled for `.js` files.
- Dynamic imports of CommonJS packages will vary in shape depending on the runtime, i.e. test vs local development, etc. It is therefore recommended to avoid dynamic imports of CommonJS packages and instead use `require`, or to use the explicit CommonJS extensions as mentioned above. If you do need to dynamically import CommonJS packages, avoid using `default` exports, as the shape of them vary across different environments and you would otherwise need to manually unwrap the import based on the shape of the module object.
## Jest Configuration
@@ -585,22 +597,33 @@ Caching is used sparingly throughout the Backstage build system. It is always us
For your productivity working with unit tests it's quite essential to have your debugging configured in IDE. It will help you to identify the root cause of the issue faster.
We cannot execute tests with just raw `jest`, because there are a few concerns such as module transforms that need to be in place for the `jest` runtime to be happy.
Therefore, we delegate to the Backstage CLI to wrap the jest run for us, since it knows how to put those things in place.
This aligns things so that your in-IDE test runs work the same way as your CI and manual command line test runs do.
With that in mind, here are some IDEs configurations to run backstage components' `jest` tests with `backstage-cli` in the role of `jest`.
#### IntelliJ IDEA
1. Update Jest configuration template by:
1. Update Jest configuration template by:
- Click on "Edit Configurations" on top panel
- In the modal dialog click on link "Edit configuration templates..." located in the bottom left corner.
- In "Jest package" you have to point to relative path of jest module (it will be suggested by IntelliJ), i.e. `~/proj/backstage/node_modules/jest`
- In "Jest config" point to your jest configuration file, use absolute path for that, i.e. `--config /Users/user/proj/backstage/packages/cli/config/jest.js --runInBand`
1. Click on "Edit Configurations" on top panel
2. In the modal dialog click on link "Edit configuration templates..." located in the bottom left corner.
3. "Configuration file": leave empty (`backstage-cli` adds the config)
4. "Node options": `--no-node-snapshot --experimental-vm-modules`
5. "Jest package": `~/workspace/backstage/node_modules/@backstage/cli` - the location of the backstage cli package.
6. "Working directory": `~/workspace/backstage`
7. "Jest Options": `repo test --runInBand --watch=false`
2. Now you can run any tests by clicking on green arrow located on `describe` or `it`.
2. Currently, intellij has an issue that if you right-click on a jest test and press "run", intellij will create a playwright run configuration instead of jest configuration, see [WEB-67720](https://youtrack.jetbrains.com/issue/WEB-67720/Jest-test-runs-as-playwright-test).
Until intellij maintainers resolve the issue, create a jest configuration manually. Happily, intellij will pre-fill the configuration from the template. The only thing you need to do is provide a path to the test file. Note, that after intellij runs test in the file you can click on the individual tests from the run panel and re-run them, this time intellij will create a correct jest run configuration.
#### VS Code
```jsonc
{
"jest.jestCommandLine": "node_modules/.bin/jest --config node_modules/@backstage/cli/config/jest.js",
"jest.jestCommandLine": "yarn test",
// In a large repo like the Backstage main repo you likely want to disable
// watch mode and the initial test run too, leaving just manual and perhaps
// on-save test runs in place.
@@ -48,9 +48,9 @@ If you are encountering errors related to React, it is likely that the versions
## Generating temporary patches
!!!info
:::info
This feature is experimental and currently only supports Yarn workspaces.
!!!
:::
After making local changes to a package in an external workspace you might often want to merge and deploy these changes in your own project. You can use Yarn patches for this purpose, but it can be quite cumbersome to create these patches manually. To make this process easier, you can use `yarn backstage-repo-tools generate-patch` command from the `@backstage/repo-tools` package to generate a patch and resolution entries in the external workspace.