Merge pull request #22233 from jamieklassen/custom-authmetadata-in-config
Custom auth metadata in config
This commit is contained in:
@@ -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
@@ -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: [],
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user