Merge pull request #2074 from spotify/rugvip/discovery
[RFC] Add DiscoveryApi
This commit is contained in:
+15
-33
@@ -24,6 +24,8 @@ import {
|
||||
ErrorAlerter,
|
||||
featureFlagsApiRef,
|
||||
FeatureFlags,
|
||||
discoveryApiRef,
|
||||
UrlPatternDiscovery,
|
||||
GoogleAuth,
|
||||
GithubAuth,
|
||||
OAuth2,
|
||||
@@ -90,6 +92,10 @@ export const apis = (config: ConfigApi) => {
|
||||
|
||||
const builder = ApiRegistry.builder();
|
||||
|
||||
const discoveryApi = builder.add(
|
||||
discoveryApiRef,
|
||||
UrlPatternDiscovery.compile(`${backendUrl}/{{ pluginId }}`),
|
||||
);
|
||||
const alertApi = builder.add(alertApiRef, new AlertApiForwarder());
|
||||
const errorApi = builder.add(
|
||||
errorApiRef,
|
||||
@@ -121,8 +127,7 @@ export const apis = (config: ConfigApi) => {
|
||||
builder.add(
|
||||
googleAuthApiRef,
|
||||
GoogleAuth.create({
|
||||
backendUrl,
|
||||
basePath: '/auth/',
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
}),
|
||||
);
|
||||
@@ -139,8 +144,7 @@ export const apis = (config: ConfigApi) => {
|
||||
const githubAuthApi = builder.add(
|
||||
githubAuthApiRef,
|
||||
GithubAuth.create({
|
||||
backendUrl,
|
||||
basePath: '/auth/',
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
}),
|
||||
);
|
||||
@@ -148,8 +152,7 @@ export const apis = (config: ConfigApi) => {
|
||||
builder.add(
|
||||
oktaAuthApiRef,
|
||||
OktaAuth.create({
|
||||
backendUrl,
|
||||
basePath: '/auth/',
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
}),
|
||||
);
|
||||
@@ -157,8 +160,7 @@ export const apis = (config: ConfigApi) => {
|
||||
builder.add(
|
||||
gitlabAuthApiRef,
|
||||
GitlabAuth.create({
|
||||
backendUrl,
|
||||
basePath: '/auth/',
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
}),
|
||||
);
|
||||
@@ -166,8 +168,7 @@ export const apis = (config: ConfigApi) => {
|
||||
builder.add(
|
||||
auth0AuthApiRef,
|
||||
Auth0Auth.create({
|
||||
backendUrl,
|
||||
basePath: '/auth/',
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
}),
|
||||
);
|
||||
@@ -175,8 +176,7 @@ export const apis = (config: ConfigApi) => {
|
||||
builder.add(
|
||||
oauth2ApiRef,
|
||||
OAuth2.create({
|
||||
backendUrl,
|
||||
basePath: '/auth/',
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
}),
|
||||
);
|
||||
@@ -189,21 +189,9 @@ export const apis = (config: ConfigApi) => {
|
||||
}),
|
||||
);
|
||||
|
||||
builder.add(
|
||||
catalogApiRef,
|
||||
new CatalogClient({
|
||||
apiOrigin: backendUrl,
|
||||
basePath: '/catalog',
|
||||
}),
|
||||
);
|
||||
builder.add(catalogApiRef, new CatalogClient({ discoveryApi }));
|
||||
|
||||
builder.add(
|
||||
scaffolderApiRef,
|
||||
new ScaffolderApi({
|
||||
apiOrigin: backendUrl,
|
||||
basePath: '/scaffolder/v1',
|
||||
}),
|
||||
);
|
||||
builder.add(scaffolderApiRef, new ScaffolderApi({ discoveryApi }));
|
||||
|
||||
builder.add(gitOpsApiRef, new GitOpsRestApi('http://localhost:3008'));
|
||||
|
||||
@@ -224,13 +212,7 @@ export const apis = (config: ConfigApi) => {
|
||||
]),
|
||||
);
|
||||
|
||||
builder.add(
|
||||
rollbarApiRef,
|
||||
new RollbarClient({
|
||||
apiOrigin: backendUrl,
|
||||
basePath: '/rollbar',
|
||||
}),
|
||||
);
|
||||
builder.add(rollbarApiRef, new RollbarClient({ discoveryApi }));
|
||||
|
||||
builder.add(
|
||||
techdocsStorageApiRef,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { createApiRef } from '../ApiRef';
|
||||
|
||||
/**
|
||||
* The discovery API is used to provide a mechanism for plugins to
|
||||
* discover the endpoint to use to talk to their backend counterpart.
|
||||
*
|
||||
* The purpose of the discovery API is to allow for many different deployment
|
||||
* setups and routing methods through a central configuration, instead
|
||||
* of letting each individual plugin manage that configuration.
|
||||
*
|
||||
* Implementations of the discovery API can be a simple as a URL pattern
|
||||
* using the pluginId, but could also have overrides for individual plugins,
|
||||
* or query a separate discovery service.
|
||||
*/
|
||||
export type DiscoveryApi = {
|
||||
/**
|
||||
* Returns the HTTP base backend URL for a given plugin, without a trailing slash.
|
||||
*
|
||||
* This method must always be called just before making a request. as opposed to
|
||||
* fetching the URL when constructing an API client. That is to ensure that more
|
||||
* flexible routing patterns can be supported.
|
||||
*
|
||||
* For example, asking for the URL for `auth` may return something
|
||||
* like `https://backstage.example.com/api/auth`
|
||||
*/
|
||||
getBaseUrl(pluginId: string): Promise<string>;
|
||||
};
|
||||
|
||||
export const discoveryApiRef = createApiRef<DiscoveryApi>({
|
||||
id: 'core.discovery',
|
||||
description: 'Provides service discovery of backend plugins',
|
||||
});
|
||||
@@ -27,6 +27,7 @@ export * from './AppThemeApi';
|
||||
export * from './ConfigApi';
|
||||
export * from './ErrorApi';
|
||||
export * from './FeatureFlagsApi';
|
||||
export * from './DiscoveryApi';
|
||||
export * from './IdentityApi';
|
||||
export * from './OAuthRequestApi';
|
||||
export * from './StorageApi';
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { UrlPatternDiscovery } from './UrlPatternDiscovery';
|
||||
|
||||
describe('UrlPatternDiscovery', () => {
|
||||
it('should not require interpolation', async () => {
|
||||
const discoveryApi = UrlPatternDiscovery.compile('http://example.com');
|
||||
await expect(discoveryApi.getBaseUrl('my-plugin')).resolves.toBe(
|
||||
'http://example.com',
|
||||
);
|
||||
});
|
||||
|
||||
it('should use a plain pattern', async () => {
|
||||
const discoveryApi = UrlPatternDiscovery.compile(
|
||||
'http://localhost:7000/{{ pluginId }}',
|
||||
);
|
||||
await expect(discoveryApi.getBaseUrl('my-plugin')).resolves.toBe(
|
||||
'http://localhost:7000/my-plugin',
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow for multiple interpolation points', async () => {
|
||||
const discoveryApi = UrlPatternDiscovery.compile(
|
||||
'https://{{pluginId }}.example.com/api/{{ pluginId}}',
|
||||
);
|
||||
await expect(discoveryApi.getBaseUrl('my-plugin')).resolves.toBe(
|
||||
'https://my-plugin.example.com/api/my-plugin',
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate that the pattern is a valid URL', () => {
|
||||
expect(() => {
|
||||
UrlPatternDiscovery.compile('example.com');
|
||||
}).toThrow('Invalid discovery URL pattern, Invalid URL: example.com');
|
||||
|
||||
expect(() => {
|
||||
UrlPatternDiscovery.compile('http://');
|
||||
}).toThrow('Invalid discovery URL pattern, Invalid URL: http://');
|
||||
|
||||
expect(() => {
|
||||
UrlPatternDiscovery.compile('abc123');
|
||||
}).toThrow('Invalid discovery URL pattern, Invalid URL: abc123');
|
||||
|
||||
expect(() => {
|
||||
UrlPatternDiscovery.compile('http://example.com:{{pluginId}}');
|
||||
}).toThrow(
|
||||
'Invalid discovery URL pattern, Invalid URL: http://example.com:pluginId',
|
||||
);
|
||||
|
||||
expect(() => {
|
||||
UrlPatternDiscovery.compile('/{{pluginId}}');
|
||||
}).toThrow('Invalid discovery URL pattern, Invalid URL: /pluginId');
|
||||
|
||||
expect(() => {
|
||||
UrlPatternDiscovery.compile('http://localhost/{{pluginId}}?forbidden');
|
||||
}).toThrow('Invalid discovery URL pattern, URL must not have a query');
|
||||
|
||||
expect(() => {
|
||||
UrlPatternDiscovery.compile('http://localhost/{{pluginId}}#forbidden');
|
||||
}).toThrow('Invalid discovery URL pattern, URL must not have a hash');
|
||||
|
||||
expect(() => {
|
||||
UrlPatternDiscovery.compile('http://localhost/{{pluginId}}/');
|
||||
}).toThrow('Invalid discovery URL pattern, URL must not end with a slash');
|
||||
|
||||
expect(() => {
|
||||
UrlPatternDiscovery.compile('http://localhost/');
|
||||
}).toThrow('Invalid discovery URL pattern, URL must not end with a slash');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { DiscoveryApi } from '../../definitions/DiscoveryApi';
|
||||
|
||||
/**
|
||||
* UrlPatternDiscovery is a lightweight DiscoveryApi implementation.
|
||||
* It uses a single template string to construct URLs for each plugin.
|
||||
*/
|
||||
export class UrlPatternDiscovery implements DiscoveryApi {
|
||||
/**
|
||||
* Creates a new UrlPatternDiscovery given a template. The the only
|
||||
* interpolation done for the template is to replace instances of `{{pluginId}}`
|
||||
* with the ID of the plugin being requested.
|
||||
*
|
||||
* Example pattern: `http://localhost:7000/api/{{ pluginId }}`
|
||||
*/
|
||||
static compile(pattern: string): UrlPatternDiscovery {
|
||||
const parts = pattern.split(/\{\{\s*pluginId\s*\}\}/);
|
||||
|
||||
try {
|
||||
const urlStr = parts.join('pluginId');
|
||||
const url = new URL(urlStr);
|
||||
if (url.hash) {
|
||||
throw new Error('URL must not have a hash');
|
||||
}
|
||||
if (url.search) {
|
||||
throw new Error('URL must not have a query');
|
||||
}
|
||||
if (urlStr.endsWith('/')) {
|
||||
throw new Error('URL must not end with a slash');
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid discovery URL pattern, ${error.message}`);
|
||||
}
|
||||
|
||||
return new UrlPatternDiscovery(parts);
|
||||
}
|
||||
|
||||
private constructor(private readonly parts: string[]) {}
|
||||
|
||||
async getBaseUrl(pluginId: string): Promise<string> {
|
||||
return this.parts.join(pluginId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// This folder contains implementations for all core APIs.
|
||||
//
|
||||
// Plugins should rely on these APIs for functionality as much as possible.
|
||||
|
||||
export { UrlPatternDiscovery } from './UrlPatternDiscovery';
|
||||
@@ -27,16 +27,17 @@ import {
|
||||
AuthRequestOptions,
|
||||
BackstageIdentity,
|
||||
} from '../../../definitions/auth';
|
||||
import { OAuthRequestApi, AuthProvider } from '../../../definitions';
|
||||
import {
|
||||
OAuthRequestApi,
|
||||
AuthProvider,
|
||||
DiscoveryApi,
|
||||
} from '../../../definitions';
|
||||
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
|
||||
import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager';
|
||||
import { Observable } from '../../../../types';
|
||||
|
||||
type CreateOptions = {
|
||||
// TODO(Following the words of Rugvip): These two should be grabbed from global config when available, they're not unique to Auth0Auth
|
||||
backendUrl: string;
|
||||
basePath: string;
|
||||
|
||||
discoveryApi: DiscoveryApi;
|
||||
oauthRequestApi: OAuthRequestApi;
|
||||
|
||||
environment?: string;
|
||||
@@ -67,15 +68,13 @@ class Auth0Auth
|
||||
BackstageIdentityApi,
|
||||
SessionStateApi {
|
||||
static create({
|
||||
backendUrl,
|
||||
basePath,
|
||||
discoveryApi,
|
||||
environment = 'development',
|
||||
provider = DEFAULT_PROVIDER,
|
||||
oauthRequestApi,
|
||||
}: CreateOptions) {
|
||||
const connector = new DefaultAuthConnector({
|
||||
backendUrl,
|
||||
basePath,
|
||||
discoveryApi,
|
||||
environment,
|
||||
provider,
|
||||
oauthRequestApi: oauthRequestApi,
|
||||
|
||||
@@ -25,7 +25,11 @@ import {
|
||||
BackstageIdentity,
|
||||
AuthRequestOptions,
|
||||
} from '../../../definitions/auth';
|
||||
import { OAuthRequestApi, AuthProvider } from '../../../definitions';
|
||||
import {
|
||||
OAuthRequestApi,
|
||||
AuthProvider,
|
||||
DiscoveryApi,
|
||||
} from '../../../definitions';
|
||||
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
|
||||
import {
|
||||
AuthSessionStore,
|
||||
@@ -34,10 +38,7 @@ import {
|
||||
import { Observable } from '../../../../types';
|
||||
|
||||
type CreateOptions = {
|
||||
// TODO(Rugvip): These two should be grabbed from global config when available, they're not unique to GithubAuth
|
||||
backendUrl: string;
|
||||
basePath: string;
|
||||
|
||||
discoveryApi: DiscoveryApi;
|
||||
oauthRequestApi: OAuthRequestApi;
|
||||
|
||||
environment?: string;
|
||||
@@ -62,15 +63,13 @@ const DEFAULT_PROVIDER = {
|
||||
|
||||
class GithubAuth implements OAuthApi, SessionStateApi {
|
||||
static create({
|
||||
backendUrl,
|
||||
basePath,
|
||||
discoveryApi,
|
||||
environment = 'development',
|
||||
provider = DEFAULT_PROVIDER,
|
||||
oauthRequestApi,
|
||||
}: CreateOptions) {
|
||||
const connector = new DefaultAuthConnector({
|
||||
backendUrl,
|
||||
basePath,
|
||||
discoveryApi,
|
||||
environment,
|
||||
provider,
|
||||
oauthRequestApi: oauthRequestApi,
|
||||
|
||||
@@ -25,15 +25,17 @@ import {
|
||||
BackstageIdentity,
|
||||
AuthRequestOptions,
|
||||
} from '../../../definitions/auth';
|
||||
import { OAuthRequestApi, AuthProvider } from '../../../definitions';
|
||||
import {
|
||||
OAuthRequestApi,
|
||||
AuthProvider,
|
||||
DiscoveryApi,
|
||||
} from '../../../definitions';
|
||||
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
|
||||
import { StaticAuthSessionManager } from '../../../../lib/AuthSessionManager';
|
||||
import { Observable } from '../../../../types';
|
||||
|
||||
type CreateOptions = {
|
||||
backendUrl: string;
|
||||
basePath: string;
|
||||
|
||||
discoveryApi: DiscoveryApi;
|
||||
oauthRequestApi: OAuthRequestApi;
|
||||
|
||||
environment?: string;
|
||||
@@ -58,15 +60,13 @@ const DEFAULT_PROVIDER = {
|
||||
|
||||
class GitlabAuth implements OAuthApi, SessionStateApi {
|
||||
static create({
|
||||
backendUrl,
|
||||
basePath,
|
||||
discoveryApi,
|
||||
environment = 'development',
|
||||
provider = DEFAULT_PROVIDER,
|
||||
oauthRequestApi,
|
||||
}: CreateOptions) {
|
||||
const connector = new DefaultAuthConnector({
|
||||
backendUrl,
|
||||
basePath,
|
||||
discoveryApi,
|
||||
environment,
|
||||
provider,
|
||||
oauthRequestApi,
|
||||
|
||||
@@ -28,16 +28,17 @@ import {
|
||||
AuthRequestOptions,
|
||||
BackstageIdentity,
|
||||
} from '../../../definitions/auth';
|
||||
import { OAuthRequestApi, AuthProvider } from '../../../definitions';
|
||||
import {
|
||||
OAuthRequestApi,
|
||||
AuthProvider,
|
||||
DiscoveryApi,
|
||||
} from '../../../definitions';
|
||||
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
|
||||
import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager';
|
||||
import { Observable } from '../../../../types';
|
||||
|
||||
type CreateOptions = {
|
||||
// TODO(Rugvip): These two should be grabbed from global config when available, they're not unique to GoogleAuth
|
||||
backendUrl: string;
|
||||
basePath: string;
|
||||
|
||||
discoveryApi: DiscoveryApi;
|
||||
oauthRequestApi: OAuthRequestApi;
|
||||
|
||||
environment?: string;
|
||||
@@ -71,15 +72,13 @@ class GoogleAuth
|
||||
BackstageIdentityApi,
|
||||
SessionStateApi {
|
||||
static create({
|
||||
backendUrl,
|
||||
basePath,
|
||||
discoveryApi,
|
||||
environment = 'development',
|
||||
provider = DEFAULT_PROVIDER,
|
||||
oauthRequestApi,
|
||||
}: CreateOptions) {
|
||||
const connector = new DefaultAuthConnector({
|
||||
backendUrl,
|
||||
basePath,
|
||||
discoveryApi,
|
||||
environment,
|
||||
provider,
|
||||
oauthRequestApi: oauthRequestApi,
|
||||
|
||||
@@ -19,7 +19,11 @@ import { DefaultAuthConnector } from '../../../../lib/AuthConnector';
|
||||
import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager';
|
||||
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
|
||||
import { Observable } from '../../../../types';
|
||||
import { AuthProvider, OAuthRequestApi } from '../../../definitions';
|
||||
import {
|
||||
AuthProvider,
|
||||
OAuthRequestApi,
|
||||
DiscoveryApi,
|
||||
} from '../../../definitions';
|
||||
import {
|
||||
AuthRequestOptions,
|
||||
BackstageIdentity,
|
||||
@@ -33,9 +37,7 @@ import {
|
||||
import { OAuth2Session } from './types';
|
||||
|
||||
type CreateOptions = {
|
||||
backendUrl: string;
|
||||
basePath: string;
|
||||
|
||||
discoveryApi: DiscoveryApi;
|
||||
oauthRequestApi: OAuthRequestApi;
|
||||
|
||||
environment?: string;
|
||||
@@ -64,15 +66,13 @@ const SCOPE_PREFIX = '';
|
||||
class OAuth2
|
||||
implements OAuthApi, OpenIdConnectApi, ProfileInfoApi, SessionStateApi {
|
||||
static create({
|
||||
backendUrl,
|
||||
basePath,
|
||||
discoveryApi,
|
||||
environment = 'development',
|
||||
provider = DEFAULT_PROVIDER,
|
||||
oauthRequestApi,
|
||||
}: CreateOptions) {
|
||||
const connector = new DefaultAuthConnector({
|
||||
backendUrl,
|
||||
basePath,
|
||||
discoveryApi,
|
||||
environment,
|
||||
provider,
|
||||
oauthRequestApi: oauthRequestApi,
|
||||
|
||||
@@ -28,15 +28,17 @@ import {
|
||||
AuthRequestOptions,
|
||||
BackstageIdentity,
|
||||
} from '../../../definitions/auth';
|
||||
import { OAuthRequestApi, AuthProvider } from '../../../definitions';
|
||||
import {
|
||||
OAuthRequestApi,
|
||||
AuthProvider,
|
||||
DiscoveryApi,
|
||||
} from '../../../definitions';
|
||||
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
|
||||
import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager';
|
||||
import { Observable } from '../../../../types';
|
||||
|
||||
type CreateOptions = {
|
||||
backendUrl: string;
|
||||
basePath: string;
|
||||
|
||||
discoveryApi: DiscoveryApi;
|
||||
oauthRequestApi: OAuthRequestApi;
|
||||
|
||||
environment?: string;
|
||||
@@ -80,15 +82,13 @@ class OktaAuth
|
||||
BackstageIdentityApi,
|
||||
SessionStateApi {
|
||||
static create({
|
||||
backendUrl,
|
||||
basePath,
|
||||
discoveryApi,
|
||||
environment = 'development',
|
||||
provider = DEFAULT_PROVIDER,
|
||||
oauthRequestApi,
|
||||
}: CreateOptions) {
|
||||
const connector = new DefaultAuthConnector({
|
||||
backendUrl,
|
||||
basePath,
|
||||
discoveryApi,
|
||||
environment,
|
||||
provider,
|
||||
oauthRequestApi: oauthRequestApi,
|
||||
|
||||
@@ -24,5 +24,6 @@ export * from './AlertApi';
|
||||
export * from './AppThemeApi';
|
||||
export * from './ConfigApi';
|
||||
export * from './ErrorApi';
|
||||
export * from './DiscoveryApi';
|
||||
export * from './OAuthRequestApi';
|
||||
export * from './StorageApi';
|
||||
|
||||
@@ -18,11 +18,12 @@ import ProviderIcon from '@material-ui/icons/AcUnit';
|
||||
import { DefaultAuthConnector } from './DefaultAuthConnector';
|
||||
import MockOAuthApi from '../../apis/implementations/OAuthRequestApi/MockOAuthApi';
|
||||
import * as loginPopup from '../loginPopup';
|
||||
import { UrlPatternDiscovery } from '../../apis';
|
||||
|
||||
const anyFetch = fetch as any;
|
||||
|
||||
const defaultOptions = {
|
||||
backendUrl: 'http://my-origin',
|
||||
discoveryApi: UrlPatternDiscovery.compile('http://my-host/api/{{pluginId}}'),
|
||||
environment: 'production',
|
||||
provider: {
|
||||
id: 'my-provider',
|
||||
@@ -115,7 +116,7 @@ describe('DefaultAuthConnector', () => {
|
||||
expect(popupSpy).toBeCalledTimes(1);
|
||||
expect(popupSpy.mock.calls[0][0]).toMatchObject({
|
||||
url:
|
||||
'http://my-origin/api/auth/my-provider/start?scope=a%20b&env=production',
|
||||
'http://my-host/api/auth/my-provider/start?scope=a%20b&env=production',
|
||||
});
|
||||
|
||||
await expect(sessionPromise).resolves.toEqual({
|
||||
@@ -141,9 +142,9 @@ describe('DefaultAuthConnector', () => {
|
||||
instantPopup: true,
|
||||
});
|
||||
|
||||
expect(popupSpy).toBeCalledTimes(1);
|
||||
|
||||
await expect(sessionPromise).resolves.toBe('my-session');
|
||||
|
||||
expect(popupSpy).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should use join func to join scopes', async () => {
|
||||
@@ -164,7 +165,7 @@ describe('DefaultAuthConnector', () => {
|
||||
expect(popupSpy).toBeCalledTimes(1);
|
||||
expect(popupSpy.mock.calls[0][0]).toMatchObject({
|
||||
url:
|
||||
'http://my-origin/api/auth/my-provider/start?scope=-ab-&env=production',
|
||||
'http://my-host/api/auth/my-provider/start?scope=-ab-&env=production',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,21 +15,19 @@
|
||||
*/
|
||||
|
||||
import { AuthRequester } from '../../apis';
|
||||
import { OAuthRequestApi, AuthProvider } from '../../apis/definitions';
|
||||
import {
|
||||
OAuthRequestApi,
|
||||
AuthProvider,
|
||||
DiscoveryApi,
|
||||
} from '../../apis/definitions';
|
||||
import { showLoginPopup } from '../loginPopup';
|
||||
import { AuthConnector, CreateSessionOptions } from './types';
|
||||
|
||||
const DEFAULT_BASE_PATH = '/api/auth/';
|
||||
|
||||
type Options<AuthSession> = {
|
||||
/**
|
||||
* The base URL of the auth backend.
|
||||
* DiscoveryApi instance used to locate the auth backend endpoint.
|
||||
*/
|
||||
backendUrl?: string;
|
||||
/**
|
||||
* Base path of the auth requests, defaults to /api/auth/
|
||||
*/
|
||||
basePath?: string;
|
||||
discoveryApi: DiscoveryApi;
|
||||
/**
|
||||
* Environment hint passed on to auth backend, for example 'production' or 'development'
|
||||
*/
|
||||
@@ -64,8 +62,7 @@ function defaultJoinScopes(scopes: Set<string>) {
|
||||
*/
|
||||
export class DefaultAuthConnector<AuthSession>
|
||||
implements AuthConnector<AuthSession> {
|
||||
private readonly backendUrl: string;
|
||||
private readonly basePath: string;
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
private readonly environment: string;
|
||||
private readonly provider: AuthProvider & { id: string };
|
||||
private readonly joinScopesFunc: (scopes: Set<string>) => string;
|
||||
@@ -74,8 +71,7 @@ export class DefaultAuthConnector<AuthSession>
|
||||
|
||||
constructor(options: Options<AuthSession>) {
|
||||
const {
|
||||
backendUrl = window.location.origin,
|
||||
basePath = DEFAULT_BASE_PATH,
|
||||
discoveryApi,
|
||||
environment,
|
||||
provider,
|
||||
joinScopes = defaultJoinScopes,
|
||||
@@ -88,8 +84,7 @@ export class DefaultAuthConnector<AuthSession>
|
||||
onAuthRequest: scopes => this.showPopup(scopes),
|
||||
});
|
||||
|
||||
this.backendUrl = backendUrl;
|
||||
this.basePath = basePath;
|
||||
this.discoveryApi = discoveryApi;
|
||||
this.environment = environment;
|
||||
this.provider = provider;
|
||||
this.joinScopesFunc = joinScopes;
|
||||
@@ -104,12 +99,15 @@ export class DefaultAuthConnector<AuthSession>
|
||||
}
|
||||
|
||||
async refreshSession(): Promise<any> {
|
||||
const res = await fetch(this.buildUrl('/refresh', { optional: true }), {
|
||||
headers: {
|
||||
'x-requested-with': 'XMLHttpRequest',
|
||||
const res = await fetch(
|
||||
await this.buildUrl('/refresh', { optional: true }),
|
||||
{
|
||||
headers: {
|
||||
'x-requested-with': 'XMLHttpRequest',
|
||||
},
|
||||
credentials: 'include',
|
||||
},
|
||||
credentials: 'include',
|
||||
}).catch(error => {
|
||||
).catch(error => {
|
||||
throw new Error(`Auth refresh request failed, ${error}`);
|
||||
});
|
||||
|
||||
@@ -134,7 +132,7 @@ export class DefaultAuthConnector<AuthSession>
|
||||
}
|
||||
|
||||
async removeSession(): Promise<void> {
|
||||
const res = await fetch(this.buildUrl('/logout'), {
|
||||
const res = await fetch(await this.buildUrl('/logout'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'x-requested-with': 'XMLHttpRequest',
|
||||
@@ -153,13 +151,12 @@ export class DefaultAuthConnector<AuthSession>
|
||||
|
||||
private async showPopup(scopes: Set<string>): Promise<AuthSession> {
|
||||
const scope = this.joinScopesFunc(scopes);
|
||||
const popupUrl = this.buildUrl('/start', { scope });
|
||||
const { origin } = new URL(this.backendUrl);
|
||||
const popupUrl = await this.buildUrl('/start', { scope });
|
||||
|
||||
const payload = await showLoginPopup({
|
||||
url: popupUrl,
|
||||
name: `${this.provider.title} Login`,
|
||||
origin,
|
||||
origin: new URL(popupUrl).origin,
|
||||
width: 450,
|
||||
height: 730,
|
||||
});
|
||||
@@ -167,16 +164,17 @@ export class DefaultAuthConnector<AuthSession>
|
||||
return await this.sessionTransform(payload);
|
||||
}
|
||||
|
||||
private buildUrl(
|
||||
private async buildUrl(
|
||||
path: string,
|
||||
query?: { [key: string]: string | boolean | undefined },
|
||||
): string {
|
||||
): Promise<string> {
|
||||
const baseUrl = await this.discoveryApi.getBaseUrl('auth');
|
||||
const queryString = this.buildQueryString({
|
||||
...query,
|
||||
env: this.environment,
|
||||
});
|
||||
|
||||
return `${this.backendUrl}${this.basePath}${this.provider.id}${path}${queryString}`;
|
||||
return `${baseUrl}/${this.provider.id}${path}${queryString}`;
|
||||
}
|
||||
|
||||
private buildQueryString(query?: {
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
ConfigApi,
|
||||
ErrorApiForwarder,
|
||||
ErrorAlerter,
|
||||
discoveryApiRef,
|
||||
UrlPatternDiscovery,
|
||||
oauthRequestApiRef,
|
||||
OAuthRequestManager,
|
||||
storageApiRef,
|
||||
@@ -24,6 +26,10 @@ export const apis = (config: ConfigApi) => {
|
||||
|
||||
const builder = ApiRegistry.builder();
|
||||
|
||||
const discoveryApi = builder.add(
|
||||
discoveryApiRef,
|
||||
UrlPatternDiscovery.compile(`${backendUrl}/{{ pluginId }}`),
|
||||
);
|
||||
const alertApi = builder.add(alertApiRef, new AlertApiForwarder());
|
||||
const errorApi = builder.add(
|
||||
errorApiRef,
|
||||
@@ -33,21 +39,9 @@ export const apis = (config: ConfigApi) => {
|
||||
builder.add(storageApiRef, WebStorage.create({ errorApi }));
|
||||
builder.add(oauthRequestApiRef, new OAuthRequestManager());
|
||||
|
||||
builder.add(
|
||||
catalogApiRef,
|
||||
new CatalogClient({
|
||||
apiOrigin: backendUrl,
|
||||
basePath: '/catalog',
|
||||
}),
|
||||
);
|
||||
builder.add(catalogApiRef, new CatalogClient({ discoveryApi }));
|
||||
|
||||
builder.add(
|
||||
scaffolderApiRef,
|
||||
new ScaffolderApi({
|
||||
apiOrigin: backendUrl,
|
||||
basePath: '/scaffolder/v1',
|
||||
}),
|
||||
);
|
||||
builder.add(scaffolderApiRef, new ScaffolderApi({ discoveryApi }));
|
||||
|
||||
return builder.build();
|
||||
};
|
||||
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
AlertApiForwarder,
|
||||
oauthRequestApiRef,
|
||||
OAuthRequestManager,
|
||||
UrlPatternDiscovery,
|
||||
discoveryApiRef,
|
||||
GoogleAuth,
|
||||
googleAuthApiRef,
|
||||
GithubAuth,
|
||||
@@ -58,46 +60,49 @@ export const oauthRequestApiFactory = createApiFactory({
|
||||
factory: () => new OAuthRequestManager(),
|
||||
});
|
||||
|
||||
export const discoveryApiFactory = createApiFactory({
|
||||
implements: discoveryApiRef,
|
||||
deps: {},
|
||||
factory: () =>
|
||||
UrlPatternDiscovery.compile(`http://localhost:7000/{{ pluginId }}`),
|
||||
});
|
||||
|
||||
export const googleAuthApiFactory = createApiFactory({
|
||||
implements: googleAuthApiRef,
|
||||
deps: { oauthRequestApi: oauthRequestApiRef },
|
||||
factory: ({ oauthRequestApi }) =>
|
||||
deps: { discoveryApi: discoveryApiRef, oauthRequestApi: oauthRequestApiRef },
|
||||
factory: ({ discoveryApi, oauthRequestApi }) =>
|
||||
GoogleAuth.create({
|
||||
backendUrl: 'http://localhost:7000',
|
||||
basePath: '/auth/',
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
}),
|
||||
});
|
||||
|
||||
export const githubAuthApiFactory = createApiFactory({
|
||||
implements: githubAuthApiRef,
|
||||
deps: { oauthRequestApi: oauthRequestApiRef },
|
||||
factory: ({ oauthRequestApi }) =>
|
||||
deps: { discoveryApi: discoveryApiRef, oauthRequestApi: oauthRequestApiRef },
|
||||
factory: ({ discoveryApi, oauthRequestApi }) =>
|
||||
GithubAuth.create({
|
||||
backendUrl: 'http://localhost:7000',
|
||||
basePath: '/auth/',
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
}),
|
||||
});
|
||||
|
||||
export const gitlabAuthApiFactory = createApiFactory({
|
||||
implements: gitlabAuthApiRef,
|
||||
deps: { oauthRequestApi: oauthRequestApiRef },
|
||||
factory: ({ oauthRequestApi }) =>
|
||||
deps: { discoveryApi: discoveryApiRef, oauthRequestApi: oauthRequestApiRef },
|
||||
factory: ({ discoveryApi, oauthRequestApi }) =>
|
||||
GitlabAuth.create({
|
||||
backendUrl: 'http://localhost:7000',
|
||||
basePath: '/auth/',
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
}),
|
||||
});
|
||||
|
||||
export const auth0AuthApiFactory = createApiFactory({
|
||||
implements: auth0AuthApiRef,
|
||||
deps: { oauthRequestApi: oauthRequestApiRef },
|
||||
factory: ({ oauthRequestApi }) =>
|
||||
deps: { discoveryApi: discoveryApiRef, oauthRequestApi: oauthRequestApiRef },
|
||||
factory: ({ discoveryApi, oauthRequestApi }) =>
|
||||
Auth0Auth.create({
|
||||
backendUrl: 'http://localhost:7000',
|
||||
basePath: '/auth/',
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -18,25 +18,21 @@ import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { CatalogClient } from './CatalogClient';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { UrlPatternDiscovery } from '@backstage/core';
|
||||
|
||||
const server = setupServer();
|
||||
const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base';
|
||||
const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl);
|
||||
|
||||
describe('CatalogClient', () => {
|
||||
beforeAll(() => server.listen());
|
||||
afterEach(() => server.resetHandlers());
|
||||
afterAll(() => server.close());
|
||||
const mockApiOrigin = 'http://backstage:9191';
|
||||
const mockBasePath = '/i-am-a-mock-base';
|
||||
let client = new CatalogClient({
|
||||
apiOrigin: mockApiOrigin,
|
||||
basePath: mockBasePath,
|
||||
});
|
||||
|
||||
let client = new CatalogClient({ discoveryApi });
|
||||
|
||||
beforeEach(() => {
|
||||
client = new CatalogClient({
|
||||
apiOrigin: mockApiOrigin,
|
||||
basePath: mockBasePath,
|
||||
});
|
||||
client = new CatalogClient({ discoveryApi });
|
||||
});
|
||||
|
||||
describe('getEntiies', () => {
|
||||
@@ -61,7 +57,7 @@ describe('CatalogClient', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
rest.get(`${mockApiOrigin}${mockBasePath}/entities`, (_, res, ctx) => {
|
||||
rest.get(`${mockBaseUrl}/entities`, (_, res, ctx) => {
|
||||
return res(ctx.json(defaultResponse));
|
||||
}),
|
||||
);
|
||||
@@ -75,15 +71,12 @@ describe('CatalogClient', () => {
|
||||
it('builds entity search filters properly', async () => {
|
||||
expect.assertions(2);
|
||||
server.use(
|
||||
rest.get(
|
||||
`${mockApiOrigin}${mockBasePath}/entities`,
|
||||
(req, res, ctx) => {
|
||||
expect(req.url.searchParams.toString()).toBe(
|
||||
'a=1&b=2&b=3&%C3%B6=%3D',
|
||||
);
|
||||
return res(ctx.json([]));
|
||||
},
|
||||
),
|
||||
rest.get(`${mockBaseUrl}/entities`, (req, res, ctx) => {
|
||||
expect(req.url.searchParams.toString()).toBe(
|
||||
'a=1&b=2&b=3&%C3%B6=%3D',
|
||||
);
|
||||
return res(ctx.json([]));
|
||||
}),
|
||||
);
|
||||
|
||||
const entities = await client.getEntities({
|
||||
|
||||
@@ -20,24 +20,17 @@ import {
|
||||
LOCATION_ANNOTATION,
|
||||
} from '@backstage/catalog-model';
|
||||
import { CatalogApi, EntityCompoundName } from './types';
|
||||
import { DiscoveryApi } from '@backstage/core';
|
||||
|
||||
export class CatalogClient implements CatalogApi {
|
||||
private apiOrigin: string;
|
||||
private basePath: string;
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
|
||||
constructor({
|
||||
apiOrigin,
|
||||
basePath,
|
||||
}: {
|
||||
apiOrigin: string;
|
||||
basePath: string;
|
||||
}) {
|
||||
this.apiOrigin = apiOrigin;
|
||||
this.basePath = basePath;
|
||||
constructor(options: { discoveryApi: DiscoveryApi }) {
|
||||
this.discoveryApi = options.discoveryApi;
|
||||
}
|
||||
|
||||
private async getRequired(path: string): Promise<any> {
|
||||
const url = `${this.apiOrigin}${this.basePath}${path}`;
|
||||
const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`;
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -50,7 +43,7 @@ export class CatalogClient implements CatalogApi {
|
||||
}
|
||||
|
||||
private async getOptional(path: string): Promise<any | undefined> {
|
||||
const url = `${this.apiOrigin}${this.basePath}${path}`;
|
||||
const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`;
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -100,7 +93,7 @@ export class CatalogClient implements CatalogApi {
|
||||
|
||||
async addLocation(type: string, target: string) {
|
||||
const response = await fetch(
|
||||
`${this.apiOrigin}${this.basePath}/locations`,
|
||||
`${await this.discoveryApi.getBaseUrl('catalog')}/locations`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -135,7 +128,7 @@ export class CatalogClient implements CatalogApi {
|
||||
|
||||
async removeEntityByUid(uid: string): Promise<void> {
|
||||
const response = await fetch(
|
||||
`${this.apiOrigin}${this.basePath}/entities/by-uid/${uid}`,
|
||||
`${await this.discoveryApi.getBaseUrl('catalog')}/entities/by-uid/${uid}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
},
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
githubAuthApiRef,
|
||||
GithubAuth,
|
||||
OAuthRequestManager,
|
||||
UrlPatternDiscovery,
|
||||
} from '@backstage/core';
|
||||
import { gitOpsApiRef, GitOpsRestApi } from '../../api';
|
||||
|
||||
@@ -37,8 +38,9 @@ describe('ProfileCatalog', () => {
|
||||
[
|
||||
githubAuthApiRef,
|
||||
GithubAuth.create({
|
||||
backendUrl: 'http://localhost:7000',
|
||||
basePath: '/auth/',
|
||||
discoveryApi: UrlPatternDiscovery.compile(
|
||||
'http://example.com/{{pluginId}}',
|
||||
),
|
||||
oauthRequestApi,
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -20,20 +20,13 @@ import {
|
||||
RollbarProject,
|
||||
RollbarTopActiveItem,
|
||||
} from './types';
|
||||
import { DiscoveryApi } from '@backstage/core';
|
||||
|
||||
export class RollbarClient implements RollbarApi {
|
||||
private apiOrigin: string;
|
||||
private basePath: string;
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
|
||||
constructor({
|
||||
apiOrigin,
|
||||
basePath,
|
||||
}: {
|
||||
apiOrigin: string;
|
||||
basePath: string;
|
||||
}) {
|
||||
this.apiOrigin = apiOrigin;
|
||||
this.basePath = basePath;
|
||||
constructor(options: { discoveryApi: DiscoveryApi }) {
|
||||
this.discoveryApi = options.discoveryApi;
|
||||
}
|
||||
|
||||
async getAllProjects(): Promise<RollbarProject[]> {
|
||||
@@ -59,7 +52,7 @@ export class RollbarClient implements RollbarApi {
|
||||
}
|
||||
|
||||
private async get(path: string): Promise<any> {
|
||||
const url = `${this.apiOrigin}${this.basePath}${path}`;
|
||||
const url = `${await this.discoveryApi.getBaseUrl('rollbar')}${path}`;
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createApiRef } from '@backstage/core';
|
||||
import { createApiRef, DiscoveryApi } from '@backstage/core';
|
||||
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
|
||||
export const scaffolderApiRef = createApiRef<ScaffolderApi>({
|
||||
@@ -23,18 +23,10 @@ export const scaffolderApiRef = createApiRef<ScaffolderApi>({
|
||||
});
|
||||
|
||||
export class ScaffolderApi {
|
||||
private apiOrigin: string;
|
||||
private basePath: string;
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
|
||||
constructor({
|
||||
apiOrigin,
|
||||
basePath,
|
||||
}: {
|
||||
apiOrigin: string;
|
||||
basePath: string;
|
||||
}) {
|
||||
this.apiOrigin = apiOrigin;
|
||||
this.basePath = basePath;
|
||||
constructor(options: { discoveryApi: DiscoveryApi }) {
|
||||
this.discoveryApi = options.discoveryApi;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,7 +38,7 @@ export class ScaffolderApi {
|
||||
template: TemplateEntityV1alpha1,
|
||||
values: Record<string, any>,
|
||||
) {
|
||||
const url = `${this.apiOrigin}${this.basePath}/jobs`;
|
||||
const url = `${await this.discoveryApi.getBaseUrl('scaffolder')}/v1/jobs`;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -65,9 +57,9 @@ export class ScaffolderApi {
|
||||
}
|
||||
|
||||
async getJob(jobId: string) {
|
||||
const url = `${this.apiOrigin}${this.basePath}/job/${encodeURIComponent(
|
||||
jobId,
|
||||
)}`;
|
||||
const url = `${await this.discoveryApi.getBaseUrl(
|
||||
'scaffolder',
|
||||
)}/v1/job/${encodeURIComponent(jobId)}`;
|
||||
return fetch(url).then(x => x.json());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user