diff --git a/.changeset/tidy-forks-pay.md b/.changeset/tidy-forks-pay.md new file mode 100644 index 0000000000..ec9a99641b --- /dev/null +++ b/.changeset/tidy-forks-pay.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-permission-node': patch +--- + +Added a new `createPermissionResourceRef` utility that encapsulates the constants and types related to a permission resource types. The `createConditionExports` and `createPermissionRule` functions have also been adapted to accept these references as arguments, deprecating their older counterparts. diff --git a/plugins/permission-node/report.api.md b/plugins/permission-node/report.api.md index a55cbbf9c5..2bbc548202 100644 --- a/plugins/permission-node/report.api.md +++ b/plugins/permission-node/report.api.md @@ -83,7 +83,23 @@ export const createConditionAuthorizer: ( ) => (decision: PolicyDecision, resource: TResource | undefined) => boolean; // @public -export const createConditionExports: < +export function createConditionExports< + TResourceType extends string, + TResource, + TRules extends Record>, +>(options: { + resourceRef: PermissionResourceRef; + rules: TRules; +}): { + conditions: Conditions; + createConditionalDecision: ( + permission: ResourcePermission, + conditions: PermissionCriteria>, + ) => ConditionalPolicyDecision; +}; + +// @public @deprecated (undocumented) +export function createConditionExports< TResourceType extends string, TResource, TRules extends Record>, @@ -91,7 +107,7 @@ export const createConditionExports: < pluginId: string; resourceType: TResourceType; rules: TRules; -}) => { +}): { conditions: Conditions; createConditionalDecision: ( permission: ResourcePermission, @@ -164,15 +180,48 @@ export type CreatePermissionIntegrationRouterResourceOptions< ) => Promise>; }; +// @public (undocumented) +export function createPermissionResourceRef(): { + with(options: { + pluginId: TPluginId; + resourceType: TResourceType; + }): PermissionResourceRef; +}; + // @public -export const createPermissionRule: < +export function createPermissionRule< + TResource, + TQuery, + TResourceType extends string, + TParams extends PermissionRuleParams = undefined, +>( + rule: CreatePermissionRuleOptions, +): PermissionRule; + +// @public @deprecated +export function createPermissionRule< TResource, TQuery, TResourceType extends string, TParams extends PermissionRuleParams = undefined, >( rule: PermissionRule, -) => PermissionRule; +): PermissionRule; + +// @public (undocumented) +export type CreatePermissionRuleOptions< + TResource, + TQuery, + TResourceType extends string, + TParams extends PermissionRuleParams = PermissionRuleParams, +> = { + name: string; + description: string; + resourceRef: PermissionResourceRef; + paramsSchema?: z.ZodSchema; + apply(resource: TResource, params: NoInfer_2): boolean; + toQuery(params: NoInfer_2): PermissionCriteria; +}; // @public export const isAndCriteria: ( @@ -189,7 +238,7 @@ export const isOrCriteria: ( criteria: PermissionCriteria, ) => criteria is AnyOfCriteria; -// @public +// @public @deprecated export const makeCreatePermissionRule: < TResource, TQuery, @@ -253,6 +302,20 @@ export interface PermissionPolicy { handle(request: PolicyQuery, user?: PolicyQueryUser): Promise; } +// @public (undocumented) +export type PermissionResourceRef< + TResource = unknown, + TQuery = unknown, + TResourceType extends string = string, + TPluginId extends string = string, +> = { + readonly $$type: '@backstage/PermissionResourceRef'; + readonly pluginId: TPluginId; + readonly resourceType: TResourceType; + readonly TQuery: TQuery; + readonly TResource: TResource; +}; + // @public export type PermissionRule< TResource, diff --git a/plugins/permission-node/src/integration/createConditionExports.ts b/plugins/permission-node/src/integration/createConditionExports.ts index 1c82434add..768e009ca4 100644 --- a/plugins/permission-node/src/integration/createConditionExports.ts +++ b/plugins/permission-node/src/integration/createConditionExports.ts @@ -23,6 +23,7 @@ import { } from '@backstage/plugin-permission-common'; import { PermissionRule } from '../types'; import { createConditionFactory } from './createConditionFactory'; +import { PermissionResourceRef } from './createPermissionResourceRef'; /** * A utility type for mapping a single {@link PermissionRule} to its @@ -73,7 +74,25 @@ export type Conditions< * * @public */ -export const createConditionExports = < +export function createConditionExports< + TResourceType extends string, + TResource, + TRules extends Record>, +>(options: { + resourceRef: PermissionResourceRef; + rules: TRules; +}): { + conditions: Conditions; + createConditionalDecision: ( + permission: ResourcePermission, + conditions: PermissionCriteria>, + ) => ConditionalPolicyDecision; +}; +/** + * @public + * @deprecated Use the version of `createConditionExports` that accepts a `resourceRef` option instead. + */ +export function createConditionExports< TResourceType extends string, TResource, TRules extends Record>, @@ -87,8 +106,32 @@ export const createConditionExports = < permission: ResourcePermission, conditions: PermissionCriteria>, ) => ConditionalPolicyDecision; -} => { - const { pluginId, resourceType, rules } = options; +}; +export function createConditionExports< + TResourceType extends string, + TResource, + TRules extends Record>, +>( + options: + | { + resourceRef: PermissionResourceRef; + rules: TRules; + } + | { + pluginId: string; + resourceType: TResourceType; + rules: TRules; + }, +): { + conditions: Conditions; + createConditionalDecision: ( + permission: ResourcePermission, + conditions: PermissionCriteria>, + ) => ConditionalPolicyDecision; +} { + const { rules } = options; + const { pluginId, resourceType } = + 'resourceRef' in options ? options.resourceRef : options; return { conditions: Object.entries(rules).reduce( @@ -108,4 +151,4 @@ export const createConditionExports = < conditions, }), }; -}; +} diff --git a/plugins/permission-node/src/integration/createConditionFactory.ts b/plugins/permission-node/src/integration/createConditionFactory.ts index 7f8cb1ced8..e2de9dd90d 100644 --- a/plugins/permission-node/src/integration/createConditionFactory.ts +++ b/plugins/permission-node/src/integration/createConditionFactory.ts @@ -29,7 +29,7 @@ import { PermissionRule } from '../types'; * The rule itself defines _how_ to check a given resource, whereas a condition also includes _what_ * to verify. * - * Plugin authors should generally use the {@link createConditionExports} in order to efficiently + * Plugin authors should generally use the {@link (createConditionExports:1)} in order to efficiently * create multiple condition factories. This helper should generally only be used to construct * condition factories for third-party rules that aren't part of the backend plugin with which * they're intended to integrate. diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts index dbd7e45c74..37973176ed 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts @@ -39,6 +39,7 @@ import { isOrCriteria, } from './util'; import { NotImplementedError } from '@backstage/errors'; +import { PermissionResourceRef } from './createPermissionResourceRef'; const permissionCriteriaSchema: z.ZodSchema< PermissionCriteria @@ -163,10 +164,22 @@ const applyConditions = ( * * @public */ -export const createConditionAuthorizer = ( +export function createConditionAuthorizer( + permissionRuleAccessor: PermissionRuleAccessor, +): (decision: PolicyDecision, resource: TResource | undefined) => boolean; +/** + * @public + * @deprecated Use the version of `createConditionAuthorizer` that accepts a `PermissionRuleAccessor` instead. + */ +export function createConditionAuthorizer( rules: PermissionRule[], -) => { - const getRule = createGetRule(rules); +): (decision: PolicyDecision, resource: TResource | undefined) => boolean; +export function createConditionAuthorizer( + rules: + | PermissionRule[] + | PermissionRuleAccessor, +): (decision: PolicyDecision, resource: TResource | undefined) => boolean { + const getRule = typeof rules === 'function' ? rules : createGetRule(rules); return ( decision: PolicyDecision, @@ -178,7 +191,7 @@ export const createConditionAuthorizer = ( return decision.result === AuthorizeResult.ALLOW; }; -}; +} /** * Options for creating a permission integration router specific diff --git a/plugins/permission-node/src/integration/createPermissionResourceRef.ts b/plugins/permission-node/src/integration/createPermissionResourceRef.ts new file mode 100644 index 0000000000..1ef7dd9d29 --- /dev/null +++ b/plugins/permission-node/src/integration/createPermissionResourceRef.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @public + */ +export type PermissionResourceRef< + TResource = unknown, + TQuery = unknown, + TResourceType extends string = string, + TPluginId extends string = string, +> = { + readonly $$type: '@backstage/PermissionResourceRef'; + readonly pluginId: TPluginId; + readonly resourceType: TResourceType; + readonly TQuery: TQuery; + readonly TResource: TResource; +}; + +/** + * @public + */ +export function createPermissionResourceRef(): { + with(options: { + pluginId: TPluginId; + resourceType: TResourceType; + }): PermissionResourceRef; +} { + return { + with(options: { + pluginId: TPluginId; + resourceType: TResourceType; + }): PermissionResourceRef { + return { + $$type: '@backstage/PermissionResourceRef', + pluginId: options.pluginId, + resourceType: options.resourceType, + TQuery: null as TQuery, + TResource: null as TResource, + }; + }, + }; +} diff --git a/plugins/permission-node/src/integration/createPermissionRule.ts b/plugins/permission-node/src/integration/createPermissionRule.ts index 44187e58a6..2e9833c32e 100644 --- a/plugins/permission-node/src/integration/createPermissionRule.ts +++ b/plugins/permission-node/src/integration/createPermissionRule.ts @@ -14,22 +14,91 @@ * limitations under the License. */ -import { PermissionRuleParams } from '@backstage/plugin-permission-common'; +import { + PermissionCriteria, + PermissionRuleParams, +} from '@backstage/plugin-permission-common'; import { PermissionRule } from '../types'; +import { z } from 'zod'; +import { PermissionResourceRef } from './createPermissionResourceRef'; +import { NoInfer } from './util'; /** - * Helper function to ensure that {@link PermissionRule} definitions are typed correctly. + * @public + */ +export type CreatePermissionRuleOptions< + TResource, + TQuery, + TResourceType extends string, + TParams extends PermissionRuleParams = PermissionRuleParams, +> = { + name: string; + description: string; + + resourceRef: PermissionResourceRef; + + /** + * A ZodSchema that reflects the structure of the parameters that are passed to + */ + paramsSchema?: z.ZodSchema; + + /** + * Apply this rule to a resource already loaded from a backing data source. The params are + * arguments supplied for the rule; for example, a rule could be `isOwner` with entityRefs as the + * params. + */ + apply(resource: TResource, params: NoInfer): boolean; + + /** + * Translate this rule to criteria suitable for use in querying a backing data store. The criteria + * can be used for loading a collection of resources efficiently with conditional criteria already + * applied. + */ + toQuery(params: NoInfer): PermissionCriteria; +}; + +/** + * Helper function to create a {@link PermissionRule} for a specific resource type using a {@link PermissionResourceRef}. * * @public */ -export const createPermissionRule = < +export function createPermissionRule< + TResource, + TQuery, + TResourceType extends string, + TParams extends PermissionRuleParams = undefined, +>( + rule: CreatePermissionRuleOptions, +): PermissionRule; +/** + * Helper function to ensure that {@link PermissionRule} definitions are typed correctly. + * + * @deprecated Use the version of `createPermissionRule` that accepts a `resourceRef` option instead. + * @public + */ +export function createPermissionRule< TResource, TQuery, TResourceType extends string, TParams extends PermissionRuleParams = undefined, >( rule: PermissionRule, -) => rule; +): PermissionRule; +export function createPermissionRule< + TResource, + TQuery, + TResourceType extends string, + TParams extends PermissionRuleParams = undefined, +>( + rule: + | PermissionRule + | CreatePermissionRuleOptions, +): PermissionRule { + if ('resourceRef' in rule) { + return { ...rule, resourceType: rule.resourceRef.resourceType }; + } + return rule; +} /** * Helper for making plugin-specific createPermissionRule functions, that have @@ -38,6 +107,7 @@ export const createPermissionRule = < * consistent types for the resource and query. * * @public + * @deprecated Use {@link (createPermissionRule:1)} directly instead with the resourceRef option. */ export const makeCreatePermissionRule = () => diff --git a/plugins/permission-node/src/integration/index.ts b/plugins/permission-node/src/integration/index.ts index 7702fea95b..0a783676db 100644 --- a/plugins/permission-node/src/integration/index.ts +++ b/plugins/permission-node/src/integration/index.ts @@ -19,4 +19,8 @@ export * from './createConditionExports'; export * from './createConditionTransformer'; export * from './createPermissionIntegrationRouter'; export * from './createPermissionRule'; +export { + createPermissionResourceRef, + type PermissionResourceRef, +} from './createPermissionResourceRef'; export { isAndCriteria, isOrCriteria, isNotCriteria } from './util';