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:
Ruben Vallejo
2023-02-13 16:02:52 -05:00
parent e2ff409a3b
commit c04498db27
10 changed files with 59 additions and 41 deletions
+1 -1
View File
@@ -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.
+12 -3
View File
@@ -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
+11 -1
View File
@@ -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';
+2 -5
View File
@@ -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)
-1
View File
@@ -23,7 +23,6 @@
export * from './types';
export * from './catalog-entity-constants';
export {
RESOURCE_TYPE_KUBERNETES_RESOURCE,
kubernetesProxyPermission,
kubernetesPermissions,
} from './permissions';
+2 -8
View File
@@ -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];