catalog-node: add new CatalogService with credentials support

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-10-17 17:48:22 +02:00
parent 7aae8e302e
commit bc13b42a49
6 changed files with 415 additions and 9 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-node': minor
---
The `catalogServiceRef` now have its own accompanying `CatalogService` interface, which also supports passing Backstage `credentials` objects in addition to tokens.
+2 -1
View File
@@ -71,6 +71,7 @@
},
"devDependencies": {
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^"
"@backstage/cli": "workspace:^",
"msw": "^1.0.0"
}
}
+24 -1
View File
@@ -3,20 +3,37 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { AddLocationRequest } from '@backstage/catalog-client';
import { AddLocationResponse } from '@backstage/catalog-client';
import { BackstageCredentials } from '@backstage/backend-plugin-api';
import { CatalogApi } from '@backstage/catalog-client';
import { CatalogProcessor } from '@backstage/plugin-catalog-node';
import { CatalogProcessorParser } from '@backstage/plugin-catalog-node';
import { CatalogRequestOptions } from '@backstage/catalog-client';
import { CompoundEntityRef } from '@backstage/catalog-model';
import { EntitiesSearchFilter } from '@backstage/plugin-catalog-node';
import { Entity } from '@backstage/catalog-model';
import { EntityProvider } from '@backstage/plugin-catalog-node';
import { ExtensionPoint } from '@backstage/backend-plugin-api';
import { GetEntitiesByRefsRequest } from '@backstage/catalog-client';
import { GetEntitiesByRefsResponse } from '@backstage/catalog-client';
import { GetEntitiesRequest } from '@backstage/catalog-client';
import { GetEntitiesResponse } from '@backstage/catalog-client';
import { GetEntityAncestorsRequest } from '@backstage/catalog-client';
import { GetEntityAncestorsResponse } from '@backstage/catalog-client';
import { GetEntityFacetsRequest } from '@backstage/catalog-client';
import { GetEntityFacetsResponse } from '@backstage/catalog-client';
import { Location as Location_2 } from '@backstage/catalog-client';
import { LocationAnalyzer } from '@backstage/plugin-catalog-node';
import { Permission } from '@backstage/plugin-permission-common';
import { PermissionRule } from '@backstage/plugin-permission-node';
import { PermissionRuleParams } from '@backstage/plugin-permission-common';
import { PlaceholderResolver } from '@backstage/plugin-catalog-node';
import { QueryEntitiesRequest } from '@backstage/catalog-client';
import { QueryEntitiesResponse } from '@backstage/catalog-client';
import { ScmLocationAnalyzer } from '@backstage/plugin-catalog-node';
import { ServiceRef } from '@backstage/backend-plugin-api';
import { ValidateEntityResponse } from '@backstage/catalog-client';
import { Validators } from '@backstage/catalog-model';
// @alpha (undocumented)
@@ -95,8 +112,14 @@ export interface CatalogProcessingExtensionPoint {
// @alpha (undocumented)
export const catalogProcessingExtensionPoint: ExtensionPoint<CatalogProcessingExtensionPoint>;
// Warning: (ae-forgotten-export) The symbol "CatalogService" needs to be exported by the entry point alpha.d.ts
//
// @alpha
export const catalogServiceRef: ServiceRef<CatalogApi, 'plugin', 'singleton'>;
export const catalogServiceRef: ServiceRef<
CatalogService,
'plugin',
'singleton'
>;
// (No @packageDocumentation comment for this package)
```
+132 -2
View File
@@ -14,12 +14,27 @@
* limitations under the License.
*/
import { createBackendModule } from '@backstage/backend-plugin-api';
import { startTestBackend } from '@backstage/backend-test-utils';
import {
createBackendModule,
createServiceFactory,
createServiceRef,
} from '@backstage/backend-plugin-api';
import {
ServiceFactoryTester,
mockCredentials,
mockServices,
registerMswTestHooks,
startTestBackend,
} from '@backstage/backend-test-utils';
import { CatalogClient } from '@backstage/catalog-client';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { catalogServiceRef } from './catalogService';
describe('catalogServiceRef', () => {
const server = setupServer();
registerMswTestHooks(server);
it('should return a catalogClient', async () => {
expect.assertions(1);
const testModule = createBackendModule({
@@ -41,4 +56,119 @@ describe('catalogServiceRef', () => {
features: [testModule],
});
});
it('should inject token from user credentials', async () => {
expect.assertions(1);
server.use(
rest.get('http://localhost/api/catalog/entities', (req, res, ctx) => {
expect(req.headers.get('authorization')).toBe(
mockCredentials.service.header({
onBehalfOf: mockCredentials.user(),
targetPluginId: 'catalog',
}),
);
return res(ctx.json({}));
}),
);
const tester = ServiceFactoryTester.from(
createServiceFactory({
service: createServiceRef<void>({ id: 'unused-dummy' }),
deps: {},
factory() {},
}),
{ dependencies: [mockServices.discovery.factory()] },
);
const catalogService = await tester.getService(catalogServiceRef);
await catalogService.getEntities(
{},
{ credentials: mockCredentials.user() },
);
});
it('should inject token from service credentials', async () => {
expect.assertions(1);
server.use(
rest.get('http://localhost/api/catalog/entities', (req, res, ctx) => {
expect(req.headers.get('authorization')).toBe(
mockCredentials.service.header({
onBehalfOf: mockCredentials.service(),
targetPluginId: 'catalog',
}),
);
return res(ctx.json({}));
}),
);
const tester = ServiceFactoryTester.from(
createServiceFactory({
service: createServiceRef<void>({ id: 'unused-dummy' }),
deps: {},
factory() {},
}),
{ dependencies: [mockServices.discovery.factory()] },
);
const catalogService = await tester.getService(catalogServiceRef);
await catalogService.getEntities(
{},
{ credentials: mockCredentials.service() },
);
});
it('should call with token', async () => {
expect.assertions(1);
server.use(
rest.get('http://localhost/api/catalog/entities', (req, res, ctx) => {
expect(req.headers.get('authorization')).toBe(
mockCredentials.user.header(),
);
return res(ctx.json({}));
}),
);
const tester = ServiceFactoryTester.from(
createServiceFactory({
service: createServiceRef<void>({ id: 'unused-dummy' }),
deps: {},
factory() {},
}),
{ dependencies: [mockServices.discovery.factory()] },
);
const catalogService = await tester.getService(catalogServiceRef);
await catalogService.getEntities(
{},
{
token: mockCredentials.user.token(),
},
);
});
it('should call without credentials', async () => {
expect.assertions(1);
server.use(
rest.get('http://localhost/api/catalog/entities', (req, res, ctx) => {
expect(req.headers.get('authorization')).toBeFalsy();
return res(ctx.json({}));
}),
);
const tester = ServiceFactoryTester.from(
createServiceFactory({
service: createServiceRef<void>({ id: 'unused-dummy' }),
deps: {},
factory() {},
}),
{ dependencies: [mockServices.discovery.factory()] },
);
const catalogService = await tester.getService(catalogServiceRef);
await catalogService.getEntities();
});
});
+251 -5
View File
@@ -18,23 +18,269 @@ import {
createServiceFactory,
createServiceRef,
coreServices,
BackstageCredentials,
DiscoveryService,
AuthService,
} from '@backstage/backend-plugin-api';
import { CatalogApi, CatalogClient } from '@backstage/catalog-client';
import {
AddLocationRequest,
AddLocationResponse,
CatalogApi,
CatalogClient,
CatalogRequestOptions,
GetEntitiesByRefsRequest,
GetEntitiesByRefsResponse,
GetEntitiesRequest,
GetEntitiesResponse,
GetEntityAncestorsRequest,
GetEntityAncestorsResponse,
GetEntityFacetsRequest,
GetEntityFacetsResponse,
Location,
QueryEntitiesRequest,
QueryEntitiesResponse,
ValidateEntityResponse,
} from '@backstage/catalog-client';
import { CompoundEntityRef, Entity } from '@backstage/catalog-model';
/**
* @public
*/
export interface CatalogServiceRequestOptions extends CatalogRequestOptions {
credentials?: BackstageCredentials;
}
/**
* A version of the {@link CatalogApi | CatalogApi} that
* accepts backend credentials in addition to a token.
*
* @public
*/
export interface CatalogService extends CatalogApi {
getEntities(
request?: GetEntitiesRequest,
options?: CatalogServiceRequestOptions,
): Promise<GetEntitiesResponse>;
getEntitiesByRefs(
request: GetEntitiesByRefsRequest,
options?: CatalogServiceRequestOptions,
): Promise<GetEntitiesByRefsResponse>;
queryEntities(
request?: QueryEntitiesRequest,
options?: CatalogServiceRequestOptions,
): Promise<QueryEntitiesResponse>;
getEntityAncestors(
request: GetEntityAncestorsRequest,
options?: CatalogServiceRequestOptions,
): Promise<GetEntityAncestorsResponse>;
getEntityByRef(
entityRef: string | CompoundEntityRef,
options?: CatalogServiceRequestOptions,
): Promise<Entity | undefined>;
removeEntityByUid(
uid: string,
options?: CatalogServiceRequestOptions,
): Promise<void>;
refreshEntity(
entityRef: string,
options?: CatalogServiceRequestOptions,
): Promise<void>;
getEntityFacets(
request: GetEntityFacetsRequest,
options?: CatalogServiceRequestOptions,
): Promise<GetEntityFacetsResponse>;
getLocationById(
id: string,
options?: CatalogServiceRequestOptions,
): Promise<Location | undefined>;
getLocationByRef(
locationRef: string,
options?: CatalogServiceRequestOptions,
): Promise<Location | undefined>;
addLocation(
location: AddLocationRequest,
options?: CatalogServiceRequestOptions,
): Promise<AddLocationResponse>;
removeLocationById(
id: string,
options?: CatalogServiceRequestOptions,
): Promise<void>;
getLocationByEntity(
entityRef: string | CompoundEntityRef,
options?: CatalogServiceRequestOptions,
): Promise<Location | undefined>;
validateEntity(
entity: Entity,
locationRef: string,
options?: CatalogServiceRequestOptions,
): Promise<ValidateEntityResponse>;
}
class DefaultCatalogService extends CatalogClient {
readonly #auth: AuthService;
constructor({
discoveryApi,
auth,
}: {
discoveryApi: DiscoveryService;
auth: AuthService;
}) {
super({ discoveryApi });
this.#auth = auth;
}
async getEntities(
request?: GetEntitiesRequest,
options?: CatalogServiceRequestOptions,
): Promise<GetEntitiesResponse> {
return super.getEntities(request, await this.#getOptions(options));
}
async getEntitiesByRefs(
request: GetEntitiesByRefsRequest,
options?: CatalogServiceRequestOptions,
): Promise<GetEntitiesByRefsResponse> {
return super.getEntitiesByRefs(request, await this.#getOptions(options));
}
async queryEntities(
request?: QueryEntitiesRequest,
options?: CatalogServiceRequestOptions,
): Promise<QueryEntitiesResponse> {
return super.queryEntities(request, await this.#getOptions(options));
}
async getEntityAncestors(
request: GetEntityAncestorsRequest,
options?: CatalogServiceRequestOptions,
): Promise<GetEntityAncestorsResponse> {
return super.getEntityAncestors(request, await this.#getOptions(options));
}
async getEntityByRef(
entityRef: string | CompoundEntityRef,
options?: CatalogServiceRequestOptions,
): Promise<Entity | undefined> {
return super.getEntityByRef(entityRef, await this.#getOptions(options));
}
async removeEntityByUid(
uid: string,
options?: CatalogServiceRequestOptions,
): Promise<void> {
return super.removeEntityByUid(uid, await this.#getOptions(options));
}
async refreshEntity(
entityRef: string,
options?: CatalogServiceRequestOptions,
): Promise<void> {
return super.refreshEntity(entityRef, await this.#getOptions(options));
}
async getEntityFacets(
request: GetEntityFacetsRequest,
options?: CatalogServiceRequestOptions,
): Promise<GetEntityFacetsResponse> {
return super.getEntityFacets(request, await this.#getOptions(options));
}
async getLocationById(
id: string,
options?: CatalogServiceRequestOptions,
): Promise<Location | undefined> {
return super.getLocationById(id, await this.#getOptions(options));
}
async getLocationByRef(
locationRef: string,
options?: CatalogServiceRequestOptions,
): Promise<Location | undefined> {
return super.getLocationByRef(locationRef, await this.#getOptions(options));
}
async addLocation(
location: AddLocationRequest,
options?: CatalogServiceRequestOptions,
): Promise<AddLocationResponse> {
return super.addLocation(location, await this.#getOptions(options));
}
async removeLocationById(
id: string,
options?: CatalogServiceRequestOptions,
): Promise<void> {
return super.removeLocationById(id, await this.#getOptions(options));
}
async getLocationByEntity(
entityRef: string | CompoundEntityRef,
options?: CatalogServiceRequestOptions,
): Promise<Location | undefined> {
return super.getLocationByEntity(
entityRef,
await this.#getOptions(options),
);
}
async validateEntity(
entity: Entity,
locationRef: string,
options?: CatalogServiceRequestOptions,
): Promise<ValidateEntityResponse> {
return super.validateEntity(
entity,
locationRef,
await this.#getOptions(options),
);
}
async #getOptions(
options?: CatalogServiceRequestOptions,
): Promise<CatalogRequestOptions | undefined> {
if (options?.token) {
return options;
}
if (options?.credentials) {
return this.#auth.getPluginRequestToken({
onBehalfOf: options.credentials,
targetPluginId: 'catalog',
});
}
return options;
}
}
/**
* The catalogService provides the catalog API.
* @alpha
*
* @public
*/
export const catalogServiceRef = createServiceRef<CatalogApi>({
export const catalogServiceRef = createServiceRef<CatalogService>({
id: 'catalog-client',
defaultFactory: async service =>
createServiceFactory({
service,
deps: {
auth: coreServices.auth,
discoveryApi: coreServices.discovery,
},
async factory({ discoveryApi }) {
return new CatalogClient({ discoveryApi });
async factory(deps) {
return new DefaultCatalogService(deps);
},
}),
});
+1
View File
@@ -5929,6 +5929,7 @@ __metadata:
"@backstage/plugin-permission-common": "workspace:^"
"@backstage/plugin-permission-node": "workspace:^"
"@backstage/types": "workspace:^"
msw: ^1.0.0
languageName: unknown
linkType: soft