diff --git a/.changeset/healthy-toes-laugh.md b/.changeset/healthy-toes-laugh.md new file mode 100644 index 0000000000..e0db38e5e3 --- /dev/null +++ b/.changeset/healthy-toes-laugh.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-permission-node': patch +--- + +Add helpers for creating PermissionRules with inferred types diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 6d7834001a..4b4036ee8a 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -306,12 +306,6 @@ export type CatalogEnvironment = { permissions: PermissionAuthorizer; }; -// @public -export type CatalogPermissionRule = PermissionRule< - Entity, - EntitiesSearchFilter ->; - // Warning: (ae-missing-release-tag) "CatalogProcessingEngine" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -528,6 +522,11 @@ export class CommonDatabase implements Database { ): Promise; } +// @public +export const createCatalogPermissionRule: ( + rule: PermissionRule, +) => PermissionRule; + // Warning: (ae-missing-release-tag) "CreateDatabaseOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @deprecated (undocumented) @@ -1311,7 +1310,13 @@ export class NextCatalogBuilder { addEntityPolicy(...policies: EntityPolicy[]): NextCatalogBuilder; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen addEntityProvider(...providers: EntityProvider[]): NextCatalogBuilder; - addPermissionRules(...permissionRules: CatalogPermissionRule[]): void; + addPermissionRules( + ...permissionRules: PermissionRule< + Entity, + EntitiesSearchFilter, + unknown[] + >[] + ): void; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen addProcessor(...processors: CatalogProcessor[]): NextCatalogBuilder; build(): Promise<{ @@ -1359,7 +1364,7 @@ export interface NextRouterOptions { // (undocumented) logger: Logger_2; // (undocumented) - permissionRules?: CatalogPermissionRule[]; + permissionRules?: PermissionRule[]; // (undocumented) refreshService?: RefreshService; } @@ -1394,12 +1399,28 @@ export function parseEntityYaml( // @public export const permissionRules: { - hasAnnotation: CatalogPermissionRule; - hasLabel: CatalogPermissionRule; - hasMetadata: CatalogPermissionRule; - hasSpec: CatalogPermissionRule; - isEntityKind: CatalogPermissionRule; - isEntityOwner: CatalogPermissionRule; + hasAnnotation: PermissionRule< + Entity, + EntitiesSearchFilter, + [annotation: string] + >; + hasLabel: PermissionRule; + hasMetadata: PermissionRule< + Entity, + EntitiesSearchFilter, + [key: string, value?: string | undefined] + >; + hasSpec: PermissionRule< + Entity, + EntitiesSearchFilter, + [key: string, value?: string | undefined] + >; + isEntityKind: PermissionRule; + isEntityOwner: PermissionRule< + Entity, + EntitiesSearchFilter, + [claims: string[]] + >; }; // Warning: (ae-missing-release-tag) "PlaceholderProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/catalog-backend/src/permissions/index.ts b/plugins/catalog-backend/src/permissions/index.ts index e7ece1dbc7..624d6dd7ac 100644 --- a/plugins/catalog-backend/src/permissions/index.ts +++ b/plugins/catalog-backend/src/permissions/index.ts @@ -15,4 +15,3 @@ */ export * from './rules'; -export type { CatalogPermissionRule } from './types'; diff --git a/plugins/catalog-backend/src/permissions/rules/createPropertyRule.ts b/plugins/catalog-backend/src/permissions/rules/createPropertyRule.ts index 92e53f930f..27a6033362 100644 --- a/plugins/catalog-backend/src/permissions/rules/createPropertyRule.ts +++ b/plugins/catalog-backend/src/permissions/rules/createPropertyRule.ts @@ -14,15 +14,12 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; -import { EntitiesSearchFilter } from '../../catalog/types'; -import { CatalogPermissionRule } from '../types'; import { get } from 'lodash'; +import { Entity } from '@backstage/catalog-model'; +import { createCatalogPermissionRule } from './util'; -export function createPropertyRule( - propertyType: 'metadata' | 'spec', -): CatalogPermissionRule { - return { +export const createPropertyRule = (propertyType: 'metadata' | 'spec') => + createCatalogPermissionRule({ name: `HAS_${propertyType.toUpperCase()}`, description: `Allow entities which have the specified ${propertyType} subfield.`, apply: (resource: Entity, key: string, value?: string) => { @@ -32,9 +29,8 @@ export function createPropertyRule( } return !!foundValue; }, - toQuery: (key: string, value?: string): EntitiesSearchFilter => ({ + toQuery: (key: string, value?: string) => ({ key: `${propertyType}.${key}`, ...(value !== undefined && { values: [value] }), }), - }; -} + }); diff --git a/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts b/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts index 57ded3d779..81ebd52789 100644 --- a/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts +++ b/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts @@ -15,21 +15,21 @@ */ import { Entity } from '@backstage/catalog-model'; -import { EntitiesSearchFilter } from '../../catalog/types'; -import { CatalogPermissionRule } from '../types'; +import { createCatalogPermissionRule } from './util'; /** - * A {@link CatalogPermissionRule} which filters for the presence of an - * annotation on a given entity. + * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which + * filters for the presence of an annotation on a given entity. + * * @public */ -export const hasAnnotation: CatalogPermissionRule = { +export const hasAnnotation = createCatalogPermissionRule({ name: 'HAS_ANNOTATION', description: 'Allow entities which are annotated with the specified annotation', apply: (resource: Entity, annotation: string) => !!resource.metadata.annotations?.hasOwnProperty(annotation), - toQuery: (annotation: string): EntitiesSearchFilter => ({ + toQuery: (annotation: string) => ({ key: `metadata.annotations.${annotation}`, }), -}; +}); diff --git a/plugins/catalog-backend/src/permissions/rules/hasLabel.ts b/plugins/catalog-backend/src/permissions/rules/hasLabel.ts index f93c5aeae6..04b00d68fa 100644 --- a/plugins/catalog-backend/src/permissions/rules/hasLabel.ts +++ b/plugins/catalog-backend/src/permissions/rules/hasLabel.ts @@ -15,20 +15,19 @@ */ import { Entity } from '@backstage/catalog-model'; -import { EntitiesSearchFilter } from '../../catalog/types'; -import { CatalogPermissionRule } from '../types'; +import { createCatalogPermissionRule } from './util'; /** - * A {@link CatalogPermissionRule} which filters for entities with a specified - * label in its metadata. + * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which + * filters for entities with a specified label in its metadata. * @public */ -export const hasLabel: CatalogPermissionRule = { +export const hasLabel = createCatalogPermissionRule({ name: 'HAS_LABEL', description: 'Allow entities which have the specified label metadata.', apply: (resource: Entity, label: string) => !!resource.metadata.labels?.hasOwnProperty(label), - toQuery: (label: string): EntitiesSearchFilter => ({ + toQuery: (label: string) => ({ key: `metadata.labels.${label}`, }), -}; +}); diff --git a/plugins/catalog-backend/src/permissions/rules/hasMetadata.ts b/plugins/catalog-backend/src/permissions/rules/hasMetadata.ts index fa592feb44..f5f25a5ecf 100644 --- a/plugins/catalog-backend/src/permissions/rules/hasMetadata.ts +++ b/plugins/catalog-backend/src/permissions/rules/hasMetadata.ts @@ -17,8 +17,9 @@ import { createPropertyRule } from './createPropertyRule'; /** - * A {@link CatalogPermissionRule} which filters for entities with the specified - * metadata subfield. Also matches on values if value is provided. + * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which + * filters for entities with the specified metadata subfield. Also matches on + * values if value is provided. * * The key argument to the `apply` and `toQuery` methods can be nested, such as * 'field.nestedfield'. diff --git a/plugins/catalog-backend/src/permissions/rules/hasSpec.ts b/plugins/catalog-backend/src/permissions/rules/hasSpec.ts index 73a7519e1a..891cf1d58c 100644 --- a/plugins/catalog-backend/src/permissions/rules/hasSpec.ts +++ b/plugins/catalog-backend/src/permissions/rules/hasSpec.ts @@ -17,8 +17,9 @@ import { createPropertyRule } from './createPropertyRule'; /** - * A {@link CatalogPermissionRule} which filters for entities with the specified - * spec subfield. Also matches on values if value is provided. + * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which + * filters for entities with the specified spec subfield. Also matches on values + * if value is provided. * * The key argument to the `apply` and `toQuery` methods can be nested, such as * 'field.nestedfield'. diff --git a/plugins/catalog-backend/src/permissions/rules/index.ts b/plugins/catalog-backend/src/permissions/rules/index.ts index 68d1e38715..4eec796c74 100644 --- a/plugins/catalog-backend/src/permissions/rules/index.ts +++ b/plugins/catalog-backend/src/permissions/rules/index.ts @@ -34,3 +34,5 @@ export const permissionRules = { isEntityKind, isEntityOwner, }; + +export { createCatalogPermissionRule } from './util'; diff --git a/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts b/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts index 64fcf7482d..6356c94dc4 100644 --- a/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts +++ b/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts @@ -15,14 +15,14 @@ */ import { Entity } from '@backstage/catalog-model'; import { EntitiesSearchFilter } from '../../catalog/types'; -import { CatalogPermissionRule } from '../types'; +import { createCatalogPermissionRule } from './util'; /** - * A {@link CatalogPermissionRule} which filters for entities with a specified - * kind. + * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which + * filters for entities with a specified kind. * @public */ -export const isEntityKind: CatalogPermissionRule = { +export const isEntityKind = createCatalogPermissionRule({ name: 'IS_ENTITY_KIND', description: 'Allow entities with the specified kind', apply(resource: Entity, kinds: string[]) { @@ -35,4 +35,4 @@ export const isEntityKind: CatalogPermissionRule = { values: kinds.map(kind => kind.toLocaleLowerCase('en-US')), }; }, -}; +}); diff --git a/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts b/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts index e89159e6b4..3176b31b87 100644 --- a/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts +++ b/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts @@ -19,15 +19,14 @@ import { RELATION_OWNED_BY, stringifyEntityRef, } from '@backstage/catalog-model'; -import { EntitiesSearchFilter } from '../../catalog/types'; -import { CatalogPermissionRule } from '../types'; +import { createCatalogPermissionRule } from './util'; /** - * A {@link CatalogPermissionRule} which filters for entities with a specified - * owner. + * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which + * filters for entities with a specified owner. * @public */ -export const isEntityOwner: CatalogPermissionRule = { +export const isEntityOwner = createCatalogPermissionRule({ name: 'IS_ENTITY_OWNER', description: 'Allow entities owned by the current user', apply: (resource: Entity, claims: string[]) => { @@ -39,8 +38,8 @@ export const isEntityOwner: CatalogPermissionRule = { .filter(relation => relation.type === RELATION_OWNED_BY) .some(relation => claims.includes(stringifyEntityRef(relation.target))); }, - toQuery: (claims: string[]): EntitiesSearchFilter => ({ + toQuery: (claims: string[]) => ({ key: 'relations.ownedBy', values: claims, }), -}; +}); diff --git a/plugins/catalog-backend/src/permissions/types.ts b/plugins/catalog-backend/src/permissions/rules/util.ts similarity index 62% rename from plugins/catalog-backend/src/permissions/types.ts rename to plugins/catalog-backend/src/permissions/rules/util.ts index cc2b8f4d45..7ef0ca3546 100644 --- a/plugins/catalog-backend/src/permissions/types.ts +++ b/plugins/catalog-backend/src/permissions/rules/util.ts @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * Copyright 2022 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. @@ -13,18 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Entity } from '@backstage/catalog-model'; -import { PermissionRule } from '@backstage/plugin-permission-node'; -import { EntitiesSearchFilter } from '../catalog/types'; +import { makeCreatePermissionRule } from '@backstage/plugin-permission-node'; +import { EntitiesSearchFilter } from '../../catalog/types'; /** - * A conditional rule that can be used to filter catalog entities for an - * authorization request. See - * {@link @backstage/plugin-permission-node#PermissionRule} for more details. + * Helper function for creating correctly-typed + * {@link @backstage/plugin-permission-node#PermissionRule}s for the + * catalog-backend. * * @public */ -export type CatalogPermissionRule = PermissionRule< +export const createCatalogPermissionRule = makeCreatePermissionRule< Entity, EntitiesSearchFilter ->; +>(); diff --git a/plugins/catalog-backend/src/service/NextCatalogBuilder.ts b/plugins/catalog-backend/src/service/NextCatalogBuilder.ts index 1dcb632e64..36d4347ede 100644 --- a/plugins/catalog-backend/src/service/NextCatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/NextCatalogBuilder.ts @@ -17,6 +17,7 @@ import { PluginDatabaseManager, UrlReader } from '@backstage/backend-common'; import { DefaultNamespaceEntityPolicy, + Entity, EntityPolicies, EntityPolicy, FieldFormatEntityPolicy, @@ -29,7 +30,7 @@ import { ScmIntegrations } from '@backstage/integration'; import { createHash } from 'crypto'; import { Router } from 'express'; import lodash from 'lodash'; -import { EntitiesCatalog } from '../catalog'; +import { EntitiesCatalog, EntitiesSearchFilter } from '../catalog'; import { DatabaseLocationsCatalog, LocationsCatalog, @@ -83,9 +84,9 @@ import { Config } from '@backstage/config'; import { Logger } from 'winston'; import { LocationService } from './types'; import { connectEntityProviders } from '../processing/connectEntityProviders'; -import { CatalogPermissionRule } from '../permissions/types'; import { permissionRules as catalogPermissionRules } from '../permissions/rules'; import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; +import { PermissionRule } from '@backstage/plugin-permission-node'; export type CatalogEnvironment = { logger: Logger; @@ -130,7 +131,11 @@ export class NextCatalogBuilder { maxSeconds: 150, }); private locationAnalyzer: LocationAnalyzer | undefined = undefined; - private permissionRules: CatalogPermissionRule[]; + private permissionRules: PermissionRule< + Entity, + EntitiesSearchFilter, + unknown[] + >[]; constructor(env: CatalogEnvironment) { this.env = env; @@ -331,7 +336,13 @@ export class NextCatalogBuilder { * * @param permissionRules - Additional permission rules */ - addPermissionRules(...permissionRules: CatalogPermissionRule[]) { + addPermissionRules( + ...permissionRules: PermissionRule< + Entity, + EntitiesSearchFilter, + unknown[] + >[] + ) { this.permissionRules.push(...permissionRules); } diff --git a/plugins/catalog-backend/src/service/NextRouter.ts b/plugins/catalog-backend/src/service/NextRouter.ts index 7aae564a45..5338f58f91 100644 --- a/plugins/catalog-backend/src/service/NextRouter.ts +++ b/plugins/catalog-backend/src/service/NextRouter.ts @@ -25,14 +25,16 @@ import { import { Config } from '@backstage/config'; import { NotFoundError } from '@backstage/errors'; import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common'; -import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node'; +import { + createPermissionIntegrationRouter, + PermissionRule, +} from '@backstage/plugin-permission-node'; import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; import yn from 'yn'; -import { EntitiesCatalog } from '../catalog'; +import { EntitiesCatalog, EntitiesSearchFilter } from '../catalog'; import { LocationAnalyzer } from '../ingestion/types'; -import { CatalogPermissionRule } from '../permissions/types'; import { basicEntityFilter, parseEntityFilterParams, @@ -49,7 +51,7 @@ export interface NextRouterOptions { refreshService?: RefreshService; logger: Logger; config: Config; - permissionRules?: CatalogPermissionRule[]; + permissionRules?: PermissionRule[]; } export async function createNextRouter( diff --git a/plugins/permission-node/api-report.md b/plugins/permission-node/api-report.md index 25a8be06c8..e743f18311 100644 --- a/plugins/permission-node/api-report.md +++ b/plugins/permission-node/api-report.md @@ -95,6 +95,22 @@ export const createPermissionIntegrationRouter: (options: { getResource: (resourceRef: string) => Promise; }) => Router; +// @public +export const createPermissionRule: < + TResource, + TQuery, + TParams extends unknown[], +>( + rule: PermissionRule, +) => PermissionRule; + +// @public +export const makeCreatePermissionRule: () => < + TParams extends unknown[], +>( + rule: PermissionRule, +) => PermissionRule; + // @public export interface PermissionPolicy { // (undocumented) diff --git a/plugins/permission-node/src/integration/createPermissionRule.ts b/plugins/permission-node/src/integration/createPermissionRule.ts new file mode 100644 index 0000000000..f3a0519a70 --- /dev/null +++ b/plugins/permission-node/src/integration/createPermissionRule.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2022 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. + */ + +import { PermissionRule } from '../types'; + +/** + * Helper function to ensure that {@link PermissionRule} definitions are typed correctly. + * + * @public + */ +export const createPermissionRule = < + TResource, + TQuery, + TParams extends unknown[], +>( + rule: PermissionRule, +) => rule; + +/** + * Helper for making plugin-specific createPermissionRule functions, that have + * the TResource and TQuery type parameters populated but infer the params from + * the supplied rule. This helps ensure that rules created for this plugin use + * consistent types for the resource and query. + * + * @public + */ +export const makeCreatePermissionRule = + () => + ( + rule: PermissionRule, + ) => + createPermissionRule(rule); diff --git a/plugins/permission-node/src/integration/index.ts b/plugins/permission-node/src/integration/index.ts index f070d57c8f..978342e4ed 100644 --- a/plugins/permission-node/src/integration/index.ts +++ b/plugins/permission-node/src/integration/index.ts @@ -18,3 +18,4 @@ export * from './createConditionFactory'; export * from './createConditionExports'; export * from './createConditionTransformer'; export * from './createPermissionIntegrationRouter'; +export * from './createPermissionRule';