kubernetes-backend: migrate to support new auth services

Co-authored-by: Fredrik Adelöw <freben@gmail.com>
Co-authored-by: Carl-Erik Bergström <cbergstrom@spotify.com>
Co-authored-by: blam <ben@blam.sh>
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-02-15 18:40:18 +01:00
parent 58b5e450dd
commit e1e540cd1d
8 changed files with 98 additions and 43 deletions
+5
View File
@@ -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.
@@ -28,6 +28,7 @@ export default async function createPlugin(
config: env.config,
catalogApi,
permissions: env.permissions,
discovery: env.discovery,
}).build();
return router;
}
+11
View File
@@ -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;
+16 -1
View File
@@ -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())
@@ -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',
@@ -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<any>) => {
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;
};
@@ -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({
@@ -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;
}