Merge pull request #31103 from backstage/catalog-add-type-in-rules-allow
Catalog: add optional spec.type to allow rules
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': minor
|
||||
---
|
||||
|
||||
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
|
||||
catalog:
|
||||
rules:
|
||||
- allow:
|
||||
- Component
|
||||
- API
|
||||
- Resource
|
||||
- System
|
||||
- Domain
|
||||
- Location
|
||||
+ - allow:
|
||||
+ - kind: Template
|
||||
+ spec.type: website
|
||||
locations:
|
||||
- type: url
|
||||
pattern: https://github.com/org/*\/blob/master/*.yaml
|
||||
```
|
||||
Vendored
+4
-1
@@ -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 `spec.type` properties.
|
||||
* E.g. [{ kind: "Component", 'spec.type': "service" }]
|
||||
*/
|
||||
allow: Array<string>;
|
||||
allow: Array<string | { kind: string; 'spec.type'?: string }>;
|
||||
/**
|
||||
* Limit this rule to a specific location
|
||||
*
|
||||
|
||||
@@ -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', 'spec.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({
|
||||
|
||||
@@ -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.
|
||||
@@ -26,9 +27,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 +35,11 @@ export type CatalogRule = {
|
||||
}>;
|
||||
};
|
||||
|
||||
type CatalogRuleAllow = {
|
||||
kind: string;
|
||||
'spec.type'?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Decides whether an entity from a given location is allowed to enter the
|
||||
* catalog, according to some rule set.
|
||||
@@ -44,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`.
|
||||
@@ -78,6 +92,9 @@ export class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer {
|
||||
* catalog:
|
||||
* rules:
|
||||
* - allow: [Component, API]
|
||||
* - allow:
|
||||
* - kind: Resource
|
||||
* 'spec.type': database
|
||||
* - allow: [Template]
|
||||
* locations:
|
||||
* - type: url
|
||||
@@ -105,7 +122,7 @@ export class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer {
|
||||
const globalRules = config
|
||||
.getConfigArray('catalog.rules')
|
||||
.map(ruleConf => ({
|
||||
allow: ruleConf.getStringArray('allow').map(kind => ({ kind })),
|
||||
allow: allowRuleParser.parse(ruleConf.get('allow')),
|
||||
locations: ruleConf
|
||||
.getOptionalConfigArray('locations')
|
||||
?.map(locationConfig => {
|
||||
@@ -199,16 +216,31 @@ 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?.toLocaleLowerCase('en-US') !==
|
||||
matcher.kind.toLocaleLowerCase('en-US')
|
||||
) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user