@backstage/plugin-permission-common
Advanced tools
Comparing version 0.6.0-next.0 to 0.6.0-next.1
# @backstage/plugin-permission-common | ||
## 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 | ||
@@ -4,0 +13,0 @@ |
@@ -61,2 +61,14 @@ 'use strict'; | ||
} | ||
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); | ||
} | ||
}; | ||
} | ||
@@ -88,11 +100,22 @@ function createPermission({ | ||
}).strict().or(zod.z.object({ anyOf: zod.z.array(permissionCriteriaSchema).nonempty() }).strict()).or(zod.z.object({ allOf: zod.z.array(permissionCriteriaSchema).nonempty() }).strict()).or(zod.z.object({ not: permissionCriteriaSchema }).strict())); | ||
const responseSchema = zod.z.object({ | ||
items: zod.z.array(zod.z.object({ | ||
id: zod.z.string(), | ||
const authorizePermissionResponseSchema = zod.z.object({ | ||
result: zod.z.literal(AuthorizeResult.ALLOW).or(zod.z.literal(AuthorizeResult.DENY)) | ||
}); | ||
const queryPermissionResponseSchema = zod.z.union([ | ||
zod.z.object({ | ||
result: zod.z.literal(AuthorizeResult.ALLOW).or(zod.z.literal(AuthorizeResult.DENY)) | ||
}).or(zod.z.object({ | ||
id: zod.z.string(), | ||
}), | ||
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" | ||
}) | ||
}); | ||
@@ -105,3 +128,9 @@ class PermissionClient { | ||
} | ||
async authorize(queries, 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) { | ||
@@ -129,4 +158,4 @@ return queries.map((_) => ({ result: AuthorizeResult.ALLOW })); | ||
const responseBody = await response.json(); | ||
this.assertValidResponse(request, responseBody); | ||
const responsesById = responseBody.items.reduce((acc, r) => { | ||
const parsedResponse = responseSchema(itemSchema, new Set(request.items.map(({ id }) => id))).parse(responseBody); | ||
const responsesById = parsedResponse.items.reduce((acc, r) => { | ||
acc[r.id] = r; | ||
@@ -140,10 +169,2 @@ return acc; | ||
} | ||
assertValidResponse(request, json) { | ||
const authorizedResponses = responseSchema.parse(json); | ||
const responseIds = authorizedResponses.items.map((r) => r.id); | ||
const hasAllRequestIds = request.items.every((r) => responseIds.includes(r.id)); | ||
if (!hasAllRequestIds) { | ||
throw new Error("Unexpected authorization response from permission-backend"); | ||
} | ||
} | ||
} | ||
@@ -160,2 +181,3 @@ | ||
exports.isUpdatePermission = isUpdatePermission; | ||
exports.toPermissionEvaluator = toPermissionEvaluator; | ||
//# sourceMappingURL=index.cjs.js.map |
@@ -69,2 +69,3 @@ import { Config } from '@backstage/config'; | ||
* @public | ||
* @deprecated Use {@link @backstage/plugin-permission-common#PermissionEvaluator} instead | ||
*/ | ||
@@ -224,2 +225,60 @@ interface PermissionAuthorizer { | ||
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; | ||
} | { | ||
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; | ||
}; | ||
@@ -267,2 +326,8 @@ /** | ||
declare function isDeletePermission(permission: Permission): boolean; | ||
/** | ||
* Convert {@link PermissionAuthorizer} to {@link PermissionEvaluator}. | ||
* | ||
* @public | ||
*/ | ||
declare function toPermissionEvaluator(permissionAuthorizer: PermissionAuthorizer): PermissionEvaluator; | ||
@@ -294,3 +359,3 @@ /** | ||
*/ | ||
declare class PermissionClient implements PermissionAuthorizer { | ||
declare class PermissionClient implements PermissionEvaluator { | ||
private readonly enabled; | ||
@@ -303,22 +368,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(queries: EvaluatePermissionRequest[], options?: AuthorizeRequestOptions): Promise<EvaluatePermissionResponse[]>; | ||
authorize(requests: AuthorizePermissionRequest[], options?: EvaluatorRequestOptions): Promise<AuthorizePermissionResponse[]>; | ||
/** | ||
* {@inheritdoc PermissionEvaluator.authorizeConditional} | ||
*/ | ||
authorizeConditional(queries: QueryPermissionRequest[], options?: EvaluatorRequestOptions): Promise<QueryPermissionResponse[]>; | ||
private makeRequest; | ||
private getAuthorizationHeader; | ||
private assertValidResponse; | ||
} | ||
export { AllOfCriteria, AnyOfCriteria, AuthorizeRequestOptions, AuthorizeResult, BasicPermission, ConditionalPolicyDecision, DefinitivePolicyDecision, DiscoveryApi, EvaluatePermissionRequest, EvaluatePermissionRequestBatch, EvaluatePermissionResponse, EvaluatePermissionResponseBatch, IdentifiedPermissionMessage, NotCriteria, Permission, PermissionAttributes, PermissionAuthorizer, PermissionBase, PermissionClient, PermissionCondition, PermissionCriteria, PermissionMessageBatch, PolicyDecision, ResourcePermission, createPermission, isCreatePermission, isDeletePermission, isPermission, isReadPermission, isResourcePermission, 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 }; |
@@ -34,2 +34,14 @@ import { ResponseError } from '@backstage/errors'; | ||
} | ||
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); | ||
} | ||
}; | ||
} | ||
@@ -61,11 +73,22 @@ function createPermission({ | ||
}).strict().or(z.object({ anyOf: z.array(permissionCriteriaSchema).nonempty() }).strict()).or(z.object({ allOf: z.array(permissionCriteriaSchema).nonempty() }).strict()).or(z.object({ not: permissionCriteriaSchema }).strict())); | ||
const responseSchema = z.object({ | ||
items: z.array(z.object({ | ||
id: z.string(), | ||
const authorizePermissionResponseSchema = z.object({ | ||
result: z.literal(AuthorizeResult.ALLOW).or(z.literal(AuthorizeResult.DENY)) | ||
}); | ||
const queryPermissionResponseSchema = z.union([ | ||
z.object({ | ||
result: z.literal(AuthorizeResult.ALLOW).or(z.literal(AuthorizeResult.DENY)) | ||
}).or(z.object({ | ||
id: z.string(), | ||
}), | ||
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" | ||
}) | ||
}); | ||
@@ -78,3 +101,9 @@ class PermissionClient { | ||
} | ||
async authorize(queries, 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) { | ||
@@ -102,4 +131,4 @@ return queries.map((_) => ({ result: AuthorizeResult.ALLOW })); | ||
const responseBody = await response.json(); | ||
this.assertValidResponse(request, responseBody); | ||
const responsesById = responseBody.items.reduce((acc, r) => { | ||
const parsedResponse = responseSchema(itemSchema, new Set(request.items.map(({ id }) => id))).parse(responseBody); | ||
const responsesById = parsedResponse.items.reduce((acc, r) => { | ||
acc[r.id] = r; | ||
@@ -113,13 +142,5 @@ return acc; | ||
} | ||
assertValidResponse(request, json) { | ||
const authorizedResponses = responseSchema.parse(json); | ||
const responseIds = authorizedResponses.items.map((r) => r.id); | ||
const hasAllRequestIds = request.items.every((r) => responseIds.includes(r.id)); | ||
if (!hasAllRequestIds) { | ||
throw new Error("Unexpected authorization response from permission-backend"); | ||
} | ||
} | ||
} | ||
export { AuthorizeResult, PermissionClient, createPermission, isCreatePermission, isDeletePermission, isPermission, isReadPermission, isResourcePermission, 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.6.0-next.0", | ||
"version": "0.6.0-next.1", | ||
"main": "dist/index.cjs.js", | ||
@@ -51,8 +51,8 @@ "types": "dist/index.d.ts", | ||
"devDependencies": { | ||
"@backstage/cli": "^0.17.0-next.1", | ||
"@backstage/cli": "^0.17.0-next.3", | ||
"@types/jest": "^26.0.7", | ||
"msw": "^0.35.0" | ||
}, | ||
"gitHead": "57d12dcc35aeb6c33b09e51d1efc3408c574f109", | ||
"gitHead": "2eca57d93ef1081f4a76a19fc994a8e9e1a19e00", | ||
"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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
88228
700