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

Add injecting authentication header automatically to some places

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2022-12-01 15:51:12 -05:00
parent a5dab8549b
commit 4662d1a36a
18 changed files with 208 additions and 101 deletions

View File

@ -0,0 +1,10 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
export const lensAuthenticationHeaderValueInjectionToken = getInjectionToken<string>({
id: "lens-authentication-header-value-token",
});

View File

@ -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<boolean
/**
* @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster
*/
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;
}
};
};
};
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<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,27 @@ 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<string[]>;
export function listNamespaces(config: KubeConfig): ListNamespaces {
const coreApi = config.makeApiClient(CoreV1Api);
return async () => {
const { body: { items }} = await coreApi.listNamespace();
return items
.map(ns => ns.metadata?.name)
.filter(isDefined);
};
}
const listNamespacesInjectable = getInjectable({
id: "list-namespaces",
instantiate: () => listNamespaces,
instantiate: (di) => {
const makeApiClient = di.inject(makeApiClientInjectable);
return (config: KubeConfig): ListNamespaces => {
const coreApi = makeApiClient(config, CoreV1Api);
return async () => {
const { body: { items }} = await coreApi.listNamespace();
return items
.map(ns => ns.metadata?.name)
.filter(isDefined);
};
};
},
});
export default listNamespacesInjectable;

View File

@ -0,0 +1,36 @@
/**
* 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 { lensAuthenticationHeaderValueInjectionToken } from "../auth/header-value";
import { lensAuthenticationHeader } from "../vars/auth-header";
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 lensAuthenticationHeaderValue = di.inject(lensAuthenticationHeaderValueInjectionToken);
return (config, apiClientType) => {
const api = config.makeApiClient(apiClientType);
api.addInterceptor((opts) => {
opts.headers ??= {};
opts.headers[lensAuthenticationHeader] = lensAuthenticationHeaderValue;
});
return api;
};
},
});
export default makeApiClientInjectable;

View File

@ -0,0 +1,40 @@
/**
* 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 { lensAuthenticationHeaderValueInjectionToken } from "../auth/header-value";
import { lensAuthenticationHeader } from "../vars/auth-header";
import fetchModuleInjectable from "./fetch-module.injectable";
import type { Fetch } from "./fetch.injectable";
import fetchInjectable from "./fetch.injectable";
/**
* This injectable should not be used to request data from external sources as it would leak the
* authentication header value
*/
const lensAuthenticatedFetchInjectable = getInjectable({
id: "lens-authenticated-fetch",
instantiate: (di): Fetch => {
const authHeaderValue = di.inject(lensAuthenticationHeaderValueInjectionToken);
const fetch = di.inject(fetchInjectable);
const { Headers } = di.inject(fetchModuleInjectable);
return async (url, init) => {
const {
headers: headersInit,
...rest
} = init ?? {};
const headers = new Headers(headersInit);
headers.set(lensAuthenticationHeader, authHeaderValue);
return fetch(url, {
headers,
...rest,
});
};
},
});
export default lensAuthenticatedFetchInjectable;

View File

@ -1,17 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import requestPromise from "request-promise-native";
export async function getAppVersionFromProxyServer(proxyPort: number): Promise<string> {
const response = await requestPromise({
method: "GET",
uri: `http://127.0.0.1:${proxyPort}/version`,
resolveWithFullResponse: true,
proxy: undefined,
});
return JSON.parse(response.body).version;
}

View File

@ -4,7 +4,6 @@
*/
export * from "./abort-controller";
export * from "./app-version";
export * from "./autobind";
export * from "./camelCase";
export * from "./cluster-id-url-parsing";

View File

@ -0,0 +1,24 @@
/**
* 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 lensProxyPortInjectable from "../../main/lens-proxy/lens-proxy-port.injectable";
import lensAuthenticatedFetchInjectable from "../fetch/lens-authed-fetch.injectable";
const requestAppVersionInjectable = getInjectable({
id: "request-app-version",
instantiate: (di) => {
const lensAuthenticatedFetch = di.inject(lensAuthenticatedFetchInjectable);
const lensProxyPort = di.inject(lensProxyPortInjectable);
return async () => {
const response = await lensAuthenticatedFetch(`http://127.0.0.1:${lensProxyPort.get()}/version`);
const body = await response.json() as { version: string };
return body.version;
};
},
});
export default requestAppVersionInjectable;

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 { 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<PrometheusProvider[]>;
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)),
);

View File

@ -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 => {

View File

@ -8,6 +8,8 @@ import { apiKubePrefix } from "../common/vars";
import type { Cluster } from "../common/cluster/cluster";
import { getInjectable } from "@ogre-tools/injectable";
import lensProxyPortInjectable from "./lens-proxy/lens-proxy-port.injectable";
import { lensAuthenticationHeaderValueInjectionToken } from "../common/auth/header-value";
import { lensAuthenticationHeader } from "../common/vars/auth-header";
export type K8sRequest = (cluster: Cluster, path: string, options?: RequestPromiseOptions) => Promise<any>;
@ -16,6 +18,7 @@ const k8sRequestInjectable = getInjectable({
instantiate: (di) => {
const lensProxyPort = di.inject(lensProxyPortInjectable);
const lensAuthenticationHeaderValue = di.inject(lensAuthenticationHeaderValueInjectionToken);
return async (
cluster: Cluster,
@ -28,6 +31,7 @@ const k8sRequestInjectable = getInjectable({
options.json ??= true;
options.timeout ??= 30000;
options.headers.Host = `${cluster.id}.${new URL(kubeProxyUrl).host}`; // required in ClusterManager.getClusterForRequest()
options.headers[lensAuthenticationHeader] = lensAuthenticationHeaderValue;
return request(kubeProxyUrl + path, options);
};

View File

@ -4,10 +4,12 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import * as uuid from "uuid";
import { lensAuthenticationHeaderValueInjectionToken } from "../../common/auth/header-value";
const authHeaderValueInjectable = getInjectable({
id: "auth-header-value",
instantiate: () => uuid.v4(),
injectionToken: lensAuthenticationHeaderValueInjectionToken,
});
export default authHeaderValueInjectable;

View File

@ -10,43 +10,49 @@ 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({
method: "get",
path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}`,
})(async ({ params, cluster }) => {
const client = (await cluster.getProxyKubeconfig()).makeApiClient(CoreV1Api);
const secretList = await client.listNamespacedSecret(params.namespace);
instantiate: (di) => {
const makeApiClient = di.inject(makeApiClientInjectable);
const secret = secretList.body.items.find(secret => {
const { annotations } = secret.metadata ?? {};
return clusterRoute({
method: "get",
path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}`,
})(async ({ params, cluster }) => {
const proxyConfig = await cluster.getProxyKubeconfig();
const client = makeApiClient(proxyConfig, CoreV1Api);
const secretList = await client.listNamespacedSecret(params.namespace);
return annotations?.["kubernetes.io/service-account.name"] === params.account;
const secret = secretList.body.items.find(secret => {
const { annotations } = secret.metadata ?? {};
return annotations?.["kubernetes.io/service-account.name"] === params.account;
});
if (!secret) {
return {
error: "No secret found",
statusCode: 404,
};
}
const kubeconfig = generateKubeConfig(params.account, secret, cluster);
if (!kubeconfig) {
return {
error: "No secret found",
statusCode: 404,
};
}
return {
response: kubeconfig,
};
});
if (!secret) {
return {
error: "No secret found",
statusCode: 404,
};
}
const kubeconfig = generateKubeConfig(params.account, secret, cluster);
if (!kubeconfig) {
return {
error: "No secret found",
statusCode: 404,
};
}
return {
response: kubeconfig,
};
}),
},
});
export default getServiceAccountRouteInjectable;

View File

@ -12,6 +12,7 @@ import { get, once } from "lodash";
import { Node, 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 { MakeApiClient } from "../../../common/cluster/make-api-client.injectable";
export interface NodeShellSessionArgs extends ShellSessionArgs {
nodeName: string;
@ -19,6 +20,7 @@ export interface NodeShellSessionArgs extends ShellSessionArgs {
export interface NodeShellSessionDependencies extends ShellSessionDependencies {
createKubeJsonApiForCluster: CreateKubeJsonApiForCluster;
makeApiClient: MakeApiClient;
}
export class NodeShellSession extends ShellSession {
@ -34,8 +36,8 @@ export class NodeShellSession extends ShellSession {
}
public async open() {
const kc = await this.cluster.getProxyKubeconfig();
const coreApi = kc.makeApiClient(CoreV1Api);
const proxyConfig = await this.cluster.getProxyKubeconfig();
const coreApi = this.dependencies.makeApiClient(proxyConfig, CoreV1Api);
const shell = await this.kubectl.getPath();
const cleanup = once(() => {
@ -48,7 +50,7 @@ export class NodeShellSession extends ShellSession {
try {
await this.createNodeShellPod(coreApi);
await this.waitForRunningPod(kc);
await this.waitForRunningPod(proxyConfig);
} catch (error) {
cleanup();

View File

@ -19,6 +19,7 @@ import appNameInjectable from "../../../common/vars/app-name.injectable";
import buildVersionInjectable from "../../vars/build-version/build-version.injectable";
import emitAppEventInjectable from "../../../common/app-event-bus/emit-event.injectable";
import statInjectable from "../../../common/fs/stat.injectable";
import makeApiClientInjectable from "../../../common/cluster/make-api-client.injectable";
export interface NodeShellSessionArgs {
websocket: WebSocket;
@ -45,6 +46,7 @@ const openNodeShellSessionInjectable = getInjectable({
spawnPty: di.inject(spawnPtyInjectable),
emitAppEvent: di.inject(emitAppEventInjectable),
stat: di.inject(statInjectable),
makeApiClient: di.inject(makeApiClientInjectable),
};
return async (args) => {

View File

@ -3,16 +3,15 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { getAppVersionFromProxyServer } from "../../../common/utils";
import exitAppInjectable from "../../electron-app/features/exit-app.injectable";
import lensProxyInjectable from "../../lens-proxy/lens-proxy.injectable";
import loggerInjectable from "../../../common/logger.injectable";
import lensProxyPortInjectable from "../../lens-proxy/lens-proxy-port.injectable";
import isWindowsInjectable from "../../../common/vars/is-windows.injectable";
import showErrorPopupInjectable from "../../electron-app/features/show-error-popup.injectable";
import { beforeApplicationIsLoadingInjectionToken } from "../runnable-tokens/before-application-is-loading-injection-token";
import buildVersionInjectable from "../../vars/build-version/build-version.injectable";
import initializeBuildVersionInjectable from "../../vars/build-version/init.injectable";
import requestAppVersionInjectable from "../../../common/utils/request-app-version.injectable";
const setupLensProxyInjectable = getInjectable({
id: "setup-lens-proxy",
@ -21,10 +20,10 @@ const setupLensProxyInjectable = getInjectable({
const lensProxy = di.inject(lensProxyInjectable);
const exitApp = di.inject(exitAppInjectable);
const logger = di.inject(loggerInjectable);
const lensProxyPort = di.inject(lensProxyPortInjectable);
const isWindows = di.inject(isWindowsInjectable);
const showErrorPopup = di.inject(showErrorPopupInjectable);
const buildVersion = di.inject(buildVersionInjectable);
const requestAppVersion = di.inject(requestAppVersionInjectable);
return {
id: "setup-lens-proxy",
@ -41,9 +40,7 @@ const setupLensProxyInjectable = getInjectable({
// test proxy connection
try {
logger.info("🔎 Testing LensProxy connection ...");
const versionFromProxy = await getAppVersionFromProxyServer(
lensProxyPort.get(),
);
const versionFromProxy = await requestAppVersion();
if (buildVersion.get() !== versionFromProxy) {
logger.error("Proxy server responded with invalid response");

View File

@ -3,11 +3,13 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { lensAuthenticationHeaderValueInjectionToken } from "../../common/auth/header-value";
import authHeaderValueStateInjectable from "./auth-header-state.injectable";
const authHeaderValueInjectable = getInjectable({
id: "auth-header-value",
instantiate: (di) => di.inject(authHeaderValueStateInjectable).get(),
injectionToken: lensAuthenticationHeaderValueInjectionToken,
});
export default authHeaderValueInjectable;