From 9377fbfb0f2449e03289f566a5ad0d3b18874a8b Mon Sep 17 00:00:00 2001 From: Juan Pablo Garcia Ripa Date: Mon, 24 Mar 2025 10:22:43 +0100 Subject: [PATCH] feat(docs): add multiton docs and fix the custom handler service Signed-off-by: Juan Pablo Garcia Ripa --- .changeset/thirty-rules-press.md | 2 +- docs/auth/service-to-service-auth.md | 22 +++++----- .../architecture/03-services.md | 29 +++++++++++++ packages/backend-defaults/report-auth.api.md | 41 ++++++++++++------- .../auth/authServiceFactory.test.ts | 4 +- .../entrypoints/auth/authServiceFactory.ts | 6 --- .../src/entrypoints/auth/external/helpers.ts | 6 +-- .../src/entrypoints/auth/external/jwks.ts | 4 +- .../src/entrypoints/auth/external/legacy.ts | 6 +-- .../src/entrypoints/auth/external/static.ts | 4 +- .../src/entrypoints/auth/external/types.ts | 12 +++++- .../src/entrypoints/auth/index.ts | 5 ++- 12 files changed, 93 insertions(+), 48 deletions(-) diff --git a/.changeset/thirty-rules-press.md b/.changeset/thirty-rules-press.md index f4758da67b..d9968317cb 100644 --- a/.changeset/thirty-rules-press.md +++ b/.changeset/thirty-rules-press.md @@ -2,4 +2,4 @@ '@backstage/backend-defaults': minor --- -Add a new `externalTokenHandlerDecoratorServiceRef` to allow custom external token validations +Add a new `externalTokenHandlersServiceRef` to allow custom external token validations diff --git a/docs/auth/service-to-service-auth.md b/docs/auth/service-to-service-auth.md index e821aeb0c7..8e8ef6a183 100644 --- a/docs/auth/service-to-service-auth.md +++ b/docs/auth/service-to-service-auth.md @@ -414,7 +414,7 @@ Each entry has one or more of the following fields: ## Adding custom or logic for validation and issuing of tokens -The `pluginTokenHandlerDecoratorServiceRef` and `externalTokenHandlerDecoratorServiceRef` can be used to decorate the existing token handler without having to re-implement the entire `AuthService` implementation. +The `pluginTokenHandlerDecoratorServiceRef` and `externalTokenHandlersServiceRef` can be used to extend the existing token handler without having to re-implement the entire `AuthService` implementation. This is particularly useful when you want to add additional logic to the handler, such as logging or metrics or custom token validation. ### PluginTokenHandler decoration @@ -446,25 +446,27 @@ const decoratedPluginTokenHandler = createServiceFactory({ ### ExternalTokenHandler decoration -The `externalTokenHandlerDecoratorServiceRef` can be used to decorate the default ExternalTokenHandler used for verify tokens from external callers. +The `externalTokenHandlersServiceRef` can be used to add custom external token handlers to the default implementation. -The `ExternalTokenHandler` interface has one methods: +The returned object should be a map of token custom types and their handler factories. The object keys will be matched to the configured `externalAccess` type in the app-config, calling the factory function with the config object for that type. Custom token handlers should implement the `TokenHandler` interface, which provides methods for verifying tokens. -- `verifyToken`: This method is used to verify a token. It takes in the token as an argument and returns a promise that resolves to an object containing the subject of the token and an optional limited user token. +For example, to add a custom token handler for a type called 'custom': ```ts import { - ExternalTokenHandler, - externalTokenHandlerDecoratorServiceRef, + TokenHandler, + externalTokenHandlersServiceRef, } from '@backstage/backend-defaults/auth'; +import { Config } from '@backstage/config'; import { createServiceFactory } from '@backstage/backend-plugin-api'; -const decoratedPluginTokenHandler = createServiceFactory({ - service: externalTokenHandlerDecoratorServiceRef, +const customExternalTokenHandlers = createServiceFactory({ + service: externalTokenHandlersServiceRef, deps: {}, async factory() { - return (defaultImplementation: ExternalTokenHandler) => - new CustomTokenHandler(defaultImplementation); + return { + custom: (config: Config) => new CustomTokenHandler(config); + }; }, }); ``` diff --git a/docs/backend-system/architecture/03-services.md b/docs/backend-system/architecture/03-services.md index cfcd606d92..3b168b81e3 100644 --- a/docs/backend-system/architecture/03-services.md +++ b/docs/backend-system/architecture/03-services.md @@ -223,6 +223,35 @@ export const customFooServiceFactory = createServiceFactory({ This allows you to provide more advanced options for the service implementation that couldn't be expressed through static configuration. It also gives users of the service implementation access to other services through dependency injection, which can be useful for their customizations. +## Multiton + +By default the service reference will point to a singleton instance of the service. This meand if a new service factory uses this reference will override the previous one. This is the most common use-case, but in some cases you may want to have multiple instances of the same service. +For some services, is desirable to extend the functionality instead of overriding it. For example, some services could have many handler to address a specific event, and you may want to add a new handler instead of overriding the previous one. In this case, you can use the `multiton` option when creating the service reference: + +```ts +// example-service-ref.ts +import { createServiceRef } from '@backstage/backend-plugin-api'; + +export interface FooService { + foo(options: FooOptions): Promise; +} + +export const fooServiceRef = createServiceRef({ + id: 'example.foo', + multiton: true, // this service ref will be an array of instances +}); +``` + +When adding this serviceRef ad dependency to a factory, the factory will receive an array of instances instead of a single instance: + +```ts +deps: {fooServices: fooServiceRef}, + factory(fooServices) { + // fooServices is an array of instances + return new Bar(fooServices); + }, +``` + ## Service Factory Options Pattern :::note Note diff --git a/packages/backend-defaults/report-auth.api.md b/packages/backend-defaults/report-auth.api.md index 56a973813d..9b7a56cff5 100644 --- a/packages/backend-defaults/report-auth.api.md +++ b/packages/backend-defaults/report-auth.api.md @@ -5,9 +5,16 @@ ```ts import { AuthService } from '@backstage/backend-plugin-api'; import { BackstagePrincipalAccessRestrictions } from '@backstage/backend-plugin-api'; +import { Config } from '@backstage/config'; import { ServiceFactory } from '@backstage/backend-plugin-api'; import { ServiceRef } from '@backstage/backend-plugin-api'; +// @public (undocumented) +export type AccessRestrictionsMap = Map< + string, // plugin ID + BackstagePrincipalAccessRestrictions +>; + // @public export const authServiceFactory: ServiceFactory< AuthService, @@ -16,22 +23,12 @@ export const authServiceFactory: ServiceFactory< >; // @public -export interface ExternalTokenHandler { - // (undocumented) - verifyToken(token: string): Promise< - | { - subject: string; - accessRestrictions?: BackstagePrincipalAccessRestrictions; - } - | undefined - >; -} - -// @public -export const externalTokenHandlerDecoratorServiceRef: ServiceRef< - (defaultImplementation: ExternalTokenHandler) => ExternalTokenHandler, +export const externalTokenHandlersServiceRef: ServiceRef< + { + [configKey: string]: (config: Config) => TokenHandler; + }, 'plugin', - 'singleton' + 'multiton' >; // @public @@ -64,5 +61,19 @@ export const pluginTokenHandlerDecoratorServiceRef: ServiceRef< 'singleton' >; +// @public +export interface TokenHandler { + // (undocumented) + add?(options: Config): TokenHandler; + // (undocumented) + verifyToken(token: string): Promise< + | { + subject: string; + allAccessRestrictions?: AccessRestrictionsMap; + } + | undefined + >; +} + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts index 51df123555..d6cc0023a8 100644 --- a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts @@ -31,7 +31,7 @@ import { setupServer } from 'msw/node'; import { toInternalBackstageCredentials } from './helpers'; import { PluginTokenHandler } from './plugin/PluginTokenHandler'; import { createServiceFactory } from '@backstage/backend-plugin-api'; -import { AccessRestriptionsMap, TokenHandler } from './external/types'; +import { AccessRestrictionsMap, TokenHandler } from './external/types'; import { Config } from '@backstage/config'; // import { ExternalTokenHandler } from './external/ExternalTokenHandler'; // import { TokenHandler } from './external/types'; @@ -512,7 +512,7 @@ describe('authServiceFactory', () => { async verifyToken(token: string): Promise< | { subject: string; - allAccessRestrictions?: AccessRestriptionsMap; + allAccessRestrictions?: AccessRestrictionsMap; } | undefined > { diff --git a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts index 48f0941b12..ae8ad205b7 100644 --- a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts @@ -59,12 +59,6 @@ export const externalTokenHandlersServiceRef = createServiceRef<{ }>({ id: 'core.auth.externalTokenHandlers', multiton: true, - // defaultFactory: async service => - // createServiceFactory({ - // service, - // deps: {}, - // factory: async () => {}, - // }), }); /** diff --git a/packages/backend-defaults/src/entrypoints/auth/external/helpers.ts b/packages/backend-defaults/src/entrypoints/auth/external/helpers.ts index d6aa0a01ff..4a01117e28 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/helpers.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/helpers.ts @@ -15,7 +15,7 @@ */ import { Config } from '@backstage/config'; -import { AccessRestriptionsMap } from './types'; +import { AccessRestrictionsMap } from './types'; /** * Parses and returns the `accessRestrictions` configuration from an @@ -25,12 +25,12 @@ import { AccessRestriptionsMap } from './types'; */ export function readAccessRestrictionsFromConfig( externalAccessEntryConfig: Config, -): AccessRestriptionsMap | undefined { +): AccessRestrictionsMap | undefined { const configs = externalAccessEntryConfig.getOptionalConfigArray('accessRestrictions') ?? []; - const result: AccessRestriptionsMap = new Map(); + const result: AccessRestrictionsMap = new Map(); for (const config of configs) { const validKeys = ['plugin', 'permission', 'permissionAttribute']; for (const key of config.keys()) { diff --git a/packages/backend-defaults/src/entrypoints/auth/external/jwks.ts b/packages/backend-defaults/src/entrypoints/auth/external/jwks.ts index 32271972fb..49167bcd35 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/jwks.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/jwks.ts @@ -20,7 +20,7 @@ import { readAccessRestrictionsFromConfig, readStringOrStringArrayFromConfig, } from './helpers'; -import { AccessRestriptionsMap, TokenHandler } from './types'; +import { AccessRestrictionsMap, TokenHandler } from './types'; /** * Handles `type: jwks` access. @@ -35,7 +35,7 @@ export class JWKSHandler implements TokenHandler { subjectPrefix?: string; url: URL; jwks: JWTVerifyGetKey; - allAccessRestrictions?: AccessRestriptionsMap; + allAccessRestrictions?: AccessRestrictionsMap; }> = []; add(config: Config) { diff --git a/packages/backend-defaults/src/entrypoints/auth/external/legacy.ts b/packages/backend-defaults/src/entrypoints/auth/external/legacy.ts index fbd96aafd7..09d759a09e 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/legacy.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/legacy.ts @@ -17,7 +17,7 @@ import { Config } from '@backstage/config'; import { base64url, decodeJwt, decodeProtectedHeader, jwtVerify } from 'jose'; import { readAccessRestrictionsFromConfig } from './helpers'; -import { AccessRestriptionsMap, TokenHandler } from './types'; +import { AccessRestrictionsMap, TokenHandler } from './types'; /** * Handles `type: legacy` access. @@ -29,7 +29,7 @@ export class LegacyTokenHandler implements TokenHandler { key: Uint8Array; result: { subject: string; - allAccessRestrictions?: AccessRestriptionsMap; + allAccessRestrictions?: AccessRestrictionsMap; }; }>(); @@ -52,7 +52,7 @@ export class LegacyTokenHandler implements TokenHandler { #doAdd( secret: string, subject: string, - allAccessRestrictions?: AccessRestriptionsMap, + allAccessRestrictions?: AccessRestrictionsMap, ) { if (!secret.match(/^\S+$/)) { throw new Error('Illegal secret, must be a valid base64 string'); diff --git a/packages/backend-defaults/src/entrypoints/auth/external/static.ts b/packages/backend-defaults/src/entrypoints/auth/external/static.ts index b335b89e58..e003f0530b 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/static.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/static.ts @@ -16,7 +16,7 @@ import { Config } from '@backstage/config'; import { readAccessRestrictionsFromConfig } from './helpers'; -import { AccessRestriptionsMap, TokenHandler } from './types'; +import { AccessRestrictionsMap, TokenHandler } from './types'; const MIN_TOKEN_LENGTH = 8; @@ -30,7 +30,7 @@ export class StaticTokenHandler implements TokenHandler { string, { subject: string; - allAccessRestrictions?: AccessRestriptionsMap; + allAccessRestrictions?: AccessRestrictionsMap; } >(); diff --git a/packages/backend-defaults/src/entrypoints/auth/external/types.ts b/packages/backend-defaults/src/entrypoints/auth/external/types.ts index e3cad90e17..6b882ef16b 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/types.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/types.ts @@ -17,17 +17,25 @@ import { BackstagePrincipalAccessRestrictions } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; -export type AccessRestriptionsMap = Map< +/** + * @public + */ +export type AccessRestrictionsMap = Map< string, // plugin ID BackstagePrincipalAccessRestrictions >; +/** + * @public + * This interface is used to handle external tokens. + * It is used by the auth service to verify tokens and extract the subject. + */ export interface TokenHandler { add?(options: Config): TokenHandler; verifyToken(token: string): Promise< | { subject: string; - allAccessRestrictions?: AccessRestriptionsMap; + allAccessRestrictions?: AccessRestrictionsMap; } | undefined >; diff --git a/packages/backend-defaults/src/entrypoints/auth/index.ts b/packages/backend-defaults/src/entrypoints/auth/index.ts index 51fe982891..96db6c0535 100644 --- a/packages/backend-defaults/src/entrypoints/auth/index.ts +++ b/packages/backend-defaults/src/entrypoints/auth/index.ts @@ -17,9 +17,10 @@ export { authServiceFactory, pluginTokenHandlerDecoratorServiceRef, - externalTokenHandlerDecoratorServiceRef, + externalTokenHandlersServiceRef, } from './authServiceFactory'; -export type { ExternalTokenHandler } from './external/ExternalTokenHandler'; +export type { TokenHandler } from './external/types'; +export type { AccessRestrictionsMap } from './external/types'; export type { PluginTokenHandler } from './plugin/PluginTokenHandler';