Merge pull request #22233 from jamieklassen/custom-authmetadata-in-config

Custom auth metadata in config
This commit is contained in:
Jamie Klassen
2024-01-17 18:46:59 -05:00
committed by GitHub
5 changed files with 344 additions and 374 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,97 @@ 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('prefers authMetadata block to top-level keys', async () => {
const sut = ConfigClusterLocator.fromConfig(
new ConfigReader({
clusters: [
{
name: 'cluster',
url: 'http://url',
authProvider: 'aws',
authMetadata: {
[ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'serviceAccount',
},
},
],
}),
authStrategy,
);
const result = await sut.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(
`cluster 'cluster' has no auth provider configured; this must be specified` +
` via the 'authProvider' or ` +
`'authMetadata.${ANNOTATION_KUBERNETES_AUTH_PROVIDER}' parameter`,
);
});
it('one cluster with dashboardParameters', async () => {
const config: Config = new ConfigReader({
clusters: [
@@ -37,9 +37,22 @@ 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 name = c.getString('name');
const authProvider =
authMetadataBlock?.[ANNOTATION_KUBERNETES_AUTH_PROVIDER] ??
c.getOptionalString('authProvider');
if (!authProvider) {
throw new Error(
`cluster '${name}' has no auth provider configured; this must be ` +
`specified via the 'authProvider' or ` +
`'authMetadata.${ANNOTATION_KUBERNETES_AUTH_PROVIDER}' parameter`,
);
}
const clusterDetails: ClusterDetails = {
name: c.getString('name'),
name,
url: c.getString('url'),
skipTLSVerify: c.getOptionalBoolean('skipTLSVerify') ?? false,
skipMetricsLookup: c.getOptionalBoolean('skipMetricsLookup') ?? false,
@@ -48,6 +61,7 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier {
authMetadata: {
[ANNOTATION_KUBERNETES_AUTH_PROVIDER]: authProvider,
...ConfigClusterLocator.parseAuthMetadata(c),
...authMetadataBlock,
},
};
@@ -14,20 +14,16 @@
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import {
ANNOTATION_KUBERNETES_AUTH_PROVIDER,
ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER,
ObjectsByEntityResponse,
KubernetesRequestAuth,
} from '@backstage/plugin-kubernetes-common';
import request from 'supertest';
import {
ClusterDetails,
FetchResponseWrapper,
KubernetesFetcher,
KubernetesServiceLocator,
ObjectFetchParams,
} from '../types/types';
import { KubernetesCredential } from '../auth/types';
import {
@@ -42,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,
@@ -60,99 +53,69 @@ 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',
},
},
],
},
},
],
} as any;
const policyMock: jest.Mocked<PermissionEvaluator> = {
authorize: jest.fn(),
authorizeConditional: jest.fn(),
items: [{ clusterOne: { pods: [{ metadata: { name: 'pod1' } }] } }],
};
const permissionsMock: ServiceMock<PermissionsService> =
mockServices.permissions.mock(policyMock);
mockServices.permissions.mock({
authorize: jest.fn(),
authorizeConditional: jest.fn(),
});
const minimalValidConfigService = mockServices.rootConfig.factory({
data: {
kubernetes: {
serviceLocatorMethod: { type: 'multiTenant' },
clusterLocatorMethods: [],
},
},
});
const withClusters = (clusters: ClusterDetails[]) =>
createBackendModule({
pluginId: 'kubernetes',
moduleId: 'testClusterSupplier',
register(env) {
env.registerInit({
deps: { extension: kubernetesClusterSupplierExtensionPoint },
async init({ extension }) {
extension.addClusterSupplier({
getClusters: jest.fn().mockResolvedValue(clusters),
});
},
});
},
});
beforeEach(async () => {
jest.resetAllMocks();
objectsProviderMock = {
getKubernetesObjectsByEntity: jest.fn().mockImplementation(_ => {
return Promise.resolve(happyK8SResult);
}),
getCustomResourcesByEntity: jest.fn().mockImplementation(_ => {
return Promise.resolve(happyK8SResult);
}),
};
const clusterSupplierMock = {
getClusters: jest.fn().mockImplementation(_ => {
return Promise.resolve([
{
name: 'some-cluster',
url: 'https://localhost:1234',
authMetadata: {
[ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'serviceAccount',
},
},
{
name: 'some-other-cluster',
url: 'https://localhost:1235',
authMetadata: {
[ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'oidc',
[ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'google',
},
},
]);
}),
getKubernetesObjectsByEntity: jest.fn().mockResolvedValue(happyK8SResult),
getCustomResourcesByEntity: jest.fn().mockResolvedValue(happyK8SResult),
};
jest.mock('@backstage/catalog-client', () => ({
CatalogClient: jest.fn().mockImplementation(() => ({
getEntityByRef: jest.fn().mockImplementation(entityRef => {
CatalogClient: jest.fn().mockReturnValue({
getEntityByRef: jest.fn().mockImplementation(async entityRef => {
if (entityRef.name === 'noentity') {
return Promise.resolve(undefined);
return undefined;
}
return Promise.resolve({
return {
kind: entityRef.kind,
metadata: {
name: entityRef.name,
namespace: entityRef.namespace,
},
} as Entity);
};
}),
})),
}),
}));
const { server } = await startTestBackend({
features: [
mockServices.rootConfig.factory({
data: {
kubernetes: {
serviceLocatorMethod: {
type: 'multiTenant',
},
clusterLocatorMethods: [
{
type: 'config',
clusters: [],
},
],
},
},
}),
minimalValidConfigService,
import('@backstage/plugin-kubernetes-backend/alpha'),
import('@backstage/plugin-permission-backend/alpha'),
import('@backstage/plugin-permission-backend-module-allow-all-policy'),
@@ -168,24 +131,33 @@ describe('KubernetesBuilder', () => {
});
},
}),
createBackendModule({
pluginId: 'kubernetes',
moduleId: 'testClusterSupplier',
register(env) {
env.registerInit({
deps: { extension: kubernetesClusterSupplierExtensionPoint },
async init({ extension }) {
extension.addClusterSupplier(clusterSupplierMock);
},
});
withClusters([
{
name: 'some-cluster',
url: 'https://localhost:1234',
authMetadata: {
[ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'serviceAccount',
},
},
}),
{
name: 'some-other-cluster',
url: 'https://localhost:1235',
authMetadata: {
[ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'oidc',
[ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'google',
},
},
]),
],
});
app = server;
});
afterEach(() => {
app.stop();
});
describe('get /clusters', () => {
it('happy path: lists clusters', async () => {
const response = await request(app).get('/api/kubernetes/clusters');
@@ -219,20 +191,16 @@ describe('KubernetesBuilder', () => {
it('happy path: lists kubernetes objects with auth in request body', async () => {
const response = await request(app)
.post('/api/kubernetes/services/test-service')
.send({
auth: {
google: 'google_token_123',
},
})
.send({ auth: { google: 'google_token_123' } })
.set('Content-Type', 'application/json');
expect(response.status).toEqual(200);
expect(response.body).toEqual(happyK8SResult);
});
it('internal error: lists kubernetes objects', async () => {
objectsProviderMock.getKubernetesObjectsByEntity = jest
.fn()
.mockRejectedValue(Error('some internal error'));
objectsProviderMock.getKubernetesObjectsByEntity.mockRejectedValue(
Error('some internal error'),
);
const response = await request(app).post(
'/api/kubernetes/services/test-service',
@@ -260,96 +228,29 @@ describe('KubernetesBuilder', () => {
},
];
const clusterSupplierMock = {
getClusters: jest.fn().mockImplementation(_ => {
return Promise.resolve(clusters);
const pod = { metadata: { name: 'pod1' } };
const mockServiceLocator: jest.Mocked<KubernetesServiceLocator> = {
getClustersByEntity: jest.fn().mockResolvedValue({
clusters: [someCluster],
}),
};
const pod = {
metadata: {
name: 'pod1',
},
};
const result: ObjectsByEntityResponse = {
items: [
{
cluster: {
name: someCluster.name,
},
errors: [],
podMetrics: [],
resources: [
{
type: 'pods',
resources: [pod],
},
],
},
],
};
const mockServiceLocator: KubernetesServiceLocator = {
getClustersByEntity(
_entity: Entity,
): Promise<{ clusters: ClusterDetails[] }> {
return Promise.resolve({ clusters: [someCluster] });
},
};
const mockFetcher: KubernetesFetcher = {
fetchPodMetricsByNamespaces(
_clusterDetails: ClusterDetails,
_credential: KubernetesCredential,
_namespaces: Set<string>,
): Promise<FetchResponseWrapper> {
return Promise.resolve({ errors: [], responses: [] });
},
fetchObjectsForService(
_params: ObjectFetchParams,
): Promise<FetchResponseWrapper> {
return Promise.resolve({
errors: [],
responses: [
{
type: 'pods',
resources: [pod],
},
],
});
},
const mockFetcher: jest.Mocked<KubernetesFetcher> = {
fetchPodMetricsByNamespaces: jest
.fn()
.mockResolvedValue({ errors: [], responses: [] }),
fetchObjectsForService: jest.fn().mockResolvedValue({
errors: [],
responses: [{ type: 'pods', resources: [pod] }],
}),
};
const { server } = await startTestBackend({
features: [
mockServices.rootConfig.factory({
data: {
kubernetes: {
serviceLocatorMethod: {
type: 'multiTenant',
},
clusterLocatorMethods: [
{
type: 'config',
clusters: [],
},
],
},
},
}),
minimalValidConfigService,
import('@backstage/plugin-kubernetes-backend/alpha'),
createBackendModule({
pluginId: 'kubernetes',
moduleId: 'testClusterSupplier',
register(env) {
env.registerInit({
deps: { extension: kubernetesClusterSupplierExtensionPoint },
async init({ extension }) {
extension.addClusterSupplier(clusterSupplierMock);
},
});
},
}),
withClusters(clusters),
createBackendModule({
pluginId: 'kubernetes',
moduleId: 'testFetcher',
@@ -376,20 +277,22 @@ describe('KubernetesBuilder', () => {
}),
],
});
app = server;
const response = await request(app)
.post('/api/kubernetes/services/test-service')
.send({
entity: {
metadata: {
name: 'thing',
},
},
});
.send({ entity: { metadata: { name: 'thing' } } });
expect(response.body).toEqual(result);
expect(response.body).toEqual({
items: [
{
cluster: { name: someCluster.name },
errors: [],
podMetrics: [],
resources: [{ type: 'pods', resources: [pod] }],
},
],
});
expect(response.status).toEqual(200);
});
@@ -406,9 +309,11 @@ describe('KubernetesBuilder', () => {
}),
};
const clusterSupplierMock = {
getClusters: jest.fn().mockImplementation(_ => {
return Promise.resolve([
const { server } = await startTestBackend({
features: [
minimalValidConfigService,
import('@backstage/plugin-kubernetes-backend/alpha'),
withClusters([
{
name: 'custom-cluster',
url: 'http://my.cluster.url',
@@ -416,40 +321,7 @@ describe('KubernetesBuilder', () => {
[ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'custom',
},
},
]);
}),
};
const { server } = await startTestBackend({
features: [
mockServices.rootConfig.factory({
data: {
kubernetes: {
serviceLocatorMethod: {
type: 'multiTenant',
},
clusterLocatorMethods: [
{
type: 'config',
clusters: [],
},
],
},
},
}),
import('@backstage/plugin-kubernetes-backend/alpha'),
createBackendModule({
pluginId: 'kubernetes',
moduleId: 'testClusterSupplier',
register(env) {
env.registerInit({
deps: { extension: kubernetesClusterSupplierExtensionPoint },
async init({ extension }) {
extension.addClusterSupplier(clusterSupplierMock);
},
});
},
}),
]),
createBackendModule({
pluginId: 'kubernetes',
moduleId: 'testAuthStrategy',
@@ -493,11 +365,7 @@ describe('KubernetesBuilder', () => {
await request(app)
.post('/api/kubernetes/services/test-service')
.send({
entity: {
metadata: {
name: 'thing',
},
},
entity: { metadata: { name: 'thing' } },
auth: { custom: 'custom-token' },
});
@@ -534,30 +402,26 @@ describe('KubernetesBuilder', () => {
);
});
it('returns the given request body with permission set to allow', async () => {
const requestBody = {
it('forwards request body to k8s', async () => {
const namespaceManifest = {
kind: 'Namespace',
apiVersion: 'v1',
metadata: {
name: 'new-ns',
},
metadata: { name: 'new-ns' },
};
const proxyEndpointRequest = request(app)
.post('/api/kubernetes/proxy/api/v1/namespaces')
.set(HEADER_KUBERNETES_CLUSTER, 'some-cluster')
.set(HEADER_KUBERNETES_AUTH, 'randomtoken')
.send(requestBody);
.send(namespaceManifest);
worker.use(rest.all(proxyEndpointRequest.url, req => req.passthrough()));
const response = await proxyEndpointRequest;
expect(response.body).toStrictEqual(requestBody);
expect(response.body).toStrictEqual(namespaceManifest);
});
it('supports yaml content type with permission set to allow', async () => {
const requestBody = `---
it('supports yaml content type', async () => {
const yamlManifest = `---
kind: Namespace
apiVersion: v1
metadata:
@@ -569,55 +433,37 @@ metadata:
.set(HEADER_KUBERNETES_CLUSTER, 'some-cluster')
.set(HEADER_KUBERNETES_AUTH, 'randomtoken')
.set('content-type', 'application/yaml')
.send(requestBody);
.send(yamlManifest);
worker.use(rest.all(proxyEndpointRequest.url, req => req.passthrough()));
const response = await proxyEndpointRequest;
expect(response.text).toEqual(requestBody);
expect(response.text).toEqual(yamlManifest);
});
it('returns a 403 response if Permission Policy is in place that blocks endpoint', async () => {
const requestBody = {
kind: 'Namespace',
apiVersion: 'v1',
metadata: {
name: 'new-ns',
},
};
it('returns 403 response when permission blocks endpoint', async () => {
permissionsMock.authorize.mockResolvedValue([
{ result: AuthorizeResult.DENY },
]);
const { server } = await startTestBackend({
features: [
mockServices.rootConfig.factory({
data: {
kubernetes: {
serviceLocatorMethod: {
type: 'multiTenant',
},
clusterLocatorMethods: [
{
type: 'config',
clusters: [],
},
],
},
},
}),
minimalValidConfigService,
permissionsMock.factory,
import('@backstage/plugin-kubernetes-backend/alpha'),
// import('@backstage/plugin-permission-backend/alpha'),
],
});
app = server;
const proxyEndpointRequest = request(server)
const proxyEndpointRequest = request(app)
.post('/api/kubernetes/proxy/api/v1/namespaces')
.set(HEADER_KUBERNETES_CLUSTER, 'some-cluster')
.set(HEADER_KUBERNETES_AUTH, 'randomtoken')
.send(requestBody);
.send({
kind: 'Namespace',
apiVersion: 'v1',
metadata: { name: 'new-ns' },
});
worker.use(rest.all(proxyEndpointRequest.url, req => req.passthrough()));
@@ -636,62 +482,17 @@ metadata:
}),
);
const clusterSupplierMock = {
getClusters: jest.fn().mockImplementation(_ => {
return Promise.resolve([
const { server } = await startTestBackend({
features: [
minimalValidConfigService,
import('@backstage/plugin-kubernetes-backend/alpha'),
withClusters([
{
name: 'custom-cluster',
url: 'http://my.cluster.url',
authMetadata: {
[ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'custom',
},
authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'custom' },
},
]);
}),
};
const { server } = await startTestBackend({
features: [
mockServices.rootConfig.factory({
data: {
kubernetes: {
serviceLocatorMethod: {
type: 'multiTenant',
},
clusterLocatorMethods: [
{
type: 'config',
clusters: [],
},
],
},
},
}),
import('@backstage/plugin-kubernetes-backend/alpha'),
createBackendModule({
pluginId: 'kubernetes',
moduleId: 'testObjectsProvider',
register(env) {
env.registerInit({
deps: { extension: kubernetesObjectsProviderExtensionPoint },
async init({ extension }) {
extension.addObjectsProvider(objectsProviderMock);
},
});
},
}),
createBackendModule({
pluginId: 'kubernetes',
moduleId: 'testClusterSupplier',
register(env) {
env.registerInit({
deps: { extension: kubernetesClusterSupplierExtensionPoint },
async init({ extension }) {
extension.addClusterSupplier(clusterSupplierMock);
},
});
},
}),
]),
createBackendModule({
pluginId: 'kubernetes',
moduleId: 'testAuthStrategy',
@@ -715,9 +516,7 @@ metadata:
],
});
app = server;
const proxyEndpointRequest = request(app)
const proxyEndpointRequest = request(server)
.get('/api/kubernetes/proxy/api/v1/namespaces')
.set(HEADER_KUBERNETES_CLUSTER, 'custom-cluster')
.set(HEADER_KUBERNETES_AUTH, 'custom-token');
@@ -727,58 +526,116 @@ metadata:
expect(response.body).toStrictEqual({ items: [] });
});
it('should not permit custom auth strategies with dashes', async () => {
const throwError = async () => {
await 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' },
},
],
},
],
},
},
}),
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;
await expect(throwError).rejects.toThrow(
'Strategy name can not include dashes',
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: [],
});
});