@@ -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.
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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<string>);
|
||||
apply(next: FetchFunction): FetchFunction;
|
||||
}
|
||||
|
||||
// @public
|
||||
export class BitbucketAuth {
|
||||
// (undocumented)
|
||||
@@ -328,6 +320,12 @@ export function createApp(
|
||||
options?: Parameters<typeof createApp_2>[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<string | undefined>);
|
||||
apply(next: FetchFunction): FetchFunction;
|
||||
setHeaderName(name: string): IdentityAwareFetchMiddleware;
|
||||
}
|
||||
|
||||
// @public
|
||||
export class LocalStorageFeatureFlags implements FeatureFlagsApi {
|
||||
// (undocumented)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 <token>` 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() {}
|
||||
}
|
||||
+153
@@ -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);
|
||||
});
|
||||
});
|
||||
+82
@@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
-43
@@ -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([]);
|
||||
});
|
||||
});
|
||||
-59
@@ -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<string | undefined>;
|
||||
|
||||
constructor(tokenFunction: () => Promise<string | undefined>) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
+19
-12
@@ -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);
|
||||
|
||||
+9
-17
@@ -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/<path> to concrete
|
||||
* http(s) URLs.
|
||||
*
|
||||
* @public
|
||||
* Handles translation from plugin://some-plugin-id/<path> to concrete http(s)
|
||||
* URLs.
|
||||
*/
|
||||
export class BackstageProtocolResolverFetchMiddleware
|
||||
implements FetchMiddleware
|
||||
{
|
||||
constructor(
|
||||
private readonly discovery: (pluginId: string) => Promise<string>,
|
||||
) {}
|
||||
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}` : ''}@`;
|
||||
+19
-28
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import fetch from 'cross-fetch';
|
||||
import { ApiRef, createApiRef } from '../system';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user