From 63ad078d638592345cd7aa28e9cf5d19c49e1f44 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Thu, 12 Jan 2023 12:41:54 -0500 Subject: [PATCH] Add authentication header requirments Signed-off-by: Sebastian Malton --- .../authorization-review.injectable.ts | 53 +++++++--------- .../cluster/list-namespaces.injectable.ts | 30 ++++++---- .../cluster/make-api-client.injectable.ts | 37 ++++++++++++ ...t-namespace-list-permissions.injectable.ts | 4 +- src/common/fetch/lens-fetch.injectable.ts | 13 +++- src/common/k8s-api/api-base.injectable.ts | 4 ++ ...te-kube-json-api-for-cluster.injectable.ts | 4 ++ .../create-kube-json-api.injectable.ts | 4 +- .../common/header-value.injectable.ts | 13 ++++ .../common/{channel.ts => vars.ts} | 2 + .../main/channel-handler.injectable.ts | 2 +- .../renderer/request-header.injectable.ts | 2 +- src/main/__test__/cluster.test.ts | 4 +- src/main/context-handler/context-handler.ts | 4 +- .../create-context-handler.injectable.ts | 2 + .../create-cluster.injectable.ts | 4 +- .../handle-proxy-upgrade.injectable.ts | 60 +++++++++++++++++++ .../create-handler-for-route.injectable.ts | 16 +++++ src/main/router/route.ts | 18 ++++-- .../files/static-file-route.injectable.ts | 1 + .../get-service-account-route.injectable.ts | 7 ++- .../node-shell-session/node-shell-session.ts | 8 ++- .../node-shell-session/open.injectable.ts | 2 + 23 files changed, 228 insertions(+), 66 deletions(-) create mode 100644 src/common/cluster/make-api-client.injectable.ts create mode 100644 src/features/auth-header/common/header-value.injectable.ts rename src/features/auth-header/common/{channel.ts => vars.ts} (86%) create mode 100644 src/main/lens-proxy/handle-proxy-upgrade.injectable.ts diff --git a/src/common/cluster/authorization-review.injectable.ts b/src/common/cluster/authorization-review.injectable.ts index 4c9b83330d..fe0bad62eb 100644 --- a/src/common/cluster/authorization-review.injectable.ts +++ b/src/common/cluster/authorization-review.injectable.ts @@ -6,8 +6,8 @@ import type { KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"; import { AuthorizationV1Api } from "@kubernetes/client-node"; import { getInjectable } from "@ogre-tools/injectable"; -import type { Logger } from "../logger"; import loggerInjectable from "../logger.injectable"; +import makeApiClientInjectable from "./make-api-client.injectable"; /** * Requests the permissions for actions on the kube cluster @@ -19,40 +19,33 @@ export type CanI = (resourceAttributes: V1ResourceAttributes) => Promise CanI; - -interface Dependencies { - logger: Logger; -} - -const authorizationReview = ({ logger }: Dependencies): AuthorizationReview => { - return (proxyConfig) => { - const api = proxyConfig.makeApiClient(AuthorizationV1Api); - - return async (resourceAttributes: V1ResourceAttributes): Promise => { - try { - const { body } = await api.createSelfSubjectAccessReview({ - apiVersion: "authorization.k8s.io/v1", - kind: "SelfSubjectAccessReview", - spec: { resourceAttributes }, - }); - - return body.status?.allowed ?? false; - } catch (error) { - logger.error(`[AUTHORIZATION-REVIEW]: failed to create access review: ${error}`, { resourceAttributes }); - - return false; - } - }; - }; -}; +export type AuthorizationReview = (proxyConfig: KubeConfig) => CanI; const authorizationReviewInjectable = getInjectable({ id: "authorization-review", - instantiate: (di) => { + instantiate: (di): AuthorizationReview => { const logger = di.inject(loggerInjectable); + const makeApiClient = di.inject(makeApiClientInjectable); - return authorizationReview({ logger }); + return (proxyConfig) => { + const api = makeApiClient(proxyConfig, AuthorizationV1Api); + + return async (resourceAttributes: V1ResourceAttributes): Promise => { + try { + const { body } = await api.createSelfSubjectAccessReview({ + apiVersion: "authorization.k8s.io/v1", + kind: "SelfSubjectAccessReview", + spec: { resourceAttributes }, + }); + + return body.status?.allowed ?? false; + } catch (error) { + logger.error(`[AUTHORIZATION-REVIEW]: failed to create access review: ${error}`, { resourceAttributes }); + + return false; + } + }; + }; }, }); diff --git a/src/common/cluster/list-namespaces.injectable.ts b/src/common/cluster/list-namespaces.injectable.ts index 468ff3ac2e..b5d549ea4c 100644 --- a/src/common/cluster/list-namespaces.injectable.ts +++ b/src/common/cluster/list-namespaces.injectable.ts @@ -6,24 +6,28 @@ import type { KubeConfig } from "@kubernetes/client-node"; import { CoreV1Api } from "@kubernetes/client-node"; import { getInjectable } from "@ogre-tools/injectable"; import { isDefined } from "../utils"; +import makeApiClientInjectable from "./make-api-client.injectable"; export type ListNamespaces = () => Promise; +export type ListNamespacesFor = (config: KubeConfig) => ListNamespaces; -export function listNamespaces(config: KubeConfig): ListNamespaces { - const coreApi = config.makeApiClient(CoreV1Api); +const listNamespacesForInjectable = getInjectable({ + id: "list-namespaces-for", + instantiate: (di): ListNamespacesFor => { + const makeApiClient = di.inject(makeApiClientInjectable); - return async () => { - const { body: { items }} = await coreApi.listNamespace(); + return (config) => { + const coreApi = makeApiClient(config, CoreV1Api); - return items - .map(ns => ns.metadata?.name) - .filter(isDefined); - }; -} + return async () => { + const { body: { items }} = await coreApi.listNamespace(); -const listNamespacesInjectable = getInjectable({ - id: "list-namespaces", - instantiate: () => listNamespaces, + return items + .map(ns => ns.metadata?.name) + .filter(isDefined); + }; + }; + }, }); -export default listNamespacesInjectable; +export default listNamespacesForInjectable; diff --git a/src/common/cluster/make-api-client.injectable.ts b/src/common/cluster/make-api-client.injectable.ts new file mode 100644 index 0000000000..7521b4c36d --- /dev/null +++ b/src/common/cluster/make-api-client.injectable.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { Authentication, Interceptor, KubeConfig } from "@kubernetes/client-node"; +import { getInjectable } from "@ogre-tools/injectable"; +import authHeaderValueInjectable from "../../features/auth-header/common/header-value.injectable"; +import { lensAuthHeaderName } from "../../features/auth-header/common/vars"; + +export interface ApiType { + defaultHeaders: any; + setDefaultAuthentication(config: Authentication): void; + addInterceptor(interceptor: Interceptor): void; +} + +export type MakeApiClient = (config: KubeConfig, apiClientType: new (server: string) => T) => T; + +const makeApiClientInjectable = getInjectable({ + id: "make-api-client", + instantiate: (di): MakeApiClient => { + const authHeaderValue = di.inject(authHeaderValueInjectable); + + return (config, apiClientType) => { + const api = config.makeApiClient(apiClientType); + + api.addInterceptor((opts) => { + opts.headers ??= {}; + opts.headers[lensAuthHeaderName] = authHeaderValue; + }); + + return api; + }; + }, +}); + +export default makeApiClientInjectable; diff --git a/src/common/cluster/request-namespace-list-permissions.injectable.ts b/src/common/cluster/request-namespace-list-permissions.injectable.ts index 62d2477e42..d436059be9 100644 --- a/src/common/cluster/request-namespace-list-permissions.injectable.ts +++ b/src/common/cluster/request-namespace-list-permissions.injectable.ts @@ -8,6 +8,7 @@ import { AuthorizationV1Api } from "@kubernetes/client-node"; import { getInjectable } from "@ogre-tools/injectable"; import loggerInjectable from "../logger.injectable"; import type { KubeApiResource } from "../rbac"; +import makeApiClientInjectable from "./make-api-client.injectable"; export type CanListResource = (resource: KubeApiResource) => boolean; @@ -26,9 +27,10 @@ const requestNamespaceListPermissionsForInjectable = getInjectable({ id: "request-namespace-list-permissions-for", instantiate: (di): RequestNamespaceListPermissionsFor => { const logger = di.inject(loggerInjectable); + const makeApiClient = di.inject(makeApiClientInjectable); return (proxyConfig) => { - const api = proxyConfig.makeApiClient(AuthorizationV1Api); + const api = makeApiClient(proxyConfig, AuthorizationV1Api); return async (namespace) => { try { diff --git a/src/common/fetch/lens-fetch.injectable.ts b/src/common/fetch/lens-fetch.injectable.ts index a90818e6cb..150936fd41 100644 --- a/src/common/fetch/lens-fetch.injectable.ts +++ b/src/common/fetch/lens-fetch.injectable.ts @@ -5,6 +5,8 @@ import { getInjectable } from "@ogre-tools/injectable"; import { Agent } from "https"; import type { RequestInit, Response } from "node-fetch"; +import authHeaderValueInjectable from "../../features/auth-header/common/header-value.injectable"; +import { lensAuthHeaderName } from "../../features/auth-header/common/vars"; import lensProxyPortInjectable from "../../main/lens-proxy/lens-proxy-port.injectable"; import lensProxyCertificateInjectable from "../certificate/lens-proxy-certificate.injectable"; import nodeFetchModuleInjectable from "./fetch-module.injectable"; @@ -16,14 +18,21 @@ export type LensFetch = (pathnameAndQuery: string, init?: LensRequestInit) => Pr const lensFetchInjectable = getInjectable({ id: "lens-fetch", instantiate: (di): LensFetch => { - const { default: fetch } = di.inject(nodeFetchModuleInjectable); + const { default: fetch, Headers } = di.inject(nodeFetchModuleInjectable); const lensProxyPort = di.inject(lensProxyPortInjectable); const lensProxyCertificate = di.inject(lensProxyCertificateInjectable); + const authHeaderValue = di.inject(authHeaderValueInjectable); - return async (pathnameAndQuery, init = {}) => { + return async (pathnameAndQuery, { + headers: _headers, + ...init + } = {}) => { const agent = new Agent({ ca: lensProxyCertificate.get().cert, }); + const headers = new Headers(_headers); + + headers.set(lensAuthHeaderName, authHeaderValue); return fetch(`https://127.0.0.1:${lensProxyPort.get()}${pathnameAndQuery}`, { ...init, diff --git a/src/common/k8s-api/api-base.injectable.ts b/src/common/k8s-api/api-base.injectable.ts index b340882672..54ecba50b3 100644 --- a/src/common/k8s-api/api-base.injectable.ts +++ b/src/common/k8s-api/api-base.injectable.ts @@ -3,6 +3,8 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; +import authHeaderValueInjectable from "../../features/auth-header/common/header-value.injectable"; +import { lensAuthHeaderName } from "../../features/auth-header/common/vars"; import { apiPrefix } from "../vars"; import isDebuggingInjectable from "../vars/is-debugging.injectable"; import isDevelopmentInjectable from "../vars/is-development.injectable"; @@ -17,6 +19,7 @@ const apiBaseInjectable = getInjectable({ const isDevelopment = di.inject(isDevelopmentInjectable); const serverAddress = di.inject(apiBaseServerAddressInjectionToken); const hostHeaderValue = di.inject(apiBaseHostHeaderInjectionToken); + const authHeaderValue = di.inject(authHeaderValueInjectable); return createJsonApi({ serverAddress, @@ -25,6 +28,7 @@ const apiBaseInjectable = getInjectable({ }, { headers: { "Host": hostHeaderValue, + [lensAuthHeaderName]: authHeaderValue, }, }); }, diff --git a/src/common/k8s-api/create-kube-json-api-for-cluster.injectable.ts b/src/common/k8s-api/create-kube-json-api-for-cluster.injectable.ts index 799b0bf963..b3dead65ba 100644 --- a/src/common/k8s-api/create-kube-json-api-for-cluster.injectable.ts +++ b/src/common/k8s-api/create-kube-json-api-for-cluster.injectable.ts @@ -3,6 +3,8 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; +import authHeaderValueInjectable from "../../features/auth-header/common/header-value.injectable"; +import { lensAuthHeaderName } from "../../features/auth-header/common/vars"; import { apiKubePrefix } from "../vars"; import isDebuggingInjectable from "../vars/is-debugging.injectable"; import { apiBaseHostHeaderInjectionToken, apiBaseServerAddressInjectionToken } from "./api-base-configs"; @@ -16,6 +18,7 @@ const createKubeJsonApiForClusterInjectable = getInjectable({ instantiate: (di): CreateKubeJsonApiForCluster => { const createKubeJsonApi = di.inject(createKubeJsonApiInjectable); const isDebugging = di.inject(isDebuggingInjectable); + const authHeaderValue = di.inject(authHeaderValueInjectable); return (clusterId) => createKubeJsonApi( { @@ -26,6 +29,7 @@ const createKubeJsonApiForClusterInjectable = getInjectable({ { headers: { "Host": `${clusterId}.${di.inject(apiBaseHostHeaderInjectionToken)}`, + [lensAuthHeaderName]: authHeaderValue, }, }, ); diff --git a/src/common/k8s-api/create-kube-json-api.injectable.ts b/src/common/k8s-api/create-kube-json-api.injectable.ts index f7b5a152ee..011ae8f8fb 100644 --- a/src/common/k8s-api/create-kube-json-api.injectable.ts +++ b/src/common/k8s-api/create-kube-json-api.injectable.ts @@ -28,13 +28,13 @@ const createKubeJsonApiInjectable = getInjectable({ const agent = new Agent({ ca: lensProxyCert.get().cert, }); - + return { agent, }; }; } - + return new KubeJsonApi(dependencies, config, reqInit); }; }, diff --git a/src/features/auth-header/common/header-value.injectable.ts b/src/features/auth-header/common/header-value.injectable.ts new file mode 100644 index 0000000000..686e2bc8df --- /dev/null +++ b/src/features/auth-header/common/header-value.injectable.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import authHeaderStateInjectable from "./header-state.injectable"; + +const authHeaderValueInjectable = getInjectable({ + id: "auth-header-value", + instantiate: (di) => `Bearer ${di.inject(authHeaderStateInjectable).get()}`, +}); + +export default authHeaderValueInjectable; diff --git a/src/features/auth-header/common/channel.ts b/src/features/auth-header/common/vars.ts similarity index 86% rename from src/features/auth-header/common/channel.ts rename to src/features/auth-header/common/vars.ts index 030750ea0f..5a863a7f11 100644 --- a/src/features/auth-header/common/channel.ts +++ b/src/features/auth-header/common/vars.ts @@ -6,3 +6,5 @@ import { getRequestChannel } from "../../../common/utils/channel/get-request-channel"; export const authHeaderChannel = getRequestChannel("auth-header-value"); + +export const lensAuthHeaderName = "Authorization"; diff --git a/src/features/auth-header/main/channel-handler.injectable.ts b/src/features/auth-header/main/channel-handler.injectable.ts index 29ed0dd069..885aef7d9a 100644 --- a/src/features/auth-header/main/channel-handler.injectable.ts +++ b/src/features/auth-header/main/channel-handler.injectable.ts @@ -4,7 +4,7 @@ */ import { getRequestChannelListenerInjectable } from "../../../main/utils/channel/channel-listeners/listener-tokens"; -import { authHeaderChannel } from "../common/channel"; +import { authHeaderChannel } from "../common/vars"; import authHeaderStateInjectable from "../common/header-state.injectable"; const authHeaderRequestListenerInjectable = getRequestChannelListenerInjectable({ diff --git a/src/features/auth-header/renderer/request-header.injectable.ts b/src/features/auth-header/renderer/request-header.injectable.ts index 8805928593..c162fecac3 100644 --- a/src/features/auth-header/renderer/request-header.injectable.ts +++ b/src/features/auth-header/renderer/request-header.injectable.ts @@ -5,7 +5,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import requestFromChannelInjectable from "../../../renderer/utils/channel/request-from-channel.injectable"; -import { authHeaderChannel } from "../common/channel"; +import { authHeaderChannel } from "../common/vars"; const requestAuthHeaderValueInjectable = getInjectable({ id: "request-auth-header-value", diff --git a/src/main/__test__/cluster.test.ts b/src/main/__test__/cluster.test.ts index be19790a23..849ea21597 100644 --- a/src/main/__test__/cluster.test.ts +++ b/src/main/__test__/cluster.test.ts @@ -10,7 +10,7 @@ import type { CreateCluster } from "../../common/cluster/create-cluster-injectio import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token"; import authorizationReviewInjectable from "../../common/cluster/authorization-review.injectable"; import requestNamespaceListPermissionsForInjectable from "../../common/cluster/request-namespace-list-permissions.injectable"; -import listNamespacesInjectable from "../../common/cluster/list-namespaces.injectable"; +import listNamespacesForInjectable from "../../common/cluster/list-namespaces.injectable"; import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable"; import type { ClusterContextHandler } from "../context-handler/context-handler"; import { parse } from "url"; @@ -42,7 +42,7 @@ describe("create clusters", () => { di.override(broadcastMessageInjectable, () => async () => {}); di.override(authorizationReviewInjectable, () => () => () => Promise.resolve(true)); di.override(requestNamespaceListPermissionsForInjectable, () => () => async () => () => true); - di.override(listNamespacesInjectable, () => () => () => Promise.resolve([ "default" ])); + di.override(listNamespacesForInjectable, () => () => () => Promise.resolve([ "default" ])); di.override(createContextHandlerInjectable, () => (cluster) => ({ restartServer: jest.fn(), stopServer: jest.fn(), diff --git a/src/main/context-handler/context-handler.ts b/src/main/context-handler/context-handler.ts index 7d40bfcd00..583c18acc1 100644 --- a/src/main/context-handler/context-handler.ts +++ b/src/main/context-handler/context-handler.ts @@ -15,6 +15,7 @@ import type { CreateKubeAuthProxy } from "../kube-auth-proxy/create-kube-auth-pr import type { GetPrometheusProviderByKind } from "../prometheus/get-by-kind.injectable"; import type { IComputedValue } from "mobx"; import type { Logger } from "../../common/logger"; +import type { MakeApiClient } from "../../common/cluster/make-api-client.injectable"; export interface PrometheusDetails { prometheusPath: string; @@ -31,6 +32,7 @@ interface PrometheusServicePreferences { export interface ContextHandlerDependencies { createKubeAuthProxy: CreateKubeAuthProxy; getPrometheusProviderByKind: GetPrometheusProviderByKind; + makeApiClient: MakeApiClient; readonly authProxyCa: string; readonly prometheusProviders: IComputedValue; readonly logger: Logger; @@ -110,7 +112,7 @@ export class ContextHandler implements ClusterContextHandler { const providers = this.listPotentialProviders(); const proxyConfig = await this.cluster.getProxyKubeconfig(); - const apiClient = proxyConfig.makeApiClient(CoreV1Api); + const apiClient = this.dependencies.makeApiClient(proxyConfig, CoreV1Api); const potentialServices = await Promise.allSettled( providers.map(provider => provider.getPrometheusService(apiClient)), ); diff --git a/src/main/context-handler/create-context-handler.injectable.ts b/src/main/context-handler/create-context-handler.injectable.ts index 1567721ac4..57ca9bd38b 100644 --- a/src/main/context-handler/create-context-handler.injectable.ts +++ b/src/main/context-handler/create-context-handler.injectable.ts @@ -12,6 +12,7 @@ import URLParse from "url-parse"; import getPrometheusProviderByKindInjectable from "../prometheus/get-by-kind.injectable"; import prometheusProvidersInjectable from "../prometheus/providers.injectable"; import loggerInjectable from "../../common/logger.injectable"; +import makeApiClientInjectable from "../../common/cluster/make-api-client.injectable"; const createContextHandlerInjectable = getInjectable({ id: "create-context-handler", @@ -22,6 +23,7 @@ const createContextHandlerInjectable = getInjectable({ getPrometheusProviderByKind: di.inject(getPrometheusProviderByKindInjectable), prometheusProviders: di.inject(prometheusProvidersInjectable), logger: di.inject(loggerInjectable), + makeApiClient: di.inject(makeApiClientInjectable), }; return (cluster: Cluster): ClusterContextHandler => { diff --git a/src/main/create-cluster/create-cluster.injectable.ts b/src/main/create-cluster/create-cluster.injectable.ts index 79eee5a151..a494ba5d60 100644 --- a/src/main/create-cluster/create-cluster.injectable.ts +++ b/src/main/create-cluster/create-cluster.injectable.ts @@ -11,7 +11,7 @@ import createKubectlInjectable from "../kubectl/create-kubectl.injectable"; import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable"; import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token"; import authorizationReviewInjectable from "../../common/cluster/authorization-review.injectable"; -import listNamespacesInjectable from "../../common/cluster/list-namespaces.injectable"; +import listNamespacesForInjectable from "../../common/cluster/list-namespaces.injectable"; import createListApiResourcesInjectable from "../cluster/request-api-resources.injectable"; import loggerInjectable from "../../common/logger.injectable"; import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable"; @@ -34,7 +34,7 @@ const createClusterInjectable = getInjectable({ createAuthorizationReview: di.inject(authorizationReviewInjectable), requestNamespaceListPermissionsFor: di.inject(requestNamespaceListPermissionsForInjectable), requestApiResources: di.inject(createListApiResourcesInjectable), - createListNamespaces: di.inject(listNamespacesInjectable), + createListNamespaces: di.inject(listNamespacesForInjectable), broadcastMessage: di.inject(broadcastMessageInjectable), loadConfigfromFile: di.inject(loadConfigfromFileInjectable), detectClusterMetadata: di.inject(detectClusterMetadataInjectable), diff --git a/src/main/lens-proxy/handle-proxy-upgrade.injectable.ts b/src/main/lens-proxy/handle-proxy-upgrade.injectable.ts new file mode 100644 index 0000000000..0ba96c8958 --- /dev/null +++ b/src/main/lens-proxy/handle-proxy-upgrade.injectable.ts @@ -0,0 +1,60 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import type { IncomingMessage } from "http"; +import type { Socket } from "net"; +import type { SetRequired } from "type-fest"; +import loggerInjectable from "../../common/logger.injectable"; +import { apiPrefix, apiKubePrefix } from "../../common/vars"; +import authHeaderStateInjectable from "../../features/auth-header/common/header-state.injectable"; +import { lensAuthHeaderName } from "../../features/auth-header/common/vars"; +import getClusterForRequestInjectable from "./get-cluster-for-request.injectable"; +import { kubeApiUpgradeRequest } from "./proxy-functions"; +import shellApiRequestInjectable from "./proxy-functions/shell-api-request.injectable"; + +const handleProxyUpgradeRequestInjectable = getInjectable({ + id: "handle-proxy-upgrade-request", + instantiate: (di) => { + const getClusterForRequest = di.inject(getClusterForRequestInjectable); + const shellApiRequest = di.inject(shellApiRequestInjectable); + const logger = di.inject(loggerInjectable); + const authHeaderValue = `Bearer ${di.inject(authHeaderStateInjectable).get()}`; + + return (req: SetRequired, socket: Socket, head: Buffer) => { + const cluster = getClusterForRequest(req); + const url = new URL(req.url, "https://localhost"); + + if (url.searchParams.get(lensAuthHeaderName) !== authHeaderValue) { + logger.warn(`[LENS-PROXY]: Request from url=${req.url} missing authentication`); + socket.destroy(); + + return; + } + + if (!cluster) { + logger.error(`[LENS-PROXY]: Could not find cluster for upgrade request from url=${req.url}`); + socket.destroy(); + + return; + } + + (async () => { + try { + if (url.pathname === apiPrefix) { + await shellApiRequest({ req, socket, cluster, head }); + } else if (url.pathname.startsWith(`${apiKubePrefix}/`)) { + await kubeApiUpgradeRequest({ req, socket, cluster, head }); + } else { + logger.warn(`[LENS-PROXY]: unknown upgrade request, url=${req.url}`); + } + } catch (error) { + logger.error("[LENS-PROXY]: failed to handle proxy upgrade", error); + } + })(); + }; + }, +}); + +export default handleProxyUpgradeRequestInjectable; diff --git a/src/main/router/create-handler-for-route.injectable.ts b/src/main/router/create-handler-for-route.injectable.ts index d2680152ff..f112a1b41b 100644 --- a/src/main/router/create-handler-for-route.injectable.ts +++ b/src/main/router/create-handler-for-route.injectable.ts @@ -5,6 +5,8 @@ import { getInjectable } from "@ogre-tools/injectable"; import type { ServerResponse } from "http"; import loggerInjectable from "../../common/logger.injectable"; +import authHeaderValueInjectable from "../../features/auth-header/common/header-value.injectable"; +import { lensAuthHeaderName } from "../../features/auth-header/common/vars"; import type { LensApiRequest, Route } from "./route"; import { contentTypes } from "./router-content-types"; import { writeServerResponseFor } from "./write-server-response"; @@ -16,10 +18,24 @@ const createHandlerForRouteInjectable = getInjectable({ id: "create-handler-for-route", instantiate: (di): CreateHandlerForRoute => { const logger = di.inject(loggerInjectable); + const authHeaderValue = di.inject(authHeaderValueInjectable); return (route) => async (request, response) => { const writeServerResponse = writeServerResponseFor(response); + if (route.requireAuthentication) { + const authHeader = request.getHeader(lensAuthHeaderName); + + if (authHeader !== authHeaderValue) { + writeServerResponse(contentTypes.txt.resultMapper({ + statusCode: 401, + response: "Missing authorization", + })); + + return; + } + } + try { const result = await route.handler(request); diff --git a/src/main/router/route.ts b/src/main/router/route.ts index ed4bcb21e9..4005ed6e85 100644 --- a/src/main/router/route.ts +++ b/src/main/router/route.ts @@ -66,6 +66,7 @@ export interface RouteHandler{ export interface BaseRoutePaths { path: Path; method: "get" | "post" | "put" | "patch" | "delete"; + requireAuthentication?: boolean; } export interface PayloadValidator { @@ -78,18 +79,21 @@ export interface ValidatorBaseRoutePaths extends B export interface Route extends BaseRoutePaths { handler: RouteHandler; + readonly requireAuthentication: boolean; } export interface BindHandler { (handler: RouteHandler): Route; } -export function route(parts: BaseRoutePaths): BindHandler { - return (handler) => ({ +export const route = ({ + requireAuthentication = true, + ...parts +}: BaseRoutePaths): BindHandler => (handler) => ({ ...parts, handler, + requireAuthentication, }); -} export interface ClusterRouteHandler{ (request: ClusterLensApiRequest): RouteResponse | Promise>; @@ -99,8 +103,10 @@ export interface BindClusterHandler { (handler: ClusterRouteHandler): Route; } -export function clusterRoute(parts: BaseRoutePaths): BindClusterHandler { - return (handler) => ({ +export const clusterRoute = ({ + requireAuthentication = true, + ...parts +}: BaseRoutePaths): BindClusterHandler => (handler) => ({ ...parts, handler: ({ cluster, ...rest }) => { if (!cluster) { @@ -112,8 +118,8 @@ export function clusterRoute(parts: BaseRoutePaths): return handler({ cluster, ...rest }); }, + requireAuthentication, }); -} export interface ValidatedClusterLensApiRequest extends ClusterLensApiRequest { payload: Payload; diff --git a/src/main/routes/files/static-file-route.injectable.ts b/src/main/routes/files/static-file-route.injectable.ts index ec46b19d59..395b22fd32 100644 --- a/src/main/routes/files/static-file-route.injectable.ts +++ b/src/main/routes/files/static-file-route.injectable.ts @@ -17,6 +17,7 @@ const staticFileRouteInjectable = getRouteInjectable({ return route({ method: "get", path: `/{path*}`, + requireAuthentication: false, })( isDevelopment ? di.inject(devStaticFileRouteHandlerInjectable) diff --git a/src/main/routes/kubeconfig-route/get-service-account-route.injectable.ts b/src/main/routes/kubeconfig-route/get-service-account-route.injectable.ts index 34bd0d789c..c24aede4a8 100644 --- a/src/main/routes/kubeconfig-route/get-service-account-route.injectable.ts +++ b/src/main/routes/kubeconfig-route/get-service-account-route.injectable.ts @@ -10,15 +10,18 @@ import type { V1Secret } from "@kubernetes/client-node"; import { CoreV1Api } from "@kubernetes/client-node"; import { clusterRoute } from "../../router/route"; import { dump } from "js-yaml"; +import makeApiClientInjectable from "../../../common/cluster/make-api-client.injectable"; const getServiceAccountRouteInjectable = getRouteInjectable({ id: "get-service-account-route", - instantiate: () => clusterRoute({ + instantiate: (di) => clusterRoute({ method: "get", path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}`, })(async ({ params, cluster }) => { - const client = (await cluster.getProxyKubeconfig()).makeApiClient(CoreV1Api); + const makeApiClient = di.inject(makeApiClientInjectable); + const config = await cluster.getProxyKubeconfig(); + const client = makeApiClient(config, CoreV1Api); const secretList = await client.listNamespacedSecret(params.namespace); const secret = secretList.body.items.find(secret => { diff --git a/src/main/shell-session/node-shell-session/node-shell-session.ts b/src/main/shell-session/node-shell-session/node-shell-session.ts index 492c70d73d..ac4023debf 100644 --- a/src/main/shell-session/node-shell-session/node-shell-session.ts +++ b/src/main/shell-session/node-shell-session/node-shell-session.ts @@ -13,6 +13,7 @@ import { NodeApi } from "../../../common/k8s-api/endpoints"; import { TerminalChannels } from "../../../common/terminal/channels"; import type { CreateKubeJsonApiForCluster } from "../../../common/k8s-api/create-kube-json-api-for-cluster.injectable"; import type { CreateKubeApi } from "../../../common/k8s-api/create-kube-api.injectable"; +import type { MakeApiClient } from "../../../common/cluster/make-api-client.injectable"; export interface NodeShellSessionArgs extends ShellSessionArgs { nodeName: string; @@ -21,6 +22,7 @@ export interface NodeShellSessionArgs extends ShellSessionArgs { export interface NodeShellSessionDependencies extends ShellSessionDependencies { createKubeJsonApiForCluster: CreateKubeJsonApiForCluster; createKubeApi: CreateKubeApi; + makeApiClient: MakeApiClient; } export class NodeShellSession extends ShellSession { @@ -36,8 +38,8 @@ export class NodeShellSession extends ShellSession { } public async open() { - const kc = await this.cluster.getProxyKubeconfig(); - const coreApi = kc.makeApiClient(CoreV1Api); + const config = await this.cluster.getProxyKubeconfig(); + const coreApi = this.dependencies.makeApiClient(config, CoreV1Api); const shell = await this.kubectl.getPath(); const cleanup = once(() => { @@ -50,7 +52,7 @@ export class NodeShellSession extends ShellSession { try { await this.createNodeShellPod(coreApi); - await this.waitForRunningPod(kc); + await this.waitForRunningPod(config); } catch (error) { cleanup(); diff --git a/src/main/shell-session/node-shell-session/open.injectable.ts b/src/main/shell-session/node-shell-session/open.injectable.ts index cce3fa5f36..6798b41aec 100644 --- a/src/main/shell-session/node-shell-session/open.injectable.ts +++ b/src/main/shell-session/node-shell-session/open.injectable.ts @@ -20,6 +20,7 @@ import buildVersionInjectable from "../../vars/build-version/build-version.injec import emitAppEventInjectable from "../../../common/app-event-bus/emit-event.injectable"; import statInjectable from "../../../common/fs/stat.injectable"; import createKubeApiInjectable from "../../../common/k8s-api/create-kube-api.injectable"; +import makeApiClientInjectable from "../../../common/cluster/make-api-client.injectable"; export interface NodeShellSessionArgs { websocket: WebSocket; @@ -47,6 +48,7 @@ const openNodeShellSessionInjectable = getInjectable({ emitAppEvent: di.inject(emitAppEventInjectable), stat: di.inject(statInjectable), createKubeApi: di.inject(createKubeApiInjectable), + makeApiClient: di.inject(makeApiClientInjectable), }; return async (args) => {