From 499fa53939fa5e34aeec94b9776b74b92e354489 Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Sun, 13 Apr 2025 17:44:10 -0400 Subject: [PATCH] chore: remove old backend system guides Signed-off-by: aramissennyeydd --- docs/auth/auth0/provider--old.md | 75 ---- docs/auth/auth0/provider.md | 2 +- docs/auth/bitbucketServer/provider--old.md | 60 --- docs/auth/bitbucketServer/provider.md | 2 +- docs/auth/identity-resolver--old.md | 379 ---------------- docs/auth/identity-resolver.md | 2 +- docs/auth/oidc--old.md | 289 ------------ docs/auth/oidc.md | 2 +- docs/auth/service-to-service-auth--old.md | 177 -------- docs/auth/service-to-service-auth.md | 4 +- docs/integrations/aws-s3/discovery--old.md | 88 ---- docs/integrations/aws-s3/discovery.md | 2 +- docs/integrations/azure/discovery--old.md | 186 -------- docs/integrations/azure/discovery.md | 2 +- docs/integrations/azure/org--old.md | 273 ------------ docs/integrations/azure/org.md | 2 +- .../bitbucketServer/discovery--old.md | 123 ----- .../integrations/bitbucketServer/discovery.md | 2 +- docs/integrations/gerrit/discovery--old.md | 73 --- docs/integrations/gerrit/discovery.md | 2 +- docs/integrations/github/discovery--old.md | 375 ---------------- docs/integrations/github/discovery.md | 2 +- docs/integrations/github/org--old.md | 365 --------------- docs/integrations/github/org.md | 2 +- docs/permissions/custom-rules--old.md | 165 ------- docs/permissions/custom-rules.md | 2 +- docs/permissions/getting-started--old.md | 169 ------- docs/permissions/getting-started.md | 2 +- .../plugin-authors/01-setup--old.md | 134 ------ docs/permissions/plugin-authors/01-setup.md | 2 +- ...02-adding-a-basic-permission-check--old.md | 387 ---------------- .../02-adding-a-basic-permission-check.md | 2 +- ...adding-a-resource-permission-check--old.md | 299 ------------- .../03-adding-a-resource-permission-check.md | 2 +- ...thorizing-access-to-paginated-data--old.md | 193 -------- ...04-authorizing-access-to-paginated-data.md | 2 +- docs/permissions/writing-a-policy--old.md | 148 ------ docs/permissions/writing-a-policy.md | 2 +- .../integrating-search-into-plugins--old.md | 421 ------------------ .../integrating-search-into-plugins.md | 2 +- 40 files changed, 21 insertions(+), 4400 deletions(-) delete mode 100644 docs/auth/auth0/provider--old.md delete mode 100644 docs/auth/bitbucketServer/provider--old.md delete mode 100644 docs/auth/identity-resolver--old.md delete mode 100644 docs/auth/oidc--old.md delete mode 100644 docs/auth/service-to-service-auth--old.md delete mode 100644 docs/integrations/aws-s3/discovery--old.md delete mode 100644 docs/integrations/azure/discovery--old.md delete mode 100644 docs/integrations/azure/org--old.md delete mode 100644 docs/integrations/bitbucketServer/discovery--old.md delete mode 100644 docs/integrations/gerrit/discovery--old.md delete mode 100644 docs/integrations/github/discovery--old.md delete mode 100644 docs/integrations/github/org--old.md delete mode 100644 docs/permissions/custom-rules--old.md delete mode 100644 docs/permissions/getting-started--old.md delete mode 100644 docs/permissions/plugin-authors/01-setup--old.md delete mode 100644 docs/permissions/plugin-authors/02-adding-a-basic-permission-check--old.md delete mode 100644 docs/permissions/plugin-authors/03-adding-a-resource-permission-check--old.md delete mode 100644 docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data--old.md delete mode 100644 docs/permissions/writing-a-policy--old.md delete mode 100644 docs/plugins/integrating-search-into-plugins--old.md diff --git a/docs/auth/auth0/provider--old.md b/docs/auth/auth0/provider--old.md deleted file mode 100644 index a089236aca..0000000000 --- a/docs/auth/auth0/provider--old.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -id: provider--old -title: Auth0 Authentication Provider -sidebar_label: Auth0 -description: Adding Auth0 as an authentication provider in Backstage ---- - -:::info -This documentation is written for the old backend which has been replaced by -[the new backend system](../../backend-system/index.md), being the default since -Backstage [version 1.24](../../releases/v1.24.0.md). If have migrated to the new -backend system, you may want to read [its own article](./provider.md) -instead. Otherwise, [consider migrating](../../backend-system/building-backends/08-migrating.md)! -::: - -The Backstage `core-plugin-api` package comes with an Auth0 authentication -provider that can authenticate users using OAuth. - -## Create an Auth0 Application - -1. Log in to the [Auth0 dashboard](https://manage.auth0.com/dashboard/) -2. Navigate to **Applications** -3. Create an Application - - Name: Backstage (or your custom app name) - - Application type: Single Page Web Application -4. Click on the Settings tab -5. Add under `Application URIs` > `Allowed Callback URLs`: - `http://localhost:7007/api/auth/auth0/handler/frame` -6. Click `Save Changes` - -## Configuration - -The provider configuration can then be added to your `app-config.yaml` under the -root `auth` configuration: - -```yaml -auth: - environment: development - providers: - auth0: - development: - clientId: ${AUTH_AUTH0_CLIENT_ID} - clientSecret: ${AUTH_AUTH0_CLIENT_SECRET} - domain: ${AUTH_AUTH0_DOMAIN_ID} - audience: ${AUTH_AUTH0_AUDIENCE} - connection: ${AUTH_AUTH0_CONNECTION} - connectionScope: ${AUTH_AUTH0_CONNECTION_SCOPE} - session: - secret: ${AUTH_SESSION_SECRET} -``` - -The Auth0 provider is a structure with these configuration keys: - -- `clientId`: The Application client ID, found on the Auth0 Application page -- `clientSecret`: The Application client secret, found on the Auth0 Application - page -- `domain`: The Application domain, found on the Auth0 Application page - -It additionally relies on the following configuration to function: - -- `session.secret`: The session secret is a key used for signing and/or encrypting cookies set by the application to maintain session state. In this case, 'your session secret' should be replaced with a long, complex, and unique string that only your application knows. - -Auth0 requires a session, so you need to give the session a secret key. - -## 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 -`SignInPage` component as shown in -[Adding the provider to the sign-in page](../index.md#sign-in-configuration). diff --git a/docs/auth/auth0/provider.md b/docs/auth/auth0/provider.md index 44a5bb0e43..90ac36f07d 100644 --- a/docs/auth/auth0/provider.md +++ b/docs/auth/auth0/provider.md @@ -8,7 +8,7 @@ description: Adding Auth0 as an authentication provider in Backstage :::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](./provider--old.md) +system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/auth/auth0/provider--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! ::: diff --git a/docs/auth/bitbucketServer/provider--old.md b/docs/auth/bitbucketServer/provider--old.md deleted file mode 100644 index fdd8fa35be..0000000000 --- a/docs/auth/bitbucketServer/provider--old.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -id: provider--od -title: Bitbucket Server Authentication Provider -sidebar_label: Bitbucket Server -description: Adding Bitbucket Server OAuth as an authentication provider in Backstage ---- - -:::info -This documentation is written for the old backend which has been replaced by -[the new backend system](../../backend-system/index.md), being the default since -Backstage [version 1.24](../../releases/v1.24.0.md). If have migrated to the new -backend system, you may want to read [its own article](./provider.md) -instead. Otherwise, [consider migrating](../../backend-system/building-backends/08-migrating.md)! -::: - -The Backstage `core-plugin-api` package comes with a Bitbucket Server authentication provider that can authenticate -users using Bitbucket Server. This does **NOT** work with Bitbucket Cloud. - -## Create an Application Link in Bitbucket Server - -To add Bitbucket Server authentication, you must create an incoming application link. Follow the steps described in -the [Bitbucket Server documentation](https://confluence.atlassian.com/bitbucketserver/configure-an-incoming-link-1108483657.html) -to create one. - -## Configuration - -The provider configuration can then be added to your `app-config.yaml` under the root `auth` configuration: - -```yaml -auth: - environment: development - providers: - bitbucketServer: - development: - host: bitbucket.org - clientId: ${AUTH_BITBUCKET_SERVER_CLIENT_ID} - clientSecret: ${AUTH_BITBUCKET_SERVER_CLIENT_SECRET} -``` - -The Bitbucket Server provider is a structure with two configuration keys: - -- `clientId`: The client ID that was generated by Bitbucket, e.g. `b0f868455c15dcdff5c5fb5d173ae684`. -- `clientSecret`: The client secret tied to the generated client ID. - -## Adding the provider to the Backstage frontend - -To add the provider to the frontend, add the `bitbucketServerAuthApi` reference and `SignInPage` component as shown -in [Adding the provider to the sign-in page](../index.md#sign-in-configuration). - -## Using Bitbucket Server for sign-in - -In order to use the Bitbucket Server provider for sign-in, you must configure it with a `signIn.resolver`. See -the [Sign-In Resolver documentation](../identity-resolver.md) for more details on how this is done. Note that for the -Bitbucket Server provider, you'll want to use `bitbucketServer` as the provider ID, -and `providers.bitbucketServer.create` for the provider factory. - -The `@backstage/plugin-auth-backend` plugin also comes with a built-in resolver that can be used if desired. -The `emailMatchingUserEntityProfileEmail` identifies users by matching their Bitbucket Server email address to the email -address of `User` entities in the catalog. Note that you must populate your catalog with matching entities or users will -not be able to sign in with this resolver. diff --git a/docs/auth/bitbucketServer/provider.md b/docs/auth/bitbucketServer/provider.md index 0da600d3fd..89b8be3ff6 100644 --- a/docs/auth/bitbucketServer/provider.md +++ b/docs/auth/bitbucketServer/provider.md @@ -8,7 +8,7 @@ description: Adding Bitbucket Server OAuth as an authentication provider in Back :::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](./provider--old.md) +system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/auth/bitbucketServer/provider--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! ::: diff --git a/docs/auth/identity-resolver--old.md b/docs/auth/identity-resolver--old.md deleted file mode 100644 index ce41fbcf2b..0000000000 --- a/docs/auth/identity-resolver--old.md +++ /dev/null @@ -1,379 +0,0 @@ ---- -id: identity-resolver--old -title: Sign-in Identities and Resolvers (Old Backend System) -description: An introduction to Backstage user identities and sign-in resolvers in the old backend system ---- - -:::info -This documentation is written for the old backend which has been replaced by -[the new backend system](../backend-system/index.md), being the default since -Backstage [version 1.24](../releases/v1.24.0.md). If have migrated to the new -backend system, you may want to read [its own article](./identity-resolver.md) -instead. Otherwise, [consider migrating](../backend-system/building-backends/08-migrating.md)! -::: - -By default, every Backstage auth provider is configured only for the use-case of -access delegation. This enables Backstage to request resources and actions from -external systems on behalf of the user, for example re-triggering a build in CI. - -If you want to use an auth provider to sign in users, you need to explicitly configure -it have sign-in enabled and also tell it how the external identities should -be mapped to user identities within Backstage. - -## Quick Start - -> See [the auth docs](./index.md) -> for a full list of auth providers and their built-in sign-in resolvers. - -Backstage projects created with `npx @backstage/create-app` come configured with a -sign-in resolver for GitHub guest access. This resolver makes all users share -a single "guest" identity and is only intended as a minimum requirement to quickly -get up and running. You can replace `github` for any of the other providers if you need. - -This resolver should not be used in production, as it uses a single shared identity, -and has no restrictions on who is able to sign-in. Be sure to read through the rest -of this page to understand the Backstage identity system once you need to install -a resolver for your production environment. - -The guest resolver can be useful for testing purposes too, and it looks like this: - -```ts -signIn: { - resolver(_, ctx) { - const userRef = 'user:default/guest' - return ctx.issueToken({ - claims: { - sub: userRef, - ent: [userRef], - }, - }), - }, -}, -``` - -## Backstage User Identity - -A user identity within Backstage is built up from two pieces of information, a -user [entity reference](../features/software-catalog/references.md), and a -set of ownership entity references. -When a user signs in, a Backstage token is generated with these two pieces of information, -which is then used to identify the user within the Backstage ecosystem. - -The user entity reference should uniquely identify the logged in user in Backstage. -It is encouraged that a matching user entity also exists within the Software Catalog, -but it is not required. If the user entity exists in the catalog it can be used to -store additional data about the user. There may even be some plugins that require -this for them to be able to function. - -The ownership references are also entity references, and it is likewise -encouraged that these entities exist within the catalog, but it is not a requirement. -The ownership references are used to determine what the user owns, as a set -of references that the user claims ownership though. For example, a user -Jane (`user:default/jane`) might have the ownership references `user:default/jane`, -`group:default/team-a`, and `group:default/admins`. Given these ownership claims, -any entity that is marked as owned by either of `user:jane`, `team-a`, or `admins` would -be considered owned by Jane. - -The ownership claims often contain the user entity reference itself, but it is not -required. It is also worth noting that the ownership claims can also be used to -resolve other relations similar to ownership, such as a claim for a `maintainer` or -`operator` status. - -The Backstage token that encapsulates the user identity is a JWT. The user entity -reference is stored in the `sub` claim of the payload, while the ownership references -are stored in a custom `ent` claim. Both the user and ownership references should -always be full entity references, as opposed to shorthands like just `jane` or `user:jane`. - -## Sign-in Resolvers - -Signing in a user into Backstage requires a mapping of the user identity from the -third-party auth provider to a Backstage user identity. This mapping can vary quite -a lot between different organizations and auth providers, and because of that there's -no default way to resolve user identities. The auth provider that one wants to use -for sign-in must instead be configured with a sign-in resolver, which is a function -that is responsible for creating this user identity mapping. - -The input to the sign-in resolver function is the result of a successful log in with -the given auth provider, as well as a context object that contains various helpers -for looking up users and issuing tokens. There are also a number of built-in sign-in -resolvers that can be used, which are covered a bit further down. - -Note that while it possible to configure multiple auth providers to be used for sign-in, -you should take care when doing so. It is best to make sure that the different auth -providers either do not have any user overlap, or that any users that are able to log -in with multiple providers always end up with the same Backstage identity. - -### Custom Resolver Example - -Let's look at an example of a custom sign-in resolver for the Google auth provider. -This all typically happens within your `packages/backend/src/plugins/auth.ts` file, -which is responsible for setting up and configuring the auth backend plugin. - -You provide the resolver as part of the options you pass when creating a new auth -provider factory. This means you need to replace the default Google provider with -one that you create. Be sure to also include the existing `defaultAuthProviderFactories` -if you want to keep all of the built-in auth providers installed. - -Now let's look at the example, with the rest of the commentary being made with in -the code comments: - -```ts -// File: packages/backend/src/plugins/auth.ts -import { - createRouter, - providers, - defaultAuthProviderFactories, -} from '@backstage/plugin-auth-backend'; -import { Router } from 'express'; -import { PluginEnvironment } from '../types'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - return await createRouter({ - ...env, - providerFactories: { - ...defaultAuthProviderFactories, - google: providers.google.create({ - signIn: { - resolver: async (info, ctx) => { - const { - profile: { email }, - } = info; - // Profiles are not always guaranteed to have an email address. - // You can also find more provider-specific information in `info.result`. - // It typically contains a `fullProfile` object as well as ID and/or access - // tokens that you can use for additional lookups. - if (!email) { - throw new Error('User profile contained no email'); - } - - // You can add your own custom validation logic here. - // Logins can be prevented by throwing an error like the one above. - myEmailValidator(email); - - // This example resolver simply uses the local part of the email as the name. - const [name] = email.split('@'); - - // This helper function handles sign-in by looking up a user in the catalog. - // The lookup can be done either by reference, annotations, or custom filters. - // - // The helper also issues a token for the user, using the standard group - // membership logic to determine the ownership references of the user. - return ctx.signInWithCatalogUser({ - entityRef: { name }, - }); - }, - }, - }), - }, - }); -} -``` - -### Built-in Resolvers - -You don't always have to write your own custom resolver. The auth backend plugin provides -built-in resolvers for many of the common sign-in patterns. You access these via the `resolvers` -property of each of the auth provider integrations. For example, the Google provider has -a built in resolver that works just like the one we defined above: - -```ts -// File: packages/backend/src/plugins/auth.ts -export default async function createPlugin( - // ... - return await createRouter({ - // ... - providerFactories: { - // ... - google: providers.google.create({ - signIn: { - resolver: providers.google.resolvers.emailLocalPartMatchingUserEntityName(), - }, - }); - } - }) -) -``` - -There are also other options, like the this one that looks up a user -by matching the `google.com/email` annotation of user entities in the catalog: - -```ts -providers.google.create({ - signIn: { - resolver: providers.google.resolvers.emailMatchingUserEntityAnnotation(), - }, -}); -``` - -## Custom Ownership Resolution - -If you want to have more control over the membership resolution and token generation -that happens during sign-in you can replace `ctx.signInWithCatalogUser` with a set -of lower-level calls: - -```ts -// File: packages/backend/src/plugins/auth.ts -import { getDefaultOwnershipEntityRefs } from '@backstage/plugin-auth-backend'; - -export default async function createPlugin( - // ... - return await createRouter({ - // ... - providerFactories: { - // ... - google: async ({ profile: { email } }, ctx) => { - if (!email) { - throw new Error('User profile contained no email'); - } - - // This step calls the catalog to look up a user entity. You could for example - // replace it with a call to a different external system. - const { entity } = await ctx.findCatalogUser({ - annotations: { - 'acme.org/email': email, - }, - }); - - // In this step we extract the ownership references from the user entity using - // the standard logic. It uses a reference to the entity itself, as well as the - // target of each `memberOf` relation where the target is of the kind `Group`. - // - // If you replace the catalog lookup with something that does not return - // an entity you will need to replace this step as well. - // - // You might also replace it if you for example want to filter out certain groups. - // - // Note that `getDefaultOwnershipEntityRefs` only includes groups to which the - // user has a direct MEMBER_OF relationship. It's perfectly fine to include - // groups that the user is transitively part of in the claims array, but the - // catalog doesn't currently provide a direct way of accessing this list of - // groups. - const ownershipRefs = getDefaultOwnershipEntityRefs(entity); - - // The last step is to issue the token, where we might provide more options in the future. - return ctx.issueToken({ - claims: { - sub: stringifyEntityRef(entity), - ent: ownershipRefs, - }, - }); - }; - } - }) -) -``` - -## Sign-In without Users in the Catalog - -While populating the catalog with organizational data unlocks more powerful ways -to browse your software ecosystem, it might not always be a viable or prioritized -option. However, even if you do not have user entities populated in your catalog, you -can still sign in users. As there are currently no built-in sign-in resolvers for -this scenario you will need to implement your own. - -Signing in a user that doesn't exist in the catalog is as simple as skipping the -catalog lookup step from the above example. Rather than looking up the user, we -instead immediately issue a token using whatever information is available. One caveat -is that it can be tricky to determine the ownership references, although it can -be achieved for example through a lookup to an external service. You typically -want to at least use the user itself as a lone ownership reference. - -Because we no longer use the catalog as an allow-list of users, it is often important -that you limit what users are allowed to sign in. This could be a simple email domain -check like in the example below, or you might for example look up the GitHub organizations -that the user belongs to using the user access token in the provided result object. - -```ts -// File: packages/backend/src/plugins/auth.ts -import { createRouter, providers } from '@backstage/plugin-auth-backend'; -import { Router } from 'express'; -import { PluginEnvironment } from '../types'; -import { - stringifyEntityRef, - DEFAULT_NAMESPACE, -} from '@backstage/catalog-model'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - return await createRouter({ - ...env, - providerFactories: { - google: providers.google.create({ - signIn: { - resolver: async ({ profile }, ctx) => { - if (!profile.email) { - throw new Error( - 'Login failed, user profile does not contain an email', - ); - } - // Split the email into the local part and the domain. - const [localPart, domain] = profile.email.split('@'); - - // Next we verify the email domain. It is recommended to include this - // kind of check if you don't look up the user in an external service. - if (domain !== 'acme.org') { - throw new Error( - `Login failed, this email ${profile.email} does not belong to the expected domain`, - ); - } - - // By using `stringifyEntityRef` we ensure that the reference is formatted correctly - const userEntity = stringifyEntityRef({ - kind: 'User', - name: localPart, - namespace: DEFAULT_NAMESPACE, - }); - return ctx.issueToken({ - claims: { - sub: userEntity, - ent: [userEntity], - }, - }); - }, - }, - }), - }, - }); -} -``` - -## AuthHandler - -Similar to a custom sign-in resolver, you can also write a custom auth handler -function which is used to verify and convert the auth response into the profile -that will be presented to the user. This is where you can customize things like -display name and profile picture. - -This is also the place where you can do authorization and validation of the user -and throw errors if the user should not be allowed access in Backstage. - -```ts -// File: packages/backend/src/plugins/auth.ts -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - return await createRouter({ - ... - providerFactories: { - google: providers.google.create({ - authHandler: async ({ - fullProfile // Type: passport.Profile, - idToken // Type: (Optional) string, - }) => { - // Custom validation code goes here - return { - profile: { - email, - picture, - displayName, - } - }; - } - }) - } - }) -} -``` diff --git a/docs/auth/identity-resolver.md b/docs/auth/identity-resolver.md index 6a59805b0b..3d15c9541e 100644 --- a/docs/auth/identity-resolver.md +++ b/docs/auth/identity-resolver.md @@ -7,7 +7,7 @@ description: An introduction to Backstage user identities and sign-in resolvers :::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](./identity-resolver--old.md) +system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/auth/identity-resolver--old.md) instead, and [consider migrating](../backend-system/building-backends/08-migrating.md)! ::: diff --git a/docs/auth/oidc--old.md b/docs/auth/oidc--old.md deleted file mode 100644 index 904e48c9a3..0000000000 --- a/docs/auth/oidc--old.md +++ /dev/null @@ -1,289 +0,0 @@ ---- -id: oidc--old -title: OIDC provider from scratch -description: This section shows how to use an OIDC provider from scratch, same steps apply for custom providers. ---- - -:::info -This documentation is written for the old backend which has been replaced by -[the new backend system](../backend-system/index.md), being the default since -Backstage [version 1.24](../releases/v1.24.0.md). If have migrated to the new -backend system, you may want to read [its own article](./oidc.md) -instead. Otherwise, [consider migrating](../backend-system/building-backends/08-migrating.md)! -::: - -This section shows how to use an OIDC provider from scratch, same steps apply for custom -providers. Please note these steps are for using a provider, not how to implement one, -and Backstage recommends creating custom providers specific to the IDP, so we'll use a -`azureOIDC` provider throughout this example, feel free to change any of those refs -to your provider name. - -## Summary - -To add providers not enabled by default like OIDC, we need to follow some steps, we -assume you already have a sign in page to which we'll add the provider so users can -sign in through the provider. In simple steps here's how you enable the provider: - -- Create an API reference to identify the provider. -- Create the API factory that will handle the authentication. -- Add or reuse an auth provider so you can authenticate. -- Add or reuse a resolver to handle the result from the authentication. -- Configure the provider to access your 3rd party auth solution. -- Add the provider to sign in page so users can login with it. - -We'll explain each step more in detail next. - -### The API reference - -An API reference exist for the sake of **Dependency Injection**, check [Utility APIs][4] -for extended explanation. - -In this OIDC example, we'll create the API reference directly in the -`packages/app/src/apis.ts` file, it is not a requirement to put the reference in this -file. Any location will do as long as it's available to be imported to where the API -factory is, as well as easily accessible to the rest of the application so any package -and plugin can inject the API instance when necessary. - -An example of such would be when you use an auth provider from a library installed with -NPM, or any other library repository, you would import the API ref from the library. - -```ts -export const azureOIDCAuthApiRef: ApiRef< - OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ - id: 'auth.my-custom-provider', -}); -``` - -Please note a few things, the ID can be anything you want as long as it doesn't conflict -with other refs, backstage recommends to use a custom name that references your custom -provider, for example we are using OIDC protocol with Azure, so we could use something -like `auth.azure.oidc` as well. - -Also we're exporting this reference, as well as the `typings`, we need to -be able to import this reference anywhere in the app, and the `typings` will tell typescript -what instance we're getting from DI when injecting the API. In this case we are defining -an API for authentication, so we tell TS that this instance complies with 4 API -interfaces: - -- The OICD API that will handle authentication. -- Profile API for requesting user profile info from the auth provider in question. -- Backstage identity API to handle and associate the user profile with backstage identity. -- Session API, to handle the session the user will have while logged in. - -### The API Factory - -A factory is a function that can take some parameters or dependencies and return an -instance of something, in our case it will be a function that requests some backstage -APIs and use them to create an instance of an OIDC API provider. - -Please note that this function only runs (creates the instance) when somewhere else in -the app you request the DI to give you an instance of the OIDC provider using the API ref -defined above, and the DI will only run this function the first time, from then on any -other DI injection will just receive the same instance created the first time, basically -the instance is cached by the DI library, a singleton. - -Let's add our OIDC API factory to the APIs array in the `packages/app/src/apis.ts` file: - -```ts title="packages/app/src/apis.ts" -/* highlight-add-next-line */ -import { OAuth2 } from '@backstage/core-app-api'; - -export const apis: AnyApiFactory[] = [ - /* highlight-add-start */ - createApiFactory({ - api: azureOIDCAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - OAuth2.create({ - configApi, - discoveryApi, - oauthRequestApi, - provider: { - id: 'my-auth-provider', - title: 'My custom auth provider', - icon: () => null, - }, - environment: configApi.getOptionalString('auth.environment'), - defaultScopes: ['openid', 'profile', 'email'], - popupOptions: { - // optional, used to customize login in popup size - size: { - fullscreen: true, - }, - /** - * or specify popup width and height - * size: { - width: 1000, - height: 1000, - } - */ - }, - }), - }), - /* highlight-add-end */ - // .. -]; -``` - -Please note we're importing the `OAuth2` class from `@backstage/core-app-api` effectively -delegating the authentication to it. Also we're using the `my-auth-provider` ID to tell -`OAuth2` to use the auth provider we'll define in the next section, and added the default -scopes to request ID, profile, email and user read permissions. - -## The Auth Provider - -The Auth Provider is responsible for authenticating with the 3rd party service, and give -us back the credentials, here's where you pick which protocol to use, be it Auth0, OAuth2, -OIDC, SAML or any other that your 3rd party IDP provider supports. - -For this example we'll use OIDC, we pass a factory to the `providerFactories` object with -the ID you picked to represent the Auth provider, this ID has to match with the provider's -`id` inside the API factory, the yaml config provider key under `auth.providers`, and the -callback URI provider segment (you'll have to configure your IDP to handle the callback -URI properly). - -```ts -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - return await createRouter({ - logger: env.logger, - config: env.config, - database: env.database, - discovery: env.discovery, - tokenManager: env.tokenManager, - providerFactories: { - ...defaultAuthProviderFactories, - /* highlight-add-next-line */ - 'my-auth-provider': providers.oidc.create({}), - }, - // .. -}) -``` - -### The Resolver - -Resolvers exist to map user identity from the 3rd party (in this case an azure IDP -provider) to the backstage user identity, for a detailed explanation check the -[Identity Resolver][1] page, it explains how to write a custom resolver as well as -linking the built in resolvers of backstage. - -The default OIDC provider does not support SignIn, we need to add such support by -adding a resolver for a SignIn request. - -The OIDC provider doesn't provide any build-in resolvers, so we'll need to define our own: - -```ts -import { - DEFAULT_NAMESPACE, - /* highlight-add-next-line */ - stringifyEntityRef, -} from '@backstage/catalog-model'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - return await createRouter({ - logger: env.logger, - config: env.config, - database: env.database, - discovery: env.discovery, - tokenManager: env.tokenManager, - providerFactories: { - ...defaultAuthProviderFactories, - 'my-auth-provider': providers.oidc.create({ - /* highlight-add-start */ - signIn: { - resolver(info, ctx) { - const userRef = stringifyEntityRef({ - kind: 'User', - name: info.result.userinfo.sub, - namespace: DEFAULT_NAMESPACE, - }); - return ctx.issueToken({ - claims: { - sub: userRef, // The user's own identity - ent: [userRef], // A list of identities that the user claims ownership through - }, - }); - }, - }, - /* highlight-add-end */ - }), - }, - // .. - }) -``` - -### The configuration - -Since we are using our custom OIDC Auth Provider, we need to add a configuration based -on the provider used, in this case based on OIDC protocol (remember the 3rd party has to -support the protocol). - -In this example we'll configure OIDC with `my-auth-provider`, to do so we need to -[Create app registration][2] in the Azure console, the only difference is that the -`http://localhost:7007/api/auth/microsoft/handler/frame` URL needs to change to -`http://localhost:7007/api/auth/my-auth-provider/handler/frame`. - -Then we need to configure the env variables for the provider, based on the provider's code -in `plugins/auth-backend/src/providers/oidc/provider.ts` we need the following variables -in the `app-config.yaml`: - -```yaml title="app-config.yaml" -auth: - environment: development - ### Providing an auth.session.secret will enable session support in the auth-backend - session: - secret: ${SESSION_SECRET} - providers: - my-auth-provider: - development: - metadataUrl: https://example.com/.well-known/openid-configuration - clientId: ${AUTH_MY_CLIENT_ID} - clientSecret: ${AUTH_MY_CLIENT_SECRET} -``` - -Anything enclosed in `${}` can be replaced directly in the yaml, or provided as -environment variables, the way you obtain all these except `scope` and `prompt` is to -check the App Registration you created: - -- `clientId`: Grab from the Overview page. -- `clientSecret`: Can only be seen when creating the secret, if you lose it you'll need a - new secret. -- `metadataUrl`: In Overview > Endpoints tab, grab OpenID Connect metadata document URL. -- `authorizationUrl` and `tokenUrl`: Open the `metadataUrl` in a browser, that json will - hold these 2 urls somewhere in there. -- `tokenEndpointAuthMethod`: Don't define it, use the default unless you know what it does. -- `tokenSignedResponseAlg`: Don't define it, use the default unless you know what it does. -- `scope`: Only used if we didn't specify `defaultScopes` in the provider's factory, - basically the same thing. -- `prompt`: Recommended to use `auto` so the browser will request login to the IDP if the - user has no active session. - -Note that for the time being, any change in this yaml file requires a restart of the app, -also you need to have the `session.secret` part to use OIDC (some other providers might -need this as well) to support user sessions. - -### The Sign In provider - -The last step is to add the provider to the `SignInPage` so users can sign in with your -new provider, please follow the [Sign In Configuration][3] docs, here's where you import -and use the API reference we defined earlier. - -## 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 -[4]: https://backstage.io/docs/api/utility-apis diff --git a/docs/auth/oidc.md b/docs/auth/oidc.md index 838ac51f1f..250736cec7 100644 --- a/docs/auth/oidc.md +++ b/docs/auth/oidc.md @@ -7,7 +7,7 @@ description: This section shows how to use an OIDC provider from scratch, same s :::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](./oidc--old.md) +system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/auth/oidc--old.md) instead, and [consider migrating](../backend-system/building-backends/08-migrating.md)! ::: diff --git a/docs/auth/service-to-service-auth--old.md b/docs/auth/service-to-service-auth--old.md deleted file mode 100644 index 8341d69341..0000000000 --- a/docs/auth/service-to-service-auth--old.md +++ /dev/null @@ -1,177 +0,0 @@ ---- -id: service-to-service-auth--old -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. ---- - -:::info -This documentation is written for the old backend which has been replaced by -[the new backend system](../backend-system/index.md), being the default since -Backstage [version 1.24](../releases/v1.24.0.md). If have migrated to the new -backend system, you may want to read [its own article](./identity-resolver.md) -instead. Otherwise, [consider migrating](../backend-system/building-backends/08-migrating.md)! -::: - -This article describes the steps needed to introduce _service-to-service auth_ (formerly _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`: - -```ts title="packages/backend/src/index.ts" -/* highlight-remove-next-line */ -const tokenManager = ServerTokenManager.noop(); -/* highlight-add-next-line */ -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: - # - 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 -service-to-service 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 - -> NOTE: The JWT must encode the `alg` header as a protected header, such as with -> [setProtectedHeader](https://github.com/panva/jose/blob/main/docs/classes/jwt_sign.SignJWT.md#setprotectedheader). - -## 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 -``` diff --git a/docs/auth/service-to-service-auth.md b/docs/auth/service-to-service-auth.md index 126b136d0b..fecb3f5191 100644 --- a/docs/auth/service-to-service-auth.md +++ b/docs/auth/service-to-service-auth.md @@ -8,7 +8,7 @@ description: This section describes service to service authentication works, bot :::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](./service-to-service-auth--old.md) +system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/auth/service-to-service-auth--old.md) instead, and [consider migrating](../backend-system/building-backends/08-migrating.md)! ::: @@ -219,7 +219,7 @@ provider's documentation. The subject returned from the token verification will become part of the credentials object that the request recipient plugins get. All subjects will have the prefix -`external:`, but you can also provide a custom subjectPrefix which will get appended before the +`external:`, but you can also provide a custom `subjectPrefix` which will get appended before the subject returned from your JWKS service (ex. `external:custom-prefix:sub`). Callers must pass along tokens with requests in the `Authorization` header when diff --git a/docs/integrations/aws-s3/discovery--old.md b/docs/integrations/aws-s3/discovery--old.md deleted file mode 100644 index 3a9d30a370..0000000000 --- a/docs/integrations/aws-s3/discovery--old.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -id: discovery--old -title: AWS S3 Discovery -sidebar_label: Discovery -# prettier-ignore -description: Automatically discovering catalog entities from an AWS S3 Bucket ---- - -:::info -This documentation is written for the old backend which has been replaced by [the new backend system](../../backend-system/index.md), being the default since Backstage [version 1.24](../../releases/v1.24.0.md). If have migrated to the new backend system, you may want to read [its own article](./discovery.md) instead. Otherwise, [consider migrating](../../backend-system/building-backends/08-migrating.md)! -::: - -The AWS S3 integration has a special entity provider for discovering catalog -entities located in an S3 Bucket. If you have a bucket that contains multiple -catalog files, and you want to automatically discover them, you can use this -provider. The provider will crawl your S3 bucket and register entities -matching the configured path. This can be useful as an alternative to static -locations or manually adding things to the catalog. - -To use the entity provider, you'll need an AWS S3 integration -[set up](locations.md) with `accessKeyId` and `secretAccessKey`, and/or -a `roleArn` or none of these (e.g., profile- or instance-based credentials). - -At production deployments, you likely manage these with the permissions attached -to your instance. - -In your configuration, you add a provider config per bucket: - -```yaml -# app-config.yaml - -catalog: - providers: - awsS3: - yourProviderId: # identifies your dataset / provider independent of config changes - bucketName: sample-bucket - prefix: prefix/ # optional - region: us-east-2 # optional, uses the default region otherwise - schedule: # same options as in SchedulerServiceTaskScheduleDefinition - # supports cron, ISO duration, "human duration" as used in code - frequency: { minutes: 30 } - # supports ISO duration, "human duration" as used in code - timeout: { minutes: 3 } -``` - -For simple setups, you can omit the provider ID at the config -which has the same effect as using `default` for it. - -```yaml -# app-config.yaml - -catalog: - providers: - awsS3: - # uses "default" as provider ID - bucketName: sample-bucket - prefix: prefix/ # optional - region: us-east-2 # optional, uses the default region otherwise - schedule: # same options as in SchedulerServiceTaskScheduleDefinition - # supports cron, ISO duration, "human duration" as used in code - frequency: { minutes: 30 } - # supports ISO duration, "human duration" as used in code - timeout: { minutes: 3 } -``` - -As this provider is not one of the default providers, you will first need to install -the AWS catalog plugin: - -```bash title="From your Backstage root directory" -yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-aws -``` - -Once you've done that, you'll also need to add the segment below to `packages/backend/src/plugins/catalog.ts`: - -```ts -/* packages/backend/src/plugins/catalog.ts */ - -import { AwsS3EntityProvider } from '@backstage/plugin-catalog-backend-module-aws'; - -const builder = await CatalogBuilder.create(env); -/** ... other processors and/or providers ... */ -builder.addEntityProvider( - AwsS3EntityProvider.fromConfig(env.config, { - logger: env.logger, - scheduler: env.scheduler, - }), -); -``` diff --git a/docs/integrations/aws-s3/discovery.md b/docs/integrations/aws-s3/discovery.md index f4d18015a2..fa1eb017fc 100644 --- a/docs/integrations/aws-s3/discovery.md +++ b/docs/integrations/aws-s3/discovery.md @@ -7,7 +7,7 @@ description: Automatically discovering catalog entities from an AWS S3 Bucket --- :::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](./discovery--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! +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](https://github.com/backstage/backstage/blob/v1.37.0/docs/integrations/aws-s3/discovery--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! ::: The AWS S3 integration has a special entity provider for discovering catalog diff --git a/docs/integrations/azure/discovery--old.md b/docs/integrations/azure/discovery--old.md deleted file mode 100644 index 27532fe6ee..0000000000 --- a/docs/integrations/azure/discovery--old.md +++ /dev/null @@ -1,186 +0,0 @@ ---- -id: discovery--old -title: Azure DevOps Discovery -sidebar_label: Discovery -# prettier-ignore -description: Automatically discovering catalog entities from repositories in an Azure DevOps organization ---- - -:::info -This documentation is written for the old backend which has been replaced by [the new backend system](../../backend-system/index.md), being the default since Backstage [version 1.24](../../releases/v1.24.0.md). If have migrated to the new backend system, you may want to read [its own article](./discovery.md) instead. Otherwise, [consider migrating](../../backend-system/building-backends/08-migrating.md)! -::: - -The Azure DevOps integration has a special entity provider for discovering -catalog entities within an Azure DevOps. The provider will crawl your Azure -DevOps organization and register entities matching the configured path. This can -be useful as an alternative to static locations or manually adding things to the -catalog. - -This guide explains how to install and configure the Azure DevOps Entity Provider (recommended) or the Azure DevOps Processor. - -## Dependencies - -### Code Search Feature - -Azure discovery is driven by the Code Search feature in Azure DevOps, this may not be enabled by default. For Azure -DevOps Services you can confirm this by looking at the installed extensions in your Organization Settings. For Azure -DevOps Server you'll find this information in your Collection Settings. - -If the Code Search extension is not listed then you can install it from the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=ms.vss-code-search&targetId=f9352dac-ba6e-434e-9241-a848a510ce3f&utm_source=vstsproduct&utm_medium=SearchExtStatus). - -### Azure Integration - -Setup [Azure integration](locations.md) with `host` and `token`. Host must be `dev.azure.com` for Cloud users, otherwise set this to your on-premise hostname. - -## Installation - -At your configuration, you add one or more provider configs: - -```yaml title="app-config.yaml" -catalog: - providers: - azureDevOps: - yourProviderId: # identifies your dataset / provider independent of config changes - organization: myorg - project: myproject - repository: service-* # this will match all repos starting with service-* - path: /catalog-info.yaml - schedule: # optional; same options as in SchedulerServiceTaskScheduleDefinition - # supports cron, ISO duration, "human duration" as used in code - frequency: { minutes: 30 } - # supports ISO duration, "human duration" as used in code - timeout: { minutes: 3 } - yourSecondProviderId: # identifies your dataset / provider independent of config changes - organization: myorg - project: '*' # this will match all projects - repository: '*' # this will match all repos - path: /catalog-info.yaml - anotherProviderId: # another identifier - organization: myorg - project: myproject - repository: '*' # this will match all repos - path: /src/*/catalog-info.yaml # this will search for files deep inside the /src folder - yetAnotherProviderId: # guess, what? Another one :) - host: selfhostedazure.yourcompany.com - organization: myorg - project: myproject - branch: development -``` - -The parameters available are: - -- **`host:`** _(optional)_ Leave empty for Cloud hosted, otherwise set to your self-hosted instance host. -- **`organization:`** Your Organization slug (or Collection for on-premise users). Required. -- **`project:`** _(required)_ Your project slug. Wildcards are supported as shown on the examples above. Using '\*' will search all projects. For a project name containing spaces, use both single and double quotes as in `project: '"My Project Name"'`. -- **`repository:`** _(optional)_ The repository name. Wildcards are supported as show on the examples above. If not set, all repositories will be searched. -- **`path:`** _(optional)_ Where to find catalog-info.yaml files. Defaults to /catalog-info.yaml. -- **`branch:`** _(optional)_ The branch name to use. -- **`schedule`**: - - **`frequency`**: - How often you want the task to run. The system does its best to avoid overlapping invocations. - - **`timeout`**: - The maximum amount of time that a single task invocation can take. - - **`initialDelay`** _(optional)_: - The amount of time that should pass before the first invocation happens. - - **`scope`** _(optional)_: - `'global'` or `'local'`. Sets the scope of concurrency control. - -_Note:_ - -- The path parameter follows the same rules as the search on Azure DevOps web interface. For more details visit the [official search documentation](https://docs.microsoft.com/en-us/azure/devops/project/search/get-started-search?view=azure-devops). -- To use branch parameters, it is necessary that the desired branch be added to the "Searchable branches" list within Azure DevOps Repositories. To do this, follow the instructions below: - -1. Access your Azure DevOps and open the repository in which you want to add the branch. -2. Click on "Settings" in the lower-left corner of the screen. -3. Select the "Options" option in the left navigation bar. -4. In the "Searchable branches" section, click on the "Add" button to add a new branch. -5. In the window that appears, enter the name of the branch you want to add and click "Add". -6. The added branch will now appear in the "Searchable branches" list. - -It may take some time before the branch is indexed and searchable. - -As this provider is not one of the default providers, you will first need to install -the Azure catalog plugin: - -```bash title="From your Backstage root directory" -yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-azure -``` - -Once you've done that, you'll also need to add the segment below to `packages/backend/src/plugins/catalog.ts`: - -```ts title="packages/backend/src/plugins/catalog.ts" -/* highlight-add-next-line */ -import { AzureDevOpsEntityProvider } from '@backstage/plugin-catalog-backend-module-azure'; - -const builder = await CatalogBuilder.create(env); -/** ... other processors and/or providers ... */ -/* highlight-add-start */ -builder.addEntityProvider( - AzureDevOpsEntityProvider.fromConfig(env.config, { - logger: env.logger, - // optional: alternatively, use scheduler with schedule defined in app-config.yaml - schedule: env.scheduler.createScheduledTaskRunner({ - frequency: { minutes: 30 }, - timeout: { minutes: 3 }, - }), - // optional: alternatively, use schedule - scheduler: env.scheduler, - }), -); -/* highlight-add-end */ -``` - -## Alternative Processor - -As an alternative to the entity provider `AzureDevOpsEntityProvider`, you can still use the `AzureDevopsDiscoveryProcessor`. - -```ts title="packages/backend/src/plugins/catalog.ts" -/* highlight-add-next-line */ -import { AzureDevOpsDiscoveryProcessor } from '@backstage/plugin-catalog-backend-module-azure'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - const builder = await CatalogBuilder.create(env); - /* highlight-add-next-line */ - builder.addProcessor( - AzureDevOpsDiscoveryProcessor.fromConfig(env.config, { - logger: env.logger, - }), - ); - - // .. -} -``` - -```yaml -catalog: - locations: - # Scan all repositories for a catalog-info.yaml in the root of the default branch - - type: azure-discovery - target: https://dev.azure.com/myorg/myproject - # Or use a custom pattern for a subset of all repositories with default repository - - type: azure-discovery - target: https://dev.azure.com/myorg/myproject/_git/service-* - # Or use a custom file format and location - - type: azure-discovery - target: https://dev.azure.com/myorg/myproject/_git/*?path=/src/*/catalog-info.yaml - # And optionally provide a specific branch name using the version parameter - - type: azure-discovery - target: https://dev.azure.com/myorg/myproject/_git/*?path=/catalog-info.yaml&version=GBtopic/catalog-info -``` - -Note the `azure-discovery` type, as this is not a regular `url` processor. - -When using a custom pattern, the target is composed of these parts: - -- The base instance URL, `https://dev.azure.com` in this case -- The organization name which is required, `myorg` in this case -- The project name which is optional, `myproject` in this case. This defaults to \*, which scans all the projects where the token has access to. -- The repository blob to scan, which accepts \* wildcard tokens and must be - added after `_git/`. This can simply be `*` to scan all repositories in the - project. -- The path within each repository to find the catalog YAML file. This will - usually be `/catalog-info.yaml`, `/src/*/catalog-info.yaml` or a similar - variation for catalog files stored in the root directory of each repository. -- The repository branch to scan which is optional, `topic/catalog-info` in this case. If omitted, the repo's default branch will be scanned. The `GB` prefix is required, as this is how Azure DevOps identifies the version as a branch. diff --git a/docs/integrations/azure/discovery.md b/docs/integrations/azure/discovery.md index 99feb9a806..b749f8e9ed 100644 --- a/docs/integrations/azure/discovery.md +++ b/docs/integrations/azure/discovery.md @@ -7,7 +7,7 @@ description: Automatically discovering catalog entities from repositories in an --- :::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](./discovery--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! +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](https://github.com/backstage/backstage/blob/v1.37.0/docs/integrations/azure/discovery--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! ::: The Azure DevOps integration has a special entity provider for discovering diff --git a/docs/integrations/azure/org--old.md b/docs/integrations/azure/org--old.md deleted file mode 100644 index 899120e88a..0000000000 --- a/docs/integrations/azure/org--old.md +++ /dev/null @@ -1,273 +0,0 @@ ---- -id: org--old -title: Microsoft Entra Tenant Data -sidebar_label: Org Data -# prettier-ignore -description: Importing users and groups from Microsoft Entra ID into Backstage ---- - -:::info -This documentation is written for the old backend which has been replaced by [the new backend system](../../backend-system/index.md), being the default since Backstage [version 1.24](../../releases/v1.24.0.md). If have migrated to the new backend system, you may want to read [its own article](./org.md) instead.Otherwise, [consider migrating](../../backend-system/building-backends/08-migrating.md)! -::: - -The Backstage catalog can be set up to ingest organizational data - users and -teams - directly from a tenant in Microsoft Entra ID via the -Microsoft Graph API. - -## Installation - -The package is not installed by default, therefore you have to add `@backstage/plugin-catalog-backend-module-msgraph` to your backend package. - -```bash title="From your Backstage root directory" -yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-msgraph -``` - -Next add the basic configuration to `app-config.yaml` - -```yaml title="app-config.yaml" -catalog: - providers: - microsoftGraphOrg: - default: - tenantId: ${AZURE_TENANT_ID} - user: - filter: accountEnabled eq true and userType eq 'member' - group: - filter: > - securityEnabled eq false - and mailEnabled eq true - and groupTypes/any(c:c+eq+'Unified') - schedule: - frequency: PT1H - timeout: PT50M -``` - -Finally, register the plugin in `catalog.ts`. -For large organizations, this plugin can take a long time, so be careful setting low frequency / timeouts and importing a large amount of users / groups for the first try. - -```ts title="packages/backend/src/plugins/catalog.ts" -/* highlight-add-next-line */ -import { MicrosoftGraphOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-msgraph'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - const builder = await CatalogBuilder.create(env); - - /* highlight-add-start */ - builder.addEntityProvider( - MicrosoftGraphOrgEntityProvider.fromConfig(env.config, { - logger: env.logger, - scheduler: env.scheduler, - }), - ); - /* highlight-add-end */ - - // .. -} -``` - -## Authenticating with Microsoft Graph - -### Local Development - -For a local dev environment, it's recommended you have the Azure CLI or Azure PowerShell installed, and are logged in to those. -Alternatively you can use VSCode with the Azure extension if you install `@azure/identity-vscode`. -When these are set up, the plugin will authenticate with the Microsoft Graph API without you needing to configure any credentials, or granting any special permissions. -If you can't do this, you'll have to create an App Registration. - -### App Registration - -If none of the other authentication methods work, you can create an app registration in the azure portal. -By default the graph plugin requires the following Application permissions (not Delegated) for Microsoft Graph: - -- `GroupMember.Read.All` -- `User.Read.All` - -If your organization required Admin Consent for these permissions, that will need to be granted. - -When authenticating with a ClientId/ClientSecret, you can either set the `AZURE_TENANT_ID`, `AZURE_CLIENT_ID` and `AZURE_CLIENT_SECRET` environment variables, or specify the values in configuration - -```yaml -microsoftGraphOrg: - default: - ##... - clientId: 9ef1aac6-b454-4e69-9cf5-7199df049281 - clientSecret: REDACTED -``` - -To authenticate with a certificate rather than a client secret, you can set the `AZURE_TENANT_ID`, `AZURE_CLIENT_ID` and `AZURE_CLIENT_CERTIFICATE_PATH` environments - -### Managed Identity - -If deploying to resources that supports Managed Identity, and has identities configured (e.g. Azure App Services, Azure Container Apps), Managed Identity should be picked up without any additional configuration. -If your app has multiple managed identities, you may need to set the `AZURE_CLIENT_ID` environment variable to tell Azure Identity which identity to use. - -To grant the managed identity the same permissions as mentioned in _App Registration_ above, [please follow this guide](https://docs.microsoft.com/en-us/azure/app-service/tutorial-connect-app-access-microsoft-graph-as-app-javascript?tabs=azure-powershell) - -## Filtering imported Users and Groups - -By default, the plugin will import all users and groups from your directory. -This can be customized through [filters](https://learn.microsoft.com/en-us/graph/filter-query-parameter) and [search](https://learn.microsoft.com/en-us/graph/search-query-parameter) queries. Keep in mind that if you omit filters and search queries for the user or group properties, the plugin will automatically import all available users or groups. - -### Groups - -A smaller set of groups can be obtained by configuring a search query or a filter. -If both `filter` and `search` are provided, then groups must match both to be ingested. - -```yaml -microsoftGraphOrg: - providerId: - group: - filter: securityEnabled eq false and mailEnabled eq true and groupTypes/any(c:c+eq+'Unified') - search: '"description:One" AND ("displayName:Video" OR "displayName:Drive")' -``` - -In addition to these groups, one additional group will be created for your organization. -All imported groups will be a child of this group. - -### Users - -There are two modes for importing users - You can import all user objects matching a `filter`. - -```yaml -microsoftGraphOrg: - providerId: - user: - filter: accountEnabled eq true and userType eq 'member' -``` - -Alternatively you can import users that are members of specific groups. -For each group matching the `search` and `filter` query, each group member will be imported. -Only direct group members will be imported, not transient users. - -```yaml -microsoftGraphOrg: - providerId: - userGroupMember: - filter: "displayName eq 'Backstage Users'" - search: '"description:One" AND ("displayName:Video" OR "displayName:Drive")' -``` - -### User photos - -By default, the photos of users will be fetched and added to each user entity. For huge organizations this may be unfeasible, as it will take a _very_ long time, and can be disabled by setting `loadPhotos` to `false`: - -```yaml -microsoftGraphOrg: - providerId: - user: - filter: ... - loadPhotos: false -``` - -## Customizing Transformation - -Ingested entities can be customized by providing custom transformers. -These can be used to completely replace the built in logic, or used to tweak it by using the default transformers (`defaultGroupTransformer`, `defaultUserTransformer` and `defaultOrganizationTransformer` -Entities can also be excluded from backstage by returning `undefined`. - -These Transformers are be registered when configuring `MicrosoftGraphOrgEntityProvider` - -```ts -builder.addEntityProvider( - MicrosoftGraphOrgEntityProvider.fromConfig(env.config, { - // ... - /* highlight-add-start */ - groupTransformer: myGroupTransformer, - userTransformer: myUserTransformer, - organizationTransformer: myOrganizationTransformer, - /* highlight-add-end */ - }), -); -``` - -When using custom transformers, you may want to customize the data returned. -Several configuration options can be provided to tweak the Microsoft Graph query to get the data you need - -```yaml -microsoftGraphOrg: - providerId: - user: - expand: manager - group: - expand: member - select: ['id', 'displayName', 'description'] -``` - -The following provides an example of each kind of transformer - -```ts -import * as MicrosoftGraph from '@microsoft/microsoft-graph-types'; -import { - defaultGroupTransformer, - defaultUserTransformer, - defaultOrganizationTransformer, -} from '@backstage/plugin-catalog-backend-module-msgraph'; -import { GroupEntity, UserEntity } from '@backstage/catalog-model'; - -// This group transformer completely replaces the built in logic with custom logic. -export async function myGroupTransformer( - group: MicrosoftGraph.Group, - groupPhoto?: string, -): Promise { - return { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Group', - metadata: { - name: group.id!, - annotations: {}, - }, - spec: { - type: 'Microsoft Entra ID', - children: [], - }, - }; -} - -// This user transformer makes use of the built in logic, but also sets the description field -export async function myUserTransformer( - graphUser: MicrosoftGraph.User, - userPhoto?: string, -): Promise { - 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 -export async function myOrganizationTransformer( - graphOrganization: MicrosoftGraph.Organization, -): Promise { - return undefined; -} -``` - -## Troubleshooting - -### No data - -First check your logs for the message `Reading msgraph users and groups`. -If you don't see this, check you've registered the provider, and that the schedule is valid - -If you see a log entry `Read 0 msgraph users and 0 msgraph groups`, check your search and filter arguments. - -If you see the start message (`Reading msgraph users and groups`) but no end message (`Read X msgraph users and Y msgraph groups`), then it is likely the job is taking a long time due to a large volume of data. -The default behavior is to import all users and groups, which is often more data than needed. -Try importing a smaller set of data (e.g. `filter: displayName eq 'John Smith'`). - -### Authentication / Token Errors - -See [Troubleshooting Azure Identity Authentication Issues](https://aka.ms/azsdk/js/identity/troubleshoot) - -### Error while reading users from Microsoft Graph: Authorization_RequestDenied - Insufficient privileges to complete the operation - -- Make sure you've granted all the required permissions to your application registration or managed identity -- Make sure the permissions are `Application` permissions rather than `Delegated` -- If your organization has configured "Admin consent" to be required, make sure this has been granted for your application permissions -- If your group queries are returning Microsoft Teams groups, you may need to grant addition permissions (e.g. `Team.ReadBasic.All`, `TeamMember.Read.All`) -- If you've added additional `select` or `expand` fields, those may need additional permissions granted diff --git a/docs/integrations/azure/org.md b/docs/integrations/azure/org.md index 068aa70101..127bc4863b 100644 --- a/docs/integrations/azure/org.md +++ b/docs/integrations/azure/org.md @@ -7,7 +7,7 @@ description: Importing users and groups from Microsoft Entra ID into Backstage --- :::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](./org--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! +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](https://github.com/backstage/backstage/blob/v1.37.0/docs/integrations/azure/org--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! ::: The Backstage catalog can be set up to ingest organizational data - users and diff --git a/docs/integrations/bitbucketServer/discovery--old.md b/docs/integrations/bitbucketServer/discovery--old.md deleted file mode 100644 index 83fe70da0d..0000000000 --- a/docs/integrations/bitbucketServer/discovery--old.md +++ /dev/null @@ -1,123 +0,0 @@ ---- -id: discovery--old -title: Bitbucket Server Discovery -sidebar_label: Discovery -# prettier-ignore -description: Automatically discovering catalog entities from repositories in Bitbucket Server ---- - -:::info -This documentation is written for the old backend which has been replaced by [the new backend system](../../backend-system/index.md), being the default since Backstage [version 1.24](../../releases/v1.24.0.md). If have migrated to the new backend system, you may want to read [its own article](./discovery.md) instead. Otherwise, [consider migrating](../../backend-system/building-backends/08-migrating.md)! -::: - -The Bitbucket Server integration has a special entity provider for discovering -catalog files located in Bitbucket Server. -The provider will search your Bitbucket Server account and register catalog files matching the configured path -as Location entity and via following processing steps add all contained catalog entities. -This can be useful as an alternative to static locations or manually adding things to the catalog. - -## Installation - -You will have to add the entity provider in the catalog initialization code of your -backend. The provider is not installed by default, therefore you have to add a -dependency to `@backstage/plugin-catalog-backend-module-bitbucket-server` to your backend -package. - -```bash -# From your Backstage root directory -yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-bitbucket-server -``` - -And then add the entity provider to your catalog builder: - -```ts title="packages/backend/src/plugins/catalog.ts" -/* highlight-add-next-line */ -import { BitbucketServerEntityProvider } from '@backstage/plugin-catalog-backend-module-bitbucket-server'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - const builder = await CatalogBuilder.create(env); - /* highlight-add-start */ - builder.addEntityProvider( - BitbucketServerEntityProvider.fromConfig(env.config, { - logger: env.logger, - scheduler: env.scheduler, - }), - ); - /* highlight-add-end */ - - // .. -} -``` - -## Configuration - -To use the entity provider, you'll need a [Bitbucket Server integration set up](locations.md). - -Additionally, you need to configure your entity provider instance(s): - -```yaml title="app-config.yaml" -catalog: - providers: - bitbucketServer: - yourProviderId: # identifies your ingested dataset - host: 'bitbucket.mycompany.com' - catalogPath: /catalog-info.yaml # default value - filters: # optional - projectKey: '^apis-.*$' # optional; RegExp - repoSlug: '^service-.*$' # optional; RegExp - skipArchivedRepos: true # optional; boolean - schedule: # same options as in TaskScheduleDefinition - # supports cron, ISO duration, "human duration" as used in code - frequency: { minutes: 30 } - # supports ISO duration, "human duration" as used in code - timeout: { minutes: 3 } -``` - -- **`host`**: - The host of the Bitbucket Server instance, **note**: the host needs to registered as an integration as well, see [location](locations.md). -- **`catalogPath`** _(optional)_: - Default: `/catalog-info.yaml`. - Path where to look for `catalog-info.yaml` files. - When started with `/`, it is an absolute path from the repo root. -- **`filters`** _(optional)_: - - **`projectKey`** _(optional)_: - Regular expression used to filter results based on the project key. - - **`repoSlug`** _(optional)_: - Regular expression used to filter results based on the repo slug. - - **`skipArchivedRepos`** _(optional)_: - Boolean flag to filter out archived repositories. -- **`schedule`**: - - **`frequency`**: - How often you want the task to run. The system does its best to avoid overlapping invocations. - - **`timeout`**: - The maximum amount of time that a single task invocation can take. - - **`initialDelay`** _(optional)_: - The amount of time that should pass before the first invocation happens. - - **`scope`** _(optional)_: - `'global'` or `'local'`. Sets the scope of concurrency control. - -## Custom location processing - -The Bitbucket Server Entity Provider will by default emit a location for each -matching repository. However, it is possible to override this functionality and take full control of how each -matching repository is processed. - -`BitbucketServerEntityProvider.fromConfig` takes an optional parameter -`options.parser` where you can set your own parser to be used for each matched -repository. - -```typescript -const provider = BitbucketServerEntityProvider.fromConfig(env.config, { - logger: env.logger, - schedule: env.scheduler, - parser: async function* customLocationParser(options: { - location: LocationSpec; - client: BitbucketServerClient; - }) { - // Custom logic for interpreting the matching repository - // See defaultBitbucketServerLocationParser for an example - }, -}); -``` diff --git a/docs/integrations/bitbucketServer/discovery.md b/docs/integrations/bitbucketServer/discovery.md index 7947913182..36282b1f48 100644 --- a/docs/integrations/bitbucketServer/discovery.md +++ b/docs/integrations/bitbucketServer/discovery.md @@ -7,7 +7,7 @@ description: Automatically discovering catalog entities from repositories in Bit --- :::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](./discovery--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! +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](https://github.com/backstage/backstage/blob/v1.37.0/docs/integrations/bitbucketServer/discovery--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! ::: The Bitbucket Server integration has a special entity provider for discovering diff --git a/docs/integrations/gerrit/discovery--old.md b/docs/integrations/gerrit/discovery--old.md deleted file mode 100644 index 250de80bdf..0000000000 --- a/docs/integrations/gerrit/discovery--old.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -id: discovery--old -title: Gerrit Discovery -sidebar_label: Discovery -# prettier-ignore -description: Automatically discovering catalog entities from Gerrit repositories ---- - -:::info -This documentation is written for the old backend which has been replaced by [the new backend system](../../backend-system/index.md), being the default since Backstage [version 1.24](../../releases/v1.24.0.md). If have migrated to the new backend system, you may want to read [its own article](./discovery.md) instead. Otherwise, [consider migrating](../../backend-system/building-backends/08-migrating.md)! -::: - -The Gerrit integration has a special entity provider for discovering catalog entities -from Gerrit repositories. The provider uses the "List Projects" API in Gerrit to get -a list of repositories and will automatically ingest all `catalog-info.yaml` files -stored in the root of the matching projects. - -## Installation - -As this provider is not one of the default providers, you will first need to install -the Gerrit provider plugin: - -```bash title="From your Backstage root directory" -yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-gerrit -``` - -Then add the plugin to the plugin catalog `packages/backend/src/plugins/catalog.ts`: - -```ts -/* packages/backend/src/plugins/catalog.ts */ -import { GerritEntityProvider } from '@backstage/plugin-catalog-backend-module-gerrit'; -const builder = await CatalogBuilder.create(env); -/** ... other processors and/or providers ... */ -builder.addEntityProvider( - GerritEntityProvider.fromConfig(env.config, { - logger: env.logger, - scheduler: env.scheduler, - }), -); -``` - -## Configuration - -To use the discovery processor, you'll need a Gerrit integration -[set up](locations.md). Then you can add any number of providers. - -```yaml -# app-config.yaml -catalog: - providers: - gerrit: - yourProviderId: # identifies your dataset / provider independent of config changes - host: gerrit-your-company.com - branch: master # Optional - query: 'state=ACTIVE&prefix=webapps' - schedule: - # supports cron, ISO duration, "human duration" as used in code - frequency: { minutes: 30 } - # supports ISO duration, "human duration" as used in code - timeout: { minutes: 3 } - backend: - host: gerrit-your-company.com - branch: master # Optional - query: 'state=ACTIVE&prefix=backend' -``` - -The provider configuration is composed of three parts: - -- **`host`**: the host of the Gerrit integration to use. -- **`branch`** _(optional)_: the branch where we will look for catalog entities (defaults to "master"). -- **`query`**: this string is directly used as the argument to the "List Project" API. - Typically, you will want to have some filter here to exclude projects that will - never contain any catalog files. diff --git a/docs/integrations/gerrit/discovery.md b/docs/integrations/gerrit/discovery.md index 137a716f35..dbbc7ba7a4 100644 --- a/docs/integrations/gerrit/discovery.md +++ b/docs/integrations/gerrit/discovery.md @@ -7,7 +7,7 @@ description: Automatically discovering catalog entities from Gerrit repositories --- :::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](./discovery--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! +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](https://github.com/backstage/backstage/blob/v1.37.0/docs/integrations/gerrit/discovery--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! ::: The Gerrit integration has a special entity provider for discovering catalog entities diff --git a/docs/integrations/github/discovery--old.md b/docs/integrations/github/discovery--old.md deleted file mode 100644 index 901462b1cb..0000000000 --- a/docs/integrations/github/discovery--old.md +++ /dev/null @@ -1,375 +0,0 @@ ---- -id: discovery--old -title: GitHub Discovery -sidebar_label: Discovery -# prettier-ignore -description: Automatically discovering catalog entities from repositories in a GitHub organization ---- - -:::info -This documentation is written for the old backend which has been replaced by [the new backend system](../../backend-system/index.md), being the default since Backstage [version 1.24](../../releases/v1.24.0.md). If have migrated to the new backend system, you may want to read [its own article](./discovery.md) instead. Otherwise, [consider migrating](../../backend-system/building-backends/08-migrating.md)! -::: - -## GitHub Provider - -The GitHub integration has a discovery provider for discovering catalog -entities within a GitHub organization. The provider will crawl the GitHub -organization and register entities matching the configured path. This can be -useful as an alternative to static locations or manually adding things to the -catalog. This is the preferred method for ingesting entities into the catalog. - -## Installation without Events Support - -You will have to add the provider in the catalog initialization code of your -backend. They are not installed by default, therefore you have to add a -dependency on `@backstage/plugin-catalog-backend-module-github` to your backend -package. - -```bash title="From your Backstage root directory" -yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-github -``` - -And then add the entity provider to your catalog builder: - -```ts title="packages/backend/src/plugins/catalog.ts" -/* highlight-add-next-line */ -import { GithubEntityProvider } from '@backstage/plugin-catalog-backend-module-github'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - const builder = await CatalogBuilder.create(env); - /* highlight-add-start */ - builder.addEntityProvider( - GithubEntityProvider.fromConfig(env.config, { - logger: env.logger, - scheduler: env.scheduler, - }), - ); - /* highlight-add-end */ - - // .. -} -``` - -## Installation with Events Support - -_For the legacy backend system, please read the sub-section below._ - -The catalog module for GitHub comes with events support enabled. -This will make it subscribe to its relevant topics (`github.push`) -and expects these events to be published via the `EventsService`. - -Additionally, you should install the -[event router by `events-backend-module-github`](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-github/README.md) -which will route received events from the generic topic `github` to more specific ones -based on the event type (e.g., `github.push`). - -In order to receive Webhook events by GitHub, you have to decide how you want them -to be ingested into Backstage and published to its `EventsService`. -You can decide between the following options (extensible): - -- [via HTTP endpoint](https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md) -- [via an AWS SQS queue](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md) - -### Legacy Backend System - -Please follow the installation instructions at - -- -- - -Additionally, you need to decide how you want to receive events from external sources like - -- [via HTTP endpoint](https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md) -- [via an AWS SQS queue](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md) - -Set up your provider - -```ts title="packages/backend/src/plugins/catalog.ts" -import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; -/* highlight-add-next-line */ -import { GithubEntityProvider } from '@backstage/plugin-catalog-backend-module-github'; -import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend'; -import { Router } from 'express'; -import { PluginEnvironment } from '../types'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - const builder = await CatalogBuilder.create(env); - builder.addProcessor(new ScaffolderEntitiesProcessor()); - /* highlight-add-start */ - const githubProvider = GithubEntityProvider.fromConfig(env.config, { - events: env.events, - logger: env.logger, - scheduler: env.scheduler, - }); - builder.addEntityProvider(githubProvider); - /* highlight-add-end */ - const { processingEngine, router } = await builder.build(); - await processingEngine.start(); - return router; -} -``` - -You can check the official docs to [configure your webhook](https://docs.github.com/en/developers/webhooks-and-events/webhooks/creating-webhooks) and to [secure your request](https://docs.github.com/en/developers/webhooks-and-events/webhooks/securing-your-webhooks). The webhook will need to be configured to forward `push` events. - -## Configuration - -To use the discovery provider, you'll need a GitHub integration -[set up](locations.md) with either a [Personal Access Token](../../getting-started/config/authentication.md) or [GitHub Apps](./github-apps.md). For Personal Access Tokens you should pay attention to the [required scopes](https://backstage.io/docs/integrations/github/locations/#token-scopes), where you will need at least the `repo` scope for reading components. For GitHub Apps you will need to grant it the [required permissions](https://backstage.io/docs/integrations/github/github-apps#app-permissions) instead, where you will need at least the `Contents: Read-only` permissions for reading components. - -Then you can add a `github` config to the catalog providers configuration: - -```yaml -catalog: - providers: - github: - # the provider ID can be any camelCase string - providerId: - organization: 'backstage' # string - catalogPath: '/catalog-info.yaml' # string - filters: - branch: 'main' # string - repository: '.*' # Regex - schedule: # same options as in SchedulerServiceTaskScheduleDefinition - # supports cron, ISO duration, "human duration" as used in code - frequency: { minutes: 30 } - # supports ISO duration, "human duration" as used in code - timeout: { minutes: 3 } - customProviderId: - organization: 'new-org' # string - catalogPath: '/custom/path/catalog-info.yaml' # string - filters: # optional filters - branch: 'develop' # optional string - repository: '.*' # optional Regex - wildcardProviderId: - organization: 'new-org' # string - catalogPath: '/groups/**/*.yaml' # this will search all folders for files that end in .yaml - filters: # optional filters - branch: 'develop' # optional string - repository: '.*' # optional Regex - topicProviderId: - organization: 'backstage' # string - catalogPath: '/catalog-info.yaml' # string - filters: - branch: 'main' # string - repository: '.*' # Regex - topic: 'backstage-exclude' # optional string - topicFilterProviderId: - organization: 'backstage' # string - catalogPath: '/catalog-info.yaml' # string - filters: - branch: 'main' # string - repository: '.*' # Regex - topic: - include: ['backstage-include'] # optional array of strings - exclude: ['experiments'] # optional array of strings - validateLocationsExist: - organization: 'backstage' # string - catalogPath: '/catalog-info.yaml' # string - filters: - branch: 'main' # string - repository: '.*' # Regex - validateLocationsExist: true # optional boolean - visibilityProviderId: - organization: 'backstage' # string - catalogPath: '/catalog-info.yaml' # string - filters: - visibility: - - public - - internal - enterpriseProviderId: - host: ghe.example.net - organization: 'backstage' # string - catalogPath: '/catalog-info.yaml' # string -``` - -This provider supports multiple organizations via unique provider IDs. - -> **Note:** It is possible but certainly not recommended to skip the provider ID level. -> If you do so, `default` will be used as provider ID. - -- **`catalogPath`** _(optional)_: - Default: `/catalog-info.yaml`. - Path where to look for `catalog-info.yaml` files. - You can use wildcards - `*` or `**` - to search the path and/or the filename. - Wildcards cannot be used if the `validateLocationsExist` option is set to `true`. -- **`filters`** _(optional)_: - - **`branch`** _(optional)_: - String used to filter results based on the branch name. - Defaults to the default Branch of the repository. - - **`repository`** _(optional)_: - Regular expression used to filter results based on the repository name. - - **`topic`** _(optional)_: - Both of the filters below may be used at the same time but the exclusion filter has the highest priority. - In the example above, a repository with the `backstage-include` topic would still be excluded - if it were also carrying the `experiments` topic. - - **`include`** _(optional)_: - An array of strings used to filter in results based on their associated GitHub topics. - If configured, only repositories with one (or more) topic(s) present in the inclusion filter will be ingested - - **`exclude`** _(optional)_: - An array of strings used to filter out results based on their associated GitHub topics. - If configured, all repositories _except_ those with one (or more) topics(s) present in the exclusion filter will be ingested. - - **`visibility`** _(optional)_: - An array of strings used to filter results based on their visibility. Available options are `private`, `internal`, `public`. If configured (non empty), only repositories with visibility present in the filter will be ingested -- **`host`** _(optional)_: - The hostname of your GitHub Enterprise instance. It must match a host defined in [integrations.github](locations.md). -- **`organization`**: - Name of your organization account/workspace. - If you want to add multiple organizations, you need to add one provider config each. -- **`validateLocationsExist`** _(optional)_: - Whether to validate locations that exist before emitting them. - This option avoids generating locations for catalog info files that do not exist in the source repository. - Defaults to `false`. - Due to limitations in the GitHub API's ability to query for repository objects, this option cannot be used in - conjunction with wildcards in the `catalogPath`. -- **`schedule`**: - - **`frequency`**: - How often you want the task to run. The system does its best to avoid overlapping invocations. - - **`timeout`**: - The maximum amount of time that a single task invocation can take. - - **`initialDelay`** _(optional)_: - The amount of time that should pass before the first invocation happens. - - **`scope`** _(optional)_: - `'global'` or `'local'`. Sets the scope of concurrency control. - -## GitHub API Rate Limits - -GitHub [rate limits](https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting) API requests to 5,000 per hour (or more for Enterprise -accounts). The snippet below refreshes the Backstage catalog data every 35 minutes, which issues an API request for each discovered location. - -If your requests are too frequent then you may get throttled by -rate limiting. You can change the refresh frequency of the catalog in your `app-config.yaml` file by controlling the `schedule`. - -```yaml -schedule: - frequency: { minutes: 35 } - timeout: { minutes: 3 } -``` - -More information about scheduling can be found on the [SchedulerServiceTaskScheduleDefinition](https://backstage.io/docs/reference/backend-plugin-api.schedulerservicetaskscheduledefinition) page. - -Alternatively, or additionally, you can configure [github-apps](github-apps.md) authentication -which carries a much higher rate limit at GitHub. - -This is true for any method of adding GitHub entities to the catalog, but -especially easy to hit with automatic discovery. - -## GitHub Processor (To Be Deprecated) - -The GitHub integration has a special discovery processor for discovering catalog -entities within a GitHub organization. The processor will crawl the GitHub -organization and register entities matching the configured path. This can be -useful as an alternative to static locations or manually adding things to the -catalog. - -## Installation - -You will have to add the processors in the catalog initialization code of your -backend. They are not installed by default, therefore you have to add a -dependency on `@backstage/plugin-catalog-backend-module-github` to your backend -package, plus `@backstage/integration` for the basic credentials management: - -```bash title="From your Backstage root directory" -yarn --cwd packages/backend add @backstage/integration @backstage/plugin-catalog-backend-module-github -``` - -And then add the processors to your catalog builder: - -```ts title="packages/backend/src/plugins/catalog.ts" -/* highlight-add-start */ -import { - GithubDiscoveryProcessor, - GithubOrgReaderProcessor, -} from '@backstage/plugin-catalog-backend-module-github'; -import { - ScmIntegrations, - DefaultGithubCredentialsProvider, -} from '@backstage/integration'; -/* highlight-add-end */ - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - const builder = await CatalogBuilder.create(env); - /* highlight-add-start */ - const integrations = ScmIntegrations.fromConfig(env.config); - const githubCredentialsProvider = - DefaultGithubCredentialsProvider.fromIntegrations(integrations); - builder.addProcessor( - GithubDiscoveryProcessor.fromConfig(env.config, { - logger: env.logger, - githubCredentialsProvider, - }), - GithubOrgReaderProcessor.fromConfig(env.config, { - logger: env.logger, - githubCredentialsProvider, - }), - ); - /* highlight-add-end */ - - // .. -} -``` - -## Configuration - -To use the discovery processor, you'll need a GitHub integration -[set up](locations.md) with either a [Personal Access Token](../../getting-started/config/authentication.md) or [GitHub Apps](./github-apps.md). - -Then you can add a location target to the catalog configuration: - -```yaml -catalog: - locations: - # (since 0.13.5) Scan all repositories for a catalog-info.yaml in the root of the default branch - - type: github-discovery - target: https://github.com/myorg - # Or use a custom pattern for a subset of all repositories with default repository - - type: github-discovery - target: https://github.com/myorg/service-*/blob/-/catalog-info.yaml - # Or use a custom file format and location - - type: github-discovery - target: https://github.com/*/blob/-/docs/your-own-format.yaml - # Or use a specific branch-name - - type: github-discovery - target: https://github.com/*/blob/backstage-docs/catalog-info.yaml -``` - -Note the `github-discovery` type, as this is not a regular `url` processor. - -When using a custom pattern, the target is composed of three parts: - -- The base organization URL, `https://github.com/myorg` in this case -- The repository blob to scan, which accepts \* wildcard tokens. This can simply - be `*` to scan all repositories in the organization. This example only looks - for repositories prefixed with `service-`. -- The path within each repository to find the catalog YAML file. This will - usually be `/blob/main/catalog-info.yaml`, `/blob/master/catalog-info.yaml` or - a similar variation for catalog files stored in the root directory of each - repository. You could also use a dash (`-`) for referring to the default - branch. - -## GitHub API Rate Limits - -GitHub [rate limits](https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting) API requests to 5,000 per hour (or more for Enterprise -accounts). The default Backstage catalog backend refreshes data every 100 -seconds, which issues an API request for each discovered location. - -This means if you have more than ~140 catalog entities, you may get throttled by -rate limiting. You can change the refresh rate of the catalog in your `packages/backend/src/plugins/catalog.ts` file: - -```typescript -const builder = await CatalogBuilder.create(env); - -// For example, to refresh every 5 minutes (300 seconds). -builder.setProcessingIntervalSeconds(300); -``` - -Alternatively, or additionally, you can configure [github-apps](github-apps.md) authentication -which carries a much higher rate limit at GitHub. - -This is true for any method of adding GitHub entities to the catalog, but -especially easy to hit with automatic discovery. diff --git a/docs/integrations/github/discovery.md b/docs/integrations/github/discovery.md index e2513cdf21..354569cdb6 100644 --- a/docs/integrations/github/discovery.md +++ b/docs/integrations/github/discovery.md @@ -7,7 +7,7 @@ description: Automatically discovering catalog entities from repositories in a G --- :::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](./discovery--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! +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](https://github.com/backstage/backstage/blob/v1.37.0/docs/integrations/github/discovery--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! ::: ## GitHub Provider diff --git a/docs/integrations/github/org--old.md b/docs/integrations/github/org--old.md deleted file mode 100644 index bf12cad036..0000000000 --- a/docs/integrations/github/org--old.md +++ /dev/null @@ -1,365 +0,0 @@ ---- -id: org--old -title: GitHub Organizational Data -sidebar_label: Org Data -# prettier-ignore -description: Importing users and groups from a GitHub organization into Backstage ---- - -:::info -This documentation is written for the old backend which has been replaced by [the new backend system](../../backend-system/index.md), being the default since Backstage [version 1.24](../../releases/v1.24.0.md). If have migrated to the new backend system, you may want to read [its own article](./org.md) instead.Otherwise, [consider migrating](../../backend-system/building-backends/08-migrating.md)! -::: - -The Backstage catalog can be set up to ingest organizational data - users and -teams - directly from an organization in GitHub or GitHub Enterprise. The result -is a hierarchy of -[`User`](../../features/software-catalog/descriptor-format.md#kind-user) and -[`Group`](../../features/software-catalog/descriptor-format.md#kind-group) kind -entities that mirror your org setup. - -> Note: This adds `User` and `Group` entities to the catalog, but does not -> provide authentication. See the -> [GitHub auth provider](../../auth/github/provider.md) for that. - -## Installation without Events Support - -This guide will use the Entity Provider method. If you for some reason prefer -the Processor method (not recommended), it is described separately below. - -The provider is not installed by default, therefore you have to add a dependency -to `@backstage/plugin-catalog-backend-module-github` to your backend package. - -```bash title="From your Backstage root directory" -yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-github -``` - -> Note: When configuring to use a Provider instead of a Processor you do not -> need to add a _location_ pointing to your GitHub server/organization - -Update the catalog plugin initialization in your backend to add the provider and -schedule it: - -```ts title="packages/backend/src/plugins/catalog.ts" -/* highlight-add-next-line */ -import { GithubOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-github'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - const builder = await CatalogBuilder.create(env); - - /* highlight-add-start */ - // The org URL below needs to match a configured integrations.github entry - // specified in your app-config. - builder.addEntityProvider( - GithubOrgEntityProvider.fromConfig(env.config, { - id: 'production', - orgUrl: 'https://github.com/backstage', - logger: env.logger, - schedule: env.scheduler.createScheduledTaskRunner({ - frequency: { minutes: 60 }, - timeout: { minutes: 15 }, - }), - }), - ); - /* highlight-add-end */ - - // .. -} -``` - -Alternatively, if you wish to ingest data from multiple GitHub organizations you can use -the `GithubMultiOrgEntityProvider` instead. Note that by default, this provider will namespace -groups according to the org they originate from to avoid potential name duplicates: - -```ts title="packages/backend/src/plugins/catalog.ts" -/* highlight-add-next-line */ -import { GithubMultiOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-github'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - const builder = await CatalogBuilder.create(env); - - /* highlight-add-start */ - // The GitHub URL below needs to match a configured integrations.github entry - // specified in your app-config. - builder.addEntityProvider( - GithubMultiOrgEntityProvider.fromConfig(env.config, { - id: 'production', - githubUrl: 'https://github.com', - // Set the following to list the GitHub orgs you wish to ingest from. You can - // also omit this option to ingest all orgs accessible by your GitHub integration - orgs: ['org-a', 'org-b'], - logger: env.logger, - schedule: env.scheduler.createScheduledTaskRunner({ - frequency: { minutes: 60 }, - timeout: { minutes: 15 }, - }), - }), - ); - /* highlight-add-end */ - - // .. -} -``` - -## Installation with Events Support - -_For the legacy backend system, please read the subsection below._ - -The catalog module `github-org` comes with events support enabled for the `GithubMultiOrgEntityProvider`. -This will make it subscribe to its relevant topics and expects these events to be published via the `EventsService`. - -Topics: - -- `github.installation` -- `github.membership` -- `github.organization` -- `github.team` - -Additionally, you should install the -[event router by `events-backend-module-github`](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-github/README.md) -which will route received events from the generic topic `github` to more specific ones -based on the event type (e.g., `github.membership`). - -In order to receive Webhook events by GitHub, you have to decide how you want them -to be ingested into Backstage and published to its `EventsService`. -You can decide between the following options (extensible): - -- [via HTTP endpoint](https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md) -- [via an AWS SQS queue](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md) - -### Legacy Backend System - -Please follow the installation instructions at - -- -- - -Additionally, you need to decide how you want to receive events from external sources like - -- [via HTTP endpoint](https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md) -- [via an AWS SQS queue](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md) - -Set up your provider - -```ts title="packages/backend/src/plugins/catalog.ts" -import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; -/* highlight-add-next-line */ -import { GithubOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-github'; -import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend'; -import { Router } from 'express'; -import { PluginEnvironment } from '../types'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - const builder = await CatalogBuilder.create(env); - builder.addProcessor(new ScaffolderEntitiesProcessor()); - /* highlight-add-start */ - const githubOrgProvider = GithubOrgEntityProvider.fromConfig(env.config, { - id: 'production', - orgUrl: 'https://github.com/backstage', - logger: env.logger, - events: env.events, - schedule: env.scheduler.createScheduledTaskRunner({ - frequency: { minutes: 60 }, - timeout: { minutes: 15 }, - }), - }); - builder.addEntityProvider(githubOrgProvider); - /* highlight-add-end */ - const { processingEngine, router } = await builder.build(); - await processingEngine.start(); - return router; -} -``` - -Or, alternatively, if using the `GithubMultiOrgEntityProvider`: - -```ts title="packages/backend/src/plugins/catalog.ts" -/* highlight-add-next-line */ -import { GithubMultiOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-github'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - const builder = await CatalogBuilder.create(env); - - /* highlight-add-start */ - // The GitHub URL below needs to match a configured integrations.github entry - // specified in your app-config. - builder.addEntityProvider( - GithubMultiOrgEntityProvider.fromConfig(env.config, { - id: 'production', - githubUrl: 'https://github.com', - // Set the following to list the GitHub orgs you wish to ingest from. You can - // also omit this option to ingest all orgs accessible by your GitHub integration - orgs: ['org-a', 'org-b'], - logger: env.logger, - events: env.events, - schedule: env.scheduler.createScheduledTaskRunner({ - frequency: { minutes: 60 }, - timeout: { minutes: 15 }, - }), - }), - ); - /* highlight-add-end */ - - // .. -} -``` - -You can check the official docs to [configure your webhook](https://docs.github.com/en/developers/webhooks-and-events/webhooks/creating-webhooks) and to [secure your request](https://docs.github.com/en/developers/webhooks-and-events/webhooks/securing-your-webhooks). -The webhook will need to be configured to forward `organization`,`team` and `membership` events. - -## Configuration - -As mentioned above, you also must have some configuration in your app-config -that describes the targets that you want to import. This lets the entity -provider know what authorization to use, and what the API endpoints are. You may -or may not have such an entry already added since before: - -```yaml -integrations: - github: - # example for public github - - host: github.com - token: ${GITHUB_TOKEN} - # example for a private GitHub Enterprise instance - - host: ghe.example.net - apiBaseUrl: https://ghe.example.net/api/v3 - token: ${GHE_TOKEN} -``` - -These examples use `${}` placeholders to reference environment variables. This -is often suitable for production setups, but also means that you will have to -supply those variables to the backend as it starts up. If you want, for local -development in particular, you can experiment first by putting the actual tokens -in a mirrored config directly in your `app-config.local.yaml` as well. - -If Backstage is configured to use GitHub Apps authentication you must grant -`Read-Only` access for `Members` under `Organization` in order to ingest users -correctly. You can modify the app's permissions under the organization settings, -`https://github.com/organizations/{ORG}/settings/apps/{APP_NAME}/permissions`. - -![permissions](../../assets/integrations/github/permissions.png) - -**Please note that when you change permissions, the app owner will get an email -that must be approved first before the changes are applied.** - -![email](../../assets/integrations/github/email.png) - -### Custom Transformers - -You can inject your own transformation logic to help map from GH API responses -into backstage entities. You can do this on the user and team requests to -enable you to do further processing or updates to the entities. - -To enable this you pass a function into the `GitHubOrgEntityProvider`. You can -pass a `UserTransformer`, `TeamTransformer` or both. The function is invoked -for each item (user or team) that is returned from the API. You can either -return an Entity (User or Group) or `undefined` if you do not want to import -that item. - -There is also a `defaultUserTransformer` and `defaultOrganizationTeamTransformer`. -You could use these and simply decorate the response from the default -transformation if you only need to change a few properties. - -### Resolving GitHub users via organization email - -When you authenticate users you should resolve them to an entity within the -catalog. Often the authentication you use could be a corporate SSO system that -provides you with email as a key. To enable you to find and resolve GitHub users -it's useful to also import the private domain verified emails into the User -entity in backstage. - -The integration attempts to return `organizationVerifiedDomainEmails` from the -GitHub API and makes this available as part of the object passed to -`UserTransformer`. The GitHub API will only return emails that use a domain -that's a verified domain for your GitHub Org. It also relies on the user having -configured such an email in their own account. The API will only return these -values when using GitHub App authentication and with the correct app permission -allowing access to emails. - -You can decorate the default `userTransformer` to replace the org email in the -returned identity. - -```ts title="packages/backend/src/plugins/catalog.ts" -const githubOrgProvider = GithubOrgEntityProvider.fromConfig(env.config, { - id: 'production', - orgUrl: 'https://github.com/backstage', - logger: env.logger, - schedule: env.scheduler.createScheduledTaskRunner({ - frequency: { minutes: 60 }, - timeout: { minutes: 15 }, - }), - /* highlight-add-start */ - userTransformer: async (user, ctx) => { - const entity = await defaultUserTransformer(user, ctx); - if (entity && user.organizationVerifiedDomainEmails?.length) { - entity.spec.profile!.email = user.organizationVerifiedDomainEmails[0]; - } - return entity; - }, - /* highlight-add-end */ -}); -``` - -Once you have imported the emails you can resolve users in your [sign-in resolver](../../auth/github/provider.md) using the catalog entity search via email - -```typescript title="packages/backend/src/plugins/auth.ts" -ctx.signInWithCatalogUser({ - filter: { - kind: ['User'], - 'spec.profile.email': email as string, - }, -}); -``` - -## Using a Processor instead of a Provider - -An alternative to using the Provider for ingesting organizational entities is to -use a Processor. This is the old way that's based on registering locations with -the proper type and target, triggering the processor to run. - -The drawback of this method is that it will leave orphaned Group/User entities -whenever they are deleted on your GitHub server, and you cannot control the -frequency with which they are refreshed, separately from other processors. - -### Processor Installation - -The `GithubOrgReaderProcessor` is not registered by default, so you have to -install and register it in the catalog plugin: - -```bash title="From your Backstage root directory" -yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-github -``` - -```typescript title="packages/backend/src/plugins/catalog.ts" -import { GithubOrgReaderProcessor } from '@backstage/plugin-catalog-backend-module-github'; - -builder.addProcessor( - GithubOrgReaderProcessor.fromConfig(env.config, { logger: env.logger }), -); -``` - -### Processor Configuration - -The integration section of your app-config needs to be set up in the same way as -for the Entity Provider - see above. - -In addition to that, you typically want to add a few static locations to your -app-config, which reference your organizations to import. The following -configuration enables an import of the teams and users under the org -`https://github.com/my-org-name` on public GitHub. - -```yaml -catalog: - locations: - - type: github-org - target: https://github.com/my-org-name - rules: - - allow: [User, Group] -``` diff --git a/docs/integrations/github/org.md b/docs/integrations/github/org.md index 7f0ee1a71f..3d0e0a4221 100644 --- a/docs/integrations/github/org.md +++ b/docs/integrations/github/org.md @@ -7,7 +7,7 @@ description: Importing users and groups from a GitHub organization into Backstag --- :::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](./org--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! +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](https://github.com/backstage/backstage/blob/v1.37.0/docs/integrations/github/org--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! ::: The Backstage catalog can be set up to ingest organizational data - users and diff --git a/docs/permissions/custom-rules--old.md b/docs/permissions/custom-rules--old.md deleted file mode 100644 index 7661e23327..0000000000 --- a/docs/permissions/custom-rules--old.md +++ /dev/null @@ -1,165 +0,0 @@ ---- -id: custom-rules--old -title: Defining custom permission rules -description: How to define custom permission rules for existing resources ---- - -:::info -This documentation is written for the old backend which has been replaced by [the new backend system](../backend-system/index.md), being the default since Backstage [version 1.24](../releases/v1.24.0.md). If have migrated to the new backend system, you may want to read [its own article](./custom-rules.md) instead. Otherwise, [consider migrating](../backend-system/building-backends/08-migrating.md)! -::: - -For some use cases, you may want to define custom [rules](../references/glossary.md#rule-permission-plugin) in addition to the ones provided by a plugin. In the [previous section](./writing-a-policy.md) we used the `isEntityOwner` rule to control access for catalog entities. Let's extend this policy with a custom rule that checks what [system](https://backstage.io/docs/features/software-catalog/system-model#system) an entity is part of. - -## Define a custom rule - -Plugins should export a rule factory that provides type-safety that ensures compatibility with the plugin's backend. The catalog plugin exports `createCatalogPermissionRule` from `@backstage/plugin-catalog-backend/alpha` for this purpose. Note: the `/alpha` path segment is temporary until this API is marked as stable. For this example, we'll define the rule and create a condition in `packages/backend/src/plugins/permission.ts`. - -We use Zod in our example below. To install, run: - -```bash -yarn workspace backend add zod -``` - -```typescript title="packages/backend/src/plugins/permission.ts" -... - -import type { Entity } from '@backstage/catalog-model'; -import { createCatalogPermissionRule } from '@backstage/plugin-catalog-backend/alpha'; -import { createConditionFactory } from '@backstage/plugin-permission-node'; -import { z } from 'zod'; - -export const isInSystemRule = createCatalogPermissionRule({ - name: 'IS_IN_SYSTEM', - description: 'Checks if an entity is part of the system provided', - resourceType: 'catalog-entity', - paramsSchema: z.object({ - systemRef: z - .string() - .describe('SystemRef to check the resource is part of'), - }), - apply: (resource: Entity, { systemRef }) => { - if (!resource.relations) { - return false; - } - - return resource.relations - .filter(relation => relation.type === 'partOf') - .some(relation => relation.targetRef === systemRef); - }, - toQuery: ({ systemRef }) => ({ - key: 'relations.partOf', - values: [systemRef], - }), -}); - -const isInSystem = createConditionFactory(isInSystemRule); - -... -``` - -For a more detailed explanation on defining rules, refer to the [documentation for plugin authors](./plugin-authors/03-adding-a-resource-permission-check.md#adding-support-for-conditional-decisions). - -Still in the `packages/backend/src/plugins/permission.ts` file, let's use the condition we just created in our `TestPermissionPolicy`. - -```ts title="packages/backend/src/plugins/permission.ts" -... -/* highlight-remove-next-line */ -import { createCatalogPermissionRule } from '@backstage/plugin-catalog-backend/alpha'; -/* highlight-add-next-line */ -import { catalogConditions, createCatalogConditionalDecision, createCatalogPermissionRule } from '@backstage/plugin-catalog-backend/alpha'; -/* highlight-remove-next-line */ -import { createConditionFactory } from '@backstage/plugin-permission-node'; -/* highlight-add-next-line */ -import { PermissionPolicy, PolicyQuery, PolicyQueryUser, createConditionFactory } from '@backstage/plugin-permission-node'; -/* highlight-add-start */ -import { AuthorizeResult, PolicyDecision, isResourcePermission } from '@backstage/plugin-permission-common'; -/* highlight-add-end */ -... - -export const isInSystemRule = createCatalogPermissionRule({ - name: 'IS_IN_SYSTEM', - description: 'Checks if an entity is part of the system provided', - resourceType: 'catalog-entity', - paramsSchema: z.object({ - systemRef: z - .string() - .describe('SystemRef to check the resource is part of'), - }), - apply: (resource: Entity, { systemRef }) => { - if (!resource.relations) { - return false; - } - - return resource.relations - .filter(relation => relation.type === 'partOf') - .some(relation => relation.targetRef === systemRef); - }, - toQuery: ({ systemRef }) => ({ - key: 'relations.partOf', - values: [systemRef], - }), -}); - -const isInSystem = createConditionFactory(isInSystemRule); - -class TestPermissionPolicy implements PermissionPolicy { - async handle( - request: PolicyQuery, - user?: PolicyQueryUser, - ): Promise { - if (isResourcePermission(request.permission, 'catalog-entity')) { - return createCatalogConditionalDecision( - request.permission, - /* highlight-remove-start */ - catalogConditions.isEntityOwner({ - claims: user?.info.ownershipEntityRefs ?? [], - }), - /* highlight-remove-end */ - /* highlight-add-start */ - { - anyOf: [ - catalogConditions.isEntityOwner({ - claims: user?.info.ownershipEntityRefs ?? [], - }), - isInSystem({ systemRef: 'interviewing' }), - ], - }, - /* highlight-add-end */ - ); - } - - return { result: AuthorizeResult.ALLOW }; - } -} - -... -``` - -## Provide the rule during plugin setup - -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 some integration point during the creation of the backend router. For the catalog, this integration point is exposed via `CatalogBuilder.addPermissionRules`. - -```typescript title="packages/backend/src/plugins/catalog.ts" -import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; -/* highlight-add-next-line */ -import { isInSystemRule } from './permission'; - -... - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - const builder = await CatalogBuilder.create(env); - /* highlight-add-next-line */ - builder.addPermissionRules(isInSystemRule); - ... - return router; -} -``` - -The updated policy will allow catalog entity resource permissions if any of the following are true: - -- User owns the target entity -- Target entity is part of the 'interviewing' system diff --git a/docs/permissions/custom-rules.md b/docs/permissions/custom-rules.md index bd35f5de8e..61b9a5326a 100644 --- a/docs/permissions/custom-rules.md +++ b/docs/permissions/custom-rules.md @@ -5,7 +5,7 @@ description: How to define custom permission rules for existing resources --- :::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](./custom-rules--old.md) instead, and [consider migrating](../backend-system/building-backends/08-migrating.md)! +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](https://github.com/backstage/backstage/blob/v1.37.0/docs/permissions/custom-rules--old.md) instead, and [consider migrating](../backend-system/building-backends/08-migrating.md)! ::: For some use cases, you may want to define custom [rules](../references/glossary.md#rule-permission-plugin) in addition to the ones provided by a plugin. In the [previous section](./writing-a-policy.md) we used the `isEntityOwner` rule to control access for catalog entities. Let's extend this policy with a custom rule that checks what [system](https://backstage.io/docs/features/software-catalog/system-model#system) an entity is part of. diff --git a/docs/permissions/getting-started--old.md b/docs/permissions/getting-started--old.md deleted file mode 100644 index 96dbfec92f..0000000000 --- a/docs/permissions/getting-started--old.md +++ /dev/null @@ -1,169 +0,0 @@ ---- -id: getting-started--old -title: Getting Started -description: How to get started with the permission framework as an integrator ---- - -:::info -This documentation is written for the old backend which has been replaced by [the new backend system](../backend-system/index.md), being the default since Backstage [version 1.24](../releases/v1.24.0.md). If have migrated to the new backend system, you may want to read [its own article](./getting-started.md) instead. Otherwise, [consider migrating](../backend-system/building-backends/08-migrating.md)! -::: - -If you prefer to watch a video instead, you can start with this video introduction: - - - -:::note Note - -This video was recorded in the January 2022 Contributors Session using `@backstage/create-app@0.4.14`. Some aspects of the demo may have changed in later releases. - -::: - -Backstage integrators control permissions by writing a policy. In general terms, a policy is simply an async function which receives a request to authorize a specific action for a user and (optional) resource, and returns a decision on whether to authorize that permission. Integrators can implement their own policies from scratch, or adopt reusable policies written by others. - -## Prerequisites - -The permissions framework depends on a few other Backstage systems, which must be set up before we can dive into writing a policy. - -### Upgrade to the latest version of Backstage - -The permissions framework itself is new to Backstage and still evolving quickly. To ensure your version of Backstage has all the latest permission-related functionality, it’s important to upgrade to the latest version. The [Backstage upgrade helper](https://backstage.github.io/upgrade-helper/) is a great tool to help ensure that you’ve made all the necessary changes during the upgrade! - -### Enable service-to-service authentication - -Service-to-service authentication allows Backstage backend code to verify that a given request originates from elsewhere in the Backstage backend. This is useful for tasks like collation of catalog entities in the search index. This type of request shouldn’t be permissioned, so it’s important to configure this feature before trying to use the permissions framework. - -To set up service-to-service authentication, follow the [service-to-service authentication docs](../auth/service-to-service-auth.md). - -### Supply an identity resolver to populate group membership on sign in - -**Note**: If you are working off of an existing Backstage instance, you likely already have some form of an identity resolver set up. - -Like many other parts of Backstage, the permissions framework relies on information about group membership. This simplifies authoring policies through the use of groups, rather than requiring each user to be listed in the configuration. Group membership is also often useful for conditional permissions, for example allowing permissions to act on an entity to be granted when a user is a member of a group that owns that entity. - -[The IdentityResolver docs](../auth/identity-resolver.md) describe the process for resolving group membership on sign in. - -## Optionally add cookie-based authentication - -Asset requests initiated by the browser will not include a token in the `Authorization` header. If these requests check authorization through the permission framework, as done in plugins like TechDocs, then you'll need to set up cookie-based authentication. Refer to the ["Authenticate API requests"](https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/authenticate-api-requests.md) tutorial for a demonstration on how to implement this behavior. - -## Integrating the permission framework with your Backstage instance - -### 1. Set up the permission backend - -The permissions framework uses a new `permission-backend` plugin to accept authorization requests from other plugins across your Backstage instance. The Backstage backend does not include this permission backend by default, so you will need to add it: - -1. Add `@backstage/plugin-permission-backend` as a dependency of your Backstage backend: - - ```bash title="From your Backstage root directory" - yarn --cwd packages/backend add @backstage/plugin-permission-backend - ``` - -2. Add the following to a new file, `packages/backend/src/plugins/permission.ts`. This adds the permission-backend router, and configures it with a policy which allows everything. - - ```typescript title="packages/backend/src/plugins/permission.ts" - import { createRouter } from '@backstage/plugin-permission-backend'; - import { - AuthorizeResult, - PolicyDecision, - } from '@backstage/plugin-permission-common'; - import { PermissionPolicy } from '@backstage/plugin-permission-node'; - import { Router } from 'express'; - import { PluginEnvironment } from '../types'; - - class TestPermissionPolicy implements PermissionPolicy { - async handle(): Promise { - return { result: AuthorizeResult.ALLOW }; - } - } - - export default async function createPlugin( - env: PluginEnvironment, - ): Promise { - return await createRouter({ - config: env.config, - logger: env.logger, - discovery: env.discovery, - policy: new TestPermissionPolicy(), - identity: env.identity, - }); - } - ``` - -3. Wire up the permission policy in `packages/backend/src/index.ts`. [The index in the example backend](https://github.com/backstage/backstage/blob/master/packages/backend/src/index.ts) shows how to do this. You’ll need to import the module from the previous step, create a plugin environment, and add the router to the express app: - - ```ts title="packages/backend/src/index.ts" - import proxy from './plugins/proxy'; - import techdocs from './plugins/techdocs'; - import search from './plugins/search'; - /* highlight-add-next-line */ - import permission from './plugins/permission'; - - async function main() { - const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs')); - const searchEnv = useHotMemoize(module, () => createEnv('search')); - const appEnv = useHotMemoize(module, () => createEnv('app')); - /* highlight-add-next-line */ - const permissionEnv = useHotMemoize(module, () => createEnv('permission')); - // .. - - apiRouter.use('/techdocs', await techdocs(techdocsEnv)); - apiRouter.use('/proxy', await proxy(proxyEnv)); - apiRouter.use('/search', await search(searchEnv)); - /* highlight-add-next-line */ - apiRouter.use('/permission', await permission(permissionEnv)); - // .. - } - ``` - -### 2. Enable and test the permissions system - -Now that the permission backend is running, it’s time to enable the permissions framework and make sure it’s working properly. - -1. Set the property `permission.enabled` to `true` in `app-config.yaml`. - - ```yaml title="app-config.yaml" - permission: - enabled: true - ``` - -2. Update the PermissionPolicy in `packages/backend/src/plugins/permission.ts` to disable a permission that’s easy for us to test. This policy rejects any attempt to delete a catalog entity: - - ```ts title="packages/backend/src/plugins/permission.ts" - import { createRouter } from '@backstage/plugin-permission-backend'; - import { - AuthorizeResult, - PolicyDecision, - } from '@backstage/plugin-permission-common'; - /* highlight-remove-next-line */ - import { PermissionPolicy } from '@backstage/plugin-permission-node'; - /* highlight-add-start */ - import { - PermissionPolicy, - PolicyQuery, - } from '@backstage/plugin-permission-node'; - /* highlight-add-end */ - import { Router } from 'express'; - import { PluginEnvironment } from '../types'; - - class TestPermissionPolicy implements PermissionPolicy { - /* highlight-remove-next-line */ - async handle(): Promise { - /* highlight-add-start */ - async handle(request: PolicyQuery): Promise { - if (request.permission.name === 'catalog.entity.delete') { - return { - result: AuthorizeResult.DENY, - }; - } - /* highlight-add-end */ - - return { result: AuthorizeResult.ALLOW }; - } - } - ``` - -3. Now that you’ve made this change, you should find that the unregister entity menu option on the catalog entity page is disabled. - -![Entity detail page showing disabled unregister entity context menu entry](../assets/permissions/disabled-unregister-entity.png) - -Now that the framework is fully configured, you can craft a permission policy that works best for your organization by utilizing a provided authorization method or by [writing your own policy](./writing-a-policy.md)! diff --git a/docs/permissions/getting-started.md b/docs/permissions/getting-started.md index cb0d29ef82..f5b9092ec5 100644 --- a/docs/permissions/getting-started.md +++ b/docs/permissions/getting-started.md @@ -5,7 +5,7 @@ description: How to get started with the permission framework as an integrator --- :::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](./getting-started--old.md) instead, and [consider migrating](../backend-system/building-backends/08-migrating.md)! +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](https://github.com/backstage/backstage/blob/v1.37.0/docs/permissions/getting-started--old.md) instead, and [consider migrating](../backend-system/building-backends/08-migrating.md)! ::: Backstage integrators control permissions by writing a policy. In general terms, a policy is simply an async function which receives a request to authorize a specific action for a user and (optional) resource, and returns a decision on whether to authorize that permission. Integrators can implement their own policies from scratch, or adopt reusable policies written by others. diff --git a/docs/permissions/plugin-authors/01-setup--old.md b/docs/permissions/plugin-authors/01-setup--old.md deleted file mode 100644 index 34802d004f..0000000000 --- a/docs/permissions/plugin-authors/01-setup--old.md +++ /dev/null @@ -1,134 +0,0 @@ ---- -id: 01-setup--old -title: 1. Tutorial setup -description: How to get started with the permission framework as a plugin author ---- - -:::info -This documentation is written for the old backend which has been replaced by [the new backend system](../../backend-system/index.md), being the default since Backstage [version 1.24](../../releases/v1.24.0.md). If have migrated to the new backend system, you may want to read [its own article](./01-setup.md) instead. Otherwise, [consider migrating](../../backend-system/building-backends/08-migrating.md)! -::: - -The following tutorial is designed to help plugin authors add support for permissions to their plugins. We'll add support for permissions to example `todo-list` and `todo-list-backend` plugins, but the process should be similar for other plugins! - -The rest of this page is focused on adding the `todo-list` and `todo-list-backend` plugins to your Backstage instance. If you want to add support for permissions to your own plugin instead, feel free to skip to the [next section](./02-adding-a-basic-permission-check.md). - -## Setup for the Tutorial - -We will use a "Todo list" feature, composed of the `todo-list` and `todo-list-backend` plugins, as well as their dependency, `todo-list-common`. - -The source code is available here: - -- [todo-list](https://github.com/backstage/backstage/blob/master/plugins/example-todo-list) -- [todo-list-backend](https://github.com/backstage/backstage/blob/master/plugins/example-todo-list-backend) -- [todo-list-common](https://github.com/backstage/backstage/blob/master/plugins/example-todo-list-common) - -1. Copy-paste the three folders into the plugins folder of your backstage application repository (removing the `example-` prefix from each folder) or run the following script from the root of your backstage application: - - ```bash - $ cd $(mktemp -d) - git clone --depth 1 --quiet --no-checkout --filter=blob:none https://github.com/backstage/backstage.git . - git checkout master -- plugins/example-todo-list/ - git checkout master -- plugins/example-todo-list-backend/ - git checkout master -- plugins/example-todo-list-common/ - sed -i '' 's/workspace:\^/\*/g' plugins/example-todo-list/package.json - sed -i '' 's/workspace:\^/\*/g' plugins/example-todo-list-backend/package.json - sed -i '' 's/workspace:\^/\*/g' plugins/example-todo-list-common/package.json - for file in plugins/*; do mv "$file" "$OLDPWD/${file/example-todo/todo}"; done - cd - - ``` - - The `plugins` directory of your project should now include `todo-list`, `todo-list-backend`, and `todo-list-common`. - - **Important**: if you are on **Windows**, make sure you have WSL and git installed on your machine before executing the script above. - -2. Add these packages as dependencies for your Backstage app: - - ```sh title="From your Backstage root directory" - yarn --cwd packages/backend add @internal/plugin-todo-list-backend @internal/plugin-todo-list-common - yarn --cwd packages/app add @internal/plugin-todo-list - ``` - -3. Include the backend and frontend plugin in your application: - - Create a new `packages/backend/src/plugins/todolist.ts` with the following content: - - ```typescript title="packages/backend/src/plugins/todolist.ts" - import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; - import { createRouter } from '@internal/plugin-todo-list-backend'; - import { Router } from 'express'; - import { PluginEnvironment } from '../types'; - - export default async function createPlugin({ - logger, - discovery, - }: PluginEnvironment): Promise { - return await createRouter({ - logger, - identity: DefaultIdentityClient.create({ - discovery, - issuer: await discovery.getExternalBaseUrl('auth'), - }), - }); - } - ``` - - Apply the following changes to `packages/backend/src/index.ts`: - - ```ts title="packages/backend/src/index.ts" - import techdocs from './plugins/techdocs'; - /* highlight-add-next-line */ - import todoList from './plugins/todolist'; - import search from './plugins/search'; - - async function main() { - const searchEnv = useHotMemoize(module, () => createEnv('search')); - const appEnv = useHotMemoize(module, () => createEnv('app')); - /* highlight-add-next-line */ - const todoListEnv = useHotMemoize(module, () => createEnv('todolist')); - // .. - - apiRouter.use('/proxy', await proxy(proxyEnv)); - apiRouter.use('/search', await search(searchEnv)); - apiRouter.use('/permission', await permission(permissionEnv)); - /* highlight-add-next-line */ - apiRouter.use('/todolist', await todoList(todoListEnv)); - // Add backends ABOVE this line; this 404 handler is the catch-all fallback - apiRouter.use(notFoundHandler()); - // .. - } - ``` - - Apply the following changes to `packages/app/src/App.tsx`: - - ```tsx title="packages/app/src/App.tsx" - /* highlight-add-next-line */ - import { TodoListPage } from '@internal/plugin-todo-list'; - - const routes = ( - - }> - {searchPage} - - } /> - {/* highlight-add-next-line */} - } /> - {/* ... */} - - ); - ``` - -Now if you start your application you should be able to reach the `/todo-list` page: - -![Todo List plugin page](../../assets/permissions/permission-todo-list-page.png) - ---- - -## Integrate the new plugin - -If you play with the UI, you will notice that it is possible to perform a few actions: - -- create a new todo item (`POST /todos`) -- view todo items (`GET /todos`) -- edit an existing todo item (`PUT /todos`) - -Let's try to bring authorization on top of each one of them. diff --git a/docs/permissions/plugin-authors/01-setup.md b/docs/permissions/plugin-authors/01-setup.md index 6ece0b0b69..743c5c9718 100644 --- a/docs/permissions/plugin-authors/01-setup.md +++ b/docs/permissions/plugin-authors/01-setup.md @@ -5,7 +5,7 @@ description: How to get started with the permission framework as a plugin author --- :::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](./01-setup--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! +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](https://github.com/backstage/backstage/blob/v1.37.0/docs/permissions/plugin-authors/01-setup--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! ::: The following tutorial is designed to help plugin authors add support for permissions to their plugins. We'll add support for permissions to example `todo-list` and `todo-list-backend` plugins, but the process should be similar for other plugins! diff --git a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check--old.md b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check--old.md deleted file mode 100644 index 7c8dad3d87..0000000000 --- a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check--old.md +++ /dev/null @@ -1,387 +0,0 @@ ---- -id: 02-adding-a-basic-permission-check--old -title: 2. Adding a basic permission check -description: Explains how to add a basic permission check to a Backstage plugin ---- - -:::info -This documentation is written for the old backend which has been replaced by [the new backend system](../../backend-system/index.md), being the default since Backstage [version 1.24](../../releases/v1.24.0.md). If have migrated to the new backend system, you may want to read [its own article](./02-adding-a-basic-permission-check.md) instead. Otherwise, [consider migrating](../../backend-system/building-backends/08-migrating.md)! -::: - -If the outcome of a permission check doesn't need to change for different [resources](../../references/glossary.md#resource-permission-plugin), you can use a _basic permission check_. For this kind of check, we simply need to define a permission, and call `authorize` with it. - -For this tutorial, we'll use a basic permission check to authorize the `create` endpoint in our todo-backend. This will allow Backstage integrators to control whether each of their users is authorized to create todos by adjusting their [permission policy](../../references/glossary.md#policy-permission-plugin). - -We'll start by creating a new permission, and then we'll use the permission api to call `authorize` with it during todo creation. - -## Creating a new permission - -Let's navigate to the file `plugins/todo-list-common/src/permissions.ts` and add our first permission: - -```ts title="plugins/todo-list-common/src/permissions.ts" -import { createPermission } from '@backstage/plugin-permission-common'; - -/* highlight-remove-start */ -export const tempExamplePermission = createPermission({ - name: 'temp.example.noop', - attributes: {}, -/* highlight-remove-end */ -/* highlight-add-start */ -export const todoListCreatePermission = createPermission({ - name: 'todo.list.create', - attributes: { action: 'create' }, -/* highlight-add-end */ -}); - -/* highlight-remove-next-line */ -export const todoListPermissions = [tempExamplePermission]; -/* highlight-add-next-line */ -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 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/tooling/cli/build-system#package-roles). This allows Backstage integrators to reference them in frontend components as well as permission policies. - -::: - -## Authorizing using the new permission - -Install the following module: - -``` -$ yarn workspace @internal/plugin-todo-list-backend \ - add @backstage/plugin-permission-common @backstage/plugin-permission-node @internal/plugin-todo-list-common -``` - -Edit `plugins/todo-list-backend/src/service/router.ts`: - -```ts title="plugins/todo-list-backend/src/service/router.ts" -/* highlight-remove-start */ -import { InputError } from '@backstage/errors'; -import { IdentityApi } from '@backstage/plugin-auth-node'; -/* highlight-remove-end */ -/* highlight-add-start */ -import { InputError, NotAllowedError } from '@backstage/errors'; -import { getBearerTokenFromAuthorizationHeader, IdentityApi } from '@backstage/plugin-auth-node'; -import { PermissionEvaluator, 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 { - logger: Logger; - identity: IdentityApi; - /* highlight-add-next-line */ - permissions: PermissionEvaluator; -} - -export async function createRouter( - options: RouterOptions, -): Promise { - /* highlight-remove-next-line */ - const { logger, identity } = options; - /* highlight-add-next-line */ - const { logger, identity, permissions } = options; - - /* highlight-add-start */ - const permissionIntegrationRouter = createPermissionIntegrationRouter({ - permissions: [todoListCreatePermission], - }); - /* highlight-add-end */ - - const router = Router(); - router.use(express.json()); - - router.get('/health', (_, response) => { - logger.info('PONG!'); - response.json({ status: 'ok' }); - }); - - /* highlight-add-next-line */ - router.use(permissionIntegrationRouter); - - router.get('/todos', async (_req, res) => { - res.json(getAll()); - }); - - router.post('/todos', async (req, res) => { - let author: string | undefined = undefined; - - const user = await identity.getIdentity({ request: req }); - author = user?.identity.userEntityRef; - /* highlight-add-start */ - const token = getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ); - const decision = ( - await permissions.authorize([{ permission: todoListCreatePermission }], { - token, - }) - )[0]; - - if (decision.result === AuthorizeResult.DENY) { - throw new NotAllowedError('Unauthorized'); - } - /* highlight-add-end */ - - if (!isTodoCreateRequest(req.body)) { - throw new InputError('Invalid payload'); - } - - const todo = add({ title: req.body.title, author }); - res.json(todo); - }); - - // ... -``` - -Pass the `permissions` object to the plugin in `packages/backend/src/plugins/todolist.ts`: - -```ts title="packages/backend/src/plugins/todolist.ts" -import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; -import { createRouter } from '@internal/plugin-todo-list-backend'; -import { Router } from 'express'; -import { PluginEnvironment } from '../types'; - -export default async function createPlugin({ - logger, - discovery, - /* highlight-add-next-line */ - permissions, -}: PluginEnvironment): Promise { - return await createRouter({ - logger, - identity: DefaultIdentityClient.create({ - discovery, - issuer: await discovery.getExternalBaseUrl('auth'), - }), - /* highlight-add-next-line */ - permissions, - }); -} -``` - -That's it! Now your plugin is fully configured. Let's try to test the logic by denying the permission. - -## Test the authorized create endpoint - -Before running this step, please make sure you followed the steps described in [Getting started](../getting-started.md) section. - -In order to test the logic above, the integrators of your backstage instance need to change their permission policy to return `DENY` for our newly-created permission: - -```ts title="packages/backend/src/plugins/permission.ts" -import { - PermissionPolicy, - /* highlight-add-start */ - PolicyQuery, - PolicyQueryUser, - /* highlight-add-end */ -} from '@backstage/plugin-permission-node'; -/* highlight-add-start */ -import { isPermission } from '@backstage/plugin-permission-common'; -import { todoListCreatePermission } from '@internal/plugin-todo-list-common'; -/* highlight-add-end */ - -class TestPermissionPolicy implements PermissionPolicy { - /* highlight-remove-next-line */ - async handle(): Promise { - /* highlight-add-start */ - async handle( - request: PolicyQuery, - _user?: PolicyQueryUser, - ): Promise { - if (isPermission(request.permission, todoListCreatePermission)) { - return { - result: AuthorizeResult.DENY, - }; - } - /* highlight-add-end */ - - return { - result: AuthorizeResult.ALLOW, - }; -} -``` - -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. - -```ts -if (isPermission(request.permission, todoListCreatePermission)) { - return { - /* highlight-remove-next-line */ - result: AuthorizeResult.DENY, - /* highlight-add-next-line */ - result: AuthorizeResult.ALLOW, - }; -} -``` - -At this point everything is working but if you run `yarn tsc` you'll get some errors, let's fix those up. - -First we'll clean up the `plugins/todo-list-backend/src/service/router.test.ts`: - -```ts title="plugins/todo-list-backend/src/service/router.test.ts" -import { getVoidLogger } from '@backstage/backend-common'; -import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; -/* highlight-add-next-line */ -import { PermissionEvaluator } from '@backstage/plugin-permission-common'; -import express from 'express'; -import request from 'supertest'; - -import { createRouter } from './router'; - -/* highlight-add-start */ -const mockedAuthorize: jest.MockedFunction = - jest.fn(); -const mockedPermissionQuery: jest.MockedFunction< - PermissionEvaluator['authorizeConditional'] -> = jest.fn(); - -const permissionEvaluator: PermissionEvaluator = { - authorize: mockedAuthorize, - authorizeConditional: mockedPermissionQuery, -}; -/* highlight-add-end */ - -describe('createRouter', () => { - let app: express.Express; - - beforeAll(async () => { - const router = await createRouter({ - logger: getVoidLogger(), - identity: {} as DefaultIdentityClient, - /* highlight-add-next-line */ - permissions: permissionEvaluator, - }); - app = express().use(router); - }); - - beforeEach(() => { - jest.resetAllMocks(); - }); - - describe('GET /health', () => { - it('returns ok', async () => { - const response = await request(app).get('/health'); - - expect(response.status).toEqual(200); - expect(response.body).toEqual({ status: 'ok' }); - }); - }); -}); -``` - -Then we want to update the `plugins/todo-list-backend/src/service/standaloneServer.ts`: - -```ts title="plugins/todo-list-backend/src/service/standaloneServer.ts" -import { - createServiceBuilder, - loadBackendConfig, - SingleHostDiscovery, - /* highlight-add-next-line */ - ServerTokenManager, -} from '@backstage/backend-common'; -import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; -/* highlight-add-next-line */ -import { ServerPermissionClient } from '@backstage/plugin-permission-node'; -import { Server } from 'http'; -import { Logger } from 'winston'; -import { createRouter } from './router'; - -export interface ServerOptions { - port: number; - enableCors: boolean; - logger: Logger; -} - -export async function startStandaloneServer( - options: ServerOptions, -): Promise { - const logger = options.logger.child({ service: 'todo-list-backend' }); - logger.debug('Starting application server...'); - const config = await loadBackendConfig({ logger, argv: process.argv }); - const discovery = SingleHostDiscovery.fromConfig(config); - /* highlight-add-start */ - const tokenManager = ServerTokenManager.fromConfig(config, { - logger, - }); - const permissions = ServerPermissionClient.fromConfig(config, { - discovery, - tokenManager, - }); - /* highlight-add-end */ - const router = await createRouter({ - logger, - identity: DefaultIdentityClient.create({ - discovery, - issuer: await discovery.getExternalBaseUrl('auth'), - }), - /* highlight-add-next-line */ - permissions, - }); - - let service = createServiceBuilder(module) - .setPort(options.port) - .addRouter('/todo-list', router); - if (options.enableCors) { - service = service.enableCors({ origin: 'http://localhost:3000' }); - } - - return await service.start().catch(err => { - logger.error(err); - process.exit(1); - }); -} - -module.hot?.accept(); -``` - -Finally, we need to update `plugins/todo-list-backend/src/plugin.ts`: - -```ts title="plugins/todo-list-backend/src/plugin.ts" -import { loggerToWinstonLogger } from '@backstage/backend-common'; -import { - coreServices, - createBackendPlugin, -} from '@backstage/backend-plugin-api'; -import { createRouter } from './service/router'; - -/** -* The example TODO list backend plugin. -* -* @public -*/ -export const exampleTodoListPlugin = createBackendPlugin({ - pluginId: 'exampleTodoList', - register(env) { - env.registerInit({ - deps: { - identity: coreServices.identity, - logger: coreServices.logger, - httpRouter: coreServices.httpRouter, - /* highlight-add-next-line */ - permissions: coreServices.permissions, - }, - /* highlight-remove-next-line */ - async init({ identity, logger, httpRouter }) { - /* highlight-add-next-line */ - async init({ identity, logger, httpRouter, permissions }) { - httpRouter.use( - await createRouter({ - identity, - logger: loggerToWinstonLogger(logger), - permissions, - }), - ); - }, - }); - }, -}); -``` - -Now when you run `yarn tsc` you should have no more errors. diff --git a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md index 4053a81f99..8ba3ac3152 100644 --- a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md +++ b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md @@ -5,7 +5,7 @@ description: Explains how to add a basic permission check to 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](./02-adding-a-basic-permission-check--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! +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](https://github.com/backstage/backstage/blob/v1.37.0/docs/permissions/plugin-authors/02-adding-a-basic-permission-check--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! ::: If the outcome of a permission check doesn't need to change for different [resources](../../references/glossary.md#resource-permission-plugin), you can use a _basic permission check_. For this kind of check, we simply need to define a permission, and call `authorize` with it. diff --git a/docs/permissions/plugin-authors/03-adding-a-resource-permission-check--old.md b/docs/permissions/plugin-authors/03-adding-a-resource-permission-check--old.md deleted file mode 100644 index ee06629e71..0000000000 --- a/docs/permissions/plugin-authors/03-adding-a-resource-permission-check--old.md +++ /dev/null @@ -1,299 +0,0 @@ ---- -id: 03-adding-a-resource-permission-check--old -title: 3. Adding a resource permission check -description: Explains how to add a resource permission check to a Backstage plugin ---- - -:::info -This documentation is written for the old backend which has been replaced by [the new backend system](../../backend-system/index.md), being the default since Backstage [version 1.24](../../releases/v1.24.0.md). If have migrated to the new backend system, you may want to read [its own article](./03-adding-a-resource-permission-check.md) instead. Otherwise, [consider migrating](../../backend-system/building-backends/08-migrating.md)! -::: - -When performing updates (or other operations) on specific [resources](../../references/glossary.md#resource-permission-plugin), the permissions framework allows for the decision to be based on characteristics of the resource itself. This means that it's possible to write policies that (for example) allow the operation for users that own a resource, and deny the operation otherwise. - -## Creating the update permission - -Let's add a new permission to the file `plugins/todo-list-common/src/permissions.ts` from [the previous section](./02-adding-a-basic-permission-check.md). - -```ts title="plugins/todo-list-common/src/permissions.ts" -import { createPermission } from '@backstage/plugin-permission-common'; - -/* highlight-add-next-line */ -export const TODO_LIST_RESOURCE_TYPE = 'todo-item'; - -export const todoListCreatePermission = createPermission({ - name: 'todo.list.create', - attributes: { action: 'create' }, -}); - -/* highlight-add-start */ -export const todoListUpdatePermission = createPermission({ - name: 'todo.list.update', - attributes: { action: 'update' }, - resourceType: TODO_LIST_RESOURCE_TYPE, -}); -/* highlight-add-end */ - -/* highlight-remove-next-line */ -export const todoListPermissions = [todoListCreatePermission]; -/* highlight-add-start */ -export const todoListPermissions = [ - todoListCreatePermission, - todoListUpdatePermission, -]; -/* highlight-add-end */ -``` - -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: - -```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 */ - -// ... - -const permissionIntegrationRouter = createPermissionIntegrationRouter({ - /* highlight-remove-next-line */ - permissions: [todoListCreatePermission], - /* highlight-add-next-line */ - permissions: [todoListCreatePermission, todoListUpdatePermission], -}); - -// ... - -router.put('/todos', async (req, res) => { - /* highlight-add-start */ - const token = getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ); - /* highlight-add-end */ - - if (!isTodoUpdateRequest(req.body)) { - throw new InputError('Invalid payload'); - } - /* highlight-add-start */ - const decision = ( - await permissions.authorize( - [{ permission: todoListUpdatePermission, resourceRef: req.body.id }], - { - token, - }, - ) - )[0]; - - if (decision.result !== AuthorizeResult.ALLOW) { - throw new NotAllowedError('Unauthorized'); - } - /* highlight-add-end */ - - res.json(update(req.body)); -}); -``` - -**Important:** Notice that we are passing an extra `resourceRef` field, with the `id` of the todo item as the value. - -This enables decisions based on characteristics of the resource, but it's important to note that policy authors will not have access to the resource ref inside of their permission policies. Instead, the policies will return conditional decisions, which we need to now support in our plugin. - -## Adding support for conditional decisions - -Install the missing module: - -```bash -$ yarn workspace @internal/plugin-todo-list-backend add zod -``` - -Create a new `plugins/todo-list-backend/src/service/rules.ts` file and append the following code: - -```typescript title="plugins/todo-list-backend/src/service/rules.ts" -import { makeCreatePermissionRule } from '@backstage/plugin-permission-node'; -import { TODO_LIST_RESOURCE_TYPE } from '@internal/plugin-todo-list-common'; -import { z } from 'zod'; -import { Todo, TodoFilter } from './todos'; - -export const createTodoListPermissionRule = makeCreatePermissionRule< - Todo, - TodoFilter, - typeof TODO_LIST_RESOURCE_TYPE ->(); - -export const isOwner = createTodoListPermissionRule({ - name: 'IS_OWNER', - description: 'Should allow only if the todo belongs to the user', - resourceType: TODO_LIST_RESOURCE_TYPE, - paramsSchema: z.object({ - userId: z.string().describe('User ID to match on the resource'), - }), - apply: (resource: Todo, { userId }) => { - return resource.author === userId; - }, - toQuery: ({ userId }) => { - return { - property: 'author', - values: [userId], - }; - }, -}); - -export const rules = { isOwner }; -``` - -`makeCreatePermissionRule` is a helper used to ensure that rules created for this plugin use consistent types for the resource and query. - -:::note Note - -To support custom rules defined by Backstage integrators, you must export `createTodoListPermissionRule` from the backend package and provide some way for custom rules to be passed in before the backend starts, likely via `createRouter`. - -::: - -We have created a new `isOwner` rule, which is going to be automatically used by the permission framework whenever a conditional response is returned in response to an authorized request with an attached `resourceRef`. -Specifically, the `apply` function is used to understand whether the passed resource should be authorized or not. - -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: - -- `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" -// ... -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 { rules } from './rules'; -/* highlight-add-end */ - -export async function createRouter( - options: RouterOptions, -): Promise { - 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()); - - // ... -} -``` - -## Provide utilities for policy authors - -Now that we have a new resource type and a corresponding rule, we need to export some utilities for policy authors to reference them. - -Create a new `plugins/todo-list-backend/src/conditionExports.ts` file and add the following code: - -```typescript title="plugins/todo-list-backend/src/conditionExports.ts" -import { TODO_LIST_RESOURCE_TYPE } from '@internal/plugin-todo-list-common'; -import { createConditionExports } from '@backstage/plugin-permission-node'; -import { rules } from './service/rules'; - -const { conditions, createConditionalDecision } = createConditionExports({ - pluginId: 'todolist', - resourceType: TODO_LIST_RESOURCE_TYPE, - rules, -}); - -export const todoListConditions = conditions; - -export const createTodoListConditionalDecision = createConditionalDecision; -``` - -Make sure `todoListConditions` and `createTodoListConditionalDecision` are exported from the `todo-list-backend` package by editing `plugins/todo-list-backend/src/index.ts`: - -```ts title="plugins/todo-list-backend/src/index.ts" -export * from './service/router'; -/* highlight-add-next-line */ -export * from './conditionExports'; -export { exampleTodoListPlugin } from './plugin'; -``` - -## Test the authorized update endpoint - -Let's go back to the permission policy's handle function and try to authorize our new permission with an `isOwner` condition. - -```ts title="packages/backend/src/plugins/permission.ts" -import { - IdentityClient -} from '@backstage/plugin-auth-node'; -import { - PermissionPolicy, - PolicyQuery, - PolicyQueryUser, -} from '@backstage/plugin-permission-node'; -import { isPermission } from '@backstage/plugin-permission-common'; -/* highlight-remove-next-line */ -import { todoListCreatePermission } from '@internal/plugin-todo-list-common'; -/* highlight-add-start */ -import { - todoListCreatePermission, - todoListUpdatePermission, -} from '@internal/plugin-todo-list-common'; -import { - todoListConditions, - createTodoListConditionalDecision, -} from '@internal/plugin-todo-list-backend'; -/* highlight-add-end */ - - -async handle( - request: PolicyQuery, - /* highlight-remove-next-line */ - _user?: PolicyQueryUser, - /* highlight-add-next-line */ - user?: PolicyQueryUser, -): Promise { - if (isPermission(request.permission, todoListCreatePermission)) { - return { - result: AuthorizeResult.ALLOW, - }; - } - /* highlight-add-start */ - if (isPermission(request.permission, todoListUpdatePermission)) { - return createTodoListConditionalDecision( - request.permission, - todoListConditions.isOwner({ - userId: user?.info.userEntityRef ?? '', - }), - ); - } - /* highlight-add-end */ - - return { - result: AuthorizeResult.ALLOW, - }; -} -``` - -For any incoming update requests, we now return a _Conditional Decision_. We are saying: - -> Hey permission framework, I can't make a decision alone. Please go to the plugin with id `todolist` and ask it to apply these conditions. - -To check that everything works as expected, you should now see an error in the UI whenever you try to edit an item that wasn’t created by you. Success! diff --git a/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md b/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md index dd8e88bb94..6ebbdf77e4 100644 --- a/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md +++ b/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md @@ -5,7 +5,7 @@ description: Explains how to add a resource permission check to a Backstage plug --- :::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](./03-adding-a-resource-permission-check--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! +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](https://github.com/backstage/backstage/blob/v1.37.0/docs/permissions/plugin-authors/03-adding-a-resource-permission-check--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! ::: When performing updates (or other operations) on specific [resources](../../references/glossary.md#resource-permission-plugin), the permissions framework allows for the decision to be based on characteristics of the resource itself. This means that it's possible to write policies that (for example) allow the operation for users that own a resource, and deny the operation otherwise. diff --git a/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data--old.md b/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data--old.md deleted file mode 100644 index cca6899ef6..0000000000 --- a/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data--old.md +++ /dev/null @@ -1,193 +0,0 @@ ---- -id: 04-authorizing-access-to-paginated-data--old -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 old backend which has been replaced by [the new backend system](../../backend-system/index.md), being the default since Backstage [version 1.24](../../releases/v1.24.0.md). If have migrated to the new backend system, you may want to read [its own article](./04-authorizing-access-to-paginated-data.md) instead. Otherwise, [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. - -One possible solution may leverage the batching functionality to authorize all of the todos, and then returning only the ones for which the decision was `ALLOW`: - -```ts -router.get('/todos', async (req, res) => { - /* highlight-add-next-line */ - const credentials = await httpAuth.credentials(req, { allow: ['user'] }); - - /* highlight-remove-next-line */ - res.json(getAll()); - /* highlight-add-start */ - const items = getAll(); - const decisions = await permissions.authorize( - items.map(({ id }) => ({ - permission: todoListReadPermission, - resourceRef: id, - })), - { credentials }, - ); - - const filteredItems = decisions.filter( - decision => decision.result === AuthorizeResult.ALLOW, - ); - res.json(filteredItems); - /* highlight-add-end */ -}); -``` - -This approach will work for simple cases, but it has a downside: it forces us to retrieve all the elements upfront and authorize them one by one. This forces the plugin implementation to handle concerns like pagination, which is currently handled by the data source. - -To avoid this situation, the permissions framework has support for filtering items in the data source itself. In this part of the tutorial, we'll describe the steps required to use that behavior. - -:::note Note - -In order to perform authorization filtering in this way, the data source must allow filters to be logically combined with AND, OR, and NOT operators. The conditional decisions returned by the permissions framework use a [nested object](https://backstage.io/docs/reference/plugin-permission-common.permissioncriteria) to combine conditions. If you're implementing a filter API from scratch, we recommend using the same shape for ease of interoperability. If not, you'll need to implement a function which transforms the nested object into your own format. - -::: - -## Creating the read permission - -Let's add another permission to the plugin. - -```ts title="plugins/todo-list-backend/src/service/permissions.ts" -import { createPermission } from '@backstage/plugin-permission-common'; - -export const TODO_LIST_RESOURCE_TYPE = 'todo-item'; - -export const todoListCreatePermission = createPermission({ - name: 'todo.list.create', - attributes: { action: 'create' }, -}); - -export const todoListUpdatePermission = createPermission({ - name: 'todo.list.update', - attributes: { action: 'update' }, - resourceType: TODO_LIST_RESOURCE_TYPE, -}); - -/* highlight-add-start */ -export const todoListReadPermission = createPermission({ - name: 'todos.list.read', - attributes: { action: 'read' }, - resourceType: TODO_LIST_RESOURCE_TYPE, -}); -/* highlight-add-end */ - -export const todoListPermissions = [ - todoListCreatePermission, - todoListUpdatePermission, - /* highlight-add-start */ - todoListReadPermission, - /* highlight-add-end */ -]; -``` - -## Using conditional policy decisions - -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'; -/* highlight-add-end */ -/* highlight-remove-next-line */ -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 */ - todoListReadPermission, -} from './permissions'; - -// ... - -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 = createConditionTransformer(Object.values(rules)); - -/* highlight-remove-next-line */ -router.get('/todos', async (_req, res) => { -/* highlight-add-start */ -router.get('/todos', async (req, res) => { - const credentials = await httpAuth.credentials(req, { allow: ['user'] }); - - const decision = ( - await permissions.authorizeConditional([{ permission: todoListReadPermission }], { - credentials, - }) - )[0]; - - if (decision.result === AuthorizeResult.DENY) { - throw new NotAllowedError('Unauthorized'); - } - - if (decision.result === AuthorizeResult.CONDITIONAL) { - const filter = transformConditions(decision.conditions); - res.json(getAll(filter)); - } else { - res.json(getAll()); - } -/* highlight-add-end */ - /* highlight-remove-next-line */ - res.json(getAll()); -}); -``` - -To make the process of handling conditional decisions easier, the permission framework provides a `createConditionTransformer` helper. This function accepts an array of permission rules, and returns a transformer function which converts the conditions to the format needed by the plugin using the `toQuery` method defined on each rule. - -Since `TodoFilter` used in our plugin matches the structure of the conditions object, we can directly pass the output of our condition transformer. If the filters were structured differently, we'd need to transform it further before passing it to the api. - -## Test the authorized read endpoint - -Let's update our permission policy to return a conditional result whenever a `todoListReadPermission` permission is received. In this case, we can reuse the decision returned for the `todosListCreate` permission. - -```ts title="packages/backend/src/plugins/permission.ts" -import { - todoListCreatePermission, - todoListUpdatePermission, - /* highlight-add-next-line */ - todoListReadPermission, -} from '@internal/plugin-todo-list-common'; - -/* highlight-remove-next-line */ -if (isPermission(request.permission, todoListUpdatePermission)) { -/* highlight-add-start */ -if ( - isPermission(request.permission, todoListUpdatePermission) || - isPermission(request.permission, todoListReadPermission) -) { -/* highlight-add-end */ - return createTodoListConditionalDecision( - request.permission, - todoListConditions.isOwner({ - userId: user?.identity.userEntityRef - }), - ); -} -``` - -Once the changes to the permission policy are saved, the UI should show only the todo items you've created. diff --git a/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md b/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md index c03183bbf0..b81382ee16 100644 --- a/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md +++ b/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md @@ -5,7 +5,7 @@ description: Explains how to authorize access to paginated data in a Backstage p --- :::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)! +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](https://github.com/backstage/backstage/blob/v1.37.0/docs/permissions/plugin-authors/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. diff --git a/docs/permissions/writing-a-policy--old.md b/docs/permissions/writing-a-policy--old.md deleted file mode 100644 index d95fa7ffb3..0000000000 --- a/docs/permissions/writing-a-policy--old.md +++ /dev/null @@ -1,148 +0,0 @@ ---- -id: writing-a-policy--old -title: Writing a permission policy -description: How to write your own permission policy as a Backstage integrator ---- - -:::info -This documentation is written for the old backend which has been replaced by [the new backend system](../backend-system/index.md), being the default since Backstage [version 1.24](../releases/v1.24.0.md). If have migrated to the new backend system, you may want to read [its own article](./writing-a-policy.md) instead. Otherwise, [consider migrating](../backend-system/building-backends/08-migrating.md)! -::: - -In the [previous section](./getting-started.md), we were able to set up the permission framework and make a simple change to our `TestPermissionPolicy` to confirm that policy is indeed wired up correctly. - -That policy looked like this: - -```typescript title="packages/backend/src/plugins/permission.ts" -class TestPermissionPolicy implements PermissionPolicy { - async handle( - request: PolicyQuery, - _user?: PolicyQueryUser, - ): Promise { - if (request.permission.name === 'catalog.entity.delete') { - return { - result: AuthorizeResult.DENY, - }; - } - - return { result: AuthorizeResult.ALLOW }; - } -} -``` - -## What's in a policy? - -Let's break this down a bit further. The request object of type [PolicyQuery](https://backstage.io/docs/reference/plugin-permission-node.policyquery) is a simple wrapper around [the Permission object](https://backstage.io/docs/reference/plugin-permission-common.permission). This permission object encapsulates information about the action that the user is attempting to perform (See [the Concepts page](./concepts.md) for more details). - -In the policy above, we are checking to see if the provided action is a catalog entity delete action, which is the permission that the catalog plugin authors have created to represent the action of unregistering a catalog entity. If this is the case, we return a [Definitive Policy Decision](https://backstage.io/docs/reference/plugin-permission-common.definitivepolicydecision) of DENY. In all other cases, we return ALLOW (resulting in an allow-by-default behavior). - -As we confirmed in the previous section, we know that this now prevents us from unregistering catalog components. Hooray! But you may notice that this prevents _anyone_ from unregistering a component, which is not a very realistic policy. Let's improve this policy by disabling the unregister action _unless you are the owner of this component_. - -## Conditional decisions - -Let's change the policy to the following: - -```ts -import { - AuthorizeResult, - PolicyDecision, - /* highlight-add-next-line */ - isPermission, -} from '@backstage/plugin-permission-common'; -/* highlight-add-start */ -import { - catalogConditions, - createCatalogConditionalDecision, -} from '@backstage/plugin-catalog-backend/alpha'; -import { - catalogEntityDeletePermission, -} from '@backstage/plugin-catalog-common/alpha'; -/* highlight-add-end */ - -class TestPermissionPolicy implements PermissionPolicy { - /* highlight-remove-next-line */ - async handle(request: PolicyQuery): Promise { - /* highlight-add-start */ - async handle( - request: PolicyQuery, - user?: PolicyQueryUser, - ): Promise { - /* highlight-add-end */ - /* highlight-remove-next-line */ - if (request.permission.name === 'catalog.entity.delete') { - /* highlight-add-next-line */ - if (isPermission(request.permission, catalogEntityDeletePermission)) { - /* highlight-remove-start */ - return { - result: AuthorizeResult.DENY, - }; - /* highlight-remove-end */ - /* highlight-add-start */ - return createCatalogConditionalDecision( - request.permission, - catalogConditions.isEntityOwner({ - claims: user?.info.ownershipEntityRefs ?? [], - }), - ); - /* highlight-add-end */ - } - return { result: AuthorizeResult.ALLOW }; - } -} -``` - -Let's walk through the new code that we just added. - -Instead of returning an Definitive Policy Decision, we use factory methods to construct a [Conditional Policy Decision](https://backstage.io/docs/reference/plugin-permission-common.conditionalpolicydecision) (See the [Concepts page](./concepts.md) for more details). Since the policy doesn't have enough information to determine if `user` is the entity owner, this criteria is encapsulated within the conditional decision. However, `createCatalogConditionalDecision` will not compile unless `request.permission` is a catalog entity [`ResourcePermission`](https://backstage.io/docs/reference/plugin-permission-common.resourcepermission). This type constraint ensures that policies return conditional decisions that are compatible with the requested permission. To address this, we use [`isPermission`](https://backstage.io/docs/reference/plugin-permission-common.ispermission) to ["narrow"](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) the type of `request.permission` to `ResourcePermission<'catalog-entity'>`. This matches the runtime behavior that was in place before, but you'll notice that the type of `request.permission` has changed within the scope of that `if` statement. - -The `catalogConditions` object contains all of the rules defined by the catalog plugin. These rules can be combined to form a [`PermissionCriteria`](https://backstage.io/docs/reference/plugin-permission-common.permissioncriteria) object, but for this case we only need to use the `isEntityOwner` rule. This rule accepts a list of entity refs that represent User identity and Group membership used to determine ownership. The second argument to `PermissionPolicy#handle` provides us with a `PolicyQueryUser` object, from which we can grab the user's `ownershipEntityRefs`. We provide an empty array as a fallback since the user may be anonymous. - -You should now be able to see in your Backstage app that the unregister entity button is enabled for entities that you own, but disabled for all other entities! - -## Resource types - -Now let's say we want to prevent all actions on catalog entities unless performed by the owner. One way to achieve this may be to simply update the `if` statement and check for each permission. If you choose to write your policy this way, it will certainly work! However, it may be difficult to maintain as the policy grows, and it may not be obvious if certain permissions are left out. We can author this same policy in a more scalable way by checking the resource type of the requested permission. - -```ts -import { - AuthorizeResult, - PolicyDecision, - /* highlight-remove-next-line */ - isPermission, - isResourcePermission, - /* highlight-add-next-line */ -} from '@backstage/plugin-permission-common'; -import { - catalogConditions, - createCatalogConditionalDecision, -} from '@backstage/plugin-catalog-backend/alpha'; -/* highlight-remove-start */ -import { - catalogEntityDeletePermission, -} from '@backstage/plugin-catalog-common/alpha'; -/* highlight-remove-end */ - -class TestPermissionPolicy implements PermissionPolicy { - async handle( - request: PolicyQuery, - user?: PolicyQueryUser, - ): Promise { - /* highlight-remove-next-line */ - if (isPermission(request.permission, catalogEntityDeletePermission)) { - /* highlight-add-next-line */ - if (isResourcePermission(request.permission, 'catalog-entity')) { - return createCatalogConditionalDecision( - request.permission, - catalogConditions.isEntityOwner({ - claims: user?.info.ownershipEntityRefs ?? [], - }), - ); - } - - return { result: AuthorizeResult.ALLOW }; - } -} -``` - -In this example, we use [`isResourcePermission`](https://backstage.io/docs/reference/plugin-permission-common.isresourcepermission) to match all permissions with a resource type of `catalog-entity`. Just like `isPermission`, this helper will "narrow" the type of `request.permission` and enable the use of `createCatalogConditionalDecision`. In addition to the behavior you observed before, you should also see that catalog entities are no longer visible unless you are the owner - success! - -_Note:_ Some catalog permissions do not have the `'catalog-entity'` resource type, such as [`catalogEntityCreatePermission`](https://github.com/backstage/backstage/blob/1e5e9fb9de9856a49e60fc70c38a4e4e94c69570/plugins/catalog-common/src/permissions.ts#L49). In those cases, a definitive decision is required because conditions can't be applied to an entity that does not exist yet. diff --git a/docs/permissions/writing-a-policy.md b/docs/permissions/writing-a-policy.md index ce662b6e1a..a479684287 100644 --- a/docs/permissions/writing-a-policy.md +++ b/docs/permissions/writing-a-policy.md @@ -5,7 +5,7 @@ description: How to write your own permission policy as a Backstage integrator --- :::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](./writing-a-policy--old.md) instead, and [consider migrating](../backend-system/building-backends/08-migrating.md)! +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](https://github.com/backstage/backstage/blob/v1.37.0/docs/permissions/writing-a-policy--old.md) instead, and [consider migrating](../backend-system/building-backends/08-migrating.md)! ::: In the [previous section](./getting-started.md), we were able to set up the permission framework and make a simple change to our `TestPermissionPolicy` to confirm that policy is indeed wired up correctly. diff --git a/docs/plugins/integrating-search-into-plugins--old.md b/docs/plugins/integrating-search-into-plugins--old.md deleted file mode 100644 index 2729dd3d21..0000000000 --- a/docs/plugins/integrating-search-into-plugins--old.md +++ /dev/null @@ -1,421 +0,0 @@ ---- -id: integrating-search-into-plugins--old -title: Integrating Search into a plugin -description: How to integrate Search into a Backstage plugin ---- - -:::info -This documentation is written for the old backend which has been replaced by [the new backend system](../backend-system/index.md), being the default since Backstage [version 1.24](../releases/v1.24.0.md). If have migrated to the new backend system, you may want to read [its own article](./integrating-search-into-plugins.md) instead. Otherwise, [consider migrating](../backend-system/building-backends/08-migrating.md)! -::: - -The Backstage Search Platform was designed to give plugin developers the APIs -and interfaces needed to offer search experiences within their plugins, while -abstracting away (and instead empowering application integrators to choose) the -specific underlying search technologies. - -On this page, you'll find concepts and tutorials for leveraging the Backstage -Search Platform in your plugin. - -## Providing data to the search platform - -### Create a collator - -> Knowing what a [collator](../features/search/concepts.md#collators) is will help you as you build it out. - -Imagine you have a plugin that is responsible for storing FAQ snippets in a database. You want other engineers to be able to easily find your questions and answers. So that means you want them to be indexed by the search platform. Lets say the FAQ snippets can be viewed at a URL like `backstage.example.biz/faq-snippets`. - -The search platform provides an interface (`DocumentCollatorFactory` from package `@backstage/plugin-search-common`) that allows you to do exactly that. It works by registering each of your entries as a "document" that later represents one search result each. - -> You can always look at a working example, e.g. [StackOverflowQuestionsCollatorFactory](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.ts), if you are unsure or want to follow best practices. - -#### 1. Install collator interface dependencies - -We will need the interface `DocumentCollatorFactory` from package `@backstage/plugin-search-common`, so let's add it to your plugins dependencies: - -```sh -# navigate to the plugin directory -# (for this tutorial our plugin lives in the backstage repo, if your plugin lives in a separate repo you need to clone that first) -cd plugins/faq-snippets - -# Create a new branch using Git command-line -git checkout -b tutorials/new-faq-snippets-collator - -# Install the package containing the interface -yarn add @backstage/plugin-search-common -``` - -#### 2. Define your document type - -Before we can start generating documents from our FAQ entries, we first have to define a document type containing all necessary information we need to later display our entry as search result. The package `@backstage/plugin-search-common` we installed earlier contains a type `IndexableDocument` that we can extend. - -Create a new file `plugins/faq-snippets/src/search/collators/FaqSnippetDocument.ts` and paste the following below: - -```ts -import { IndexableDocument } from '@backstage/plugin-search-common'; - -export interface FaqSnippetDocument extends IndexableDocument { - answered_by: string; -} -``` - -#### 3. Use Backstage App configuration - -Your new collator could benefit from using configuration directly from the Backstage `app-config.yaml` file which is located on the project's root folder: - -```yaml -faq: - baseUrl: https://backstage.example.biz/faq-snippets -``` - -#### 4. Implement your collator - -Imagine your FAQs can be retrieved at the URL `https://backstage.example.biz/faq-snippets` with following JSON response format: - -```json -{ - "items": [ - { - "id": 42, - "question": "What is The Answer to the Ultimate Question of Life, the Universe, and Everything?", - "answer": "Forty-two", - "user": "Deep Thought" - } - ] -} -``` - -Below we provide an example implementation of how the FAQ collator factory could look like using our new document type, placed in the `plugins/faq-snippets/src/search/collators/FaqCollatorFactory.ts` file: - -```ts -import fetch from 'cross-fetch'; -import { Logger } from 'winston'; -import { Config } from '@backstage/config'; -import { Readable } from 'stream'; -import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; - -import { FaqDocument } from './FaqDocument'; - -export type FaqCollatorFactoryOptions = { - baseUrl?: string; - logger: Logger; -}; - -export class FaqCollatorFactory implements DocumentCollatorFactory { - private readonly baseUrl: string | undefined; - private readonly logger: Logger; - public readonly type: string = 'faq-snippets'; - - private constructor(options: FaqCollatorFactoryOptions) { - this.baseUrl = options.baseUrl; - this.logger = options.logger; - } - - static fromConfig(config: Config, options: FaqCollatorFactoryOptions) { - const baseUrl = - config.getOptionalString('faq.baseUrl') || - 'https://backstage.example.biz/faq-snippets'; - return new FaqCollatorFactory({ ...options, baseUrl }); - } - - async getCollator() { - return Readable.from(this.execute()); - } - - async *execute(): AsyncGenerator { - if (!this.baseUrl) { - this.logger.error(`No faq.baseUrl configured in your app-config.yaml`); - return; - } - - const response = await fetch(this.baseUrl); - const data = await response.json(); - - for (const faq of data.items) { - yield { - title: faq.question, - location: `/faq-snippets/${faq.id}`, - text: faq.answer, - answered_by: faq.user, - }; - } - } -} -``` - -#### 5. Test your collator - -To verify your implementation works as expected make sure to add tests for it. For your convenience, there is the [`TestPipeline`](https://backstage.io/docs/reference/plugin-search-backend-node.testpipeline) utility that emulates a pipeline into which you can integrate your custom collator. - -Look at [DefaultTechDocsCollatorFactory test](https://github.com/backstage/backstage/blob/de294ce5c410c9eb56da6870a1fab795268f60e3/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.test.ts), for an example. - -#### 6. Make your plugins collator discoverable for others - -If you want to make your collator discoverable for other adopters, add it to the list of [plugins integrated to search](https://backstage.io/docs/features/search/#plugins-integrated-with-backstage-search). - -## Building a search experience into your plugin - -While the core Search plugin offers components and extensions that empower app -integrators to compose a global search experience, you may find that you want a -narrower search experience just within your plugin. This could be as literal as -an autocomplete-style search bar focused on documents provided by your plugin -(for example, the [TechDocsSearch](https://github.com/backstage/backstage/blob/master/plugins/techdocs/src/search/components/TechDocsSearch.tsx) -component), or as abstract as a widget that presents a list of links that -are contextually related to something else on the page. - -### Search Experience Concepts - -Knowing these high-level concepts will help you as you craft your in-plugin -search experience. - -- All search experiences must be wrapped in a ``, which - is provided by `@backstage/plugin-search-react`. This context keeps track - of state necessary to perform search queries and display any results. As - inputs to the query are updated (e.g. a `term` or `filter` values), the - updated query is executed and `results` are refreshed. Check out the - [SearchContextValue](https://backstage.io/docs/reference/plugin-search-react.searchcontextvalue) - for details. -- The aforementioned state can be modified and/or consumed via the - `useSearch()` hook, also exported by `@backstage/plugin-search-react`. -- For more literal search experiences, reusable components are available - to import and compose into a cohesive experience in your plugin (e.g. - `` or ``). You can see all such - components in [Backstage's storybook](https://backstage.io/storybook/?path=/story/plugins-search-searchbar--default). - -### Search Experience Tutorials - -The following tutorials make use of packages and plugins that you may not yet -have as dependencies for your plugin; be sure to add them before you use them! - -- [`@backstage/plugin-search-react`](https://www.npmjs.com/package/@backstage/plugin-search-react) - A - package containing components, hooks, and types that are shared across all - frontend plugins, including plugins like yours! -- [`@backstage/plugin-search`](https://www.npmjs.com/package/@backstage/plugin-search) - The - main search plugin, used by app integrators to compose global search - experiences. -- [`@backstage/core-components`](https://www.npmjs.com/package/@backstage/core-components) - A - package containing generic components useful for a variety of experiences - built in Backstage. - -#### Improved "404" page experience - -Imagine you have a plugin that allows users to manage _widgets_. Perhaps they -can be viewed at a URL like `backstage.example.biz/widgets/{widgetName}`. -At some point, a widget is renamed, and links to that widget's page from -chat systems, wikis, or browser bookmarks become stale, resulting in errors or -404s. - -What if instead of showing a broken page or the generic "looks like someone -dropped the mic" 404 page, you showed a list of possibly related widgets? - -```javascript -import { Link } from '@backstage/core-components'; -import { SearchResult } from '@backstage/plugin-search'; -import { SearchContextProvider } from '@backstage/plugin-search-react'; - -export const Widget404Page = ({ widgetName }) => { - // Supplying this to runs a pre-filtered search with - // the given widgetName as the search term, focused on search result of type - // "widget" with no other filters. - const preFiltered = { - term: widgetName, - types: ['widget'], - filters: {}, - }; - - return ( - - {/* The component allows us to iterate through results and - display them in whatever way fits best! */} - - {({ results }) => ( - {results.map(({ document }) => ( - - {document.title} - - ))} - )} - - - ); -); -``` - -Not all search experiences require user input! As you can see, it's possible to -leverage the Backstage Search Platform's frontend framework without necessarily -giving users input controls. - -#### Simple search page - -Of course, it's also possible to provide a more fully featured search -experience in your plugin. The simplest way is to leverage reusable components -provided by the `@backstage/plugin-search` package, like this: - -```javascript -import { useProfile } from '@internal/api'; -import { - Content, - ContentHeader, - PageWithHeader, -} from '@backstage/core-components'; -import { SearchBar, SearchResult } from '@backstage/plugin-search'; -import { SearchContextProvider } from '@backstage/plugin-search-react'; - -export const ManageMyWidgets = () => { - const { primaryTeam } = useProfile(); - // In this example, note how we are pre-filtering results down to a specific - // owner field value (the currently logged-in user's team), but allowing the - // search term to be controlled by the user via the component. - const preFiltered = { - types: ['widget'], - term: '', - filters: { - owner: primaryTeam, - }, - }; - - return ( - - - - - - - {/* Render results here, just like above */} - - - - - ); -}; -``` - -#### Custom search control surfaces - -If the reusable search components provided by `@backstage/plugin-search` aren't -adequate, no problem! There's an API in place that you can use to author your -own components to control the various parts of the search context. - -```javascript -import { useSearch } from '@backstage/plugin-search-react'; -import ChipInput from 'material-ui-chip-input'; - -export const CustomChipFilter = ({ name }) => { - const { filters, setFilters } = useSearch(); - const chipValues = filters[name] || []; - - // When a chip value is changed, update the filters value by calling the - // setFilters function from the search context. - const handleChipChange = (chip, index) => { - // There may be filters set for other fields. Be sure to maintain them. - setFilters(prevState => { - const { [name]: filter = [], ...others } = prevState; - - if (index === undefined) { - filter.push(chip); - } else { - filter.splice(index, 1); - } - - return { ...others, [name]: filter }; - }); - }; - - return ( - - ); -}; -``` - -Check out the [SearchContextValue type](https://github.com/backstage/backstage/blob/master/plugins/search-react/src/context/SearchContext.tsx) -for more details on what methods and values are available for manipulating and -reading the search context. - -If you produce something generic and reusable, consider contributing your -component upstream so that all users of the Backstage Search Platform can -benefit. Issues and pull requests welcome. - -#### Custom search results - -Search results throughout Backstage are rendered as lists so that list items can easily be customized; although a [default result list item](https://backstage.io/storybook/?path=/story/plugins-search-defaultresultlistitem--default) is available, plugins are in the best position to provide custom result list items that surface relevant information only known to the plugin. - -The example below imagines `YourCustomSearchResult` as a type of search result that contains associated `tags` which could be rendered as chips below the title/text. - -```tsx -import { Link } from '@backstage/core-components'; -import { useAnalytics } from '@backstage/core-plugin-api'; -import { ResultHighlight } from '@backstage/plugin-search-common'; -import { HighlightedSearchResultText } from '@backstage/plugin-search-react'; - -type CustomSearchResultListItemProps = { - result: YourCustomSearchResult; - rank?: number; - highlight?: ResultHighlight; -}; - -export const CustomSearchResultListItem = ( - props: CustomSearchResultListItemProps, -) => { - const { title, text, location, tags } = props.result; - - const analytics = useAnalytics(); - const handleClick = () => { - analytics.captureEvent('discover', title, { - attributes: { to: location }, - value: props.rank, - }); - }; - - return ( - - - - - ) : ( - title - ) - } - secondary={ - highlight?.fields?.text ? ( - - ) : ( - text - ) - } - /> - {tags && - tags.map((tag: string) => ( - - ))} - - - - - ); -}; -``` - -The optional use of the `` component makes it possible to highlight relevant parts of the result based on the user's search query. - -**Note on Analytics**: In order for app integrators to track and improve search experiences across Backstage, it's important for them to understand when and what users search for, as well as what they click on after searching. When providing a custom result component, it's your responsibility as a plugin developer to instrument it according to search analytics conventions. In particular: - -- You must use the `analytics.captureEvent` method, from the `useAnalytics()` hook (detailed [plugin analytics docs are here](./analytics.md)). -- You must ensure that the action of the event, representing a click on a search result item, is `discover`, and the subject is the `title` of the clicked result. In addition, the `to` attribute should be set to the result's `location`, and the `value` of the event must be set to the `rank` (passed in as a prop). -- You must ensure that the aforementioned `captureEvent` method is called when a user clicks the link; you should further ensure that the `noTrack` prop is added to the link (which disables default link click tracking, in favor of this custom instrumentation). - -For other examples and inspiration on custom result list items, check out the [``](https://github.com/backstage/backstage/blob/c981e83/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.tsx) or [``](https://github.com/backstage/backstage/blob/c981e83/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx) components. diff --git a/docs/plugins/integrating-search-into-plugins.md b/docs/plugins/integrating-search-into-plugins.md index 374a8aeb8a..d6f2037529 100644 --- a/docs/plugins/integrating-search-into-plugins.md +++ b/docs/plugins/integrating-search-into-plugins.md @@ -5,7 +5,7 @@ description: How to integrate Search into 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](./integrating-search-into-plugins--old.md) instead, and [consider migrating](../backend-system/building-backends/08-migrating.md)! +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](https://github.com/backstage/backstage/blob/v1.37.0/docs/plugins/integrating-search-into-plugins--old.md) instead, and [consider migrating](../backend-system/building-backends/08-migrating.md)! ::: The Backstage Search Platform was designed to give plugin developers the APIs