kubernetes-backend proxy API invoke Authentication Strategies when Backstage-Kubernetes-Authorization-X-X are provided
Signed-off-by: Andres Mauricio Gomez P <andmagom@outlook.com>
This commit is contained in:
@@ -509,6 +509,134 @@ describe('KubernetesProxy', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should invoke AuthStrategy if Backstage-Kubernetes-Authorization-X-X are provided', async () => {
|
||||
const strategy: jest.Mocked<AuthenticationStrategy> = {
|
||||
getCredential: jest
|
||||
.fn()
|
||||
.mockReturnValue({ type: 'bearer token', token: 'MY_TOKEN3' }),
|
||||
validateCluster: jest.fn(),
|
||||
};
|
||||
|
||||
proxy = new KubernetesProxy({
|
||||
logger: getVoidLogger(),
|
||||
clusterSupplier: clusterSupplier,
|
||||
authStrategy: strategy,
|
||||
});
|
||||
|
||||
worker.use(
|
||||
rest.get('https://localhost:9999/api/v1/namespaces', (req, res, ctx) => {
|
||||
if (!req.headers.get('Authorization')) {
|
||||
return res(ctx.status(401));
|
||||
}
|
||||
|
||||
if (req.headers.get('Authorization') !== 'Bearer MY_TOKEN3') {
|
||||
return res(ctx.status(403));
|
||||
}
|
||||
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
kind: 'NamespaceList',
|
||||
apiVersion: 'v1',
|
||||
items: [],
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
clusterSupplier.getClusters.mockResolvedValue([
|
||||
{
|
||||
name: 'cluster1',
|
||||
url: 'https://localhost:9999',
|
||||
authMetadata: {},
|
||||
},
|
||||
]);
|
||||
|
||||
const requestPromise = setupProxyPromise({
|
||||
proxyPath: '/mountpath',
|
||||
requestPath: '/api/v1/namespaces',
|
||||
|
||||
headers: {
|
||||
[HEADER_KUBERNETES_CLUSTER]: 'cluster1',
|
||||
'Backstage-Kubernetes-Authorization-google': 'MY_TOKEN1',
|
||||
'Backstage-Kubernetes-Authorization-aks': 'MY_TOKEN2',
|
||||
'Backstage-Kubernetes-Authorization-oidc-okta': 'MY_TOKEN3',
|
||||
'Backstage-Kubernetes-Authorization-oidc-gitlab': 'MY_TOKEN4',
|
||||
'Backstage-Kubernetes-Authorization-pinniped-audience1': 'MY_TOKEN5',
|
||||
'Backstage-Kubernetes-Authorization-pinniped-au-b-c-d-e': 'MY_TOKEN6',
|
||||
},
|
||||
});
|
||||
|
||||
const response = await requestPromise;
|
||||
|
||||
const authObj = {
|
||||
google: 'MY_TOKEN1',
|
||||
aks: 'MY_TOKEN2',
|
||||
oidc: { okta: 'MY_TOKEN3', gitlab: 'MY_TOKEN4' },
|
||||
pinniped: { audience1: 'MY_TOKEN5', 'au-b-c-d-e': 'MY_TOKEN6' },
|
||||
};
|
||||
|
||||
expect(strategy.getCredential).toHaveBeenCalledTimes(1);
|
||||
expect(strategy.getCredential).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
authObj,
|
||||
);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toStrictEqual({
|
||||
kind: 'NamespaceList',
|
||||
apiVersion: 'v1',
|
||||
items: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should invoke the Auth strategy with an empty auth object when no Backstage-Kubernetes-Authorization-X-X are provided', async () => {
|
||||
worker.use(
|
||||
rest.get('https://localhost:9999/api/v1/namespaces', (_, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
kind: 'NamespaceList',
|
||||
apiVersion: 'v1',
|
||||
items: [],
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
clusterSupplier.getClusters.mockResolvedValue([
|
||||
{
|
||||
name: 'cluster1',
|
||||
url: 'https://localhost:9999',
|
||||
authMetadata: {},
|
||||
},
|
||||
]);
|
||||
|
||||
const requestPromise = setupProxyPromise({
|
||||
proxyPath: '/mountpath',
|
||||
requestPath: '/api/v1/namespaces',
|
||||
|
||||
headers: {
|
||||
[HEADER_KUBERNETES_CLUSTER]: 'cluster1',
|
||||
},
|
||||
});
|
||||
|
||||
const response = await requestPromise;
|
||||
|
||||
const authObj = {};
|
||||
|
||||
expect(authStrategy.getCredential).toHaveBeenCalledTimes(1);
|
||||
expect(authStrategy.getCredential).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
authObj,
|
||||
);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toStrictEqual({
|
||||
kind: 'NamespaceList',
|
||||
apiVersion: 'v1',
|
||||
items: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a response with a localKubectlProxy auth provider configuration', async () => {
|
||||
proxy = new KubernetesProxy({
|
||||
logger: getVoidLogger(),
|
||||
|
||||
@@ -21,7 +21,10 @@ import {
|
||||
serializeError,
|
||||
} from '@backstage/errors';
|
||||
import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node';
|
||||
import { kubernetesProxyPermission } from '@backstage/plugin-kubernetes-common';
|
||||
import {
|
||||
KubernetesRequestAuth,
|
||||
kubernetesProxyPermission,
|
||||
} from '@backstage/plugin-kubernetes-common';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
PermissionEvaluator,
|
||||
@@ -34,6 +37,7 @@ import { AuthenticationStrategy } from '../auth';
|
||||
import { ClusterDetails, KubernetesClustersSupplier } from '../types/types';
|
||||
|
||||
import type { Request } from 'express';
|
||||
import { IncomingHttpHeaders } from 'http';
|
||||
|
||||
export const APPLICATION_JSON: string = 'application/json';
|
||||
|
||||
@@ -113,9 +117,15 @@ export class KubernetesProxy {
|
||||
if (authHeader) {
|
||||
req.headers.authorization = authHeader;
|
||||
} else {
|
||||
// Map Backstage-Kubernetes-Authorization-X-X headers to a KubernetesRequestAuth object
|
||||
const authObj = KubernetesProxy.authHeadersToKubernetesRequestAuth(
|
||||
req.headers,
|
||||
);
|
||||
|
||||
const credential = await this.getClusterForRequest(req).then(cd => {
|
||||
return this.authStrategy.getCredential(cd, {});
|
||||
return this.authStrategy.getCredential(cd, authObj);
|
||||
});
|
||||
|
||||
if (credential.type === 'bearer token') {
|
||||
req.headers.authorization = `Bearer ${credential.token}`;
|
||||
}
|
||||
@@ -221,4 +231,52 @@ export class KubernetesProxy {
|
||||
|
||||
return cluster;
|
||||
}
|
||||
|
||||
private static authHeadersToKubernetesRequestAuth(
|
||||
originalHeaders: IncomingHttpHeaders,
|
||||
): KubernetesRequestAuth {
|
||||
return Object.keys(originalHeaders)
|
||||
.filter(header => header.startsWith('backstage-kubernetes-authorization'))
|
||||
.map(header =>
|
||||
KubernetesProxy.headerToDictionary(header, originalHeaders),
|
||||
)
|
||||
.filter(headerAsDic => Object.keys(headerAsDic).length !== 0)
|
||||
.reduce(KubernetesProxy.combineHeaders, {});
|
||||
}
|
||||
|
||||
private static headerToDictionary(
|
||||
header: string,
|
||||
originalHeaders: IncomingHttpHeaders,
|
||||
): KubernetesRequestAuth {
|
||||
const obj: KubernetesRequestAuth = {};
|
||||
const headerSplitted = header.split('-');
|
||||
if (headerSplitted.length >= 4) {
|
||||
const framework = headerSplitted[3].toLowerCase();
|
||||
if (headerSplitted.length >= 5) {
|
||||
const provider = headerSplitted.slice(4).join('-').toLowerCase();
|
||||
obj[framework] = { [provider]: originalHeaders[header] };
|
||||
} else {
|
||||
obj[framework] = originalHeaders[header];
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
private static combineHeaders(
|
||||
authObj: any,
|
||||
header: any,
|
||||
): KubernetesRequestAuth {
|
||||
const framework = Object.keys(header)[0];
|
||||
|
||||
if (authObj[framework]) {
|
||||
authObj[framework] = {
|
||||
...authObj[framework],
|
||||
...header[framework],
|
||||
};
|
||||
} else {
|
||||
authObj[framework] = header[framework];
|
||||
}
|
||||
|
||||
return authObj;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user