From c04498db278bf3751964207c69d24a37706dea27 Mon Sep 17 00:00:00 2001 From: Ruben Vallejo Date: Mon, 13 Feb 2023 16:02:52 -0500 Subject: [PATCH] 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 --- .changeset/eleven-bats-tease.md | 2 +- docs/features/kubernetes/proxy.md | 15 ++++++++--- plugins/kubernetes-backend/api-report.md | 12 ++++++++- .../src/service/KubernetesBuilder.ts | 9 ++----- .../src/service/KubernetesProxy.test.ts | 10 +++---- .../src/service/KubernetesProxy.ts | 27 ++++++++++++------- .../kubernetes-backend/src/service/index.ts | 7 ++++- plugins/kubernetes-common/api-report.md | 7 ++--- plugins/kubernetes-common/src/index.ts | 1 - plugins/kubernetes-common/src/permissions.ts | 10 ++----- 10 files changed, 59 insertions(+), 41 deletions(-) diff --git a/.changeset/eleven-bats-tease.md b/.changeset/eleven-bats-tease.md index 400a0fb722..732055ca52 100644 --- a/.changeset/eleven-bats-tease.md +++ b/.changeset/eleven-bats-tease.md @@ -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. diff --git a/docs/features/kubernetes/proxy.md b/docs/features/kubernetes/proxy.md index 5848d62e3e..08d80d3e30 100644 --- a/docs/features/kubernetes/proxy.md +++ b/docs/features/kubernetes/proxy.md @@ -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 diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index 431ddce244..449131a26e 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -142,6 +142,9 @@ export class GoogleServiceAccountAuthTranslator ): Promise; } +// @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) diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index c84f87b36f..367e5edef3 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -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 diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts index a033e7d52d..d2404af839 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts @@ -68,7 +68,7 @@ describe('KubernetesProxy', () => { getClusters: jest.fn(), }; - const permissions: jest.Mocked = { + const permissionApi: jest.Mocked = { 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) diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts index 59d209d93a..49a381b75a 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts @@ -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); }, }); diff --git a/plugins/kubernetes-backend/src/service/index.ts b/plugins/kubernetes-backend/src/service/index.ts index 620f788ac2..2f3e907879 100644 --- a/plugins/kubernetes-backend/src/service/index.ts +++ b/plugins/kubernetes-backend/src/service/index.ts @@ -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'; diff --git a/plugins/kubernetes-common/api-report.md b/plugins/kubernetes-common/api-report.md index 5fdd40d152..6c88074e50 100644 --- a/plugins/kubernetes-common/api-report.md +++ b/plugins/kubernetes-common/api-report.md @@ -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) diff --git a/plugins/kubernetes-common/src/index.ts b/plugins/kubernetes-common/src/index.ts index e9fb2b35d3..d5cf8dcf61 100644 --- a/plugins/kubernetes-common/src/index.ts +++ b/plugins/kubernetes-common/src/index.ts @@ -23,7 +23,6 @@ export * from './types'; export * from './catalog-entity-constants'; export { - RESOURCE_TYPE_KUBERNETES_RESOURCE, kubernetesProxyPermission, kubernetesPermissions, } from './permissions'; diff --git a/plugins/kubernetes-common/src/permissions.ts b/plugins/kubernetes-common/src/permissions.ts index 3123796aa3..d3ccbe8171 100644 --- a/plugins/kubernetes-common/src/permissions.ts +++ b/plugins/kubernetes-common/src/permissions.ts @@ -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];