Add identity token to api requests
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
---
|
||||
'@backstage/catalog-client': minor
|
||||
'@backstage/plugin-auth-backend': minor
|
||||
'@backstage/plugin-catalog': minor
|
||||
'@backstage/plugin-catalog-import': minor
|
||||
'@backstage/plugin-fossa': minor
|
||||
'@backstage/plugin-kubernetes': minor
|
||||
'@backstage/plugin-rollbar': minor
|
||||
'@backstage/plugin-scaffolder-backend': minor
|
||||
---
|
||||
|
||||
Add identity token to api requests
|
||||
@@ -18,7 +18,7 @@ import { Entity } from '@backstage/catalog-model';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { CatalogClient } from './CatalogClient';
|
||||
import { CatalogListResponse, DiscoveryApi } from './types';
|
||||
import { CatalogListResponse, DiscoveryApi, IdentityApi } from './types';
|
||||
|
||||
const server = setupServer();
|
||||
const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base';
|
||||
@@ -27,6 +27,20 @@ const discoveryApi: DiscoveryApi = {
|
||||
return mockBaseUrl;
|
||||
},
|
||||
};
|
||||
const identityApi: IdentityApi = {
|
||||
getUserId() {
|
||||
return 'jane-fonda';
|
||||
},
|
||||
getProfile() {
|
||||
return { email: 'jane-fonda@spotify.com' };
|
||||
},
|
||||
async getIdToken() {
|
||||
return Promise.resolve('fake-id-token');
|
||||
},
|
||||
async signOut() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
|
||||
describe('CatalogClient', () => {
|
||||
let client: CatalogClient;
|
||||
@@ -36,7 +50,7 @@ describe('CatalogClient', () => {
|
||||
afterEach(() => server.resetHandlers());
|
||||
|
||||
beforeEach(() => {
|
||||
client = new CatalogClient({ discoveryApi });
|
||||
client = new CatalogClient({ discoveryApi, identityApi });
|
||||
});
|
||||
|
||||
describe('getEntities', () => {
|
||||
|
||||
@@ -28,13 +28,19 @@ import {
|
||||
CatalogEntitiesRequest,
|
||||
CatalogListResponse,
|
||||
DiscoveryApi,
|
||||
IdentityApi,
|
||||
} from './types';
|
||||
|
||||
export class CatalogClient implements CatalogApi {
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
private readonly identityApi: IdentityApi;
|
||||
|
||||
constructor(options: { discoveryApi: DiscoveryApi }) {
|
||||
constructor(options: {
|
||||
discoveryApi: DiscoveryApi;
|
||||
identityApi: IdentityApi;
|
||||
}) {
|
||||
this.discoveryApi = options.discoveryApi;
|
||||
this.identityApi = options.identityApi;
|
||||
}
|
||||
|
||||
async getLocationById(id: String): Promise<Location | undefined> {
|
||||
@@ -76,12 +82,14 @@ export class CatalogClient implements CatalogApi {
|
||||
target,
|
||||
dryRun,
|
||||
}: AddLocationRequest): Promise<AddLocationResponse> {
|
||||
const idToken = await this.identityApi.getIdToken();
|
||||
const response = await fetch(
|
||||
`${await this.discoveryApi.getBaseUrl('catalog')}/locations${
|
||||
dryRun ? '?dryRun=true' : ''
|
||||
}`,
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${idToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
@@ -119,9 +127,13 @@ export class CatalogClient implements CatalogApi {
|
||||
}
|
||||
|
||||
async removeEntityByUid(uid: string): Promise<void> {
|
||||
const idToken = await this.identityApi.getIdToken();
|
||||
const response = await fetch(
|
||||
`${await this.discoveryApi.getBaseUrl('catalog')}/entities/by-uid/${uid}`,
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${idToken}`,
|
||||
},
|
||||
method: 'DELETE',
|
||||
},
|
||||
);
|
||||
@@ -140,7 +152,12 @@ export class CatalogClient implements CatalogApi {
|
||||
|
||||
private async getRequired(path: string): Promise<any> {
|
||||
const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`;
|
||||
const response = await fetch(url);
|
||||
const idToken = await this.identityApi.getIdToken();
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
authorization: `Bearer ${idToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const payload = await response.text();
|
||||
@@ -153,7 +170,12 @@ export class CatalogClient implements CatalogApi {
|
||||
|
||||
private async getOptional(path: string): Promise<any | undefined> {
|
||||
const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`;
|
||||
const response = await fetch(url);
|
||||
const idToken = await this.identityApi.getIdToken();
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
authorization: `Bearer ${idToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
|
||||
@@ -53,3 +53,17 @@ export type AddLocationResponse = {
|
||||
export type DiscoveryApi = {
|
||||
getBaseUrl(pluginId: string): Promise<string>;
|
||||
};
|
||||
/**
|
||||
* This is a copy of the core IdentityApi, to avoid importing core.
|
||||
*/
|
||||
export type ProfileInfo = {
|
||||
email?: string;
|
||||
displayName?: string;
|
||||
picture?: string;
|
||||
};
|
||||
export type IdentityApi = {
|
||||
getUserId(): string;
|
||||
getProfile(): ProfileInfo;
|
||||
getIdToken(): Promise<string | undefined>;
|
||||
signOut(): Promise<void>;
|
||||
};
|
||||
|
||||
@@ -14,35 +14,109 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { UserEntity } from '@backstage/catalog-model';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { CatalogIdentityClient } from './CatalogIdentityClient';
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
|
||||
const server = setupServer();
|
||||
const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base';
|
||||
const discovery: PluginEndpointDiscovery = {
|
||||
async getBaseUrl(_pluginId) {
|
||||
return mockBaseUrl;
|
||||
},
|
||||
async getExternalBaseUrl(_pluginId) {
|
||||
return mockBaseUrl;
|
||||
},
|
||||
};
|
||||
|
||||
describe('CatalogIdentityClient', () => {
|
||||
const catalogApi: jest.Mocked<CatalogApi> = {
|
||||
getLocationById: jest.fn(),
|
||||
getEntityByName: jest.fn(),
|
||||
getEntities: jest.fn(),
|
||||
addLocation: jest.fn(),
|
||||
getLocationByEntity: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
};
|
||||
let client: CatalogIdentityClient;
|
||||
|
||||
afterEach(() => jest.resetAllMocks());
|
||||
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
|
||||
afterAll(() => server.close());
|
||||
afterEach(() => server.resetHandlers());
|
||||
|
||||
it('passes through the correct search params', async () => {
|
||||
catalogApi.getEntities.mockResolvedValueOnce({ items: [{} as UserEntity] });
|
||||
const client = new CatalogIdentityClient({
|
||||
catalogApi: catalogApi as CatalogApi,
|
||||
beforeEach(() => {
|
||||
client = new CatalogIdentityClient({ discovery });
|
||||
});
|
||||
|
||||
describe('findUser', () => {
|
||||
const defaultServiceResponse: UserEntity[] = [
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'User',
|
||||
metadata: {
|
||||
name: 'Test1',
|
||||
namespace: 'test1',
|
||||
annotations: {
|
||||
key: 'value',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
memberOf: ['group1'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
rest.get(`${mockBaseUrl}/entities`, (_, res, ctx) => {
|
||||
return res(ctx.json(defaultServiceResponse));
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
client.findUser({ annotations: { key: 'value' } });
|
||||
it('should entities from correct endpoint', async () => {
|
||||
const response = await client.findUser({ annotations: { key: 'value' } });
|
||||
expect(response).toEqual(defaultServiceResponse[0]);
|
||||
});
|
||||
|
||||
expect(catalogApi.getEntities).toBeCalledWith({
|
||||
filter: {
|
||||
kind: 'user',
|
||||
'metadata.annotations.key': 'value',
|
||||
},
|
||||
it('builds entity search filters properly', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
server.use(
|
||||
rest.get(`${mockBaseUrl}/entities`, (req, res, ctx) => {
|
||||
expect(req.url.search).toBe(
|
||||
'?filter=kind=user,metadata.annotations.key=value',
|
||||
);
|
||||
return res(ctx.json(defaultServiceResponse));
|
||||
}),
|
||||
);
|
||||
|
||||
const response = await client.findUser({ annotations: { key: 'value' } });
|
||||
|
||||
expect(response).toEqual(defaultServiceResponse[0]);
|
||||
});
|
||||
|
||||
it('omits authorization header if not available', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
server.use(
|
||||
rest.get(`${mockBaseUrl}/entities`, (req, res, ctx) => {
|
||||
expect(req.headers.has('authorization')).toBe(false);
|
||||
return res(ctx.json([]));
|
||||
}),
|
||||
);
|
||||
|
||||
client.findUser({ annotations: { key: 'value' } });
|
||||
});
|
||||
|
||||
it('adds authorization header if available', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
server.use(
|
||||
rest.get(`${mockBaseUrl}/entities`, (req, res, ctx) => {
|
||||
expect(req.headers.get('authorization')).toEqual('hello');
|
||||
return res(ctx.json([]));
|
||||
}),
|
||||
);
|
||||
|
||||
client.findUser(
|
||||
{ annotations: { key: 'value' } },
|
||||
{ headers: { authorization: 'hello' } },
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,10 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ConflictError, NotFoundError } from '@backstage/backend-common';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import fetch from 'cross-fetch';
|
||||
import {
|
||||
ConflictError,
|
||||
NotFoundError,
|
||||
PluginEndpointDiscovery,
|
||||
} from '@backstage/backend-common';
|
||||
import { UserEntity } from '@backstage/catalog-model';
|
||||
|
||||
type UserQuery = {
|
||||
annotations: Record<string, string>;
|
||||
};
|
||||
@@ -26,10 +29,10 @@ type UserQuery = {
|
||||
* A catalog client tailored for reading out identity data from the catalog.
|
||||
*/
|
||||
export class CatalogIdentityClient {
|
||||
private readonly catalogApi: CatalogApi;
|
||||
private readonly discovery: PluginEndpointDiscovery;
|
||||
|
||||
constructor(options: { catalogApi: CatalogApi }) {
|
||||
this.catalogApi = options.catalogApi;
|
||||
constructor(options: { discovery: PluginEndpointDiscovery }) {
|
||||
this.discovery = options.discovery;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,24 +40,54 @@ export class CatalogIdentityClient {
|
||||
*
|
||||
* Throws a NotFoundError or ConflictError if 0 or multiple users are found.
|
||||
*/
|
||||
async findUser(query: UserQuery): Promise<UserEntity> {
|
||||
async findUser(
|
||||
query: UserQuery,
|
||||
options?: { headers?: Record<string, string> },
|
||||
): Promise<UserEntity> {
|
||||
const filter: Record<string, string> = {
|
||||
kind: 'user',
|
||||
};
|
||||
for (const [key, value] of Object.entries(query.annotations)) {
|
||||
filter[`metadata.annotations.${key}`] = value;
|
||||
}
|
||||
const params: string[] = [];
|
||||
|
||||
const { items } = await this.catalogApi.getEntities({ filter });
|
||||
const filterParts: string[] = [];
|
||||
for (const [key, value] of Object.entries(filter)) {
|
||||
for (const v of [value].flat()) {
|
||||
filterParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`);
|
||||
}
|
||||
}
|
||||
if (filterParts.length) {
|
||||
params.push(`filter=${filterParts.join(',')}`);
|
||||
}
|
||||
const queryPart = params.length ? `?${params.join('&')}` : '';
|
||||
|
||||
if (items.length !== 1) {
|
||||
if (items.length > 1) {
|
||||
const url = `${await this.discovery.getBaseUrl(
|
||||
'catalog',
|
||||
)}/entities${queryPart}`;
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
...options?.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const payload = await response.text();
|
||||
const message = `Request failed with ${response.status} ${response.statusText}, ${payload}`;
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
const entities: UserEntity[] = await response.json();
|
||||
|
||||
if (entities.length !== 1) {
|
||||
if (entities.length > 1) {
|
||||
throw new ConflictError('User lookup resulted in multiple matches');
|
||||
} else {
|
||||
throw new NotFoundError('User not found');
|
||||
}
|
||||
}
|
||||
|
||||
return items[0] as UserEntity;
|
||||
return entities[0] as UserEntity;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
PassportDoneCallback,
|
||||
} from '../../lib/passport';
|
||||
import { AuthProviderFactory, RedirectInfo } from '../types';
|
||||
import { TokenIssuer } from '../../identity';
|
||||
|
||||
type PrivateInfo = {
|
||||
refreshToken: string;
|
||||
@@ -46,16 +47,19 @@ type PrivateInfo = {
|
||||
export type GoogleAuthProviderOptions = OAuthProviderOptions & {
|
||||
logger: Logger;
|
||||
identityClient: CatalogIdentityClient;
|
||||
tokenIssuer: TokenIssuer;
|
||||
};
|
||||
|
||||
export class GoogleAuthProvider implements OAuthHandlers {
|
||||
private readonly _strategy: GoogleStrategy;
|
||||
private readonly logger: Logger;
|
||||
private readonly identityClient: CatalogIdentityClient;
|
||||
private readonly tokenIssuer: TokenIssuer;
|
||||
|
||||
constructor(options: GoogleAuthProviderOptions) {
|
||||
this.logger = options.logger;
|
||||
this.identityClient = options.identityClient;
|
||||
this.tokenIssuer = options.tokenIssuer;
|
||||
// TODO: throw error if env variables not set?
|
||||
this._strategy = new GoogleStrategy(
|
||||
{
|
||||
@@ -150,11 +154,21 @@ export class GoogleAuthProvider implements OAuthHandlers {
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await this.identityClient.findUser({
|
||||
annotations: {
|
||||
'google.com/email': profile.email,
|
||||
},
|
||||
const token = await this.tokenIssuer.issueToken({
|
||||
claims: { sub: 'backstage.io/auth-backend' },
|
||||
});
|
||||
const user = await this.identityClient.findUser(
|
||||
{
|
||||
annotations: {
|
||||
'google.com/email': profile.email,
|
||||
},
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
...response,
|
||||
@@ -180,7 +194,7 @@ export const createGoogleProvider: AuthProviderFactory = ({
|
||||
config,
|
||||
logger,
|
||||
tokenIssuer,
|
||||
catalogApi,
|
||||
discovery,
|
||||
}) =>
|
||||
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
|
||||
const clientId = envConfig.getString('clientId');
|
||||
@@ -192,7 +206,8 @@ export const createGoogleProvider: AuthProviderFactory = ({
|
||||
clientSecret,
|
||||
callbackUrl,
|
||||
logger,
|
||||
identityClient: new CatalogIdentityClient({ catalogApi }),
|
||||
tokenIssuer,
|
||||
identityClient: new CatalogIdentityClient({ discovery }),
|
||||
});
|
||||
|
||||
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { Config } from '@backstage/config';
|
||||
import express from 'express';
|
||||
import { Logger } from 'winston';
|
||||
@@ -132,7 +131,6 @@ export type AuthProviderFactoryOptions = {
|
||||
logger: Logger;
|
||||
tokenIssuer: TokenIssuer;
|
||||
discovery: PluginEndpointDiscovery;
|
||||
catalogApi: CatalogApi;
|
||||
identityResolver?: ExperimentalIdentityResolver;
|
||||
};
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
PluginDatabaseManager,
|
||||
PluginEndpointDiscovery,
|
||||
} from '@backstage/backend-common';
|
||||
import { CatalogClient } from '@backstage/catalog-client';
|
||||
import { Config } from '@backstage/config';
|
||||
import { createOidcRouter, DatabaseKeyStore, TokenFactory } from '../identity';
|
||||
import session from 'express-session';
|
||||
@@ -66,7 +65,6 @@ export async function createRouter({
|
||||
keyDurationSeconds,
|
||||
logger: logger.child({ component: 'token-factory' }),
|
||||
});
|
||||
const catalogApi = new CatalogClient({ discoveryApi: discovery });
|
||||
|
||||
const secret = config.getOptionalString('auth.session.secret');
|
||||
if (secret) {
|
||||
@@ -103,7 +101,6 @@ export async function createRouter({
|
||||
logger,
|
||||
tokenIssuer,
|
||||
discovery,
|
||||
catalogApi,
|
||||
});
|
||||
|
||||
const r = Router();
|
||||
|
||||
@@ -15,23 +15,31 @@
|
||||
*/
|
||||
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { DiscoveryApi, OAuthApi, ConfigApi } from '@backstage/core';
|
||||
import {
|
||||
DiscoveryApi,
|
||||
IdentityApi,
|
||||
OAuthApi,
|
||||
ConfigApi,
|
||||
} from '@backstage/core';
|
||||
import { CatalogImportApi } from './CatalogImportApi';
|
||||
import { PartialEntity } from '../util/types';
|
||||
import { GitHubIntegrationConfig } from '@backstage/integration';
|
||||
|
||||
export class CatalogImportClient implements CatalogImportApi {
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
private readonly identityApi: IdentityApi;
|
||||
private readonly githubAuthApi: OAuthApi;
|
||||
private readonly configApi: ConfigApi;
|
||||
|
||||
constructor(options: {
|
||||
discoveryApi: DiscoveryApi;
|
||||
githubAuthApi: OAuthApi;
|
||||
identityApi: IdentityApi;
|
||||
configApi: ConfigApi;
|
||||
}) {
|
||||
this.discoveryApi = options.discoveryApi;
|
||||
this.githubAuthApi = options.githubAuthApi;
|
||||
this.identityApi = options.identityApi;
|
||||
this.configApi = options.configApi;
|
||||
}
|
||||
|
||||
@@ -40,10 +48,12 @@ export class CatalogImportClient implements CatalogImportApi {
|
||||
}: {
|
||||
repo: string;
|
||||
}): Promise<PartialEntity[]> {
|
||||
const idToken = await this.identityApi.getIdToken();
|
||||
const response = await fetch(
|
||||
`${await this.discoveryApi.getBaseUrl('catalog')}/analyze-location`,
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${idToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
@@ -69,10 +79,12 @@ export class CatalogImportClient implements CatalogImportApi {
|
||||
}: {
|
||||
location: string;
|
||||
}): Promise<void> {
|
||||
const idToken = await this.identityApi.getIdToken();
|
||||
const response = await fetch(
|
||||
`${await this.discoveryApi.getBaseUrl('catalog')}/locations`,
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${idToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
createRouteRef,
|
||||
discoveryApiRef,
|
||||
githubAuthApiRef,
|
||||
identityApiRef,
|
||||
configApiRef,
|
||||
createRoutableExtension,
|
||||
} from '@backstage/core';
|
||||
@@ -39,10 +40,16 @@ export const catalogImportPlugin = createPlugin({
|
||||
deps: {
|
||||
discoveryApi: discoveryApiRef,
|
||||
githubAuthApi: githubAuthApiRef,
|
||||
identityApi: identityApiRef,
|
||||
configApi: configApiRef,
|
||||
},
|
||||
factory: ({ discoveryApi, githubAuthApi, configApi }) =>
|
||||
new CatalogImportClient({ discoveryApi, githubAuthApi, configApi }),
|
||||
factory: ({ discoveryApi, githubAuthApi, identityApi, configApi }) =>
|
||||
new CatalogImportClient({
|
||||
discoveryApi,
|
||||
githubAuthApi,
|
||||
identityApi,
|
||||
configApi,
|
||||
}),
|
||||
}),
|
||||
],
|
||||
routes: {
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
createPlugin,
|
||||
discoveryApiRef,
|
||||
createRoutableExtension,
|
||||
identityApiRef,
|
||||
} from '@backstage/core';
|
||||
import {
|
||||
catalogApiRef,
|
||||
@@ -32,8 +33,9 @@ export const catalogPlugin = createPlugin({
|
||||
apis: [
|
||||
createApiFactory({
|
||||
api: catalogApiRef,
|
||||
deps: { discoveryApi: discoveryApiRef },
|
||||
factory: ({ discoveryApi }) => new CatalogClient({ discoveryApi }),
|
||||
deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef },
|
||||
factory: ({ discoveryApi, identityApi }) =>
|
||||
new CatalogClient({ discoveryApi, identityApi }),
|
||||
}),
|
||||
],
|
||||
routes: {
|
||||
|
||||
@@ -14,28 +14,38 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DiscoveryApi } from '@backstage/core';
|
||||
import { DiscoveryApi, IdentityApi } from '@backstage/core';
|
||||
import fetch from 'cross-fetch';
|
||||
import { FindingSummary, FossaApi } from './FossaApi';
|
||||
|
||||
export class FossaClient implements FossaApi {
|
||||
discoveryApi: DiscoveryApi;
|
||||
identityApi?: IdentityApi;
|
||||
organizationId?: string;
|
||||
|
||||
constructor({
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
organizationId,
|
||||
}: {
|
||||
discoveryApi: DiscoveryApi;
|
||||
identityApi?: IdentityApi;
|
||||
organizationId?: string;
|
||||
}) {
|
||||
this.discoveryApi = discoveryApi;
|
||||
this.identityApi = identityApi;
|
||||
this.organizationId = organizationId;
|
||||
}
|
||||
|
||||
private async callApi(path: string): Promise<any> {
|
||||
const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/fossa`;
|
||||
const response = await fetch(`${apiUrl}/${path}`);
|
||||
const headers: Record<string, string> = {};
|
||||
if (this.identityApi) {
|
||||
headers.authorization = `Bearer ${this.identityApi.getIdToken()}`;
|
||||
}
|
||||
const response = await fetch(`${apiUrl}/${path}`, {
|
||||
headers,
|
||||
});
|
||||
if (response.status === 200) {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
createApiFactory,
|
||||
createPlugin,
|
||||
discoveryApiRef,
|
||||
identityApiRef,
|
||||
} from '@backstage/core';
|
||||
import { fossaApiRef, FossaClient } from './api';
|
||||
|
||||
@@ -27,10 +28,15 @@ export const fossaPlugin = createPlugin({
|
||||
apis: [
|
||||
createApiFactory({
|
||||
api: fossaApiRef,
|
||||
deps: { configApi: configApiRef, discoveryApi: discoveryApiRef },
|
||||
factory: ({ configApi, discoveryApi }) =>
|
||||
deps: {
|
||||
configApi: configApiRef,
|
||||
discoveryApi: discoveryApiRef,
|
||||
identityApi: identityApiRef,
|
||||
},
|
||||
factory: ({ configApi, discoveryApi, identityApi }) =>
|
||||
new FossaClient({
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
organizationId: configApi.getOptionalString('fossa.organizationId'),
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DiscoveryApi } from '@backstage/core';
|
||||
import { DiscoveryApi, IdentityApi } from '@backstage/core';
|
||||
import { KubernetesApi } from './types';
|
||||
import {
|
||||
KubernetesRequestBody,
|
||||
@@ -23,9 +23,14 @@ import {
|
||||
|
||||
export class KubernetesBackendClient implements KubernetesApi {
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
private readonly identityApi: IdentityApi;
|
||||
|
||||
constructor(options: { discoveryApi: DiscoveryApi }) {
|
||||
constructor(options: {
|
||||
discoveryApi: DiscoveryApi;
|
||||
identityApi: IdentityApi;
|
||||
}) {
|
||||
this.discoveryApi = options.discoveryApi;
|
||||
this.identityApi = options.identityApi;
|
||||
}
|
||||
|
||||
private async getRequired(
|
||||
@@ -33,9 +38,11 @@ export class KubernetesBackendClient implements KubernetesApi {
|
||||
requestBody: KubernetesRequestBody,
|
||||
): Promise<any> {
|
||||
const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}${path}`;
|
||||
const idToken = await this.identityApi.getIdToken();
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
authorization: `Bearer ${idToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
createPlugin,
|
||||
createRouteRef,
|
||||
discoveryApiRef,
|
||||
identityApiRef,
|
||||
googleAuthApiRef,
|
||||
} from '@backstage/core';
|
||||
import { KubernetesBackendClient } from './api/KubernetesBackendClient';
|
||||
@@ -35,9 +36,9 @@ export const plugin = createPlugin({
|
||||
apis: [
|
||||
createApiFactory({
|
||||
api: kubernetesApiRef,
|
||||
deps: { discoveryApi: discoveryApiRef },
|
||||
factory: ({ discoveryApi }) =>
|
||||
new KubernetesBackendClient({ discoveryApi }),
|
||||
deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef },
|
||||
factory: ({ discoveryApi, identityApi }) =>
|
||||
new KubernetesBackendClient({ discoveryApi, identityApi }),
|
||||
}),
|
||||
createApiFactory({
|
||||
api: kubernetesAuthProvidersApiRef,
|
||||
|
||||
@@ -20,13 +20,18 @@ import {
|
||||
RollbarProject,
|
||||
RollbarTopActiveItem,
|
||||
} from './types';
|
||||
import { DiscoveryApi } from '@backstage/core';
|
||||
import { DiscoveryApi, IdentityApi } from '@backstage/core';
|
||||
|
||||
export class RollbarClient implements RollbarApi {
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
private readonly identityApi: IdentityApi;
|
||||
|
||||
constructor(options: { discoveryApi: DiscoveryApi }) {
|
||||
constructor(options: {
|
||||
discoveryApi: DiscoveryApi;
|
||||
identityApi: IdentityApi;
|
||||
}) {
|
||||
this.discoveryApi = options.discoveryApi;
|
||||
this.identityApi = options.identityApi;
|
||||
}
|
||||
|
||||
async getAllProjects(): Promise<RollbarProject[]> {
|
||||
@@ -53,7 +58,12 @@ export class RollbarClient implements RollbarApi {
|
||||
|
||||
private async get(path: string): Promise<any> {
|
||||
const url = `${await this.discoveryApi.getBaseUrl('rollbar')}${path}`;
|
||||
const response = await fetch(url);
|
||||
const idToken = await this.identityApi.getIdToken();
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
authorization: `Bearer ${idToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const payload = await response.text();
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
createPlugin,
|
||||
createApiFactory,
|
||||
discoveryApiRef,
|
||||
identityApiRef,
|
||||
} from '@backstage/core';
|
||||
import { rootRouteRef, entityRouteRef } from './routes';
|
||||
import { RollbarHome } from './components/RollbarHome/RollbarHome';
|
||||
@@ -30,8 +31,9 @@ export const plugin = createPlugin({
|
||||
apis: [
|
||||
createApiFactory({
|
||||
api: rollbarApiRef,
|
||||
deps: { discoveryApi: discoveryApiRef },
|
||||
factory: ({ discoveryApi }) => new RollbarClient({ discoveryApi }),
|
||||
deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef },
|
||||
factory: ({ discoveryApi, identityApi }) =>
|
||||
new RollbarClient({ discoveryApi, identityApi }),
|
||||
}),
|
||||
],
|
||||
register({ router }) {
|
||||
|
||||
@@ -37,7 +37,10 @@ export class CatalogEntityClient {
|
||||
*
|
||||
* Throws a NotFoundError or ConflictError if 0 or multiple templates are found.
|
||||
*/
|
||||
async findTemplate(templateName: string): Promise<TemplateEntityV1alpha1> {
|
||||
async findTemplate(
|
||||
templateName: string,
|
||||
options?: { headers?: Record<string, string> },
|
||||
): Promise<TemplateEntityV1alpha1> {
|
||||
const conditions = [
|
||||
'kind=template',
|
||||
`metadata.name=${encodeURIComponent(templateName)}`,
|
||||
@@ -46,6 +49,11 @@ export class CatalogEntityClient {
|
||||
const baseUrl = await this.discovery.getBaseUrl('catalog');
|
||||
const response = await fetch(
|
||||
`${baseUrl}/entities?filter=${conditions.join(',')}`,
|
||||
{
|
||||
headers: {
|
||||
...options?.headers,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -97,7 +97,12 @@ export async function createRouter(
|
||||
},
|
||||
};
|
||||
|
||||
const template = await entityClient.findTemplate(templateName);
|
||||
// Forward authorization header from client
|
||||
const template = await entityClient.findTemplate(templateName, {
|
||||
headers: req.headers.authorization
|
||||
? { authorization: req.headers.authorization }
|
||||
: {},
|
||||
});
|
||||
|
||||
const validationResult: ValidatorResult = validate(
|
||||
values,
|
||||
|
||||
Reference in New Issue
Block a user