From 0ae759dad4503fe150680fa0c5a37bb806cefc0a Mon Sep 17 00:00:00 2001 From: Joon Park Date: Wed, 5 Jan 2022 17:36:33 +0000 Subject: [PATCH] Catalog permission rules (#8766) Add catalog permission rules Signed-off-by: Joon Park --- .changeset/eight-rats-raise.md | 5 + plugins/catalog-backend/api-report.md | 17 +++ plugins/catalog-backend/package.json | 1 + plugins/catalog-backend/src/index.ts | 1 + .../catalog-backend/src/permissions/index.ts | 18 +++ .../rules/createPropertyRule.test.ts | 132 ++++++++++++++++++ .../permissions/rules/createPropertyRule.ts | 40 ++++++ .../permissions/rules/hasAnnotation.test.ts | 81 +++++++++++ .../src/permissions/rules/hasAnnotation.ts | 35 +++++ .../src/permissions/rules/hasLabel.test.ts | 81 +++++++++++ .../src/permissions/rules/hasLabel.ts | 34 +++++ .../src/permissions/rules/hasMetadata.ts | 27 ++++ .../src/permissions/rules/hasSpec.ts | 27 ++++ .../src/permissions/rules/index.ts | 36 +++++ .../permissions/rules/isEntityKind.test.ts | 53 +++++++ .../src/permissions/rules/isEntityKind.ts | 38 +++++ .../permissions/rules/isEntityOwner.test.ts | 90 ++++++++++++ .../src/permissions/rules/isEntityOwner.ts | 46 ++++++ .../catalog-backend/src/permissions/types.ts | 30 ++++ 19 files changed, 792 insertions(+) create mode 100644 .changeset/eight-rats-raise.md create mode 100644 plugins/catalog-backend/src/permissions/index.ts create mode 100644 plugins/catalog-backend/src/permissions/rules/createPropertyRule.test.ts create mode 100644 plugins/catalog-backend/src/permissions/rules/createPropertyRule.ts create mode 100644 plugins/catalog-backend/src/permissions/rules/hasAnnotation.test.ts create mode 100644 plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts create mode 100644 plugins/catalog-backend/src/permissions/rules/hasLabel.test.ts create mode 100644 plugins/catalog-backend/src/permissions/rules/hasLabel.ts create mode 100644 plugins/catalog-backend/src/permissions/rules/hasMetadata.ts create mode 100644 plugins/catalog-backend/src/permissions/rules/hasSpec.ts create mode 100644 plugins/catalog-backend/src/permissions/rules/index.ts create mode 100644 plugins/catalog-backend/src/permissions/rules/isEntityKind.test.ts create mode 100644 plugins/catalog-backend/src/permissions/rules/isEntityKind.ts create mode 100644 plugins/catalog-backend/src/permissions/rules/isEntityOwner.test.ts create mode 100644 plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts create mode 100644 plugins/catalog-backend/src/permissions/types.ts diff --git a/.changeset/eight-rats-raise.md b/.changeset/eight-rats-raise.md new file mode 100644 index 0000000000..7bc2a07f04 --- /dev/null +++ b/.changeset/eight-rats-raise.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Add catalog permission rules. diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index f773905ca0..17c7ad1d85 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -25,6 +25,7 @@ import { Location as Location_2 } from '@backstage/catalog-model'; import { LocationSpec } from '@backstage/catalog-model'; import { Logger as Logger_2 } from 'winston'; import { Organizations } from 'aws-sdk'; +import { PermissionRule } from '@backstage/plugin-permission-node'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { ResourceEntityV1alpha1 } from '@backstage/catalog-model'; @@ -303,6 +304,12 @@ export type CatalogEnvironment = { reader: UrlReader; }; +// @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) @@ -1380,6 +1387,16 @@ export function parseEntityYaml( location: LocationSpec, ): Iterable; +// @public +export const permissionRules: { + hasAnnotation: CatalogPermissionRule; + hasLabel: CatalogPermissionRule; + hasMetadata: CatalogPermissionRule; + hasSpec: CatalogPermissionRule; + isEntityKind: CatalogPermissionRule; + isEntityOwner: CatalogPermissionRule; +}; + // Warning: (ae-missing-release-tag) "PlaceholderProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 01e79314c5..fe27ddb4c7 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -36,6 +36,7 @@ "@backstage/config": "^0.1.11", "@backstage/errors": "^0.1.5", "@backstage/integration": "^0.7.0", + "@backstage/plugin-permission-node": "^0.2.3", "@backstage/search-common": "^0.2.1", "@backstage/types": "^0.1.1", "@octokit/graphql": "^4.5.8", diff --git a/plugins/catalog-backend/src/index.ts b/plugins/catalog-backend/src/index.ts index 7ad888bee4..43bbc19730 100644 --- a/plugins/catalog-backend/src/index.ts +++ b/plugins/catalog-backend/src/index.ts @@ -28,3 +28,4 @@ export * from './util'; export * from './processing'; export * from './providers'; export * from './service'; +export * from './permissions'; diff --git a/plugins/catalog-backend/src/permissions/index.ts b/plugins/catalog-backend/src/permissions/index.ts new file mode 100644 index 0000000000..e7ece1dbc7 --- /dev/null +++ b/plugins/catalog-backend/src/permissions/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2021 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. + */ + +export * from './rules'; +export type { CatalogPermissionRule } from './types'; diff --git a/plugins/catalog-backend/src/permissions/rules/createPropertyRule.test.ts b/plugins/catalog-backend/src/permissions/rules/createPropertyRule.test.ts new file mode 100644 index 0000000000..80c86cf250 --- /dev/null +++ b/plugins/catalog-backend/src/permissions/rules/createPropertyRule.test.ts @@ -0,0 +1,132 @@ +/* + * Copyright 2021 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 { createPropertyRule } from './createPropertyRule'; + +describe('createPropertyRule', () => { + const { name, description, apply, toQuery } = createPropertyRule('metadata'); + + it('formats the rule name correctly', () => { + expect(name).toBe('HAS_METADATA'); + }); + + it('formats the rule description correctly', () => { + expect(description).toBe( + 'Allow entities which have the specified metadata subfield.', + ); + }); + + describe('apply', () => { + describe('key only', () => { + it('returns false when specified key is not present', () => { + expect( + apply( + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test-component', + }, + }, + 'org.name', + ), + ).toBe(false); + }); + + it('returns true when specified key is present', () => { + expect( + apply( + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test-component', + org: { + name: 'test-org', + }, + }, + }, + 'org.name', + ), + ).toBe(true); + }); + }); + + describe('key and value', () => { + it('returns false when specified key is not present', () => { + expect( + apply( + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test-component', + }, + }, + 'org.name', + 'test-org', + ), + ).toBe(false); + }); + + it('returns false when specified value is not present', () => { + expect( + apply( + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test-component', + org: { + name: 'another-org', + }, + }, + }, + 'org.name', + 'test-org', + ), + ).toBe(false); + }); + + it('returns true when specified key and value is present', () => { + expect( + apply( + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test-component', + org: { + name: 'test-org', + }, + }, + }, + 'org.name', + 'test-org', + ), + ).toBe(true); + }); + }); + }); + + describe('toQuery', () => { + it('returns an appropriate catalog-backend filter', () => { + expect(toQuery('backstage.io/test-component')).toEqual({ + key: 'metadata.backstage.io/test-component', + }); + }); + }); +}); diff --git a/plugins/catalog-backend/src/permissions/rules/createPropertyRule.ts b/plugins/catalog-backend/src/permissions/rules/createPropertyRule.ts new file mode 100644 index 0000000000..92e53f930f --- /dev/null +++ b/plugins/catalog-backend/src/permissions/rules/createPropertyRule.ts @@ -0,0 +1,40 @@ +/* + * 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 { Entity } from '@backstage/catalog-model'; +import { EntitiesSearchFilter } from '../../catalog/types'; +import { CatalogPermissionRule } from '../types'; +import { get } from 'lodash'; + +export function createPropertyRule( + propertyType: 'metadata' | 'spec', +): CatalogPermissionRule { + return { + name: `HAS_${propertyType.toUpperCase()}`, + description: `Allow entities which have the specified ${propertyType} subfield.`, + apply: (resource: Entity, key: string, value?: string) => { + const foundValue = get(resource[propertyType], key); + if (value !== undefined) { + return value === foundValue; + } + return !!foundValue; + }, + toQuery: (key: string, value?: string): EntitiesSearchFilter => ({ + key: `${propertyType}.${key}`, + ...(value !== undefined && { values: [value] }), + }), + }; +} diff --git a/plugins/catalog-backend/src/permissions/rules/hasAnnotation.test.ts b/plugins/catalog-backend/src/permissions/rules/hasAnnotation.test.ts new file mode 100644 index 0000000000..1596270907 --- /dev/null +++ b/plugins/catalog-backend/src/permissions/rules/hasAnnotation.test.ts @@ -0,0 +1,81 @@ +/* + * Copyright 2021 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 { hasAnnotation } from './hasAnnotation'; + +describe('hasAnnotation permission rule', () => { + describe('apply', () => { + it('returns false when specified annotation is not present', () => { + expect( + hasAnnotation.apply( + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test-component', + annotations: { + 'other-annotation': 'foo', + }, + }, + }, + 'backstage.io/test-annotation', + ), + ).toEqual(false); + }); + + it('returns false when no annotations are present', () => { + expect( + hasAnnotation.apply( + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test-component', + }, + }, + 'backstage.io/test-annotation', + ), + ).toEqual(false); + }); + + it('returns true when specified annotation is present', () => { + expect( + hasAnnotation.apply( + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test-component', + annotations: { + 'other-annotation': 'foo', + 'backstage.io/test-annotation': 'bar', + }, + }, + }, + 'backstage.io/test-annotation', + ), + ).toEqual(true); + }); + }); + + describe('toQuery', () => { + it('returns an appropriate catalog-backend filter', () => { + expect(hasAnnotation.toQuery('backstage.io/test-annotation')).toEqual({ + key: 'metadata.annotations.backstage.io/test-annotation', + }); + }); + }); +}); diff --git a/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts b/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts new file mode 100644 index 0000000000..57ded3d779 --- /dev/null +++ b/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2021 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 { Entity } from '@backstage/catalog-model'; +import { EntitiesSearchFilter } from '../../catalog/types'; +import { CatalogPermissionRule } from '../types'; + +/** + * A {@link CatalogPermissionRule} which filters for the presence of an + * annotation on a given entity. + * @public + */ +export const hasAnnotation: CatalogPermissionRule = { + 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 => ({ + key: `metadata.annotations.${annotation}`, + }), +}; diff --git a/plugins/catalog-backend/src/permissions/rules/hasLabel.test.ts b/plugins/catalog-backend/src/permissions/rules/hasLabel.test.ts new file mode 100644 index 0000000000..a1b9cb5ad0 --- /dev/null +++ b/plugins/catalog-backend/src/permissions/rules/hasLabel.test.ts @@ -0,0 +1,81 @@ +/* + * Copyright 2021 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 { hasLabel } from './hasLabel'; + +describe('hasLabel permission rule', () => { + describe('apply', () => { + it('returns false when specified label is not present', () => { + expect( + hasLabel.apply( + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test-component', + labels: { + somelabel: 'foo', + }, + }, + }, + 'backstage.io/testlabel', + ), + ).toEqual(false); + }); + + it('returns false when no annotations are present', () => { + expect( + hasLabel.apply( + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test-component', + }, + }, + 'backstage.io/testlabel', + ), + ).toEqual(false); + }); + + it('returns true when specified annotation is present', () => { + expect( + hasLabel.apply( + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test-component', + labels: { + somelabel: 'foo', + 'backstage.io/testlabel': 'bar', + }, + }, + }, + 'backstage.io/testlabel', + ), + ).toEqual(true); + }); + }); + + describe('toQuery', () => { + it('returns an appropriate catalog-backend filter', () => { + expect(hasLabel.toQuery('backstage.io/testlabel')).toEqual({ + key: 'metadata.labels.backstage.io/testlabel', + }); + }); + }); +}); diff --git a/plugins/catalog-backend/src/permissions/rules/hasLabel.ts b/plugins/catalog-backend/src/permissions/rules/hasLabel.ts new file mode 100644 index 0000000000..f93c5aeae6 --- /dev/null +++ b/plugins/catalog-backend/src/permissions/rules/hasLabel.ts @@ -0,0 +1,34 @@ +/* + * 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 { Entity } from '@backstage/catalog-model'; +import { EntitiesSearchFilter } from '../../catalog/types'; +import { CatalogPermissionRule } from '../types'; + +/** + * A {@link CatalogPermissionRule} which filters for entities with a specified + * label in its metadata. + * @public + */ +export const hasLabel: CatalogPermissionRule = { + 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 => ({ + key: `metadata.labels.${label}`, + }), +}; diff --git a/plugins/catalog-backend/src/permissions/rules/hasMetadata.ts b/plugins/catalog-backend/src/permissions/rules/hasMetadata.ts new file mode 100644 index 0000000000..fa592feb44 --- /dev/null +++ b/plugins/catalog-backend/src/permissions/rules/hasMetadata.ts @@ -0,0 +1,27 @@ +/* + * 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 { createPropertyRule } from './createPropertyRule'; + +/** + * A {@link CatalogPermissionRule} 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'. + * @public + */ +export const hasMetadata = createPropertyRule('metadata'); diff --git a/plugins/catalog-backend/src/permissions/rules/hasSpec.ts b/plugins/catalog-backend/src/permissions/rules/hasSpec.ts new file mode 100644 index 0000000000..73a7519e1a --- /dev/null +++ b/plugins/catalog-backend/src/permissions/rules/hasSpec.ts @@ -0,0 +1,27 @@ +/* + * 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 { createPropertyRule } from './createPropertyRule'; + +/** + * A {@link CatalogPermissionRule} 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'. + * @public + */ +export const hasSpec = createPropertyRule('spec'); diff --git a/plugins/catalog-backend/src/permissions/rules/index.ts b/plugins/catalog-backend/src/permissions/rules/index.ts new file mode 100644 index 0000000000..68d1e38715 --- /dev/null +++ b/plugins/catalog-backend/src/permissions/rules/index.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2021 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 { hasAnnotation } from './hasAnnotation'; +import { isEntityKind } from './isEntityKind'; +import { isEntityOwner } from './isEntityOwner'; +import { hasLabel } from './hasLabel'; +import { hasMetadata } from './hasMetadata'; +import { hasSpec } from './hasSpec'; + +/** + * These permission rules can be used to conditionally filter catalog entities + * or describe a user's access to the entities. + * @public + */ +export const permissionRules = { + hasAnnotation, + hasLabel, + hasMetadata, + hasSpec, + isEntityKind, + isEntityOwner, +}; diff --git a/plugins/catalog-backend/src/permissions/rules/isEntityKind.test.ts b/plugins/catalog-backend/src/permissions/rules/isEntityKind.test.ts new file mode 100644 index 0000000000..e11e102728 --- /dev/null +++ b/plugins/catalog-backend/src/permissions/rules/isEntityKind.test.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2021 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 { Entity } from '@backstage/catalog-model'; +import { isEntityKind } from './isEntityKind'; + +describe('isEntityKind', () => { + describe('apply', () => { + it('returns true when entity is the correct kind', () => { + const component: Entity = { + apiVersion: 'a', + kind: 'b', + metadata: { + name: 'some-component', + }, + }; + expect(isEntityKind.apply(component, ['b'])).toBe(true); + }); + + it('returns false when entity is not the correct kind', () => { + const component: Entity = { + apiVersion: 'a', + kind: 'b', + metadata: { + name: 'some-component', + }, + }; + expect(isEntityKind.apply(component, ['c'])).toBe(false); + }); + }); + + describe('toQuery', () => { + it('returns an appropriate catalog-backend filter', () => { + expect(isEntityKind.toQuery(['b'])).toEqual({ + key: 'kind', + values: ['b'], + }); + }); + }); +}); diff --git a/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts b/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts new file mode 100644 index 0000000000..64fcf7482d --- /dev/null +++ b/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2021 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 { Entity } from '@backstage/catalog-model'; +import { EntitiesSearchFilter } from '../../catalog/types'; +import { CatalogPermissionRule } from '../types'; + +/** + * A {@link CatalogPermissionRule} which filters for entities with a specified + * kind. + * @public + */ +export const isEntityKind: CatalogPermissionRule = { + name: 'IS_ENTITY_KIND', + description: 'Allow entities with the specified kind', + apply(resource: Entity, kinds: string[]) { + const resourceKind = resource.kind.toLocaleLowerCase('en-US'); + return kinds.some(kind => kind.toLocaleLowerCase('en-US') === resourceKind); + }, + toQuery(kinds: string[]): EntitiesSearchFilter { + return { + key: 'kind', + values: kinds.map(kind => kind.toLocaleLowerCase('en-US')), + }; + }, +}; diff --git a/plugins/catalog-backend/src/permissions/rules/isEntityOwner.test.ts b/plugins/catalog-backend/src/permissions/rules/isEntityOwner.test.ts new file mode 100644 index 0000000000..282d2558ef --- /dev/null +++ b/plugins/catalog-backend/src/permissions/rules/isEntityOwner.test.ts @@ -0,0 +1,90 @@ +/* + * Copyright 2021 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 { Entity } from '@backstage/catalog-model'; +import { isEntityOwner } from './isEntityOwner'; + +describe('isEntityOwner', () => { + describe('apply', () => { + it('returns true when entity is owned by the given user', () => { + const component: Entity = { + apiVersion: 'a', + kind: 'b', + metadata: { + name: 'some-component', + }, + relations: [ + { + type: 'ownedBy', + target: { + kind: 'user', + namespace: 'default', + name: 'spiderman', + }, + }, + ], + }; + expect(isEntityOwner.apply(component, ['user:default/spiderman'])).toBe( + true, + ); + }); + + it('returns false when entity is not owned by the given user', () => { + const component: Entity = { + apiVersion: 'a', + kind: 'b', + metadata: { + name: 'some-component', + }, + relations: [ + { + type: 'ownedBy', + target: { + kind: 'user', + namespace: 'default', + name: 'green-goblin', + }, + }, + ], + }; + expect(isEntityOwner.apply(component, ['user:default/spiderman'])).toBe( + false, + ); + }); + + it('returns false when entity does not have an owner', () => { + const component: Entity = { + apiVersion: 'a', + kind: 'b', + metadata: { + name: 'some-component', + }, + }; + expect(isEntityOwner.apply(component, ['user:default/spiderman'])).toBe( + false, + ); + }); + }); + + describe('toQuery', () => { + it('returns an appropriate catalog-backend filter', () => { + expect(isEntityOwner.toQuery(['user:default/spiderman'])).toEqual({ + key: 'relations.ownedBy', + values: ['user:default/spiderman'], + }); + }); + }); +}); diff --git a/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts b/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts new file mode 100644 index 0000000000..e89159e6b4 --- /dev/null +++ b/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2021 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 { + Entity, + RELATION_OWNED_BY, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { EntitiesSearchFilter } from '../../catalog/types'; +import { CatalogPermissionRule } from '../types'; + +/** + * A {@link CatalogPermissionRule} which filters for entities with a specified + * owner. + * @public + */ +export const isEntityOwner: CatalogPermissionRule = { + name: 'IS_ENTITY_OWNER', + description: 'Allow entities owned by the current user', + apply: (resource: Entity, claims: string[]) => { + if (!resource.relations) { + return false; + } + + return resource.relations + .filter(relation => relation.type === RELATION_OWNED_BY) + .some(relation => claims.includes(stringifyEntityRef(relation.target))); + }, + toQuery: (claims: string[]): EntitiesSearchFilter => ({ + key: 'relations.ownedBy', + values: claims, + }), +}; diff --git a/plugins/catalog-backend/src/permissions/types.ts b/plugins/catalog-backend/src/permissions/types.ts new file mode 100644 index 0000000000..cc2b8f4d45 --- /dev/null +++ b/plugins/catalog-backend/src/permissions/types.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2021 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 { Entity } from '@backstage/catalog-model'; +import { PermissionRule } 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. + * + * @public + */ +export type CatalogPermissionRule = PermissionRule< + Entity, + EntitiesSearchFilter +>;