From 15575563f7a38907d6c07a36e05ed97150a77431 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 5 Sep 2025 19:10:55 +0200 Subject: [PATCH 1/7] catalog-backend: add support for type in catalog.rules.allow Signed-off-by: Vincenzo Scamporlino --- .../src/ingestion/CatalogRules.test.ts | 38 ++++++++++++++++++- .../src/ingestion/CatalogRules.ts | 29 +++++++++++--- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts index 19c231884c..67d1e86248 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts @@ -27,8 +27,14 @@ const entity = { kind: 'Group', } as Entity, component: { - kind: 'component', + kind: 'Component', } as Entity, + componentWithType: { + kind: 'Component', + spec: { + type: 'service', + }, + } as unknown as Entity, location: { kind: 'Location', } as Entity, @@ -164,6 +170,9 @@ describe('DefaultCatalogRulesEnforcer', () => { expect(enforcer.isAllowed(entity.user, location.x)).toBe(false); expect(enforcer.isAllowed(entity.group, location.y)).toBe(false); expect(enforcer.isAllowed(entity.component, location.z)).toBe(true); + expect(enforcer.isAllowed(entity.componentWithType, location.z)).toBe( + true, + ); expect(enforcer.isAllowed(entity.location, location.z)).toBe(true); }); @@ -268,6 +277,33 @@ describe('DefaultCatalogRulesEnforcer', () => { }); }); + it('should only allow components with a specific type', () => { + const enforcer = DefaultCatalogRulesEnforcer.fromConfig( + new ConfigReader({ + catalog: { + rules: [ + { + allow: [{ kind: 'Component', type: 'service' }], + locations: [{ type: 'url', pattern: 'https://github.com/b/**' }], + }, + ], + }, + }), + ); + + expect(enforcer.isAllowed(entity.component, location.w)).toBe(false); + expect(enforcer.isAllowed(entity.component, location.y)).toBe(false); + expect(enforcer.isAllowed(entity.component, location.z)).toBe(false); + + expect(enforcer.isAllowed(entity.componentWithType, location.w)).toBe(true); + expect(enforcer.isAllowed(entity.componentWithType, location.y)).toBe( + false, + ); + expect(enforcer.isAllowed(entity.componentWithType, location.z)).toBe( + false, + ); + }); + it('should allow locations with a hidden folder', () => { const enforcer = DefaultCatalogRulesEnforcer.fromConfig( new ConfigReader({ diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.ts index 8fb237df30..ddb6398bf6 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.ts @@ -26,9 +26,7 @@ import { minimatch } from 'minimatch'; * An undefined list of matchers means match all, an empty list of matchers means match none. */ export type CatalogRule = { - allow: Array<{ - kind: string; - }>; + allow: CatalogRuleAllow[]; locations?: Array<{ exact?: string; type: string; @@ -36,6 +34,11 @@ export type CatalogRule = { }>; }; +type CatalogRuleAllow = { + kind: string; + type?: string; +}; + /** * Decides whether an entity from a given location is allowed to enter the * catalog, according to some rule set. @@ -78,6 +81,9 @@ export class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer { * catalog: * rules: * - allow: [Component, API] + * - allow: + * - kind: Resource + * type: database * - allow: [Template] * locations: * - type: url @@ -105,7 +111,14 @@ export class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer { const globalRules = config .getConfigArray('catalog.rules') .map(ruleConf => ({ - allow: ruleConf.getStringArray('allow').map(kind => ({ kind })), + allow: ruleConf + .get>('allow') + .map(kindOrObject => { + if (typeof kindOrObject === 'string') { + return { kind: kindOrObject }; + } + return kindOrObject; + }), locations: ruleConf .getOptionalConfigArray('locations') ?.map(locationConfig => { @@ -199,13 +212,17 @@ export class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer { return false; } - private matchEntity(entity: Entity, matchers?: { kind: string }[]): boolean { + private matchEntity(entity: Entity, matchers?: CatalogRuleAllow[]): boolean { if (!matchers) { return true; } for (const matcher of matchers) { - if (entity?.kind?.toLowerCase() !== matcher.kind.toLowerCase()) { + if (entity.kind?.toLowerCase() !== matcher.kind.toLowerCase()) { + continue; + } + + if (matcher.type && matcher.type !== entity.spec?.type) { continue; } From 36694fb7749c2b8f2db8a918a907688f27fded66 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 5 Sep 2025 19:12:33 +0200 Subject: [PATCH 2/7] catalog-backend: allow type to catalog.rules.allow Signed-off-by: Vincenzo Scamporlino --- plugins/catalog-backend/config.d.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/catalog-backend/config.d.ts b/plugins/catalog-backend/config.d.ts index 6329bcb111..d346f01e37 100644 --- a/plugins/catalog-backend/config.d.ts +++ b/plugins/catalog-backend/config.d.ts @@ -38,8 +38,11 @@ export interface Config { * Allow entities of these particular kinds. * * E.g. ["Component", "API", "Template", "Location"] + * + * You can also specify the type of the entity by using an object with `kind` and optional `type` properties. + * E.g. [{ kind: "Component", type: "service" }] */ - allow: Array; + allow: Array; /** * Limit this rule to a specific location * From 9b40a55ded2b3a5f5819d57d573d9e0993c34c49 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 5 Sep 2025 19:12:59 +0200 Subject: [PATCH 3/7] catalog-backend: type changeset Signed-off-by: Vincenzo Scamporlino --- .changeset/rotten-hairs-attack.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .changeset/rotten-hairs-attack.md diff --git a/.changeset/rotten-hairs-attack.md b/.changeset/rotten-hairs-attack.md new file mode 100644 index 0000000000..1b89ad46e7 --- /dev/null +++ b/.changeset/rotten-hairs-attack.md @@ -0,0 +1,25 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Add support for specifying an entity `type` in `catalog.rules.allow` rules within the catalog configuration. + +For example, this enables allowing all `Template` entities with the type `website`: + +```diff + catalog: + rules: + - allow: + - Component + - API + - Resource + - System + - Domain + - Location ++ - allow: ++ - kind: Template ++ type: website + locations: + - type: url + pattern: https://github.com/org/*\/blob/master/*.yaml +``` From 77980e3f80b0e0001ddf78331ba9abe509000e81 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Wed, 10 Sep 2025 09:37:31 +0200 Subject: [PATCH 4/7] catalog-backend: rename type to spec.type Signed-off-by: Vincenzo Scamporlino --- .changeset/rotten-hairs-attack.md | 4 ++-- plugins/catalog-backend/config.d.ts | 2 +- plugins/catalog-backend/src/ingestion/CatalogRules.test.ts | 2 +- plugins/catalog-backend/src/ingestion/CatalogRules.ts | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.changeset/rotten-hairs-attack.md b/.changeset/rotten-hairs-attack.md index 1b89ad46e7..7754441f78 100644 --- a/.changeset/rotten-hairs-attack.md +++ b/.changeset/rotten-hairs-attack.md @@ -2,7 +2,7 @@ '@backstage/plugin-catalog-backend': minor --- -Add support for specifying an entity `type` in `catalog.rules.allow` rules within the catalog configuration. +Add support for specifying an entity `spec.type` in `catalog.rules.allow` rules within the catalog configuration. For example, this enables allowing all `Template` entities with the type `website`: @@ -18,7 +18,7 @@ For example, this enables allowing all `Template` entities with the type `websit - Location + - allow: + - kind: Template -+ type: website ++ spec.type: website locations: - type: url pattern: https://github.com/org/*\/blob/master/*.yaml diff --git a/plugins/catalog-backend/config.d.ts b/plugins/catalog-backend/config.d.ts index d346f01e37..4b349405cc 100644 --- a/plugins/catalog-backend/config.d.ts +++ b/plugins/catalog-backend/config.d.ts @@ -42,7 +42,7 @@ export interface Config { * You can also specify the type of the entity by using an object with `kind` and optional `type` properties. * E.g. [{ kind: "Component", type: "service" }] */ - allow: Array; + allow: Array; /** * Limit this rule to a specific location * diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts index 67d1e86248..a28ceb0d48 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts @@ -283,7 +283,7 @@ describe('DefaultCatalogRulesEnforcer', () => { catalog: { rules: [ { - allow: [{ kind: 'Component', type: 'service' }], + allow: [{ kind: 'Component', 'spec.type': 'service' }], locations: [{ type: 'url', pattern: 'https://github.com/b/**' }], }, ], diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.ts index ddb6398bf6..b348f5b752 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.ts @@ -36,7 +36,7 @@ export type CatalogRule = { type CatalogRuleAllow = { kind: string; - type?: string; + 'spec.type'?: string; }; /** @@ -222,7 +222,7 @@ export class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer { continue; } - if (matcher.type && matcher.type !== entity.spec?.type) { + if (matcher['spec.type'] && matcher['spec.type'] !== entity.spec?.type) { continue; } From 3c3466dd51cecba1b650f33f2610a9fe8fe2032f Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 11 Sep 2025 20:33:46 +0200 Subject: [PATCH 5/7] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: Vincenzo Scamporlino --- .changeset/rotten-hairs-attack.md | 2 +- plugins/catalog-backend/config.d.ts | 4 ++-- plugins/catalog-backend/src/ingestion/CatalogRules.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.changeset/rotten-hairs-attack.md b/.changeset/rotten-hairs-attack.md index 7754441f78..aa6dabc78c 100644 --- a/.changeset/rotten-hairs-attack.md +++ b/.changeset/rotten-hairs-attack.md @@ -2,7 +2,7 @@ '@backstage/plugin-catalog-backend': minor --- -Add support for specifying an entity `spec.type` in `catalog.rules.allow` rules within the catalog configuration. +Add support for specifying an entity `spec.type` in `catalog.rules` and `catalog.locations.rules` within the catalog configuration. For example, this enables allowing all `Template` entities with the type `website`: diff --git a/plugins/catalog-backend/config.d.ts b/plugins/catalog-backend/config.d.ts index 4b349405cc..a506476dbf 100644 --- a/plugins/catalog-backend/config.d.ts +++ b/plugins/catalog-backend/config.d.ts @@ -39,8 +39,8 @@ export interface Config { * * E.g. ["Component", "API", "Template", "Location"] * - * You can also specify the type of the entity by using an object with `kind` and optional `type` properties. - * E.g. [{ kind: "Component", type: "service" }] + * You can also specify the type of the entity by using an object with `kind` and optional `spec.type` properties. + * E.g. [{ kind: "Component", 'spec.type': "service" }] */ allow: Array; /** diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.ts index b348f5b752..ea4e520177 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.ts @@ -83,7 +83,7 @@ export class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer { * - allow: [Component, API] * - allow: * - kind: Resource - * type: database + * 'spec.type': database * - allow: [Template] * locations: * - type: url From 0364b9f107cc1be37e4342d555483b35b5eddbfa Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 11 Sep 2025 20:41:09 +0200 Subject: [PATCH 6/7] catalog-backend: improve validation Signed-off-by: Vincenzo Scamporlino --- .../src/ingestion/CatalogRules.ts | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.ts index ea4e520177..f6026a84cd 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.ts @@ -19,6 +19,7 @@ import { Entity } from '@backstage/catalog-model'; import path from 'path'; import { LocationSpec } from '@backstage/plugin-catalog-common'; import { minimatch } from 'minimatch'; +import { z } from 'zod'; /** * Rules to apply to catalog entities. @@ -47,6 +48,16 @@ export type CatalogRulesEnforcer = { isAllowed(entity: Entity, location: LocationSpec): boolean; }; +const allowRuleParser = z.array( + z + .object({ + kind: z.string(), + 'spec.type': z.string().optional(), + }) + .or(z.string()) + .transform(val => (typeof val === 'string' ? { kind: val } : val)), +); + /** * Implements the default catalog rule set, consuming the config keys * `catalog.rules` and `catalog.locations.[].rules`. @@ -111,14 +122,7 @@ export class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer { const globalRules = config .getConfigArray('catalog.rules') .map(ruleConf => ({ - allow: ruleConf - .get>('allow') - .map(kindOrObject => { - if (typeof kindOrObject === 'string') { - return { kind: kindOrObject }; - } - return kindOrObject; - }), + allow: allowRuleParser.parse(ruleConf.get('allow')), locations: ruleConf .getOptionalConfigArray('locations') ?.map(locationConfig => { From 62bb3b75d076389678c1538b5bf2a55fbbd7ccf2 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Mon, 15 Sep 2025 11:50:34 +0200 Subject: [PATCH 7/7] catalog-backend: make type comparison lowercase Signed-off-by: Vincenzo Scamporlino --- .../src/ingestion/CatalogRules.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.ts index f6026a84cd..15e620a443 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.ts @@ -222,12 +222,23 @@ export class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer { } for (const matcher of matchers) { - if (entity.kind?.toLowerCase() !== matcher.kind.toLowerCase()) { + if ( + entity.kind?.toLocaleLowerCase('en-US') !== + matcher.kind.toLocaleLowerCase('en-US') + ) { continue; } - if (matcher['spec.type'] && matcher['spec.type'] !== entity.spec?.type) { - continue; + if (matcher['spec.type']) { + if (typeof entity.spec?.type !== 'string') { + continue; + } + if ( + matcher['spec.type'].toLocaleLowerCase('en-US') !== + entity.spec.type.toLocaleLowerCase('en-US') + ) { + continue; + } } return true;