From 15ba00ff7dc25828d797f2615b09e49c522d3090 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 16 Feb 2024 13:24:42 +0100 Subject: [PATCH] catalog-backend: migrate to support new auth services Signed-off-by: Patrik Oldsberg --- .changeset/purple-kiwis-complain.md | 5 + plugins/catalog-backend/api-report.md | 10 +- plugins/catalog-backend/src/catalog/types.ts | 19 +-- .../service/AuthorizedEntitiesCatalog.test.ts | 79 ++++++----- .../src/service/AuthorizedEntitiesCatalog.ts | 41 +++--- .../service/AuthorizedLocationService.test.ts | 63 ++++----- .../src/service/AuthorizedLocationService.ts | 45 ++++--- .../service/AuthorizedRefreshService.test.ts | 5 +- .../src/service/AuthorizedRefreshService.ts | 10 +- .../src/service/CatalogBuilder.ts | 49 +++++-- .../src/service/CatalogPlugin.ts | 9 ++ .../service/DefaultEntitiesCatalog.test.ts | 93 ++++++++++--- .../src/service/DefaultRefreshService.test.ts | 10 +- .../src/service/createRouter.test.ts | 127 +++++++++--------- .../src/service/createRouter.ts | 77 ++++------- .../request/parseQueryEntitiesParams.ts | 6 +- plugins/catalog-backend/src/service/types.ts | 17 ++- 17 files changed, 396 insertions(+), 269 deletions(-) create mode 100644 .changeset/purple-kiwis-complain.md diff --git a/.changeset/purple-kiwis-complain.md b/.changeset/purple-kiwis-complain.md new file mode 100644 index 0000000000..a5bcb3ed67 --- /dev/null +++ b/.changeset/purple-kiwis-complain.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Migrated to support new auth services. The `CatalogBuilder.create` method now accepts a `discovery` option, which is recommended to forward from the plugin environment, as it will otherwise fall back to use the `HostDiscovery` implementation. diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 92a2fbb280..ef2379766d 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -11,6 +11,7 @@ import { AnalyzeLocationGenerateEntity as AnalyzeLocationGenerateEntity_2 } from import { AnalyzeLocationRequest as AnalyzeLocationRequest_2 } from '@backstage/plugin-catalog-common'; import { AnalyzeLocationResponse as AnalyzeLocationResponse_2 } from '@backstage/plugin-catalog-common'; import { AnalyzeOptions as AnalyzeOptions_2 } from '@backstage/plugin-catalog-node'; +import { AuthService } from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; import { CatalogCollatorEntityTransformer as CatalogCollatorEntityTransformer_2 } from '@backstage/plugin-search-backend-module-catalog'; import { CatalogEntityDocument } from '@backstage/plugin-catalog-common'; @@ -28,6 +29,7 @@ import { Config } from '@backstage/config'; import { DefaultCatalogCollatorFactory as DefaultCatalogCollatorFactory_2 } from '@backstage/plugin-search-backend-module-catalog'; import { DefaultCatalogCollatorFactoryOptions as DefaultCatalogCollatorFactoryOptions_2 } from '@backstage/plugin-search-backend-module-catalog'; import { DeferredEntity as DeferredEntity_2 } from '@backstage/plugin-catalog-node'; +import { DiscoveryService } from '@backstage/backend-plugin-api'; import { EntitiesSearchFilter as EntitiesSearchFilter_2 } from '@backstage/plugin-catalog-node'; import { Entity } from '@backstage/catalog-model'; import { EntityFilter as EntityFilter_2 } from '@backstage/plugin-catalog-node'; @@ -38,15 +40,16 @@ import { EntityProviderMutation as EntityProviderMutation_2 } from '@backstage/p import { EntityRelationSpec as EntityRelationSpec_2 } from '@backstage/plugin-catalog-node'; import { EventBroker } from '@backstage/plugin-events-node'; import { GetEntitiesRequest } from '@backstage/catalog-client'; +import { HttpAuthService } from '@backstage/backend-plugin-api'; import { LocationSpec as LocationSpec_2 } from '@backstage/plugin-catalog-common'; import { locationSpecToLocationEntity as locationSpecToLocationEntity_2 } from '@backstage/plugin-catalog-node'; import { locationSpecToMetadataName as locationSpecToMetadataName_2 } from '@backstage/plugin-catalog-node'; import { Logger } from 'winston'; import { Permission } from '@backstage/plugin-permission-common'; import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; -import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { PermissionRule } from '@backstage/plugin-permission-node'; import { PermissionRuleParams } from '@backstage/plugin-permission-common'; +import { PermissionsService } from '@backstage/backend-plugin-api'; import { PlaceholderResolver as PlaceholderResolver_2 } from '@backstage/plugin-catalog-node'; import { PlaceholderResolverParams as PlaceholderResolverParams_2 } from '@backstage/plugin-catalog-node'; import { PlaceholderResolverRead as PlaceholderResolverRead_2 } from '@backstage/plugin-catalog-node'; @@ -189,8 +192,11 @@ export type CatalogEnvironment = { database: PluginDatabaseManager; config: Config; reader: UrlReader; - permissions: PermissionEvaluator | PermissionAuthorizer; + permissions: PermissionsService | PermissionAuthorizer; scheduler?: PluginTaskScheduler; + discovery?: DiscoveryService; + auth?: AuthService; + httpAuth?: HttpAuthService; }; // @public diff --git a/plugins/catalog-backend/src/catalog/types.ts b/plugins/catalog-backend/src/catalog/types.ts index e9f5f154c2..9cc83098b3 100644 --- a/plugins/catalog-backend/src/catalog/types.ts +++ b/plugins/catalog-backend/src/catalog/types.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { BackstageCredentials } from '@backstage/backend-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { EntityFilter } from '@backstage/plugin-catalog-node'; @@ -48,7 +49,7 @@ export type EntitiesRequest = { fields?: (entity: Entity) => Entity; order?: EntityOrder[]; pagination?: EntityPagination; - authorizationToken?: string; + credentials: BackstageCredentials; }; export type EntitiesResponse = { @@ -75,9 +76,9 @@ export interface EntitiesBatchRequest { */ fields?: (entity: Entity) => Entity; /** - * The optional token that authorizes the action. + * The credentials that authorizes the action. */ - authorizationToken?: string; + credentials: BackstageCredentials; } export interface EntitiesBatchResponse { @@ -115,9 +116,9 @@ export interface EntityFacetsRequest { */ facets: string[]; /** - * The optional token that authorizes the action. + * The credentials that authorizes the action. */ - authorizationToken?: string; + credentials: BackstageCredentials; } /** @@ -157,7 +158,7 @@ export interface EntitiesCatalog { */ removeEntityByUid( uid: string, - options?: { authorizationToken?: string }, + options: { credentials: BackstageCredentials }, ): Promise; /** @@ -167,7 +168,7 @@ export interface EntitiesCatalog { */ entityAncestry( entityRef: string, - options?: { authorizationToken?: string }, + options: { credentials: BackstageCredentials }, ): Promise; /** @@ -192,7 +193,7 @@ export type QueryEntitiesRequest = * for the current and the next pagination requests. */ export interface QueryEntitiesInitialRequest { - authorizationToken?: string; + credentials: BackstageCredentials; fields?: (entity: Entity) => Entity; limit?: number; filter?: EntityFilter; @@ -208,7 +209,7 @@ export interface QueryEntitiesInitialRequest { * move forward or backward on the data. */ export interface QueryEntitiesCursorRequest { - authorizationToken?: string; + credentials: BackstageCredentials; fields?: (entity: Entity) => Entity; limit?: number; cursor: Cursor; diff --git a/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.test.ts index 4fe0311fff..3e2dee7ae0 100644 --- a/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.test.ts @@ -23,6 +23,7 @@ import { AuthorizedEntitiesCatalog } from './AuthorizedEntitiesCatalog'; import { Cursor, QueryEntitiesResponse } from '../catalog/types'; import { Entity } from '@backstage/catalog-model'; import { EntityFilter } from '@backstage/plugin-catalog-node'; +import { mockCredentials } from '@backstage/backend-test-utils'; describe('AuthorizedEntitiesCatalog', () => { const fakeCatalog = { @@ -60,7 +61,7 @@ describe('AuthorizedEntitiesCatalog', () => { expect( await catalog.entities({ - authorizationToken: 'abcd', + credentials: mockCredentials.none(), }), ).toEqual({ entities: [], @@ -77,10 +78,10 @@ describe('AuthorizedEntitiesCatalog', () => { ]); const catalog = createCatalog(isEntityKind); - await catalog.entities({ authorizationToken: 'abcd' }); + await catalog.entities({ credentials: mockCredentials.none() }); expect(fakeCatalog.entities).toHaveBeenCalledWith({ - authorizationToken: 'abcd', + credentials: mockCredentials.none(), filter: { key: 'kind', values: ['b'] }, }); }); @@ -91,10 +92,10 @@ describe('AuthorizedEntitiesCatalog', () => { ]); const catalog = createCatalog(); - await catalog.entities({ authorizationToken: 'abcd' }); + await catalog.entities({ credentials: mockCredentials.none() }); expect(fakeCatalog.entities).toHaveBeenCalledWith({ - authorizationToken: 'abcd', + credentials: mockCredentials.none(), }); }); }); @@ -109,7 +110,7 @@ describe('AuthorizedEntitiesCatalog', () => { await expect( catalog.entitiesBatch({ entityRefs: ['component:default/component-a'], - authorizationToken: 'abcd', + credentials: mockCredentials.none(), }), ).resolves.toEqual({ items: [null], @@ -132,12 +133,12 @@ describe('AuthorizedEntitiesCatalog', () => { await catalog.entitiesBatch({ entityRefs: ['component:default/component-a'], - authorizationToken: 'abcd', + credentials: mockCredentials.none(), }); expect(fakeCatalog.entitiesBatch).toHaveBeenCalledWith({ entityRefs: ['component:default/component-a'], - authorizationToken: 'abcd', + credentials: mockCredentials.none(), filter: { key: 'kind', values: ['b'] }, }); }); @@ -150,12 +151,12 @@ describe('AuthorizedEntitiesCatalog', () => { await catalog.entitiesBatch({ entityRefs: ['component:default/component-a'], - authorizationToken: 'abcd', + credentials: mockCredentials.none(), }); expect(fakeCatalog.entitiesBatch).toHaveBeenCalledWith({ entityRefs: ['component:default/component-a'], - authorizationToken: 'abcd', + credentials: mockCredentials.none(), }); }); }); @@ -169,7 +170,7 @@ describe('AuthorizedEntitiesCatalog', () => { await expect( catalog.queryEntities({ - authorizationToken: 'abcd', + credentials: mockCredentials.none(), filter: { key: 'kind', values: ['b'] }, }), ).resolves.toEqual({ @@ -188,12 +189,12 @@ describe('AuthorizedEntitiesCatalog', () => { const catalog = createCatalog(); await catalog.queryEntities({ - authorizationToken: 'abcd', + credentials: mockCredentials.none(), filter: { key: 'kind', values: ['b'] }, }); expect(fakeCatalog.queryEntities).toHaveBeenCalledWith({ - authorizationToken: 'abcd', + credentials: mockCredentials.none(), filter: { key: 'kind', values: ['b'] }, }); }); @@ -243,12 +244,12 @@ describe('AuthorizedEntitiesCatalog', () => { const catalog = createCatalog(isEntityKind); let response = await catalog.queryEntities({ - authorizationToken: 'abcd', + credentials: mockCredentials.none(), filter: { key: 'name', values: ['name'] }, }); expect(fakeCatalog.queryEntities).toHaveBeenCalledWith({ - authorizationToken: 'abcd', + credentials: mockCredentials.none(), filter: { allOf: [{ key: 'kind', values: ['b'] }, requestFilter] }, }); @@ -276,12 +277,12 @@ describe('AuthorizedEntitiesCatalog', () => { orderFieldValues: ['a', null], }; response = await catalog.queryEntities({ - authorizationToken: 'abcd', + credentials: mockCredentials.none(), cursor, }); expect(fakeCatalog.queryEntities).toHaveBeenNthCalledWith(2, { - authorizationToken: 'abcd', + credentials: mockCredentials.none(), cursor: { ...cursor, filter: { allOf: [{ key: 'kind', values: ['b'] }, requestFilter] }, @@ -324,7 +325,9 @@ describe('AuthorizedEntitiesCatalog', () => { ); await expect(() => - catalog.removeEntityByUid('uid', { authorizationToken: 'abcd' }), + catalog.removeEntityByUid('uid', { + credentials: mockCredentials.none(), + }), ).rejects.toThrow(NotAllowedError); }); @@ -343,7 +346,9 @@ describe('AuthorizedEntitiesCatalog', () => { ); await expect(() => - catalog.removeEntityByUid('uid', { authorizationToken: 'abcd' }), + catalog.removeEntityByUid('uid', { + credentials: mockCredentials.none(), + }), ).rejects.toThrow(NotAllowedError); }); @@ -363,9 +368,13 @@ describe('AuthorizedEntitiesCatalog', () => { createConditionTransformer([isEntityKind]), ); - await catalog.removeEntityByUid('uid', { authorizationToken: 'abcd' }); + await catalog.removeEntityByUid('uid', { + credentials: mockCredentials.none(), + }); - expect(fakeCatalog.removeEntityByUid).toHaveBeenCalledWith('uid'); + expect(fakeCatalog.removeEntityByUid).toHaveBeenCalledWith('uid', { + credentials: mockCredentials.none(), + }); }); it('calls underlying catalog method on ALLOW', async () => { @@ -383,9 +392,13 @@ describe('AuthorizedEntitiesCatalog', () => { createConditionTransformer([]), ); - await catalog.removeEntityByUid('uid', { authorizationToken: 'abcd' }); + await catalog.removeEntityByUid('uid', { + credentials: mockCredentials.none(), + }); - expect(fakeCatalog.removeEntityByUid).toHaveBeenCalledWith('uid'); + expect(fakeCatalog.removeEntityByUid).toHaveBeenCalledWith('uid', { + credentials: mockCredentials.none(), + }); }); }); @@ -398,7 +411,7 @@ describe('AuthorizedEntitiesCatalog', () => { await expect(() => catalog.entityAncestry('backstage:default/component', { - authorizationToken: 'Bearer abcd', + credentials: mockCredentials.none(), }), ).rejects.toThrow(NotAllowedError); }); @@ -443,7 +456,7 @@ describe('AuthorizedEntitiesCatalog', () => { const ancestryResult = await catalog.entityAncestry( 'backstage:default/a', - { authorizationToken: 'Bearer abcd' }, + { credentials: mockCredentials.none() }, ); expect(ancestryResult).toEqual({ @@ -476,7 +489,7 @@ describe('AuthorizedEntitiesCatalog', () => { expect( await catalog.facets({ facets: ['a'], - authorizationToken: 'abcd', + credentials: mockCredentials.none(), }), ).toEqual({ facets: { a: [] }, @@ -492,11 +505,14 @@ describe('AuthorizedEntitiesCatalog', () => { ]); const catalog = createCatalog(isEntityKind); - await catalog.facets({ facets: ['a'], authorizationToken: 'abcd' }); + await catalog.facets({ + facets: ['a'], + credentials: mockCredentials.none(), + }); expect(fakeCatalog.facets).toHaveBeenCalledWith({ facets: ['a'], - authorizationToken: 'abcd', + credentials: mockCredentials.none(), filter: { key: 'kind', values: ['b'] }, }); }); @@ -507,11 +523,14 @@ describe('AuthorizedEntitiesCatalog', () => { ]); const catalog = createCatalog(); - await catalog.facets({ facets: ['a'], authorizationToken: 'abcd' }); + await catalog.facets({ + facets: ['a'], + credentials: mockCredentials.none(), + }); expect(fakeCatalog.facets).toHaveBeenCalledWith({ facets: ['a'], - authorizationToken: 'abcd', + credentials: mockCredentials.none(), }); }); }); diff --git a/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.ts b/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.ts index f302a2b75a..5358e90825 100644 --- a/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.ts @@ -20,10 +20,7 @@ import { catalogEntityReadPermission, } from '@backstage/plugin-catalog-common/alpha'; import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; -import { - AuthorizeResult, - PermissionEvaluator, -} from '@backstage/plugin-permission-common'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { ConditionTransformer } from '@backstage/plugin-permission-node'; import { Cursor, @@ -41,19 +38,23 @@ import { import { basicEntityFilter } from './request'; import { isQueryEntitiesCursorRequest } from './util'; import { EntityFilter } from '@backstage/plugin-catalog-node'; +import { + BackstageCredentials, + PermissionsService, +} from '@backstage/backend-plugin-api'; export class AuthorizedEntitiesCatalog implements EntitiesCatalog { constructor( private readonly entitiesCatalog: EntitiesCatalog, - private readonly permissionApi: PermissionEvaluator, + private readonly permissionApi: PermissionsService, private readonly transformConditions: ConditionTransformer, ) {} - async entities(request?: EntitiesRequest): Promise { + async entities(request: EntitiesRequest): Promise { const authorizeDecision = ( await this.permissionApi.authorizeConditional( [{ permission: catalogEntityReadPermission }], - { token: request?.authorizationToken }, + { credentials: request.credentials }, ) )[0]; @@ -85,7 +86,7 @@ export class AuthorizedEntitiesCatalog implements EntitiesCatalog { const authorizeDecision = ( await this.permissionApi.authorizeConditional( [{ permission: catalogEntityReadPermission }], - { token: request?.authorizationToken }, + { credentials: request.credentials }, ) )[0]; @@ -116,7 +117,7 @@ export class AuthorizedEntitiesCatalog implements EntitiesCatalog { const authorizeDecision = ( await this.permissionApi.authorizeConditional( [{ permission: catalogEntityReadPermission }], - { token: request.authorizationToken }, + { credentials: request.credentials }, ) )[0]; @@ -186,12 +187,12 @@ export class AuthorizedEntitiesCatalog implements EntitiesCatalog { async removeEntityByUid( uid: string, - options?: { authorizationToken?: string }, + options: { credentials: BackstageCredentials }, ): Promise { const authorizeResponse = ( await this.permissionApi.authorizeConditional( [{ permission: catalogEntityDeletePermission }], - { token: options?.authorizationToken }, + { credentials: options.credentials }, ) )[0]; if (authorizeResponse.result === AuthorizeResult.DENY) { @@ -202,6 +203,7 @@ export class AuthorizedEntitiesCatalog implements EntitiesCatalog { authorizeResponse.conditions, ); const { entities } = await this.entitiesCatalog.entities({ + credentials: options.credentials, filter: { allOf: [permissionFilter, basicEntityFilter({ 'metadata.uid': uid })], }, @@ -210,30 +212,35 @@ export class AuthorizedEntitiesCatalog implements EntitiesCatalog { throw new NotAllowedError(); } } - return this.entitiesCatalog.removeEntityByUid(uid); + return this.entitiesCatalog.removeEntityByUid(uid, { + credentials: options.credentials, + }); } async entityAncestry( entityRef: string, - options?: { authorizationToken?: string }, + options: { credentials: BackstageCredentials }, ): Promise { const rootEntityAuthorizeResponse = ( await this.permissionApi.authorize( [{ permission: catalogEntityReadPermission, resourceRef: entityRef }], - { token: options?.authorizationToken }, + { credentials: options.credentials }, ) )[0]; if (rootEntityAuthorizeResponse.result === AuthorizeResult.DENY) { throw new NotAllowedError(); } - const ancestryResult = await this.entitiesCatalog.entityAncestry(entityRef); + const ancestryResult = await this.entitiesCatalog.entityAncestry( + entityRef, + { credentials: options.credentials }, + ); const authorizeResponse = await this.permissionApi.authorize( ancestryResult.items.map(item => ({ permission: catalogEntityReadPermission, resourceRef: stringifyEntityRef(item.entity), })), - { token: options?.authorizationToken }, + { credentials: options.credentials }, ); const unauthorizedAncestryItems = ancestryResult.items.filter( (_, index) => authorizeResponse[index].result === AuthorizeResult.DENY, @@ -268,7 +275,7 @@ export class AuthorizedEntitiesCatalog implements EntitiesCatalog { const authorizeDecision = ( await this.permissionApi.authorizeConditional( [{ permission: catalogEntityReadPermission }], - { token: request?.authorizationToken }, + { credentials: request.credentials }, ) )[0]; diff --git a/plugins/catalog-backend/src/service/AuthorizedLocationService.test.ts b/plugins/catalog-backend/src/service/AuthorizedLocationService.test.ts index c2ffee8003..2eae1c7553 100644 --- a/plugins/catalog-backend/src/service/AuthorizedLocationService.test.ts +++ b/plugins/catalog-backend/src/service/AuthorizedLocationService.test.ts @@ -17,6 +17,7 @@ import { NotAllowedError, NotFoundError } from '@backstage/errors'; import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { AuthorizedLocationService } from './AuthorizedLocationService'; +import { mockCredentials } from '@backstage/backend-test-utils'; describe('AuthorizedLocationService', () => { const fakeLocationService = { @@ -45,6 +46,10 @@ describe('AuthorizedLocationService', () => { const createService = () => new AuthorizedLocationService(fakeLocationService, fakePermissionApi); + const mockOptions = { + credentials: mockCredentials.none(), + }; + afterEach(() => { jest.resetAllMocks(); }); @@ -55,13 +60,12 @@ describe('AuthorizedLocationService', () => { const service = createService(); const spec = { type: 'type', target: 'target' }; - await service.createLocation(spec, false, { - authorizationToken: 'Bearer authtoken', - }); + await service.createLocation(spec, false, mockOptions); expect(fakeLocationService.createLocation).toHaveBeenCalledWith( spec, false, + mockOptions, ); }); @@ -71,9 +75,7 @@ describe('AuthorizedLocationService', () => { const spec = { type: 'type', target: 'target' }; await expect(() => - service.createLocation(spec, false, { - authorizationToken: 'Bearer authtoken', - }), + service.createLocation(spec, false, mockOptions), ).rejects.toThrow(NotAllowedError); }); }); @@ -83,7 +85,7 @@ describe('AuthorizedLocationService', () => { mockAllow(); const service = createService(); - await service.listLocations({ authorizationToken: 'Bearer authtoken' }); + await service.listLocations(mockOptions); expect(fakeLocationService.listLocations).toHaveBeenCalled(); }); @@ -92,9 +94,7 @@ describe('AuthorizedLocationService', () => { mockDeny(); const service = createService(); - const locations = await service.listLocations({ - authorizationToken: 'Bearer authtoken', - }); + const locations = await service.listLocations(mockOptions); expect(locations).toEqual([]); }); @@ -105,11 +105,12 @@ describe('AuthorizedLocationService', () => { mockAllow(); const service = createService(); - await service.getLocation('id', { - authorizationToken: 'Bearer authtoken', - }); + await service.getLocation('id', mockOptions); - expect(fakeLocationService.getLocation).toHaveBeenCalledWith('id'); + expect(fakeLocationService.getLocation).toHaveBeenCalledWith( + 'id', + mockOptions, + ); }); it('throws error on DENY', async () => { @@ -117,7 +118,7 @@ describe('AuthorizedLocationService', () => { const service = createService(); await expect(() => - service.getLocation('id', { authorizationToken: 'Bearer authtoken' }), + service.getLocation('id', mockOptions), ).rejects.toThrow(NotFoundError); }); }); @@ -127,11 +128,12 @@ describe('AuthorizedLocationService', () => { mockAllow(); const service = createService(); - await service.deleteLocation('id', { - authorizationToken: 'Bearer authtoken', - }); + await service.deleteLocation('id', mockOptions); - expect(fakeLocationService.deleteLocation).toHaveBeenCalledWith('id'); + expect(fakeLocationService.deleteLocation).toHaveBeenCalledWith( + 'id', + mockOptions, + ); }); it('throws error on DENY', async () => { @@ -139,9 +141,7 @@ describe('AuthorizedLocationService', () => { const service = createService(); await expect(() => - service.deleteLocation('id', { - authorizationToken: 'Bearer authtoken', - }), + service.deleteLocation('id', mockOptions), ).rejects.toThrow(NotAllowedError); }); }); @@ -153,16 +153,17 @@ describe('AuthorizedLocationService', () => { await service.getLocationByEntity( { kind: 'c', namespace: 'ns', name: 'n' }, - { - authorizationToken: 'Bearer authtoken', - }, + mockOptions, ); - expect(fakeLocationService.getLocationByEntity).toHaveBeenCalledWith({ - kind: 'c', - namespace: 'ns', - name: 'n', - }); + expect(fakeLocationService.getLocationByEntity).toHaveBeenCalledWith( + { + kind: 'c', + namespace: 'ns', + name: 'n', + }, + mockOptions, + ); }); it('throws error on DENY', async () => { @@ -172,7 +173,7 @@ describe('AuthorizedLocationService', () => { await expect(() => service.getLocationByEntity( { kind: 'c', namespace: 'ns', name: 'n' }, - { authorizationToken: 'Bearer authtoken' }, + mockOptions, ), ).rejects.toThrow(NotFoundError); }); diff --git a/plugins/catalog-backend/src/service/AuthorizedLocationService.ts b/plugins/catalog-backend/src/service/AuthorizedLocationService.ts index 0b9d50b20e..3eb3eede12 100644 --- a/plugins/catalog-backend/src/service/AuthorizedLocationService.ts +++ b/plugins/catalog-backend/src/service/AuthorizedLocationService.ts @@ -22,23 +22,24 @@ import { catalogLocationDeletePermission, catalogLocationReadPermission, } from '@backstage/plugin-catalog-common/alpha'; -import { - AuthorizeResult, - PermissionEvaluator, -} from '@backstage/plugin-permission-common'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { LocationInput, LocationService } from './types'; +import { + BackstageCredentials, + PermissionsService, +} from '@backstage/backend-plugin-api'; export class AuthorizedLocationService implements LocationService { constructor( private readonly locationService: LocationService, - private readonly permissionApi: PermissionEvaluator, + private readonly permissionApi: PermissionsService, ) {} async createLocation( spec: LocationInput, dryRun: boolean, - options?: { - authorizationToken?: string; + options: { + credentials: BackstageCredentials; }, ): Promise<{ location: Location; @@ -48,7 +49,7 @@ export class AuthorizedLocationService implements LocationService { const authorizationResponse = ( await this.permissionApi.authorize( [{ permission: catalogLocationCreatePermission }], - { token: options?.authorizationToken }, + { credentials: options.credentials }, ) )[0]; @@ -56,16 +57,16 @@ export class AuthorizedLocationService implements LocationService { throw new NotAllowedError(); } - return this.locationService.createLocation(spec, dryRun); + return this.locationService.createLocation(spec, dryRun, options); } - async listLocations(options?: { - authorizationToken?: string; + async listLocations(options: { + credentials: BackstageCredentials; }): Promise { const authorizationResponse = ( await this.permissionApi.authorize( [{ permission: catalogLocationReadPermission }], - { token: options?.authorizationToken }, + { credentials: options.credentials }, ) )[0]; @@ -73,17 +74,17 @@ export class AuthorizedLocationService implements LocationService { return []; } - return this.locationService.listLocations(); + return this.locationService.listLocations(options); } async getLocation( id: string, - options?: { authorizationToken?: string }, + options: { credentials: BackstageCredentials }, ): Promise { const authorizationResponse = ( await this.permissionApi.authorize( [{ permission: catalogLocationReadPermission }], - { token: options?.authorizationToken }, + { credentials: options.credentials }, ) )[0]; @@ -91,17 +92,17 @@ export class AuthorizedLocationService implements LocationService { throw new NotFoundError(`Found no location with ID ${id}`); } - return this.locationService.getLocation(id); + return this.locationService.getLocation(id, options); } async deleteLocation( id: string, - options?: { authorizationToken?: string }, + options: { credentials: BackstageCredentials }, ): Promise { const authorizationResponse = ( await this.permissionApi.authorize( [{ permission: catalogLocationDeletePermission }], - { token: options?.authorizationToken }, + { credentials: options.credentials }, ) )[0]; @@ -109,23 +110,23 @@ export class AuthorizedLocationService implements LocationService { throw new NotAllowedError(); } - return this.locationService.deleteLocation(id); + return this.locationService.deleteLocation(id, options); } async getLocationByEntity( entityRef: CompoundEntityRef | string, - options?: { authorizationToken?: string | undefined } | undefined, + options: { credentials: BackstageCredentials }, ): Promise { const authorizationResponse = ( await this.permissionApi.authorize( [{ permission: catalogLocationReadPermission }], - { token: options?.authorizationToken }, + { credentials: options.credentials }, ) )[0]; if (authorizationResponse.result === AuthorizeResult.DENY) { throw new NotFoundError(); } - return this.locationService.getLocationByEntity(entityRef); + return this.locationService.getLocationByEntity(entityRef, options); } } diff --git a/plugins/catalog-backend/src/service/AuthorizedRefreshService.test.ts b/plugins/catalog-backend/src/service/AuthorizedRefreshService.test.ts index f37bd0ec49..ba9ef2db73 100644 --- a/plugins/catalog-backend/src/service/AuthorizedRefreshService.test.ts +++ b/plugins/catalog-backend/src/service/AuthorizedRefreshService.test.ts @@ -18,6 +18,7 @@ import { NotAllowedError } from '@backstage/errors'; import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { ServerPermissionClient } from '@backstage/plugin-permission-node'; import { AuthorizedRefreshService } from './AuthorizedRefreshService'; +import { mockCredentials } from '@backstage/backend-test-utils'; describe('AuthorizedRefreshService', () => { const refreshService = { @@ -46,7 +47,7 @@ describe('AuthorizedRefreshService', () => { await expect(() => authorizedService.refresh({ entityRef: 'some entity ref', - authorizationToken: 'some auth token', + credentials: mockCredentials.none(), }), ).rejects.toThrow(NotAllowedError); }); @@ -64,7 +65,7 @@ describe('AuthorizedRefreshService', () => { const options = { entityRef: 'some entity ref', - authorizationToken: 'some auth token', + credentials: mockCredentials.none(), }; await authorizedService.refresh(options); diff --git a/plugins/catalog-backend/src/service/AuthorizedRefreshService.ts b/plugins/catalog-backend/src/service/AuthorizedRefreshService.ts index cbc728750a..a5a2491a04 100644 --- a/plugins/catalog-backend/src/service/AuthorizedRefreshService.ts +++ b/plugins/catalog-backend/src/service/AuthorizedRefreshService.ts @@ -16,16 +16,14 @@ import { NotAllowedError } from '@backstage/errors'; import { catalogEntityRefreshPermission } from '@backstage/plugin-catalog-common/alpha'; -import { - AuthorizeResult, - PermissionEvaluator, -} from '@backstage/plugin-permission-common'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { RefreshOptions, RefreshService } from './types'; +import { PermissionsService } from '@backstage/backend-plugin-api'; export class AuthorizedRefreshService implements RefreshService { constructor( private readonly service: RefreshService, - private readonly permissionApi: PermissionEvaluator, + private readonly permissionApi: PermissionsService, ) {} async refresh(options: RefreshOptions) { @@ -37,7 +35,7 @@ export class AuthorizedRefreshService implements RefreshService { resourceRef: options.entityRef, }, ], - { token: options.authorizationToken }, + { credentials: options.credentials }, ) )[0]; if (authorizeDecision.result !== AuthorizeResult.ALLOW) { diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index 0abaf2ec0a..7fcee231fd 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -14,7 +14,12 @@ * limitations under the License. */ -import { PluginDatabaseManager, UrlReader } from '@backstage/backend-common'; +import { + PluginDatabaseManager, + HostDiscovery, + UrlReader, + createLegacyAuthAdapters, +} from '@backstage/backend-common'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { DefaultNamespaceEntityPolicy, @@ -84,7 +89,6 @@ import { permissionRules as catalogPermissionRules } from '../permissions/rules' import { PermissionRule } from '@backstage/plugin-permission-node'; import { PermissionAuthorizer, - PermissionEvaluator, toPermissionEvaluator, } from '@backstage/plugin-permission-common'; import { @@ -102,6 +106,12 @@ import { DefaultProviderDatabase } from '../database/DefaultProviderDatabase'; import { DefaultCatalogDatabase } from '../database/DefaultCatalogDatabase'; import { EventBroker } from '@backstage/plugin-events-node'; import { durationToMilliseconds } from '@backstage/types'; +import { + DiscoveryService, + AuthService, + HttpAuthService, + PermissionsService, +} from '@backstage/backend-plugin-api'; /** * This is a duplicate of the alpha `CatalogPermissionRule` type, for use in the stable API. @@ -118,8 +128,11 @@ export type CatalogEnvironment = { database: PluginDatabaseManager; config: Config; reader: UrlReader; - permissions: PermissionEvaluator | PermissionAuthorizer; + permissions: PermissionsService | PermissionAuthorizer; scheduler?: PluginTaskScheduler; + discovery?: DiscoveryService; + auth?: AuthService; + httpAuth?: HttpAuthService; }; /** @@ -438,7 +451,19 @@ export class CatalogBuilder { processingEngine: CatalogProcessingEngine; router: Router; }> { - const { config, database, logger, permissions, scheduler } = this.env; + const { + config, + database, + logger, + permissions, + scheduler, + discovery = HostDiscovery.fromConfig(config), + } = this.env; + + const { auth, httpAuth } = createLegacyAuthAdapters({ + ...this.env, + discovery, + }); const policy = this.buildEntityPolicy(); const processors = this.buildProcessors(); @@ -486,25 +511,26 @@ export class CatalogBuilder { stitcher, }); - let permissionEvaluator: PermissionEvaluator; + let permissionsService: PermissionsService; if ('authorizeConditional' in permissions) { - permissionEvaluator = permissions as PermissionEvaluator; + permissionsService = permissions as PermissionsService; } else { logger.warn( 'PermissionAuthorizer is deprecated. Please use an instance of PermissionEvaluator instead of PermissionAuthorizer in PluginEnvironment#permissions', ); - permissionEvaluator = toPermissionEvaluator(permissions); + permissionsService = toPermissionEvaluator(permissions); } const entitiesCatalog = new AuthorizedEntitiesCatalog( unauthorizedEntitiesCatalog, - permissionEvaluator, + permissionsService, createConditionTransformer(this.permissionRules), ); const permissionIntegrationRouter = createPermissionIntegrationRouter({ resourceType: RESOURCE_TYPE_CATALOG_ENTITY, getResources: async (resourceRefs: string[]) => { const { entities } = await unauthorizedEntitiesCatalog.entities({ + credentials: await auth.getOwnServiceCredentials(), filter: { anyOf: resourceRefs.map(resourceRef => { const { kind, namespace, name } = parseEntityRef(resourceRef); @@ -558,12 +584,13 @@ export class CatalogBuilder { new DefaultLocationService(locationStore, orchestrator, { allowedLocationTypes: this.allowedLocationType, }), - permissionEvaluator, + permissionsService, ); const refreshService = new AuthorizedRefreshService( new DefaultRefreshService({ database: catalogDatabase }), - permissionEvaluator, + permissionsService, ); + const router = await createRouter({ entitiesCatalog, locationAnalyzer, @@ -573,6 +600,8 @@ export class CatalogBuilder { logger, config, permissionIntegrationRouter, + auth, + httpAuth, }); await connectEntityProviders(providerDatabase, entityProviders); diff --git a/plugins/catalog-backend/src/service/CatalogPlugin.ts b/plugins/catalog-backend/src/service/CatalogPlugin.ts index 255df84d58..87e4316c11 100644 --- a/plugins/catalog-backend/src/service/CatalogPlugin.ts +++ b/plugins/catalog-backend/src/service/CatalogPlugin.ts @@ -178,6 +178,9 @@ export const catalogPlugin = createBackendPlugin({ httpRouter: coreServices.httpRouter, lifecycle: coreServices.lifecycle, scheduler: coreServices.scheduler, + discovery: coreServices.discovery, + auth: coreServices.auth, + httpAuth: coreServices.httpAuth, }, async init({ logger, @@ -188,6 +191,9 @@ export const catalogPlugin = createBackendPlugin({ httpRouter, lifecycle, scheduler, + discovery, + auth, + httpAuth, }) { const winstonLogger = loggerToWinstonLogger(logger); const builder = await CatalogBuilder.create({ @@ -197,6 +203,9 @@ export const catalogPlugin = createBackendPlugin({ database, scheduler, logger: winstonLogger, + discovery, + auth, + httpAuth, }); if (processingExtensions.onProcessingErrorHandler) { builder.subscribe({ diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts index fe7d57c89a..4d8dc7c7a4 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts @@ -15,7 +15,11 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; +import { + TestDatabaseId, + TestDatabases, + mockCredentials, +} from '@backstage/backend-test-utils'; import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; import { Knex } from 'knex'; import { v4 as uuid, v4 } from 'uuid'; @@ -304,8 +308,10 @@ describe('DefaultEntitiesCatalog', () => { const testFilter = { key: 'spec.test', }; - const request = { filter: testFilter }; - const { entities } = await catalog.entities(request); + const { entities } = await catalog.entities({ + filter: testFilter, + credentials: mockCredentials.none(), + }); expect(entities.length).toBe(1); expect(entities[0]).toEqual(entity2); @@ -343,8 +349,10 @@ describe('DefaultEntitiesCatalog', () => { key: 'spec.test', }, }; - const request = { filter: testFilter }; - const { entities } = await catalog.entities(request); + const { entities } = await catalog.entities({ + filter: testFilter, + credentials: mockCredentials.none(), + }); expect(entities.length).toBe(1); expect(entities[0]).toEqual(entity1); @@ -406,7 +414,7 @@ describe('DefaultEntitiesCatalog', () => { values: ['red'], }, }; - const request = { + const { entities } = await catalog.entities({ filter: { allOf: [ testFilter1, @@ -415,8 +423,8 @@ describe('DefaultEntitiesCatalog', () => { }, ], }, - }; - const { entities } = await catalog.entities(request); + credentials: mockCredentials.none(), + }); expect(entities.length).toBe(2); expect(entities).toContainEqual(entity2); @@ -455,14 +463,15 @@ describe('DefaultEntitiesCatalog', () => { const testFilter2 = { key: 'metadata.desc', }; - const request = { + const { entities } = await catalog.entities({ filter: { not: { allOf: [testFilter1, testFilter2], }, }, - }; - const { entities } = await catalog.entities(request); + + credentials: mockCredentials.none(), + }); expect(entities.length).toBe(1); expect(entities).toContainEqual(entity1); @@ -498,8 +507,10 @@ describe('DefaultEntitiesCatalog', () => { key: 'kind', values: [], }; - const request = { filter: testFilter }; - const { entities } = await catalog.entities(request); + const { entities } = await catalog.entities({ + filter: testFilter, + credentials: mockCredentials.none(), + }); expect(entities.length).toBe(0); }, @@ -603,9 +614,11 @@ describe('DefaultEntitiesCatalog', () => { stitcher, }); - function f(request: EntitiesRequest): Promise { + function f( + request: Omit, + ): Promise { return catalog - .entities(request) + .entities({ ...request, credentials: mockCredentials.none() }) .then(response => response.entities.map(e => e.metadata.name)); } @@ -701,6 +714,7 @@ describe('DefaultEntitiesCatalog', () => { 'k:default/does-not-exist', 'k:default/two', ], + credentials: mockCredentials.none(), }); expect(items.map(e => e && stringifyEntityRef(e))).toEqual([ @@ -749,6 +763,7 @@ describe('DefaultEntitiesCatalog', () => { const { items } = await catalog.entitiesBatch({ entityRefs: ['k:default/two', 'k:default/one'], filter: { key: 'spec.owner', values: ['me'] }, + credentials: mockCredentials.none(), }); expect(items.map(e => e && stringifyEntityRef(e))).toEqual([ @@ -804,6 +819,7 @@ describe('DefaultEntitiesCatalog', () => { filter, limit, orderFields: [{ field: 'metadata.name', order: 'asc' }], + credentials: mockCredentials.none(), }; const response1 = await catalog.queryEntities(request1); expect(response1.items).toEqual([entityFrom('A'), entityFrom('B')]); @@ -815,6 +831,7 @@ describe('DefaultEntitiesCatalog', () => { const request2: QueryEntitiesCursorRequest = { cursor: response1.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response2 = await catalog.queryEntities(request2); expect(response2.items).toEqual([entityFrom('C'), entityFrom('D')]); @@ -826,6 +843,7 @@ describe('DefaultEntitiesCatalog', () => { const request3: QueryEntitiesCursorRequest = { cursor: response2.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response3 = await catalog.queryEntities(request3); expect(response3.items).toEqual([entityFrom('E'), entityFrom('F')]); @@ -837,6 +855,7 @@ describe('DefaultEntitiesCatalog', () => { const request4: QueryEntitiesCursorRequest = { cursor: response3.pageInfo.prevCursor!, limit, + credentials: mockCredentials.none(), }; const response4 = await catalog.queryEntities(request4); expect(response4.items).toEqual([entityFrom('C'), entityFrom('D')]); @@ -848,6 +867,7 @@ describe('DefaultEntitiesCatalog', () => { const request5: QueryEntitiesCursorRequest = { cursor: response4.pageInfo.prevCursor!, limit, + credentials: mockCredentials.none(), }; const response5 = await catalog.queryEntities(request5); expect(response5.items).toEqual([entityFrom('A'), entityFrom('B')]); @@ -859,6 +879,7 @@ describe('DefaultEntitiesCatalog', () => { const request6: QueryEntitiesCursorRequest = { cursor: response5.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response6 = await catalog.queryEntities(request6); expect(response6.items).toEqual([entityFrom('C'), entityFrom('D')]); @@ -870,6 +891,7 @@ describe('DefaultEntitiesCatalog', () => { const request7: QueryEntitiesCursorRequest = { cursor: response6.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response7 = await catalog.queryEntities(request7); expect(response7.items).toEqual([entityFrom('E'), entityFrom('F')]); @@ -881,6 +903,7 @@ describe('DefaultEntitiesCatalog', () => { const request7bis: QueryEntitiesCursorRequest = { cursor: response6.pageInfo.nextCursor!, limit: limit + 1, + credentials: mockCredentials.none(), }; const response7bis = await catalog.queryEntities(request7bis); expect(response7bis.items).toEqual([ @@ -896,6 +919,7 @@ describe('DefaultEntitiesCatalog', () => { const request8: QueryEntitiesCursorRequest = { cursor: response7.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response8 = await catalog.queryEntities(request8); expect(response8.items).toEqual([entityFrom('G')]); @@ -949,6 +973,7 @@ describe('DefaultEntitiesCatalog', () => { filter, limit, orderFields: [{ field: 'metadata.name', order: 'desc' }], + credentials: mockCredentials.none(), }; const response1 = await catalog.queryEntities(request1); expect(response1.items).toEqual([entityFrom('G'), entityFrom('F')]); @@ -960,6 +985,7 @@ describe('DefaultEntitiesCatalog', () => { const request2: QueryEntitiesCursorRequest = { cursor: response1.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response2 = await catalog.queryEntities(request2); expect(response2.items).toEqual([entityFrom('E'), entityFrom('D')]); @@ -971,6 +997,7 @@ describe('DefaultEntitiesCatalog', () => { const request3: QueryEntitiesCursorRequest = { cursor: response2.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response3 = await catalog.queryEntities(request3); expect(response3.items).toEqual([entityFrom('C'), entityFrom('B')]); @@ -982,6 +1009,7 @@ describe('DefaultEntitiesCatalog', () => { const request4: QueryEntitiesCursorRequest = { cursor: response3.pageInfo.prevCursor!, limit, + credentials: mockCredentials.none(), }; const response4 = await catalog.queryEntities(request4); @@ -994,6 +1022,7 @@ describe('DefaultEntitiesCatalog', () => { const request5: QueryEntitiesCursorRequest = { cursor: response4.pageInfo.prevCursor!, limit, + credentials: mockCredentials.none(), }; const response5 = await catalog.queryEntities(request5); expect(response5.items).toEqual([entityFrom('G'), entityFrom('F')]); @@ -1005,6 +1034,7 @@ describe('DefaultEntitiesCatalog', () => { const request6: QueryEntitiesCursorRequest = { cursor: response5.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response6 = await catalog.queryEntities(request6); expect(response6.items).toEqual([entityFrom('E'), entityFrom('D')]); @@ -1016,6 +1046,7 @@ describe('DefaultEntitiesCatalog', () => { const request7: QueryEntitiesCursorRequest = { cursor: response6.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response7 = await catalog.queryEntities(request7); expect(response7.items).toEqual([entityFrom('C'), entityFrom('B')]); @@ -1027,6 +1058,7 @@ describe('DefaultEntitiesCatalog', () => { const request7bis: QueryEntitiesCursorRequest = { cursor: response6.pageInfo.nextCursor!, limit: limit + 1, + credentials: mockCredentials.none(), }; const response7bis = await catalog.queryEntities(request7bis); expect(response7bis.items).toEqual([ @@ -1042,6 +1074,7 @@ describe('DefaultEntitiesCatalog', () => { const request8: QueryEntitiesCursorRequest = { cursor: response7.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response8 = await catalog.queryEntities(request8); expect(response8.items).toEqual([entityFrom('A')]); @@ -1094,6 +1127,7 @@ describe('DefaultEntitiesCatalog', () => { orderFields: [{ field: 'metadata.name', order: 'asc' }], fullTextFilter: { term: 'cAt ' }, + credentials: mockCredentials.none(), }; const response = await catalog.queryEntities(request); expect(response.items).toEqual([ @@ -1152,6 +1186,7 @@ describe('DefaultEntitiesCatalog', () => { filter, limit: 100, fullTextFilter: { term: 'cAt ', fields: ['metadata.title'] }, + credentials: mockCredentials.none(), }; const response = await catalog.queryEntities(request); expect(response.items).toEqual([ @@ -1177,6 +1212,7 @@ describe('DefaultEntitiesCatalog', () => { const paginatedResponseNext = await catalog.queryEntities({ cursor: paginatedResponse.pageInfo.nextCursor!, + credentials: mockCredentials.none(), }); expect(paginatedResponseNext.items).toEqual([ entityFrom('4', { uid: 'id4', title: 'dogcat' }), @@ -1187,6 +1223,7 @@ describe('DefaultEntitiesCatalog', () => { const paginatedResponsePrev = await catalog.queryEntities({ cursor: paginatedResponseNext.pageInfo.prevCursor!, + credentials: mockCredentials.none(), }); expect(paginatedResponsePrev).toMatchObject(paginatedResponse); }, @@ -1251,6 +1288,7 @@ describe('DefaultEntitiesCatalog', () => { term: 'KiNg ', fields: ['metadata.title', 'metadata.name'], }, + credentials: mockCredentials.none(), }; const response = await catalog.queryEntities(request); @@ -1278,6 +1316,7 @@ describe('DefaultEntitiesCatalog', () => { const paginatedResponseNext = await catalog.queryEntities({ cursor: paginatedResponse.pageInfo.nextCursor!, + credentials: mockCredentials.none(), }); expect(paginatedResponseNext.items).toEqual([ entityFrom('NotACatKing', { uid: 'id2', title: 'atcatss' }), @@ -1289,6 +1328,7 @@ describe('DefaultEntitiesCatalog', () => { const paginatedResponsePrev = await catalog.queryEntities({ cursor: paginatedResponseNext.pageInfo.prevCursor!, + credentials: mockCredentials.none(), }); expect(paginatedResponsePrev).toMatchObject(paginatedResponse); }, @@ -1319,6 +1359,7 @@ describe('DefaultEntitiesCatalog', () => { const request: QueryEntitiesInitialRequest = { limit: 0, + credentials: mockCredentials.none(), }; const response = await catalog.queryEntities(request); expect(response).toEqual({ totalItems: 20, items: [], pageInfo: {} }); @@ -1351,6 +1392,7 @@ describe('DefaultEntitiesCatalog', () => { const request1: QueryEntitiesInitialRequest = { limit, orderFields: [{ field: 'metadata.name', order: 'asc' }], + credentials: mockCredentials.none(), }; const response1 = await catalog.queryEntities(request1); expect(response1.items).toMatchObject([ @@ -1365,6 +1407,7 @@ describe('DefaultEntitiesCatalog', () => { const request2: QueryEntitiesCursorRequest = { cursor: response1.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response2 = await catalog.queryEntities(request2); expect(response2.items).toMatchObject([ @@ -1379,6 +1422,7 @@ describe('DefaultEntitiesCatalog', () => { const request3: QueryEntitiesCursorRequest = { cursor: response2.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response3 = await catalog.queryEntities(request3); expect(response3.items).toEqual([entityFrom('CC'), entityFrom('DD')]); @@ -1390,6 +1434,7 @@ describe('DefaultEntitiesCatalog', () => { const request4: QueryEntitiesCursorRequest = { cursor: response3.pageInfo.prevCursor!, limit, + credentials: mockCredentials.none(), }; const response4 = await catalog.queryEntities(request4); expect(response4.items).toMatchObject([ @@ -1404,6 +1449,7 @@ describe('DefaultEntitiesCatalog', () => { const request5: QueryEntitiesCursorRequest = { cursor: response4.pageInfo.prevCursor!, limit, + credentials: mockCredentials.none(), }; const response5 = await catalog.queryEntities(request5); expect(response5.items).toMatchObject([ @@ -1471,6 +1517,7 @@ describe('DefaultEntitiesCatalog', () => { values: ['included'], }, orderFields: [{ field: 'metadata.name', order: 'asc' }], + credentials: mockCredentials.none(), }; const response1 = await catalog.queryEntities(request1); expect(response1.items).toMatchObject([ @@ -1485,6 +1532,7 @@ describe('DefaultEntitiesCatalog', () => { const request2: QueryEntitiesCursorRequest = { cursor: response1.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response2 = await catalog.queryEntities(request2); expect(response2.items).toMatchObject([ @@ -1528,6 +1576,7 @@ describe('DefaultEntitiesCatalog', () => { // initial request const request1: QueryEntitiesInitialRequest = { limit, + credentials: mockCredentials.none(), }; const response1 = await catalog.queryEntities(request1); expect(response1.items).toMatchObject([ @@ -1542,6 +1591,7 @@ describe('DefaultEntitiesCatalog', () => { const request2: QueryEntitiesCursorRequest = { cursor: response1.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response2 = await catalog.queryEntities(request2); expect(response2.items).toMatchObject([ @@ -1556,6 +1606,7 @@ describe('DefaultEntitiesCatalog', () => { const request3: QueryEntitiesCursorRequest = { cursor: response2.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response3 = await catalog.queryEntities(request3); expect(response3.items).toMatchObject([ @@ -1570,6 +1621,7 @@ describe('DefaultEntitiesCatalog', () => { const request4: QueryEntitiesCursorRequest = { cursor: response3.pageInfo.prevCursor!, limit, + credentials: mockCredentials.none(), }; const response4 = await catalog.queryEntities(request4); expect(response4.items).toMatchObject([ @@ -1584,6 +1636,7 @@ describe('DefaultEntitiesCatalog', () => { const request5: QueryEntitiesCursorRequest = { cursor: response4.pageInfo.prevCursor!, limit, + credentials: mockCredentials.none(), }; const response5 = await catalog.queryEntities(request5); expect(response5.items).toMatchObject([ @@ -1719,7 +1772,12 @@ describe('DefaultEntitiesCatalog', () => { stitcher, }); - await expect(catalog.facets({ facets: ['kind'] })).resolves.toEqual({ + await expect( + catalog.facets({ + facets: ['kind'], + credentials: mockCredentials.none(), + }), + ).resolves.toEqual({ facets: { kind: [ { value: 'k', count: 2 }, @@ -1732,6 +1790,7 @@ describe('DefaultEntitiesCatalog', () => { catalog.facets({ facets: ['kind'], filter: { not: { key: 'metadata.name', values: ['two'] } }, + credentials: mockCredentials.none(), }), ).resolves.toEqual({ facets: { @@ -1775,6 +1834,7 @@ describe('DefaultEntitiesCatalog', () => { await expect( catalog.facets({ facets: ['metadata.annotations.a.b/c.d', 'metadata.labels.e.f/g.h'], + credentials: mockCredentials.none(), }), ).resolves.toEqual({ facets: { @@ -1823,6 +1883,7 @@ describe('DefaultEntitiesCatalog', () => { await expect( catalog.facets({ facets: ['metadata.tags'], + credentials: mockCredentials.none(), }), ).resolves.toEqual({ facets: { diff --git a/plugins/catalog-backend/src/service/DefaultRefreshService.test.ts b/plugins/catalog-backend/src/service/DefaultRefreshService.test.ts index e1b775cce6..9ca9ae0a5f 100644 --- a/plugins/catalog-backend/src/service/DefaultRefreshService.test.ts +++ b/plugins/catalog-backend/src/service/DefaultRefreshService.test.ts @@ -15,7 +15,11 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; +import { + TestDatabaseId, + TestDatabases, + mockCredentials, +} from '@backstage/backend-test-utils'; import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; import { createHash } from 'crypto'; import { Knex } from 'knex'; @@ -220,6 +224,7 @@ describe('DefaultRefreshService', () => { await refreshService.refresh({ entityRef: 'component:default/mycomp', + credentials: mockCredentials.none(), }); await expect( @@ -273,6 +278,7 @@ describe('DefaultRefreshService', () => { await refreshService.refresh({ entityRef: 'api:default/myapi', + credentials: mockCredentials.none(), }); await expect(waitForRefresh(knex, 'api:default/myapi')).resolves.toBe( @@ -324,6 +330,7 @@ describe('DefaultRefreshService', () => { await refreshService.refresh({ entityRef: 'component:default/mycomp', + credentials: mockCredentials.none(), }); await expect( @@ -334,6 +341,7 @@ describe('DefaultRefreshService', () => { await refreshService.refresh({ entityRef: 'component:default/mycomp', + credentials: mockCredentials.none(), }); await expect( diff --git a/plugins/catalog-backend/src/service/createRouter.test.ts b/plugins/catalog-backend/src/service/createRouter.test.ts index 3eb00c5bd6..5177f3b42b 100644 --- a/plugins/catalog-backend/src/service/createRouter.test.ts +++ b/plugins/catalog-backend/src/service/createRouter.test.ts @@ -41,6 +41,7 @@ import { z } from 'zod'; import { decodeCursor, encodeCursor } from './util'; import { wrapInOpenApiTestServer } from '@backstage/backend-openapi-utils'; import { Server } from 'http'; +import { mockCredentials, mockServices } from '@backstage/backend-test-utils'; describe('createRouter readonly disabled', () => { let entitiesCatalog: jest.Mocked; @@ -75,6 +76,8 @@ describe('createRouter readonly disabled', () => { refreshService, config: new ConfigReader(undefined), permissionIntegrationRouter: express.Router(), + auth: mockServices.auth(), + httpAuth: mockServices.httpAuth(), }); app = wrapInOpenApiTestServer(express().use(router)); }); @@ -88,15 +91,30 @@ describe('createRouter readonly disabled', () => { const response = await request(app) .post('/refresh') .set('Content-Type', 'application/json') - .set('authorization', 'Bearer someauthtoken') .send({ entityRef: 'Component/default:foo' }); expect(response.status).toBe(200); expect(refreshService.refresh).toHaveBeenCalledWith({ entityRef: 'Component/default:foo', - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), + }); + }); + + it('should support passing the token in the request body for backwards compatibility', async () => { + const response = await request(app) + .post('/refresh') + .set('Content-Type', 'application/json') + .send({ + entityRef: 'Component/default:foo', + authorizationToken: mockCredentials.user.token('user:default/other'), + }); + expect(response.status).toBe(200); + expect(refreshService.refresh).toHaveBeenCalledWith({ + entityRef: 'Component/default:foo', + credentials: mockCredentials.user('user:default/other'), }); }); }); + describe('GET /entities', () => { it('happy path: lists entities', async () => { const entities: Entity[] = [ @@ -137,6 +155,7 @@ describe('createRouter readonly disabled', () => { { allOf: [{ key: 'c', values: ['4'] }] }, ], }, + credentials: mockCredentials.user(), }); }); }); @@ -196,6 +215,7 @@ describe('createRouter readonly disabled', () => { fields: undefined, term: '', }, + credentials: mockCredentials.user(), }); }); @@ -235,6 +255,7 @@ describe('createRouter readonly disabled', () => { fields: undefined, term: '', }, + credentials: mockCredentials.user(), }); }); @@ -257,6 +278,7 @@ describe('createRouter readonly disabled', () => { expect(entitiesCatalog.queryEntities).toHaveBeenCalledTimes(1); expect(entitiesCatalog.queryEntities).toHaveBeenCalledWith({ cursor, + credentials: mockCredentials.user(), }); expect(response.status).toEqual(200); expect(response.body).toEqual({ @@ -291,6 +313,7 @@ describe('createRouter readonly disabled', () => { expect(entitiesCatalog.queryEntities).toHaveBeenCalledTimes(1); expect(entitiesCatalog.queryEntities).toHaveBeenCalledWith({ cursor, + credentials: mockCredentials.user(), }); expect(response.status).toEqual(200); expect(response.body).toEqual({ @@ -370,6 +393,7 @@ describe('createRouter readonly disabled', () => { expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1); expect(entitiesCatalog.entities).toHaveBeenCalledWith({ filter: basicEntityFilter({ 'metadata.uid': 'zzz' }), + credentials: mockCredentials.user(), }); expect(response.status).toEqual(200); expect(response.body).toEqual(expect.objectContaining(entity)); @@ -386,6 +410,7 @@ describe('createRouter readonly disabled', () => { expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1); expect(entitiesCatalog.entities).toHaveBeenCalledWith({ filter: basicEntityFilter({ 'metadata.uid': 'zzz' }), + credentials: mockCredentials.user(), }); expect(response.status).toEqual(404); expect(response.text).toMatch(/uid/); @@ -416,6 +441,7 @@ describe('createRouter readonly disabled', () => { 'metadata.namespace': 'ns', 'metadata.name': 'n', }), + credentials: mockCredentials.user(), }); expect(response.status).toEqual(200); expect(response.body).toEqual(expect.objectContaining(entity)); @@ -436,6 +462,7 @@ describe('createRouter readonly disabled', () => { 'metadata.namespace': 'd', 'metadata.name': 'c', }), + credentials: mockCredentials.user(), }); expect(response.status).toEqual(404); expect(response.text).toMatch(/name/); @@ -446,13 +473,10 @@ describe('createRouter readonly disabled', () => { it('can remove', async () => { entitiesCatalog.removeEntityByUid.mockResolvedValue(undefined); - const response = await request(app) - .delete('/entities/by-uid/apa') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).delete('/entities/by-uid/apa'); expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledTimes(1); expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledWith('apa', { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(204); }); @@ -462,13 +486,10 @@ describe('createRouter readonly disabled', () => { new NotFoundError('nope'), ); - const response = await request(app) - .delete('/entities/by-uid/apa') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).delete('/entities/by-uid/apa'); expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledTimes(1); expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledWith('apa', { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(404); }); @@ -518,6 +539,7 @@ describe('createRouter readonly disabled', () => { expect(entitiesCatalog.entitiesBatch).toHaveBeenCalledWith({ entityRefs: [entityRef], fields: expect.any(Function), + credentials: mockCredentials.user(), }); expect(response.status).toEqual(200); expect(response.body).toEqual({ items: [entity] }); @@ -531,13 +553,10 @@ describe('createRouter readonly disabled', () => { ]; locationService.listLocations.mockResolvedValueOnce(locations); - const response = await request(app) - .get('/locations') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).get('/locations'); expect(locationService.listLocations).toHaveBeenCalledTimes(1); expect(locationService.listLocations).toHaveBeenCalledWith({ - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(200); expect(response.body).toEqual([ @@ -555,13 +574,10 @@ describe('createRouter readonly disabled', () => { }; locationService.getLocation.mockResolvedValueOnce(location); - const response = await request(app) - .get('/locations/foo') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).get('/locations/foo'); expect(locationService.getLocation).toHaveBeenCalledTimes(1); expect(locationService.getLocation).toHaveBeenCalledWith('foo', { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(200); @@ -582,7 +598,7 @@ describe('createRouter readonly disabled', () => { const response = await request(app) .post('/locations') - .set('authorization', 'Bearer someauthtoken') + .send(spec); expect(locationService.createLocation).not.toHaveBeenCalled(); @@ -602,12 +618,12 @@ describe('createRouter readonly disabled', () => { const response = await request(app) .post('/locations') - .set('authorization', 'Bearer someauthtoken') + .send(spec); expect(locationService.createLocation).toHaveBeenCalledTimes(1); expect(locationService.createLocation).toHaveBeenCalledWith(spec, false, { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(201); expect(response.body).toEqual( @@ -630,12 +646,12 @@ describe('createRouter readonly disabled', () => { const response = await request(app) .post('/locations?dryRun=true') - .set('authorization', 'Bearer someauthtoken') + .send(spec); expect(locationService.createLocation).toHaveBeenCalledTimes(1); expect(locationService.createLocation).toHaveBeenCalledWith(spec, true, { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(201); expect(response.body).toEqual( @@ -650,13 +666,10 @@ describe('createRouter readonly disabled', () => { it('deletes the location', async () => { locationService.deleteLocation.mockResolvedValueOnce(undefined); - const response = await request(app) - .delete('/locations/foo') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).delete('/locations/foo'); expect(locationService.deleteLocation).toHaveBeenCalledTimes(1); expect(locationService.deleteLocation).toHaveBeenCalledWith('foo', { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(204); @@ -672,15 +685,12 @@ describe('createRouter readonly disabled', () => { }; locationService.getLocationByEntity.mockResolvedValueOnce(location); - const response = await request(app) - .get('/locations/by-entity/c/ns/n') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).get('/locations/by-entity/c/ns/n'); expect(locationService.getLocationByEntity).toHaveBeenCalledTimes(1); expect(locationService.getLocationByEntity).toHaveBeenCalledWith( { kind: 'c', namespace: 'ns', name: 'n' }, { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }, ); @@ -837,6 +847,8 @@ describe('createRouter readonly enabled', () => { }, }), permissionIntegrationRouter: express.Router(), + auth: mockServices.auth(), + httpAuth: mockServices.httpAuth(), }); app = express().use(router); }); @@ -866,13 +878,10 @@ describe('createRouter readonly enabled', () => { describe('DELETE /entities/by-uid/:uid', () => { // this delete is allowed as there is no other way to remove entities it('is allowed', async () => { - const response = await request(app) - .delete('/entities/by-uid/apa') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).delete('/entities/by-uid/apa'); expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledTimes(1); expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledWith('apa', { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(204); }); @@ -885,13 +894,10 @@ describe('createRouter readonly enabled', () => { ]; locationService.listLocations.mockResolvedValueOnce(locations); - const response = await request(app) - .get('/locations') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).get('/locations'); expect(locationService.listLocations).toHaveBeenCalledTimes(1); expect(locationService.listLocations).toHaveBeenCalledWith({ - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(200); @@ -910,13 +916,10 @@ describe('createRouter readonly enabled', () => { }; locationService.getLocation.mockResolvedValueOnce(location); - const response = await request(app) - .get('/locations/foo') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).get('/locations/foo'); expect(locationService.getLocation).toHaveBeenCalledTimes(1); expect(locationService.getLocation).toHaveBeenCalledWith('foo', { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(200); @@ -937,7 +940,7 @@ describe('createRouter readonly enabled', () => { const response = await request(app) .post('/locations') - .set('authorization', 'Bearer someauthtoken') + .send(spec); expect(locationService.createLocation).not.toHaveBeenCalled(); @@ -958,12 +961,12 @@ describe('createRouter readonly enabled', () => { const response = await request(app) .post('/locations?dryRun=true') - .set('authorization', 'Bearer someauthtoken') + .send(spec); expect(locationService.createLocation).toHaveBeenCalledTimes(1); expect(locationService.createLocation).toHaveBeenCalledWith(spec, true, { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(201); expect(response.body).toEqual( @@ -976,10 +979,7 @@ describe('createRouter readonly enabled', () => { describe('DELETE /locations', () => { it('is not allowed', async () => { - const response = await request(app) - .delete('/locations/foo') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).delete('/locations/foo'); expect(locationService.deleteLocation).not.toHaveBeenCalled(); expect(response.status).toEqual(403); }); @@ -994,15 +994,12 @@ describe('createRouter readonly enabled', () => { }; locationService.getLocationByEntity.mockResolvedValueOnce(location); - const response = await request(app) - .get('/locations/by-entity/c/ns/n') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).get('/locations/by-entity/c/ns/n'); expect(locationService.getLocationByEntity).toHaveBeenCalledTimes(1); expect(locationService.getLocationByEntity).toHaveBeenCalledWith( { kind: 'c', namespace: 'ns', name: 'n' }, { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }, ); @@ -1065,6 +1062,8 @@ describe('NextRouter permissioning', () => { ), ), }), + auth: mockServices.auth(), + httpAuth: mockServices.httpAuth(), }); app = express().use(router); }); diff --git a/plugins/catalog-backend/src/service/createRouter.ts b/plugins/catalog-backend/src/service/createRouter.ts index 2522966408..922782d75a 100644 --- a/plugins/catalog-backend/src/service/createRouter.ts +++ b/plugins/catalog-backend/src/service/createRouter.ts @@ -41,7 +41,7 @@ import { } from './request'; import { parseEntityFacetParams } from './request/parseEntityFacetParams'; import { parseEntityOrderParams } from './request/parseEntityOrderParams'; -import { LocationService, RefreshOptions, RefreshService } from './types'; +import { LocationService, RefreshService } from './types'; import { disallowReadonlyMode, encodeCursor, @@ -50,8 +50,8 @@ import { } from './util'; import { createOpenApiRouter } from '../schema/openapi.generated'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; -import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; import { parseEntityPaginationParams } from './request/parseEntityPaginationParams'; +import { AuthService, HttpAuthService } from '@backstage/backend-plugin-api'; /** * Options used by {@link createRouter}. @@ -68,6 +68,8 @@ export interface RouterOptions { logger: Logger; config: Config; permissionIntegrationRouter?: express.Router; + auth: AuthService; + httpAuth: HttpAuthService; } /** @@ -94,6 +96,8 @@ export async function createRouter( config, logger, permissionIntegrationRouter, + auth, + httpAuth, } = options; const readonlyEnabled = @@ -104,12 +108,16 @@ export async function createRouter( if (refreshService) { router.post('/refresh', async (req, res) => { - const refreshOptions: RefreshOptions = req.body; - refreshOptions.authorizationToken = getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ); + const { authorizationToken, ...restBody } = req.body; - await refreshService.refresh(refreshOptions); + const credentials = authorizationToken + ? await auth.authenticate(authorizationToken) + : await httpAuth.credentials(req); + + await refreshService.refresh({ + ...restBody, + credentials, + }); res.status(200).end(); }); } @@ -126,9 +134,7 @@ export async function createRouter( fields: parseEntityTransformParams(req.query), order: parseEntityOrderParams(req.query), pagination: parseEntityPaginationParams(req.query), - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); // Add a Link header to the next page @@ -147,9 +153,7 @@ export async function createRouter( await entitiesCatalog.queryEntities({ limit: req.query.limit, ...parseQueryEntitiesParams(req.query), - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); res.json({ @@ -169,9 +173,7 @@ export async function createRouter( const { uid } = req.params; const { entities } = await entitiesCatalog.entities({ filter: basicEntityFilter({ 'metadata.uid': uid }), - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); if (!entities.length) { throw new NotFoundError(`No entity with uid ${uid}`); @@ -181,9 +183,7 @@ export async function createRouter( .delete('/entities/by-uid/:uid', async (req, res) => { const { uid } = req.params; await entitiesCatalog.removeEntityByUid(uid, { - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); res.status(204).end(); }) @@ -195,9 +195,7 @@ export async function createRouter( 'metadata.namespace': namespace, 'metadata.name': name, }), - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); if (!entities.length) { throw new NotFoundError( @@ -212,22 +210,17 @@ export async function createRouter( const { kind, namespace, name } = req.params; const entityRef = stringifyEntityRef({ kind, namespace, name }); const response = await entitiesCatalog.entityAncestry(entityRef, { - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); res.status(200).json(response); }, ) .post('/entities/by-refs', async (req, res) => { const request = entitiesBatchRequest(req); - const token = getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ); const response = await entitiesCatalog.entitiesBatch({ entityRefs: request.entityRefs, fields: parseEntityTransformParams(req.query, request.fields), - authorizationToken: token, + credentials: await httpAuth.credentials(req), }); res.status(200).json(response); }) @@ -235,9 +228,7 @@ export async function createRouter( const response = await entitiesCatalog.facets({ filter: parseEntityFilterParams(req.query), facets: parseEntityFacetParams(req.query), - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); res.status(200).json(response); }); @@ -256,17 +247,13 @@ export async function createRouter( } const output = await locationService.createLocation(location, dryRun, { - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); res.status(201).json(output); }) .get('/locations', async (req, res) => { const locations = await locationService.listLocations({ - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); res.status(200).json(locations.map(l => ({ data: l }))); }) @@ -274,9 +261,7 @@ export async function createRouter( .get('/locations/:id', async (req, res) => { const { id } = req.params; const output = await locationService.getLocation(id, { - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); res.status(200).json(output); }) @@ -285,9 +270,7 @@ export async function createRouter( const { id } = req.params; await locationService.deleteLocation(id, { - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); res.status(204).end(); }) @@ -295,11 +278,7 @@ export async function createRouter( const { kind, namespace, name } = req.params; const output = await locationService.getLocationByEntity( { kind, namespace, name }, - { - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), - }, + { credentials: await httpAuth.credentials(req) }, ); res.status(200).json(output); }); diff --git a/plugins/catalog-backend/src/service/request/parseQueryEntitiesParams.ts b/plugins/catalog-backend/src/service/request/parseQueryEntitiesParams.ts index b1d5e8dd1f..919e97a1f7 100644 --- a/plugins/catalog-backend/src/service/request/parseQueryEntitiesParams.ts +++ b/plugins/catalog-backend/src/service/request/parseQueryEntitiesParams.ts @@ -28,12 +28,12 @@ import { internal } from '@backstage/backend-openapi-utils'; export function parseQueryEntitiesParams( params: internal.QuerySchema, -): Omit { +): Omit { const fields = parseEntityTransformParams(params); if (params.cursor) { const decodedCursor = decodeCursor(params.cursor); - const response: Omit = { + const response: Omit = { cursor: decodedCursor, fields, }; @@ -43,7 +43,7 @@ export function parseQueryEntitiesParams( const filter = parseEntityFilterParams(params); const orderFields = parseEntityOrderFieldParams(params); - const response: Omit = { + const response: Omit = { fields, filter, orderFields, diff --git a/plugins/catalog-backend/src/service/types.ts b/plugins/catalog-backend/src/service/types.ts index 878afe5662..30470e5af0 100644 --- a/plugins/catalog-backend/src/service/types.ts +++ b/plugins/catalog-backend/src/service/types.ts @@ -16,6 +16,7 @@ import { CompoundEntityRef, Entity } from '@backstage/catalog-model'; import { Location } from '@backstage/catalog-client'; +import { BackstageCredentials } from '@backstage/backend-plugin-api'; /** * Holds the information required to create a new location in the catalog location store. @@ -35,22 +36,24 @@ export interface LocationService { createLocation( location: LocationInput, dryRun: boolean, - options?: { - authorizationToken?: string; + options: { + credentials: BackstageCredentials; }, ): Promise<{ location: Location; entities: Entity[]; exists?: boolean }>; - listLocations(options?: { authorizationToken?: string }): Promise; + listLocations(options: { + credentials: BackstageCredentials; + }): Promise; getLocation( id: string, - options?: { authorizationToken?: string }, + options: { credentials: BackstageCredentials }, ): Promise; deleteLocation( id: string, - options?: { authorizationToken?: string }, + options: { credentials: BackstageCredentials }, ): Promise; getLocationByEntity( entityRef: CompoundEntityRef | string, - options?: { authorizationToken?: string }, + options: { credentials: BackstageCredentials }, ): Promise; } @@ -62,7 +65,7 @@ export interface LocationService { export type RefreshOptions = { /** The reference to a single entity that should be refreshed */ entityRef: string; - authorizationToken?: string; + credentials: BackstageCredentials; }; /**