enable custom auth metadata in config

Signed-off-by: Jamie Klassen <jamie.klassen@broadcom.com>
This commit is contained in:
Jamie Klassen
2024-01-12 12:40:40 -05:00
parent 762809def5
commit 7f6ff255f1
5 changed files with 185 additions and 50 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-kubernetes-backend': patch
---
Custom per-cluster auth metadata (mainly for use with custom `AuthenticationStrategy` implementations) can now be specified in the `authMetadata` property of clusters in the app-config.
+7 -4
View File
@@ -46,13 +46,16 @@ export interface Config {
/** @visibility secret */
serviceAccountToken?: string;
/** @visibility frontend */
authProvider:
authProvider?:
| 'aks'
| 'aws'
| 'google'
| 'serviceAccount'
| 'azure'
| 'google'
| 'googleServiceAccount'
| 'oidc'
| 'googleServiceAccount';
| 'serviceAccount';
/** @visibility secret */
authMetadata?: object;
/** @visibility frontend */
oidcTokenProvider?: string;
/** @visibility frontend */
@@ -177,6 +177,67 @@ describe('ConfigClusterLocator', () => {
]);
});
it('reads custom authMetadata', async () => {
const config: Config = new ConfigReader({
clusters: [
{
name: 'cluster',
url: 'http://url',
authProvider: 'authProvider',
authMetadata: { 'custom-key': 'custom-value' },
},
],
});
const result = await ConfigClusterLocator.fromConfig(
config,
authStrategy,
).getClusters();
expect(result).toMatchObject([
{
authMetadata: expect.objectContaining({ 'custom-key': 'custom-value' }),
},
]);
});
it('reads authProvider from metadata block', async () => {
const config: Config = new ConfigReader({
clusters: [
{
name: 'cluster',
url: 'http://url',
authMetadata: {
[ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'serviceAccount',
},
},
],
});
const result = await ConfigClusterLocator.fromConfig(
config,
authStrategy,
).getClusters();
expect(result).toMatchObject([
{
authMetadata: {
[ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'serviceAccount',
},
},
]);
});
it('forbids cluster without auth provider', () => {
const config: Config = new ConfigReader({
clusters: [{ name: 'cluster', url: 'http://url' }],
});
expect(() => ConfigClusterLocator.fromConfig(config, authStrategy)).toThrow(
'Missing required config value',
);
});
it('one cluster with dashboardParameters', async () => {
const config: Config = new ConfigReader({
clusters: [
@@ -37,7 +37,12 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier {
): ConfigClusterLocator {
return new ConfigClusterLocator(
config.getConfigArray('clusters').map(c => {
const authProvider = c.getString('authProvider');
const authMetadataBlock = c.getOptional<{
[ANNOTATION_KUBERNETES_AUTH_PROVIDER]?: string;
}>('authMetadata');
const authProvider =
authMetadataBlock?.[ANNOTATION_KUBERNETES_AUTH_PROVIDER] ??
c.getString('authProvider');
const clusterDetails: ClusterDetails = {
name: c.getString('name'),
url: c.getString('url'),
@@ -48,6 +53,7 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier {
authMetadata: {
[ANNOTATION_KUBERNETES_AUTH_PROVIDER]: authProvider,
...ConfigClusterLocator.parseAuthMetadata(c),
...authMetadataBlock,
},
};
@@ -17,7 +17,6 @@
import {
ANNOTATION_KUBERNETES_AUTH_PROVIDER,
ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER,
ObjectsByEntityResponse,
KubernetesRequestAuth,
} from '@backstage/plugin-kubernetes-common';
import request from 'supertest';
@@ -39,10 +38,7 @@ import {
startTestBackend,
} from '@backstage/backend-test-utils';
import { rest } from 'msw';
import {
AuthorizeResult,
PermissionEvaluator,
} from '@backstage/plugin-permission-common';
import { AuthorizeResult } from '@backstage/plugin-permission-common';
import {
PermissionsService,
createBackendModule,
@@ -57,9 +53,9 @@ import {
} from '@backstage/plugin-kubernetes-node';
import { ExtendedHttpServer } from '@backstage/backend-app-api';
describe('KubernetesBuilder', () => {
describe('API integration tests', () => {
let app: ExtendedHttpServer;
let objectsProviderMock: KubernetesObjectsProvider;
let objectsProviderMock: jest.Mocked<KubernetesObjectsProvider>;
const happyK8SResult = {
items: [{ clusterOne: { pods: [{ metadata: { name: 'pod1' } }] } }],
};
@@ -530,52 +526,116 @@ metadata:
expect(response.body).toStrictEqual({ items: [] });
});
it('should not permit custom auth strategies with dashes', () => {
const throwError = () =>
startTestBackend({
features: [
import('@backstage/plugin-kubernetes-backend/alpha'),
createBackendModule({
pluginId: 'kubernetes',
moduleId: 'testAuthStrategy',
register(env) {
env.registerInit({
deps: { extension: kubernetesAuthStrategyExtensionPoint },
async init({ extension }) {
extension.addAuthStrategy('custom-strategy', {
getCredential: jest
.fn<
Promise<KubernetesCredential>,
[ClusterDetails, KubernetesRequestAuth]
>()
.mockResolvedValue({ type: 'anonymous' }),
validateCluster: jest.fn().mockReturnValue([]),
});
it('reads custom auth metadata from config', async () => {
const authStrategy = {
getCredential: jest.fn().mockResolvedValue({ type: 'anonymous' }),
validateCluster: jest.fn().mockReturnValue([]),
};
worker.use(
rest.get('http://my.cluster/api', (_req, res, ctx) =>
res(ctx.json({})),
),
);
const { server } = await startTestBackend({
features: [
mockServices.rootConfig.factory({
data: {
kubernetes: {
serviceLocatorMethod: { type: 'multiTenant' },
clusterLocatorMethods: [
{
type: 'config',
clusters: [
{
name: 'cluster',
url: 'http://my.cluster',
authProvider: 'custom',
authMetadata: { 'custom-key': 'custom-value' },
},
],
},
});
],
},
}),
],
});
return expect(throwError).rejects.toThrow(
'Strategy name can not include dashes',
},
}),
import('@backstage/plugin-kubernetes-backend/alpha'),
createBackendModule({
pluginId: 'kubernetes',
moduleId: 'testAuthStrategy',
register(env) {
env.registerInit({
deps: { extension: kubernetesAuthStrategyExtensionPoint },
async init({ extension }) {
extension.addAuthStrategy('custom', authStrategy);
},
});
},
}),
],
});
app = server;
const proxyEndpointRequest = request(app).get(
'/api/kubernetes/proxy/api',
);
worker.use(rest.all(proxyEndpointRequest.url, req => req.passthrough()));
const response = await proxyEndpointRequest;
expect(response.body).toStrictEqual({});
expect(authStrategy.getCredential).toHaveBeenCalledWith(
expect.objectContaining({
authMetadata: expect.objectContaining({
'custom-key': 'custom-value',
}),
}),
expect.anything(),
);
});
});
describe('get /.well-known/backstage/permissions/metadata', () => {
it('lists permissions supported by the kubernetes plugin', async () => {
const response = await request(app).get(
'/api/kubernetes/.well-known/backstage/permissions/metadata',
);
expect(response.status).toEqual(200);
expect(response.body).toMatchObject({
permissions: [
{ type: 'basic', name: 'kubernetes.proxy', attributes: {} },
it('forbids custom auth strategies with dashes', () => {
const throwError = () =>
startTestBackend({
features: [
import('@backstage/plugin-kubernetes-backend/alpha'),
createBackendModule({
pluginId: 'kubernetes',
moduleId: 'testAuthStrategy',
register(env) {
env.registerInit({
deps: { extension: kubernetesAuthStrategyExtensionPoint },
async init({ extension }) {
extension.addAuthStrategy('custom-strategy', {
getCredential: jest
.fn<
Promise<KubernetesCredential>,
[ClusterDetails, KubernetesRequestAuth]
>()
.mockResolvedValue({ type: 'anonymous' }),
validateCluster: jest.fn().mockReturnValue([]),
});
},
});
},
}),
],
rules: [],
});
return expect(throwError).rejects.toThrow(
'Strategy name can not include dashes',
);
});
it('serves permission integration endpoint', async () => {
const response = await request(app).get(
'/api/kubernetes/.well-known/backstage/permissions/metadata',
);
expect(response.status).toEqual(200);
expect(response.body).toMatchObject({
permissions: [
{ type: 'basic', name: 'kubernetes.proxy', attributes: {} },
],
rules: [],
});
});