mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Replace admin check with check for access (#297)
Signed-off-by: Adam Malcontenti-Wilson <adman.com@gmail.com>
This commit is contained in:
parent
fb287c435c
commit
e21e0b577b
@ -20,7 +20,7 @@ interface Props extends RouteComponentProps<{}> {
|
|||||||
export class Storage extends React.Component<Props> {
|
export class Storage extends React.Component<Props> {
|
||||||
static get tabRoutes() {
|
static get tabRoutes() {
|
||||||
const tabRoutes: TabRoute[] = [];
|
const tabRoutes: TabRoute[] = [];
|
||||||
const { isClusterAdmin } = configStore;
|
const { allowedResources } = configStore;
|
||||||
const query = namespaceStore.getContextParams()
|
const query = namespaceStore.getContextParams()
|
||||||
|
|
||||||
tabRoutes.push({
|
tabRoutes.push({
|
||||||
@ -30,7 +30,7 @@ export class Storage extends React.Component<Props> {
|
|||||||
path: volumeClaimsRoute.path,
|
path: volumeClaimsRoute.path,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (isClusterAdmin) {
|
if (allowedResources.includes('persistentvolumes')) {
|
||||||
tabRoutes.push({
|
tabRoutes.push({
|
||||||
title: <Trans>Persistent Volumes</Trans>,
|
title: <Trans>Persistent Volumes</Trans>,
|
||||||
component: PersistentVolumes,
|
component: PersistentVolumes,
|
||||||
@ -39,7 +39,7 @@ export class Storage extends React.Component<Props> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isClusterAdmin) {
|
if (allowedResources.includes('storageclasses')) {
|
||||||
tabRoutes.push({
|
tabRoutes.push({
|
||||||
title: <Trans>Storage Classes</Trans>,
|
title: <Trans>Storage Classes</Trans>,
|
||||||
component: StorageClasses,
|
component: StorageClasses,
|
||||||
|
|||||||
@ -21,7 +21,7 @@ interface Props extends RouteComponentProps<{}> {
|
|||||||
export class UserManagement extends React.Component<Props> {
|
export class UserManagement extends React.Component<Props> {
|
||||||
static get tabRoutes() {
|
static get tabRoutes() {
|
||||||
const tabRoutes: TabRoute[] = [];
|
const tabRoutes: TabRoute[] = [];
|
||||||
const { isClusterAdmin } = configStore;
|
const { allowedResources } = configStore;
|
||||||
const query = namespaceStore.getContextParams()
|
const query = namespaceStore.getContextParams()
|
||||||
tabRoutes.push(
|
tabRoutes.push(
|
||||||
{
|
{
|
||||||
@ -43,7 +43,7 @@ export class UserManagement extends React.Component<Props> {
|
|||||||
path: rolesRoute.path,
|
path: rolesRoute.path,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if (isClusterAdmin) {
|
if (allowedResources.includes("podsecuritypolicies")) {
|
||||||
tabRoutes.push({
|
tabRoutes.push({
|
||||||
title: <Trans>Pod Security Policies</Trans>,
|
title: <Trans>Pod Security Policies</Trans>,
|
||||||
component: PodSecurityPolicies,
|
component: PodSecurityPolicies,
|
||||||
|
|||||||
@ -46,7 +46,7 @@ class App extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const homeUrl = configStore.isClusterAdmin ? clusterURL() : workloadsURL();
|
const homeUrl = clusterURL();
|
||||||
return (
|
return (
|
||||||
<I18nProvider i18n={_i18n}>
|
<I18nProvider i18n={_i18n}>
|
||||||
<Router history={browserHistory}>
|
<Router history={browserHistory}>
|
||||||
|
|||||||
@ -71,7 +71,7 @@ export class Sidebar extends React.Component<Props> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { toggle, isPinned, className } = this.props;
|
const { toggle, isPinned, className } = this.props;
|
||||||
const { isClusterAdmin } = configStore;
|
const { isClusterAdmin, allowedResources } = configStore;
|
||||||
const query = namespaceStore.getContextParams();
|
const query = namespaceStore.getContextParams();
|
||||||
return (
|
return (
|
||||||
<SidebarContext.Provider value={{ pinned: isPinned }}>
|
<SidebarContext.Provider value={{ pinned: isPinned }}>
|
||||||
@ -91,14 +91,13 @@ export class Sidebar extends React.Component<Props> {
|
|||||||
<div className="sidebar-nav flex column box grow-fixed">
|
<div className="sidebar-nav flex column box grow-fixed">
|
||||||
<SidebarNavItem
|
<SidebarNavItem
|
||||||
id="cluster"
|
id="cluster"
|
||||||
isHidden={!isClusterAdmin}
|
|
||||||
url={clusterURL()}
|
url={clusterURL()}
|
||||||
text={<Trans>Cluster</Trans>}
|
text={<Trans>Cluster</Trans>}
|
||||||
icon={<Icon svg="kube"/>}
|
icon={<Icon svg="kube"/>}
|
||||||
/>
|
/>
|
||||||
<SidebarNavItem
|
<SidebarNavItem
|
||||||
id="nodes"
|
id="nodes"
|
||||||
isHidden={!isClusterAdmin}
|
isHidden={!allowedResources.includes('nodes')}
|
||||||
url={nodesURL()}
|
url={nodesURL()}
|
||||||
text={<Trans>Nodes</Trans>}
|
text={<Trans>Nodes</Trans>}
|
||||||
icon={<Icon svg="nodes"/>}
|
icon={<Icon svg="nodes"/>}
|
||||||
@ -166,7 +165,7 @@ export class Sidebar extends React.Component<Props> {
|
|||||||
/>
|
/>
|
||||||
<SidebarNavItem
|
<SidebarNavItem
|
||||||
id="custom-resources"
|
id="custom-resources"
|
||||||
isHidden={!isClusterAdmin}
|
isHidden={!allowedResources.includes('customresourcedefinitions')}
|
||||||
url={crdURL()}
|
url={crdURL()}
|
||||||
subMenus={CustomResources.tabRoutes}
|
subMenus={CustomResources.tabRoutes}
|
||||||
routePath={crdRoute.path}
|
routePath={crdRoute.path}
|
||||||
|
|||||||
@ -45,6 +45,10 @@ export class ConfigStore {
|
|||||||
return this.config.allowedNamespaces || [];
|
return this.config.allowedNamespaces || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get allowedResources() {
|
||||||
|
return this.config.allowedResources;
|
||||||
|
}
|
||||||
|
|
||||||
get isClusterAdmin() {
|
get isClusterAdmin() {
|
||||||
return this.config.isClusterAdmin;
|
return this.config.isClusterAdmin;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,18 +57,15 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async loadItems(namespaces?: string[]): Promise<T[]> {
|
protected async loadItems(allowedNamespaces?: string[]): Promise<T[]> {
|
||||||
if (!configStore.isClusterAdmin && !this.api.isNamespaced) {
|
if (!this.api.isNamespaced || !allowedNamespaces) {
|
||||||
return []
|
|
||||||
}
|
|
||||||
if (!namespaces) {
|
|
||||||
const { limit } = this;
|
const { limit } = this;
|
||||||
const query: IKubeApiQueryParams = limit ? { limit } : {};
|
const query: IKubeApiQueryParams = limit ? { limit } : {};
|
||||||
return this.api.list({}, query);
|
return this.api.list({}, query);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return Promise
|
return Promise
|
||||||
.all(namespaces.map(namespace => this.api.list({ namespace })))
|
.all(allowedNamespaces.map(namespace => this.api.list({ namespace })))
|
||||||
.then(items => items.flat())
|
.then(items => items.flat())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -155,7 +152,6 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
subscribe(apis = [this.api]) {
|
subscribe(apis = [this.api]) {
|
||||||
apis = apis.filter(api => !configStore.isClusterAdmin ? api.isNamespaced : true);
|
|
||||||
return KubeApi.watchAll(...apis);
|
return KubeApi.watchAll(...apis);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ export interface IConfig extends Partial<IClusterInfo> {
|
|||||||
username?: string;
|
username?: string;
|
||||||
token?: string;
|
token?: string;
|
||||||
allowedNamespaces?: string[];
|
allowedNamespaces?: string[];
|
||||||
|
allowedResources?: string[];
|
||||||
isClusterAdmin?: boolean;
|
isClusterAdmin?: boolean;
|
||||||
chartsEnabled: boolean;
|
chartsEnabled: boolean;
|
||||||
kubectlAccess?: boolean; // User accessed via kubectl-lens plugin
|
kubectlAccess?: boolean; // User accessed via kubectl-lens plugin
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { FeatureStatusMap } from "./feature"
|
|||||||
import * as k8s from "./k8s"
|
import * as k8s from "./k8s"
|
||||||
import { clusterStore } from "../common/cluster-store"
|
import { clusterStore } from "../common/cluster-store"
|
||||||
import logger from "./logger"
|
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 * as fm from "./feature-manager";
|
||||||
import { Kubectl } from "./kubectl";
|
import { Kubectl } from "./kubectl";
|
||||||
import { PromiseIpc } from "electron-promise-ipc"
|
import { PromiseIpc } from "electron-promise-ipc"
|
||||||
@ -137,9 +137,7 @@ export class Cluster implements ClusterInfo {
|
|||||||
this.distribution = this.detectKubernetesDistribution(this.version)
|
this.distribution = this.detectKubernetesDistribution(this.version)
|
||||||
this.features = await fm.getFeatures(this.contextHandler)
|
this.features = await fm.getFeatures(this.contextHandler)
|
||||||
this.isAdmin = await this.isClusterAdmin()
|
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 = new Kubectl(this.version)
|
||||||
this.kubeCtl.ensureKubectl()
|
this.kubeCtl.ensureKubectl()
|
||||||
}
|
}
|
||||||
@ -227,30 +225,29 @@ export class Cluster implements ClusterInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async isClusterAdmin(): Promise<boolean> {
|
public async canI(resourceAttributes: V1ResourceAttributes): Promise<boolean> {
|
||||||
const requestOpts: request.RequestPromiseOptions = {
|
const authApi = this.contextHandler.kc.makeApiClient(AuthorizationV1Api)
|
||||||
body: {
|
|
||||||
kind: "SelfSubjectAccessReview",
|
|
||||||
apiVersion: "authorization.k8s.io/v1",
|
|
||||||
spec: {
|
|
||||||
resourceAttributes: {
|
|
||||||
namespace: "kube-system",
|
|
||||||
resource: "*",
|
|
||||||
verb: "create",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
method: "post"
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const response = await this.k8sRequest("/apis/authorization.k8s.io/v1/selfsubjectaccessreviews", requestOpts)
|
const accessReview = await authApi.createSelfSubjectAccessReview({
|
||||||
return response.status.allowed === true
|
apiVersion: "authorization.k8s.io/v1",
|
||||||
|
kind: "SelfSubjectAccessReview",
|
||||||
|
spec: { resourceAttributes }
|
||||||
|
})
|
||||||
|
return accessReview.body.status.allowed === true
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
logger.error(`failed to request selfSubjectAccessReview: ${error.message}`)
|
logger.error(`failed to request selfSubjectAccessReview: ${error.message}`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async isClusterAdmin(): Promise<boolean> {
|
||||||
|
return this.canI({
|
||||||
|
namespace: "kube-system",
|
||||||
|
resource: "*",
|
||||||
|
verb: "create",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
protected detectKubernetesDistribution(kubernetesVersion: string): string {
|
protected detectKubernetesDistribution(kubernetesVersion: string): string {
|
||||||
if (kubernetesVersion.includes("gke")) {
|
if (kubernetesVersion.includes("gke")) {
|
||||||
return "gke"
|
return "gke"
|
||||||
|
|||||||
@ -5,33 +5,19 @@ import { getAppVersion } from "../../common/app-utils"
|
|||||||
import { CoreV1Api, AuthorizationV1Api } from "@kubernetes/client-node"
|
import { CoreV1Api, AuthorizationV1Api } from "@kubernetes/client-node"
|
||||||
import { Cluster } from "../cluster"
|
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) {
|
async function getAllowedNamespaces(cluster: Cluster) {
|
||||||
const api = cluster.contextHandler.kc.makeApiClient(CoreV1Api)
|
const api = cluster.contextHandler.kc.makeApiClient(CoreV1Api)
|
||||||
const authApi = cluster.contextHandler.kc.makeApiClient(AuthorizationV1Api)
|
|
||||||
try {
|
try {
|
||||||
const namespaceList = await api.listNamespace()
|
const namespaceList = await api.listNamespace()
|
||||||
const nsAccessStatuses = await Promise.all(
|
const nsAccessStatuses = await Promise.all(
|
||||||
namespaceList.body.items.map(ns => {
|
namespaceList.body.items.map(ns => cluster.canI({
|
||||||
return selfSubjectAccessReview(authApi, ns.metadata.name)
|
namespace: ns.metadata.name,
|
||||||
})
|
resource: "pods",
|
||||||
|
verb: "list",
|
||||||
|
}))
|
||||||
)
|
)
|
||||||
return namespaceList.body.items
|
return namespaceList.body.items
|
||||||
.filter((ns, i) => nsAccessStatuses[i].body.status.allowed)
|
.filter((ns, i) => nsAccessStatuses[i])
|
||||||
.map(ns => ns.metadata.name)
|
.map(ns => ns.metadata.name)
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
const kc = cluster.contextHandler.kc
|
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 {
|
class ConfigRoute extends LensApi {
|
||||||
|
|
||||||
public async routeConfig(request: LensApiRequest) {
|
public async routeConfig(request: LensApiRequest) {
|
||||||
@ -56,6 +62,7 @@ class ConfigRoute extends LensApi {
|
|||||||
kubeVersion: cluster.version,
|
kubeVersion: cluster.version,
|
||||||
chartsEnabled: true,
|
chartsEnabled: true,
|
||||||
isClusterAdmin: cluster.isAdmin,
|
isClusterAdmin: cluster.isAdmin,
|
||||||
|
allowedResources: await getAllowedResources(cluster),
|
||||||
allowedNamespaces: await getAllowedNamespaces(cluster)
|
allowedNamespaces: await getAllowedNamespaces(cluster)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user