@backstage/plugin-permission-common 0.3.1 → 0.5.1

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,54 @@
1
1
  # @backstage/plugin-permission-common
2
2
 
3
+ ## 0.5.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Fix for the previous release with missing type declarations.
8
+ - Updated dependencies
9
+ - @backstage/config@0.1.15
10
+ - @backstage/errors@0.2.2
11
+
12
+ ## 0.5.0
13
+
14
+ ### Minor Changes
15
+
16
+ - 8c646beb24: **BREAKING** `PermissionCriteria` now requires at least one condition in `anyOf` and `allOf` arrays. This addresses some ambiguous behavior outlined in #9280.
17
+
18
+ ### Patch Changes
19
+
20
+ - 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5
21
+ - c77c5c7eb6: Added `backstage.role` to `package.json`
22
+ - Updated dependencies
23
+ - @backstage/errors@0.2.1
24
+ - @backstage/config@0.1.14
25
+
26
+ ## 0.4.0
27
+
28
+ ### Minor Changes
29
+
30
+ - b768259244: **BREAKING**: Authorize API request and response types have been updated. The existing `AuthorizeRequest` and `AuthorizeResponse` types now match the entire request and response objects for the /authorize endpoint, and new types `AuthorizeQuery` and `AuthorizeDecision` have been introduced for individual items in the request and response batches respectively.
31
+
32
+ **BREAKING**: PermissionClient has been updated to use the new request and response format in the latest version of @backstage/permission-backend.
33
+
34
+ ### Patch Changes
35
+
36
+ - Updated dependencies
37
+ - @backstage/config@0.1.13
38
+
39
+ ## 0.4.0-next.0
40
+
41
+ ### Minor Changes
42
+
43
+ - b768259244: **BREAKING**: Authorize API request and response types have been updated. The existing `AuthorizeRequest` and `AuthorizeResponse` types now match the entire request and response objects for the /authorize endpoint, and new types `AuthorizeQuery` and `AuthorizeDecision` have been introduced for individual items in the request and response batches respectively.
44
+
45
+ **BREAKING**: PermissionClient has been updated to use the new request and response format in the latest version of @backstage/permission-backend.
46
+
47
+ ### Patch Changes
48
+
49
+ - Updated dependencies
50
+ - @backstage/config@0.1.13-next.0
51
+
3
52
  ## 0.3.1
4
53
 
5
54
  ### Patch Changes
package/dist/index.cjs.js CHANGED
@@ -53,33 +53,37 @@ function isDeletePermission(permission) {
53
53
  const permissionCriteriaSchema = zod.z.lazy(() => zod.z.object({
54
54
  rule: zod.z.string(),
55
55
  params: zod.z.array(zod.z.unknown())
56
- }).or(zod.z.object({ anyOf: zod.z.array(permissionCriteriaSchema) })).or(zod.z.object({ allOf: zod.z.array(permissionCriteriaSchema) })).or(zod.z.object({ not: permissionCriteriaSchema })));
57
- const responseSchema = zod.z.array(zod.z.object({
58
- id: zod.z.string(),
59
- result: zod.z.literal(AuthorizeResult.ALLOW).or(zod.z.literal(AuthorizeResult.DENY))
60
- }).or(zod.z.object({
61
- id: zod.z.string(),
62
- result: zod.z.literal(AuthorizeResult.CONDITIONAL),
63
- conditions: permissionCriteriaSchema
64
- })));
56
+ }).strict().or(zod.z.object({ anyOf: zod.z.array(permissionCriteriaSchema).nonempty() }).strict()).or(zod.z.object({ allOf: zod.z.array(permissionCriteriaSchema).nonempty() }).strict()).or(zod.z.object({ not: permissionCriteriaSchema }).strict()));
57
+ const responseSchema = zod.z.object({
58
+ items: zod.z.array(zod.z.object({
59
+ id: zod.z.string(),
60
+ result: zod.z.literal(AuthorizeResult.ALLOW).or(zod.z.literal(AuthorizeResult.DENY))
61
+ }).or(zod.z.object({
62
+ id: zod.z.string(),
63
+ result: zod.z.literal(AuthorizeResult.CONDITIONAL),
64
+ conditions: permissionCriteriaSchema
65
+ })))
66
+ });
65
67
  class PermissionClient {
66
68
  constructor(options) {
67
69
  var _a;
68
70
  this.discovery = options.discovery;
69
71
  this.enabled = (_a = options.config.getOptionalBoolean("permission.enabled")) != null ? _a : false;
70
72
  }
71
- async authorize(requests, options) {
73
+ async authorize(queries, options) {
72
74
  if (!this.enabled) {
73
- return requests.map((_) => ({ result: AuthorizeResult.ALLOW }));
75
+ return queries.map((_) => ({ result: AuthorizeResult.ALLOW }));
74
76
  }
75
- const identifiedRequests = requests.map((request) => ({
76
- id: uuid__namespace.v4(),
77
- ...request
78
- }));
77
+ const request = {
78
+ items: queries.map((query) => ({
79
+ id: uuid__namespace.v4(),
80
+ ...query
81
+ }))
82
+ };
79
83
  const permissionApi = await this.discovery.getBaseUrl("permission");
80
84
  const response = await fetch__default["default"](`${permissionApi}/authorize`, {
81
85
  method: "POST",
82
- body: JSON.stringify(identifiedRequests),
86
+ body: JSON.stringify(request),
83
87
  headers: {
84
88
  ...this.getAuthorizationHeader(options == null ? void 0 : options.token),
85
89
  "content-type": "application/json"
@@ -88,21 +92,21 @@ class PermissionClient {
88
92
  if (!response.ok) {
89
93
  throw await errors.ResponseError.fromResponse(response);
90
94
  }
91
- const identifiedResponses = await response.json();
92
- this.assertValidResponses(identifiedRequests, identifiedResponses);
93
- const responsesById = identifiedResponses.reduce((acc, r) => {
95
+ const responseBody = await response.json();
96
+ this.assertValidResponse(request, responseBody);
97
+ const responsesById = responseBody.items.reduce((acc, r) => {
94
98
  acc[r.id] = r;
95
99
  return acc;
96
100
  }, {});
97
- return identifiedRequests.map((request) => responsesById[request.id]);
101
+ return request.items.map((query) => responsesById[query.id]);
98
102
  }
99
103
  getAuthorizationHeader(token) {
100
104
  return token ? { Authorization: `Bearer ${token}` } : {};
101
105
  }
102
- assertValidResponses(requests, json) {
106
+ assertValidResponse(request, json) {
103
107
  const authorizedResponses = responseSchema.parse(json);
104
- const responseIds = authorizedResponses.map((r) => r.id);
105
- const hasAllRequestIds = requests.every((r) => responseIds.includes(r.id));
108
+ const responseIds = authorizedResponses.items.map((r) => r.id);
109
+ const hasAllRequestIds = request.items.every((r) => responseIds.includes(r.id));
106
110
  if (!hasAllRequestIds) {
107
111
  throw new Error("Unexpected authorization response from permission-backend");
108
112
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs.js","sources":["../src/types/api.ts","../src/permissions/util.ts","../src/PermissionClient.ts"],"sourcesContent":["/*\n * Copyright 2021 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 { Permission } from './permission';\n\n/**\n * A request with a UUID identifier, so that batched responses can be matched up with the original\n * requests.\n * @public\n */\nexport type Identified<T> = T & { id: string };\n\n/**\n * The result of an authorization request.\n * @public\n */\nexport enum AuthorizeResult {\n /**\n * The authorization request is denied.\n */\n DENY = 'DENY',\n /**\n * The authorization request is allowed.\n */\n ALLOW = 'ALLOW',\n /**\n * The authorization request is allowed if the provided conditions are met.\n */\n CONDITIONAL = 'CONDITIONAL',\n}\n\n/**\n * An authorization request for {@link PermissionClient#authorize}.\n * @public\n */\nexport type AuthorizeRequest = {\n permission: Permission;\n resourceRef?: string;\n};\n\n/**\n * A condition returned with a CONDITIONAL authorization response.\n *\n * Conditions are a reference to a rule defined by a plugin, and parameters to apply the rule. For\n * example, a rule might be `isOwner` from the catalog-backend, and params may be a list of entity\n * claims from a identity token.\n * @public\n */\nexport type PermissionCondition<TParams extends unknown[] = unknown[]> = {\n rule: string;\n params: TParams;\n};\n\n/**\n * Composes several {@link PermissionCondition}s as criteria with a nested AND/OR structure.\n * @public\n */\nexport type PermissionCriteria<TQuery> =\n | { allOf: PermissionCriteria<TQuery>[] }\n | { anyOf: PermissionCriteria<TQuery>[] }\n | { not: PermissionCriteria<TQuery> }\n | TQuery;\n\n/**\n * An authorization response from {@link PermissionClient#authorize}.\n * @public\n */\nexport type AuthorizeResponse =\n | { result: AuthorizeResult.ALLOW | AuthorizeResult.DENY }\n | {\n result: AuthorizeResult.CONDITIONAL;\n conditions: PermissionCriteria<PermissionCondition>;\n };\n","/*\n * Copyright 2021 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 { Permission } from '../types';\n\n/**\n * Check if a given permission is related to a create action.\n * @public\n */\nexport function isCreatePermission(permission: Permission) {\n return permission.attributes.action === 'create';\n}\n\n/**\n * Check if a given permission is related to a read action.\n * @public\n */\nexport function isReadPermission(permission: Permission) {\n return permission.attributes.action === 'read';\n}\n\n/**\n * Check if a given permission is related to an update action.\n * @public\n */\nexport function isUpdatePermission(permission: Permission) {\n return permission.attributes.action === 'update';\n}\n\n/**\n * Check if a given permission is related to a delete action.\n * @public\n */\nexport function isDeletePermission(permission: Permission) {\n return permission.attributes.action === 'delete';\n}\n","/*\n * Copyright 2021 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 { Config } from '@backstage/config';\nimport { ResponseError } from '@backstage/errors';\nimport fetch from 'cross-fetch';\nimport * as uuid from 'uuid';\nimport { z } from 'zod';\nimport {\n AuthorizeResult,\n AuthorizeRequest,\n AuthorizeResponse,\n Identified,\n PermissionCriteria,\n PermissionCondition,\n} from './types/api';\nimport { DiscoveryApi } from './types/discovery';\nimport {\n PermissionAuthorizer,\n AuthorizeRequestOptions,\n} from './types/permission';\n\nconst permissionCriteriaSchema: z.ZodSchema<\n PermissionCriteria<PermissionCondition>\n> = z.lazy(() =>\n z\n .object({\n rule: z.string(),\n params: z.array(z.unknown()),\n })\n .or(z.object({ anyOf: z.array(permissionCriteriaSchema) }))\n .or(z.object({ allOf: z.array(permissionCriteriaSchema) }))\n .or(z.object({ not: permissionCriteriaSchema })),\n);\n\nconst responseSchema = z.array(\n z\n .object({\n id: z.string(),\n result: z\n .literal(AuthorizeResult.ALLOW)\n .or(z.literal(AuthorizeResult.DENY)),\n })\n .or(\n z.object({\n id: z.string(),\n result: z.literal(AuthorizeResult.CONDITIONAL),\n conditions: permissionCriteriaSchema,\n }),\n ),\n);\n\n/**\n * An isomorphic client for requesting authorization for Backstage permissions.\n * @public\n */\nexport class PermissionClient implements PermissionAuthorizer {\n private readonly enabled: boolean;\n private readonly discovery: DiscoveryApi;\n\n constructor(options: { discovery: DiscoveryApi; config: Config }) {\n this.discovery = options.discovery;\n this.enabled =\n options.config.getOptionalBoolean('permission.enabled') ?? false;\n }\n\n /**\n * Request authorization from the permission-backend for the given set of permissions.\n *\n * Authorization requests check that a given Backstage user can perform a protected operation,\n * potentially for a specific resource (such as a catalog entity). The Backstage identity token\n * should be included in the `options` if available.\n *\n * Permissions can be imported from plugins exposing them, such as `catalogEntityReadPermission`.\n *\n * The response will be either ALLOW or DENY when either the permission has no resourceType, or a\n * resourceRef is provided in the request. For permissions with a resourceType, CONDITIONAL may be\n * returned if no resourceRef is provided in the request. Conditional responses are intended only\n * for backends which have access to the data source for permissioned resources, so that filters\n * can be applied when loading collections of resources.\n * @public\n */\n async authorize(\n requests: AuthorizeRequest[],\n options?: AuthorizeRequestOptions,\n ): Promise<AuthorizeResponse[]> {\n // TODO(permissions): it would be great to provide some kind of typing guarantee that\n // conditional responses will only ever be returned for requests containing a resourceType\n // but no resourceRef. That way clients who aren't prepared to handle filtering according\n // to conditions can be guaranteed that they won't unexpectedly get a CONDITIONAL response.\n\n if (!this.enabled) {\n return requests.map(_ => ({ result: AuthorizeResult.ALLOW }));\n }\n\n const identifiedRequests: Identified<AuthorizeRequest>[] = requests.map(\n request => ({\n id: uuid.v4(),\n ...request,\n }),\n );\n\n const permissionApi = await this.discovery.getBaseUrl('permission');\n const response = await fetch(`${permissionApi}/authorize`, {\n method: 'POST',\n body: JSON.stringify(identifiedRequests),\n headers: {\n ...this.getAuthorizationHeader(options?.token),\n 'content-type': 'application/json',\n },\n });\n if (!response.ok) {\n throw await ResponseError.fromResponse(response);\n }\n\n const identifiedResponses = await response.json();\n this.assertValidResponses(identifiedRequests, identifiedResponses);\n\n const responsesById = identifiedResponses.reduce((acc, r) => {\n acc[r.id] = r;\n return acc;\n }, {} as Record<string, Identified<AuthorizeResponse>>);\n\n return identifiedRequests.map(request => responsesById[request.id]);\n }\n\n private getAuthorizationHeader(token?: string): Record<string, string> {\n return token ? { Authorization: `Bearer ${token}` } : {};\n }\n\n private assertValidResponses(\n requests: Identified<AuthorizeRequest>[],\n json: any,\n ): asserts json is Identified<AuthorizeResponse>[] {\n const authorizedResponses = responseSchema.parse(json);\n const responseIds = authorizedResponses.map(r => r.id);\n const hasAllRequestIds = requests.every(r => responseIds.includes(r.id));\n if (!hasAllRequestIds) {\n throw new Error(\n 'Unexpected authorization response from permission-backend',\n );\n }\n }\n}\n"],"names":["z","uuid","fetch","ResponseError"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA6BY,oCAAA,qBAAL;AAIL,6BAAO;AAIP,8BAAQ;AAIR,oCAAc;AAZJ;AAAA;;4BCPuB,YAAwB;AACzD,SAAO,WAAW,WAAW,WAAW;AAAA;0BAOT,YAAwB;AACvD,SAAO,WAAW,WAAW,WAAW;AAAA;4BAOP,YAAwB;AACzD,SAAO,WAAW,WAAW,WAAW;AAAA;4BAOP,YAAwB;AACzD,SAAO,WAAW,WAAW,WAAW;AAAA;;ACZ1C,MAAM,2BAEFA,MAAE,KAAK,MACTA,MACG,OAAO;AAAA,EACN,MAAMA,MAAE;AAAA,EACR,QAAQA,MAAE,MAAMA,MAAE;AAAA,GAEnB,GAAGA,MAAE,OAAO,EAAE,OAAOA,MAAE,MAAM,8BAC7B,GAAGA,MAAE,OAAO,EAAE,OAAOA,MAAE,MAAM,8BAC7B,GAAGA,MAAE,OAAO,EAAE,KAAK;AAGxB,MAAM,iBAAiBA,MAAE,MACvBA,MACG,OAAO;AAAA,EACN,IAAIA,MAAE;AAAA,EACN,QAAQA,MACL,QAAQ,gBAAgB,OACxB,GAAGA,MAAE,QAAQ,gBAAgB;AAAA,GAEjC,GACCA,MAAE,OAAO;AAAA,EACP,IAAIA,MAAE;AAAA,EACN,QAAQA,MAAE,QAAQ,gBAAgB;AAAA,EAClC,YAAY;AAAA;uBAS0C;AAAA,EAI5D,YAAY,SAAsD;AAzEpE;AA0EI,SAAK,YAAY,QAAQ;AACzB,SAAK,UACH,cAAQ,OAAO,mBAAmB,0BAAlC,YAA2D;AAAA;AAAA,QAmBzD,UACJ,UACA,SAC8B;AAM9B,QAAI,CAAC,KAAK,SAAS;AACjB,aAAO,SAAS,IAAI,UAAQ,QAAQ,gBAAgB;AAAA;AAGtD,UAAM,qBAAqD,SAAS,IAClE;AAAY,MACV,IAAIC,gBAAK;AAAA,SACN;AAAA;AAIP,UAAM,gBAAgB,MAAM,KAAK,UAAU,WAAW;AACtD,UAAM,WAAW,MAAMC,0BAAM,GAAG,2BAA2B;AAAA,MACzD,QAAQ;AAAA,MACR,MAAM,KAAK,UAAU;AAAA,MACrB,SAAS;AAAA,WACJ,KAAK,uBAAuB,mCAAS;AAAA,QACxC,gBAAgB;AAAA;AAAA;AAGpB,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,MAAMC,qBAAc,aAAa;AAAA;AAGzC,UAAM,sBAAsB,MAAM,SAAS;AAC3C,SAAK,qBAAqB,oBAAoB;AAE9C,UAAM,gBAAgB,oBAAoB,OAAO,CAAC,KAAK,MAAM;AAC3D,UAAI,EAAE,MAAM;AACZ,aAAO;AAAA,OACN;AAEH,WAAO,mBAAmB,IAAI,aAAW,cAAc,QAAQ;AAAA;AAAA,EAGzD,uBAAuB,OAAwC;AACrE,WAAO,QAAQ,EAAE,eAAe,UAAU,YAAY;AAAA;AAAA,EAGhD,qBACN,UACA,MACiD;AACjD,UAAM,sBAAsB,eAAe,MAAM;AACjD,UAAM,cAAc,oBAAoB,IAAI,OAAK,EAAE;AACnD,UAAM,mBAAmB,SAAS,MAAM,OAAK,YAAY,SAAS,EAAE;AACpE,QAAI,CAAC,kBAAkB;AACrB,YAAM,IAAI,MACR;AAAA;AAAA;AAAA;;;;;;;;;"}
1
+ {"version":3,"file":"index.cjs.js","sources":["../src/types/api.ts","../src/permissions/util.ts","../src/PermissionClient.ts"],"sourcesContent":["/*\n * Copyright 2021 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 { Permission } from './permission';\n\n/**\n * A request with a UUID identifier, so that batched responses can be matched up with the original\n * requests.\n * @public\n */\nexport type Identified<T> = T & { id: string };\n\n/**\n * The result of an authorization request.\n * @public\n */\nexport enum AuthorizeResult {\n /**\n * The authorization request is denied.\n */\n DENY = 'DENY',\n /**\n * The authorization request is allowed.\n */\n ALLOW = 'ALLOW',\n /**\n * The authorization request is allowed if the provided conditions are met.\n */\n CONDITIONAL = 'CONDITIONAL',\n}\n\n/**\n * An individual authorization request for {@link PermissionClient#authorize}.\n * @public\n */\nexport type AuthorizeQuery = {\n permission: Permission;\n resourceRef?: string;\n};\n\n/**\n * A batch of authorization requests from {@link PermissionClient#authorize}.\n * @public\n */\nexport type AuthorizeRequest = {\n items: Identified<AuthorizeQuery>[];\n};\n\n/**\n * A condition returned with a CONDITIONAL authorization response.\n *\n * Conditions are a reference to a rule defined by a plugin, and parameters to apply the rule. For\n * example, a rule might be `isOwner` from the catalog-backend, and params may be a list of entity\n * claims from a identity token.\n * @public\n */\nexport type PermissionCondition<TParams extends unknown[] = unknown[]> = {\n rule: string;\n params: TParams;\n};\n\n/**\n * Utility type to represent an array with 1 or more elements.\n * @ignore\n */\ntype NonEmptyArray<T> = [T, ...T[]];\n\n/**\n * Represnts a logical AND for the provided criteria.\n * @public\n */\nexport type AllOfCriteria<TQuery> = {\n allOf: NonEmptyArray<PermissionCriteria<TQuery>>;\n};\n\n/**\n * Represnts a logical OR for the provided criteria.\n * @public\n */\nexport type AnyOfCriteria<TQuery> = {\n anyOf: NonEmptyArray<PermissionCriteria<TQuery>>;\n};\n\n/**\n * Represents a negation of the provided criteria.\n * @public\n */\nexport type NotCriteria<TQuery> = {\n not: PermissionCriteria<TQuery>;\n};\n\n/**\n * Composes several {@link PermissionCondition}s as criteria with a nested AND/OR structure.\n * @public\n */\nexport type PermissionCriteria<TQuery> =\n | AllOfCriteria<TQuery>\n | AnyOfCriteria<TQuery>\n | NotCriteria<TQuery>\n | TQuery;\n\n/**\n * An individual authorization response from {@link PermissionClient#authorize}.\n * @public\n */\nexport type AuthorizeDecision =\n | { result: AuthorizeResult.ALLOW | AuthorizeResult.DENY }\n | {\n result: AuthorizeResult.CONDITIONAL;\n conditions: PermissionCriteria<PermissionCondition>;\n };\n\n/**\n * A batch of authorization responses from {@link PermissionClient#authorize}.\n * @public\n */\nexport type AuthorizeResponse = {\n items: Identified<AuthorizeDecision>[];\n};\n","/*\n * Copyright 2021 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 { Permission } from '../types';\n\n/**\n * Check if a given permission is related to a create action.\n * @public\n */\nexport function isCreatePermission(permission: Permission) {\n return permission.attributes.action === 'create';\n}\n\n/**\n * Check if a given permission is related to a read action.\n * @public\n */\nexport function isReadPermission(permission: Permission) {\n return permission.attributes.action === 'read';\n}\n\n/**\n * Check if a given permission is related to an update action.\n * @public\n */\nexport function isUpdatePermission(permission: Permission) {\n return permission.attributes.action === 'update';\n}\n\n/**\n * Check if a given permission is related to a delete action.\n * @public\n */\nexport function isDeletePermission(permission: Permission) {\n return permission.attributes.action === 'delete';\n}\n","/*\n * Copyright 2021 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 { Config } from '@backstage/config';\nimport { ResponseError } from '@backstage/errors';\nimport fetch from 'cross-fetch';\nimport * as uuid from 'uuid';\nimport { z } from 'zod';\nimport {\n AuthorizeResult,\n AuthorizeQuery,\n AuthorizeDecision,\n Identified,\n PermissionCriteria,\n PermissionCondition,\n AuthorizeResponse,\n AuthorizeRequest,\n} from './types/api';\nimport { DiscoveryApi } from './types/discovery';\nimport {\n PermissionAuthorizer,\n AuthorizeRequestOptions,\n} from './types/permission';\n\nconst permissionCriteriaSchema: z.ZodSchema<\n PermissionCriteria<PermissionCondition>\n> = z.lazy(() =>\n z\n .object({\n rule: z.string(),\n params: z.array(z.unknown()),\n })\n .strict()\n .or(\n z\n .object({ anyOf: z.array(permissionCriteriaSchema).nonempty() })\n .strict(),\n )\n .or(\n z\n .object({ allOf: z.array(permissionCriteriaSchema).nonempty() })\n .strict(),\n )\n .or(z.object({ not: permissionCriteriaSchema }).strict()),\n);\n\nconst responseSchema = z.object({\n items: z.array(\n z\n .object({\n id: z.string(),\n result: z\n .literal(AuthorizeResult.ALLOW)\n .or(z.literal(AuthorizeResult.DENY)),\n })\n .or(\n z.object({\n id: z.string(),\n result: z.literal(AuthorizeResult.CONDITIONAL),\n conditions: permissionCriteriaSchema,\n }),\n ),\n ),\n});\n\n/**\n * An isomorphic client for requesting authorization for Backstage permissions.\n * @public\n */\nexport class PermissionClient implements PermissionAuthorizer {\n private readonly enabled: boolean;\n private readonly discovery: DiscoveryApi;\n\n constructor(options: { discovery: DiscoveryApi; config: Config }) {\n this.discovery = options.discovery;\n this.enabled =\n options.config.getOptionalBoolean('permission.enabled') ?? false;\n }\n\n /**\n * Request authorization from the permission-backend for the given set of permissions.\n *\n * Authorization requests check that a given Backstage user can perform a protected operation,\n * potentially for a specific resource (such as a catalog entity). The Backstage identity token\n * should be included in the `options` if available.\n *\n * Permissions can be imported from plugins exposing them, such as `catalogEntityReadPermission`.\n *\n * The response will be either ALLOW or DENY when either the permission has no resourceType, or a\n * resourceRef is provided in the request. For permissions with a resourceType, CONDITIONAL may be\n * returned if no resourceRef is provided in the request. Conditional responses are intended only\n * for backends which have access to the data source for permissioned resources, so that filters\n * can be applied when loading collections of resources.\n * @public\n */\n async authorize(\n queries: AuthorizeQuery[],\n options?: AuthorizeRequestOptions,\n ): Promise<AuthorizeDecision[]> {\n // TODO(permissions): it would be great to provide some kind of typing guarantee that\n // conditional responses will only ever be returned for requests containing a resourceType\n // but no resourceRef. That way clients who aren't prepared to handle filtering according\n // to conditions can be guaranteed that they won't unexpectedly get a CONDITIONAL response.\n\n if (!this.enabled) {\n return queries.map(_ => ({ result: AuthorizeResult.ALLOW }));\n }\n\n const request: AuthorizeRequest = {\n items: queries.map(query => ({\n id: uuid.v4(),\n ...query,\n })),\n };\n\n const permissionApi = await this.discovery.getBaseUrl('permission');\n const response = await fetch(`${permissionApi}/authorize`, {\n method: 'POST',\n body: JSON.stringify(request),\n headers: {\n ...this.getAuthorizationHeader(options?.token),\n 'content-type': 'application/json',\n },\n });\n if (!response.ok) {\n throw await ResponseError.fromResponse(response);\n }\n\n const responseBody = await response.json();\n this.assertValidResponse(request, responseBody);\n\n const responsesById = responseBody.items.reduce((acc, r) => {\n acc[r.id] = r;\n return acc;\n }, {} as Record<string, Identified<AuthorizeDecision>>);\n\n return request.items.map(query => responsesById[query.id]);\n }\n\n private getAuthorizationHeader(token?: string): Record<string, string> {\n return token ? { Authorization: `Bearer ${token}` } : {};\n }\n\n private assertValidResponse(\n request: AuthorizeRequest,\n json: any,\n ): asserts json is AuthorizeResponse {\n const authorizedResponses = responseSchema.parse(json);\n const responseIds = authorizedResponses.items.map(r => r.id);\n const hasAllRequestIds = request.items.every(r =>\n responseIds.includes(r.id),\n );\n if (!hasAllRequestIds) {\n throw new Error(\n 'Unexpected authorization response from permission-backend',\n );\n }\n }\n}\n"],"names":["z","uuid","fetch","ResponseError"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA6BY,oCAAA,qBAAL;AAIL,6BAAO;AAIP,8BAAQ;AAIR,oCAAc;AAZJ;AAAA;;4BCPuB,YAAwB;AACzD,SAAO,WAAW,WAAW,WAAW;AAAA;0BAOT,YAAwB;AACvD,SAAO,WAAW,WAAW,WAAW;AAAA;4BAOP,YAAwB;AACzD,SAAO,WAAW,WAAW,WAAW;AAAA;4BAOP,YAAwB;AACzD,SAAO,WAAW,WAAW,WAAW;AAAA;;ACV1C,MAAM,2BAEFA,MAAE,KAAK,MACTA,MACG,OAAO;AAAA,EACN,MAAMA,MAAE;AAAA,EACR,QAAQA,MAAE,MAAMA,MAAE;AAAA,GAEnB,SACA,GACCA,MACG,OAAO,EAAE,OAAOA,MAAE,MAAM,0BAA0B,cAClD,UAEJ,GACCA,MACG,OAAO,EAAE,OAAOA,MAAE,MAAM,0BAA0B,cAClD,UAEJ,GAAGA,MAAE,OAAO,EAAE,KAAK,4BAA4B;AAGpD,MAAM,iBAAiBA,MAAE,OAAO;AAAA,EAC9B,OAAOA,MAAE,MACPA,MACG,OAAO;AAAA,IACN,IAAIA,MAAE;AAAA,IACN,QAAQA,MACL,QAAQ,gBAAgB,OACxB,GAAGA,MAAE,QAAQ,gBAAgB;AAAA,KAEjC,GACCA,MAAE,OAAO;AAAA,IACP,IAAIA,MAAE;AAAA,IACN,QAAQA,MAAE,QAAQ,gBAAgB;AAAA,IAClC,YAAY;AAAA;AAAA;uBAUwC;AAAA,EAI5D,YAAY,SAAsD;AAtFpE;AAuFI,SAAK,YAAY,QAAQ;AACzB,SAAK,UACH,cAAQ,OAAO,mBAAmB,0BAAlC,YAA2D;AAAA;AAAA,QAmBzD,UACJ,SACA,SAC8B;AAM9B,QAAI,CAAC,KAAK,SAAS;AACjB,aAAO,QAAQ,IAAI,UAAQ,QAAQ,gBAAgB;AAAA;AAGrD,UAAM,UAA4B;AAAA,MAChC,OAAO,QAAQ,IAAI;AAAU,QAC3B,IAAIC,gBAAK;AAAA,WACN;AAAA;AAAA;AAIP,UAAM,gBAAgB,MAAM,KAAK,UAAU,WAAW;AACtD,UAAM,WAAW,MAAMC,0BAAM,GAAG,2BAA2B;AAAA,MACzD,QAAQ;AAAA,MACR,MAAM,KAAK,UAAU;AAAA,MACrB,SAAS;AAAA,WACJ,KAAK,uBAAuB,mCAAS;AAAA,QACxC,gBAAgB;AAAA;AAAA;AAGpB,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,MAAMC,qBAAc,aAAa;AAAA;AAGzC,UAAM,eAAe,MAAM,SAAS;AACpC,SAAK,oBAAoB,SAAS;AAElC,UAAM,gBAAgB,aAAa,MAAM,OAAO,CAAC,KAAK,MAAM;AAC1D,UAAI,EAAE,MAAM;AACZ,aAAO;AAAA,OACN;AAEH,WAAO,QAAQ,MAAM,IAAI,WAAS,cAAc,MAAM;AAAA;AAAA,EAGhD,uBAAuB,OAAwC;AACrE,WAAO,QAAQ,EAAE,eAAe,UAAU,YAAY;AAAA;AAAA,EAGhD,oBACN,SACA,MACmC;AACnC,UAAM,sBAAsB,eAAe,MAAM;AACjD,UAAM,cAAc,oBAAoB,MAAM,IAAI,OAAK,EAAE;AACzD,UAAM,mBAAmB,QAAQ,MAAM,MAAM,OAC3C,YAAY,SAAS,EAAE;AAEzB,QAAI,CAAC,kBAAkB;AACrB,YAAM,IAAI,MACR;AAAA;AAAA;AAAA;;;;;;;;;"}
package/dist/index.d.ts CHANGED
@@ -29,7 +29,7 @@ declare type Permission = {
29
29
  * @public
30
30
  */
31
31
  interface PermissionAuthorizer {
32
- authorize(requests: AuthorizeRequest[], options?: AuthorizeRequestOptions): Promise<AuthorizeResponse[]>;
32
+ authorize(queries: AuthorizeQuery[], options?: AuthorizeRequestOptions): Promise<AuthorizeDecision[]>;
33
33
  }
34
34
  /**
35
35
  * Options for authorization requests.
@@ -66,13 +66,20 @@ declare enum AuthorizeResult {
66
66
  CONDITIONAL = "CONDITIONAL"
67
67
  }
68
68
  /**
69
- * An authorization request for {@link PermissionClient#authorize}.
69
+ * An individual authorization request for {@link PermissionClient#authorize}.
70
70
  * @public
71
71
  */
72
- declare type AuthorizeRequest = {
72
+ declare type AuthorizeQuery = {
73
73
  permission: Permission;
74
74
  resourceRef?: string;
75
75
  };
76
+ /**
77
+ * A batch of authorization requests from {@link PermissionClient#authorize}.
78
+ * @public
79
+ */
80
+ declare type AuthorizeRequest = {
81
+ items: Identified<AuthorizeQuery>[];
82
+ };
76
83
  /**
77
84
  * A condition returned with a CONDITIONAL authorization response.
78
85
  *
@@ -86,26 +93,53 @@ declare type PermissionCondition<TParams extends unknown[] = unknown[]> = {
86
93
  params: TParams;
87
94
  };
88
95
  /**
89
- * Composes several {@link PermissionCondition}s as criteria with a nested AND/OR structure.
96
+ * Utility type to represent an array with 1 or more elements.
97
+ * @ignore
98
+ */
99
+ declare type NonEmptyArray<T> = [T, ...T[]];
100
+ /**
101
+ * Represnts a logical AND for the provided criteria.
90
102
  * @public
91
103
  */
92
- declare type PermissionCriteria<TQuery> = {
93
- allOf: PermissionCriteria<TQuery>[];
94
- } | {
95
- anyOf: PermissionCriteria<TQuery>[];
96
- } | {
104
+ declare type AllOfCriteria<TQuery> = {
105
+ allOf: NonEmptyArray<PermissionCriteria<TQuery>>;
106
+ };
107
+ /**
108
+ * Represnts a logical OR for the provided criteria.
109
+ * @public
110
+ */
111
+ declare type AnyOfCriteria<TQuery> = {
112
+ anyOf: NonEmptyArray<PermissionCriteria<TQuery>>;
113
+ };
114
+ /**
115
+ * Represents a negation of the provided criteria.
116
+ * @public
117
+ */
118
+ declare type NotCriteria<TQuery> = {
97
119
  not: PermissionCriteria<TQuery>;
98
- } | TQuery;
120
+ };
99
121
  /**
100
- * An authorization response from {@link PermissionClient#authorize}.
122
+ * Composes several {@link PermissionCondition}s as criteria with a nested AND/OR structure.
101
123
  * @public
102
124
  */
103
- declare type AuthorizeResponse = {
125
+ declare type PermissionCriteria<TQuery> = AllOfCriteria<TQuery> | AnyOfCriteria<TQuery> | NotCriteria<TQuery> | TQuery;
126
+ /**
127
+ * An individual authorization response from {@link PermissionClient#authorize}.
128
+ * @public
129
+ */
130
+ declare type AuthorizeDecision = {
104
131
  result: AuthorizeResult.ALLOW | AuthorizeResult.DENY;
105
132
  } | {
106
133
  result: AuthorizeResult.CONDITIONAL;
107
134
  conditions: PermissionCriteria<PermissionCondition>;
108
135
  };
136
+ /**
137
+ * A batch of authorization responses from {@link PermissionClient#authorize}.
138
+ * @public
139
+ */
140
+ declare type AuthorizeResponse = {
141
+ items: Identified<AuthorizeDecision>[];
142
+ };
109
143
 
110
144
  /**
111
145
  * This is a copy of the core DiscoveryApi, to avoid importing core.
@@ -164,9 +198,9 @@ declare class PermissionClient implements PermissionAuthorizer {
164
198
  * can be applied when loading collections of resources.
165
199
  * @public
166
200
  */
167
- authorize(requests: AuthorizeRequest[], options?: AuthorizeRequestOptions): Promise<AuthorizeResponse[]>;
201
+ authorize(queries: AuthorizeQuery[], options?: AuthorizeRequestOptions): Promise<AuthorizeDecision[]>;
168
202
  private getAuthorizationHeader;
169
- private assertValidResponses;
203
+ private assertValidResponse;
170
204
  }
171
205
 
172
- export { AuthorizeRequest, AuthorizeRequestOptions, AuthorizeResponse, AuthorizeResult, DiscoveryApi, Identified, Permission, PermissionAttributes, PermissionAuthorizer, PermissionClient, PermissionCondition, PermissionCriteria, isCreatePermission, isDeletePermission, isReadPermission, isUpdatePermission };
206
+ export { AllOfCriteria, AnyOfCriteria, AuthorizeDecision, AuthorizeQuery, AuthorizeRequest, AuthorizeRequestOptions, AuthorizeResponse, AuthorizeResult, DiscoveryApi, Identified, NotCriteria, Permission, PermissionAttributes, PermissionAuthorizer, PermissionClient, PermissionCondition, PermissionCriteria, isCreatePermission, isDeletePermission, isReadPermission, isUpdatePermission };
package/dist/index.esm.js CHANGED
@@ -26,33 +26,37 @@ function isDeletePermission(permission) {
26
26
  const permissionCriteriaSchema = z.lazy(() => z.object({
27
27
  rule: z.string(),
28
28
  params: z.array(z.unknown())
29
- }).or(z.object({ anyOf: z.array(permissionCriteriaSchema) })).or(z.object({ allOf: z.array(permissionCriteriaSchema) })).or(z.object({ not: permissionCriteriaSchema })));
30
- const responseSchema = z.array(z.object({
31
- id: z.string(),
32
- result: z.literal(AuthorizeResult.ALLOW).or(z.literal(AuthorizeResult.DENY))
33
- }).or(z.object({
34
- id: z.string(),
35
- result: z.literal(AuthorizeResult.CONDITIONAL),
36
- conditions: permissionCriteriaSchema
37
- })));
29
+ }).strict().or(z.object({ anyOf: z.array(permissionCriteriaSchema).nonempty() }).strict()).or(z.object({ allOf: z.array(permissionCriteriaSchema).nonempty() }).strict()).or(z.object({ not: permissionCriteriaSchema }).strict()));
30
+ const responseSchema = z.object({
31
+ items: z.array(z.object({
32
+ id: z.string(),
33
+ result: z.literal(AuthorizeResult.ALLOW).or(z.literal(AuthorizeResult.DENY))
34
+ }).or(z.object({
35
+ id: z.string(),
36
+ result: z.literal(AuthorizeResult.CONDITIONAL),
37
+ conditions: permissionCriteriaSchema
38
+ })))
39
+ });
38
40
  class PermissionClient {
39
41
  constructor(options) {
40
42
  var _a;
41
43
  this.discovery = options.discovery;
42
44
  this.enabled = (_a = options.config.getOptionalBoolean("permission.enabled")) != null ? _a : false;
43
45
  }
44
- async authorize(requests, options) {
46
+ async authorize(queries, options) {
45
47
  if (!this.enabled) {
46
- return requests.map((_) => ({ result: AuthorizeResult.ALLOW }));
48
+ return queries.map((_) => ({ result: AuthorizeResult.ALLOW }));
47
49
  }
48
- const identifiedRequests = requests.map((request) => ({
49
- id: uuid.v4(),
50
- ...request
51
- }));
50
+ const request = {
51
+ items: queries.map((query) => ({
52
+ id: uuid.v4(),
53
+ ...query
54
+ }))
55
+ };
52
56
  const permissionApi = await this.discovery.getBaseUrl("permission");
53
57
  const response = await fetch(`${permissionApi}/authorize`, {
54
58
  method: "POST",
55
- body: JSON.stringify(identifiedRequests),
59
+ body: JSON.stringify(request),
56
60
  headers: {
57
61
  ...this.getAuthorizationHeader(options == null ? void 0 : options.token),
58
62
  "content-type": "application/json"
@@ -61,21 +65,21 @@ class PermissionClient {
61
65
  if (!response.ok) {
62
66
  throw await ResponseError.fromResponse(response);
63
67
  }
64
- const identifiedResponses = await response.json();
65
- this.assertValidResponses(identifiedRequests, identifiedResponses);
66
- const responsesById = identifiedResponses.reduce((acc, r) => {
68
+ const responseBody = await response.json();
69
+ this.assertValidResponse(request, responseBody);
70
+ const responsesById = responseBody.items.reduce((acc, r) => {
67
71
  acc[r.id] = r;
68
72
  return acc;
69
73
  }, {});
70
- return identifiedRequests.map((request) => responsesById[request.id]);
74
+ return request.items.map((query) => responsesById[query.id]);
71
75
  }
72
76
  getAuthorizationHeader(token) {
73
77
  return token ? { Authorization: `Bearer ${token}` } : {};
74
78
  }
75
- assertValidResponses(requests, json) {
79
+ assertValidResponse(request, json) {
76
80
  const authorizedResponses = responseSchema.parse(json);
77
- const responseIds = authorizedResponses.map((r) => r.id);
78
- const hasAllRequestIds = requests.every((r) => responseIds.includes(r.id));
81
+ const responseIds = authorizedResponses.items.map((r) => r.id);
82
+ const hasAllRequestIds = request.items.every((r) => responseIds.includes(r.id));
79
83
  if (!hasAllRequestIds) {
80
84
  throw new Error("Unexpected authorization response from permission-backend");
81
85
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.esm.js","sources":["../src/types/api.ts","../src/permissions/util.ts","../src/PermissionClient.ts"],"sourcesContent":["/*\n * Copyright 2021 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 { Permission } from './permission';\n\n/**\n * A request with a UUID identifier, so that batched responses can be matched up with the original\n * requests.\n * @public\n */\nexport type Identified<T> = T & { id: string };\n\n/**\n * The result of an authorization request.\n * @public\n */\nexport enum AuthorizeResult {\n /**\n * The authorization request is denied.\n */\n DENY = 'DENY',\n /**\n * The authorization request is allowed.\n */\n ALLOW = 'ALLOW',\n /**\n * The authorization request is allowed if the provided conditions are met.\n */\n CONDITIONAL = 'CONDITIONAL',\n}\n\n/**\n * An authorization request for {@link PermissionClient#authorize}.\n * @public\n */\nexport type AuthorizeRequest = {\n permission: Permission;\n resourceRef?: string;\n};\n\n/**\n * A condition returned with a CONDITIONAL authorization response.\n *\n * Conditions are a reference to a rule defined by a plugin, and parameters to apply the rule. For\n * example, a rule might be `isOwner` from the catalog-backend, and params may be a list of entity\n * claims from a identity token.\n * @public\n */\nexport type PermissionCondition<TParams extends unknown[] = unknown[]> = {\n rule: string;\n params: TParams;\n};\n\n/**\n * Composes several {@link PermissionCondition}s as criteria with a nested AND/OR structure.\n * @public\n */\nexport type PermissionCriteria<TQuery> =\n | { allOf: PermissionCriteria<TQuery>[] }\n | { anyOf: PermissionCriteria<TQuery>[] }\n | { not: PermissionCriteria<TQuery> }\n | TQuery;\n\n/**\n * An authorization response from {@link PermissionClient#authorize}.\n * @public\n */\nexport type AuthorizeResponse =\n | { result: AuthorizeResult.ALLOW | AuthorizeResult.DENY }\n | {\n result: AuthorizeResult.CONDITIONAL;\n conditions: PermissionCriteria<PermissionCondition>;\n };\n","/*\n * Copyright 2021 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 { Permission } from '../types';\n\n/**\n * Check if a given permission is related to a create action.\n * @public\n */\nexport function isCreatePermission(permission: Permission) {\n return permission.attributes.action === 'create';\n}\n\n/**\n * Check if a given permission is related to a read action.\n * @public\n */\nexport function isReadPermission(permission: Permission) {\n return permission.attributes.action === 'read';\n}\n\n/**\n * Check if a given permission is related to an update action.\n * @public\n */\nexport function isUpdatePermission(permission: Permission) {\n return permission.attributes.action === 'update';\n}\n\n/**\n * Check if a given permission is related to a delete action.\n * @public\n */\nexport function isDeletePermission(permission: Permission) {\n return permission.attributes.action === 'delete';\n}\n","/*\n * Copyright 2021 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 { Config } from '@backstage/config';\nimport { ResponseError } from '@backstage/errors';\nimport fetch from 'cross-fetch';\nimport * as uuid from 'uuid';\nimport { z } from 'zod';\nimport {\n AuthorizeResult,\n AuthorizeRequest,\n AuthorizeResponse,\n Identified,\n PermissionCriteria,\n PermissionCondition,\n} from './types/api';\nimport { DiscoveryApi } from './types/discovery';\nimport {\n PermissionAuthorizer,\n AuthorizeRequestOptions,\n} from './types/permission';\n\nconst permissionCriteriaSchema: z.ZodSchema<\n PermissionCriteria<PermissionCondition>\n> = z.lazy(() =>\n z\n .object({\n rule: z.string(),\n params: z.array(z.unknown()),\n })\n .or(z.object({ anyOf: z.array(permissionCriteriaSchema) }))\n .or(z.object({ allOf: z.array(permissionCriteriaSchema) }))\n .or(z.object({ not: permissionCriteriaSchema })),\n);\n\nconst responseSchema = z.array(\n z\n .object({\n id: z.string(),\n result: z\n .literal(AuthorizeResult.ALLOW)\n .or(z.literal(AuthorizeResult.DENY)),\n })\n .or(\n z.object({\n id: z.string(),\n result: z.literal(AuthorizeResult.CONDITIONAL),\n conditions: permissionCriteriaSchema,\n }),\n ),\n);\n\n/**\n * An isomorphic client for requesting authorization for Backstage permissions.\n * @public\n */\nexport class PermissionClient implements PermissionAuthorizer {\n private readonly enabled: boolean;\n private readonly discovery: DiscoveryApi;\n\n constructor(options: { discovery: DiscoveryApi; config: Config }) {\n this.discovery = options.discovery;\n this.enabled =\n options.config.getOptionalBoolean('permission.enabled') ?? false;\n }\n\n /**\n * Request authorization from the permission-backend for the given set of permissions.\n *\n * Authorization requests check that a given Backstage user can perform a protected operation,\n * potentially for a specific resource (such as a catalog entity). The Backstage identity token\n * should be included in the `options` if available.\n *\n * Permissions can be imported from plugins exposing them, such as `catalogEntityReadPermission`.\n *\n * The response will be either ALLOW or DENY when either the permission has no resourceType, or a\n * resourceRef is provided in the request. For permissions with a resourceType, CONDITIONAL may be\n * returned if no resourceRef is provided in the request. Conditional responses are intended only\n * for backends which have access to the data source for permissioned resources, so that filters\n * can be applied when loading collections of resources.\n * @public\n */\n async authorize(\n requests: AuthorizeRequest[],\n options?: AuthorizeRequestOptions,\n ): Promise<AuthorizeResponse[]> {\n // TODO(permissions): it would be great to provide some kind of typing guarantee that\n // conditional responses will only ever be returned for requests containing a resourceType\n // but no resourceRef. That way clients who aren't prepared to handle filtering according\n // to conditions can be guaranteed that they won't unexpectedly get a CONDITIONAL response.\n\n if (!this.enabled) {\n return requests.map(_ => ({ result: AuthorizeResult.ALLOW }));\n }\n\n const identifiedRequests: Identified<AuthorizeRequest>[] = requests.map(\n request => ({\n id: uuid.v4(),\n ...request,\n }),\n );\n\n const permissionApi = await this.discovery.getBaseUrl('permission');\n const response = await fetch(`${permissionApi}/authorize`, {\n method: 'POST',\n body: JSON.stringify(identifiedRequests),\n headers: {\n ...this.getAuthorizationHeader(options?.token),\n 'content-type': 'application/json',\n },\n });\n if (!response.ok) {\n throw await ResponseError.fromResponse(response);\n }\n\n const identifiedResponses = await response.json();\n this.assertValidResponses(identifiedRequests, identifiedResponses);\n\n const responsesById = identifiedResponses.reduce((acc, r) => {\n acc[r.id] = r;\n return acc;\n }, {} as Record<string, Identified<AuthorizeResponse>>);\n\n return identifiedRequests.map(request => responsesById[request.id]);\n }\n\n private getAuthorizationHeader(token?: string): Record<string, string> {\n return token ? { Authorization: `Bearer ${token}` } : {};\n }\n\n private assertValidResponses(\n requests: Identified<AuthorizeRequest>[],\n json: any,\n ): asserts json is Identified<AuthorizeResponse>[] {\n const authorizedResponses = responseSchema.parse(json);\n const responseIds = authorizedResponses.map(r => r.id);\n const hasAllRequestIds = requests.every(r => responseIds.includes(r.id));\n if (!hasAllRequestIds) {\n throw new Error(\n 'Unexpected authorization response from permission-backend',\n );\n }\n }\n}\n"],"names":[],"mappings":";;;;;IA6BY,oCAAA,qBAAL;AAIL,6BAAO;AAIP,8BAAQ;AAIR,oCAAc;AAZJ;AAAA;;4BCPuB,YAAwB;AACzD,SAAO,WAAW,WAAW,WAAW;AAAA;0BAOT,YAAwB;AACvD,SAAO,WAAW,WAAW,WAAW;AAAA;4BAOP,YAAwB;AACzD,SAAO,WAAW,WAAW,WAAW;AAAA;4BAOP,YAAwB;AACzD,SAAO,WAAW,WAAW,WAAW;AAAA;;ACZ1C,MAAM,2BAEF,EAAE,KAAK,MACT,EACG,OAAO;AAAA,EACN,MAAM,EAAE;AAAA,EACR,QAAQ,EAAE,MAAM,EAAE;AAAA,GAEnB,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,8BAC7B,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,8BAC7B,GAAG,EAAE,OAAO,EAAE,KAAK;AAGxB,MAAM,iBAAiB,EAAE,MACvB,EACG,OAAO;AAAA,EACN,IAAI,EAAE;AAAA,EACN,QAAQ,EACL,QAAQ,gBAAgB,OACxB,GAAG,EAAE,QAAQ,gBAAgB;AAAA,GAEjC,GACC,EAAE,OAAO;AAAA,EACP,IAAI,EAAE;AAAA,EACN,QAAQ,EAAE,QAAQ,gBAAgB;AAAA,EAClC,YAAY;AAAA;uBAS0C;AAAA,EAI5D,YAAY,SAAsD;AAzEpE;AA0EI,SAAK,YAAY,QAAQ;AACzB,SAAK,UACH,cAAQ,OAAO,mBAAmB,0BAAlC,YAA2D;AAAA;AAAA,QAmBzD,UACJ,UACA,SAC8B;AAM9B,QAAI,CAAC,KAAK,SAAS;AACjB,aAAO,SAAS,IAAI,UAAQ,QAAQ,gBAAgB;AAAA;AAGtD,UAAM,qBAAqD,SAAS,IAClE;AAAY,MACV,IAAI,KAAK;AAAA,SACN;AAAA;AAIP,UAAM,gBAAgB,MAAM,KAAK,UAAU,WAAW;AACtD,UAAM,WAAW,MAAM,MAAM,GAAG,2BAA2B;AAAA,MACzD,QAAQ;AAAA,MACR,MAAM,KAAK,UAAU;AAAA,MACrB,SAAS;AAAA,WACJ,KAAK,uBAAuB,mCAAS;AAAA,QACxC,gBAAgB;AAAA;AAAA;AAGpB,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,MAAM,cAAc,aAAa;AAAA;AAGzC,UAAM,sBAAsB,MAAM,SAAS;AAC3C,SAAK,qBAAqB,oBAAoB;AAE9C,UAAM,gBAAgB,oBAAoB,OAAO,CAAC,KAAK,MAAM;AAC3D,UAAI,EAAE,MAAM;AACZ,aAAO;AAAA,OACN;AAEH,WAAO,mBAAmB,IAAI,aAAW,cAAc,QAAQ;AAAA;AAAA,EAGzD,uBAAuB,OAAwC;AACrE,WAAO,QAAQ,EAAE,eAAe,UAAU,YAAY;AAAA;AAAA,EAGhD,qBACN,UACA,MACiD;AACjD,UAAM,sBAAsB,eAAe,MAAM;AACjD,UAAM,cAAc,oBAAoB,IAAI,OAAK,EAAE;AACnD,UAAM,mBAAmB,SAAS,MAAM,OAAK,YAAY,SAAS,EAAE;AACpE,QAAI,CAAC,kBAAkB;AACrB,YAAM,IAAI,MACR;AAAA;AAAA;AAAA;;;;"}
1
+ {"version":3,"file":"index.esm.js","sources":["../src/types/api.ts","../src/permissions/util.ts","../src/PermissionClient.ts"],"sourcesContent":["/*\n * Copyright 2021 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 { Permission } from './permission';\n\n/**\n * A request with a UUID identifier, so that batched responses can be matched up with the original\n * requests.\n * @public\n */\nexport type Identified<T> = T & { id: string };\n\n/**\n * The result of an authorization request.\n * @public\n */\nexport enum AuthorizeResult {\n /**\n * The authorization request is denied.\n */\n DENY = 'DENY',\n /**\n * The authorization request is allowed.\n */\n ALLOW = 'ALLOW',\n /**\n * The authorization request is allowed if the provided conditions are met.\n */\n CONDITIONAL = 'CONDITIONAL',\n}\n\n/**\n * An individual authorization request for {@link PermissionClient#authorize}.\n * @public\n */\nexport type AuthorizeQuery = {\n permission: Permission;\n resourceRef?: string;\n};\n\n/**\n * A batch of authorization requests from {@link PermissionClient#authorize}.\n * @public\n */\nexport type AuthorizeRequest = {\n items: Identified<AuthorizeQuery>[];\n};\n\n/**\n * A condition returned with a CONDITIONAL authorization response.\n *\n * Conditions are a reference to a rule defined by a plugin, and parameters to apply the rule. For\n * example, a rule might be `isOwner` from the catalog-backend, and params may be a list of entity\n * claims from a identity token.\n * @public\n */\nexport type PermissionCondition<TParams extends unknown[] = unknown[]> = {\n rule: string;\n params: TParams;\n};\n\n/**\n * Utility type to represent an array with 1 or more elements.\n * @ignore\n */\ntype NonEmptyArray<T> = [T, ...T[]];\n\n/**\n * Represnts a logical AND for the provided criteria.\n * @public\n */\nexport type AllOfCriteria<TQuery> = {\n allOf: NonEmptyArray<PermissionCriteria<TQuery>>;\n};\n\n/**\n * Represnts a logical OR for the provided criteria.\n * @public\n */\nexport type AnyOfCriteria<TQuery> = {\n anyOf: NonEmptyArray<PermissionCriteria<TQuery>>;\n};\n\n/**\n * Represents a negation of the provided criteria.\n * @public\n */\nexport type NotCriteria<TQuery> = {\n not: PermissionCriteria<TQuery>;\n};\n\n/**\n * Composes several {@link PermissionCondition}s as criteria with a nested AND/OR structure.\n * @public\n */\nexport type PermissionCriteria<TQuery> =\n | AllOfCriteria<TQuery>\n | AnyOfCriteria<TQuery>\n | NotCriteria<TQuery>\n | TQuery;\n\n/**\n * An individual authorization response from {@link PermissionClient#authorize}.\n * @public\n */\nexport type AuthorizeDecision =\n | { result: AuthorizeResult.ALLOW | AuthorizeResult.DENY }\n | {\n result: AuthorizeResult.CONDITIONAL;\n conditions: PermissionCriteria<PermissionCondition>;\n };\n\n/**\n * A batch of authorization responses from {@link PermissionClient#authorize}.\n * @public\n */\nexport type AuthorizeResponse = {\n items: Identified<AuthorizeDecision>[];\n};\n","/*\n * Copyright 2021 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 { Permission } from '../types';\n\n/**\n * Check if a given permission is related to a create action.\n * @public\n */\nexport function isCreatePermission(permission: Permission) {\n return permission.attributes.action === 'create';\n}\n\n/**\n * Check if a given permission is related to a read action.\n * @public\n */\nexport function isReadPermission(permission: Permission) {\n return permission.attributes.action === 'read';\n}\n\n/**\n * Check if a given permission is related to an update action.\n * @public\n */\nexport function isUpdatePermission(permission: Permission) {\n return permission.attributes.action === 'update';\n}\n\n/**\n * Check if a given permission is related to a delete action.\n * @public\n */\nexport function isDeletePermission(permission: Permission) {\n return permission.attributes.action === 'delete';\n}\n","/*\n * Copyright 2021 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 { Config } from '@backstage/config';\nimport { ResponseError } from '@backstage/errors';\nimport fetch from 'cross-fetch';\nimport * as uuid from 'uuid';\nimport { z } from 'zod';\nimport {\n AuthorizeResult,\n AuthorizeQuery,\n AuthorizeDecision,\n Identified,\n PermissionCriteria,\n PermissionCondition,\n AuthorizeResponse,\n AuthorizeRequest,\n} from './types/api';\nimport { DiscoveryApi } from './types/discovery';\nimport {\n PermissionAuthorizer,\n AuthorizeRequestOptions,\n} from './types/permission';\n\nconst permissionCriteriaSchema: z.ZodSchema<\n PermissionCriteria<PermissionCondition>\n> = z.lazy(() =>\n z\n .object({\n rule: z.string(),\n params: z.array(z.unknown()),\n })\n .strict()\n .or(\n z\n .object({ anyOf: z.array(permissionCriteriaSchema).nonempty() })\n .strict(),\n )\n .or(\n z\n .object({ allOf: z.array(permissionCriteriaSchema).nonempty() })\n .strict(),\n )\n .or(z.object({ not: permissionCriteriaSchema }).strict()),\n);\n\nconst responseSchema = z.object({\n items: z.array(\n z\n .object({\n id: z.string(),\n result: z\n .literal(AuthorizeResult.ALLOW)\n .or(z.literal(AuthorizeResult.DENY)),\n })\n .or(\n z.object({\n id: z.string(),\n result: z.literal(AuthorizeResult.CONDITIONAL),\n conditions: permissionCriteriaSchema,\n }),\n ),\n ),\n});\n\n/**\n * An isomorphic client for requesting authorization for Backstage permissions.\n * @public\n */\nexport class PermissionClient implements PermissionAuthorizer {\n private readonly enabled: boolean;\n private readonly discovery: DiscoveryApi;\n\n constructor(options: { discovery: DiscoveryApi; config: Config }) {\n this.discovery = options.discovery;\n this.enabled =\n options.config.getOptionalBoolean('permission.enabled') ?? false;\n }\n\n /**\n * Request authorization from the permission-backend for the given set of permissions.\n *\n * Authorization requests check that a given Backstage user can perform a protected operation,\n * potentially for a specific resource (such as a catalog entity). The Backstage identity token\n * should be included in the `options` if available.\n *\n * Permissions can be imported from plugins exposing them, such as `catalogEntityReadPermission`.\n *\n * The response will be either ALLOW or DENY when either the permission has no resourceType, or a\n * resourceRef is provided in the request. For permissions with a resourceType, CONDITIONAL may be\n * returned if no resourceRef is provided in the request. Conditional responses are intended only\n * for backends which have access to the data source for permissioned resources, so that filters\n * can be applied when loading collections of resources.\n * @public\n */\n async authorize(\n queries: AuthorizeQuery[],\n options?: AuthorizeRequestOptions,\n ): Promise<AuthorizeDecision[]> {\n // TODO(permissions): it would be great to provide some kind of typing guarantee that\n // conditional responses will only ever be returned for requests containing a resourceType\n // but no resourceRef. That way clients who aren't prepared to handle filtering according\n // to conditions can be guaranteed that they won't unexpectedly get a CONDITIONAL response.\n\n if (!this.enabled) {\n return queries.map(_ => ({ result: AuthorizeResult.ALLOW }));\n }\n\n const request: AuthorizeRequest = {\n items: queries.map(query => ({\n id: uuid.v4(),\n ...query,\n })),\n };\n\n const permissionApi = await this.discovery.getBaseUrl('permission');\n const response = await fetch(`${permissionApi}/authorize`, {\n method: 'POST',\n body: JSON.stringify(request),\n headers: {\n ...this.getAuthorizationHeader(options?.token),\n 'content-type': 'application/json',\n },\n });\n if (!response.ok) {\n throw await ResponseError.fromResponse(response);\n }\n\n const responseBody = await response.json();\n this.assertValidResponse(request, responseBody);\n\n const responsesById = responseBody.items.reduce((acc, r) => {\n acc[r.id] = r;\n return acc;\n }, {} as Record<string, Identified<AuthorizeDecision>>);\n\n return request.items.map(query => responsesById[query.id]);\n }\n\n private getAuthorizationHeader(token?: string): Record<string, string> {\n return token ? { Authorization: `Bearer ${token}` } : {};\n }\n\n private assertValidResponse(\n request: AuthorizeRequest,\n json: any,\n ): asserts json is AuthorizeResponse {\n const authorizedResponses = responseSchema.parse(json);\n const responseIds = authorizedResponses.items.map(r => r.id);\n const hasAllRequestIds = request.items.every(r =>\n responseIds.includes(r.id),\n );\n if (!hasAllRequestIds) {\n throw new Error(\n 'Unexpected authorization response from permission-backend',\n );\n }\n }\n}\n"],"names":[],"mappings":";;;;;IA6BY,oCAAA,qBAAL;AAIL,6BAAO;AAIP,8BAAQ;AAIR,oCAAc;AAZJ;AAAA;;4BCPuB,YAAwB;AACzD,SAAO,WAAW,WAAW,WAAW;AAAA;0BAOT,YAAwB;AACvD,SAAO,WAAW,WAAW,WAAW;AAAA;4BAOP,YAAwB;AACzD,SAAO,WAAW,WAAW,WAAW;AAAA;4BAOP,YAAwB;AACzD,SAAO,WAAW,WAAW,WAAW;AAAA;;ACV1C,MAAM,2BAEF,EAAE,KAAK,MACT,EACG,OAAO;AAAA,EACN,MAAM,EAAE;AAAA,EACR,QAAQ,EAAE,MAAM,EAAE;AAAA,GAEnB,SACA,GACC,EACG,OAAO,EAAE,OAAO,EAAE,MAAM,0BAA0B,cAClD,UAEJ,GACC,EACG,OAAO,EAAE,OAAO,EAAE,MAAM,0BAA0B,cAClD,UAEJ,GAAG,EAAE,OAAO,EAAE,KAAK,4BAA4B;AAGpD,MAAM,iBAAiB,EAAE,OAAO;AAAA,EAC9B,OAAO,EAAE,MACP,EACG,OAAO;AAAA,IACN,IAAI,EAAE;AAAA,IACN,QAAQ,EACL,QAAQ,gBAAgB,OACxB,GAAG,EAAE,QAAQ,gBAAgB;AAAA,KAEjC,GACC,EAAE,OAAO;AAAA,IACP,IAAI,EAAE;AAAA,IACN,QAAQ,EAAE,QAAQ,gBAAgB;AAAA,IAClC,YAAY;AAAA;AAAA;uBAUwC;AAAA,EAI5D,YAAY,SAAsD;AAtFpE;AAuFI,SAAK,YAAY,QAAQ;AACzB,SAAK,UACH,cAAQ,OAAO,mBAAmB,0BAAlC,YAA2D;AAAA;AAAA,QAmBzD,UACJ,SACA,SAC8B;AAM9B,QAAI,CAAC,KAAK,SAAS;AACjB,aAAO,QAAQ,IAAI,UAAQ,QAAQ,gBAAgB;AAAA;AAGrD,UAAM,UAA4B;AAAA,MAChC,OAAO,QAAQ,IAAI;AAAU,QAC3B,IAAI,KAAK;AAAA,WACN;AAAA;AAAA;AAIP,UAAM,gBAAgB,MAAM,KAAK,UAAU,WAAW;AACtD,UAAM,WAAW,MAAM,MAAM,GAAG,2BAA2B;AAAA,MACzD,QAAQ;AAAA,MACR,MAAM,KAAK,UAAU;AAAA,MACrB,SAAS;AAAA,WACJ,KAAK,uBAAuB,mCAAS;AAAA,QACxC,gBAAgB;AAAA;AAAA;AAGpB,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,MAAM,cAAc,aAAa;AAAA;AAGzC,UAAM,eAAe,MAAM,SAAS;AACpC,SAAK,oBAAoB,SAAS;AAElC,UAAM,gBAAgB,aAAa,MAAM,OAAO,CAAC,KAAK,MAAM;AAC1D,UAAI,EAAE,MAAM;AACZ,aAAO;AAAA,OACN;AAEH,WAAO,QAAQ,MAAM,IAAI,WAAS,cAAc,MAAM;AAAA;AAAA,EAGhD,uBAAuB,OAAwC;AACrE,WAAO,QAAQ,EAAE,eAAe,UAAU,YAAY;AAAA;AAAA,EAGhD,oBACN,SACA,MACmC;AACnC,UAAM,sBAAsB,eAAe,MAAM;AACjD,UAAM,cAAc,oBAAoB,MAAM,IAAI,OAAK,EAAE;AACzD,UAAM,mBAAmB,QAAQ,MAAM,MAAM,OAC3C,YAAY,SAAS,EAAE;AAEzB,QAAI,CAAC,kBAAkB;AACrB,YAAM,IAAI,MACR;AAAA;AAAA;AAAA;;;;"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@backstage/plugin-permission-common",
3
3
  "description": "Isomorphic types and client for Backstage permissions and authorization",
4
- "version": "0.3.1",
4
+ "version": "0.5.1",
5
5
  "main": "dist/index.cjs.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "publishConfig": {
@@ -10,6 +10,9 @@
10
10
  "module": "dist/index.esm.js",
11
11
  "types": "dist/index.d.ts"
12
12
  },
13
+ "backstage": {
14
+ "role": "common-library"
15
+ },
13
16
  "homepage": "https://backstage.io",
14
17
  "repository": {
15
18
  "type": "git",
@@ -27,28 +30,28 @@
27
30
  ],
28
31
  "configSchema": "config.d.ts",
29
32
  "scripts": {
30
- "build": "backstage-cli build",
31
- "lint": "backstage-cli lint",
32
- "test": "backstage-cli test",
33
- "prepack": "backstage-cli prepack",
34
- "postpack": "backstage-cli postpack",
35
- "clean": "backstage-cli clean"
33
+ "build": "backstage-cli package build",
34
+ "lint": "backstage-cli package lint",
35
+ "test": "backstage-cli package test",
36
+ "prepack": "backstage-cli package prepack",
37
+ "postpack": "backstage-cli package postpack",
38
+ "clean": "backstage-cli package clean"
36
39
  },
37
40
  "bugs": {
38
41
  "url": "https://github.com/backstage/backstage/issues"
39
42
  },
40
43
  "dependencies": {
41
- "@backstage/config": "^0.1.12",
42
- "@backstage/errors": "^0.2.0",
43
- "cross-fetch": "^3.0.6",
44
+ "@backstage/config": "^0.1.15",
45
+ "@backstage/errors": "^0.2.2",
46
+ "cross-fetch": "^3.1.5",
44
47
  "uuid": "^8.0.0",
45
48
  "zod": "^3.11.6"
46
49
  },
47
50
  "devDependencies": {
48
- "@backstage/cli": "^0.11.0",
51
+ "@backstage/cli": "^0.14.0",
49
52
  "@types/jest": "^26.0.7",
50
53
  "msw": "^0.35.0"
51
54
  },
52
- "gitHead": "da66c61bdd63cdb3f0f0cd2e26dc9e6454d93c7b",
55
+ "gitHead": "e244b348c473700e7d5e5fbcef38bd9f9fd1d0ba",
53
56
  "module": "dist/index.esm.js"
54
57
  }