@backstage/plugin-permission-common 0.2.0 → 0.3.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.
- package/CHANGELOG.md +16 -0
- package/dist/index.cjs.js +15 -16
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +19 -12
- package/dist/index.esm.js +9 -9
- package/dist/index.esm.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @backstage/plugin-permission-common
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 0e8ec6d974: - Add `PermissionAuthorizer` interface matching `PermissionClient` to allow alternative implementations like the `ServerPermissionClient` in @backstage/plugin-permission-node.
|
|
8
|
+
|
|
9
|
+
Breaking Changes:
|
|
10
|
+
|
|
11
|
+
- Remove "api" suffixes from constructor parameters in PermissionClient
|
|
12
|
+
|
|
13
|
+
```diff
|
|
14
|
+
const { config, discovery } = options;
|
|
15
|
+
- const permissionClient = new PermissionClient({ discoveryApi: discovery, configApi: config });
|
|
16
|
+
+ const permissionClient = new PermissionClient({ discovery, config });
|
|
17
|
+
```
|
|
18
|
+
|
|
3
19
|
## 0.2.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
package/dist/index.cjs.js
CHANGED
|
@@ -18,26 +18,24 @@ function _interopNamespace(e) {
|
|
|
18
18
|
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
19
19
|
Object.defineProperty(n, k, d.get ? d : {
|
|
20
20
|
enumerable: true,
|
|
21
|
-
get: function () {
|
|
22
|
-
return e[k];
|
|
23
|
-
}
|
|
21
|
+
get: function () { return e[k]; }
|
|
24
22
|
});
|
|
25
23
|
}
|
|
26
24
|
});
|
|
27
25
|
}
|
|
28
|
-
n[
|
|
26
|
+
n["default"] = e;
|
|
29
27
|
return Object.freeze(n);
|
|
30
28
|
}
|
|
31
29
|
|
|
32
30
|
var fetch__default = /*#__PURE__*/_interopDefaultLegacy(fetch);
|
|
33
31
|
var uuid__namespace = /*#__PURE__*/_interopNamespace(uuid);
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
(function(AuthorizeResult2) {
|
|
33
|
+
var AuthorizeResult = /* @__PURE__ */ ((AuthorizeResult2) => {
|
|
37
34
|
AuthorizeResult2["DENY"] = "DENY";
|
|
38
35
|
AuthorizeResult2["ALLOW"] = "ALLOW";
|
|
39
36
|
AuthorizeResult2["CONDITIONAL"] = "CONDITIONAL";
|
|
40
|
-
|
|
37
|
+
return AuthorizeResult2;
|
|
38
|
+
})(AuthorizeResult || {});
|
|
41
39
|
|
|
42
40
|
function isCreatePermission(permission) {
|
|
43
41
|
return permission.attributes.action === "create";
|
|
@@ -55,31 +53,31 @@ function isDeletePermission(permission) {
|
|
|
55
53
|
const permissionCriteriaSchema = zod.z.lazy(() => zod.z.object({
|
|
56
54
|
rule: zod.z.string(),
|
|
57
55
|
params: zod.z.array(zod.z.unknown())
|
|
58
|
-
}).or(zod.z.object({anyOf: zod.z.array(permissionCriteriaSchema)})).or(zod.z.object({allOf: zod.z.array(permissionCriteriaSchema)})).or(zod.z.object({not: permissionCriteriaSchema})));
|
|
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 })));
|
|
59
57
|
const responseSchema = zod.z.array(zod.z.object({
|
|
60
58
|
id: zod.z.string(),
|
|
61
|
-
result: zod.z.literal(
|
|
59
|
+
result: zod.z.literal(AuthorizeResult.ALLOW).or(zod.z.literal(AuthorizeResult.DENY))
|
|
62
60
|
}).or(zod.z.object({
|
|
63
61
|
id: zod.z.string(),
|
|
64
|
-
result: zod.z.literal(
|
|
62
|
+
result: zod.z.literal(AuthorizeResult.CONDITIONAL),
|
|
65
63
|
conditions: permissionCriteriaSchema
|
|
66
64
|
})));
|
|
67
65
|
class PermissionClient {
|
|
68
66
|
constructor(options) {
|
|
69
67
|
var _a;
|
|
70
|
-
this.
|
|
71
|
-
this.enabled = (_a = options.
|
|
68
|
+
this.discovery = options.discovery;
|
|
69
|
+
this.enabled = (_a = options.config.getOptionalBoolean("permission.enabled")) != null ? _a : false;
|
|
72
70
|
}
|
|
73
71
|
async authorize(requests, options) {
|
|
74
72
|
if (!this.enabled) {
|
|
75
|
-
return requests.map((_) => ({result:
|
|
73
|
+
return requests.map((_) => ({ result: AuthorizeResult.ALLOW }));
|
|
76
74
|
}
|
|
77
75
|
const identifiedRequests = requests.map((request) => ({
|
|
78
76
|
id: uuid__namespace.v4(),
|
|
79
77
|
...request
|
|
80
78
|
}));
|
|
81
|
-
const permissionApi = await this.
|
|
82
|
-
const response = await fetch__default[
|
|
79
|
+
const permissionApi = await this.discovery.getBaseUrl("permission");
|
|
80
|
+
const response = await fetch__default["default"](`${permissionApi}/authorize`, {
|
|
83
81
|
method: "POST",
|
|
84
82
|
body: JSON.stringify(identifiedRequests),
|
|
85
83
|
headers: {
|
|
@@ -99,7 +97,7 @@ class PermissionClient {
|
|
|
99
97
|
return identifiedRequests.map((request) => responsesById[request.id]);
|
|
100
98
|
}
|
|
101
99
|
getAuthorizationHeader(token) {
|
|
102
|
-
return token ? {Authorization: `Bearer ${token}`} : {};
|
|
100
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
103
101
|
}
|
|
104
102
|
assertValidResponses(requests, json) {
|
|
105
103
|
const authorizedResponses = responseSchema.parse(json);
|
|
@@ -111,6 +109,7 @@ class PermissionClient {
|
|
|
111
109
|
}
|
|
112
110
|
}
|
|
113
111
|
|
|
112
|
+
exports.AuthorizeResult = AuthorizeResult;
|
|
114
113
|
exports.PermissionClient = PermissionClient;
|
|
115
114
|
exports.isCreatePermission = isCreatePermission;
|
|
116
115
|
exports.isDeletePermission = isDeletePermission;
|
package/dist/index.cjs.js.map
CHANGED
|
@@ -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';\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 * Options for authorization requests; currently only an optional auth token.\n * @public\n */\nexport type AuthorizeRequestOptions = {\n token?: string;\n};\n\n/**\n * An isomorphic client for requesting authorization for Backstage permissions.\n * @public\n */\nexport class PermissionClient {\n private readonly enabled: boolean;\n private readonly discoveryApi: DiscoveryApi;\n\n constructor(options: { discoveryApi: DiscoveryApi; configApi: Config }) {\n this.discoveryApi = options.discoveryApi;\n this.enabled =\n options.configApi.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.discoveryApi.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":["AuthorizeResult","z","uuid","fetch","ResponseError"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BYA;AAAL,UAAK,kBAAL;AAIL,6BAAO;AAIP,8BAAQ;AAIR,oCAAc;AAAA,GAZJA;;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;;AChB1C,MAAM,2BAEFC,MAAE,KAAK,MACTA,MACG,OAAO;AAAA,EACN,MAAMA,MAAE;AAAA,EACR,QAAQA,MAAE,MAAMA,MAAE;AAAA,GAEnB,GAAGA,MAAE,OAAO,CAAE,OAAOA,MAAE,MAAM,6BAC7B,GAAGA,MAAE,OAAO,CAAE,OAAOA,MAAE,MAAM,6BAC7B,GAAGA,MAAE,OAAO,CAAE,KAAK;AAGxB,MAAM,iBAAiBA,MAAE,MACvBA,MACG,OAAO;AAAA,EACN,IAAIA,MAAE;AAAA,EACN,QAAQA,MACL,QAAQD,wBAAgB,OACxB,GAAGC,MAAE,QAAQD,wBAAgB;AAAA,GAEjC,GACCC,MAAE,OAAO;AAAA,EACP,IAAIA,MAAE;AAAA,EACN,QAAQA,MAAE,QAAQD,wBAAgB;AAAA,EAClC,YAAY;AAAA;uBAiBU;AAAA,EAI5B,YAAY,SAA4D;AA7E1E;AA8EI,SAAK,eAAe,QAAQ;AAC5B,SAAK,UACH,cAAQ,UAAU,mBAAmB,0BAArC,YAA8D;AAAA;AAAA,QAmB5D,UACJ,UACA,SAC8B;AAM9B,QAAI,CAAC,KAAK,SAAS;AACjB,aAAO,SAAS,IAAI,SAAQ,QAAQA,wBAAgB;AAAA;AAGtD,UAAM,qBAAqD,SAAS,IAClE;AAAY,MACV,IAAIE,gBAAK;AAAA,SACN;AAAA;AAIP,UAAM,gBAAgB,MAAM,KAAK,aAAa,WAAW;AACzD,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,CAAE,eAAe,UAAU,WAAY;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 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;;;;;;;;;"}
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,20 @@ declare type Permission = {
|
|
|
24
24
|
attributes: PermissionAttributes;
|
|
25
25
|
resourceType?: string;
|
|
26
26
|
};
|
|
27
|
+
/**
|
|
28
|
+
* A client interacting with the permission backend can implement this authorizer interface.
|
|
29
|
+
* @public
|
|
30
|
+
*/
|
|
31
|
+
interface PermissionAuthorizer {
|
|
32
|
+
authorize(requests: AuthorizeRequest[], options?: AuthorizeRequestOptions): Promise<AuthorizeResponse[]>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Options for authorization requests.
|
|
36
|
+
* @public
|
|
37
|
+
*/
|
|
38
|
+
declare type AuthorizeRequestOptions = {
|
|
39
|
+
token?: string;
|
|
40
|
+
};
|
|
27
41
|
|
|
28
42
|
/**
|
|
29
43
|
* A request with a UUID identifier, so that batched responses can be matched up with the original
|
|
@@ -123,23 +137,16 @@ declare function isUpdatePermission(permission: Permission): boolean;
|
|
|
123
137
|
*/
|
|
124
138
|
declare function isDeletePermission(permission: Permission): boolean;
|
|
125
139
|
|
|
126
|
-
/**
|
|
127
|
-
* Options for authorization requests; currently only an optional auth token.
|
|
128
|
-
* @public
|
|
129
|
-
*/
|
|
130
|
-
declare type AuthorizeRequestOptions = {
|
|
131
|
-
token?: string;
|
|
132
|
-
};
|
|
133
140
|
/**
|
|
134
141
|
* An isomorphic client for requesting authorization for Backstage permissions.
|
|
135
142
|
* @public
|
|
136
143
|
*/
|
|
137
|
-
declare class PermissionClient {
|
|
144
|
+
declare class PermissionClient implements PermissionAuthorizer {
|
|
138
145
|
private readonly enabled;
|
|
139
|
-
private readonly
|
|
146
|
+
private readonly discovery;
|
|
140
147
|
constructor(options: {
|
|
141
|
-
|
|
142
|
-
|
|
148
|
+
discovery: DiscoveryApi;
|
|
149
|
+
config: Config;
|
|
143
150
|
});
|
|
144
151
|
/**
|
|
145
152
|
* Request authorization from the permission-backend for the given set of permissions.
|
|
@@ -162,4 +169,4 @@ declare class PermissionClient {
|
|
|
162
169
|
private assertValidResponses;
|
|
163
170
|
}
|
|
164
171
|
|
|
165
|
-
export { AuthorizeRequest, AuthorizeRequestOptions, AuthorizeResponse, AuthorizeResult, DiscoveryApi, Identified, Permission, PermissionAttributes, PermissionClient, PermissionCondition, PermissionCriteria, isCreatePermission, isDeletePermission, isReadPermission, isUpdatePermission };
|
|
172
|
+
export { AuthorizeRequest, AuthorizeRequestOptions, AuthorizeResponse, AuthorizeResult, DiscoveryApi, Identified, Permission, PermissionAttributes, PermissionAuthorizer, PermissionClient, PermissionCondition, PermissionCriteria, isCreatePermission, isDeletePermission, isReadPermission, isUpdatePermission };
|
package/dist/index.esm.js
CHANGED
|
@@ -3,12 +3,12 @@ import fetch from 'cross-fetch';
|
|
|
3
3
|
import * as uuid from 'uuid';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
|
|
6
|
-
var AuthorizeResult
|
|
7
|
-
(function(AuthorizeResult2) {
|
|
6
|
+
var AuthorizeResult = /* @__PURE__ */ ((AuthorizeResult2) => {
|
|
8
7
|
AuthorizeResult2["DENY"] = "DENY";
|
|
9
8
|
AuthorizeResult2["ALLOW"] = "ALLOW";
|
|
10
9
|
AuthorizeResult2["CONDITIONAL"] = "CONDITIONAL";
|
|
11
|
-
|
|
10
|
+
return AuthorizeResult2;
|
|
11
|
+
})(AuthorizeResult || {});
|
|
12
12
|
|
|
13
13
|
function isCreatePermission(permission) {
|
|
14
14
|
return permission.attributes.action === "create";
|
|
@@ -26,7 +26,7 @@ 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})));
|
|
29
|
+
}).or(z.object({ anyOf: z.array(permissionCriteriaSchema) })).or(z.object({ allOf: z.array(permissionCriteriaSchema) })).or(z.object({ not: permissionCriteriaSchema })));
|
|
30
30
|
const responseSchema = z.array(z.object({
|
|
31
31
|
id: z.string(),
|
|
32
32
|
result: z.literal(AuthorizeResult.ALLOW).or(z.literal(AuthorizeResult.DENY))
|
|
@@ -38,18 +38,18 @@ const responseSchema = z.array(z.object({
|
|
|
38
38
|
class PermissionClient {
|
|
39
39
|
constructor(options) {
|
|
40
40
|
var _a;
|
|
41
|
-
this.
|
|
42
|
-
this.enabled = (_a = options.
|
|
41
|
+
this.discovery = options.discovery;
|
|
42
|
+
this.enabled = (_a = options.config.getOptionalBoolean("permission.enabled")) != null ? _a : false;
|
|
43
43
|
}
|
|
44
44
|
async authorize(requests, options) {
|
|
45
45
|
if (!this.enabled) {
|
|
46
|
-
return requests.map((_) => ({result: AuthorizeResult.ALLOW}));
|
|
46
|
+
return requests.map((_) => ({ result: AuthorizeResult.ALLOW }));
|
|
47
47
|
}
|
|
48
48
|
const identifiedRequests = requests.map((request) => ({
|
|
49
49
|
id: uuid.v4(),
|
|
50
50
|
...request
|
|
51
51
|
}));
|
|
52
|
-
const permissionApi = await this.
|
|
52
|
+
const permissionApi = await this.discovery.getBaseUrl("permission");
|
|
53
53
|
const response = await fetch(`${permissionApi}/authorize`, {
|
|
54
54
|
method: "POST",
|
|
55
55
|
body: JSON.stringify(identifiedRequests),
|
|
@@ -70,7 +70,7 @@ class PermissionClient {
|
|
|
70
70
|
return identifiedRequests.map((request) => responsesById[request.id]);
|
|
71
71
|
}
|
|
72
72
|
getAuthorizationHeader(token) {
|
|
73
|
-
return token ? {Authorization: `Bearer ${token}`} : {};
|
|
73
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
74
74
|
}
|
|
75
75
|
assertValidResponses(requests, json) {
|
|
76
76
|
const authorizedResponses = responseSchema.parse(json);
|
package/dist/index.esm.js.map
CHANGED
|
@@ -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';\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 * Options for authorization requests; currently only an optional auth token.\n * @public\n */\nexport type AuthorizeRequestOptions = {\n token?: string;\n};\n\n/**\n * An isomorphic client for requesting authorization for Backstage permissions.\n * @public\n */\nexport class PermissionClient {\n private readonly enabled: boolean;\n private readonly discoveryApi: DiscoveryApi;\n\n constructor(options: { discoveryApi: DiscoveryApi; configApi: Config }) {\n this.discoveryApi = options.discoveryApi;\n this.enabled =\n options.configApi.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.discoveryApi.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;AAAL,UAAK,kBAAL;AAIL,6BAAO;AAIP,8BAAQ;AAIR,oCAAc;AAAA,GAZJ;;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;;AChB1C,MAAM,2BAEF,EAAE,KAAK,MACT,EACG,OAAO;AAAA,EACN,MAAM,EAAE;AAAA,EACR,QAAQ,EAAE,MAAM,EAAE;AAAA,GAEnB,GAAG,EAAE,OAAO,CAAE,OAAO,EAAE,MAAM,6BAC7B,GAAG,EAAE,OAAO,CAAE,OAAO,EAAE,MAAM,6BAC7B,GAAG,EAAE,OAAO,CAAE,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;uBAiBU;AAAA,EAI5B,YAAY,SAA4D;AA7E1E;AA8EI,SAAK,eAAe,QAAQ;AAC5B,SAAK,UACH,cAAQ,UAAU,mBAAmB,0BAArC,YAA8D;AAAA;AAAA,QAmB5D,UACJ,UACA,SAC8B;AAM9B,QAAI,CAAC,KAAK,SAAS;AACjB,aAAO,SAAS,IAAI,SAAQ,QAAQ,gBAAgB;AAAA;AAGtD,UAAM,qBAAqD,SAAS,IAClE;AAAY,MACV,IAAI,KAAK;AAAA,SACN;AAAA;AAIP,UAAM,gBAAgB,MAAM,KAAK,aAAa,WAAW;AACzD,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,CAAE,eAAe,UAAU,WAAY;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 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;;;;"}
|
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.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"main": "dist/index.cjs.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"publishConfig": {
|
|
@@ -45,10 +45,10 @@
|
|
|
45
45
|
"zod": "^3.11.6"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@backstage/cli": "^0.
|
|
48
|
+
"@backstage/cli": "^0.10.3",
|
|
49
49
|
"@types/jest": "^26.0.7",
|
|
50
50
|
"msw": "^0.35.0"
|
|
51
51
|
},
|
|
52
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "b315430f9dfcfa19ab0dd90f5b4ac6904938fba7",
|
|
53
53
|
"module": "dist/index.esm.js"
|
|
54
54
|
}
|