diff --git a/.changeset/flat-plums-shout.md b/.changeset/flat-plums-shout.md new file mode 100644 index 0000000000..250d6a1b35 --- /dev/null +++ b/.changeset/flat-plums-shout.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Allow unknown typed location from being registered via the location service by configuration settings diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 55f2d114db..0460fbc5e9 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -150,6 +150,7 @@ export class CatalogBuilder { getDefaultProcessors(): CatalogProcessor[]; replaceEntityPolicies(policies: EntityPolicy[]): CatalogBuilder; replaceProcessors(processors: CatalogProcessor[]): CatalogBuilder; + setAllowedLocationTypes(allowedLocationTypes: string[]): CatalogBuilder; setEntityDataParser(parser: CatalogProcessorParser): CatalogBuilder; setFieldFormatValidators(validators: Partial): CatalogBuilder; setLocationAnalyzer(locationAnalyzer: LocationAnalyzer): CatalogBuilder; diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index e9e35301c3..25f9fde313 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -147,7 +147,8 @@ export class CatalogBuilder { maxSeconds: 150, }); private locationAnalyzer: LocationAnalyzer | undefined = undefined; - private permissionRules: CatalogPermissionRule[]; + private readonly permissionRules: CatalogPermissionRule[]; + private allowedLocationType: string[]; /** * Creates a catalog builder. @@ -167,6 +168,7 @@ export class CatalogBuilder { this.processorsReplace = false; this.parser = undefined; this.permissionRules = Object.values(catalogPermissionRules); + this.allowedLocationType = ['url']; } /** @@ -363,6 +365,16 @@ export class CatalogBuilder { this.permissionRules.push(...permissionRules.flat()); } + /** + * Sets up the allowed location types from being registered via the location service. + * + * @param allowedLocationTypes - the allowed location types + */ + setAllowedLocationTypes(allowedLocationTypes: string[]): CatalogBuilder { + this.allowedLocationType = allowedLocationTypes; + return this; + } + /** * Wires up and returns all of the component parts of the catalog */ @@ -465,7 +477,9 @@ export class CatalogBuilder { const locationAnalyzer = this.locationAnalyzer ?? new RepoLocationAnalyzer(logger, integrations); const locationService = new AuthorizedLocationService( - new DefaultLocationService(locationStore, orchestrator), + new DefaultLocationService(locationStore, orchestrator, { + allowedLocationTypes: this.allowedLocationType, + }), permissionEvaluator, ); const refreshService = new AuthorizedRefreshService( diff --git a/plugins/catalog-backend/src/service/DefaultLocationService.test.ts b/plugins/catalog-backend/src/service/DefaultLocationService.test.ts index 4399d48d69..8169fc484d 100644 --- a/plugins/catalog-backend/src/service/DefaultLocationService.test.ts +++ b/plugins/catalog-backend/src/service/DefaultLocationService.test.ts @@ -144,6 +144,7 @@ describe('DefaultLocationServiceTest', () => { store.listLocations.mockResolvedValueOnce([ { id: '137', ...locationSpec }, ]); + const result = await locationService.createLocation( { type: 'url', target: 'https://backstage.io/catalog-info.yaml' }, true, @@ -225,6 +226,7 @@ describe('DefaultLocationServiceTest', () => { store.listLocations.mockResolvedValueOnce([ { id: '987', type: 'url', target: 'https://example.com' }, ]); + const result = await locationService.createLocation( { type: 'url', target: 'https://backstage.io/catalog-info.yaml' }, true, @@ -259,7 +261,41 @@ describe('DefaultLocationServiceTest', () => { }); }); - it('should not allow locations of unknown types', async () => { + it('should create location with unknown type if configuration allows it', async () => { + const locationSpec = { + type: 'unknown', + target: 'https://backstage.io/catalog-info.yaml', + }; + + store.createLocation.mockResolvedValue({ + ...locationSpec, + id: '123', + }); + + const locationServiceAllowingUnknownType = new DefaultLocationService( + store, + orchestrator, + { + allowedLocationTypes: ['url', 'unknown'], + }, + ); + await expect( + locationServiceAllowingUnknownType.createLocation(locationSpec, false), + ).resolves.toEqual({ + entities: [], + location: { + id: '123', + target: 'https://backstage.io/catalog-info.yaml', + type: 'unknown', + }, + }); + expect(store.createLocation).toBeCalledWith({ + target: 'https://backstage.io/catalog-info.yaml', + type: 'unknown', + }); + }); + + it('should not allow locations of unknown types by default', async () => { await expect( locationService.createLocation( { diff --git a/plugins/catalog-backend/src/service/DefaultLocationService.ts b/plugins/catalog-backend/src/service/DefaultLocationService.ts index 819cead63d..6603ebc08b 100644 --- a/plugins/catalog-backend/src/service/DefaultLocationService.ts +++ b/plugins/catalog-backend/src/service/DefaultLocationService.ts @@ -27,18 +27,29 @@ import { locationSpecToMetadataName } from '../util/conversion'; import { InputError } from '@backstage/errors'; import { DeferredEntity } from '@backstage/plugin-catalog-node'; +export type DefaultLocationServiceOptions = { + allowedLocationTypes: string[]; +}; + export class DefaultLocationService implements LocationService { constructor( private readonly store: LocationStore, private readonly orchestrator: CatalogProcessingOrchestrator, + private readonly options: DefaultLocationServiceOptions = { + allowedLocationTypes: ['url'], + }, ) {} async createLocation( input: LocationInput, dryRun: boolean, ): Promise<{ location: Location; entities: Entity[]; exists?: boolean }> { - if (input.type !== 'url') { - throw new InputError(`Registered locations must be of type 'url'`); + if (!this.options.allowedLocationTypes.includes(input.type)) { + throw new InputError( + `Registered locations must be of an allowed type ${JSON.stringify( + this.options.allowedLocationTypes, + )}`, + ); } if (dryRun) { return this.dryRunCreateLocation(input);