diff --git a/.changeset/bright-rules-know.md b/.changeset/bright-rules-know.md new file mode 100644 index 0000000000..bc837a0bae --- /dev/null +++ b/.changeset/bright-rules-know.md @@ -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 diff --git a/packages/catalog-client/src/CatalogClient.test.ts b/packages/catalog-client/src/CatalogClient.test.ts index 6369f95b76..9ee4fe3165 100644 --- a/packages/catalog-client/src/CatalogClient.test.ts +++ b/packages/catalog-client/src/CatalogClient.test.ts @@ -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', () => { diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index 362b1e71da..c559cb1210 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -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 { @@ -76,12 +82,14 @@ export class CatalogClient implements CatalogApi { target, dryRun, }: AddLocationRequest): Promise { + 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 { + 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 { 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 { 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) { diff --git a/packages/catalog-client/src/types.ts b/packages/catalog-client/src/types.ts index e72317d444..0742dfed53 100644 --- a/packages/catalog-client/src/types.ts +++ b/packages/catalog-client/src/types.ts @@ -53,3 +53,17 @@ export type AddLocationResponse = { export type DiscoveryApi = { getBaseUrl(pluginId: string): Promise; }; +/** + * 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; + signOut(): Promise; +}; diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts index ec6aa1b6ba..0594785760 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts @@ -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 = { - 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' } }, + ); }); }); }); diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts index 5bc4e5de21..b746d59cad 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts @@ -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; }; @@ -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 { + async findUser( + query: UserQuery, + options?: { headers?: Record }, + ): Promise { const filter: Record = { 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; } } diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index ae4f5fabd0..10f3ae96f2 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -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, { diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 1a4e7b118a..8339258654 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -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; }; diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 85d60661e9..742a269b73 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -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(); diff --git a/plugins/catalog-import/src/api/CatalogImportClient.ts b/plugins/catalog-import/src/api/CatalogImportClient.ts index d1509c2105..9b52de0ba4 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.ts @@ -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 { + 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 { + 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', diff --git a/plugins/catalog-import/src/plugin.ts b/plugins/catalog-import/src/plugin.ts index 42210d0ed1..60e5128df0 100644 --- a/plugins/catalog-import/src/plugin.ts +++ b/plugins/catalog-import/src/plugin.ts @@ -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: { diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index c19925775d..557762ee46 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -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: { diff --git a/plugins/fossa/src/api/FossaClient.ts b/plugins/fossa/src/api/FossaClient.ts index 05a889177a..6730f7ddb3 100644 --- a/plugins/fossa/src/api/FossaClient.ts +++ b/plugins/fossa/src/api/FossaClient.ts @@ -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 { const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/fossa`; - const response = await fetch(`${apiUrl}/${path}`); + const headers: Record = {}; + if (this.identityApi) { + headers.authorization = `Bearer ${this.identityApi.getIdToken()}`; + } + const response = await fetch(`${apiUrl}/${path}`, { + headers, + }); if (response.status === 200) { return await response.json(); } diff --git a/plugins/fossa/src/plugin.ts b/plugins/fossa/src/plugin.ts index cb1de3911f..638be4a0bf 100644 --- a/plugins/fossa/src/plugin.ts +++ b/plugins/fossa/src/plugin.ts @@ -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'), }), }), diff --git a/plugins/kubernetes/src/api/KubernetesBackendClient.ts b/plugins/kubernetes/src/api/KubernetesBackendClient.ts index 287b411259..e005b29517 100644 --- a/plugins/kubernetes/src/api/KubernetesBackendClient.ts +++ b/plugins/kubernetes/src/api/KubernetesBackendClient.ts @@ -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 { 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), diff --git a/plugins/kubernetes/src/plugin.ts b/plugins/kubernetes/src/plugin.ts index cff9e1468b..04ab7a086f 100644 --- a/plugins/kubernetes/src/plugin.ts +++ b/plugins/kubernetes/src/plugin.ts @@ -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, diff --git a/plugins/rollbar/src/api/RollbarClient.ts b/plugins/rollbar/src/api/RollbarClient.ts index e612890655..1041accf19 100644 --- a/plugins/rollbar/src/api/RollbarClient.ts +++ b/plugins/rollbar/src/api/RollbarClient.ts @@ -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 { @@ -53,7 +58,12 @@ export class RollbarClient implements RollbarApi { private async get(path: string): Promise { 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(); diff --git a/plugins/rollbar/src/plugin.ts b/plugins/rollbar/src/plugin.ts index c3ca6173ff..9456c10c6b 100644 --- a/plugins/rollbar/src/plugin.ts +++ b/plugins/rollbar/src/plugin.ts @@ -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 }) { diff --git a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts index 4541f03a4a..c5e20202c5 100644 --- a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts +++ b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts @@ -37,7 +37,10 @@ export class CatalogEntityClient { * * Throws a NotFoundError or ConflictError if 0 or multiple templates are found. */ - async findTemplate(templateName: string): Promise { + async findTemplate( + templateName: string, + options?: { headers?: Record }, + ): Promise { 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) { diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 9098e5d05e..070b7cffc7 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -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,