diff --git a/.changeset/rare-dryers-check.md b/.changeset/rare-dryers-check.md new file mode 100644 index 0000000000..584481f54d --- /dev/null +++ b/.changeset/rare-dryers-check.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-backend': minor +--- + +**BREAKING**: The `KubernetesBuilder.createBuilder` method now requires the `discovery` service to be forwarded from the plugin environment. This is part of the migration to support new auth services. diff --git a/packages/backend/src/plugins/kubernetes.ts b/packages/backend/src/plugins/kubernetes.ts index 3bc6648862..5581083879 100644 --- a/packages/backend/src/plugins/kubernetes.ts +++ b/packages/backend/src/plugins/kubernetes.ts @@ -28,6 +28,7 @@ export default async function createPlugin( config: env.config, catalogApi, permissions: env.permissions, + discovery: env.discovery, }).build(); return router; } diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index afdb3bcf9a..60331ca044 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -5,12 +5,15 @@ ```ts import { AuthenticationStrategy as AuthenticationStrategy_2 } from '@backstage/plugin-kubernetes-node'; import { AuthMetadata as AuthMetadata_2 } from '@backstage/plugin-kubernetes-node'; +import { AuthService } from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; import { ClusterDetails as ClusterDetails_2 } from '@backstage/plugin-kubernetes-node'; import { Config } from '@backstage/config'; import { CustomResource as CustomResource_2 } from '@backstage/plugin-kubernetes-node'; +import { DiscoveryService } from '@backstage/backend-plugin-api'; import { Duration } from 'luxon'; import express from 'express'; +import { HttpAuthService } from '@backstage/backend-plugin-api'; import * as k8sAuthTypes from '@backstage/plugin-kubernetes-node'; import { KubernetesClustersSupplier as KubernetesClustersSupplier_2 } from '@backstage/plugin-kubernetes-node'; import { KubernetesCredential as KubernetesCredential_2 } from '@backstage/plugin-kubernetes-node'; @@ -190,6 +193,8 @@ export class KubernetesBuilder { catalogApi: CatalogApi, proxy: KubernetesProxy, permissionApi: PermissionEvaluator, + authService: AuthService, + httpAuth: HttpAuthService, ): express.Router; // (undocumented) protected buildServiceLocator( @@ -272,11 +277,17 @@ export type KubernetesCredential = k8sAuthTypes.KubernetesCredential; // @public (undocumented) export interface KubernetesEnvironment { + // (undocumented) + auth?: AuthService; // (undocumented) catalogApi: CatalogApi; // (undocumented) config: Config; // (undocumented) + discovery: DiscoveryService; + // (undocumented) + httpAuth?: HttpAuthService; + // (undocumented) logger: Logger; // (undocumented) permissions: PermissionEvaluator; diff --git a/plugins/kubernetes-backend/src/plugin.ts b/plugins/kubernetes-backend/src/plugin.ts index 74a8a68e53..d3aacd5190 100644 --- a/plugins/kubernetes-backend/src/plugin.ts +++ b/plugins/kubernetes-backend/src/plugin.ts @@ -178,10 +178,22 @@ export const kubernetesPlugin = createBackendPlugin({ http: coreServices.httpRouter, logger: coreServices.logger, config: coreServices.rootConfig, + discovery: coreServices.discovery, catalogApi: catalogServiceRef, permissions: coreServices.permissions, + auth: coreServices.auth, + httpAuth: coreServices.httpAuth, }, - async init({ http, logger, config, catalogApi, permissions }) { + async init({ + http, + logger, + config, + discovery, + catalogApi, + permissions, + auth, + httpAuth, + }) { const winstonLogger = loggerToWinstonLogger(logger); // TODO: expose all of the customization & extension points of the builder here const builder: KubernetesBuilder = KubernetesBuilder.createBuilder({ @@ -189,6 +201,9 @@ export const kubernetesPlugin = createBackendPlugin({ config, catalogApi, permissions, + discovery, + auth, + httpAuth, }) .setObjectsProvider(extPointObjectsProvider.getObjectsProvider()) .setClusterSupplier(extPointClusterSuplier.getClusterSupplier()) diff --git a/plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts b/plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts index d3dcc83bb0..9195551993 100644 --- a/plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts +++ b/plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts @@ -15,7 +15,11 @@ */ import request from 'supertest'; -import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { + mockCredentials, + mockServices, + startTestBackend, +} from '@backstage/backend-test-utils'; import { ExtendedHttpServer } from '@backstage/backend-app-api'; import { kubernetesObjectsProviderExtensionPoint } from '@backstage/plugin-kubernetes-node'; import { createBackendModule } from '@backstage/backend-plugin-api'; @@ -102,12 +106,6 @@ describe('resourcesRoutes', () => { }, ], }, - backend: { - auth: { - // TODO: Remove once migrated to support new auth services - dangerouslyDisableDefaultAuthPolicy: true, - }, - }, }, }), import('@backstage/plugin-kubernetes-backend/alpha'), @@ -141,7 +139,6 @@ describe('resourcesRoutes', () => { }, }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(200, { items: [ { @@ -168,7 +165,6 @@ describe('resourcesRoutes', () => { }, }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(400, { error: { name: 'InputError', message: 'entity is a required field' }, request: { @@ -189,7 +185,6 @@ describe('resourcesRoutes', () => { }, }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(400, { error: { name: 'InputError', @@ -214,7 +209,6 @@ describe('resourcesRoutes', () => { }, }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(400, { error: { name: 'InputError', @@ -231,6 +225,7 @@ describe('resourcesRoutes', () => { it('401 when no Auth header', async () => { await request(app) .post('/api/kubernetes/resources/workloads/query') + .set('authorization', mockCredentials.none.header()) .send({ entityRef: 'component:someComponent', auth: { @@ -239,7 +234,10 @@ describe('resourcesRoutes', () => { }) .set('Content-Type', 'application/json') .expect(401, { - error: { name: 'AuthenticationError', message: 'No Backstage token' }, + error: { + name: 'AuthenticationError', + message: '', + }, request: { method: 'POST', url: '/api/kubernetes/resources/workloads/query', @@ -258,9 +256,12 @@ describe('resourcesRoutes', () => { }, }) .set('Content-Type', 'application/json') - .set('Authorization', 'ffffff') + .set('Authorization', mockCredentials.user.invalidHeader()) .expect(401, { - error: { name: 'AuthenticationError', message: 'No Backstage token' }, + error: { + name: 'AuthenticationError', + message: 'User token is invalid', + }, request: { method: 'POST', url: '/api/kubernetes/resources/workloads/query', @@ -279,7 +280,6 @@ describe('resourcesRoutes', () => { }, }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(500, { error: { name: 'Error', @@ -312,7 +312,6 @@ describe('resourcesRoutes', () => { ], }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(200, { items: [ { @@ -340,7 +339,6 @@ describe('resourcesRoutes', () => { }, }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(400, { error: { name: 'InputError', @@ -365,7 +363,6 @@ describe('resourcesRoutes', () => { customResources: 'somestring', }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(400, { error: { name: 'InputError', @@ -390,7 +387,6 @@ describe('resourcesRoutes', () => { customResources: [], }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(400, { error: { name: 'InputError', @@ -420,7 +416,6 @@ describe('resourcesRoutes', () => { ], }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(400, { error: { name: 'InputError', message: 'entity is a required field' }, request: { @@ -448,7 +443,6 @@ describe('resourcesRoutes', () => { ], }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(400, { error: { name: 'InputError', @@ -480,7 +474,6 @@ describe('resourcesRoutes', () => { ], }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(400, { error: { name: 'InputError', @@ -497,6 +490,7 @@ describe('resourcesRoutes', () => { it('401 when no Auth header', async () => { await request(app) .post('/api/kubernetes/resources/custom/query') + .set('authorization', mockCredentials.none.header()) .send({ entityRef: 'component:someComponent', auth: { @@ -512,7 +506,10 @@ describe('resourcesRoutes', () => { }) .set('Content-Type', 'application/json') .expect(401, { - error: { name: 'AuthenticationError', message: 'No Backstage token' }, + error: { + name: 'AuthenticationError', + message: '', + }, request: { method: 'POST', url: '/api/kubernetes/resources/custom/query', @@ -538,9 +535,12 @@ describe('resourcesRoutes', () => { ], }) .set('Content-Type', 'application/json') - .set('Authorization', 'ffffff') + .set('Authorization', mockCredentials.user.invalidHeader()) .expect(401, { - error: { name: 'AuthenticationError', message: 'No Backstage token' }, + error: { + name: 'AuthenticationError', + message: 'User token is invalid', + }, request: { method: 'POST', url: '/api/kubernetes/resources/custom/query', @@ -566,7 +566,6 @@ describe('resourcesRoutes', () => { ], }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(500, { error: { name: 'Error', diff --git a/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts b/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts index 0468799908..6e05f69d04 100644 --- a/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts +++ b/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts @@ -19,15 +19,17 @@ import { stringifyEntityRef, } from '@backstage/catalog-model'; import { CatalogApi } from '@backstage/catalog-client'; -import { InputError, AuthenticationError } from '@backstage/errors'; +import { InputError } from '@backstage/errors'; import express, { Request } from 'express'; import { KubernetesObjectsProvider } from '@backstage/plugin-kubernetes-node'; -import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; +import { AuthService, HttpAuthService } from '@backstage/backend-plugin-api'; export const addResourceRoutesToRouter = ( router: express.Router, catalogApi: CatalogApi, objectsProvider: KubernetesObjectsProvider, + auth: AuthService, + httpAuth: HttpAuthService, ) => { const getEntityByReq = async (req: Request) => { const rawEntityRef = req.body.entityRef; @@ -44,23 +46,18 @@ export const addResourceRoutesToRouter = ( throw new InputError(`Invalid entity ref, ${error}`); } - const token = getBearerTokenFromAuthorizationHeader( - req.headers.authorization, - ); - - if (!token) { - throw new AuthenticationError('No Backstage token'); - } - - const entity = await catalogApi.getEntityByRef(entityRef, { - token: token, + const { token } = await auth.getPluginRequestToken({ + onBehalfOf: await httpAuth.credentials(req), + targetPluginId: 'catalog', }); + const entity = await catalogApi.getEntityByRef(entityRef, { token }); if (!entity) { throw new InputError( `Entity ref missing, ${stringifyEntityRef(entityRef)}`, ); } + return entity; }; diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts index 295d11570f..1ac653bb58 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts @@ -36,6 +36,7 @@ import { import { setupServer } from 'msw/node'; import { ServiceMock, + mockCredentials, mockServices, setupRequestMockHandlers, startTestBackend, @@ -757,9 +758,9 @@ metadata: }); it('serves permission integration endpoint', async () => { - const response = await request(app).get( - '/api/kubernetes/.well-known/backstage/permissions/metadata', - ); + const response = await request(app) + .get('/api/kubernetes/.well-known/backstage/permissions/metadata') + .set('authorization', mockCredentials.service.header()); expect(response.status).toEqual(200); expect(response.body).toMatchObject({ diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index fec4bc5cc0..c63d039ce9 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -64,6 +64,12 @@ import { } from './KubernetesFanOutHandler'; import { KubernetesClientBasedFetcher } from './KubernetesFetcher'; import { KubernetesProxy } from './KubernetesProxy'; +import { createLegacyAuthAdapters } from '@backstage/backend-common'; +import { + AuthService, + DiscoveryService, + HttpAuthService, +} from '@backstage/backend-plugin-api'; /** * @@ -73,7 +79,10 @@ export interface KubernetesEnvironment { logger: Logger; config: Config; catalogApi: CatalogApi; + discovery: DiscoveryService; permissions: PermissionEvaluator; + auth?: AuthService; + httpAuth?: HttpAuthService; } /** @@ -131,6 +140,13 @@ export class KubernetesBuilder { router: Router(), } as unknown as KubernetesBuilderReturn; } + + const { auth, httpAuth } = createLegacyAuthAdapters({ + auth: this.env.auth, + httpAuth: this.env.httpAuth, + discovery: this.env.discovery, + }); + const customResources = this.buildCustomResources(); const fetcher = this.getFetcher(); @@ -158,6 +174,8 @@ export class KubernetesBuilder { this.env.catalogApi, proxy, permissions, + auth, + httpAuth, ); return { @@ -337,6 +355,8 @@ export class KubernetesBuilder { catalogApi: CatalogApi, proxy: KubernetesProxy, permissionApi: PermissionEvaluator, + authService: AuthService, + httpAuth: HttpAuthService, ): express.Router { const logger = this.env.logger; const router = Router(); @@ -391,7 +411,13 @@ export class KubernetesBuilder { }); }); - addResourceRoutesToRouter(router, catalogApi, objectsProvider); + addResourceRoutesToRouter( + router, + catalogApi, + objectsProvider, + authService, + httpAuth, + ); return router; }