1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Add authentication header requirments

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2023-01-12 12:41:54 -05:00
parent 4867286179
commit 63ad078d63
23 changed files with 228 additions and 66 deletions

View File

@ -6,8 +6,8 @@
import type { KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"; import type { KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node";
import { AuthorizationV1Api } from "@kubernetes/client-node"; import { AuthorizationV1Api } from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import type { Logger } from "../logger";
import loggerInjectable from "../logger.injectable"; import loggerInjectable from "../logger.injectable";
import makeApiClientInjectable from "./make-api-client.injectable";
/** /**
* Requests the permissions for actions on the kube cluster * Requests the permissions for actions on the kube cluster
@ -19,40 +19,33 @@ export type CanI = (resourceAttributes: V1ResourceAttributes) => Promise<boolean
/** /**
* @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster * @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster
*/ */
export type AuthorizationReview = (proxyConfig: KubeConfig) => CanI; export type AuthorizationReview = (proxyConfig: KubeConfig) => CanI;
interface Dependencies {
logger: Logger;
}
const authorizationReview = ({ logger }: Dependencies): AuthorizationReview => {
return (proxyConfig) => {
const api = proxyConfig.makeApiClient(AuthorizationV1Api);
return async (resourceAttributes: V1ResourceAttributes): Promise<boolean> => {
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;
}
};
};
};
const authorizationReviewInjectable = getInjectable({ const authorizationReviewInjectable = getInjectable({
id: "authorization-review", id: "authorization-review",
instantiate: (di) => { instantiate: (di): AuthorizationReview => {
const logger = di.inject(loggerInjectable); 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<boolean> => {
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;
}
};
};
}, },
}); });

View File

@ -6,24 +6,28 @@ import type { KubeConfig } from "@kubernetes/client-node";
import { CoreV1Api } from "@kubernetes/client-node"; import { CoreV1Api } from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { isDefined } from "../utils"; import { isDefined } from "../utils";
import makeApiClientInjectable from "./make-api-client.injectable";
export type ListNamespaces = () => Promise<string[]>; export type ListNamespaces = () => Promise<string[]>;
export type ListNamespacesFor = (config: KubeConfig) => ListNamespaces;
export function listNamespaces(config: KubeConfig): ListNamespaces { const listNamespacesForInjectable = getInjectable({
const coreApi = config.makeApiClient(CoreV1Api); id: "list-namespaces-for",
instantiate: (di): ListNamespacesFor => {
const makeApiClient = di.inject(makeApiClientInjectable);
return async () => { return (config) => {
const { body: { items }} = await coreApi.listNamespace(); const coreApi = makeApiClient(config, CoreV1Api);
return items return async () => {
.map(ns => ns.metadata?.name) const { body: { items }} = await coreApi.listNamespace();
.filter(isDefined);
};
}
const listNamespacesInjectable = getInjectable({ return items
id: "list-namespaces", .map(ns => ns.metadata?.name)
instantiate: () => listNamespaces, .filter(isDefined);
};
};
},
}); });
export default listNamespacesInjectable; export default listNamespacesForInjectable;

View File

@ -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 = <T extends ApiType>(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;

View File

@ -8,6 +8,7 @@ import { AuthorizationV1Api } from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import loggerInjectable from "../logger.injectable"; import loggerInjectable from "../logger.injectable";
import type { KubeApiResource } from "../rbac"; import type { KubeApiResource } from "../rbac";
import makeApiClientInjectable from "./make-api-client.injectable";
export type CanListResource = (resource: KubeApiResource) => boolean; export type CanListResource = (resource: KubeApiResource) => boolean;
@ -26,9 +27,10 @@ const requestNamespaceListPermissionsForInjectable = getInjectable({
id: "request-namespace-list-permissions-for", id: "request-namespace-list-permissions-for",
instantiate: (di): RequestNamespaceListPermissionsFor => { instantiate: (di): RequestNamespaceListPermissionsFor => {
const logger = di.inject(loggerInjectable); const logger = di.inject(loggerInjectable);
const makeApiClient = di.inject(makeApiClientInjectable);
return (proxyConfig) => { return (proxyConfig) => {
const api = proxyConfig.makeApiClient(AuthorizationV1Api); const api = makeApiClient(proxyConfig, AuthorizationV1Api);
return async (namespace) => { return async (namespace) => {
try { try {

View File

@ -5,6 +5,8 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { Agent } from "https"; import { Agent } from "https";
import type { RequestInit, Response } from "node-fetch"; 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 lensProxyPortInjectable from "../../main/lens-proxy/lens-proxy-port.injectable";
import lensProxyCertificateInjectable from "../certificate/lens-proxy-certificate.injectable"; import lensProxyCertificateInjectable from "../certificate/lens-proxy-certificate.injectable";
import nodeFetchModuleInjectable from "./fetch-module.injectable"; import nodeFetchModuleInjectable from "./fetch-module.injectable";
@ -16,14 +18,21 @@ export type LensFetch = (pathnameAndQuery: string, init?: LensRequestInit) => Pr
const lensFetchInjectable = getInjectable({ const lensFetchInjectable = getInjectable({
id: "lens-fetch", id: "lens-fetch",
instantiate: (di): LensFetch => { instantiate: (di): LensFetch => {
const { default: fetch } = di.inject(nodeFetchModuleInjectable); const { default: fetch, Headers } = di.inject(nodeFetchModuleInjectable);
const lensProxyPort = di.inject(lensProxyPortInjectable); const lensProxyPort = di.inject(lensProxyPortInjectable);
const lensProxyCertificate = di.inject(lensProxyCertificateInjectable); const lensProxyCertificate = di.inject(lensProxyCertificateInjectable);
const authHeaderValue = di.inject(authHeaderValueInjectable);
return async (pathnameAndQuery, init = {}) => { return async (pathnameAndQuery, {
headers: _headers,
...init
} = {}) => {
const agent = new Agent({ const agent = new Agent({
ca: lensProxyCertificate.get().cert, ca: lensProxyCertificate.get().cert,
}); });
const headers = new Headers(_headers);
headers.set(lensAuthHeaderName, authHeaderValue);
return fetch(`https://127.0.0.1:${lensProxyPort.get()}${pathnameAndQuery}`, { return fetch(`https://127.0.0.1:${lensProxyPort.get()}${pathnameAndQuery}`, {
...init, ...init,

View File

@ -3,6 +3,8 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; 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 { apiPrefix } from "../vars";
import isDebuggingInjectable from "../vars/is-debugging.injectable"; import isDebuggingInjectable from "../vars/is-debugging.injectable";
import isDevelopmentInjectable from "../vars/is-development.injectable"; import isDevelopmentInjectable from "../vars/is-development.injectable";
@ -17,6 +19,7 @@ const apiBaseInjectable = getInjectable({
const isDevelopment = di.inject(isDevelopmentInjectable); const isDevelopment = di.inject(isDevelopmentInjectable);
const serverAddress = di.inject(apiBaseServerAddressInjectionToken); const serverAddress = di.inject(apiBaseServerAddressInjectionToken);
const hostHeaderValue = di.inject(apiBaseHostHeaderInjectionToken); const hostHeaderValue = di.inject(apiBaseHostHeaderInjectionToken);
const authHeaderValue = di.inject(authHeaderValueInjectable);
return createJsonApi({ return createJsonApi({
serverAddress, serverAddress,
@ -25,6 +28,7 @@ const apiBaseInjectable = getInjectable({
}, { }, {
headers: { headers: {
"Host": hostHeaderValue, "Host": hostHeaderValue,
[lensAuthHeaderName]: authHeaderValue,
}, },
}); });
}, },

View File

@ -3,6 +3,8 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; 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 { apiKubePrefix } from "../vars";
import isDebuggingInjectable from "../vars/is-debugging.injectable"; import isDebuggingInjectable from "../vars/is-debugging.injectable";
import { apiBaseHostHeaderInjectionToken, apiBaseServerAddressInjectionToken } from "./api-base-configs"; import { apiBaseHostHeaderInjectionToken, apiBaseServerAddressInjectionToken } from "./api-base-configs";
@ -16,6 +18,7 @@ const createKubeJsonApiForClusterInjectable = getInjectable({
instantiate: (di): CreateKubeJsonApiForCluster => { instantiate: (di): CreateKubeJsonApiForCluster => {
const createKubeJsonApi = di.inject(createKubeJsonApiInjectable); const createKubeJsonApi = di.inject(createKubeJsonApiInjectable);
const isDebugging = di.inject(isDebuggingInjectable); const isDebugging = di.inject(isDebuggingInjectable);
const authHeaderValue = di.inject(authHeaderValueInjectable);
return (clusterId) => createKubeJsonApi( return (clusterId) => createKubeJsonApi(
{ {
@ -26,6 +29,7 @@ const createKubeJsonApiForClusterInjectable = getInjectable({
{ {
headers: { headers: {
"Host": `${clusterId}.${di.inject(apiBaseHostHeaderInjectionToken)}`, "Host": `${clusterId}.${di.inject(apiBaseHostHeaderInjectionToken)}`,
[lensAuthHeaderName]: authHeaderValue,
}, },
}, },
); );

View File

@ -28,13 +28,13 @@ const createKubeJsonApiInjectable = getInjectable({
const agent = new Agent({ const agent = new Agent({
ca: lensProxyCert.get().cert, ca: lensProxyCert.get().cert,
}); });
return { return {
agent, agent,
}; };
}; };
} }
return new KubeJsonApi(dependencies, config, reqInit); return new KubeJsonApi(dependencies, config, reqInit);
}; };
}, },

View File

@ -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;

View File

@ -6,3 +6,5 @@
import { getRequestChannel } from "../../../common/utils/channel/get-request-channel"; import { getRequestChannel } from "../../../common/utils/channel/get-request-channel";
export const authHeaderChannel = getRequestChannel<void, string>("auth-header-value"); export const authHeaderChannel = getRequestChannel<void, string>("auth-header-value");
export const lensAuthHeaderName = "Authorization";

View File

@ -4,7 +4,7 @@
*/ */
import { getRequestChannelListenerInjectable } from "../../../main/utils/channel/channel-listeners/listener-tokens"; 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"; import authHeaderStateInjectable from "../common/header-state.injectable";
const authHeaderRequestListenerInjectable = getRequestChannelListenerInjectable({ const authHeaderRequestListenerInjectable = getRequestChannelListenerInjectable({

View File

@ -5,7 +5,7 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import requestFromChannelInjectable from "../../../renderer/utils/channel/request-from-channel.injectable"; import requestFromChannelInjectable from "../../../renderer/utils/channel/request-from-channel.injectable";
import { authHeaderChannel } from "../common/channel"; import { authHeaderChannel } from "../common/vars";
const requestAuthHeaderValueInjectable = getInjectable({ const requestAuthHeaderValueInjectable = getInjectable({
id: "request-auth-header-value", id: "request-auth-header-value",

View File

@ -10,7 +10,7 @@ import type { CreateCluster } from "../../common/cluster/create-cluster-injectio
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token"; import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
import authorizationReviewInjectable from "../../common/cluster/authorization-review.injectable"; import authorizationReviewInjectable from "../../common/cluster/authorization-review.injectable";
import requestNamespaceListPermissionsForInjectable from "../../common/cluster/request-namespace-list-permissions.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 createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
import type { ClusterContextHandler } from "../context-handler/context-handler"; import type { ClusterContextHandler } from "../context-handler/context-handler";
import { parse } from "url"; import { parse } from "url";
@ -42,7 +42,7 @@ describe("create clusters", () => {
di.override(broadcastMessageInjectable, () => async () => {}); di.override(broadcastMessageInjectable, () => async () => {});
di.override(authorizationReviewInjectable, () => () => () => Promise.resolve(true)); di.override(authorizationReviewInjectable, () => () => () => Promise.resolve(true));
di.override(requestNamespaceListPermissionsForInjectable, () => () => async () => () => true); di.override(requestNamespaceListPermissionsForInjectable, () => () => async () => () => true);
di.override(listNamespacesInjectable, () => () => () => Promise.resolve([ "default" ])); di.override(listNamespacesForInjectable, () => () => () => Promise.resolve([ "default" ]));
di.override(createContextHandlerInjectable, () => (cluster) => ({ di.override(createContextHandlerInjectable, () => (cluster) => ({
restartServer: jest.fn(), restartServer: jest.fn(),
stopServer: jest.fn(), stopServer: jest.fn(),

View File

@ -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 { GetPrometheusProviderByKind } from "../prometheus/get-by-kind.injectable";
import type { IComputedValue } from "mobx"; import type { IComputedValue } from "mobx";
import type { Logger } from "../../common/logger"; import type { Logger } from "../../common/logger";
import type { MakeApiClient } from "../../common/cluster/make-api-client.injectable";
export interface PrometheusDetails { export interface PrometheusDetails {
prometheusPath: string; prometheusPath: string;
@ -31,6 +32,7 @@ interface PrometheusServicePreferences {
export interface ContextHandlerDependencies { export interface ContextHandlerDependencies {
createKubeAuthProxy: CreateKubeAuthProxy; createKubeAuthProxy: CreateKubeAuthProxy;
getPrometheusProviderByKind: GetPrometheusProviderByKind; getPrometheusProviderByKind: GetPrometheusProviderByKind;
makeApiClient: MakeApiClient;
readonly authProxyCa: string; readonly authProxyCa: string;
readonly prometheusProviders: IComputedValue<PrometheusProvider[]>; readonly prometheusProviders: IComputedValue<PrometheusProvider[]>;
readonly logger: Logger; readonly logger: Logger;
@ -110,7 +112,7 @@ export class ContextHandler implements ClusterContextHandler {
const providers = this.listPotentialProviders(); const providers = this.listPotentialProviders();
const proxyConfig = await this.cluster.getProxyKubeconfig(); const proxyConfig = await this.cluster.getProxyKubeconfig();
const apiClient = proxyConfig.makeApiClient(CoreV1Api); const apiClient = this.dependencies.makeApiClient(proxyConfig, CoreV1Api);
const potentialServices = await Promise.allSettled( const potentialServices = await Promise.allSettled(
providers.map(provider => provider.getPrometheusService(apiClient)), providers.map(provider => provider.getPrometheusService(apiClient)),
); );

View File

@ -12,6 +12,7 @@ import URLParse from "url-parse";
import getPrometheusProviderByKindInjectable from "../prometheus/get-by-kind.injectable"; import getPrometheusProviderByKindInjectable from "../prometheus/get-by-kind.injectable";
import prometheusProvidersInjectable from "../prometheus/providers.injectable"; import prometheusProvidersInjectable from "../prometheus/providers.injectable";
import loggerInjectable from "../../common/logger.injectable"; import loggerInjectable from "../../common/logger.injectable";
import makeApiClientInjectable from "../../common/cluster/make-api-client.injectable";
const createContextHandlerInjectable = getInjectable({ const createContextHandlerInjectable = getInjectable({
id: "create-context-handler", id: "create-context-handler",
@ -22,6 +23,7 @@ const createContextHandlerInjectable = getInjectable({
getPrometheusProviderByKind: di.inject(getPrometheusProviderByKindInjectable), getPrometheusProviderByKind: di.inject(getPrometheusProviderByKindInjectable),
prometheusProviders: di.inject(prometheusProvidersInjectable), prometheusProviders: di.inject(prometheusProvidersInjectable),
logger: di.inject(loggerInjectable), logger: di.inject(loggerInjectable),
makeApiClient: di.inject(makeApiClientInjectable),
}; };
return (cluster: Cluster): ClusterContextHandler => { return (cluster: Cluster): ClusterContextHandler => {

View File

@ -11,7 +11,7 @@ import createKubectlInjectable from "../kubectl/create-kubectl.injectable";
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable"; import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token"; import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
import authorizationReviewInjectable from "../../common/cluster/authorization-review.injectable"; 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 createListApiResourcesInjectable from "../cluster/request-api-resources.injectable";
import loggerInjectable from "../../common/logger.injectable"; import loggerInjectable from "../../common/logger.injectable";
import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable"; import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable";
@ -34,7 +34,7 @@ const createClusterInjectable = getInjectable({
createAuthorizationReview: di.inject(authorizationReviewInjectable), createAuthorizationReview: di.inject(authorizationReviewInjectable),
requestNamespaceListPermissionsFor: di.inject(requestNamespaceListPermissionsForInjectable), requestNamespaceListPermissionsFor: di.inject(requestNamespaceListPermissionsForInjectable),
requestApiResources: di.inject(createListApiResourcesInjectable), requestApiResources: di.inject(createListApiResourcesInjectable),
createListNamespaces: di.inject(listNamespacesInjectable), createListNamespaces: di.inject(listNamespacesForInjectable),
broadcastMessage: di.inject(broadcastMessageInjectable), broadcastMessage: di.inject(broadcastMessageInjectable),
loadConfigfromFile: di.inject(loadConfigfromFileInjectable), loadConfigfromFile: di.inject(loadConfigfromFileInjectable),
detectClusterMetadata: di.inject(detectClusterMetadataInjectable), detectClusterMetadata: di.inject(detectClusterMetadataInjectable),

View File

@ -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<IncomingMessage, "url" | "method">, 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;

View File

@ -5,6 +5,8 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import type { ServerResponse } from "http"; import type { ServerResponse } from "http";
import loggerInjectable from "../../common/logger.injectable"; 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 type { LensApiRequest, Route } from "./route";
import { contentTypes } from "./router-content-types"; import { contentTypes } from "./router-content-types";
import { writeServerResponseFor } from "./write-server-response"; import { writeServerResponseFor } from "./write-server-response";
@ -16,10 +18,24 @@ const createHandlerForRouteInjectable = getInjectable({
id: "create-handler-for-route", id: "create-handler-for-route",
instantiate: (di): CreateHandlerForRoute => { instantiate: (di): CreateHandlerForRoute => {
const logger = di.inject(loggerInjectable); const logger = di.inject(loggerInjectable);
const authHeaderValue = di.inject(authHeaderValueInjectable);
return (route) => async (request, response) => { return (route) => async (request, response) => {
const writeServerResponse = writeServerResponseFor(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 { try {
const result = await route.handler(request); const result = await route.handler(request);

View File

@ -66,6 +66,7 @@ export interface RouteHandler<TResponse, Path extends string>{
export interface BaseRoutePaths<Path extends string> { export interface BaseRoutePaths<Path extends string> {
path: Path; path: Path;
method: "get" | "post" | "put" | "patch" | "delete"; method: "get" | "post" | "put" | "patch" | "delete";
requireAuthentication?: boolean;
} }
export interface PayloadValidator<Payload> { export interface PayloadValidator<Payload> {
@ -78,18 +79,21 @@ export interface ValidatorBaseRoutePaths<Path extends string, Payload> extends B
export interface Route<TResponse, Path extends string> extends BaseRoutePaths<Path> { export interface Route<TResponse, Path extends string> extends BaseRoutePaths<Path> {
handler: RouteHandler<TResponse, Path>; handler: RouteHandler<TResponse, Path>;
readonly requireAuthentication: boolean;
} }
export interface BindHandler<Path extends string> { export interface BindHandler<Path extends string> {
<TResponse>(handler: RouteHandler<TResponse, Path>): Route<TResponse, Path>; <TResponse>(handler: RouteHandler<TResponse, Path>): Route<TResponse, Path>;
} }
export function route<Path extends string>(parts: BaseRoutePaths<Path>): BindHandler<Path> { export const route = <Path extends string>({
return (handler) => ({ requireAuthentication = true,
...parts
}: BaseRoutePaths<Path>): BindHandler<Path> => (handler) => ({
...parts, ...parts,
handler, handler,
requireAuthentication,
}); });
}
export interface ClusterRouteHandler<Response, Path extends string>{ export interface ClusterRouteHandler<Response, Path extends string>{
(request: ClusterLensApiRequest<Path>): RouteResponse<Response> | Promise<RouteResponse<Response>>; (request: ClusterLensApiRequest<Path>): RouteResponse<Response> | Promise<RouteResponse<Response>>;
@ -99,8 +103,10 @@ export interface BindClusterHandler<Path extends string> {
<TResponse>(handler: ClusterRouteHandler<TResponse, Path>): Route<TResponse, Path>; <TResponse>(handler: ClusterRouteHandler<TResponse, Path>): Route<TResponse, Path>;
} }
export function clusterRoute<Path extends string>(parts: BaseRoutePaths<Path>): BindClusterHandler<Path> { export const clusterRoute = <Path extends string>({
return (handler) => ({ requireAuthentication = true,
...parts
}: BaseRoutePaths<Path>): BindClusterHandler<Path> => (handler) => ({
...parts, ...parts,
handler: ({ cluster, ...rest }) => { handler: ({ cluster, ...rest }) => {
if (!cluster) { if (!cluster) {
@ -112,8 +118,8 @@ export function clusterRoute<Path extends string>(parts: BaseRoutePaths<Path>):
return handler({ cluster, ...rest }); return handler({ cluster, ...rest });
}, },
requireAuthentication,
}); });
}
export interface ValidatedClusterLensApiRequest<Path extends string, Payload> extends ClusterLensApiRequest<Path> { export interface ValidatedClusterLensApiRequest<Path extends string, Payload> extends ClusterLensApiRequest<Path> {
payload: Payload; payload: Payload;

View File

@ -17,6 +17,7 @@ const staticFileRouteInjectable = getRouteInjectable({
return route({ return route({
method: "get", method: "get",
path: `/{path*}`, path: `/{path*}`,
requireAuthentication: false,
})( })(
isDevelopment isDevelopment
? di.inject(devStaticFileRouteHandlerInjectable) ? di.inject(devStaticFileRouteHandlerInjectable)

View File

@ -10,15 +10,18 @@ import type { V1Secret } from "@kubernetes/client-node";
import { CoreV1Api } from "@kubernetes/client-node"; import { CoreV1Api } from "@kubernetes/client-node";
import { clusterRoute } from "../../router/route"; import { clusterRoute } from "../../router/route";
import { dump } from "js-yaml"; import { dump } from "js-yaml";
import makeApiClientInjectable from "../../../common/cluster/make-api-client.injectable";
const getServiceAccountRouteInjectable = getRouteInjectable({ const getServiceAccountRouteInjectable = getRouteInjectable({
id: "get-service-account-route", id: "get-service-account-route",
instantiate: () => clusterRoute({ instantiate: (di) => clusterRoute({
method: "get", method: "get",
path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}`, path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}`,
})(async ({ params, cluster }) => { })(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 secretList = await client.listNamespacedSecret(params.namespace);
const secret = secretList.body.items.find(secret => { const secret = secretList.body.items.find(secret => {

View File

@ -13,6 +13,7 @@ import { NodeApi } from "../../../common/k8s-api/endpoints";
import { TerminalChannels } from "../../../common/terminal/channels"; import { TerminalChannels } from "../../../common/terminal/channels";
import type { CreateKubeJsonApiForCluster } from "../../../common/k8s-api/create-kube-json-api-for-cluster.injectable"; 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 { CreateKubeApi } from "../../../common/k8s-api/create-kube-api.injectable";
import type { MakeApiClient } from "../../../common/cluster/make-api-client.injectable";
export interface NodeShellSessionArgs extends ShellSessionArgs { export interface NodeShellSessionArgs extends ShellSessionArgs {
nodeName: string; nodeName: string;
@ -21,6 +22,7 @@ export interface NodeShellSessionArgs extends ShellSessionArgs {
export interface NodeShellSessionDependencies extends ShellSessionDependencies { export interface NodeShellSessionDependencies extends ShellSessionDependencies {
createKubeJsonApiForCluster: CreateKubeJsonApiForCluster; createKubeJsonApiForCluster: CreateKubeJsonApiForCluster;
createKubeApi: CreateKubeApi; createKubeApi: CreateKubeApi;
makeApiClient: MakeApiClient;
} }
export class NodeShellSession extends ShellSession { export class NodeShellSession extends ShellSession {
@ -36,8 +38,8 @@ export class NodeShellSession extends ShellSession {
} }
public async open() { public async open() {
const kc = await this.cluster.getProxyKubeconfig(); const config = await this.cluster.getProxyKubeconfig();
const coreApi = kc.makeApiClient(CoreV1Api); const coreApi = this.dependencies.makeApiClient(config, CoreV1Api);
const shell = await this.kubectl.getPath(); const shell = await this.kubectl.getPath();
const cleanup = once(() => { const cleanup = once(() => {
@ -50,7 +52,7 @@ export class NodeShellSession extends ShellSession {
try { try {
await this.createNodeShellPod(coreApi); await this.createNodeShellPod(coreApi);
await this.waitForRunningPod(kc); await this.waitForRunningPod(config);
} catch (error) { } catch (error) {
cleanup(); cleanup();

View File

@ -20,6 +20,7 @@ import buildVersionInjectable from "../../vars/build-version/build-version.injec
import emitAppEventInjectable from "../../../common/app-event-bus/emit-event.injectable"; import emitAppEventInjectable from "../../../common/app-event-bus/emit-event.injectable";
import statInjectable from "../../../common/fs/stat.injectable"; import statInjectable from "../../../common/fs/stat.injectable";
import createKubeApiInjectable from "../../../common/k8s-api/create-kube-api.injectable"; import createKubeApiInjectable from "../../../common/k8s-api/create-kube-api.injectable";
import makeApiClientInjectable from "../../../common/cluster/make-api-client.injectable";
export interface NodeShellSessionArgs { export interface NodeShellSessionArgs {
websocket: WebSocket; websocket: WebSocket;
@ -47,6 +48,7 @@ const openNodeShellSessionInjectable = getInjectable({
emitAppEvent: di.inject(emitAppEventInjectable), emitAppEvent: di.inject(emitAppEventInjectable),
stat: di.inject(statInjectable), stat: di.inject(statInjectable),
createKubeApi: di.inject(createKubeApiInjectable), createKubeApi: di.inject(createKubeApiInjectable),
makeApiClient: di.inject(makeApiClientInjectable),
}; };
return async (args) => { return async (args) => {