From 7927005152203ac15b48645a9ca84536fe20ad58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 7 Jul 2021 17:10:36 +0200 Subject: [PATCH 1/3] Add `fetchApiRef` which implements fetch, plus Backstage token header when available. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This intends to be the basis for other plugins' data fetching needs, so that they can transparently interact with the catalog and other parts of the Backstage ecosystem without explicitly having to deal with authenticating themselves. Signed-off-by: Fredrik Adelöw --- .changeset/blue-queens-sniff.md | 7 ++ .changeset/lovely-drinks-kiss.md | 5 ++ packages/app-defaults/src/defaults/apis.ts | 23 +++++ packages/catalog-client/api-report.md | 8 +- packages/catalog-client/src/CatalogClient.ts | 17 ++-- .../catalog-client/src/types/discovery.ts | 2 +- packages/catalog-client/src/types/fetch.ts | 26 ++++++ packages/catalog-client/src/types/index.ts | 1 + packages/core-app-api/api-report.md | 35 ++++++++ packages/core-app-api/package.json | 1 + ...ageProtocolResolverFetchMiddleware.test.ts | 86 +++++++++++++++++++ ...ackstageProtocolResolverFetchMiddleware.ts | 64 ++++++++++++++ .../FetchApi/FetchApiBuilder.ts | 55 ++++++++++++ .../IdentityAwareFetchMiddleware.test.ts | 43 ++++++++++ .../FetchApi/IdentityAwareFetchMiddleware.ts | 59 +++++++++++++ .../apis/implementations/FetchApi/index.ts | 20 +++++ .../apis/implementations/FetchApi/types.ts | 39 +++++++++ .../src/apis/implementations/index.ts | 1 + packages/core-plugin-api/api-report.md | 9 ++ packages/core-plugin-api/package.json | 2 +- .../src/apis/definitions/FetchApi.ts | 38 ++++++++ .../src/apis/definitions/index.ts | 1 + 22 files changed, 532 insertions(+), 10 deletions(-) create mode 100644 .changeset/blue-queens-sniff.md create mode 100644 .changeset/lovely-drinks-kiss.md create mode 100644 packages/catalog-client/src/types/fetch.ts create mode 100644 packages/core-app-api/src/apis/implementations/FetchApi/BackstageProtocolResolverFetchMiddleware.test.ts create mode 100644 packages/core-app-api/src/apis/implementations/FetchApi/BackstageProtocolResolverFetchMiddleware.ts create mode 100644 packages/core-app-api/src/apis/implementations/FetchApi/FetchApiBuilder.ts create mode 100644 packages/core-app-api/src/apis/implementations/FetchApi/IdentityAwareFetchMiddleware.test.ts create mode 100644 packages/core-app-api/src/apis/implementations/FetchApi/IdentityAwareFetchMiddleware.ts create mode 100644 packages/core-app-api/src/apis/implementations/FetchApi/index.ts create mode 100644 packages/core-app-api/src/apis/implementations/FetchApi/types.ts create mode 100644 packages/core-plugin-api/src/apis/definitions/FetchApi.ts diff --git a/.changeset/blue-queens-sniff.md b/.changeset/blue-queens-sniff.md new file mode 100644 index 0000000000..9a26507f92 --- /dev/null +++ b/.changeset/blue-queens-sniff.md @@ -0,0 +1,7 @@ +--- +'@backstage/app-defaults': patch +'@backstage/core-app-api': patch +'@backstage/core-plugin-api': patch +--- + +Add `FetchApi` and related `fetchApiRef` which implement fetch, with an added Backstage token header when available. diff --git a/.changeset/lovely-drinks-kiss.md b/.changeset/lovely-drinks-kiss.md new file mode 100644 index 0000000000..f343888920 --- /dev/null +++ b/.changeset/lovely-drinks-kiss.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-client': patch +--- + +Add the ability to supply a custom `fetchApi`. In the default frontend app setup, this will use the `fetchApiRef` implementation. diff --git a/packages/app-defaults/src/defaults/apis.ts b/packages/app-defaults/src/defaults/apis.ts index d3c76d52bd..06562caaef 100644 --- a/packages/app-defaults/src/defaults/apis.ts +++ b/packages/app-defaults/src/defaults/apis.ts @@ -34,6 +34,9 @@ import { OneLoginAuth, UnhandledErrorForwarder, AtlassianAuth, + FetchApiBuilder, + IdentityAwareFetchMiddleware, + BackstageProtocolResolverFetchMiddleware, } from '@backstage/core-app-api'; import { @@ -42,6 +45,8 @@ import { analyticsApiRef, errorApiRef, discoveryApiRef, + fetchApiRef, + identityApiRef, oauthRequestApiRef, googleAuthApiRef, githubAuthApiRef, @@ -92,6 +97,24 @@ export const apis = [ deps: { errorApi: errorApiRef }, factory: ({ errorApi }) => WebStorage.create({ errorApi }), }), + createApiFactory({ + api: fetchApiRef, + deps: { identityApi: identityApiRef, discoveryApi: discoveryApiRef }, + factory: ({ identityApi, discoveryApi }) => { + return FetchApiBuilder.create() + .with( + new IdentityAwareFetchMiddleware(() => { + return identityApi.getCredentials().then(r => r.token); + }), + ) + .with( + new BackstageProtocolResolverFetchMiddleware(plugin => { + return discoveryApi.getBaseUrl(plugin); + }), + ) + .build(); + }, + }), createApiFactory({ api: oauthRequestApiRef, deps: {}, diff --git a/packages/catalog-client/api-report.md b/packages/catalog-client/api-report.md index e16ca740f6..41aea63716 100644 --- a/packages/catalog-client/api-report.md +++ b/packages/catalog-client/api-report.md @@ -5,6 +5,7 @@ ```ts import { Entity } from '@backstage/catalog-model'; import { EntityName } from '@backstage/catalog-model'; +import { default as fetch_2 } from 'cross-fetch'; import { Location as Location_2 } from '@backstage/catalog-model'; // @public @@ -71,7 +72,7 @@ export interface CatalogApi { // @public export class CatalogClient implements CatalogApi { - constructor(options: { discoveryApi: DiscoveryApi }); + constructor(options: { discoveryApi: DiscoveryApi; fetchApi?: FetchApi }); addLocation( { type, target, dryRun, presence }: AddLocationRequest, options?: CatalogRequestOptions, @@ -155,4 +156,9 @@ export type DiscoveryApi = { // @public export const ENTITY_STATUS_CATALOG_PROCESSING_TYPE = 'backstage.io/catalog-processing'; + +// @public +export type FetchApi = { + fetch: typeof fetch_2; +}; ``` diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index 29d01fbe5c..047a8c7cce 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -25,7 +25,7 @@ import { stringifyLocationReference, } from '@backstage/catalog-model'; import { ResponseError } from '@backstage/errors'; -import fetch from 'cross-fetch'; +import crossFetch from 'cross-fetch'; import { CATALOG_FILTER_EXISTS, AddLocationRequest, @@ -38,6 +38,7 @@ import { CatalogEntityAncestorsResponse, } from './types/api'; import { DiscoveryApi } from './types/discovery'; +import { FetchApi } from './types/fetch'; /** * A frontend and backend compatible client for communicating with the Backstage Catalog. @@ -46,9 +47,11 @@ import { DiscoveryApi } from './types/discovery'; * */ export class CatalogClient implements CatalogApi { private readonly discoveryApi: DiscoveryApi; + private readonly fetchApi: FetchApi; - constructor(options: { discoveryApi: DiscoveryApi }) { + constructor(options: { discoveryApi: DiscoveryApi; fetchApi?: FetchApi }) { this.discoveryApi = options.discoveryApi; + this.fetchApi = options.fetchApi || { fetch: crossFetch }; } /** @@ -206,7 +209,7 @@ export class CatalogClient implements CatalogApi { * @public */ async refreshEntity(entityRef: string, options?: CatalogRequestOptions) { - const response = await fetch( + const response = await this.fetchApi.fetch( `${await this.discoveryApi.getBaseUrl('catalog')}/refresh`, { headers: { @@ -237,7 +240,7 @@ export class CatalogClient implements CatalogApi { { type = 'url', target, dryRun, presence }: AddLocationRequest, options?: CatalogRequestOptions, ): Promise { - const response = await fetch( + const response = await this.fetchApi.fetch( `${await this.discoveryApi.getBaseUrl('catalog')}/locations${ dryRun ? '?dryRun=true' : '' }`, @@ -376,7 +379,7 @@ export class CatalogClient implements CatalogApi { const headers: Record = options?.token ? { Authorization: `Bearer ${options.token}` } : {}; - const response = await fetch(url, { method, headers }); + const response = await this.fetchApi.fetch(url, { method, headers }); if (!response.ok) { throw await ResponseError.fromResponse(response); @@ -392,7 +395,7 @@ export class CatalogClient implements CatalogApi { const headers: Record = options?.token ? { Authorization: `Bearer ${options.token}` } : {}; - const response = await fetch(url, { method, headers }); + const response = await this.fetchApi.fetch(url, { method, headers }); if (!response.ok) { throw await ResponseError.fromResponse(response); @@ -410,7 +413,7 @@ export class CatalogClient implements CatalogApi { const headers: Record = options?.token ? { Authorization: `Bearer ${options.token}` } : {}; - const response = await fetch(url, { method, headers }); + const response = await this.fetchApi.fetch(url, { method, headers }); if (!response.ok) { if (response.status === 404) { diff --git a/packages/catalog-client/src/types/discovery.ts b/packages/catalog-client/src/types/discovery.ts index 19ee5ed19c..5fa14372e3 100644 --- a/packages/catalog-client/src/types/discovery.ts +++ b/packages/catalog-client/src/types/discovery.ts @@ -15,7 +15,7 @@ */ /** - * This is a copy of the core DiscoveryApi, to avoid importing core. + * This is a copy of the DiscoveryApi, to avoid importing core-plugin-api. * * @public */ diff --git a/packages/catalog-client/src/types/fetch.ts b/packages/catalog-client/src/types/fetch.ts new file mode 100644 index 0000000000..cdddd31810 --- /dev/null +++ b/packages/catalog-client/src/types/fetch.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fetch from 'cross-fetch'; + +/** + * This is a copy of FetchApi, to avoid importing core-plugin-api. + * + * @public + */ +export type FetchApi = { + fetch: typeof fetch; +}; diff --git a/packages/catalog-client/src/types/index.ts b/packages/catalog-client/src/types/index.ts index 3552a72e3c..f3c6fab4e9 100644 --- a/packages/catalog-client/src/types/index.ts +++ b/packages/catalog-client/src/types/index.ts @@ -25,5 +25,6 @@ export type { CatalogEntityAncestorsResponse, } from './api'; export type { DiscoveryApi } from './discovery'; +export type { FetchApi } from './fetch'; export { CATALOG_FILTER_EXISTS } from './api'; export { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from './status'; diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index 8a3f0edf14..3277be7841 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -26,6 +26,7 @@ import { bitbucketAuthApiRef } from '@backstage/core-plugin-api'; import { ComponentType } from 'react'; import { ConfigReader } from '@backstage/config'; import { createApp as createApp_2 } from '@backstage/app-defaults'; +import crossFetch from 'cross-fetch'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { ErrorApi } from '@backstage/core-plugin-api'; import { ErrorApiError } from '@backstage/core-plugin-api'; @@ -34,6 +35,7 @@ import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { FeatureFlag } from '@backstage/core-plugin-api'; import { FeatureFlagsApi } from '@backstage/core-plugin-api'; import { FeatureFlagsSaveOptions } from '@backstage/core-plugin-api'; +import { FetchApi } from '@backstage/core-plugin-api'; import { gitlabAuthApiRef } from '@backstage/core-plugin-api'; import { googleAuthApiRef } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; @@ -288,6 +290,14 @@ export type BackstagePluginWithAnyOutput = Omit< )[]; }; +// @public +export class BackstageProtocolResolverFetchMiddleware + implements FetchMiddleware +{ + constructor(discovery: (pluginId: string) => Promise); + apply(next: FetchFunction): FetchFunction; +} + // @public export class BitbucketAuth { // (undocumented) @@ -369,6 +379,24 @@ export type FeatureFlaggedProps = { } ); +// @public +export class FetchApiBuilder { + // (undocumented) + build(): FetchApi; + // (undocumented) + static create(): FetchApiBuilder; + // (undocumented) + with(middleware: FetchMiddleware): FetchApiBuilder; +} + +// @public +export type FetchFunction = typeof crossFetch; + +// @public +export interface FetchMiddleware { + apply(next: FetchFunction): FetchFunction; +} + // @public export const FlatRoutes: (props: FlatRoutesProps) => JSX.Element | null; @@ -426,6 +454,13 @@ export class GoogleAuth { static create(options: OAuthApiCreateOptions): typeof googleAuthApiRef.T; } +// @public +export class IdentityAwareFetchMiddleware implements FetchMiddleware { + constructor(tokenFunction: () => Promise); + apply(next: FetchFunction): FetchFunction; + setHeaderName(name: string): IdentityAwareFetchMiddleware; +} + // @public export class LocalStorageFeatureFlags implements FeatureFlagsApi { // (undocumented) diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index 23079d7ba0..d138417143 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -39,6 +39,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@types/prop-types": "^15.7.3", + "cross-fetch": "^3.0.6", "prop-types": "^15.7.2", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4", diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/BackstageProtocolResolverFetchMiddleware.test.ts b/packages/core-app-api/src/apis/implementations/FetchApi/BackstageProtocolResolverFetchMiddleware.test.ts new file mode 100644 index 0000000000..7dcb90573b --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/FetchApi/BackstageProtocolResolverFetchMiddleware.test.ts @@ -0,0 +1,86 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BackstageProtocolResolverFetchMiddleware } from './BackstageProtocolResolverFetchMiddleware'; + +describe('BackstageProtocolResolverFetchMiddleware', () => { + it.each([['https://passthrough.com/a']])( + 'passes through regular URLs, %p', + async (url: string) => { + const resolve = jest.fn(); + const middleware = new BackstageProtocolResolverFetchMiddleware(resolve); + const inner = jest.fn(); + const outer = middleware.apply(inner); + + await outer(url); + expect(inner.mock.calls[0][0]).toBe(url); + expect(resolve).not.toBeCalled(); + }, + ); + + it.each([ + [ + 'backstage://my-plugin/sub/path', + 'my-plugin', + 'https://real.com/base', + 'https://real.com/base/sub/path', + ], + [ + 'backstage://my-plugin/sub/path/', + 'my-plugin', + 'https://real.com/base/', + 'https://real.com/base/sub/path/', + ], + ['backstage://x', 'x', 'http://real.com:8080', 'http://real.com:8080'], + [ + 'backstage://x/a/b?c=d&e=f#g', + 'x', + 'https://real.com/base', + 'https://real.com/base/a/b?c=d&e=f#g', + ], + [ + 'backstage://x?c=d&e=f#g', + 'x', + 'https://real.com:8080/base', + 'https://real.com:8080/base?c=d&e=f#g', + ], + [ + 'backstage://username:password@x?c=d&e=f#g', + 'x', + 'https://real.com:8080/base', + 'https://username:password@real.com:8080/base?c=d&e=f#g', + ], + [ + 'backstage://x?c=d&e=f#g', + 'x', + 'https://username:password@real.com:8080/base', + 'https://username:password@real.com:8080/base?c=d&e=f#g', + ], + ])( + 'resolves backstage URLs, %p', + async (original, host, resolved, result) => { + const resolve = jest.fn(); + const middleware = new BackstageProtocolResolverFetchMiddleware(resolve); + const inner = jest.fn(); + const outer = middleware.apply(inner); + + resolve.mockResolvedValueOnce(resolved); + await outer(original); + expect(inner.mock.calls[0][0]).toBe(result); + expect(resolve).toHaveBeenLastCalledWith(host); + }, + ); +}); diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/BackstageProtocolResolverFetchMiddleware.ts b/packages/core-app-api/src/apis/implementations/FetchApi/BackstageProtocolResolverFetchMiddleware.ts new file mode 100644 index 0000000000..03d08c3096 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/FetchApi/BackstageProtocolResolverFetchMiddleware.ts @@ -0,0 +1,64 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { FetchFunction, FetchMiddleware } from './types'; + +function join(left: string, right: string): string { + if (!right || right === '/') { + return left; + } + + return `${left.replace(/\/$/, '')}/${right.replace(/^\//, '')}`; +} + +/** + * Handles translation from backstage://some-plugin-id/ to concrete + * http(s) URLs. + * + * @public + */ +export class BackstageProtocolResolverFetchMiddleware + implements FetchMiddleware +{ + constructor( + private readonly discovery: (pluginId: string) => Promise, + ) {} + + /** + * {@inheritdoc FetchMiddleware.apply} + */ + apply(next: FetchFunction): FetchFunction { + return async (input, init) => { + const request = new Request(input, init); + const { protocol, hostname, pathname, search, hash, username, password } = + new URL(request.url); + + if (protocol !== 'backstage:') { + return next(input, init); + } + + let base = await this.discovery(hostname); + if (username || password) { + const baseUrl = new URL(base); + const authority = `${username}${password ? `:${password}` : ''}@`; + base = `${baseUrl.protocol}//${authority}${baseUrl.host}${baseUrl.pathname}`; + } + + const target = `${join(base, pathname)}${search}${hash}`; + return next(target, request); + }; + } +} diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/FetchApiBuilder.ts b/packages/core-app-api/src/apis/implementations/FetchApi/FetchApiBuilder.ts new file mode 100644 index 0000000000..1c96eb1ad6 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/FetchApi/FetchApiBuilder.ts @@ -0,0 +1,55 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { FetchApi } from '@backstage/core-plugin-api'; +import crossFetch from 'cross-fetch'; +import { FetchFunction, FetchMiddleware } from './types'; + +/** + * Builds a fetch API, based on the builtin fetch wrapped by a set of optional + * middleware implementations that add behaviors. + * + * @public + */ +export class FetchApiBuilder { + static create(): FetchApiBuilder { + return new FetchApiBuilder(crossFetch, []); + } + + private constructor( + private readonly implementation: FetchFunction, + private readonly middleware: FetchMiddleware[], + ) {} + + with(middleware: FetchMiddleware): FetchApiBuilder { + return new FetchApiBuilder(this.implementation, [ + ...this.middleware, + middleware, + ]); + } + + build(): FetchApi { + let result = this.implementation; + + for (const m of this.middleware) { + result = m.apply(result); + } + + return { + fetch: result, + }; + } +} diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAwareFetchMiddleware.test.ts b/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAwareFetchMiddleware.test.ts new file mode 100644 index 0000000000..381ebd62a9 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAwareFetchMiddleware.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { IdentityAwareFetchMiddleware } from './IdentityAwareFetchMiddleware'; + +describe('IdentityAwareFetchMiddleware', () => { + it('injects the header only when a token is available', async () => { + const tokenFunction = jest.fn(); + const middleware = new IdentityAwareFetchMiddleware(tokenFunction); + const inner = jest.fn(); + const outer = middleware.apply(inner); + + // No token available + tokenFunction.mockResolvedValueOnce(undefined); + await outer(new Request('https://example.com')); + expect([...inner.mock.calls[0][0].headers.entries()]).toEqual([]); + + // Supply a token, header gets added + tokenFunction.mockResolvedValueOnce('token'); + await outer(new Request('https://example.com')); + expect([...inner.mock.calls[1][0].headers.entries()]).toEqual([ + ['backstage-token', 'token'], + ]); + + // Token no longer available + tokenFunction.mockResolvedValueOnce(undefined); + await outer(new Request('https://example.com')); + expect([...inner.mock.calls[2][0].headers.entries()]).toEqual([]); + }); +}); diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAwareFetchMiddleware.ts b/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAwareFetchMiddleware.ts new file mode 100644 index 0000000000..474ca83894 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAwareFetchMiddleware.ts @@ -0,0 +1,59 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { FetchFunction, FetchMiddleware } from './types'; + +const DEFAULT_HEADER_NAME = 'backstage-token'; + +/** + * A fetch middleware, which injects a Backstage token header when the user is + * signed in. + * + * @public + */ +export class IdentityAwareFetchMiddleware implements FetchMiddleware { + private headerName: string; + private tokenFunction: () => Promise; + + constructor(tokenFunction: () => Promise) { + this.headerName = DEFAULT_HEADER_NAME; + this.tokenFunction = tokenFunction; + } + + /** + * {@inheritdoc FetchMiddleware.apply} + */ + apply(next: FetchFunction): FetchFunction { + return async (input, init) => { + const token = await this.tokenFunction(); + if (typeof token !== 'string') { + return next(input, init); + } + + const request = new Request(input, init); + request.headers.set(this.headerName, token); + return next(request); + }; + } + + /** + * Changes the header name from the default value to a custom one. + */ + setHeaderName(name: string): IdentityAwareFetchMiddleware { + this.headerName = name; + return this; + } +} diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/index.ts b/packages/core-app-api/src/apis/implementations/FetchApi/index.ts new file mode 100644 index 0000000000..7544bd8bf2 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/FetchApi/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { BackstageProtocolResolverFetchMiddleware } from './BackstageProtocolResolverFetchMiddleware'; +export { FetchApiBuilder } from './FetchApiBuilder'; +export { IdentityAwareFetchMiddleware } from './IdentityAwareFetchMiddleware'; +export type { FetchFunction, FetchMiddleware } from './types'; diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/types.ts b/packages/core-app-api/src/apis/implementations/FetchApi/types.ts new file mode 100644 index 0000000000..4cf0bf47f4 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/FetchApi/types.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import crossFetch from 'cross-fetch'; + +/** + * The type of a fetch call. + * + * @public + */ +export type FetchFunction = typeof crossFetch; + +/** + * A middleware that modifies the behavior of an ongoing fetch. + * + * @public + */ +export interface FetchMiddleware { + /** + * Applies this middleware to an inner implementation. + * + * @param next - The next, inner, implementation, that this middleware shall + * call out to as part of the request cycle. + */ + apply(next: FetchFunction): FetchFunction; +} diff --git a/packages/core-app-api/src/apis/implementations/index.ts b/packages/core-app-api/src/apis/implementations/index.ts index c494f966f2..1c79d3d164 100644 --- a/packages/core-app-api/src/apis/implementations/index.ts +++ b/packages/core-app-api/src/apis/implementations/index.ts @@ -27,5 +27,6 @@ export * from './ConfigApi'; export * from './DiscoveryApi'; export * from './ErrorApi'; export * from './FeatureFlagsApi'; +export * from './FetchApi'; export * from './OAuthRequestApi'; export * from './StorageApi'; diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 9fd4f57ae9..148115fd69 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -9,6 +9,7 @@ import { BackstagePlugin as BackstagePlugin_2 } from '@backstage/core-plugin-api import { BackstageTheme } from '@backstage/theme'; import { ComponentType } from 'react'; import { Config } from '@backstage/config'; +import { default as fetch_2 } from 'cross-fetch'; import { IconComponent as IconComponent_2 } from '@backstage/core-plugin-api'; import { IdentityApi as IdentityApi_2 } from '@backstage/core-plugin-api'; import { Observable as Observable_2 } from '@backstage/types'; @@ -505,6 +506,14 @@ export enum FeatureFlagState { None = 0, } +// @public +export type FetchApi = { + fetch: typeof fetch_2; +}; + +// @public +export const fetchApiRef: ApiRef; + // @public export function getComponentData( node: ReactNode, diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index 72827bde73..365cec3134 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -34,6 +34,7 @@ "@backstage/types": "^0.1.1", "@backstage/version-bridge": "^0.1.1", "@material-ui/core": "^4.12.2", + "cross-fetch": "^3.0.6", "history": "^5.0.0", "prop-types": "^15.7.2", "react-router-dom": "6.0.0-beta.0", @@ -56,7 +57,6 @@ "@types/node": "^14.14.32", "@types/prop-types": "^15.7.3", "@types/zen-observable": "^0.8.0", - "cross-fetch": "^3.0.6", "msw": "^0.35.0" }, "files": [ diff --git a/packages/core-plugin-api/src/apis/definitions/FetchApi.ts b/packages/core-plugin-api/src/apis/definitions/FetchApi.ts new file mode 100644 index 0000000000..30eb950634 --- /dev/null +++ b/packages/core-plugin-api/src/apis/definitions/FetchApi.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fetch from 'cross-fetch'; +import { ApiRef, createApiRef } from '../system'; + +/** + * A wrapper for the fetch API, that has additional behaviors such as the + * ability to automatically inject auth information where necessary. + * + * @public + */ +export type FetchApi = { + fetch: typeof fetch; +}; + +/** + * A wrapper for the fetch API, that has additional behaviors such as the + * ability to automatically inject auth information where necessary. + * + * @public + */ +export const fetchApiRef: ApiRef = createApiRef({ + id: 'core.fetch', +}); diff --git a/packages/core-plugin-api/src/apis/definitions/index.ts b/packages/core-plugin-api/src/apis/definitions/index.ts index b7666bcb54..67d442587d 100644 --- a/packages/core-plugin-api/src/apis/definitions/index.ts +++ b/packages/core-plugin-api/src/apis/definitions/index.ts @@ -29,6 +29,7 @@ export * from './ConfigApi'; export * from './DiscoveryApi'; export * from './ErrorApi'; export * from './FeatureFlagsApi'; +export * from './FetchApi'; export * from './IdentityApi'; export * from './OAuthRequestApi'; export * from './StorageApi'; From afb7f31840d506c270035d1b53e5f4ede553e1bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 9 Dec 2021 15:10:20 +0100 Subject: [PATCH 2/3] fixed the browser behavior for BackstageProtocolResolverFetchMiddleware MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../BackstageProtocolResolverFetchMiddleware.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/BackstageProtocolResolverFetchMiddleware.ts b/packages/core-app-api/src/apis/implementations/FetchApi/BackstageProtocolResolverFetchMiddleware.ts index 03d08c3096..2144d0058d 100644 --- a/packages/core-app-api/src/apis/implementations/FetchApi/BackstageProtocolResolverFetchMiddleware.ts +++ b/packages/core-app-api/src/apis/implementations/FetchApi/BackstageProtocolResolverFetchMiddleware.ts @@ -43,13 +43,18 @@ export class BackstageProtocolResolverFetchMiddleware apply(next: FetchFunction): FetchFunction { return async (input, init) => { const request = new Request(input, init); - const { protocol, hostname, pathname, search, hash, username, password } = - new URL(request.url); + const prefix = 'backstage://'; - if (protocol !== 'backstage:') { + if (!request.url.startsWith(prefix)) { return next(input, init); } + // Switch to a known protocol, since browser URL parsing misbehaves wildly + // on foreign protocols + const { hostname, pathname, search, hash, username, password } = new URL( + `http://${request.url.substring(prefix.length)}`, + ); + let base = await this.discovery(hostname); if (username || password) { const baseUrl = new URL(base); From 3fa31ec84a4b564397d410228df25a9c75dac2d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 14 Dec 2021 14:11:30 +0100 Subject: [PATCH 3/3] address comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/itchy-grapes-think.md | 25 +++ packages/app-defaults/src/defaults/apis.ts | 32 ++-- packages/catalog-client/api-report.md | 3 +- packages/catalog-client/src/types/fetch.ts | 2 - packages/core-app-api/api-report.md | 50 +++--- packages/core-app-api/package.json | 1 - .../FetchApi/FetchMiddlewares.ts | 76 +++++++++ ...dentityAuthInjectorFetchMiddleware.test.ts | 153 ++++++++++++++++++ .../IdentityAuthInjectorFetchMiddleware.ts | 82 ++++++++++ .../IdentityAwareFetchMiddleware.test.ts | 43 ----- .../FetchApi/IdentityAwareFetchMiddleware.ts | 59 ------- ...inProtocolResolverFetchMiddleware.test.ts} | 31 ++-- ... PluginProtocolResolverFetchMiddleware.ts} | 26 ++- .../{FetchApiBuilder.ts => createFetchApi.ts} | 47 +++--- .../apis/implementations/FetchApi/index.ts | 7 +- .../apis/implementations/FetchApi/types.ts | 11 +- packages/core-plugin-api/api-report.md | 3 +- packages/core-plugin-api/package.json | 2 +- .../src/apis/definitions/FetchApi.ts | 1 - plugins/catalog/api-report.md | 2 +- plugins/catalog/src/CatalogClientWrapper.ts | 7 + plugins/catalog/src/plugin.ts | 16 +- 22 files changed, 444 insertions(+), 235 deletions(-) create mode 100644 .changeset/itchy-grapes-think.md create mode 100644 packages/core-app-api/src/apis/implementations/FetchApi/FetchMiddlewares.ts create mode 100644 packages/core-app-api/src/apis/implementations/FetchApi/IdentityAuthInjectorFetchMiddleware.test.ts create mode 100644 packages/core-app-api/src/apis/implementations/FetchApi/IdentityAuthInjectorFetchMiddleware.ts delete mode 100644 packages/core-app-api/src/apis/implementations/FetchApi/IdentityAwareFetchMiddleware.test.ts delete mode 100644 packages/core-app-api/src/apis/implementations/FetchApi/IdentityAwareFetchMiddleware.ts rename packages/core-app-api/src/apis/implementations/FetchApi/{BackstageProtocolResolverFetchMiddleware.test.ts => PluginProtocolResolverFetchMiddleware.test.ts} (69%) rename packages/core-app-api/src/apis/implementations/FetchApi/{BackstageProtocolResolverFetchMiddleware.ts => PluginProtocolResolverFetchMiddleware.ts} (75%) rename packages/core-app-api/src/apis/implementations/FetchApi/{FetchApiBuilder.ts => createFetchApi.ts} (54%) diff --git a/.changeset/itchy-grapes-think.md b/.changeset/itchy-grapes-think.md new file mode 100644 index 0000000000..c453b24922 --- /dev/null +++ b/.changeset/itchy-grapes-think.md @@ -0,0 +1,25 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Deprecated the `CatalogClientWrapper` class. + +The default implementation of `catalogApiRef` that this plugin exposes, is now powered by the new `fetchApiRef`. The default implementation of _that_ API, in turn, has the ability to inject the user's Backstage token in requests in a similar manner to what the deprecated `CatalogClientWrapper` used to do. The latter has therefore been taken out of the default catalog API implementation. + +If you use a custom `fetchApiRef` implementation that does NOT issue tokens, or use a custom `catalogApiRef` implementation which does NOT use the default `fetchApiRef`, you can still for some time wrap your catalog API in this class to get back the old behavior: + +```ts +// Add this to your packages/app/src/plugins.ts if you want to get back the old +// catalog client behavior: +createApiFactory({ + api: catalogApiRef, + deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, + factory: ({ discoveryApi, identityApi }) => + new CatalogClientWrapper({ + client: new CatalogClient({ discoveryApi }), + identityApi, + }), +}), +``` + +But do consider migrating to making use of the `fetchApiRef` as soon as convenient, since the wrapper class will be removed in a future release. diff --git a/packages/app-defaults/src/defaults/apis.ts b/packages/app-defaults/src/defaults/apis.ts index 06562caaef..29aa62bdab 100644 --- a/packages/app-defaults/src/defaults/apis.ts +++ b/packages/app-defaults/src/defaults/apis.ts @@ -34,9 +34,8 @@ import { OneLoginAuth, UnhandledErrorForwarder, AtlassianAuth, - FetchApiBuilder, - IdentityAwareFetchMiddleware, - BackstageProtocolResolverFetchMiddleware, + createFetchApi, + FetchMiddlewares, } from '@backstage/core-app-api'; import { @@ -99,20 +98,23 @@ export const apis = [ }), createApiFactory({ api: fetchApiRef, - deps: { identityApi: identityApiRef, discoveryApi: discoveryApiRef }, - factory: ({ identityApi, discoveryApi }) => { - return FetchApiBuilder.create() - .with( - new IdentityAwareFetchMiddleware(() => { - return identityApi.getCredentials().then(r => r.token); + deps: { + configApi: configApiRef, + identityApi: identityApiRef, + discoveryApi: discoveryApiRef, + }, + factory: ({ configApi, identityApi, discoveryApi }) => { + return createFetchApi({ + middleware: [ + FetchMiddlewares.resolvePluginProtocol({ + discoveryApi, }), - ) - .with( - new BackstageProtocolResolverFetchMiddleware(plugin => { - return discoveryApi.getBaseUrl(plugin); + FetchMiddlewares.injectIdentityAuth({ + identityApi, + config: configApi, }), - ) - .build(); + ], + }); }, }), createApiFactory({ diff --git a/packages/catalog-client/api-report.md b/packages/catalog-client/api-report.md index 41aea63716..495c98a104 100644 --- a/packages/catalog-client/api-report.md +++ b/packages/catalog-client/api-report.md @@ -5,7 +5,6 @@ ```ts import { Entity } from '@backstage/catalog-model'; import { EntityName } from '@backstage/catalog-model'; -import { default as fetch_2 } from 'cross-fetch'; import { Location as Location_2 } from '@backstage/catalog-model'; // @public @@ -159,6 +158,6 @@ export const ENTITY_STATUS_CATALOG_PROCESSING_TYPE = // @public export type FetchApi = { - fetch: typeof fetch_2; + fetch: typeof fetch; }; ``` diff --git a/packages/catalog-client/src/types/fetch.ts b/packages/catalog-client/src/types/fetch.ts index cdddd31810..59de6a68f1 100644 --- a/packages/catalog-client/src/types/fetch.ts +++ b/packages/catalog-client/src/types/fetch.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import fetch from 'cross-fetch'; - /** * This is a copy of FetchApi, to avoid importing core-plugin-api. * diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index 3277be7841..78bfd83251 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -24,9 +24,9 @@ import { BackstageIdentityApi } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { bitbucketAuthApiRef } from '@backstage/core-plugin-api'; import { ComponentType } from 'react'; +import { Config } from '@backstage/config'; import { ConfigReader } from '@backstage/config'; import { createApp as createApp_2 } from '@backstage/app-defaults'; -import crossFetch from 'cross-fetch'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { ErrorApi } from '@backstage/core-plugin-api'; import { ErrorApiError } from '@backstage/core-plugin-api'; @@ -290,14 +290,6 @@ export type BackstagePluginWithAnyOutput = Omit< )[]; }; -// @public -export class BackstageProtocolResolverFetchMiddleware - implements FetchMiddleware -{ - constructor(discovery: (pluginId: string) => Promise); - apply(next: FetchFunction): FetchFunction; -} - // @public export class BitbucketAuth { // (undocumented) @@ -328,6 +320,12 @@ export function createApp( options?: Parameters[0], ): BackstageApp & AppContext; +// @public +export function createFetchApi(options: { + baseImplementation?: typeof fetch | undefined; + middleware?: FetchMiddleware | FetchMiddleware[] | undefined; +}): FetchApi; + // @public export function createSpecializedApp(options: AppOptions): BackstageApp; @@ -380,21 +378,24 @@ export type FeatureFlaggedProps = { ); // @public -export class FetchApiBuilder { - // (undocumented) - build(): FetchApi; - // (undocumented) - static create(): FetchApiBuilder; - // (undocumented) - with(middleware: FetchMiddleware): FetchApiBuilder; +export interface FetchMiddleware { + apply(next: typeof fetch): typeof fetch; } // @public -export type FetchFunction = typeof crossFetch; - -// @public -export interface FetchMiddleware { - apply(next: FetchFunction): FetchFunction; +export class FetchMiddlewares { + static injectIdentityAuth(options: { + identityApi: IdentityApi; + config?: Config; + urlPrefixAllowlist?: string[]; + header?: { + name: string; + value: (backstageToken: string) => string; + }; + }): FetchMiddleware; + static resolvePluginProtocol(options: { + discoveryApi: DiscoveryApi; + }): FetchMiddleware; } // @public @@ -454,13 +455,6 @@ export class GoogleAuth { static create(options: OAuthApiCreateOptions): typeof googleAuthApiRef.T; } -// @public -export class IdentityAwareFetchMiddleware implements FetchMiddleware { - constructor(tokenFunction: () => Promise); - apply(next: FetchFunction): FetchFunction; - setHeaderName(name: string): IdentityAwareFetchMiddleware; -} - // @public export class LocalStorageFeatureFlags implements FeatureFlagsApi { // (undocumented) diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index d138417143..23079d7ba0 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -39,7 +39,6 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@types/prop-types": "^15.7.3", - "cross-fetch": "^3.0.6", "prop-types": "^15.7.2", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4", diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/FetchMiddlewares.ts b/packages/core-app-api/src/apis/implementations/FetchApi/FetchMiddlewares.ts new file mode 100644 index 0000000000..1cf09ff20d --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/FetchApi/FetchMiddlewares.ts @@ -0,0 +1,76 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Config } from '@backstage/config'; +import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; +import { IdentityAuthInjectorFetchMiddleware } from './IdentityAuthInjectorFetchMiddleware'; +import { PluginProtocolResolverFetchMiddleware } from './PluginProtocolResolverFetchMiddleware'; +import { FetchMiddleware } from './types'; + +/** + * A collection of common middlewares for the FetchApi. + * + * @public + */ +export class FetchMiddlewares { + /** + * Handles translation from `plugin://` URLs to concrete http(s) URLs based on + * the discovery API. + * + * @remarks + * + * If the request is for `plugin://catalog/entities?filter=x=y`, the discovery + * API will be queried for `'catalog'`. If it returned + * `https://backstage.example.net/api/catalog`, the resulting query would be + * `https://backstage.example.net/api/catalog/entities?filter=x=y`. + * + * If the incoming URL protocol was not `plugin`, the request is just passed + * through verbatim to the underlying implementation. + */ + static resolvePluginProtocol(options: { + discoveryApi: DiscoveryApi; + }): FetchMiddleware { + return new PluginProtocolResolverFetchMiddleware(options.discoveryApi); + } + + /** + * Injects a Backstage token header when the user is signed in. + * + * @remarks + * + * Per default, an `Authorization: Bearer ` is generated. This can be + * customized using the `header` option. + * + * The header injection only happens on allowlisted URLs. Per default, if the + * `config` option is passed in, the `backend.baseUrl` is allowlisted, unless + * the `urlPrefixAllowlist` option is passed in, in which case it takes + * precedence. If you pass in neither config nor an allowlist, the middleware + * will have no effect. + */ + static injectIdentityAuth(options: { + identityApi: IdentityApi; + config?: Config; + urlPrefixAllowlist?: string[]; + header?: { + name: string; + value: (backstageToken: string) => string; + }; + }): FetchMiddleware { + return IdentityAuthInjectorFetchMiddleware.create(options); + } + + private constructor() {} +} diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAuthInjectorFetchMiddleware.test.ts b/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAuthInjectorFetchMiddleware.test.ts new file mode 100644 index 0000000000..b69a4b9f7a --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAuthInjectorFetchMiddleware.test.ts @@ -0,0 +1,153 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConfigReader } from '@backstage/config'; +import { IdentityApi } from '@backstage/core-plugin-api'; +import { IdentityAuthInjectorFetchMiddleware } from './IdentityAuthInjectorFetchMiddleware'; + +describe('IdentityAuthInjectorFetchMiddleware', () => { + it('creates using defaults', async () => { + const middleware = IdentityAuthInjectorFetchMiddleware.create({ + identityApi: undefined as any, + }); + expect(middleware.urlPrefixAllowlist).toEqual([]); + expect(middleware.headerName).toEqual('authorization'); + expect(middleware.headerValue('t')).toEqual('Bearer t'); + }); + + it('creates using config', async () => { + const middleware = IdentityAuthInjectorFetchMiddleware.create({ + identityApi: undefined as any, + config: new ConfigReader({ + backend: { baseUrl: 'https://example.com/api' }, + }), + header: { name: 'auth', value: t => `${t}!` }, + }); + expect(middleware.urlPrefixAllowlist).toEqual(['https://example.com/api']); + expect(middleware.headerName).toEqual('auth'); + expect(middleware.headerValue('t')).toEqual('t!'); + }); + + it('creates using explicit allowlist', async () => { + const middleware = IdentityAuthInjectorFetchMiddleware.create({ + identityApi: undefined as any, + config: new ConfigReader({ + backend: { baseUrl: 'https://example.com/api' }, + }), + urlPrefixAllowlist: ['https://a.com', 'http://b.com:8080/'], + }); + expect(middleware.urlPrefixAllowlist).toEqual([ + 'https://a.com', + 'http://b.com:8080', + ]); + }); + + it('injects the header only when a token is available', async () => { + const tokenFunction = jest.fn(); + const identityApi = { + getCredentials: tokenFunction, + } as unknown as IdentityApi; + + const middleware = new IdentityAuthInjectorFetchMiddleware( + identityApi, + ['https://example.com'], + 'Authorization', + token => `Bearer ${token}`, + ); + const inner = jest.fn(); + const outer = middleware.apply(inner); + + // No token available + tokenFunction.mockResolvedValueOnce({ token: undefined }); + await outer(new Request('https://example.com')); + expect([...inner.mock.calls[0][0].headers.entries()]).toEqual([]); + + // Supply a token, header gets added + tokenFunction.mockResolvedValueOnce({ token: 'token' }); + await outer(new Request('https://example.com')); + expect([...inner.mock.calls[1][0].headers.entries()]).toEqual([ + ['authorization', 'Bearer token'], + ]); + + // Token no longer available + tokenFunction.mockResolvedValueOnce({ token: undefined }); + await outer(new Request('https://example.com')); + expect([...inner.mock.calls[2][0].headers.entries()]).toEqual([]); + }); + + it('does not overwrite an existing header with the same name', async () => { + const identityApi = { + getCredentials: () => ({ token: 'token' }), + } as unknown as IdentityApi; + + const middleware = new IdentityAuthInjectorFetchMiddleware( + identityApi, + ['https://example.com'], + 'Authorization', + token => `Bearer ${token}`, + ); + const inner = jest.fn(); + const outer = middleware.apply(inner); + + // No token available + await outer(new Request('https://example.com')); + expect([...inner.mock.calls[0][0].headers.entries()]).toEqual([ + ['authorization', 'Bearer token'], + ]); + + // Supply a token, header gets added + await outer( + new Request('https://example.com', { + headers: { authorization: 'do-not-clobber' }, + }), + ); + expect([...inner.mock.calls[1][0].headers.entries()]).toEqual([ + ['authorization', 'do-not-clobber'], + ]); + }); + + it('does not affect requests outside the allowlist', async () => { + const identityApi = { + getCredentials: () => ({ token: 'token' }), + } as unknown as IdentityApi; + + const middleware = new IdentityAuthInjectorFetchMiddleware( + identityApi, + ['https://example.com:8080/root'], + 'Authorization', + token => `Bearer ${token}`, + ); + + const inner = jest.fn(); + const outer = middleware.apply(inner); + + await outer(new Request('https://example.com:8080/root')); + await outer(new Request('https://example.com:8080/root/sub')); + await outer(new Request('https://example.com:8080/root2')); + await outer(new Request('https://example.com/root')); + await outer(new Request('http://example.com:8080/root')); + await outer(new Request('https://example.com/root')); + + const no: string[][] = []; + const yes: string[][] = [['authorization', 'Bearer token']]; + expect([...inner.mock.calls[0][0].headers.entries()]).toEqual(yes); + expect([...inner.mock.calls[1][0].headers.entries()]).toEqual(yes); + expect([...inner.mock.calls[2][0].headers.entries()]).toEqual(no); + expect([...inner.mock.calls[3][0].headers.entries()]).toEqual(no); + expect([...inner.mock.calls[4][0].headers.entries()]).toEqual(no); + expect([...inner.mock.calls[5][0].headers.entries()]).toEqual(no); + }); +}); diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAuthInjectorFetchMiddleware.ts b/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAuthInjectorFetchMiddleware.ts new file mode 100644 index 0000000000..46ada0a505 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAuthInjectorFetchMiddleware.ts @@ -0,0 +1,82 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Config } from '@backstage/config'; +import { IdentityApi } from '@backstage/core-plugin-api'; +import { FetchMiddleware } from './types'; + +/** + * A fetch middleware, which injects a Backstage token header when the user is + * signed in. + */ +export class IdentityAuthInjectorFetchMiddleware implements FetchMiddleware { + static create(options: { + identityApi: IdentityApi; + config?: Config; + urlPrefixAllowlist?: string[]; + header?: { + name: string; + value: (backstageToken: string) => string; + }; + }): IdentityAuthInjectorFetchMiddleware { + const allowlist: string[] = []; + if (options.urlPrefixAllowlist) { + allowlist.push(...options.urlPrefixAllowlist); + } else if (options.config) { + allowlist.push(options.config.getString('backend.baseUrl')); + } + + const headerName = options.header?.name || 'authorization'; + const headerValue = options.header?.value || (token => `Bearer ${token}`); + + return new IdentityAuthInjectorFetchMiddleware( + options.identityApi, + allowlist.map(prefix => prefix.replace(/\/$/, '')), + headerName, + headerValue, + ); + } + + constructor( + public readonly identityApi: IdentityApi, + public readonly urlPrefixAllowlist: string[], + public readonly headerName: string, + public readonly headerValue: (pluginId: string) => string, + ) {} + + apply(next: typeof fetch): typeof fetch { + return async (input, init) => { + // Skip this middleware if the header already exists, or if the URL + // doesn't match any of the allowlist items, or if there was no token + const request = new Request(input, init); + const { token } = await this.identityApi.getCredentials(); + if ( + request.headers.get(this.headerName) || + !this.urlPrefixAllowlist.some( + prefix => + request.url === prefix || request.url.startsWith(`${prefix}/`), + ) || + typeof token !== 'string' || + !token + ) { + return next(input, init); + } + + request.headers.set(this.headerName, this.headerValue(token)); + return next(request); + }; + } +} diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAwareFetchMiddleware.test.ts b/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAwareFetchMiddleware.test.ts deleted file mode 100644 index 381ebd62a9..0000000000 --- a/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAwareFetchMiddleware.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { IdentityAwareFetchMiddleware } from './IdentityAwareFetchMiddleware'; - -describe('IdentityAwareFetchMiddleware', () => { - it('injects the header only when a token is available', async () => { - const tokenFunction = jest.fn(); - const middleware = new IdentityAwareFetchMiddleware(tokenFunction); - const inner = jest.fn(); - const outer = middleware.apply(inner); - - // No token available - tokenFunction.mockResolvedValueOnce(undefined); - await outer(new Request('https://example.com')); - expect([...inner.mock.calls[0][0].headers.entries()]).toEqual([]); - - // Supply a token, header gets added - tokenFunction.mockResolvedValueOnce('token'); - await outer(new Request('https://example.com')); - expect([...inner.mock.calls[1][0].headers.entries()]).toEqual([ - ['backstage-token', 'token'], - ]); - - // Token no longer available - tokenFunction.mockResolvedValueOnce(undefined); - await outer(new Request('https://example.com')); - expect([...inner.mock.calls[2][0].headers.entries()]).toEqual([]); - }); -}); diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAwareFetchMiddleware.ts b/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAwareFetchMiddleware.ts deleted file mode 100644 index 474ca83894..0000000000 --- a/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAwareFetchMiddleware.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FetchFunction, FetchMiddleware } from './types'; - -const DEFAULT_HEADER_NAME = 'backstage-token'; - -/** - * A fetch middleware, which injects a Backstage token header when the user is - * signed in. - * - * @public - */ -export class IdentityAwareFetchMiddleware implements FetchMiddleware { - private headerName: string; - private tokenFunction: () => Promise; - - constructor(tokenFunction: () => Promise) { - this.headerName = DEFAULT_HEADER_NAME; - this.tokenFunction = tokenFunction; - } - - /** - * {@inheritdoc FetchMiddleware.apply} - */ - apply(next: FetchFunction): FetchFunction { - return async (input, init) => { - const token = await this.tokenFunction(); - if (typeof token !== 'string') { - return next(input, init); - } - - const request = new Request(input, init); - request.headers.set(this.headerName, token); - return next(request); - }; - } - - /** - * Changes the header name from the default value to a custom one. - */ - setHeaderName(name: string): IdentityAwareFetchMiddleware { - this.headerName = name; - return this; - } -} diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/BackstageProtocolResolverFetchMiddleware.test.ts b/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.test.ts similarity index 69% rename from packages/core-app-api/src/apis/implementations/FetchApi/BackstageProtocolResolverFetchMiddleware.test.ts rename to packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.test.ts index 7dcb90573b..2f4ec6b536 100644 --- a/packages/core-app-api/src/apis/implementations/FetchApi/BackstageProtocolResolverFetchMiddleware.test.ts +++ b/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.test.ts @@ -14,14 +14,18 @@ * limitations under the License. */ -import { BackstageProtocolResolverFetchMiddleware } from './BackstageProtocolResolverFetchMiddleware'; +import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { PluginProtocolResolverFetchMiddleware } from './PluginProtocolResolverFetchMiddleware'; -describe('BackstageProtocolResolverFetchMiddleware', () => { +describe('PluginProtocolResolverFetchMiddleware', () => { it.each([['https://passthrough.com/a']])( 'passes through regular URLs, %p', - async (url: string) => { + async url => { const resolve = jest.fn(); - const middleware = new BackstageProtocolResolverFetchMiddleware(resolve); + const discoveryApi = { getBaseUrl: resolve } as unknown as DiscoveryApi; + const middleware = new PluginProtocolResolverFetchMiddleware( + discoveryApi, + ); const inner = jest.fn(); const outer = middleware.apply(inner); @@ -33,38 +37,38 @@ describe('BackstageProtocolResolverFetchMiddleware', () => { it.each([ [ - 'backstage://my-plugin/sub/path', + 'plugin://my-plugin/sub/path', 'my-plugin', 'https://real.com/base', 'https://real.com/base/sub/path', ], [ - 'backstage://my-plugin/sub/path/', + 'plugin://my-plugin/sub/path/', 'my-plugin', 'https://real.com/base/', 'https://real.com/base/sub/path/', ], - ['backstage://x', 'x', 'http://real.com:8080', 'http://real.com:8080'], + ['plugin://x', 'x', 'http://real.com:8080', 'http://real.com:8080'], [ - 'backstage://x/a/b?c=d&e=f#g', + 'plugin://x/a/b?c=d&e=f#g', 'x', 'https://real.com/base', 'https://real.com/base/a/b?c=d&e=f#g', ], [ - 'backstage://x?c=d&e=f#g', + 'plugin://x?c=d&e=f#g', 'x', 'https://real.com:8080/base', 'https://real.com:8080/base?c=d&e=f#g', ], [ - 'backstage://username:password@x?c=d&e=f#g', + 'plugin://username:password@x?c=d&e=f#g', 'x', 'https://real.com:8080/base', 'https://username:password@real.com:8080/base?c=d&e=f#g', ], [ - 'backstage://x?c=d&e=f#g', + 'plugin://x?c=d&e=f#g', 'x', 'https://username:password@real.com:8080/base', 'https://username:password@real.com:8080/base?c=d&e=f#g', @@ -73,7 +77,10 @@ describe('BackstageProtocolResolverFetchMiddleware', () => { 'resolves backstage URLs, %p', async (original, host, resolved, result) => { const resolve = jest.fn(); - const middleware = new BackstageProtocolResolverFetchMiddleware(resolve); + const discoveryApi = { getBaseUrl: resolve } as unknown as DiscoveryApi; + const middleware = new PluginProtocolResolverFetchMiddleware( + discoveryApi, + ); const inner = jest.fn(); const outer = middleware.apply(inner); diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/BackstageProtocolResolverFetchMiddleware.ts b/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.ts similarity index 75% rename from packages/core-app-api/src/apis/implementations/FetchApi/BackstageProtocolResolverFetchMiddleware.ts rename to packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.ts index 2144d0058d..72834731fe 100644 --- a/packages/core-app-api/src/apis/implementations/FetchApi/BackstageProtocolResolverFetchMiddleware.ts +++ b/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { FetchFunction, FetchMiddleware } from './types'; +import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { FetchMiddleware } from './types'; function join(left: string, right: string): string { if (!right || right === '/') { @@ -25,25 +26,16 @@ function join(left: string, right: string): string { } /** - * Handles translation from backstage://some-plugin-id/ to concrete - * http(s) URLs. - * - * @public + * Handles translation from plugin://some-plugin-id/ to concrete http(s) + * URLs. */ -export class BackstageProtocolResolverFetchMiddleware - implements FetchMiddleware -{ - constructor( - private readonly discovery: (pluginId: string) => Promise, - ) {} +export class PluginProtocolResolverFetchMiddleware implements FetchMiddleware { + constructor(private readonly discoveryApi: DiscoveryApi) {} - /** - * {@inheritdoc FetchMiddleware.apply} - */ - apply(next: FetchFunction): FetchFunction { + apply(next: typeof fetch): typeof fetch { return async (input, init) => { const request = new Request(input, init); - const prefix = 'backstage://'; + const prefix = 'plugin://'; if (!request.url.startsWith(prefix)) { return next(input, init); @@ -55,7 +47,7 @@ export class BackstageProtocolResolverFetchMiddleware `http://${request.url.substring(prefix.length)}`, ); - let base = await this.discovery(hostname); + let base = await this.discoveryApi.getBaseUrl(hostname); if (username || password) { const baseUrl = new URL(base); const authority = `${username}${password ? `:${password}` : ''}@`; diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/FetchApiBuilder.ts b/packages/core-app-api/src/apis/implementations/FetchApi/createFetchApi.ts similarity index 54% rename from packages/core-app-api/src/apis/implementations/FetchApi/FetchApiBuilder.ts rename to packages/core-app-api/src/apis/implementations/FetchApi/createFetchApi.ts index 1c96eb1ad6..1b85695daa 100644 --- a/packages/core-app-api/src/apis/implementations/FetchApi/FetchApiBuilder.ts +++ b/packages/core-app-api/src/apis/implementations/FetchApi/createFetchApi.ts @@ -15,41 +15,32 @@ */ import { FetchApi } from '@backstage/core-plugin-api'; -import crossFetch from 'cross-fetch'; -import { FetchFunction, FetchMiddleware } from './types'; +import { FetchMiddleware } from './types'; /** * Builds a fetch API, based on the builtin fetch wrapped by a set of optional * middleware implementations that add behaviors. * + * @remarks + * + * The middleware are applied in reverse order, i.e. the last one will be + * "closest" to the base implementation. Passing in `[M1, M2, M3]` effectively + * leads to `M1(M2(M3(baseImplementation)))`. + * * @public */ -export class FetchApiBuilder { - static create(): FetchApiBuilder { - return new FetchApiBuilder(crossFetch, []); +export function createFetchApi(options: { + baseImplementation?: typeof fetch | undefined; + middleware?: FetchMiddleware | FetchMiddleware[] | undefined; +}): FetchApi { + let result = options.baseImplementation || global.fetch; + + const middleware = [options.middleware ?? []].flat().reverse(); + for (const m of middleware) { + result = m.apply(result); } - private constructor( - private readonly implementation: FetchFunction, - private readonly middleware: FetchMiddleware[], - ) {} - - with(middleware: FetchMiddleware): FetchApiBuilder { - return new FetchApiBuilder(this.implementation, [ - ...this.middleware, - middleware, - ]); - } - - build(): FetchApi { - let result = this.implementation; - - for (const m of this.middleware) { - result = m.apply(result); - } - - return { - fetch: result, - }; - } + return { + fetch: result, + }; } diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/index.ts b/packages/core-app-api/src/apis/implementations/FetchApi/index.ts index 7544bd8bf2..0f82a9020b 100644 --- a/packages/core-app-api/src/apis/implementations/FetchApi/index.ts +++ b/packages/core-app-api/src/apis/implementations/FetchApi/index.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -export { BackstageProtocolResolverFetchMiddleware } from './BackstageProtocolResolverFetchMiddleware'; -export { FetchApiBuilder } from './FetchApiBuilder'; -export { IdentityAwareFetchMiddleware } from './IdentityAwareFetchMiddleware'; -export type { FetchFunction, FetchMiddleware } from './types'; +export { createFetchApi } from './createFetchApi'; +export { FetchMiddlewares } from './FetchMiddlewares'; +export type { FetchMiddleware } from './types'; diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/types.ts b/packages/core-app-api/src/apis/implementations/FetchApi/types.ts index 4cf0bf47f4..5bf029cbf0 100644 --- a/packages/core-app-api/src/apis/implementations/FetchApi/types.ts +++ b/packages/core-app-api/src/apis/implementations/FetchApi/types.ts @@ -14,15 +14,6 @@ * limitations under the License. */ -import crossFetch from 'cross-fetch'; - -/** - * The type of a fetch call. - * - * @public - */ -export type FetchFunction = typeof crossFetch; - /** * A middleware that modifies the behavior of an ongoing fetch. * @@ -35,5 +26,5 @@ export interface FetchMiddleware { * @param next - The next, inner, implementation, that this middleware shall * call out to as part of the request cycle. */ - apply(next: FetchFunction): FetchFunction; + apply(next: typeof fetch): typeof fetch; } diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 148115fd69..67c10837ac 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -9,7 +9,6 @@ import { BackstagePlugin as BackstagePlugin_2 } from '@backstage/core-plugin-api import { BackstageTheme } from '@backstage/theme'; import { ComponentType } from 'react'; import { Config } from '@backstage/config'; -import { default as fetch_2 } from 'cross-fetch'; import { IconComponent as IconComponent_2 } from '@backstage/core-plugin-api'; import { IdentityApi as IdentityApi_2 } from '@backstage/core-plugin-api'; import { Observable as Observable_2 } from '@backstage/types'; @@ -508,7 +507,7 @@ export enum FeatureFlagState { // @public export type FetchApi = { - fetch: typeof fetch_2; + fetch: typeof fetch; }; // @public diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index 365cec3134..72827bde73 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -34,7 +34,6 @@ "@backstage/types": "^0.1.1", "@backstage/version-bridge": "^0.1.1", "@material-ui/core": "^4.12.2", - "cross-fetch": "^3.0.6", "history": "^5.0.0", "prop-types": "^15.7.2", "react-router-dom": "6.0.0-beta.0", @@ -57,6 +56,7 @@ "@types/node": "^14.14.32", "@types/prop-types": "^15.7.3", "@types/zen-observable": "^0.8.0", + "cross-fetch": "^3.0.6", "msw": "^0.35.0" }, "files": [ diff --git a/packages/core-plugin-api/src/apis/definitions/FetchApi.ts b/packages/core-plugin-api/src/apis/definitions/FetchApi.ts index 30eb950634..ba304157ea 100644 --- a/packages/core-plugin-api/src/apis/definitions/FetchApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/FetchApi.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import fetch from 'cross-fetch'; import { ApiRef, createApiRef } from '../system'; /** diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index 2be7fc8575..80271c89af 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -66,7 +66,7 @@ export type BackstageOverrides = Overrides & { // Warning: (ae-missing-release-tag) "CatalogClientWrapper" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public +// @public @deprecated export class CatalogClientWrapper implements CatalogApi { constructor(options: { client: CatalogClient; identityApi: IdentityApi }); // (undocumented) diff --git a/plugins/catalog/src/CatalogClientWrapper.ts b/plugins/catalog/src/CatalogClientWrapper.ts index 0463cbf30e..c5f072e96c 100644 --- a/plugins/catalog/src/CatalogClientWrapper.ts +++ b/plugins/catalog/src/CatalogClientWrapper.ts @@ -30,6 +30,13 @@ import { IdentityApi } from '@backstage/core-plugin-api'; /** * CatalogClient wrapper that injects identity token for all requests + * + * @deprecated The default catalog client now uses the `fetchApiRef` + * implementation, which in turn by default issues tokens just the same as this + * class used to assist in doing. If you use a custom `fetchApiRef` + * implementation that does NOT issue tokens, or use a custom `catalogApiRef` + * implementation which does not use the default `fetchApiRef`, you can wrap + * your catalog API in this class to get back the old behavior. */ export class CatalogClientWrapper implements CatalogApi { private readonly identityApi: IdentityApi; diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index 20f937b2e7..66fdf4638f 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -22,7 +22,6 @@ import { entityRouteRef, starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; -import { CatalogClientWrapper } from './CatalogClientWrapper'; import { createComponentRouteRef, viewTechDocRouteRef } from './routes'; import { createApiFactory, @@ -30,7 +29,7 @@ import { createPlugin, createRoutableExtension, discoveryApiRef, - identityApiRef, + fetchApiRef, storageApiRef, } from '@backstage/core-plugin-api'; @@ -39,14 +38,13 @@ export const catalogPlugin = createPlugin({ apis: [ createApiFactory({ api: catalogApiRef, - deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, - factory: ({ discoveryApi, identityApi }) => - new CatalogClientWrapper({ - client: new CatalogClient({ discoveryApi }), - identityApi, - }), + deps: { + discoveryApi: discoveryApiRef, + fetchApi: fetchApiRef, + }, + factory: ({ discoveryApi, fetchApi }) => + new CatalogClient({ discoveryApi, fetchApi }), }), - createApiFactory({ api: starredEntitiesApiRef, deps: { storageApi: storageApiRef },