From e21e0b577b7e2803dea401c709fb46c05a0a1add Mon Sep 17 00:00:00 2001 From: Adam Malcontenti-Wilson Date: Sun, 10 May 2020 01:25:34 +1000 Subject: [PATCH] Replace admin check with check for access (#297) Signed-off-by: Adam Malcontenti-Wilson --- .../client/components/+storage/storage.tsx | 6 +-- .../+user-management/user-management.tsx | 4 +- dashboard/client/components/app.tsx | 2 +- .../client/components/layout/sidebar.tsx | 7 ++- dashboard/client/config.store.ts | 4 ++ dashboard/client/kube-object.store.ts | 10 ++-- dashboard/server/common/config.ts | 1 + src/main/cluster.ts | 39 +++++++-------- src/main/routes/config.ts | 47 +++++++++++-------- 9 files changed, 62 insertions(+), 58 deletions(-) diff --git a/dashboard/client/components/+storage/storage.tsx b/dashboard/client/components/+storage/storage.tsx index 7878441c61..bf8205ebd1 100644 --- a/dashboard/client/components/+storage/storage.tsx +++ b/dashboard/client/components/+storage/storage.tsx @@ -20,7 +20,7 @@ interface Props extends RouteComponentProps<{}> { export class Storage extends React.Component { static get tabRoutes() { const tabRoutes: TabRoute[] = []; - const { isClusterAdmin } = configStore; + const { allowedResources } = configStore; const query = namespaceStore.getContextParams() tabRoutes.push({ @@ -30,7 +30,7 @@ export class Storage extends React.Component { path: volumeClaimsRoute.path, }) - if (isClusterAdmin) { + if (allowedResources.includes('persistentvolumes')) { tabRoutes.push({ title: Persistent Volumes, component: PersistentVolumes, @@ -39,7 +39,7 @@ export class Storage extends React.Component { }); } - if (isClusterAdmin) { + if (allowedResources.includes('storageclasses')) { tabRoutes.push({ title: Storage Classes, component: StorageClasses, diff --git a/dashboard/client/components/+user-management/user-management.tsx b/dashboard/client/components/+user-management/user-management.tsx index 93b812fc2c..3806cc37d7 100644 --- a/dashboard/client/components/+user-management/user-management.tsx +++ b/dashboard/client/components/+user-management/user-management.tsx @@ -21,7 +21,7 @@ interface Props extends RouteComponentProps<{}> { export class UserManagement extends React.Component { static get tabRoutes() { const tabRoutes: TabRoute[] = []; - const { isClusterAdmin } = configStore; + const { allowedResources } = configStore; const query = namespaceStore.getContextParams() tabRoutes.push( { @@ -43,7 +43,7 @@ export class UserManagement extends React.Component { path: rolesRoute.path, }, ) - if (isClusterAdmin) { + if (allowedResources.includes("podsecuritypolicies")) { tabRoutes.push({ title: Pod Security Policies, component: PodSecurityPolicies, diff --git a/dashboard/client/components/app.tsx b/dashboard/client/components/app.tsx index b07d72749c..516c2d4903 100755 --- a/dashboard/client/components/app.tsx +++ b/dashboard/client/components/app.tsx @@ -46,7 +46,7 @@ class App extends React.Component { }; render() { - const homeUrl = configStore.isClusterAdmin ? clusterURL() : workloadsURL(); + const homeUrl = clusterURL(); return ( diff --git a/dashboard/client/components/layout/sidebar.tsx b/dashboard/client/components/layout/sidebar.tsx index 3edc2ffc67..e005ecd87c 100644 --- a/dashboard/client/components/layout/sidebar.tsx +++ b/dashboard/client/components/layout/sidebar.tsx @@ -71,7 +71,7 @@ export class Sidebar extends React.Component { render() { const { toggle, isPinned, className } = this.props; - const { isClusterAdmin } = configStore; + const { isClusterAdmin, allowedResources } = configStore; const query = namespaceStore.getContextParams(); return ( @@ -91,14 +91,13 @@ export class Sidebar extends React.Component {
Cluster} icon={} /> Nodes} icon={} @@ -166,7 +165,7 @@ export class Sidebar extends React.Component { /> extends ItemSt } } - protected async loadItems(namespaces?: string[]): Promise { - if (!configStore.isClusterAdmin && !this.api.isNamespaced) { - return [] - } - if (!namespaces) { + protected async loadItems(allowedNamespaces?: string[]): Promise { + if (!this.api.isNamespaced || !allowedNamespaces) { const { limit } = this; const query: IKubeApiQueryParams = limit ? { limit } : {}; return this.api.list({}, query); } else { return Promise - .all(namespaces.map(namespace => this.api.list({ namespace }))) + .all(allowedNamespaces.map(namespace => this.api.list({ namespace }))) .then(items => items.flat()) } } @@ -155,7 +152,6 @@ export abstract class KubeObjectStore extends ItemSt } subscribe(apis = [this.api]) { - apis = apis.filter(api => !configStore.isClusterAdmin ? api.isNamespaced : true); return KubeApi.watchAll(...apis); } diff --git a/dashboard/server/common/config.ts b/dashboard/server/common/config.ts index 11c8b48a38..eec70f4ddd 100644 --- a/dashboard/server/common/config.ts +++ b/dashboard/server/common/config.ts @@ -5,6 +5,7 @@ export interface IConfig extends Partial { username?: string; token?: string; allowedNamespaces?: string[]; + allowedResources?: string[]; isClusterAdmin?: boolean; chartsEnabled: boolean; kubectlAccess?: boolean; // User accessed via kubectl-lens plugin diff --git a/src/main/cluster.ts b/src/main/cluster.ts index cb900e898a..4971abe5cc 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -3,7 +3,7 @@ import { FeatureStatusMap } from "./feature" import * as k8s from "./k8s" import { clusterStore } from "../common/cluster-store" import logger from "./logger" -import { KubeConfig, CoreV1Api } from "@kubernetes/client-node" +import { KubeConfig, CoreV1Api, AuthorizationV1Api, V1ResourceAttributes } from "@kubernetes/client-node" import * as fm from "./feature-manager"; import { Kubectl } from "./kubectl"; import { PromiseIpc } from "electron-promise-ipc" @@ -137,9 +137,7 @@ export class Cluster implements ClusterInfo { this.distribution = this.detectKubernetesDistribution(this.version) this.features = await fm.getFeatures(this.contextHandler) this.isAdmin = await this.isClusterAdmin() - if (this.isAdmin) { - this.nodes = await this.getNodeCount() - } + this.nodes = await this.getNodeCount() this.kubeCtl = new Kubectl(this.version) this.kubeCtl.ensureKubectl() } @@ -227,30 +225,29 @@ export class Cluster implements ClusterInfo { } } - protected async isClusterAdmin(): Promise { - const requestOpts: request.RequestPromiseOptions = { - body: { - kind: "SelfSubjectAccessReview", - apiVersion: "authorization.k8s.io/v1", - spec: { - resourceAttributes: { - namespace: "kube-system", - resource: "*", - verb: "create", - } - } - }, - method: "post" - } + public async canI(resourceAttributes: V1ResourceAttributes): Promise { + const authApi = this.contextHandler.kc.makeApiClient(AuthorizationV1Api) try { - const response = await this.k8sRequest("/apis/authorization.k8s.io/v1/selfsubjectaccessreviews", requestOpts) - return response.status.allowed === true + const accessReview = await authApi.createSelfSubjectAccessReview({ + apiVersion: "authorization.k8s.io/v1", + kind: "SelfSubjectAccessReview", + spec: { resourceAttributes } + }) + return accessReview.body.status.allowed === true } catch(error) { logger.error(`failed to request selfSubjectAccessReview: ${error.message}`) return false } } + protected async isClusterAdmin(): Promise { + return this.canI({ + namespace: "kube-system", + resource: "*", + verb: "create", + }) + } + protected detectKubernetesDistribution(kubernetesVersion: string): string { if (kubernetesVersion.includes("gke")) { return "gke" diff --git a/src/main/routes/config.ts b/src/main/routes/config.ts index 1f45b689f6..5ac4022bf1 100644 --- a/src/main/routes/config.ts +++ b/src/main/routes/config.ts @@ -5,33 +5,19 @@ import { getAppVersion } from "../../common/app-utils" import { CoreV1Api, AuthorizationV1Api } from "@kubernetes/client-node" import { Cluster } from "../cluster" - -function selfSubjectAccessReview(authApi: AuthorizationV1Api, namespace: string) { - return authApi.createSelfSubjectAccessReview({ - apiVersion: "authorization.k8s.io/v1", - kind: "SelfSubjectAccessReview", - spec: { - resourceAttributes: { - namespace: namespace, - resource: "pods", - verb: "list", - } - } - }) -} - async function getAllowedNamespaces(cluster: Cluster) { const api = cluster.contextHandler.kc.makeApiClient(CoreV1Api) - const authApi = cluster.contextHandler.kc.makeApiClient(AuthorizationV1Api) try { const namespaceList = await api.listNamespace() const nsAccessStatuses = await Promise.all( - namespaceList.body.items.map(ns => { - return selfSubjectAccessReview(authApi, ns.metadata.name) - }) + namespaceList.body.items.map(ns => cluster.canI({ + namespace: ns.metadata.name, + resource: "pods", + verb: "list", + })) ) return namespaceList.body.items - .filter((ns, i) => nsAccessStatuses[i].body.status.allowed) + .filter((ns, i) => nsAccessStatuses[i]) .map(ns => ns.metadata.name) } catch(error) { const kc = cluster.contextHandler.kc @@ -44,6 +30,26 @@ async function getAllowedNamespaces(cluster: Cluster) { } } +async function getAllowedResources(cluster: Cluster) { + // TODO: auto-populate all resources dynamically + const resources = [ + "nodes", "persistentvolumes", "storageclasses", "customresourcedefinitions", + "podsecuritypolicies" + ] + try { + const resourceAccessStatuses = await Promise.all( + resources.map(resource => cluster.canI({ + resource: resource, + verb: "list" + })) + ) + return resources + .filter((resource, i) => resourceAccessStatuses[i]) + } catch(error) { + return [] + } +} + class ConfigRoute extends LensApi { public async routeConfig(request: LensApiRequest) { @@ -56,6 +62,7 @@ class ConfigRoute extends LensApi { kubeVersion: cluster.version, chartsEnabled: true, isClusterAdmin: cluster.isAdmin, + allowedResources: await getAllowedResources(cluster), allowedNamespaces: await getAllowedNamespaces(cluster) };