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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user