diff --git a/.changeset/eleven-bats-tease.md b/.changeset/eleven-bats-tease.md new file mode 100644 index 0000000000..732055ca52 --- /dev/null +++ b/.changeset/eleven-bats-tease.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-backend': minor +--- + +**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/.changeset/wise-garlics-camp.md b/.changeset/wise-garlics-camp.md new file mode 100644 index 0000000000..e7639ad71c --- /dev/null +++ b/.changeset/wise-garlics-camp.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-common': patch +--- + +Introduced proxy permission types to be used with the kubernetes proxy endpoint's permission framework integration. diff --git a/docs/features/kubernetes/installation.md b/docs/features/kubernetes/installation.md index ae7826a804..17028ef90f 100644 --- a/docs/features/kubernetes/installation.md +++ b/docs/features/kubernetes/installation.md @@ -75,6 +75,7 @@ export default async function createPlugin( logger: env.logger, config: env.config, catalogApi, + permissions: env.permissions, }).build(); return router; } diff --git a/docs/features/kubernetes/proxy.md b/docs/features/kubernetes/proxy.md index 5848d62e3e..b53dfeb29d 100644 --- a/docs/features/kubernetes/proxy.md +++ b/docs/features/kubernetes/proxy.md @@ -22,6 +22,7 @@ import { discoveryApiRef, googleAuthApiRef, useApi, + identityApiRef, } from '@backstage/core-plugin-api'; const CLUSTER_NAME = ''; // use a known cluster name @@ -32,6 +33,10 @@ const token = await googleAuthApi.getAccessToken( 'https://www.googleapis.com/auth/cloud-platform', ); +// get a backstage ID token +const identityApi = useApi(identityApiRef); +const { token: userToken } = await identityApi.getCredentials(); + const discoveryApi = useApi(discoveryApiRef); const kubernetesBaseUrl = await discoveryApi.getBaseUrl('kubernetes'); const kubernetesProxyEndpoint = `${kubernetesBaseUrl}/proxy`; @@ -40,8 +45,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,16 +55,19 @@ await fetch(`${kubernetesProxyEndpoint}/api/v1/namespaces`, { ## How it works The proxy will interpret the -[`X-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 +[`Backstage-Kubernetes-Cluster`](https://backstage.io/docs/reference/plugin-kubernetes-backend.header_kubernetes_cluster) +header as the name of the cluster to target. This name will be compared to each cluster returned by all the configured [cluster locators](https://backstage.io/docs/features/kubernetes/configuration#clusterlocatormethods) -- the first cluster whose [`name` field](https://backstage.io/docs/features/kubernetes/configuration#clustersname) matches 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. +Then the request will be forwarded to the cluster. + +Overall, the only changes to each request are: + +- the endpoint's base URL prefix is stripped. +- the `Backstage-Kubernetes-Authorization` header becomes the `Authorization` header that is used when forwarding the request. ## Authentication @@ -73,9 +82,59 @@ providers](https://backstage.io/docs/features/kubernetes/authentication#client-s The proxy has no provisions for mTLS, so it cannot be used to connect to clusters using the [x509 Client Certs](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#x509-client-certs) -authentication strategy. [Bearer -tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#putting-a-bearer-token-in-a-request) -will be forwarded as-is. +authentication strategy.\ +The current `/proxy` Implementation expects a +[Bearer +token](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#putting-a-bearer-token-in-a-request) +to be provided as a `Backstage-Kubernetes-Authorization` header for a target cluster. This token will be used as the `Authorization` header when forwarding a request to a target cluster. + +## How to disable the proxy endpoint via PermissionPolicy + +The kubernetes plugin can disable the use of the `proxy` endpoint by leveraging the permission framework. This integration allows admins to use well defined PermissionPolicies to restrict the use of the endpoint all together. The `proxy` endpoint can return 403 errors even if it has a valid ID token attached that a cluster would authorize thus allowing integrators the confidence that Backstage is not accessing kubernetes clusters on behalf of undesired parties. + +This feature assumes your backstage instance has enabled the [permissions framework](https://backstage.io/docs/permissions/getting-started) + +A sample policy like: + +[packages/backend/src/plugins/permissions.ts](https://github.com/backstage/backstage/blob/master/packages/backend/src/plugins/permission.ts) + +```typescript +import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; +import { + AuthorizeResult, + PolicyDecision, +} from '@backstage/plugin-permission-common'; +import { + PermissionPolicy, + PolicyQuery, +} from '@backstage/plugin-permission-node'; + +class KubernetesDenyAllProxyEndpointPolicy implements PermissionPolicy { + async handle( + request: PolicyQuery, + user?: BackstageIdentityResponse, + ): Promise { + if (request.permission.name === 'kubernetes.proxy') { + return { + result: AuthorizeResult.DENY, + }; + } + return { result: AuthorizeResult.ALLOW }; + } +} +``` + +would leverage the permission framework to return the following response: + +```json +{ + "error": { + "name": "NotAllowedError" + } +} +``` + +even if a valid ID token was attached that a cluster would authorize. ## Other known limitations diff --git a/packages/backend/src/plugins/kubernetes.ts b/packages/backend/src/plugins/kubernetes.ts index 3bc7a5dcbe..3bc6648862 100644 --- a/packages/backend/src/plugins/kubernetes.ts +++ b/packages/backend/src/plugins/kubernetes.ts @@ -27,6 +27,7 @@ export default async function createPlugin( logger: env.logger, config: env.config, catalogApi, + permissions: env.permissions, }).build(); return router; } diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index a6a4bcfc58..449131a26e 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -17,6 +17,7 @@ import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; import type { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; import { Logger } from 'winston'; import type { ObjectsByEntityResponse } from '@backstage/plugin-kubernetes-common'; +import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import type { RequestHandler } from 'express'; import { TokenCredential } from '@azure/identity'; @@ -141,6 +142,9 @@ export class GoogleServiceAccountAuthTranslator ): Promise; } +// @public +export const HEADER_KUBERNETES_AUTH: string; + // @public export const HEADER_KUBERNETES_CLUSTER: string; @@ -200,6 +204,7 @@ export class KubernetesBuilder { clusterSupplier: KubernetesClustersSupplier, catalogApi: CatalogApi, proxy: KubernetesProxy, + permissionApi: PermissionEvaluator, ): express.Router; // (undocumented) protected buildServiceLocator( @@ -271,6 +276,8 @@ export interface KubernetesEnvironment { config: Config; // (undocumented) logger: Logger; + // (undocumented) + permissions: PermissionEvaluator; } // @public @@ -340,9 +347,16 @@ export type KubernetesObjectTypes = export class KubernetesProxy { constructor(logger: Logger, clusterSupplier: KubernetesClustersSupplier); // (undocumented) - createRequestHandler(): RequestHandler; + createRequestHandler( + options: KubernetesProxyCreateRequestHandlerOptions, + ): RequestHandler; } +// @public +export type KubernetesProxyCreateRequestHandlerOptions = { + permissionApi: PermissionEvaluator; +}; + // @public export interface KubernetesServiceLocator { // (undocumented) @@ -418,6 +432,8 @@ export interface RouterOptions { discovery: PluginEndpointDiscovery; // (undocumented) logger: Logger; + // (undocumented) + permissions: PermissionEvaluator; } // @public (undocumented) diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 850f3c67f4..e052fb4f58 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -42,6 +42,8 @@ "@backstage/errors": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "@backstage/plugin-kubernetes-common": "workspace:^", + "@backstage/plugin-permission-common": "workspace:^", + "@backstage/plugin-permission-node": "workspace:^", "@google-cloud/container": "^4.0.0", "@jest-mock/express": "^2.0.1", "@kubernetes/client-node": "0.18.1", diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts index cc2dea26c0..8eb7a066df 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts @@ -31,16 +31,24 @@ import { import { KubernetesBuilder } from './KubernetesBuilder'; import { KubernetesFanOutHandler } from './KubernetesFanOutHandler'; import { CatalogApi } from '@backstage/catalog-client'; -import { HEADER_KUBERNETES_CLUSTER } from './KubernetesProxy'; +import { + HEADER_KUBERNETES_CLUSTER, + HEADER_KUBERNETES_AUTH, +} from './KubernetesProxy'; import { setupServer } from 'msw/node'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { rest } from 'msw'; +import { + AuthorizeResult, + PermissionEvaluator, +} from '@backstage/plugin-permission-common'; describe('KubernetesBuilder', () => { let app: express.Express; let kubernetesFanOutHandler: jest.Mocked; let config: Config; let catalogApi: CatalogApi; + let permissions: jest.Mocked; beforeAll(async () => { const logger = getVoidLogger(); @@ -74,10 +82,16 @@ describe('KubernetesBuilder', () => { getKubernetesObjectsByEntity: jest.fn(), } as any; + permissions = { + authorize: jest.fn(), + authorizeConditional: jest.fn(), + }; + const { router } = await KubernetesBuilder.createBuilder({ config, logger, catalogApi, + permissions, }) .setObjectsProvider(kubernetesFanOutHandler) .setClusterSupplier(clusterSupplier) @@ -250,6 +264,7 @@ describe('KubernetesBuilder', () => { logger, config, catalogApi, + permissions, }) .setClusterSupplier(clusterSupplier) .setServiceLocator(serviceLocator) @@ -271,27 +286,32 @@ describe('KubernetesBuilder', () => { expect(response.status).toEqual(200); }); }); - describe('post /proxy', () => { const worker = setupServer(); setupRequestMockHandlers(worker); beforeEach(() => { worker.use( - rest.post('https://localhost:1234/api/v1/namespaces', (req, res, ctx) => - req - .arrayBuffer() - .then(body => - res( - ctx.set('content-type', `${req.headers.get('content-type')}`), - ctx.body(body), - ), - ), + rest.post( + 'https://localhost:1234/api/v1/namespaces', + (req, res, ctx) => { + if (!req.headers.get('Authorization')) { + return res(ctx.status(401)); + } + return req + .arrayBuffer() + .then(body => + res( + ctx.set('content-type', `${req.headers.get('content-type')}`), + ctx.body(body), + ), + ); + }, ), ); }); - it('returns the given request body', async () => { + it('returns the given request body with permission set to allow', async () => { const requestBody = { kind: 'Namespace', apiVersion: 'v1', @@ -300,9 +320,14 @@ describe('KubernetesBuilder', () => { }, }; + permissions.authorize.mockReturnValue( + Promise.resolve([{ result: AuthorizeResult.ALLOW }]), + ); + const proxyEndpointRequest = request(app) .post('/proxy/api/v1/namespaces') .set(HEADER_KUBERNETES_CLUSTER, 'some-cluster') + .set(HEADER_KUBERNETES_AUTH, 'randomtoken') .send(requestBody); worker.use(rest.all(proxyEndpointRequest.url, req => req.passthrough())); @@ -312,7 +337,7 @@ describe('KubernetesBuilder', () => { expect(response.body).toStrictEqual(requestBody); }); - it('supports yaml content type', async () => { + it('supports yaml content type with permission set to allow', async () => { const requestBody = `--- kind: Namespace apiVersion: v1 @@ -320,9 +345,14 @@ metadata: name: new-ns `; + permissions.authorize.mockReturnValue( + Promise.resolve([{ result: AuthorizeResult.ALLOW }]), + ); + const proxyEndpointRequest = request(app) .post('/proxy/api/v1/namespaces') .set(HEADER_KUBERNETES_CLUSTER, 'some-cluster') + .set(HEADER_KUBERNETES_AUTH, 'randomtoken') .set('content-type', 'application/yaml') .send(requestBody); @@ -331,5 +361,50 @@ metadata: const response = await proxyEndpointRequest; expect(response.text).toEqual(requestBody); }); + + 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', + }, + }; + + permissions.authorize.mockReturnValue( + Promise.resolve([{ result: AuthorizeResult.DENY }]), + ); + + const proxyEndpointRequest = request(app) + .post('/proxy/api/v1/namespaces') + .set(HEADER_KUBERNETES_CLUSTER, 'some-cluster') + .set(HEADER_KUBERNETES_AUTH, 'randomtoken') + .send(requestBody); + + worker.use(rest.all(proxyEndpointRequest.url, req => req.passthrough())); + + const response = await proxyEndpointRequest; + + expect(response.status).toEqual(403); + }); + }); + describe('get /.well-known/backstage/permissions/metadata', () => { + it('lists permissions supported by the kubernetes plugin', async () => { + const response = await request(app).get( + '/.well-known/backstage/permissions/metadata', + ); + + expect(response.status).toEqual(200); + expect(response.body).toMatchObject({ + permissions: [ + { + type: 'basic', + name: 'kubernetes.proxy', + attributes: {}, + }, + ], + rules: [], + }); + }); }); }); diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index 74ec9eada2..367e5edef3 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -15,6 +15,9 @@ */ import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; +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'; import Router from 'express-promise-router'; import { Duration } from 'luxon'; @@ -49,6 +52,7 @@ export interface KubernetesEnvironment { logger: Logger; config: Config; catalogApi: CatalogApi; + permissions: PermissionEvaluator; } /** @@ -89,6 +93,7 @@ export class KubernetesBuilder { public async build(): KubernetesBuilderReturn { const logger = this.env.logger; const config = this.env.config; + const permissions = this.env.permissions; logger.info('Initializing Kubernetes backend'); @@ -126,6 +131,7 @@ export class KubernetesBuilder { clusterSupplier, this.env.catalogApi, proxy, + permissions, ); return { @@ -262,12 +268,17 @@ export class KubernetesBuilder { clusterSupplier: KubernetesClustersSupplier, catalogApi: CatalogApi, proxy: KubernetesProxy, + permissionApi: PermissionEvaluator, ): express.Router { const logger = this.env.logger; const router = Router(); - router.use('/proxy', proxy.createRequestHandler()); + router.use('/proxy', proxy.createRequestHandler({ permissionApi })); router.use(express.json()); - + router.use( + createPermissionIntegrationRouter({ + permissions: kubernetesPermissions, + }), + ); // @deprecated router.post('/services/:serviceId', async (req, res) => { const serviceId = req.params.serviceId; diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts index 4254bb33da..d2404af839 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts @@ -30,6 +30,10 @@ import { HEADER_KUBERNETES_CLUSTER, KubernetesProxy, } from './KubernetesProxy'; +import { + AuthorizeResult, + PermissionEvaluator, +} from '@backstage/plugin-permission-common'; describe('KubernetesProxy', () => { let proxy: KubernetesProxy; @@ -64,6 +68,11 @@ describe('KubernetesProxy', () => { getClusters: jest.fn(), }; + const permissionApi: jest.Mocked = { + authorize: jest.fn(), + authorizeConditional: jest.fn(), + }; + beforeEach(() => { jest.resetAllMocks(); proxy = new KubernetesProxy(getVoidLogger(), clusterSupplier); @@ -71,13 +80,16 @@ describe('KubernetesProxy', () => { it('should return a ERROR_NOT_FOUND if no clusters are found', async () => { clusterSupplier.getClusters.mockResolvedValue([]); + permissionApi.authorize.mockReturnValue( + Promise.resolve([{ result: AuthorizeResult.ALLOW }]), + ); const req = buildMockRequest('test', 'api'); const { res, next } = getMockRes(); - await expect(proxy.createRequestHandler()(req, res, next)).rejects.toThrow( - NotFoundError, - ); + await expect( + proxy.createRequestHandler({ permissionApi })(req, res, next), + ).rejects.toThrow(NotFoundError); }); it('should pass the exact response from Kubernetes', async () => { @@ -100,7 +112,13 @@ describe('KubernetesProxy', () => { authProvider: 'serviceAccount', }, ] as ClusterDetails[]); - const app = express().use('/mountpath', proxy.createRequestHandler()); + const app = express().use( + '/mountpath', + proxy.createRequestHandler({ permissionApi }), + ); + permissionApi.authorize.mockReturnValue( + Promise.resolve([{ result: AuthorizeResult.ALLOW }]), + ); const requestPromise = request(app) .get('/mountpath/api') .set(HEADER_KUBERNETES_CLUSTER, 'cluster1'); diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts index 178105ef79..49a381b75a 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts @@ -18,9 +18,16 @@ import { ErrorResponseBody, ForwardedError, InputError, + NotAllowedError, NotFoundError, serializeError, } from '@backstage/errors'; +import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; +import { kubernetesProxyPermission } from '@backstage/plugin-kubernetes-common'; +import { + PermissionEvaluator, + AuthorizeResult, +} from '@backstage/plugin-permission-common'; import { bufferFromFileOrString } from '@kubernetes/client-node'; import type { Request, RequestHandler } from 'express'; import { createProxyMiddleware } from 'http-proxy-middleware'; @@ -34,7 +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. + * @public + */ +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. @@ -49,8 +73,29 @@ export class KubernetesProxy { private readonly clusterSupplier: KubernetesClustersSupplier, ) {} - public createRequestHandler(): RequestHandler { + public createRequestHandler( + options: KubernetesProxyCreateRequestHandlerOptions, + ): RequestHandler { + const { permissionApi } = options; return async (req, res, next) => { + const token = getBearerTokenFromAuthorizationHeader( + req.header('authorization'), + ); + + const authorizeResponse = ( + await permissionApi.authorize( + [{ permission: kubernetesProxyPermission }], + { + token, + }, + ) + )[0]; + + if (authorizeResponse.result === AuthorizeResult.DENY) { + res.status(403).json({ error: new NotAllowedError('Unauthorized') }); + return; + } + const middleware = await this.getMiddleware(req); middleware(req, res, next); }; @@ -104,6 +149,11 @@ export class KubernetesProxy { res.status(500).json(body); }, + onProxyReq: (proxyReq, req) => { + // 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); + }, }); this.middlewareForClusterName.set(originalCluster.name, middleware); 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-backend/src/service/router.ts b/plugins/kubernetes-backend/src/service/router.ts index 341956553e..f7e58f535c 100644 --- a/plugins/kubernetes-backend/src/service/router.ts +++ b/plugins/kubernetes-backend/src/service/router.ts @@ -21,6 +21,7 @@ import express from 'express'; import { KubernetesBuilder } from './KubernetesBuilder'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { CatalogApi } from '@backstage/catalog-client'; +import { PermissionEvaluator } from '@backstage/plugin-permission-common'; /** * @@ -32,6 +33,7 @@ export interface RouterOptions { catalogApi: CatalogApi; clusterSupplier?: KubernetesClustersSupplier; discovery: PluginEndpointDiscovery; + permissions: PermissionEvaluator; } /** diff --git a/plugins/kubernetes-backend/src/service/standaloneApplication.ts b/plugins/kubernetes-backend/src/service/standaloneApplication.ts index 8234f89d48..e3afb5a385 100644 --- a/plugins/kubernetes-backend/src/service/standaloneApplication.ts +++ b/plugins/kubernetes-backend/src/service/standaloneApplication.ts @@ -28,6 +28,7 @@ import { Logger } from 'winston'; import { createRouter } from './router'; import { ConfigReader } from '@backstage/config'; import { CatalogClient } from '@backstage/catalog-client'; +import { PermissionEvaluator } from '@backstage/plugin-permission-common'; export interface ApplicationOptions { enableCors: boolean; @@ -47,6 +48,8 @@ export async function createStandaloneApplication( discoveryApi: SingleHostDiscovery.fromConfig(config), }); + const permissions = {} as PermissionEvaluator; + app.use(helmet()); if (enableCors) { app.use(cors()); @@ -54,7 +57,10 @@ export async function createStandaloneApplication( app.use(compression()); app.use(express.json()); app.use(requestLoggingHandler()); - app.use('/', await createRouter({ logger, config, discovery, catalogApi })); + app.use( + '/', + await createRouter({ logger, config, discovery, catalogApi, permissions }), + ); app.use(notFoundHandler()); app.use(errorHandler()); diff --git a/plugins/kubernetes-common/api-report.md b/plugins/kubernetes-common/api-report.md index fb41895f1a..6c88074e50 100644 --- a/plugins/kubernetes-common/api-report.md +++ b/plugins/kubernetes-common/api-report.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { BasicPermission } from '@backstage/plugin-permission-common'; import { Entity } from '@backstage/catalog-model'; import type { JsonObject } from '@backstage/types'; import { PodStatus } from '@kubernetes/client-node'; @@ -217,6 +218,12 @@ export type KubernetesErrorTypes = // @public (undocumented) export type KubernetesFetchError = StatusError | RawFetchError; +// @public +export const kubernetesPermissions: BasicPermission[]; + +// @public +export const kubernetesProxyPermission: BasicPermission; + // @public (undocumented) export interface KubernetesRequestAuth { // (undocumented) diff --git a/plugins/kubernetes-common/package.json b/plugins/kubernetes-common/package.json index 2d83db84d4..50c7571e72 100644 --- a/plugins/kubernetes-common/package.json +++ b/plugins/kubernetes-common/package.json @@ -39,6 +39,7 @@ }, "dependencies": { "@backstage/catalog-model": "workspace:^", + "@backstage/plugin-permission-common": "workspace:^", "@kubernetes/client-node": "0.18.1" }, "devDependencies": { diff --git a/plugins/kubernetes-common/src/index.ts b/plugins/kubernetes-common/src/index.ts index ade73e4f79..d5cf8dcf61 100644 --- a/plugins/kubernetes-common/src/index.ts +++ b/plugins/kubernetes-common/src/index.ts @@ -22,3 +22,7 @@ export * from './types'; export * from './catalog-entity-constants'; +export { + kubernetesProxyPermission, + kubernetesPermissions, +} from './permissions'; diff --git a/plugins/kubernetes-common/src/permissions.ts b/plugins/kubernetes-common/src/permissions.ts new file mode 100644 index 0000000000..d3ccbe8171 --- /dev/null +++ b/plugins/kubernetes-common/src/permissions.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createPermission } from '@backstage/plugin-permission-common'; + +/** This permission is used to check access to the proxy endpoint + * @public + */ +export const kubernetesProxyPermission = createPermission({ + name: 'kubernetes.proxy', + attributes: {}, +}); + +/** + * List of all Kubernetes permissions. + * @public + */ +export const kubernetesPermissions = [kubernetesProxyPermission]; diff --git a/yarn.lock b/yarn.lock index d8a50af792..542918e285 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6763,6 +6763,8 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" "@backstage/plugin-kubernetes-common": "workspace:^" + "@backstage/plugin-permission-common": "workspace:^" + "@backstage/plugin-permission-node": "workspace:^" "@google-cloud/container": ^4.0.0 "@jest-mock/express": ^2.0.1 "@kubernetes/client-node": 0.18.1 @@ -6799,6 +6801,7 @@ __metadata: dependencies: "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" + "@backstage/plugin-permission-common": "workspace:^" "@kubernetes/client-node": 0.18.1 languageName: unknown linkType: soft