@backstage/backend-defaults 0.17.7 → 0.17.9-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/config.schema.json +1 -1
  3. package/dist/alpha/entrypoints/actionsRegistry/DefaultActionsRegistryService.cjs.js +34 -9
  4. package/dist/alpha/entrypoints/actionsRegistry/DefaultActionsRegistryService.cjs.js.map +1 -1
  5. package/dist/alpha/entrypoints/actionsRegistry/actionFilters.cjs.js +67 -0
  6. package/dist/alpha/entrypoints/actionsRegistry/actionFilters.cjs.js.map +1 -0
  7. package/dist/alpha/entrypoints/actionsRegistry/actionsRegistryServiceFactory.cjs.js +3 -0
  8. package/dist/alpha/entrypoints/actionsRegistry/actionsRegistryServiceFactory.cjs.js.map +1 -1
  9. package/dist/entrypoints/auth/DefaultAuthService.cjs.js +18 -2
  10. package/dist/entrypoints/auth/DefaultAuthService.cjs.js.map +1 -1
  11. package/dist/entrypoints/auth/JwksClient.cjs.js +61 -32
  12. package/dist/entrypoints/auth/JwksClient.cjs.js.map +1 -1
  13. package/dist/entrypoints/auth/external/ExternalAuthTokenHandler.cjs.js +2 -1
  14. package/dist/entrypoints/auth/external/ExternalAuthTokenHandler.cjs.js.map +1 -1
  15. package/dist/entrypoints/auth/helpers.cjs.js +7 -1
  16. package/dist/entrypoints/auth/helpers.cjs.js.map +1 -1
  17. package/dist/entrypoints/auth/plugin/PluginTokenHandler.cjs.js +0 -1
  18. package/dist/entrypoints/auth/plugin/PluginTokenHandler.cjs.js.map +1 -1
  19. package/dist/entrypoints/auth/user/UserTokenHandler.cjs.js +0 -1
  20. package/dist/entrypoints/auth/user/UserTokenHandler.cjs.js.map +1 -1
  21. package/dist/entrypoints/database/DatabaseManager.cjs.js +12 -0
  22. package/dist/entrypoints/database/DatabaseManager.cjs.js.map +1 -1
  23. package/dist/entrypoints/database/connectors/postgres.cjs.js +216 -59
  24. package/dist/entrypoints/database/connectors/postgres.cjs.js.map +1 -1
  25. package/dist/entrypoints/urlReader/lib/AwsCodeCommitUrlReader.cjs.js +4 -0
  26. package/dist/entrypoints/urlReader/lib/AwsCodeCommitUrlReader.cjs.js.map +1 -1
  27. package/dist/entrypoints/urlReader/lib/AwsS3UrlReader.cjs.js +8 -1
  28. package/dist/entrypoints/urlReader/lib/AwsS3UrlReader.cjs.js.map +1 -1
  29. package/dist/entrypoints/urlReader/lib/AzureBlobStorageUrlReader.cjs.js +8 -1
  30. package/dist/entrypoints/urlReader/lib/AzureBlobStorageUrlReader.cjs.js.map +1 -1
  31. package/dist/entrypoints/urlReader/lib/FetchUrlReader.cjs.js +8 -4
  32. package/dist/entrypoints/urlReader/lib/FetchUrlReader.cjs.js.map +1 -1
  33. package/dist/entrypoints/urlReader/lib/GitlabUrlReader.cjs.js +19 -12
  34. package/dist/entrypoints/urlReader/lib/GitlabUrlReader.cjs.js.map +1 -1
  35. package/dist/entrypoints/urlReader/lib/GoogleGcsUrlReader.cjs.js +2 -1
  36. package/dist/entrypoints/urlReader/lib/GoogleGcsUrlReader.cjs.js.map +1 -1
  37. package/dist/entrypoints/urlReader/lib/HarnessUrlReader.cjs.js +58 -25
  38. package/dist/entrypoints/urlReader/lib/HarnessUrlReader.cjs.js.map +1 -1
  39. package/dist/entrypoints/urlReader/lib/util.cjs.js +30 -0
  40. package/dist/entrypoints/urlReader/lib/util.cjs.js.map +1 -1
  41. package/dist/package.json.cjs.js +1 -1
  42. package/dist/urlReader.d.ts +7 -1
  43. package/package.json +17 -17
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @backstage/backend-defaults
2
2
 
3
+ ## 0.17.9-next.0
4
+
5
+ ### Patch Changes
6
+
7
+ - 45d760b: Improve action handling consistency.
8
+ - 8d71644: Reduced PostgreSQL connection churn during backend startup when many plugins initialize databases or schemas.
9
+ - 416be1b: Updated Harness URL reading to validate cross-origin redirect destinations
10
+ against `backend.reading.allow`.
11
+ - 5aac521: Improved service credential handling during plugin-to-plugin delegation.
12
+ - 61449ae: Reduced PostgreSQL startup connections when multiple plugins share a database by reusing the database existence check.
13
+ - 8b3c83e: Improved input validation and path handling for cloud storage URL readers.
14
+ - 61d97c0: Improved input validation for cloud storage URL readers.
15
+ - e895def: Fixed handling of GitLab URLs for instances configured with a relative base path.
16
+ - e13e278: Updated URL reader allow list matching so configured paths match either the exact path or paths below it at a segment boundary.
17
+ - e3b587c: Improve authentication reliability during signing key rotation by performing budgeted JWKS reloads when a newly published key is requested during the remote key set cooldown.
18
+ - Updated dependencies
19
+ - @backstage/integration@2.1.2-next.0
20
+ - @backstage/backend-app-api@1.7.4-next.0
21
+ - @backstage/plugin-auth-node@0.7.5-next.0
22
+ - @backstage/plugin-permission-node@0.11.4-next.0
23
+ - @backstage/backend-plugin-api@1.10.1-next.0
24
+ - @backstage/plugin-events-node@0.4.26-next.0
25
+
3
26
  ## 0.17.7
4
27
 
5
28
  ### Patch Changes
@@ -2307,7 +2307,7 @@
2307
2307
  "items": {
2308
2308
  "type": "string"
2309
2309
  },
2310
- "description": "An optional list of paths. In case they are present only targets matching any of them will are allowed. You can use trailing slashes to make sure only subdirectories are allowed, for example `/mydir/` will allow targets with paths like `/mydir/a` but will block paths like `/mydir2`."
2310
+ "description": "An optional list of paths. When present, only targets matching an exact path or a path below it at a segment boundary are allowed. For example, `/mydir` allows `/mydir` and `/mydir/a`, but blocks `/mydir2`. A trailing slash excludes the exact path without the slash, so `/mydir/` allows `/mydir/a` but not `/mydir`."
2311
2311
  }
2312
2312
  },
2313
2313
  "required": [
@@ -6,6 +6,7 @@ var v3 = require('zod/v3');
6
6
  var zodToJsonSchema = require('zod-to-json-schema');
7
7
  var errors = require('@backstage/errors');
8
8
  var pluginPermissionCommon = require('@backstage/plugin-permission-common');
9
+ var actionFilters = require('./actionFilters.cjs.js');
9
10
 
10
11
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
11
12
 
@@ -17,13 +18,15 @@ class DefaultActionsRegistryService {
17
18
  logger;
18
19
  httpAuth;
19
20
  auth;
21
+ config;
20
22
  metadata;
21
23
  permissions;
22
24
  permissionsRegistry;
23
- constructor(logger, httpAuth, auth, metadata, permissions, permissionsRegistry) {
25
+ constructor(logger, httpAuth, auth, config, metadata, permissions, permissionsRegistry) {
24
26
  this.logger = logger;
25
27
  this.httpAuth = httpAuth;
26
28
  this.auth = auth;
29
+ this.config = config;
27
30
  this.metadata = metadata;
28
31
  this.permissions = permissions;
29
32
  this.permissionsRegistry = permissionsRegistry;
@@ -32,6 +35,7 @@ class DefaultActionsRegistryService {
32
35
  httpAuth,
33
36
  logger,
34
37
  auth,
38
+ config,
35
39
  metadata,
36
40
  permissions,
37
41
  permissionsRegistry
@@ -40,6 +44,7 @@ class DefaultActionsRegistryService {
40
44
  logger,
41
45
  httpAuth,
42
46
  auth,
47
+ config,
43
48
  metadata,
44
49
  permissions,
45
50
  permissionsRegistry
@@ -50,7 +55,9 @@ class DefaultActionsRegistryService {
50
55
  router.use("/.backstage/actions/", express.json());
51
56
  router.get("/.backstage/actions/v1/actions", async (req, res) => {
52
57
  const credentials = await this.httpAuth.credentials(req);
53
- const entries = Array.from(this.actions.entries());
58
+ const entries = Array.from(this.actions.entries()).filter(
59
+ (entry) => this.isActionAllowed(entry)
60
+ );
54
61
  const allowedActions = await this.filterByPermissions(
55
62
  entries,
56
63
  credentials
@@ -62,13 +69,7 @@ class DefaultActionsRegistryService {
62
69
  title: action.title,
63
70
  description: action.description,
64
71
  pluginId: this.metadata.getId(),
65
- attributes: {
66
- // Inspired by the @modelcontextprotocol/sdk defaults for the hints.
67
- // https://github.com/modelcontextprotocol/typescript-sdk/blob/dd69efa1de8646bb6b195ff8d5f52e13739f4550/src/types.ts#L777-L812
68
- destructive: action.attributes?.destructive ?? !action.attributes?.readOnly,
69
- idempotent: action.attributes?.idempotent ?? false,
70
- readOnly: action.attributes?.readOnly ?? false
71
- },
72
+ attributes: this.toActionAttributes(action),
72
73
  examples: action.examples,
73
74
  schema: {
74
75
  input: action.schema?.input ? zodToJsonSchema__default.default(action.schema.input(v3.z)) : zodToJsonSchema__default.default(v3.z.object({})),
@@ -91,6 +92,9 @@ class DefaultActionsRegistryService {
91
92
  if (!action) {
92
93
  throw new errors.NotFoundError(`Action "${req.params.actionId}" not found`);
93
94
  }
95
+ if (!this.isActionAllowed([req.params.actionId, action])) {
96
+ throw new errors.NotFoundError(`Action "${req.params.actionId}" not found`);
97
+ }
94
98
  if (action.visibilityPermission) {
95
99
  const [decision] = await this.permissions.authorize(
96
100
  [{ permission: action.visibilityPermission }],
@@ -163,6 +167,27 @@ class DefaultActionsRegistryService {
163
167
  }
164
168
  this.actions.set(id, options);
165
169
  }
170
+ isActionAllowed([id, action]) {
171
+ const pluginSources = this.config.getOptionalStringArray(
172
+ "backend.actions.pluginSources"
173
+ );
174
+ if (pluginSources && !pluginSources.includes(this.metadata.getId())) {
175
+ return false;
176
+ }
177
+ return actionFilters.filterActions(this.config, [
178
+ {
179
+ id,
180
+ attributes: this.toActionAttributes(action)
181
+ }
182
+ ]).length === 1;
183
+ }
184
+ toActionAttributes(action) {
185
+ return {
186
+ destructive: action.attributes?.destructive ?? !action.attributes?.readOnly,
187
+ idempotent: action.attributes?.idempotent ?? false,
188
+ readOnly: action.attributes?.readOnly ?? false
189
+ };
190
+ }
166
191
  async filterByPermissions(entries, credentials) {
167
192
  const permissionedEntries = entries.filter(
168
193
  ([_, action]) => action.visibilityPermission
@@ -1 +1 @@
1
- {"version":3,"file":"DefaultActionsRegistryService.cjs.js","sources":["../../../../src/alpha/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AuthService,\n BackstageCredentials,\n HttpAuthService,\n LoggerService,\n PermissionsRegistryService,\n PermissionsService,\n PluginMetadataService,\n} from '@backstage/backend-plugin-api';\nimport PromiseRouter from 'express-promise-router';\nimport { Router, json } from 'express';\nimport { z, AnyZodObject } from 'zod/v3';\nimport zodToJsonSchema from 'zod-to-json-schema';\nimport {\n ActionsRegistryActionOptions,\n ActionsRegistryService,\n} from '@backstage/backend-plugin-api/alpha';\nimport { InputError, NotAllowedError, NotFoundError } from '@backstage/errors';\nimport { AuthorizeResult } from '@backstage/plugin-permission-common';\n\ntype ActionEntry = [string, ActionsRegistryActionOptions<any, any, any>];\n\nexport class DefaultActionsRegistryService implements ActionsRegistryService {\n private actions: Map<string, ActionsRegistryActionOptions<any, any, any>> =\n new Map();\n\n private readonly logger: LoggerService;\n private readonly httpAuth: HttpAuthService;\n private readonly auth: AuthService;\n private readonly metadata: PluginMetadataService;\n private readonly permissions: PermissionsService;\n private readonly permissionsRegistry: PermissionsRegistryService;\n\n private constructor(\n logger: LoggerService,\n httpAuth: HttpAuthService,\n auth: AuthService,\n metadata: PluginMetadataService,\n permissions: PermissionsService,\n permissionsRegistry: PermissionsRegistryService,\n ) {\n this.logger = logger;\n this.httpAuth = httpAuth;\n this.auth = auth;\n this.metadata = metadata;\n this.permissions = permissions;\n this.permissionsRegistry = permissionsRegistry;\n }\n\n static create({\n httpAuth,\n logger,\n auth,\n metadata,\n permissions,\n permissionsRegistry,\n }: {\n httpAuth: HttpAuthService;\n logger: LoggerService;\n auth: AuthService;\n metadata: PluginMetadataService;\n permissions: PermissionsService;\n permissionsRegistry: PermissionsRegistryService;\n }): DefaultActionsRegistryService {\n return new DefaultActionsRegistryService(\n logger,\n httpAuth,\n auth,\n metadata,\n permissions,\n permissionsRegistry,\n );\n }\n\n createRouter(): Router {\n const router = PromiseRouter();\n router.use('/.backstage/actions/', json());\n\n router.get('/.backstage/actions/v1/actions', async (req, res) => {\n const credentials = await this.httpAuth.credentials(req);\n const entries = Array.from(this.actions.entries());\n\n const allowedActions = await this.filterByPermissions(\n entries,\n credentials,\n );\n\n return res.json({\n actions: allowedActions.map(([id, action]) => ({\n id,\n name: action.name,\n title: action.title,\n description: action.description,\n pluginId: this.metadata.getId(),\n attributes: {\n // Inspired by the @modelcontextprotocol/sdk defaults for the hints.\n // https://github.com/modelcontextprotocol/typescript-sdk/blob/dd69efa1de8646bb6b195ff8d5f52e13739f4550/src/types.ts#L777-L812\n destructive:\n action.attributes?.destructive ?? !action.attributes?.readOnly,\n idempotent: action.attributes?.idempotent ?? false,\n readOnly: action.attributes?.readOnly ?? false,\n },\n examples: action.examples,\n schema: {\n input: action.schema?.input\n ? zodToJsonSchema(action.schema.input(z))\n : zodToJsonSchema(z.object({})),\n output: action.schema?.output\n ? zodToJsonSchema(action.schema.output(z))\n : zodToJsonSchema(z.object({})),\n ...(action.schema?.secrets && {\n secrets: zodToJsonSchema(action.schema.secrets(z)),\n }),\n },\n })),\n });\n });\n\n const invokeHandler =\n (opts: { wrapped: boolean }) =>\n async (\n req: import('express').Request,\n res: import('express').Response,\n ) => {\n const credentials = await this.httpAuth.credentials(req);\n if (this.auth.isPrincipal(credentials, 'none')) {\n throw new NotAllowedError(\n `Actions must be invoked by an authenticated principal, not an anonymous request`,\n );\n }\n\n const action = this.actions.get(req.params.actionId);\n\n if (!action) {\n throw new NotFoundError(`Action \"${req.params.actionId}\" not found`);\n }\n\n if (action.visibilityPermission) {\n const [decision] = await this.permissions.authorize(\n [{ permission: action.visibilityPermission }],\n { credentials },\n );\n if (decision.result !== AuthorizeResult.ALLOW) {\n throw new NotFoundError(\n `Action \"${req.params.actionId}\" not found`,\n );\n }\n }\n\n const rawInput = opts.wrapped ? req.body.input : req.body;\n const rawSecrets = opts.wrapped ? req.body.secrets : undefined;\n\n const input = action.schema?.input\n ? action.schema.input(z).safeParse(rawInput)\n : ({ success: true, data: undefined } as const);\n\n if (!input.success) {\n throw new InputError(\n `Invalid input to action \"${req.params.actionId}\"`,\n input.error,\n );\n }\n\n if (action.schema?.secrets && !rawSecrets) {\n throw new InputError(\n `Action \"${req.params.actionId}\" requires secrets but none were provided`,\n );\n }\n\n if (!action.schema?.secrets && rawSecrets) {\n throw new InputError(\n `Action \"${req.params.actionId}\" does not accept secrets`,\n );\n }\n\n const secrets = action.schema?.secrets\n ? action.schema.secrets(z).safeParse(rawSecrets)\n : ({ success: true, data: undefined } as const);\n\n if (!secrets.success) {\n throw new InputError(\n `Invalid secrets for action \"${req.params.actionId}\"`,\n secrets.error,\n );\n }\n\n const result = await action.action({\n input: input.data,\n secrets: secrets.data,\n credentials,\n logger: this.logger,\n });\n\n const output = action.schema?.output\n ? action.schema.output(z).safeParse(result?.output)\n : ({ success: true, data: result?.output } as const);\n\n if (!output.success) {\n throw new InputError(\n `Invalid output from action \"${req.params.actionId}\"`,\n output.error,\n );\n }\n\n res.json({ output: output.data });\n };\n\n // Deprecated: remove v1 invoke route once all callers have migrated to v2\n router.post(\n '/.backstage/actions/v1/actions/:actionId/invoke',\n invokeHandler({ wrapped: false }),\n );\n\n router.post(\n '/.backstage/actions/v2/actions/:actionId/invoke',\n invokeHandler({ wrapped: true }),\n );\n\n return router;\n }\n\n register<\n TInputSchema extends AnyZodObject,\n TOutputSchema extends AnyZodObject,\n TSecretsSchema extends AnyZodObject | undefined = undefined,\n >(\n options: ActionsRegistryActionOptions<\n TInputSchema,\n TOutputSchema,\n TSecretsSchema\n >,\n ): void {\n const id = `${this.metadata.getId()}:${options.name}`;\n\n if (this.actions.has(id)) {\n throw new Error(`Action with id \"${id}\" is already registered`);\n }\n\n if (options.visibilityPermission) {\n this.permissionsRegistry.addPermissions([options.visibilityPermission]);\n }\n\n this.actions.set(id, options);\n }\n\n private async filterByPermissions(\n entries: ActionEntry[],\n credentials: BackstageCredentials,\n ): Promise<ActionEntry[]> {\n const permissionedEntries = entries.filter(\n ([_, action]) => action.visibilityPermission,\n );\n\n if (permissionedEntries.length === 0) {\n return entries;\n }\n\n const decisions = await this.permissions.authorize(\n permissionedEntries.map(([_, action]) => ({\n permission: action.visibilityPermission!,\n })),\n { credentials },\n );\n\n const deniedIds = new Set(\n permissionedEntries\n .filter((_, index) => decisions[index].result !== AuthorizeResult.ALLOW)\n .map(([id]) => id),\n );\n\n return entries.filter(([id]) => !deniedIds.has(id));\n }\n}\n"],"names":["PromiseRouter","json","zodToJsonSchema","z","NotAllowedError","NotFoundError","AuthorizeResult","InputError"],"mappings":"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"}
1
+ {"version":3,"file":"DefaultActionsRegistryService.cjs.js","sources":["../../../../src/alpha/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AuthService,\n BackstageCredentials,\n HttpAuthService,\n LoggerService,\n PermissionsRegistryService,\n PermissionsService,\n PluginMetadataService,\n RootConfigService,\n} from '@backstage/backend-plugin-api';\nimport PromiseRouter from 'express-promise-router';\nimport { Router, json } from 'express';\nimport { z, AnyZodObject } from 'zod/v3';\nimport zodToJsonSchema from 'zod-to-json-schema';\nimport type {\n ActionsRegistryActionOptions,\n ActionsRegistryService,\n ActionsServiceAction,\n} from '@backstage/backend-plugin-api/alpha';\nimport { InputError, NotAllowedError, NotFoundError } from '@backstage/errors';\nimport { AuthorizeResult } from '@backstage/plugin-permission-common';\nimport { filterActions } from './actionFilters';\n\ntype ActionEntry = [string, ActionsRegistryActionOptions<any, any, any>];\n\nexport class DefaultActionsRegistryService implements ActionsRegistryService {\n private actions: Map<string, ActionsRegistryActionOptions<any, any, any>> =\n new Map();\n\n private readonly logger: LoggerService;\n private readonly httpAuth: HttpAuthService;\n private readonly auth: AuthService;\n private readonly config: RootConfigService;\n private readonly metadata: PluginMetadataService;\n private readonly permissions: PermissionsService;\n private readonly permissionsRegistry: PermissionsRegistryService;\n\n private constructor(\n logger: LoggerService,\n httpAuth: HttpAuthService,\n auth: AuthService,\n config: RootConfigService,\n metadata: PluginMetadataService,\n permissions: PermissionsService,\n permissionsRegistry: PermissionsRegistryService,\n ) {\n this.logger = logger;\n this.httpAuth = httpAuth;\n this.auth = auth;\n this.config = config;\n this.metadata = metadata;\n this.permissions = permissions;\n this.permissionsRegistry = permissionsRegistry;\n }\n\n static create({\n httpAuth,\n logger,\n auth,\n config,\n metadata,\n permissions,\n permissionsRegistry,\n }: {\n httpAuth: HttpAuthService;\n logger: LoggerService;\n auth: AuthService;\n config: RootConfigService;\n metadata: PluginMetadataService;\n permissions: PermissionsService;\n permissionsRegistry: PermissionsRegistryService;\n }): DefaultActionsRegistryService {\n return new DefaultActionsRegistryService(\n logger,\n httpAuth,\n auth,\n config,\n metadata,\n permissions,\n permissionsRegistry,\n );\n }\n\n createRouter(): Router {\n const router = PromiseRouter();\n router.use('/.backstage/actions/', json());\n\n router.get('/.backstage/actions/v1/actions', async (req, res) => {\n const credentials = await this.httpAuth.credentials(req);\n const entries = Array.from(this.actions.entries()).filter(entry =>\n this.isActionAllowed(entry),\n );\n\n const allowedActions = await this.filterByPermissions(\n entries,\n credentials,\n );\n\n return res.json({\n actions: allowedActions.map(([id, action]) => ({\n id,\n name: action.name,\n title: action.title,\n description: action.description,\n pluginId: this.metadata.getId(),\n attributes: this.toActionAttributes(action),\n examples: action.examples,\n schema: {\n input: action.schema?.input\n ? zodToJsonSchema(action.schema.input(z))\n : zodToJsonSchema(z.object({})),\n output: action.schema?.output\n ? zodToJsonSchema(action.schema.output(z))\n : zodToJsonSchema(z.object({})),\n ...(action.schema?.secrets && {\n secrets: zodToJsonSchema(action.schema.secrets(z)),\n }),\n },\n })),\n });\n });\n\n const invokeHandler =\n (opts: { wrapped: boolean }) =>\n async (\n req: import('express').Request,\n res: import('express').Response,\n ) => {\n const credentials = await this.httpAuth.credentials(req);\n if (this.auth.isPrincipal(credentials, 'none')) {\n throw new NotAllowedError(\n `Actions must be invoked by an authenticated principal, not an anonymous request`,\n );\n }\n\n const action = this.actions.get(req.params.actionId);\n\n if (!action) {\n throw new NotFoundError(`Action \"${req.params.actionId}\" not found`);\n }\n\n if (!this.isActionAllowed([req.params.actionId, action])) {\n throw new NotFoundError(`Action \"${req.params.actionId}\" not found`);\n }\n\n if (action.visibilityPermission) {\n const [decision] = await this.permissions.authorize(\n [{ permission: action.visibilityPermission }],\n { credentials },\n );\n if (decision.result !== AuthorizeResult.ALLOW) {\n throw new NotFoundError(\n `Action \"${req.params.actionId}\" not found`,\n );\n }\n }\n\n const rawInput = opts.wrapped ? req.body.input : req.body;\n const rawSecrets = opts.wrapped ? req.body.secrets : undefined;\n\n const input = action.schema?.input\n ? action.schema.input(z).safeParse(rawInput)\n : ({ success: true, data: undefined } as const);\n\n if (!input.success) {\n throw new InputError(\n `Invalid input to action \"${req.params.actionId}\"`,\n input.error,\n );\n }\n\n if (action.schema?.secrets && !rawSecrets) {\n throw new InputError(\n `Action \"${req.params.actionId}\" requires secrets but none were provided`,\n );\n }\n\n if (!action.schema?.secrets && rawSecrets) {\n throw new InputError(\n `Action \"${req.params.actionId}\" does not accept secrets`,\n );\n }\n\n const secrets = action.schema?.secrets\n ? action.schema.secrets(z).safeParse(rawSecrets)\n : ({ success: true, data: undefined } as const);\n\n if (!secrets.success) {\n throw new InputError(\n `Invalid secrets for action \"${req.params.actionId}\"`,\n secrets.error,\n );\n }\n\n const result = await action.action({\n input: input.data,\n secrets: secrets.data,\n credentials,\n logger: this.logger,\n });\n\n const output = action.schema?.output\n ? action.schema.output(z).safeParse(result?.output)\n : ({ success: true, data: result?.output } as const);\n\n if (!output.success) {\n throw new InputError(\n `Invalid output from action \"${req.params.actionId}\"`,\n output.error,\n );\n }\n\n res.json({ output: output.data });\n };\n\n // Deprecated: remove v1 invoke route once all callers have migrated to v2\n router.post(\n '/.backstage/actions/v1/actions/:actionId/invoke',\n invokeHandler({ wrapped: false }),\n );\n\n router.post(\n '/.backstage/actions/v2/actions/:actionId/invoke',\n invokeHandler({ wrapped: true }),\n );\n\n return router;\n }\n\n register<\n TInputSchema extends AnyZodObject,\n TOutputSchema extends AnyZodObject,\n TSecretsSchema extends AnyZodObject | undefined = undefined,\n >(\n options: ActionsRegistryActionOptions<\n TInputSchema,\n TOutputSchema,\n TSecretsSchema\n >,\n ): void {\n const id = `${this.metadata.getId()}:${options.name}`;\n\n if (this.actions.has(id)) {\n throw new Error(`Action with id \"${id}\" is already registered`);\n }\n\n if (options.visibilityPermission) {\n this.permissionsRegistry.addPermissions([options.visibilityPermission]);\n }\n\n this.actions.set(id, options);\n }\n\n private isActionAllowed([id, action]: ActionEntry): boolean {\n const pluginSources = this.config.getOptionalStringArray(\n 'backend.actions.pluginSources',\n );\n\n if (pluginSources && !pluginSources.includes(this.metadata.getId())) {\n return false;\n }\n\n return (\n filterActions(this.config, [\n {\n id,\n attributes: this.toActionAttributes(action),\n },\n ]).length === 1\n );\n }\n\n private toActionAttributes(\n action: ActionEntry[1],\n ): ActionsServiceAction['attributes'] {\n // Resolve optional attributes to the values exposed by ActionsService.\n return {\n destructive:\n action.attributes?.destructive ?? !action.attributes?.readOnly,\n idempotent: action.attributes?.idempotent ?? false,\n readOnly: action.attributes?.readOnly ?? false,\n };\n }\n\n private async filterByPermissions(\n entries: ActionEntry[],\n credentials: BackstageCredentials,\n ): Promise<ActionEntry[]> {\n const permissionedEntries = entries.filter(\n ([_, action]) => action.visibilityPermission,\n );\n\n if (permissionedEntries.length === 0) {\n return entries;\n }\n\n const decisions = await this.permissions.authorize(\n permissionedEntries.map(([_, action]) => ({\n permission: action.visibilityPermission!,\n })),\n { credentials },\n );\n\n const deniedIds = new Set(\n permissionedEntries\n .filter((_, index) => decisions[index].result !== AuthorizeResult.ALLOW)\n .map(([id]) => id),\n );\n\n return entries.filter(([id]) => !deniedIds.has(id));\n }\n}\n"],"names":["PromiseRouter","json","zodToJsonSchema","z","NotAllowedError","NotFoundError","AuthorizeResult","InputError","filterActions"],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+
3
+ var minimatch = require('minimatch');
4
+
5
+ const ACTION_ATTRIBUTE_KEYS = [
6
+ "destructive",
7
+ "readOnly",
8
+ "idempotent"
9
+ ];
10
+ function parseFilterRules(configArray) {
11
+ return configArray.map((ruleConfig) => {
12
+ const idPattern = ruleConfig.getOptionalString("id");
13
+ const attributesConfig = ruleConfig.getOptionalConfig("attributes");
14
+ const rule = {};
15
+ if (idPattern) {
16
+ rule.idMatcher = new minimatch.Minimatch(idPattern);
17
+ }
18
+ if (attributesConfig) {
19
+ rule.attributes = {};
20
+ for (const key of ACTION_ATTRIBUTE_KEYS) {
21
+ const value = attributesConfig.getOptionalBoolean(key);
22
+ if (value !== void 0) {
23
+ rule.attributes[key] = value;
24
+ }
25
+ }
26
+ }
27
+ return rule;
28
+ });
29
+ }
30
+ function matchesRule(action, rule) {
31
+ if (rule.idMatcher && !rule.idMatcher.match(action.id)) {
32
+ return false;
33
+ }
34
+ if (rule.attributes) {
35
+ for (const key of ACTION_ATTRIBUTE_KEYS) {
36
+ const value = rule.attributes[key];
37
+ if (value !== void 0 && action.attributes[key] !== value) {
38
+ return false;
39
+ }
40
+ }
41
+ }
42
+ return true;
43
+ }
44
+ function filterActions(config, actions) {
45
+ const filterConfig = config.getOptionalConfig("backend.actions.filter");
46
+ if (!filterConfig) {
47
+ return actions;
48
+ }
49
+ const includeRules = parseFilterRules(
50
+ filterConfig.getOptionalConfigArray("include") ?? []
51
+ );
52
+ const excludeRules = parseFilterRules(
53
+ filterConfig.getOptionalConfigArray("exclude") ?? []
54
+ );
55
+ return actions.filter((action) => {
56
+ if (excludeRules.some((rule) => matchesRule(action, rule))) {
57
+ return false;
58
+ }
59
+ if (includeRules.length === 0) {
60
+ return true;
61
+ }
62
+ return includeRules.some((rule) => matchesRule(action, rule));
63
+ });
64
+ }
65
+
66
+ exports.filterActions = filterActions;
67
+ //# sourceMappingURL=actionFilters.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actionFilters.cjs.js","sources":["../../../../src/alpha/entrypoints/actionsRegistry/actionFilters.ts"],"sourcesContent":["/*\n * Copyright 2026 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { ActionsServiceAction } from '@backstage/backend-plugin-api/alpha';\nimport type { RootConfigService } from '@backstage/backend-plugin-api';\nimport type { Config } from '@backstage/config';\nimport { Minimatch } from 'minimatch';\n\ninterface ActionFilterTarget {\n id: string;\n attributes: ActionsServiceAction['attributes'];\n}\n\nconst ACTION_ATTRIBUTE_KEYS = [\n 'destructive',\n 'readOnly',\n 'idempotent',\n] as const;\n\ninterface ActionFilterRule {\n idMatcher?: Minimatch;\n attributes?: Partial<Record<(typeof ACTION_ATTRIBUTE_KEYS)[number], boolean>>;\n}\n\nfunction parseFilterRules(configArray: Array<Config>): Array<ActionFilterRule> {\n return configArray.map(ruleConfig => {\n const idPattern = ruleConfig.getOptionalString('id');\n const attributesConfig = ruleConfig.getOptionalConfig('attributes');\n const rule: ActionFilterRule = {};\n\n if (idPattern) {\n rule.idMatcher = new Minimatch(idPattern);\n }\n\n if (attributesConfig) {\n rule.attributes = {};\n for (const key of ACTION_ATTRIBUTE_KEYS) {\n const value = attributesConfig.getOptionalBoolean(key);\n if (value !== undefined) {\n rule.attributes[key] = value;\n }\n }\n }\n\n return rule;\n });\n}\n\nfunction matchesRule(\n action: ActionFilterTarget,\n rule: ActionFilterRule,\n): boolean {\n if (rule.idMatcher && !rule.idMatcher.match(action.id)) {\n return false;\n }\n\n if (rule.attributes) {\n for (const key of ACTION_ATTRIBUTE_KEYS) {\n const value = rule.attributes[key];\n if (value !== undefined && action.attributes[key] !== value) {\n return false;\n }\n }\n }\n\n return true;\n}\n\n/**\n * Applies the configured action include and exclude rules.\n *\n * @param config - Root configuration containing the action filters\n * @param actions - Actions with resolved attributes to filter\n * @returns The actions permitted by the configured filters\n *\n * @remarks\n * Exclude rules take precedence. Without include rules, every action that is\n * not excluded is permitted.\n */\nexport function filterActions<TAction extends ActionFilterTarget>(\n config: RootConfigService,\n actions: Array<TAction>,\n): Array<TAction> {\n const filterConfig = config.getOptionalConfig('backend.actions.filter');\n\n if (!filterConfig) {\n return actions;\n }\n\n const includeRules = parseFilterRules(\n filterConfig.getOptionalConfigArray('include') ?? [],\n );\n const excludeRules = parseFilterRules(\n filterConfig.getOptionalConfigArray('exclude') ?? [],\n );\n\n return actions.filter(action => {\n if (excludeRules.some(rule => matchesRule(action, rule))) {\n return false;\n }\n\n if (includeRules.length === 0) {\n return true;\n }\n\n return includeRules.some(rule => matchesRule(action, rule));\n });\n}\n"],"names":["Minimatch"],"mappings":";;;;AA0BA,MAAM,qBAAA,GAAwB;AAAA,EAC5B,aAAA;AAAA,EACA,UAAA;AAAA,EACA;AACF,CAAA;AAOA,SAAS,iBAAiB,WAAA,EAAqD;AAC7E,EAAA,OAAO,WAAA,CAAY,IAAI,CAAA,UAAA,KAAc;AACnC,IAAA,MAAM,SAAA,GAAY,UAAA,CAAW,iBAAA,CAAkB,IAAI,CAAA;AACnD,IAAA,MAAM,gBAAA,GAAmB,UAAA,CAAW,iBAAA,CAAkB,YAAY,CAAA;AAClE,IAAA,MAAM,OAAyB,EAAC;AAEhC,IAAA,IAAI,SAAA,EAAW;AACb,MAAA,IAAA,CAAK,SAAA,GAAY,IAAIA,mBAAA,CAAU,SAAS,CAAA;AAAA,IAC1C;AAEA,IAAA,IAAI,gBAAA,EAAkB;AACpB,MAAA,IAAA,CAAK,aAAa,EAAC;AACnB,MAAA,KAAA,MAAW,OAAO,qBAAA,EAAuB;AACvC,QAAA,MAAM,KAAA,GAAQ,gBAAA,CAAiB,kBAAA,CAAmB,GAAG,CAAA;AACrD,QAAA,IAAI,UAAU,MAAA,EAAW;AACvB,UAAA,IAAA,CAAK,UAAA,CAAW,GAAG,CAAA,GAAI,KAAA;AAAA,QACzB;AAAA,MACF;AAAA,IACF;AAEA,IAAA,OAAO,IAAA;AAAA,EACT,CAAC,CAAA;AACH;AAEA,SAAS,WAAA,CACP,QACA,IAAA,EACS;AACT,EAAA,IAAI,IAAA,CAAK,aAAa,CAAC,IAAA,CAAK,UAAU,KAAA,CAAM,MAAA,CAAO,EAAE,CAAA,EAAG;AACtD,IAAA,OAAO,KAAA;AAAA,EACT;AAEA,EAAA,IAAI,KAAK,UAAA,EAAY;AACnB,IAAA,KAAA,MAAW,OAAO,qBAAA,EAAuB;AACvC,MAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,UAAA,CAAW,GAAG,CAAA;AACjC,MAAA,IAAI,UAAU,MAAA,IAAa,MAAA,CAAO,UAAA,CAAW,GAAG,MAAM,KAAA,EAAO;AAC3D,QAAA,OAAO,KAAA;AAAA,MACT;AAAA,IACF;AAAA,EACF;AAEA,EAAA,OAAO,IAAA;AACT;AAaO,SAAS,aAAA,CACd,QACA,OAAA,EACgB;AAChB,EAAA,MAAM,YAAA,GAAe,MAAA,CAAO,iBAAA,CAAkB,wBAAwB,CAAA;AAEtE,EAAA,IAAI,CAAC,YAAA,EAAc;AACjB,IAAA,OAAO,OAAA;AAAA,EACT;AAEA,EAAA,MAAM,YAAA,GAAe,gBAAA;AAAA,IACnB,YAAA,CAAa,sBAAA,CAAuB,SAAS,CAAA,IAAK;AAAC,GACrD;AACA,EAAA,MAAM,YAAA,GAAe,gBAAA;AAAA,IACnB,YAAA,CAAa,sBAAA,CAAuB,SAAS,CAAA,IAAK;AAAC,GACrD;AAEA,EAAA,OAAO,OAAA,CAAQ,OAAO,CAAA,MAAA,KAAU;AAC9B,IAAA,IAAI,aAAa,IAAA,CAAK,CAAA,IAAA,KAAQ,YAAY,MAAA,EAAQ,IAAI,CAAC,CAAA,EAAG;AACxD,MAAA,OAAO,KAAA;AAAA,IACT;AAEA,IAAA,IAAI,YAAA,CAAa,WAAW,CAAA,EAAG;AAC7B,MAAA,OAAO,IAAA;AAAA,IACT;AAEA,IAAA,OAAO,aAAa,IAAA,CAAK,CAAA,IAAA,KAAQ,WAAA,CAAY,MAAA,EAAQ,IAAI,CAAC,CAAA;AAAA,EAC5D,CAAC,CAAA;AACH;;;;"}
@@ -12,6 +12,7 @@ const actionsRegistryServiceFactory = backendPluginApi.createServiceFactory({
12
12
  httpAuth: backendPluginApi.coreServices.httpAuth,
13
13
  logger: backendPluginApi.coreServices.logger,
14
14
  auth: backendPluginApi.coreServices.auth,
15
+ config: backendPluginApi.coreServices.rootConfig,
15
16
  permissions: backendPluginApi.coreServices.permissions,
16
17
  permissionsRegistry: backendPluginApi.coreServices.permissionsRegistry
17
18
  },
@@ -21,6 +22,7 @@ const actionsRegistryServiceFactory = backendPluginApi.createServiceFactory({
21
22
  httpAuth,
22
23
  logger,
23
24
  auth,
25
+ config,
24
26
  permissions,
25
27
  permissionsRegistry
26
28
  }) => {
@@ -28,6 +30,7 @@ const actionsRegistryServiceFactory = backendPluginApi.createServiceFactory({
28
30
  httpAuth,
29
31
  logger,
30
32
  auth,
33
+ config,
31
34
  metadata,
32
35
  permissions,
33
36
  permissionsRegistry
@@ -1 +1 @@
1
- {"version":3,"file":"actionsRegistryServiceFactory.cjs.js","sources":["../../../../src/alpha/entrypoints/actionsRegistry/actionsRegistryServiceFactory.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n coreServices,\n createServiceFactory,\n} from '@backstage/backend-plugin-api';\nimport { DefaultActionsRegistryService } from './DefaultActionsRegistryService';\nimport { actionsRegistryServiceRef } from '@backstage/backend-plugin-api/alpha';\n\n/**\n * @public\n */\nexport const actionsRegistryServiceFactory = createServiceFactory({\n service: actionsRegistryServiceRef,\n deps: {\n metadata: coreServices.pluginMetadata,\n httpRouter: coreServices.httpRouter,\n httpAuth: coreServices.httpAuth,\n logger: coreServices.logger,\n auth: coreServices.auth,\n permissions: coreServices.permissions,\n permissionsRegistry: coreServices.permissionsRegistry,\n },\n factory: ({\n metadata,\n httpRouter,\n httpAuth,\n logger,\n auth,\n permissions,\n permissionsRegistry,\n }) => {\n const actionsRegistryService = DefaultActionsRegistryService.create({\n httpAuth,\n logger,\n auth,\n metadata,\n permissions,\n permissionsRegistry,\n });\n\n httpRouter.use(actionsRegistryService.createRouter());\n\n return actionsRegistryService;\n },\n});\n"],"names":["createServiceFactory","actionsRegistryServiceRef","coreServices","DefaultActionsRegistryService"],"mappings":";;;;;;AA0BO,MAAM,gCAAgCA,qCAAA,CAAqB;AAAA,EAChE,OAAA,EAASC,+BAAA;AAAA,EACT,IAAA,EAAM;AAAA,IACJ,UAAUC,6BAAA,CAAa,cAAA;AAAA,IACvB,YAAYA,6BAAA,CAAa,UAAA;AAAA,IACzB,UAAUA,6BAAA,CAAa,QAAA;AAAA,IACvB,QAAQA,6BAAA,CAAa,MAAA;AAAA,IACrB,MAAMA,6BAAA,CAAa,IAAA;AAAA,IACnB,aAAaA,6BAAA,CAAa,WAAA;AAAA,IAC1B,qBAAqBA,6BAAA,CAAa;AAAA,GACpC;AAAA,EACA,SAAS,CAAC;AAAA,IACR,QAAA;AAAA,IACA,UAAA;AAAA,IACA,QAAA;AAAA,IACA,MAAA;AAAA,IACA,IAAA;AAAA,IACA,WAAA;AAAA,IACA;AAAA,GACF,KAAM;AACJ,IAAA,MAAM,sBAAA,GAAyBC,4DAA8B,MAAA,CAAO;AAAA,MAClE,QAAA;AAAA,MACA,MAAA;AAAA,MACA,IAAA;AAAA,MACA,QAAA;AAAA,MACA,WAAA;AAAA,MACA;AAAA,KACD,CAAA;AAED,IAAA,UAAA,CAAW,GAAA,CAAI,sBAAA,CAAuB,YAAA,EAAc,CAAA;AAEpD,IAAA,OAAO,sBAAA;AAAA,EACT;AACF,CAAC;;;;"}
1
+ {"version":3,"file":"actionsRegistryServiceFactory.cjs.js","sources":["../../../../src/alpha/entrypoints/actionsRegistry/actionsRegistryServiceFactory.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n coreServices,\n createServiceFactory,\n} from '@backstage/backend-plugin-api';\nimport { DefaultActionsRegistryService } from './DefaultActionsRegistryService';\nimport { actionsRegistryServiceRef } from '@backstage/backend-plugin-api/alpha';\n\n/**\n * @public\n */\nexport const actionsRegistryServiceFactory = createServiceFactory({\n service: actionsRegistryServiceRef,\n deps: {\n metadata: coreServices.pluginMetadata,\n httpRouter: coreServices.httpRouter,\n httpAuth: coreServices.httpAuth,\n logger: coreServices.logger,\n auth: coreServices.auth,\n config: coreServices.rootConfig,\n permissions: coreServices.permissions,\n permissionsRegistry: coreServices.permissionsRegistry,\n },\n factory: ({\n metadata,\n httpRouter,\n httpAuth,\n logger,\n auth,\n config,\n permissions,\n permissionsRegistry,\n }) => {\n const actionsRegistryService = DefaultActionsRegistryService.create({\n httpAuth,\n logger,\n auth,\n config,\n metadata,\n permissions,\n permissionsRegistry,\n });\n\n httpRouter.use(actionsRegistryService.createRouter());\n\n return actionsRegistryService;\n },\n});\n"],"names":["createServiceFactory","actionsRegistryServiceRef","coreServices","DefaultActionsRegistryService"],"mappings":";;;;;;AA0BO,MAAM,gCAAgCA,qCAAA,CAAqB;AAAA,EAChE,OAAA,EAASC,+BAAA;AAAA,EACT,IAAA,EAAM;AAAA,IACJ,UAAUC,6BAAA,CAAa,cAAA;AAAA,IACvB,YAAYA,6BAAA,CAAa,UAAA;AAAA,IACzB,UAAUA,6BAAA,CAAa,QAAA;AAAA,IACvB,QAAQA,6BAAA,CAAa,MAAA;AAAA,IACrB,MAAMA,6BAAA,CAAa,IAAA;AAAA,IACnB,QAAQA,6BAAA,CAAa,UAAA;AAAA,IACrB,aAAaA,6BAAA,CAAa,WAAA;AAAA,IAC1B,qBAAqBA,6BAAA,CAAa;AAAA,GACpC;AAAA,EACA,SAAS,CAAC;AAAA,IACR,QAAA;AAAA,IACA,UAAA;AAAA,IACA,QAAA;AAAA,IACA,MAAA;AAAA,IACA,IAAA;AAAA,IACA,MAAA;AAAA,IACA,WAAA;AAAA,IACA;AAAA,GACF,KAAM;AACJ,IAAA,MAAM,sBAAA,GAAyBC,4DAA8B,MAAA,CAAO;AAAA,MAClE,QAAA;AAAA,MACA,MAAA;AAAA,MACA,IAAA;AAAA,MACA,MAAA;AAAA,MACA,QAAA;AAAA,MACA,WAAA;AAAA,MACA;AAAA,KACD,CAAA;AAED,IAAA,UAAA,CAAW,GAAA,CAAI,sBAAA,CAAuB,YAAA,EAAc,CAAA;AAEpD,IAAA,OAAO,sBAAA;AAAA,EACT;AACF,CAAC;;;;"}
@@ -56,7 +56,8 @@ class DefaultAuthService {
56
56
  return helpers.createCredentialsWithServicePrincipal(
57
57
  externalResult.subject,
58
58
  void 0,
59
- externalResult.accessRestrictions
59
+ externalResult.accessRestrictions,
60
+ externalResult.allAccessRestrictions
60
61
  );
61
62
  }
62
63
  throw new errors.AuthenticationError("Illegal token");
@@ -86,11 +87,26 @@ class DefaultAuthService {
86
87
  }
87
88
  switch (type) {
88
89
  // TODO: Check whether the principal is ourselves
89
- case "service":
90
+ case "service": {
91
+ const servicePrincipal = internalForward.principal;
92
+ if (servicePrincipal.accessRestrictions) {
93
+ const targetRestrictions = internalForward.allAccessRestrictions?.get(targetPluginId);
94
+ if (!targetRestrictions) {
95
+ throw new errors.NotAllowedError(
96
+ `Access to target plugin '${targetPluginId}' is not included in token's access restrictions`
97
+ );
98
+ }
99
+ if (targetRestrictions.permissionNames?.length || targetRestrictions.permissionAttributes) {
100
+ throw new errors.NotAllowedError(
101
+ `Access to target plugin '${targetPluginId}' is restricted and cannot be delegated`
102
+ );
103
+ }
104
+ }
90
105
  return this.pluginTokenHandler.issueToken({
91
106
  pluginId: this.pluginId,
92
107
  targetPluginId
93
108
  });
109
+ }
94
110
  case "user": {
95
111
  const { token } = internalForward;
96
112
  if (!token) {
@@ -1 +1 @@
1
- {"version":3,"file":"DefaultAuthService.cjs.js","sources":["../../../src/entrypoints/auth/DefaultAuthService.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AuthService,\n BackstageCredentials,\n BackstageNonePrincipal,\n BackstagePrincipalTypes,\n BackstageServicePrincipal,\n BackstageUserPrincipal,\n} from '@backstage/backend-plugin-api';\nimport { AuthenticationError } from '@backstage/errors';\nimport { JsonObject } from '@backstage/types';\nimport { decodeJwt } from 'jose';\nimport { ExternalAuthTokenHandler } from './external/ExternalAuthTokenHandler';\nimport {\n createCredentialsWithNonePrincipal,\n createCredentialsWithServicePrincipal,\n createCredentialsWithUserPrincipal,\n toInternalBackstageCredentials,\n} from './helpers';\nimport { PluginTokenHandler } from './plugin/PluginTokenHandler';\nimport { PluginKeySource } from './plugin/keys/types';\nimport { UserTokenHandler } from './user/UserTokenHandler';\n\n/** @internal */\nexport class DefaultAuthService implements AuthService {\n private readonly userTokenHandler: UserTokenHandler;\n private readonly pluginTokenHandler: PluginTokenHandler;\n private readonly externalTokenHandler: ExternalAuthTokenHandler;\n private readonly pluginId: string;\n private readonly disableDefaultAuthPolicy: boolean;\n private readonly pluginKeySource: PluginKeySource;\n\n constructor(\n userTokenHandler: UserTokenHandler,\n pluginTokenHandler: PluginTokenHandler,\n externalTokenHandler: ExternalAuthTokenHandler,\n pluginId: string,\n disableDefaultAuthPolicy: boolean,\n pluginKeySource: PluginKeySource,\n ) {\n this.userTokenHandler = userTokenHandler;\n this.pluginTokenHandler = pluginTokenHandler;\n this.externalTokenHandler = externalTokenHandler;\n this.pluginId = pluginId;\n this.disableDefaultAuthPolicy = disableDefaultAuthPolicy;\n this.pluginKeySource = pluginKeySource;\n }\n\n async authenticate(\n token: string,\n options?: {\n allowLimitedAccess?: boolean;\n },\n ): Promise<BackstageCredentials> {\n const pluginResult = await this.pluginTokenHandler.verifyToken(token);\n if (pluginResult) {\n if (pluginResult.limitedUserToken) {\n const userResult = await this.userTokenHandler.verifyToken(\n pluginResult.limitedUserToken,\n );\n if (!userResult) {\n throw new AuthenticationError(\n 'Invalid user token in plugin token obo claim',\n );\n }\n return createCredentialsWithUserPrincipal(\n userResult.userEntityRef,\n pluginResult.limitedUserToken,\n this.#getJwtExpiration(pluginResult.limitedUserToken),\n pluginResult.subject,\n );\n }\n return createCredentialsWithServicePrincipal(pluginResult.subject);\n }\n\n const userResult = await this.userTokenHandler.verifyToken(token);\n if (userResult) {\n if (\n !options?.allowLimitedAccess &&\n this.userTokenHandler.isLimitedUserToken(token)\n ) {\n throw new AuthenticationError('Illegal limited user token');\n }\n\n return createCredentialsWithUserPrincipal(\n userResult.userEntityRef,\n token,\n this.#getJwtExpiration(token),\n );\n }\n\n const externalResult = await this.externalTokenHandler.verifyToken(token);\n if (externalResult) {\n return createCredentialsWithServicePrincipal(\n externalResult.subject,\n undefined,\n externalResult.accessRestrictions,\n );\n }\n\n throw new AuthenticationError('Illegal token');\n }\n\n isPrincipal<TType extends keyof BackstagePrincipalTypes>(\n credentials: BackstageCredentials,\n type: TType,\n ): credentials is BackstageCredentials<BackstagePrincipalTypes[TType]> {\n const principal = credentials.principal as\n | BackstageUserPrincipal\n | BackstageServicePrincipal;\n\n if (type === 'unknown') {\n return true;\n }\n\n if (principal.type !== type) {\n return false;\n }\n\n return true;\n }\n\n async getNoneCredentials(): Promise<\n BackstageCredentials<BackstageNonePrincipal>\n > {\n return createCredentialsWithNonePrincipal();\n }\n\n async getOwnServiceCredentials(): Promise<\n BackstageCredentials<BackstageServicePrincipal>\n > {\n return createCredentialsWithServicePrincipal(`plugin:${this.pluginId}`);\n }\n\n async getPluginRequestToken(options: {\n onBehalfOf: BackstageCredentials;\n targetPluginId: string;\n }): Promise<{ token: string }> {\n const { targetPluginId } = options;\n const internalForward = toInternalBackstageCredentials(options.onBehalfOf);\n const { type } = internalForward.principal;\n\n // Since disabling the default policy means we'll be allowing\n // unauthenticated requests through, we might have unauthenticated\n // credentials from service calls that reach this point. If that's the case,\n // we'll want to keep \"forwarding\" the unauthenticated credentials, which we\n // do by returning an empty token.\n if (type === 'none' && this.disableDefaultAuthPolicy) {\n return { token: '' };\n }\n\n // check whether a plugin support the new auth system\n // by checking the public keys endpoint existence.\n switch (type) {\n // TODO: Check whether the principal is ourselves\n case 'service':\n return this.pluginTokenHandler.issueToken({\n pluginId: this.pluginId,\n targetPluginId,\n });\n case 'user': {\n const { token } = internalForward;\n if (!token) {\n throw new Error('User credentials is unexpectedly missing token');\n }\n const onBehalfOf = await this.userTokenHandler.createLimitedUserToken(\n token,\n );\n return this.pluginTokenHandler.issueToken({\n pluginId: this.pluginId,\n targetPluginId,\n onBehalfOf: {\n limitedUserToken: onBehalfOf.token,\n expiresAt: onBehalfOf.expiresAt,\n },\n });\n }\n default:\n throw new AuthenticationError(\n `Refused to issue service token for credential type '${type}'`,\n );\n }\n }\n\n async getLimitedUserToken(\n credentials: BackstageCredentials<BackstageUserPrincipal>,\n ): Promise<{ token: string; expiresAt: Date }> {\n const { token: backstageToken } =\n toInternalBackstageCredentials(credentials);\n if (!backstageToken) {\n throw new AuthenticationError(\n 'User credentials is unexpectedly missing token',\n );\n }\n\n return this.userTokenHandler.createLimitedUserToken(backstageToken);\n }\n\n async listPublicServiceKeys(): Promise<{ keys: JsonObject[] }> {\n const { keys } = await this.pluginKeySource.listKeys();\n return { keys: keys.map(({ key }) => key) };\n }\n\n #getJwtExpiration(token: string) {\n const { exp } = decodeJwt(token);\n if (!exp) {\n throw new AuthenticationError('User token is missing expiration');\n }\n return new Date(exp * 1000);\n }\n}\n"],"names":["userResult","AuthenticationError","createCredentialsWithUserPrincipal","createCredentialsWithServicePrincipal","createCredentialsWithNonePrincipal","toInternalBackstageCredentials","decodeJwt"],"mappings":"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"}
1
+ {"version":3,"file":"DefaultAuthService.cjs.js","sources":["../../../src/entrypoints/auth/DefaultAuthService.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AuthService,\n BackstageCredentials,\n BackstageNonePrincipal,\n BackstagePrincipalTypes,\n BackstageServicePrincipal,\n BackstageUserPrincipal,\n} from '@backstage/backend-plugin-api';\nimport { AuthenticationError, NotAllowedError } from '@backstage/errors';\nimport { JsonObject } from '@backstage/types';\nimport { decodeJwt } from 'jose';\nimport { ExternalAuthTokenHandler } from './external/ExternalAuthTokenHandler';\nimport {\n createCredentialsWithNonePrincipal,\n createCredentialsWithServicePrincipal,\n createCredentialsWithUserPrincipal,\n toInternalBackstageCredentials,\n} from './helpers';\nimport { PluginTokenHandler } from './plugin/PluginTokenHandler';\nimport { PluginKeySource } from './plugin/keys/types';\nimport { UserTokenHandler } from './user/UserTokenHandler';\n\n/** @internal */\nexport class DefaultAuthService implements AuthService {\n private readonly userTokenHandler: UserTokenHandler;\n private readonly pluginTokenHandler: PluginTokenHandler;\n private readonly externalTokenHandler: ExternalAuthTokenHandler;\n private readonly pluginId: string;\n private readonly disableDefaultAuthPolicy: boolean;\n private readonly pluginKeySource: PluginKeySource;\n\n constructor(\n userTokenHandler: UserTokenHandler,\n pluginTokenHandler: PluginTokenHandler,\n externalTokenHandler: ExternalAuthTokenHandler,\n pluginId: string,\n disableDefaultAuthPolicy: boolean,\n pluginKeySource: PluginKeySource,\n ) {\n this.userTokenHandler = userTokenHandler;\n this.pluginTokenHandler = pluginTokenHandler;\n this.externalTokenHandler = externalTokenHandler;\n this.pluginId = pluginId;\n this.disableDefaultAuthPolicy = disableDefaultAuthPolicy;\n this.pluginKeySource = pluginKeySource;\n }\n\n async authenticate(\n token: string,\n options?: {\n allowLimitedAccess?: boolean;\n },\n ): Promise<BackstageCredentials> {\n const pluginResult = await this.pluginTokenHandler.verifyToken(token);\n if (pluginResult) {\n if (pluginResult.limitedUserToken) {\n const userResult = await this.userTokenHandler.verifyToken(\n pluginResult.limitedUserToken,\n );\n if (!userResult) {\n throw new AuthenticationError(\n 'Invalid user token in plugin token obo claim',\n );\n }\n return createCredentialsWithUserPrincipal(\n userResult.userEntityRef,\n pluginResult.limitedUserToken,\n this.#getJwtExpiration(pluginResult.limitedUserToken),\n pluginResult.subject,\n );\n }\n return createCredentialsWithServicePrincipal(pluginResult.subject);\n }\n\n const userResult = await this.userTokenHandler.verifyToken(token);\n if (userResult) {\n if (\n !options?.allowLimitedAccess &&\n this.userTokenHandler.isLimitedUserToken(token)\n ) {\n throw new AuthenticationError('Illegal limited user token');\n }\n\n return createCredentialsWithUserPrincipal(\n userResult.userEntityRef,\n token,\n this.#getJwtExpiration(token),\n );\n }\n\n const externalResult = await this.externalTokenHandler.verifyToken(token);\n if (externalResult) {\n return createCredentialsWithServicePrincipal(\n externalResult.subject,\n undefined,\n externalResult.accessRestrictions,\n externalResult.allAccessRestrictions,\n );\n }\n\n throw new AuthenticationError('Illegal token');\n }\n\n isPrincipal<TType extends keyof BackstagePrincipalTypes>(\n credentials: BackstageCredentials,\n type: TType,\n ): credentials is BackstageCredentials<BackstagePrincipalTypes[TType]> {\n const principal = credentials.principal as\n | BackstageUserPrincipal\n | BackstageServicePrincipal;\n\n if (type === 'unknown') {\n return true;\n }\n\n if (principal.type !== type) {\n return false;\n }\n\n return true;\n }\n\n async getNoneCredentials(): Promise<\n BackstageCredentials<BackstageNonePrincipal>\n > {\n return createCredentialsWithNonePrincipal();\n }\n\n async getOwnServiceCredentials(): Promise<\n BackstageCredentials<BackstageServicePrincipal>\n > {\n return createCredentialsWithServicePrincipal(`plugin:${this.pluginId}`);\n }\n\n async getPluginRequestToken(options: {\n onBehalfOf: BackstageCredentials;\n targetPluginId: string;\n }): Promise<{ token: string }> {\n const { targetPluginId } = options;\n const internalForward = toInternalBackstageCredentials(options.onBehalfOf);\n const { type } = internalForward.principal;\n\n // Since disabling the default policy means we'll be allowing\n // unauthenticated requests through, we might have unauthenticated\n // credentials from service calls that reach this point. If that's the case,\n // we'll want to keep \"forwarding\" the unauthenticated credentials, which we\n // do by returning an empty token.\n if (type === 'none' && this.disableDefaultAuthPolicy) {\n return { token: '' };\n }\n\n // check whether a plugin support the new auth system\n // by checking the public keys endpoint existence.\n switch (type) {\n // TODO: Check whether the principal is ourselves\n case 'service': {\n const servicePrincipal =\n internalForward.principal as BackstageServicePrincipal;\n if (servicePrincipal.accessRestrictions) {\n const targetRestrictions =\n internalForward.allAccessRestrictions?.get(targetPluginId);\n if (!targetRestrictions) {\n throw new NotAllowedError(\n `Access to target plugin '${targetPluginId}' is not included in token's access restrictions`,\n );\n }\n if (\n targetRestrictions.permissionNames?.length ||\n targetRestrictions.permissionAttributes\n ) {\n throw new NotAllowedError(\n `Access to target plugin '${targetPluginId}' is restricted and cannot be delegated`,\n );\n }\n }\n return this.pluginTokenHandler.issueToken({\n pluginId: this.pluginId,\n targetPluginId,\n });\n }\n case 'user': {\n const { token } = internalForward;\n if (!token) {\n throw new Error('User credentials is unexpectedly missing token');\n }\n const onBehalfOf = await this.userTokenHandler.createLimitedUserToken(\n token,\n );\n return this.pluginTokenHandler.issueToken({\n pluginId: this.pluginId,\n targetPluginId,\n onBehalfOf: {\n limitedUserToken: onBehalfOf.token,\n expiresAt: onBehalfOf.expiresAt,\n },\n });\n }\n default:\n throw new AuthenticationError(\n `Refused to issue service token for credential type '${type}'`,\n );\n }\n }\n\n async getLimitedUserToken(\n credentials: BackstageCredentials<BackstageUserPrincipal>,\n ): Promise<{ token: string; expiresAt: Date }> {\n const { token: backstageToken } =\n toInternalBackstageCredentials(credentials);\n if (!backstageToken) {\n throw new AuthenticationError(\n 'User credentials is unexpectedly missing token',\n );\n }\n\n return this.userTokenHandler.createLimitedUserToken(backstageToken);\n }\n\n async listPublicServiceKeys(): Promise<{ keys: JsonObject[] }> {\n const { keys } = await this.pluginKeySource.listKeys();\n return { keys: keys.map(({ key }) => key) };\n }\n\n #getJwtExpiration(token: string) {\n const { exp } = decodeJwt(token);\n if (!exp) {\n throw new AuthenticationError('User token is missing expiration');\n }\n return new Date(exp * 1000);\n }\n}\n"],"names":["userResult","AuthenticationError","createCredentialsWithUserPrincipal","createCredentialsWithServicePrincipal","createCredentialsWithNonePrincipal","toInternalBackstageCredentials","NotAllowedError","decodeJwt"],"mappings":"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"}
@@ -1,48 +1,77 @@
1
1
  'use strict';
2
2
 
3
- var errors = require('@backstage/errors');
4
3
  var jose = require('jose');
5
4
 
6
- const CLOCK_MARGIN_S = 10;
5
+ const FORCED_RELOAD_LIMIT = 10;
6
+ const FORCED_RELOAD_WINDOW_MS = 6e4;
7
+ const KEY_STORE_CACHE_LIMIT = 100;
8
+ class JwksKeyStore {
9
+ #resolver;
10
+ #tryUseForcedReload;
11
+ constructor(endpoint, tryUseForcedReload) {
12
+ this.#resolver = jose.createRemoteJWKSet(endpoint);
13
+ this.#tryUseForcedReload = tryUseForcedReload;
14
+ }
15
+ getKey = async (protectedHeader, token) => {
16
+ const coolingDown = this.#resolver.coolingDown;
17
+ try {
18
+ return await this.#resolver(protectedHeader, token);
19
+ } catch (error) {
20
+ if (!(error instanceof jose.errors.JWKSNoMatchingKey)) {
21
+ throw error;
22
+ }
23
+ if (!coolingDown || !await this.#reload()) {
24
+ throw error;
25
+ }
26
+ return this.#resolver(protectedHeader, token);
27
+ }
28
+ };
29
+ async #reload() {
30
+ if (!this.#resolver.reloading && !this.#tryUseForcedReload()) {
31
+ return false;
32
+ }
33
+ await this.#resolver.reload();
34
+ return true;
35
+ }
36
+ }
7
37
  class JwksClient {
8
- #keyStore;
9
- #keyStoreUpdated = 0;
38
+ #keyStores = /* @__PURE__ */ new Map();
39
+ #forcedReloads = [];
10
40
  getEndpoint;
11
41
  constructor(getEndpoint) {
12
42
  this.getEndpoint = getEndpoint;
13
43
  }
14
44
  get getKey() {
15
- if (!this.#keyStore) {
16
- throw new errors.AuthenticationError(
17
- "refreshKeyStore must be called before jwksClient.getKey"
18
- );
19
- }
20
- return this.#keyStore;
45
+ return this.#getKey;
21
46
  }
22
- /**
23
- * If the last keystore refresh is stale, update the keystore URL to the latest
24
- */
25
- async refreshKeyStore(rawJwtToken) {
26
- const payload = await jose.decodeJwt(rawJwtToken);
27
- const header = await jose.decodeProtectedHeader(rawJwtToken);
28
- let keyStoreHasKey;
29
- try {
30
- if (this.#keyStore) {
31
- const [_, rawPayload, rawSignature] = rawJwtToken.split(".");
32
- keyStoreHasKey = await this.#keyStore(header, {
33
- payload: rawPayload,
34
- signature: rawSignature
35
- });
36
- }
37
- } catch (error) {
38
- keyStoreHasKey = false;
47
+ #getKey = async (protectedHeader, token) => {
48
+ const keyStore = await this.#getKeyStore();
49
+ return keyStore.getKey(protectedHeader, token);
50
+ };
51
+ async #getKeyStore() {
52
+ const endpoint = await this.getEndpoint();
53
+ let keyStore = this.#keyStores.get(endpoint.href);
54
+ if (keyStore) {
55
+ this.#keyStores.delete(endpoint.href);
56
+ } else {
57
+ keyStore = new JwksKeyStore(endpoint, () => this.#tryUseForcedReload());
58
+ }
59
+ this.#keyStores.set(endpoint.href, keyStore);
60
+ if (this.#keyStores.size > KEY_STORE_CACHE_LIMIT) {
61
+ this.#keyStores.delete(this.#keyStores.keys().next().value);
39
62
  }
40
- const issuedAfterLastRefresh = payload?.iat && payload.iat > this.#keyStoreUpdated - CLOCK_MARGIN_S;
41
- if (!this.#keyStore || !keyStoreHasKey && issuedAfterLastRefresh) {
42
- const endpoint = await this.getEndpoint();
43
- this.#keyStore = jose.createRemoteJWKSet(endpoint);
44
- this.#keyStoreUpdated = Date.now() / 1e3;
63
+ return keyStore;
64
+ }
65
+ #tryUseForcedReload() {
66
+ const windowStart = Date.now() - FORCED_RELOAD_WINDOW_MS;
67
+ this.#forcedReloads = this.#forcedReloads.filter(
68
+ (timestamp) => timestamp > windowStart
69
+ );
70
+ if (this.#forcedReloads.length >= FORCED_RELOAD_LIMIT) {
71
+ return false;
45
72
  }
73
+ this.#forcedReloads.push(Date.now());
74
+ return true;
46
75
  }
47
76
  }
48
77
 
@@ -1 +1 @@
1
- {"version":3,"file":"JwksClient.cjs.js","sources":["../../../src/entrypoints/auth/JwksClient.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { AuthenticationError } from '@backstage/errors';\nimport {\n createRemoteJWKSet,\n decodeJwt,\n decodeProtectedHeader,\n FlattenedJWSInput,\n JWSHeaderParameters,\n} from 'jose';\nimport { GetKeyFunction } from 'jose';\n\nconst CLOCK_MARGIN_S = 10;\n\nexport class JwksClient {\n #keyStore?: GetKeyFunction<JWSHeaderParameters, FlattenedJWSInput>;\n #keyStoreUpdated: number = 0;\n\n private readonly getEndpoint: () => Promise<URL>;\n\n constructor(getEndpoint: () => Promise<URL>) {\n this.getEndpoint = getEndpoint;\n }\n\n get getKey() {\n if (!this.#keyStore) {\n throw new AuthenticationError(\n 'refreshKeyStore must be called before jwksClient.getKey',\n );\n }\n return this.#keyStore;\n }\n\n /**\n * If the last keystore refresh is stale, update the keystore URL to the latest\n */\n async refreshKeyStore(rawJwtToken: string): Promise<void> {\n const payload = await decodeJwt(rawJwtToken);\n const header = await decodeProtectedHeader(rawJwtToken);\n\n // Refresh public keys if needed\n let keyStoreHasKey;\n try {\n if (this.#keyStore) {\n // Check if the key is present in the keystore\n const [_, rawPayload, rawSignature] = rawJwtToken.split('.');\n keyStoreHasKey = await this.#keyStore(header, {\n payload: rawPayload,\n signature: rawSignature,\n });\n }\n } catch (error) {\n keyStoreHasKey = false;\n }\n // Refresh public key URL if needed\n // Add a small margin in case clocks are out of sync\n const issuedAfterLastRefresh =\n payload?.iat && payload.iat > this.#keyStoreUpdated - CLOCK_MARGIN_S;\n if (!this.#keyStore || (!keyStoreHasKey && issuedAfterLastRefresh)) {\n const endpoint = await this.getEndpoint();\n this.#keyStore = createRemoteJWKSet(endpoint);\n this.#keyStoreUpdated = Date.now() / 1000;\n }\n }\n}\n"],"names":["AuthenticationError","decodeJwt","decodeProtectedHeader","createRemoteJWKSet"],"mappings":";;;;;AA0BA,MAAM,cAAA,GAAiB,EAAA;AAEhB,MAAM,UAAA,CAAW;AAAA,EACtB,SAAA;AAAA,EACA,gBAAA,GAA2B,CAAA;AAAA,EAEV,WAAA;AAAA,EAEjB,YAAY,WAAA,EAAiC;AAC3C,IAAA,IAAA,CAAK,WAAA,GAAc,WAAA;AAAA,EACrB;AAAA,EAEA,IAAI,MAAA,GAAS;AACX,IAAA,IAAI,CAAC,KAAK,SAAA,EAAW;AACnB,MAAA,MAAM,IAAIA,0BAAA;AAAA,QACR;AAAA,OACF;AAAA,IACF;AACA,IAAA,OAAO,IAAA,CAAK,SAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,gBAAgB,WAAA,EAAoC;AACxD,IAAA,MAAM,OAAA,GAAU,MAAMC,cAAA,CAAU,WAAW,CAAA;AAC3C,IAAA,MAAM,MAAA,GAAS,MAAMC,0BAAA,CAAsB,WAAW,CAAA;AAGtD,IAAA,IAAI,cAAA;AACJ,IAAA,IAAI;AACF,MAAA,IAAI,KAAK,SAAA,EAAW;AAElB,QAAA,MAAM,CAAC,CAAA,EAAG,UAAA,EAAY,YAAY,CAAA,GAAI,WAAA,CAAY,MAAM,GAAG,CAAA;AAC3D,QAAA,cAAA,GAAiB,MAAM,IAAA,CAAK,SAAA,CAAU,MAAA,EAAQ;AAAA,UAC5C,OAAA,EAAS,UAAA;AAAA,UACT,SAAA,EAAW;AAAA,SACZ,CAAA;AAAA,MACH;AAAA,IACF,SAAS,KAAA,EAAO;AACd,MAAA,cAAA,GAAiB,KAAA;AAAA,IACnB;AAGA,IAAA,MAAM,yBACJ,OAAA,EAAS,GAAA,IAAO,OAAA,CAAQ,GAAA,GAAM,KAAK,gBAAA,GAAmB,cAAA;AACxD,IAAA,IAAI,CAAC,IAAA,CAAK,SAAA,IAAc,CAAC,kBAAkB,sBAAA,EAAyB;AAClE,MAAA,MAAM,QAAA,GAAW,MAAM,IAAA,CAAK,WAAA,EAAY;AACxC,MAAA,IAAA,CAAK,SAAA,GAAYC,wBAAmB,QAAQ,CAAA;AAC5C,MAAA,IAAA,CAAK,gBAAA,GAAmB,IAAA,CAAK,GAAA,EAAI,GAAI,GAAA;AAAA,IACvC;AAAA,EACF;AACF;;;;"}
1
+ {"version":3,"file":"JwksClient.cjs.js","sources":["../../../src/entrypoints/auth/JwksClient.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n createRemoteJWKSet,\n errors,\n FlattenedJWSInput,\n GetKeyFunction,\n JWSHeaderParameters,\n} from 'jose';\n\nconst FORCED_RELOAD_LIMIT = 10;\nconst FORCED_RELOAD_WINDOW_MS = 60_000;\nconst KEY_STORE_CACHE_LIMIT = 100;\n\ntype RemoteJWKSet = ReturnType<typeof createRemoteJWKSet>;\n\nclass JwksKeyStore {\n readonly #resolver: RemoteJWKSet;\n readonly #tryUseForcedReload: () => boolean;\n\n constructor(endpoint: URL, tryUseForcedReload: () => boolean) {\n this.#resolver = createRemoteJWKSet(endpoint);\n this.#tryUseForcedReload = tryUseForcedReload;\n }\n\n getKey: GetKeyFunction<JWSHeaderParameters, FlattenedJWSInput> = async (\n protectedHeader,\n token,\n ) => {\n const coolingDown = this.#resolver.coolingDown;\n\n try {\n return await this.#resolver(protectedHeader, token);\n } catch (error) {\n if (!(error instanceof errors.JWKSNoMatchingKey)) {\n throw error;\n }\n\n if (!coolingDown || !(await this.#reload())) {\n throw error;\n }\n return this.#resolver(protectedHeader, token);\n }\n };\n\n async #reload(): Promise<boolean> {\n if (!this.#resolver.reloading && !this.#tryUseForcedReload()) {\n return false;\n }\n await this.#resolver.reload();\n return true;\n }\n}\n\nexport class JwksClient {\n #keyStores = new Map<string, JwksKeyStore>();\n #forcedReloads: number[] = [];\n\n private readonly getEndpoint: () => Promise<URL>;\n\n constructor(getEndpoint: () => Promise<URL>) {\n this.getEndpoint = getEndpoint;\n }\n\n get getKey() {\n return this.#getKey;\n }\n\n #getKey: GetKeyFunction<JWSHeaderParameters, FlattenedJWSInput> = async (\n protectedHeader,\n token,\n ) => {\n const keyStore = await this.#getKeyStore();\n return keyStore.getKey(protectedHeader, token);\n };\n\n async #getKeyStore(): Promise<JwksKeyStore> {\n const endpoint = await this.getEndpoint();\n let keyStore = this.#keyStores.get(endpoint.href);\n if (keyStore) {\n // Reinsert entries to keep the least recently used endpoint first.\n this.#keyStores.delete(endpoint.href);\n } else {\n keyStore = new JwksKeyStore(endpoint, () => this.#tryUseForcedReload());\n }\n this.#keyStores.set(endpoint.href, keyStore);\n if (this.#keyStores.size > KEY_STORE_CACHE_LIMIT) {\n this.#keyStores.delete(this.#keyStores.keys().next().value!);\n }\n return keyStore;\n }\n\n #tryUseForcedReload(): boolean {\n const windowStart = Date.now() - FORCED_RELOAD_WINDOW_MS;\n this.#forcedReloads = this.#forcedReloads.filter(\n timestamp => timestamp > windowStart,\n );\n if (this.#forcedReloads.length >= FORCED_RELOAD_LIMIT) {\n return false;\n }\n this.#forcedReloads.push(Date.now());\n return true;\n }\n}\n"],"names":["createRemoteJWKSet","errors"],"mappings":";;;;AAwBA,MAAM,mBAAA,GAAsB,EAAA;AAC5B,MAAM,uBAAA,GAA0B,GAAA;AAChC,MAAM,qBAAA,GAAwB,GAAA;AAI9B,MAAM,YAAA,CAAa;AAAA,EACR,SAAA;AAAA,EACA,mBAAA;AAAA,EAET,WAAA,CAAY,UAAe,kBAAA,EAAmC;AAC5D,IAAA,IAAA,CAAK,SAAA,GAAYA,wBAAmB,QAAQ,CAAA;AAC5C,IAAA,IAAA,CAAK,mBAAA,GAAsB,kBAAA;AAAA,EAC7B;AAAA,EAEA,MAAA,GAAiE,OAC/D,eAAA,EACA,KAAA,KACG;AACH,IAAA,MAAM,WAAA,GAAc,KAAK,SAAA,CAAU,WAAA;AAEnC,IAAA,IAAI;AACF,MAAA,OAAO,MAAM,IAAA,CAAK,SAAA,CAAU,eAAA,EAAiB,KAAK,CAAA;AAAA,IACpD,SAAS,KAAA,EAAO;AACd,MAAA,IAAI,EAAE,KAAA,YAAiBC,WAAA,CAAO,iBAAA,CAAA,EAAoB;AAChD,QAAA,MAAM,KAAA;AAAA,MACR;AAEA,MAAA,IAAI,CAAC,WAAA,IAAe,CAAE,MAAM,IAAA,CAAK,SAAQ,EAAI;AAC3C,QAAA,MAAM,KAAA;AAAA,MACR;AACA,MAAA,OAAO,IAAA,CAAK,SAAA,CAAU,eAAA,EAAiB,KAAK,CAAA;AAAA,IAC9C;AAAA,EACF,CAAA;AAAA,EAEA,MAAM,OAAA,GAA4B;AAChC,IAAA,IAAI,CAAC,IAAA,CAAK,SAAA,CAAU,aAAa,CAAC,IAAA,CAAK,qBAAoB,EAAG;AAC5D,MAAA,OAAO,KAAA;AAAA,IACT;AACA,IAAA,MAAM,IAAA,CAAK,UAAU,MAAA,EAAO;AAC5B,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAEO,MAAM,UAAA,CAAW;AAAA,EACtB,UAAA,uBAAiB,GAAA,EAA0B;AAAA,EAC3C,iBAA2B,EAAC;AAAA,EAEX,WAAA;AAAA,EAEjB,YAAY,WAAA,EAAiC;AAC3C,IAAA,IAAA,CAAK,WAAA,GAAc,WAAA;AAAA,EACrB;AAAA,EAEA,IAAI,MAAA,GAAS;AACX,IAAA,OAAO,IAAA,CAAK,OAAA;AAAA,EACd;AAAA,EAEA,OAAA,GAAkE,OAChE,eAAA,EACA,KAAA,KACG;AACH,IAAA,MAAM,QAAA,GAAW,MAAM,IAAA,CAAK,YAAA,EAAa;AACzC,IAAA,OAAO,QAAA,CAAS,MAAA,CAAO,eAAA,EAAiB,KAAK,CAAA;AAAA,EAC/C,CAAA;AAAA,EAEA,MAAM,YAAA,GAAsC;AAC1C,IAAA,MAAM,QAAA,GAAW,MAAM,IAAA,CAAK,WAAA,EAAY;AACxC,IAAA,IAAI,QAAA,GAAW,IAAA,CAAK,UAAA,CAAW,GAAA,CAAI,SAAS,IAAI,CAAA;AAChD,IAAA,IAAI,QAAA,EAAU;AAEZ,MAAA,IAAA,CAAK,UAAA,CAAW,MAAA,CAAO,QAAA,CAAS,IAAI,CAAA;AAAA,IACtC,CAAA,MAAO;AACL,MAAA,QAAA,GAAW,IAAI,YAAA,CAAa,QAAA,EAAU,MAAM,IAAA,CAAK,qBAAqB,CAAA;AAAA,IACxE;AACA,IAAA,IAAA,CAAK,UAAA,CAAW,GAAA,CAAI,QAAA,CAAS,IAAA,EAAM,QAAQ,CAAA;AAC3C,IAAA,IAAI,IAAA,CAAK,UAAA,CAAW,IAAA,GAAO,qBAAA,EAAuB;AAChD,MAAA,IAAA,CAAK,UAAA,CAAW,OAAO,IAAA,CAAK,UAAA,CAAW,MAAK,CAAE,IAAA,GAAO,KAAM,CAAA;AAAA,IAC7D;AACA,IAAA,OAAO,QAAA;AAAA,EACT;AAAA,EAEA,mBAAA,GAA+B;AAC7B,IAAA,MAAM,WAAA,GAAc,IAAA,CAAK,GAAA,EAAI,GAAI,uBAAA;AACjC,IAAA,IAAA,CAAK,cAAA,GAAiB,KAAK,cAAA,CAAe,MAAA;AAAA,MACxC,eAAa,SAAA,GAAY;AAAA,KAC3B;AACA,IAAA,IAAI,IAAA,CAAK,cAAA,CAAe,MAAA,IAAU,mBAAA,EAAqB;AACrD,MAAA,OAAO,KAAA;AAAA,IACT;AACA,IAAA,IAAA,CAAK,cAAA,CAAe,IAAA,CAAK,IAAA,CAAK,GAAA,EAAK,CAAA;AACnC,IAAA,OAAO,IAAA;AAAA,EACT;AACF;;;;"}
@@ -100,7 +100,8 @@ class ExternalAuthTokenHandler {
100
100
  }
101
101
  return {
102
102
  ...result,
103
- accessRestrictions
103
+ accessRestrictions,
104
+ allAccessRestrictions
104
105
  };
105
106
  }
106
107
  return result;