Refactor header names, add options object to requestHandler and add new changes to api-reports and changeset along with docs
Signed-off-by: Ruben Vallejo <rvallejo@vmware.com>
This commit is contained in:
@@ -2,4 +2,4 @@
|
||||
'@backstage/plugin-kubernetes-backend': minor
|
||||
---
|
||||
|
||||
`KubernetesBuilder.create` now requires a `permissions` field of type `PermissionEvaluator`. The kubernetes `/proxy` endpoint now requires two tokens: the `X-Kubernetes-Authorization` header should contain a bearer token for the target cluster, and the `Authorization` header should contain a backstage identity token.
|
||||
**BREAKING**: `KubernetesBuilder.create` now requires a `permissions` field of type `PermissionEvaluator`. The kubernetes `/proxy` endpoint now requires two tokens: the `Backstage-Kubernetes-Authorization` header should contain a bearer token for the target cluster, and the `Authorization` header should contain a backstage identity token. The kubernetes `/proxy` endpoint now requires a `Backstage-Kubernetes-Cluster` header replacing the previously required `X-Kubernetes-Cluster` header.
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
googleAuthApiRef,
|
||||
useApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node';
|
||||
|
||||
const CLUSTER_NAME = ''; // use a known cluster name
|
||||
|
||||
@@ -32,6 +33,11 @@ const token = await googleAuthApi.getAccessToken(
|
||||
'https://www.googleapis.com/auth/cloud-platform',
|
||||
);
|
||||
|
||||
// get a bearer token from backstage `/auth`
|
||||
const userToken = getBearerTokenFromAuthorizationHeader(
|
||||
req.header('authorization'),
|
||||
);
|
||||
|
||||
const discoveryApi = useApi(discoveryApiRef);
|
||||
const kubernetesBaseUrl = await discoveryApi.getBaseUrl('kubernetes');
|
||||
const kubernetesProxyEndpoint = `${kubernetesBaseUrl}/proxy`;
|
||||
@@ -40,8 +46,9 @@ const kubernetesProxyEndpoint = `${kubernetesBaseUrl}/proxy`;
|
||||
await fetch(`${kubernetesProxyEndpoint}/api/v1/namespaces`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Kubernetes-Cluster': CLUSTER_NAME,
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Backstage-Kubernetes-Cluster': CLUSTER_NAME,
|
||||
'Backstage-Kubernetes-Authorization': `Bearer ${token}`,
|
||||
Authorization: `Bearer ${userToken}`,
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -49,7 +56,7 @@ await fetch(`${kubernetesProxyEndpoint}/api/v1/namespaces`, {
|
||||
## How it works
|
||||
|
||||
The proxy will interpret the
|
||||
[`X-Kubernetes-Cluster`
|
||||
[`Backstage-Kubernetes-Cluster`
|
||||
header](https://backstage.io/docs/reference/plugin-kubernetes-backend.header_kubernetes_cluster)
|
||||
as the name of the cluster to target. This name will be compared to each cluster
|
||||
returned by all the configured [cluster
|
||||
@@ -60,6 +67,8 @@ the value in the header will be targeted.
|
||||
Then the request will be forwarded verbatim (but with the endpoint's base URL
|
||||
prefix stripped) to the cluster.
|
||||
|
||||
The proxy will also interpret the `Backstage-Kubernetes-Authorization` header as the `Authorization` header to use when forwarding a request to a target cluster.
|
||||
|
||||
## Authentication
|
||||
|
||||
Until some security and permission decisions are made (see [this
|
||||
|
||||
@@ -142,6 +142,9 @@ export class GoogleServiceAccountAuthTranslator
|
||||
): Promise<GKEClusterDetails>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export const HEADER_KUBERNETES_AUTH: string;
|
||||
|
||||
// @public
|
||||
export const HEADER_KUBERNETES_CLUSTER: string;
|
||||
|
||||
@@ -344,9 +347,16 @@ export type KubernetesObjectTypes =
|
||||
export class KubernetesProxy {
|
||||
constructor(logger: Logger, clusterSupplier: KubernetesClustersSupplier);
|
||||
// (undocumented)
|
||||
createRequestHandler(permissionApi: PermissionEvaluator): RequestHandler;
|
||||
createRequestHandler(
|
||||
options: KubernetesProxyCreateRequestHandlerOptions,
|
||||
): RequestHandler;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type KubernetesProxyCreateRequestHandlerOptions = {
|
||||
permissionApi: PermissionEvaluator;
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface KubernetesServiceLocator {
|
||||
// (undocumented)
|
||||
|
||||
@@ -15,10 +15,7 @@
|
||||
*/
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { Config } from '@backstage/config';
|
||||
import {
|
||||
kubernetesPermissions,
|
||||
RESOURCE_TYPE_KUBERNETES_RESOURCE,
|
||||
} from '@backstage/plugin-kubernetes-common';
|
||||
import { kubernetesPermissions } from '@backstage/plugin-kubernetes-common';
|
||||
import { PermissionEvaluator } from '@backstage/plugin-permission-common';
|
||||
import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';
|
||||
import express from 'express';
|
||||
@@ -275,13 +272,11 @@ export class KubernetesBuilder {
|
||||
): express.Router {
|
||||
const logger = this.env.logger;
|
||||
const router = Router();
|
||||
router.use('/proxy', proxy.createRequestHandler(permissionApi));
|
||||
router.use('/proxy', proxy.createRequestHandler({ permissionApi }));
|
||||
router.use(express.json());
|
||||
router.use(
|
||||
createPermissionIntegrationRouter({
|
||||
resourceType: RESOURCE_TYPE_KUBERNETES_RESOURCE,
|
||||
permissions: kubernetesPermissions,
|
||||
rules: [],
|
||||
}),
|
||||
);
|
||||
// @deprecated
|
||||
|
||||
@@ -68,7 +68,7 @@ describe('KubernetesProxy', () => {
|
||||
getClusters: jest.fn(),
|
||||
};
|
||||
|
||||
const permissions: jest.Mocked<PermissionEvaluator> = {
|
||||
const permissionApi: jest.Mocked<PermissionEvaluator> = {
|
||||
authorize: jest.fn(),
|
||||
authorizeConditional: jest.fn(),
|
||||
};
|
||||
@@ -80,7 +80,7 @@ describe('KubernetesProxy', () => {
|
||||
|
||||
it('should return a ERROR_NOT_FOUND if no clusters are found', async () => {
|
||||
clusterSupplier.getClusters.mockResolvedValue([]);
|
||||
permissions.authorize.mockReturnValue(
|
||||
permissionApi.authorize.mockReturnValue(
|
||||
Promise.resolve([{ result: AuthorizeResult.ALLOW }]),
|
||||
);
|
||||
|
||||
@@ -88,7 +88,7 @@ describe('KubernetesProxy', () => {
|
||||
const { res, next } = getMockRes();
|
||||
|
||||
await expect(
|
||||
proxy.createRequestHandler(permissions)(req, res, next),
|
||||
proxy.createRequestHandler({ permissionApi })(req, res, next),
|
||||
).rejects.toThrow(NotFoundError);
|
||||
});
|
||||
|
||||
@@ -114,9 +114,9 @@ describe('KubernetesProxy', () => {
|
||||
] as ClusterDetails[]);
|
||||
const app = express().use(
|
||||
'/mountpath',
|
||||
proxy.createRequestHandler(permissions),
|
||||
proxy.createRequestHandler({ permissionApi }),
|
||||
);
|
||||
permissions.authorize.mockReturnValue(
|
||||
permissionApi.authorize.mockReturnValue(
|
||||
Promise.resolve([{ result: AuthorizeResult.ALLOW }]),
|
||||
);
|
||||
const requestPromise = request(app)
|
||||
|
||||
@@ -41,14 +41,24 @@ export const APPLICATION_JSON: string = 'application/json';
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const HEADER_KUBERNETES_CLUSTER: string = 'X-Kubernetes-Cluster';
|
||||
export const HEADER_KUBERNETES_CLUSTER: string = 'Backstage-Kubernetes-Cluster';
|
||||
|
||||
/**
|
||||
* The header that is used to specify the Authentication Authorities token.
|
||||
* e.x if using the google auth provider as your authentication authority then this field would be the google provided bearer token.
|
||||
* @alpha
|
||||
* @public
|
||||
*/
|
||||
export const HEADER_KUBERNETES_AUTH: string = 'X-Kubernetes-Authorization';
|
||||
export const HEADER_KUBERNETES_AUTH: string =
|
||||
'Backstage-Kubernetes-Authorization';
|
||||
|
||||
/**
|
||||
* The options object expected to be passed as a parameter to KubernetesProxy.createRequestHandler().
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type KubernetesProxyCreateRequestHandlerOptions = {
|
||||
permissionApi: PermissionEvaluator;
|
||||
};
|
||||
|
||||
/**
|
||||
* A proxy that routes requests to the Kubernetes API.
|
||||
@@ -64,8 +74,9 @@ export class KubernetesProxy {
|
||||
) {}
|
||||
|
||||
public createRequestHandler(
|
||||
permissionApi: PermissionEvaluator,
|
||||
options: KubernetesProxyCreateRequestHandlerOptions,
|
||||
): RequestHandler {
|
||||
const { permissionApi } = options;
|
||||
return async (req, res, next) => {
|
||||
const token = getBearerTokenFromAuthorizationHeader(
|
||||
req.header('authorization'),
|
||||
@@ -81,9 +92,7 @@ export class KubernetesProxy {
|
||||
)[0];
|
||||
|
||||
if (authorizeResponse.result === AuthorizeResult.DENY) {
|
||||
res
|
||||
.status(403)
|
||||
.json({ error: new NotAllowedError('Unauthorized').message });
|
||||
res.status(403).json({ error: new NotAllowedError('Unauthorized') });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -141,8 +150,8 @@ export class KubernetesProxy {
|
||||
res.status(500).json(body);
|
||||
},
|
||||
onProxyReq: (proxyReq, req) => {
|
||||
// the kubernetes proxy endpoint expects a header field labeled `X-Kubernetes-Authorization` that will be used to authenticate with the Kubernetes Api. The token provided as a value should be an Authentication Providers bearer token.
|
||||
const token = req.header('X-Kubernetes-Authorization') ?? '';
|
||||
// the kubernetes proxy endpoint expects a header field labeled `Backstage-Kubernetes-Authorization` that will be used to authenticate with the Kubernetes Api. The token provided as a value should be an bearer token for the target cluster.
|
||||
const token = req.header(HEADER_KUBERNETES_AUTH) ?? '';
|
||||
proxyReq.setHeader('Authorization', token);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -16,5 +16,10 @@
|
||||
|
||||
export * from './KubernetesBuilder';
|
||||
export { DEFAULT_OBJECTS } from './KubernetesFanOutHandler';
|
||||
export { HEADER_KUBERNETES_CLUSTER, KubernetesProxy } from './KubernetesProxy';
|
||||
export {
|
||||
HEADER_KUBERNETES_CLUSTER,
|
||||
KubernetesProxy,
|
||||
HEADER_KUBERNETES_AUTH,
|
||||
} from './KubernetesProxy';
|
||||
export type { KubernetesProxyCreateRequestHandlerOptions } from './KubernetesProxy';
|
||||
export * from './router';
|
||||
|
||||
@@ -218,10 +218,10 @@ export type KubernetesErrorTypes =
|
||||
// @public (undocumented)
|
||||
export type KubernetesFetchError = StatusError | RawFetchError;
|
||||
|
||||
// @alpha
|
||||
// @public
|
||||
export const kubernetesPermissions: BasicPermission[];
|
||||
|
||||
// @alpha
|
||||
// @public
|
||||
export const kubernetesProxyPermission: BasicPermission;
|
||||
|
||||
// @public (undocumented)
|
||||
@@ -288,9 +288,6 @@ export interface ReplicaSetsFetchResponse {
|
||||
type: 'replicasets';
|
||||
}
|
||||
|
||||
// @alpha
|
||||
export const RESOURCE_TYPE_KUBERNETES_RESOURCE = 'kubernetes-resource';
|
||||
|
||||
// @public (undocumented)
|
||||
export interface ServiceFetchResponse {
|
||||
// (undocumented)
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
export * from './types';
|
||||
export * from './catalog-entity-constants';
|
||||
export {
|
||||
RESOURCE_TYPE_KUBERNETES_RESOURCE,
|
||||
kubernetesProxyPermission,
|
||||
kubernetesPermissions,
|
||||
} from './permissions';
|
||||
|
||||
@@ -16,14 +16,8 @@
|
||||
|
||||
import { createPermission } from '@backstage/plugin-permission-common';
|
||||
|
||||
/**
|
||||
* Permission resource type which corresponds to cluster entities.
|
||||
* @alpha
|
||||
*/
|
||||
export const RESOURCE_TYPE_KUBERNETES_RESOURCE = 'kubernetes-resource';
|
||||
|
||||
/** This permission is used to check access to the proxy endpoint
|
||||
* @alpha
|
||||
* @public
|
||||
*/
|
||||
export const kubernetesProxyPermission = createPermission({
|
||||
name: 'kubernetes.proxy',
|
||||
@@ -32,6 +26,6 @@ export const kubernetesProxyPermission = createPermission({
|
||||
|
||||
/**
|
||||
* List of all Kubernetes permissions.
|
||||
* @alpha
|
||||
* @public
|
||||
*/
|
||||
export const kubernetesPermissions = [kubernetesProxyPermission];
|
||||
|
||||
Reference in New Issue
Block a user