@backstage/plugin-permission-common
Advanced tools
Comparing version 0.0.0-nightly-20220923026 to 0.0.0-nightly-20220923030237
223
CHANGELOG.md
# @backstage/plugin-permission-common | ||
## 0.0.0-nightly-20220923030237 | ||
### Patch Changes | ||
- Updated dependencies | ||
- @backstage/config@0.0.0-nightly-20220923030237 | ||
- @backstage/errors@0.0.0-nightly-20220923030237 | ||
## 0.6.4 | ||
### Patch Changes | ||
- 7d47def9c4: Removed dependency on `@types/jest`. | ||
- 667d917488: Updated dependency `msw` to `^0.47.0`. | ||
- 87ec2ba4d6: Updated dependency `msw` to `^0.46.0`. | ||
- bf5e9030eb: Updated dependency `msw` to `^0.45.0`. | ||
- Updated dependencies | ||
- @backstage/config@1.0.2 | ||
- @backstage/errors@1.1.1 | ||
## 0.6.4-next.2 | ||
### Patch Changes | ||
- 7d47def9c4: Removed dependency on `@types/jest`. | ||
- Updated dependencies | ||
- @backstage/config@1.0.2-next.0 | ||
- @backstage/errors@1.1.1-next.0 | ||
## 0.6.4-next.1 | ||
### Patch Changes | ||
- 667d917488: Updated dependency `msw` to `^0.47.0`. | ||
- 87ec2ba4d6: Updated dependency `msw` to `^0.46.0`. | ||
## 0.6.4-next.0 | ||
### Patch Changes | ||
- bf5e9030eb: Updated dependency `msw` to `^0.45.0`. | ||
## 0.6.3 | ||
### Patch Changes | ||
- a70869e775: Updated dependency `msw` to `^0.43.0`. | ||
- 8006d0f9bf: Updated dependency `msw` to `^0.44.0`. | ||
- Updated dependencies | ||
- @backstage/errors@1.1.0 | ||
## 0.6.3-next.1 | ||
### Patch Changes | ||
- a70869e775: Updated dependency `msw` to `^0.43.0`. | ||
## 0.6.3-next.0 | ||
### Patch Changes | ||
- Updated dependencies | ||
- @backstage/errors@1.1.0-next.0 | ||
## 0.6.2 | ||
### Patch Changes | ||
- 8f7b1835df: Updated dependency `msw` to `^0.41.0`. | ||
## 0.6.2-next.0 | ||
### Patch Changes | ||
- 8f7b1835df: Updated dependency `msw` to `^0.41.0`. | ||
## 0.6.1 | ||
### Patch Changes | ||
- Updated dependencies | ||
- @backstage/config@1.0.1 | ||
## 0.6.1-next.0 | ||
### Patch Changes | ||
- Updated dependencies | ||
- @backstage/config@1.0.1-next.0 | ||
## 0.6.0 | ||
### Minor Changes | ||
- 8012ac46a0: Add `resourceType` property to `PermissionCondition` type to allow matching them with `ResourcePermission` instances. | ||
- c98d271466: Refactor api types into more specific, decoupled names. | ||
- **BREAKING:** | ||
- Renamed `AuthorizeDecision` to `EvaluatePermissionResponse` | ||
- Renamed `AuthorizeQuery` to `EvaluatePermissionRequest` | ||
- Renamed `AuthorizeRequest` to `EvaluatePermissionRequestBatch` | ||
- Renamed `AuthorizeResponse` to `EvaluatePermissionResponseBatch` | ||
- Renamed `Identified` to `IdentifiedPermissionMessage` | ||
- Add `PermissionMessageBatch` helper type | ||
- Add `ConditionalPolicyDecision`, `DefinitivePolicyDecision`, and `PolicyDecision` types from `@backstage/plugin-permission-node` | ||
### Patch Changes | ||
- 90754d4fa9: Removed [strict](https://github.com/colinhacks/zod#strict) validation from `PermissionCriteria` schemas to support backward-compatible changes. | ||
- 2b07063d77: Added `PermissionEvaluator`, which will replace the existing `PermissionAuthorizer` interface. This new interface provides stronger type safety and validation by splitting `PermissionAuthorizer.authorize()` into two methods: | ||
- `authorize()`: Used when the caller requires a definitive decision. | ||
- `authorizeConditional()`: Used when the caller can optimize the evaluation of any conditional decisions. For example, a plugin backend may want to use conditions in a database query instead of evaluating each resource in memory. | ||
- 8012ac46a0: Add `isPermission` helper method. | ||
- 95284162d6: - Add more specific `Permission` types. | ||
- Add `createPermission` helper to infer the appropriate type for some permission input. | ||
- Add `isResourcePermission` helper to refine Permissions to ResourcePermissions. | ||
## 0.6.0-next.1 | ||
### Patch Changes | ||
- 2b07063d77: Added `PermissionEvaluator`, which will replace the existing `PermissionAuthorizer` interface. This new interface provides stronger type safety and validation by splitting `PermissionAuthorizer.authorize()` into two methods: | ||
- `authorize()`: Used when the caller requires a definitive decision. | ||
- `authorizeConditional()`: Used when the caller can optimize the evaluation of any conditional decisions. For example, a plugin backend may want to use conditions in a database query instead of evaluating each resource in memory. | ||
## 0.6.0-next.0 | ||
### Minor Changes | ||
- 8012ac46a0: Add `resourceType` property to `PermissionCondition` type to allow matching them with `ResourcePermission` instances. | ||
- c98d271466: Refactor api types into more specific, decoupled names. | ||
- **BREAKING:** | ||
- Renamed `AuthorizeDecision` to `EvaluatePermissionResponse` | ||
- Renamed `AuthorizeQuery` to `EvaluatePermissionRequest` | ||
- Renamed `AuthorizeRequest` to `EvaluatePermissionRequestBatch` | ||
- Renamed `AuthorizeResponse` to `EvaluatePermissionResponseBatch` | ||
- Renamed `Identified` to `IdentifiedPermissionMessage` | ||
- Add `PermissionMessageBatch` helper type | ||
- Add `ConditionalPolicyDecision`, `DefinitivePolicyDecision`, and `PolicyDecision` types from `@backstage/plugin-permission-node` | ||
### Patch Changes | ||
- 8012ac46a0: Add `isPermission` helper method. | ||
- 95284162d6: - Add more specific `Permission` types. | ||
- Add `createPermission` helper to infer the appropriate type for some permission input. | ||
- Add `isResourcePermission` helper to refine Permissions to ResourcePermissions. | ||
## 0.5.3 | ||
### Patch Changes | ||
- f24ef7864e: Minor typo fixes | ||
- Updated dependencies | ||
- @backstage/config@1.0.0 | ||
- @backstage/errors@1.0.0 | ||
## 0.5.2 | ||
### Patch Changes | ||
- 79b9d8a861: Add api doc comments to `Permission` type properties. | ||
## 0.5.1 | ||
### Patch Changes | ||
- Fix for the previous release with missing type declarations. | ||
- Updated dependencies | ||
- @backstage/config@0.1.15 | ||
- @backstage/errors@0.2.2 | ||
## 0.5.0 | ||
### Minor Changes | ||
- 8c646beb24: **BREAKING** `PermissionCriteria` now requires at least one condition in `anyOf` and `allOf` arrays. This addresses some ambiguous behavior outlined in #9280. | ||
### Patch Changes | ||
- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 | ||
- c77c5c7eb6: Added `backstage.role` to `package.json` | ||
- Updated dependencies | ||
- @backstage/errors@0.2.1 | ||
- @backstage/config@0.1.14 | ||
## 0.4.0 | ||
### Minor Changes | ||
- 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. | ||
**BREAKING**: PermissionClient has been updated to use the new request and response format in the latest version of @backstage/permission-backend. | ||
### Patch Changes | ||
- Updated dependencies | ||
- @backstage/config@0.1.13 | ||
## 0.4.0-next.0 | ||
### Minor Changes | ||
- 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. | ||
**BREAKING**: PermissionClient has been updated to use the new request and response format in the latest version of @backstage/permission-backend. | ||
### Patch Changes | ||
- Updated dependencies | ||
- @backstage/config@0.1.13-next.0 | ||
## 0.3.1 | ||
### Patch Changes | ||
- Updated dependencies | ||
- @backstage/config@0.1.12 | ||
- @backstage/errors@0.2.0 | ||
## 0.3.0 | ||
@@ -4,0 +227,0 @@ |
@@ -40,2 +40,11 @@ 'use strict'; | ||
function isPermission(permission, comparedPermission) { | ||
return permission.name === comparedPermission.name; | ||
} | ||
function isResourcePermission(permission, resourceType) { | ||
if (!("resourceType" in permission)) { | ||
return false; | ||
} | ||
return !resourceType || permission.resourceType === resourceType; | ||
} | ||
function isCreatePermission(permission) { | ||
@@ -53,15 +62,71 @@ return permission.attributes.action === "create"; | ||
} | ||
function toPermissionEvaluator(permissionAuthorizer) { | ||
return { | ||
authorize: async (requests, options) => { | ||
const response = await permissionAuthorizer.authorize(requests, options); | ||
return response; | ||
}, | ||
authorizeConditional(requests, options) { | ||
const parsedRequests = requests; | ||
return permissionAuthorizer.authorize(parsedRequests, options); | ||
} | ||
}; | ||
} | ||
const permissionCriteriaSchema = zod.z.lazy(() => zod.z.object({ | ||
rule: zod.z.string(), | ||
params: zod.z.array(zod.z.unknown()) | ||
}).or(zod.z.object({ anyOf: zod.z.array(permissionCriteriaSchema) })).or(zod.z.object({ allOf: zod.z.array(permissionCriteriaSchema) })).or(zod.z.object({ not: permissionCriteriaSchema }))); | ||
const responseSchema = zod.z.array(zod.z.object({ | ||
id: zod.z.string(), | ||
function createPermission({ | ||
name, | ||
attributes, | ||
resourceType | ||
}) { | ||
if (resourceType) { | ||
return { | ||
type: "resource", | ||
name, | ||
attributes, | ||
resourceType | ||
}; | ||
} | ||
return { | ||
type: "basic", | ||
name, | ||
attributes | ||
}; | ||
} | ||
const permissionCriteriaSchema = zod.z.lazy( | ||
() => zod.z.object({ | ||
rule: zod.z.string(), | ||
resourceType: zod.z.string(), | ||
params: zod.z.array(zod.z.unknown()) | ||
}).or(zod.z.object({ anyOf: zod.z.array(permissionCriteriaSchema).nonempty() })).or(zod.z.object({ allOf: zod.z.array(permissionCriteriaSchema).nonempty() })).or(zod.z.object({ not: permissionCriteriaSchema })) | ||
); | ||
const authorizePermissionResponseSchema = zod.z.object({ | ||
result: zod.z.literal(AuthorizeResult.ALLOW).or(zod.z.literal(AuthorizeResult.DENY)) | ||
}).or(zod.z.object({ | ||
id: zod.z.string(), | ||
result: zod.z.literal(AuthorizeResult.CONDITIONAL), | ||
conditions: permissionCriteriaSchema | ||
}))); | ||
}); | ||
const queryPermissionResponseSchema = zod.z.union([ | ||
zod.z.object({ | ||
result: zod.z.literal(AuthorizeResult.ALLOW).or(zod.z.literal(AuthorizeResult.DENY)) | ||
}), | ||
zod.z.object({ | ||
result: zod.z.literal(AuthorizeResult.CONDITIONAL), | ||
pluginId: zod.z.string(), | ||
resourceType: zod.z.string(), | ||
conditions: permissionCriteriaSchema | ||
}) | ||
]); | ||
const responseSchema = (itemSchema, ids) => zod.z.object({ | ||
items: zod.z.array( | ||
zod.z.intersection( | ||
zod.z.object({ | ||
id: zod.z.string() | ||
}), | ||
itemSchema | ||
) | ||
).refine( | ||
(items) => items.length === ids.size && items.every(({ id }) => ids.has(id)), | ||
{ | ||
message: "Items in response do not match request" | ||
} | ||
) | ||
}); | ||
class PermissionClient { | ||
@@ -74,13 +139,25 @@ constructor(options) { | ||
async authorize(requests, options) { | ||
return this.makeRequest( | ||
requests, | ||
authorizePermissionResponseSchema, | ||
options | ||
); | ||
} | ||
async authorizeConditional(queries, options) { | ||
return this.makeRequest(queries, queryPermissionResponseSchema, options); | ||
} | ||
async makeRequest(queries, itemSchema, options) { | ||
if (!this.enabled) { | ||
return requests.map((_) => ({ result: AuthorizeResult.ALLOW })); | ||
return queries.map((_) => ({ result: AuthorizeResult.ALLOW })); | ||
} | ||
const identifiedRequests = requests.map((request) => ({ | ||
id: uuid__namespace.v4(), | ||
...request | ||
})); | ||
const request = { | ||
items: queries.map((query) => ({ | ||
id: uuid__namespace.v4(), | ||
...query | ||
})) | ||
}; | ||
const permissionApi = await this.discovery.getBaseUrl("permission"); | ||
const response = await fetch__default["default"](`${permissionApi}/authorize`, { | ||
method: "POST", | ||
body: JSON.stringify(identifiedRequests), | ||
body: JSON.stringify(request), | ||
headers: { | ||
@@ -94,9 +171,12 @@ ...this.getAuthorizationHeader(options == null ? void 0 : options.token), | ||
} | ||
const identifiedResponses = await response.json(); | ||
this.assertValidResponses(identifiedRequests, identifiedResponses); | ||
const responsesById = identifiedResponses.reduce((acc, r) => { | ||
const responseBody = await response.json(); | ||
const parsedResponse = responseSchema( | ||
itemSchema, | ||
new Set(request.items.map(({ id }) => id)) | ||
).parse(responseBody); | ||
const responsesById = parsedResponse.items.reduce((acc, r) => { | ||
acc[r.id] = r; | ||
return acc; | ||
}, {}); | ||
return identifiedRequests.map((request) => responsesById[request.id]); | ||
return request.items.map((query) => responsesById[query.id]); | ||
} | ||
@@ -106,10 +186,2 @@ getAuthorizationHeader(token) { | ||
} | ||
assertValidResponses(requests, json) { | ||
const authorizedResponses = responseSchema.parse(json); | ||
const responseIds = authorizedResponses.map((r) => r.id); | ||
const hasAllRequestIds = requests.every((r) => responseIds.includes(r.id)); | ||
if (!hasAllRequestIds) { | ||
throw new Error("Unexpected authorization response from permission-backend"); | ||
} | ||
} | ||
} | ||
@@ -119,6 +191,10 @@ | ||
exports.PermissionClient = PermissionClient; | ||
exports.createPermission = createPermission; | ||
exports.isCreatePermission = isCreatePermission; | ||
exports.isDeletePermission = isDeletePermission; | ||
exports.isPermission = isPermission; | ||
exports.isReadPermission = isReadPermission; | ||
exports.isResourcePermission = isResourcePermission; | ||
exports.isUpdatePermission = isUpdatePermission; | ||
exports.toPermissionEvaluator = toPermissionEvaluator; | ||
//# sourceMappingURL=index.cjs.js.map |
@@ -12,4 +12,30 @@ import { Config } from '@backstage/config'; | ||
/** | ||
* Generic type for building {@link Permission} types. | ||
* @public | ||
*/ | ||
declare type PermissionBase<TType extends string, TFields extends object> = { | ||
/** | ||
* The name of the permission. | ||
*/ | ||
name: string; | ||
/** | ||
* {@link PermissionAttributes} which describe characteristics of the permission, to help | ||
* policy authors make consistent decisions for similar permissions without referring to them | ||
* all by name. | ||
*/ | ||
attributes: PermissionAttributes; | ||
} & { | ||
/** | ||
* String value indicating the type of the permission (e.g. 'basic', | ||
* 'resource'). The allowed authorization flows in the permission system | ||
* depend on the type. For example, a `resourceRef` should only be provided | ||
* when authorizing permissions of type 'resource'. | ||
*/ | ||
type: TType; | ||
} & TFields; | ||
/** | ||
* A permission that can be checked through authorization. | ||
* | ||
* @remarks | ||
* | ||
* Permissions are the "what" part of authorization, the action to be performed. This may be reading | ||
@@ -23,13 +49,27 @@ * an entity from the catalog, executing a software template, or any other action a plugin author | ||
*/ | ||
declare type Permission = { | ||
name: string; | ||
attributes: PermissionAttributes; | ||
resourceType?: string; | ||
}; | ||
declare type Permission = BasicPermission | ResourcePermission; | ||
/** | ||
* A standard {@link Permission} with no additional capabilities or restrictions. | ||
* @public | ||
*/ | ||
declare type BasicPermission = PermissionBase<'basic', {}>; | ||
/** | ||
* ResourcePermissions are {@link Permission}s that can be authorized based on | ||
* characteristics of a resource such a catalog entity. | ||
* @public | ||
*/ | ||
declare type ResourcePermission<TResourceType extends string = string> = PermissionBase<'resource', { | ||
/** | ||
* Denotes the type of the resource whose resourceRef should be passed when | ||
* authorizing. | ||
*/ | ||
resourceType: TResourceType; | ||
}>; | ||
/** | ||
* A client interacting with the permission backend can implement this authorizer interface. | ||
* @public | ||
* @deprecated Use {@link @backstage/plugin-permission-common#PermissionEvaluator} instead | ||
*/ | ||
interface PermissionAuthorizer { | ||
authorize(requests: AuthorizeRequest[], options?: AuthorizeRequestOptions): Promise<AuthorizeResponse[]>; | ||
authorize(requests: EvaluatePermissionRequest[], options?: AuthorizeRequestOptions): Promise<EvaluatePermissionResponse[]>; | ||
} | ||
@@ -49,6 +89,13 @@ /** | ||
*/ | ||
declare type Identified<T> = T & { | ||
declare type IdentifiedPermissionMessage<T> = T & { | ||
id: string; | ||
}; | ||
/** | ||
* A batch of request or response items. | ||
* @public | ||
*/ | ||
declare type PermissionMessageBatch<T> = { | ||
items: IdentifiedPermissionMessage<T>[]; | ||
}; | ||
/** | ||
* The result of an authorization request. | ||
@@ -72,10 +119,37 @@ * @public | ||
/** | ||
* An authorization request for {@link PermissionClient#authorize}. | ||
* A definitive decision returned by the {@link @backstage/plugin-permission-node#PermissionPolicy}. | ||
* | ||
* @remarks | ||
* | ||
* This indicates that the policy unconditionally allows (or denies) the request. | ||
* | ||
* @public | ||
*/ | ||
declare type AuthorizeRequest = { | ||
permission: Permission; | ||
resourceRef?: string; | ||
declare type DefinitivePolicyDecision = { | ||
result: AuthorizeResult.ALLOW | AuthorizeResult.DENY; | ||
}; | ||
/** | ||
* A conditional decision returned by the {@link @backstage/plugin-permission-node#PermissionPolicy}. | ||
* | ||
* @remarks | ||
* | ||
* This indicates that the policy allows authorization for the request, given that the returned | ||
* conditions hold when evaluated. The conditions will be evaluated by the corresponding plugin | ||
* which knows about the referenced permission rules. | ||
* | ||
* @public | ||
*/ | ||
declare type ConditionalPolicyDecision = { | ||
result: AuthorizeResult.CONDITIONAL; | ||
pluginId: string; | ||
resourceType: string; | ||
conditions: PermissionCriteria<PermissionCondition>; | ||
}; | ||
/** | ||
* A decision returned by the {@link @backstage/plugin-permission-node#PermissionPolicy}. | ||
* | ||
* @public | ||
*/ | ||
declare type PolicyDecision = DefinitivePolicyDecision | ConditionalPolicyDecision; | ||
/** | ||
* A condition returned with a CONDITIONAL authorization response. | ||
@@ -88,3 +162,4 @@ * | ||
*/ | ||
declare type PermissionCondition<TParams extends unknown[] = unknown[]> = { | ||
declare type PermissionCondition<TResourceType extends string = string, TParams extends unknown[] = unknown[]> = { | ||
resourceType: TResourceType; | ||
rule: string; | ||
@@ -94,22 +169,120 @@ params: TParams; | ||
/** | ||
* Composes several {@link PermissionCondition}s as criteria with a nested AND/OR structure. | ||
* Utility type to represent an array with 1 or more elements. | ||
* @ignore | ||
*/ | ||
declare type NonEmptyArray<T> = [T, ...T[]]; | ||
/** | ||
* Represents a logical AND for the provided criteria. | ||
* @public | ||
*/ | ||
declare type PermissionCriteria<TQuery> = { | ||
allOf: PermissionCriteria<TQuery>[]; | ||
} | { | ||
anyOf: PermissionCriteria<TQuery>[]; | ||
} | { | ||
declare type AllOfCriteria<TQuery> = { | ||
allOf: NonEmptyArray<PermissionCriteria<TQuery>>; | ||
}; | ||
/** | ||
* Represents a logical OR for the provided criteria. | ||
* @public | ||
*/ | ||
declare type AnyOfCriteria<TQuery> = { | ||
anyOf: NonEmptyArray<PermissionCriteria<TQuery>>; | ||
}; | ||
/** | ||
* Represents a negation of the provided criteria. | ||
* @public | ||
*/ | ||
declare type NotCriteria<TQuery> = { | ||
not: PermissionCriteria<TQuery>; | ||
} | TQuery; | ||
}; | ||
/** | ||
* An authorization response from {@link PermissionClient#authorize}. | ||
* Composes several {@link PermissionCondition}s as criteria with a nested AND/OR structure. | ||
* @public | ||
*/ | ||
declare type AuthorizeResponse = { | ||
result: AuthorizeResult.ALLOW | AuthorizeResult.DENY; | ||
declare type PermissionCriteria<TQuery> = AllOfCriteria<TQuery> | AnyOfCriteria<TQuery> | NotCriteria<TQuery> | TQuery; | ||
/** | ||
* An individual request sent to the permission backend. | ||
* @public | ||
*/ | ||
declare type EvaluatePermissionRequest = { | ||
permission: Permission; | ||
resourceRef?: string; | ||
}; | ||
/** | ||
* A batch of requests sent to the permission backend. | ||
* @public | ||
*/ | ||
declare type EvaluatePermissionRequestBatch = PermissionMessageBatch<EvaluatePermissionRequest>; | ||
/** | ||
* An individual response from the permission backend. | ||
* | ||
* @remarks | ||
* | ||
* This response type is an alias of {@link PolicyDecision} to maintain separation between the | ||
* {@link @backstage/plugin-permission-node#PermissionPolicy} interface and the permission backend | ||
* api. They may diverge at some point in the future. The response | ||
* | ||
* @public | ||
*/ | ||
declare type EvaluatePermissionResponse = PolicyDecision; | ||
/** | ||
* A batch of responses from the permission backend. | ||
* @public | ||
*/ | ||
declare type EvaluatePermissionResponseBatch = PermissionMessageBatch<EvaluatePermissionResponse>; | ||
/** | ||
* Request object for {@link PermissionEvaluator.authorize}. If a {@link ResourcePermission} | ||
* is provided, it must include a corresponding `resourceRef`. | ||
* @public | ||
*/ | ||
declare type AuthorizePermissionRequest = { | ||
permission: Exclude<Permission, ResourcePermission>; | ||
resourceRef?: never; | ||
} | { | ||
result: AuthorizeResult.CONDITIONAL; | ||
conditions: PermissionCriteria<PermissionCondition>; | ||
permission: ResourcePermission; | ||
resourceRef: string; | ||
}; | ||
/** | ||
* Response object for {@link PermissionEvaluator.authorize}. | ||
* @public | ||
*/ | ||
declare type AuthorizePermissionResponse = DefinitivePolicyDecision; | ||
/** | ||
* Request object for {@link PermissionEvaluator.authorizeConditional}. | ||
* @public | ||
*/ | ||
declare type QueryPermissionRequest = { | ||
permission: ResourcePermission; | ||
resourceRef?: never; | ||
}; | ||
/** | ||
* Response object for {@link PermissionEvaluator.authorizeConditional}. | ||
* @public | ||
*/ | ||
declare type QueryPermissionResponse = PolicyDecision; | ||
/** | ||
* A client interacting with the permission backend can implement this evaluator interface. | ||
* | ||
* @public | ||
*/ | ||
interface PermissionEvaluator { | ||
/** | ||
* Evaluates {@link Permission | Permissions} and returns a definitive decision. | ||
*/ | ||
authorize(requests: AuthorizePermissionRequest[], options?: EvaluatorRequestOptions): Promise<AuthorizePermissionResponse[]>; | ||
/** | ||
* Evaluates {@link ResourcePermission | ResourcePermissions} and returns both definitive and | ||
* conditional decisions, depending on the configured | ||
* {@link @backstage/plugin-permission-node#PermissionPolicy}. This method is useful when the | ||
* caller needs more control over the processing of conditional decisions. For example, a plugin | ||
* backend may want to use {@link PermissionCriteria | conditions} in a database query instead of | ||
* evaluating each resource in memory. | ||
*/ | ||
authorizeConditional(requests: QueryPermissionRequest[], options?: EvaluatorRequestOptions): Promise<QueryPermissionResponse[]>; | ||
} | ||
/** | ||
* Options for {@link PermissionEvaluator} requests. | ||
* The Backstage identity token should be defined if available. | ||
* @public | ||
*/ | ||
declare type EvaluatorRequestOptions = { | ||
token?: string; | ||
}; | ||
@@ -126,2 +299,14 @@ /** | ||
/** | ||
* Check if the two parameters are equivalent permissions. | ||
* @public | ||
*/ | ||
declare function isPermission<T extends Permission>(permission: Permission, comparedPermission: T): permission is T; | ||
/** | ||
* Check if a given permission is a {@link ResourcePermission}. When | ||
* `resourceType` is supplied as the second parameter, also checks if | ||
* the permission has the specified resource type. | ||
* @public | ||
*/ | ||
declare function isResourcePermission<T extends string = string>(permission: Permission, resourceType?: T): permission is ResourcePermission<T>; | ||
/** | ||
* Check if a given permission is related to a create action. | ||
@@ -146,8 +331,35 @@ * @public | ||
declare function isDeletePermission(permission: Permission): boolean; | ||
/** | ||
* Convert {@link PermissionAuthorizer} to {@link PermissionEvaluator}. | ||
* | ||
* @public | ||
*/ | ||
declare function toPermissionEvaluator(permissionAuthorizer: PermissionAuthorizer): PermissionEvaluator; | ||
/** | ||
* Utility function for creating a valid {@link ResourcePermission}, inferring | ||
* the appropriate type and resource type parameter. | ||
* | ||
* @public | ||
*/ | ||
declare function createPermission<TResourceType extends string>(input: { | ||
name: string; | ||
attributes: PermissionAttributes; | ||
resourceType: TResourceType; | ||
}): ResourcePermission<TResourceType>; | ||
/** | ||
* Utility function for creating a valid {@link BasicPermission}. | ||
* | ||
* @public | ||
*/ | ||
declare function createPermission(input: { | ||
name: string; | ||
attributes: PermissionAttributes; | ||
}): BasicPermission; | ||
/** | ||
* An isomorphic client for requesting authorization for Backstage permissions. | ||
* @public | ||
*/ | ||
declare class PermissionClient implements PermissionAuthorizer { | ||
declare class PermissionClient implements PermissionEvaluator { | ||
private readonly enabled; | ||
@@ -160,22 +372,13 @@ private readonly discovery; | ||
/** | ||
* Request authorization from the permission-backend for the given set of permissions. | ||
* | ||
* Authorization requests check that a given Backstage user can perform a protected operation, | ||
* potentially for a specific resource (such as a catalog entity). The Backstage identity token | ||
* should be included in the `options` if available. | ||
* | ||
* Permissions can be imported from plugins exposing them, such as `catalogEntityReadPermission`. | ||
* | ||
* The response will be either ALLOW or DENY when either the permission has no resourceType, or a | ||
* resourceRef is provided in the request. For permissions with a resourceType, CONDITIONAL may be | ||
* returned if no resourceRef is provided in the request. Conditional responses are intended only | ||
* for backends which have access to the data source for permissioned resources, so that filters | ||
* can be applied when loading collections of resources. | ||
* @public | ||
* {@inheritdoc PermissionEvaluator.authorize} | ||
*/ | ||
authorize(requests: AuthorizeRequest[], options?: AuthorizeRequestOptions): Promise<AuthorizeResponse[]>; | ||
authorize(requests: AuthorizePermissionRequest[], options?: EvaluatorRequestOptions): Promise<AuthorizePermissionResponse[]>; | ||
/** | ||
* {@inheritdoc PermissionEvaluator.authorizeConditional} | ||
*/ | ||
authorizeConditional(queries: QueryPermissionRequest[], options?: EvaluatorRequestOptions): Promise<QueryPermissionResponse[]>; | ||
private makeRequest; | ||
private getAuthorizationHeader; | ||
private assertValidResponses; | ||
} | ||
export { AuthorizeRequest, AuthorizeRequestOptions, AuthorizeResponse, AuthorizeResult, DiscoveryApi, Identified, Permission, PermissionAttributes, PermissionAuthorizer, PermissionClient, PermissionCondition, PermissionCriteria, isCreatePermission, isDeletePermission, isReadPermission, isUpdatePermission }; | ||
export { AllOfCriteria, AnyOfCriteria, AuthorizePermissionRequest, AuthorizePermissionResponse, AuthorizeRequestOptions, AuthorizeResult, BasicPermission, ConditionalPolicyDecision, DefinitivePolicyDecision, DiscoveryApi, EvaluatePermissionRequest, EvaluatePermissionRequestBatch, EvaluatePermissionResponse, EvaluatePermissionResponseBatch, EvaluatorRequestOptions, IdentifiedPermissionMessage, NotCriteria, Permission, PermissionAttributes, PermissionAuthorizer, PermissionBase, PermissionClient, PermissionCondition, PermissionCriteria, PermissionEvaluator, PermissionMessageBatch, PolicyDecision, QueryPermissionRequest, QueryPermissionResponse, ResourcePermission, createPermission, isCreatePermission, isDeletePermission, isPermission, isReadPermission, isResourcePermission, isUpdatePermission, toPermissionEvaluator }; |
@@ -13,2 +13,11 @@ import { ResponseError } from '@backstage/errors'; | ||
function isPermission(permission, comparedPermission) { | ||
return permission.name === comparedPermission.name; | ||
} | ||
function isResourcePermission(permission, resourceType) { | ||
if (!("resourceType" in permission)) { | ||
return false; | ||
} | ||
return !resourceType || permission.resourceType === resourceType; | ||
} | ||
function isCreatePermission(permission) { | ||
@@ -26,15 +35,71 @@ return permission.attributes.action === "create"; | ||
} | ||
function toPermissionEvaluator(permissionAuthorizer) { | ||
return { | ||
authorize: async (requests, options) => { | ||
const response = await permissionAuthorizer.authorize(requests, options); | ||
return response; | ||
}, | ||
authorizeConditional(requests, options) { | ||
const parsedRequests = requests; | ||
return permissionAuthorizer.authorize(parsedRequests, options); | ||
} | ||
}; | ||
} | ||
const permissionCriteriaSchema = z.lazy(() => z.object({ | ||
rule: z.string(), | ||
params: z.array(z.unknown()) | ||
}).or(z.object({ anyOf: z.array(permissionCriteriaSchema) })).or(z.object({ allOf: z.array(permissionCriteriaSchema) })).or(z.object({ not: permissionCriteriaSchema }))); | ||
const responseSchema = z.array(z.object({ | ||
id: z.string(), | ||
function createPermission({ | ||
name, | ||
attributes, | ||
resourceType | ||
}) { | ||
if (resourceType) { | ||
return { | ||
type: "resource", | ||
name, | ||
attributes, | ||
resourceType | ||
}; | ||
} | ||
return { | ||
type: "basic", | ||
name, | ||
attributes | ||
}; | ||
} | ||
const permissionCriteriaSchema = z.lazy( | ||
() => z.object({ | ||
rule: z.string(), | ||
resourceType: z.string(), | ||
params: z.array(z.unknown()) | ||
}).or(z.object({ anyOf: z.array(permissionCriteriaSchema).nonempty() })).or(z.object({ allOf: z.array(permissionCriteriaSchema).nonempty() })).or(z.object({ not: permissionCriteriaSchema })) | ||
); | ||
const authorizePermissionResponseSchema = z.object({ | ||
result: z.literal(AuthorizeResult.ALLOW).or(z.literal(AuthorizeResult.DENY)) | ||
}).or(z.object({ | ||
id: z.string(), | ||
result: z.literal(AuthorizeResult.CONDITIONAL), | ||
conditions: permissionCriteriaSchema | ||
}))); | ||
}); | ||
const queryPermissionResponseSchema = z.union([ | ||
z.object({ | ||
result: z.literal(AuthorizeResult.ALLOW).or(z.literal(AuthorizeResult.DENY)) | ||
}), | ||
z.object({ | ||
result: z.literal(AuthorizeResult.CONDITIONAL), | ||
pluginId: z.string(), | ||
resourceType: z.string(), | ||
conditions: permissionCriteriaSchema | ||
}) | ||
]); | ||
const responseSchema = (itemSchema, ids) => z.object({ | ||
items: z.array( | ||
z.intersection( | ||
z.object({ | ||
id: z.string() | ||
}), | ||
itemSchema | ||
) | ||
).refine( | ||
(items) => items.length === ids.size && items.every(({ id }) => ids.has(id)), | ||
{ | ||
message: "Items in response do not match request" | ||
} | ||
) | ||
}); | ||
class PermissionClient { | ||
@@ -47,13 +112,25 @@ constructor(options) { | ||
async authorize(requests, options) { | ||
return this.makeRequest( | ||
requests, | ||
authorizePermissionResponseSchema, | ||
options | ||
); | ||
} | ||
async authorizeConditional(queries, options) { | ||
return this.makeRequest(queries, queryPermissionResponseSchema, options); | ||
} | ||
async makeRequest(queries, itemSchema, options) { | ||
if (!this.enabled) { | ||
return requests.map((_) => ({ result: AuthorizeResult.ALLOW })); | ||
return queries.map((_) => ({ result: AuthorizeResult.ALLOW })); | ||
} | ||
const identifiedRequests = requests.map((request) => ({ | ||
id: uuid.v4(), | ||
...request | ||
})); | ||
const request = { | ||
items: queries.map((query) => ({ | ||
id: uuid.v4(), | ||
...query | ||
})) | ||
}; | ||
const permissionApi = await this.discovery.getBaseUrl("permission"); | ||
const response = await fetch(`${permissionApi}/authorize`, { | ||
method: "POST", | ||
body: JSON.stringify(identifiedRequests), | ||
body: JSON.stringify(request), | ||
headers: { | ||
@@ -67,9 +144,12 @@ ...this.getAuthorizationHeader(options == null ? void 0 : options.token), | ||
} | ||
const identifiedResponses = await response.json(); | ||
this.assertValidResponses(identifiedRequests, identifiedResponses); | ||
const responsesById = identifiedResponses.reduce((acc, r) => { | ||
const responseBody = await response.json(); | ||
const parsedResponse = responseSchema( | ||
itemSchema, | ||
new Set(request.items.map(({ id }) => id)) | ||
).parse(responseBody); | ||
const responsesById = parsedResponse.items.reduce((acc, r) => { | ||
acc[r.id] = r; | ||
return acc; | ||
}, {}); | ||
return identifiedRequests.map((request) => responsesById[request.id]); | ||
return request.items.map((query) => responsesById[query.id]); | ||
} | ||
@@ -79,13 +159,5 @@ getAuthorizationHeader(token) { | ||
} | ||
assertValidResponses(requests, json) { | ||
const authorizedResponses = responseSchema.parse(json); | ||
const responseIds = authorizedResponses.map((r) => r.id); | ||
const hasAllRequestIds = requests.every((r) => responseIds.includes(r.id)); | ||
if (!hasAllRequestIds) { | ||
throw new Error("Unexpected authorization response from permission-backend"); | ||
} | ||
} | ||
} | ||
export { AuthorizeResult, PermissionClient, isCreatePermission, isDeletePermission, isReadPermission, isUpdatePermission }; | ||
export { AuthorizeResult, PermissionClient, createPermission, isCreatePermission, isDeletePermission, isPermission, isReadPermission, isResourcePermission, isUpdatePermission, toPermissionEvaluator }; | ||
//# sourceMappingURL=index.esm.js.map |
{ | ||
"name": "@backstage/plugin-permission-common", | ||
"description": "Isomorphic types and client for Backstage permissions and authorization", | ||
"version": "0.0.0-nightly-20220923026", | ||
"version": "0.0.0-nightly-20220923030237", | ||
"main": "dist/index.cjs.js", | ||
@@ -13,2 +13,5 @@ "types": "dist/index.d.ts", | ||
}, | ||
"backstage": { | ||
"role": "common-library" | ||
}, | ||
"homepage": "https://backstage.io", | ||
@@ -31,8 +34,8 @@ "repository": { | ||
"scripts": { | ||
"build": "backstage-cli build", | ||
"lint": "backstage-cli lint", | ||
"test": "backstage-cli test", | ||
"prepack": "backstage-cli prepack", | ||
"postpack": "backstage-cli postpack", | ||
"clean": "backstage-cli clean" | ||
"build": "backstage-cli package build", | ||
"lint": "backstage-cli package lint", | ||
"test": "backstage-cli package test", | ||
"prepack": "backstage-cli package prepack", | ||
"postpack": "backstage-cli package postpack", | ||
"clean": "backstage-cli package clean" | ||
}, | ||
@@ -43,5 +46,5 @@ "bugs": { | ||
"dependencies": { | ||
"@backstage/config": "^0.0.0-nightly-20220923026", | ||
"@backstage/errors": "^0.0.0-nightly-20220923026", | ||
"cross-fetch": "^3.0.6", | ||
"@backstage/config": "^0.0.0-nightly-20220923030237", | ||
"@backstage/errors": "^0.0.0-nightly-20220923030237", | ||
"cross-fetch": "^3.1.5", | ||
"uuid": "^8.0.0", | ||
@@ -51,7 +54,6 @@ "zod": "^3.11.6" | ||
"devDependencies": { | ||
"@backstage/cli": "^0.0.0-nightly-20220923026", | ||
"@types/jest": "^26.0.7", | ||
"msw": "^0.35.0" | ||
"@backstage/cli": "^0.0.0-nightly-20220923030237", | ||
"msw": "^0.47.0" | ||
}, | ||
"module": "dist/index.esm.js" | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
80169
2
195853
734
3
28
1