diff --git a/dashboard/client/api/json-api.ts b/dashboard/client/api/json-api.ts index abf278497a80..5f50296b0da9 100644 --- a/dashboard/client/api/json-api.ts +++ b/dashboard/client/api/json-api.ts @@ -105,8 +105,9 @@ export class JsonApi { this.onData.emit(data, res); this.writeLog({ ...log, data }); return data; - } - else { + } else if (log.method === "GET" && res.status === 403) { + this.writeLog({ ...log, data }); + } else { const error = new JsonApiErrorParsed(data, this.parseError(data, res)); this.onError.emit(error, res); this.writeLog({ ...log, error }) diff --git a/dashboard/client/api/kube-watch-api.ts b/dashboard/client/api/kube-watch-api.ts index b6abe9fb1866..3628f9d79073 100644 --- a/dashboard/client/api/kube-watch-api.ts +++ b/dashboard/client/api/kube-watch-api.ts @@ -107,8 +107,12 @@ export class KubeWatchApi { const { apiBase, namespace } = KubeApi.parseApi(url); const api = apiManager.getApi(apiBase); if (api) { - await api.refreshResourceVersion({ namespace }); - this.reconnect(); + try { + await api.refreshResourceVersion({ namespace }); + this.reconnect(); + } catch(error) { + console.debug("failed to refresh resource version", error) + } } } } diff --git a/dashboard/client/api/rbac.ts b/dashboard/client/api/rbac.ts new file mode 100644 index 000000000000..fb33b82334bd --- /dev/null +++ b/dashboard/client/api/rbac.ts @@ -0,0 +1,6 @@ +import { configStore } from "../config.store"; + +export function isAllowedResource(resource: string) { + const { allowedResources } = configStore; + return allowedResources.includes(resource) +} diff --git a/dashboard/client/components/+cluster/cluster.tsx b/dashboard/client/components/+cluster/cluster.tsx index 979f811067cc..13df2cad0a76 100644 --- a/dashboard/client/components/+cluster/cluster.tsx +++ b/dashboard/client/components/+cluster/cluster.tsx @@ -13,6 +13,7 @@ import { nodesStore } from "../+nodes/nodes.store"; import { podsStore } from "../+workloads-pods/pods.store"; import { clusterStore } from "./cluster.store"; import { eventStore } from "../+events/event.store"; +import { isAllowedResource } from "../../api/rbac"; @observer export class Cluster extends React.Component { @@ -25,6 +26,9 @@ export class Cluster extends React.Component { async componentDidMount() { const { dependentStores } = this; + if (!isAllowedResource("nodes")) { + dependentStores.splice(dependentStores.indexOf(nodesStore), 1) + } this.watchers.forEach(watcher => watcher.start(true)); await Promise.all([ diff --git a/dashboard/client/components/+config/config.tsx b/dashboard/client/components/+config/config.tsx index 935cd9affd0d..71c6ead90702 100644 --- a/dashboard/client/components/+config/config.tsx +++ b/dashboard/client/components/+config/config.tsx @@ -9,8 +9,8 @@ import { namespaceStore } from "../+namespaces/namespace.store"; import { resourceQuotaRoute, ResourceQuotas, resourceQuotaURL } from "../+config-resource-quotas"; import { configURL } from "./config.route"; import { HorizontalPodAutoscalers, hpaRoute, hpaURL } from "../+config-autoscalers"; -import { Certificates, ClusterIssuers, Issuers } from "../+custom-resources/certmanager.k8s.io"; import { buildURL } from "../../navigation"; +import { isAllowedResource } from "../../api/rbac" export const certificatesURL = buildURL("/certificates"); export const issuersURL = buildURL("/issuers"); @@ -20,32 +20,40 @@ export const clusterIssuersURL = buildURL("/clusterissuers"); export class Config extends React.Component { static get tabRoutes(): TabRoute[] { const query = namespaceStore.getContextParams() - return [ - { + const routes: TabRoute[] = [] + if (isAllowedResource("configmaps")) { + routes.push({ title: ConfigMaps, component: ConfigMaps, url: configMapsURL({ query }), path: configMapsRoute.path, - }, - { + }) + } + if (isAllowedResource("secrets")) { + routes.push({ title: Secrets, component: Secrets, url: secretsURL({ query }), path: secretsRoute.path, - }, - { + }) + } + if (isAllowedResource("resourcequotas")) { + routes.push({ title: Resource Quotas, component: ResourceQuotas, url: resourceQuotaURL({ query }), path: resourceQuotaRoute.path, - }, - { + }) + } + if (isAllowedResource("horizontalpodautoscalers")) { + routes.push({ title: HPA, component: HorizontalPodAutoscalers, url: hpaURL({ query }), path: hpaRoute.path, - }, - ] + }) + } + return routes; } render() { diff --git a/dashboard/client/components/+namespaces/namespace-select.tsx b/dashboard/client/components/+namespaces/namespace-select.tsx index b5004685c9e0..b4b5fd15cbd6 100644 --- a/dashboard/client/components/+namespaces/namespace-select.tsx +++ b/dashboard/client/components/+namespaces/namespace-select.tsx @@ -11,6 +11,7 @@ import { namespaceStore } from "./namespace.store"; import { _i18n } from "../../i18n"; import { FilterIcon } from "../item-object-list/filter-icon"; import { FilterType } from "../item-object-list/page-filters.store"; +import { isAllowedResource } from "../../api/rbac" interface Props extends SelectProps { showIcons?: boolean; @@ -33,7 +34,9 @@ export class NamespaceSelect extends React.Component { private unsubscribe = noop; async componentDidMount() { - if (!namespaceStore.isLoaded) await namespaceStore.loadAll(); + if (isAllowedResource("namespaces") && !namespaceStore.isLoaded) { + await namespaceStore.loadAll(); + } this.unsubscribe = namespaceStore.subscribe(); } diff --git a/dashboard/client/components/+network/network.tsx b/dashboard/client/components/+network/network.tsx index 409bde8fc28a..768d73254766 100644 --- a/dashboard/client/components/+network/network.tsx +++ b/dashboard/client/components/+network/network.tsx @@ -12,6 +12,7 @@ import { Ingresses, ingressRoute, ingressURL } from "../+network-ingresses"; import { NetworkPolicies, networkPoliciesRoute, networkPoliciesURL } from "../+network-policies"; import { namespaceStore } from "../+namespaces/namespace.store"; import { networkURL } from "./network.route"; +import { isAllowedResource } from "../../api/rbac"; interface Props extends RouteComponentProps<{}> { } @@ -20,32 +21,40 @@ interface Props extends RouteComponentProps<{}> { export class Network extends React.Component { static get tabRoutes(): TabRoute[] { const query = namespaceStore.getContextParams() - return [ - { + const routes: TabRoute[] = []; + if (isAllowedResource("services")) { + routes.push({ title: Services, component: Services, url: servicesURL({ query }), path: servicesRoute.path, - }, - { + }) + } + if (isAllowedResource("endpoints")) { + routes.push({ title: Endpoints, component: Endpoints, url: endpointURL({ query }), path: endpointRoute.path, - }, - { + }) + } + if (isAllowedResource("ingresses")) { + routes.push({ title: Ingresses, component: Ingresses, url: ingressURL({ query }), path: ingressRoute.path, - }, - { + }) + } + if (isAllowedResource("networkpolicies")) { + routes.push({ title: Network Policies, component: NetworkPolicies, url: networkPoliciesURL({ query }), path: networkPoliciesRoute.path, - }, - ] + }) + } + return routes } render() { @@ -59,4 +68,4 @@ export class Network extends React.Component { ) } -} \ No newline at end of file +} diff --git a/dashboard/client/components/+workloads-overview/overview-statuses.scss b/dashboard/client/components/+workloads-overview/overview-statuses.scss index 7160d6782156..21e6e17cfb31 100644 --- a/dashboard/client/components/+workloads-overview/overview-statuses.scss +++ b/dashboard/client/components/+workloads-overview/overview-statuses.scss @@ -16,10 +16,11 @@ .workloads { display: grid; grid-template-columns: repeat(auto-fit, 155px); - justify-content: space-between; + justify-content: space-evenly; grid-gap: $margin; padding: $padding * 2; + .workload { margin-bottom: $margin * 2; @@ -32,4 +33,4 @@ } } } -} \ No newline at end of file +} diff --git a/dashboard/client/components/+workloads-overview/overview-statuses.tsx b/dashboard/client/components/+workloads-overview/overview-statuses.tsx index 3c0a8cac0dc2..83575fec86c9 100644 --- a/dashboard/client/components/+workloads-overview/overview-statuses.tsx +++ b/dashboard/client/components/+workloads-overview/overview-statuses.tsx @@ -15,17 +15,20 @@ import { cronJobStore } from "../+workloads-cronjobs/cronjob.store"; import { namespaceStore } from "../+namespaces/namespace.store"; import { PageFiltersList } from "../item-object-list/page-filters-list"; import { NamespaceSelectFilter } from "../+namespaces/namespace-select"; +import { configStore } from "../../config.store"; +import { isAllowedResource } from "../../api/rbac"; @observer export class OverviewStatuses extends React.Component { render() { + const { allowedResources } = configStore; const { contextNs } = namespaceStore; - const pods = podsStore.getAllByNs(contextNs); - const deployments = deploymentStore.getAllByNs(contextNs); - const statefulSets = statefulSetStore.getAllByNs(contextNs); - const daemonSets = daemonSetStore.getAllByNs(contextNs); - const jobs = jobStore.getAllByNs(contextNs); - const cronJobs = cronJobStore.getAllByNs(contextNs); + const pods = isAllowedResource("pods") ? podsStore.getAllByNs(contextNs) : []; + const deployments = isAllowedResource("deployments") ? deploymentStore.getAllByNs(contextNs) : []; + const statefulSets = isAllowedResource("statefulsets") ? statefulSetStore.getAllByNs(contextNs) : []; + const daemonSets = isAllowedResource("daemonsets") ? daemonSetStore.getAllByNs(contextNs) : []; + const jobs = isAllowedResource("jobs") ? jobStore.getAllByNs(contextNs) : []; + const cronJobs = isAllowedResource("cronjobs") ? cronJobStore.getAllByNs(contextNs) : []; return (
@@ -34,30 +37,42 @@ export class OverviewStatuses extends React.Component {
+ { isAllowedResource("pods") &&
Pods ({pods.length})
+ } + { isAllowedResource("deployments") &&
Deployments ({deployments.length})
+ } + { isAllowedResource("statefulsets") &&
StatefulSets ({statefulSets.length})
+ } + { isAllowedResource("daemonsets") &&
DaemonSets ({daemonSets.length})
+ } + { isAllowedResource("jobs") &&
Jobs ({jobs.length})
+ } + { isAllowedResource("cronjobs") &&
CronJobs ({cronJobs.length})
+ }
) diff --git a/dashboard/client/components/+workloads-overview/overview.tsx b/dashboard/client/components/+workloads-overview/overview.tsx index f47fd7ae195e..c3f08d13c2b6 100644 --- a/dashboard/client/components/+workloads-overview/overview.tsx +++ b/dashboard/client/components/+workloads-overview/overview.tsx @@ -16,6 +16,8 @@ import { jobStore } from "../+workloads-jobs/job.store"; import { cronJobStore } from "../+workloads-cronjobs/cronjob.store"; import { Spinner } from "../spinner"; import { Events } from "../+events"; +import { KubeObjectStore } from "../../kube-object.store"; +import { isAllowedResource } from "../../api/rbac" interface Props extends RouteComponentProps { } @@ -26,16 +28,31 @@ export class WorkloadsOverview extends React.Component { @observable isUnmounting = false; async componentDidMount() { - const stores = [ - podsStore, - deploymentStore, - daemonSetStore, - statefulSetStore, - replicaSetStore, - jobStore, - cronJobStore, - eventStore, - ]; + const stores: KubeObjectStore[] = []; + if (isAllowedResource("pods")) { + stores.push(podsStore); + } + if (isAllowedResource("deployments")) { + stores.push(deploymentStore); + } + if (isAllowedResource("daemonsets")) { + stores.push(daemonSetStore); + } + if (isAllowedResource("statefulsets")) { + stores.push(statefulSetStore); + } + if (isAllowedResource("replicasets")) { + stores.push(replicaSetStore); + } + if (isAllowedResource("jobs")) { + stores.push(jobStore); + } + if (isAllowedResource("cronjobs")) { + stores.push(cronJobStore); + } + if (isAllowedResource("events")) { + stores.push(eventStore); + } this.isReady = stores.every(store => store.isLoaded); await Promise.all(stores.map(store => store.loadAll())); this.isReady = true; @@ -55,11 +72,11 @@ export class WorkloadsOverview extends React.Component { return ( <> - + /> } ) } @@ -71,4 +88,4 @@ export class WorkloadsOverview extends React.Component { ) } -} \ No newline at end of file +} diff --git a/dashboard/client/components/+workloads/workloads.tsx b/dashboard/client/components/+workloads/workloads.tsx index 855151e72c95..895f1ab3a246 100644 --- a/dashboard/client/components/+workloads/workloads.tsx +++ b/dashboard/client/components/+workloads/workloads.tsx @@ -15,6 +15,7 @@ import { DaemonSets } from "../+workloads-daemonsets"; import { StatefulSets } from "../+workloads-statefulsets"; import { Jobs } from "../+workloads-jobs"; import { CronJobs } from "../+workloads-cronjobs"; +import { isAllowedResource } from "../../api/rbac" interface Props extends RouteComponentProps { } @@ -23,50 +24,63 @@ interface Props extends RouteComponentProps { export class Workloads extends React.Component { static get tabRoutes(): TabRoute[] { const query = namespaceStore.getContextParams(); - return [ + const routes: TabRoute[] = [ { title: Overview, component: WorkloadsOverview, url: overviewURL({ query }), path: overviewRoute.path - }, - { + } + ] + if (isAllowedResource("pods")) { + routes.push({ title: Pods, component: Pods, url: podsURL({ query }), path: podsRoute.path - }, - { + }) + } + if (isAllowedResource("deployments")) { + routes.push({ title: Deployments, component: Deployments, url: deploymentsURL({ query }), path: deploymentsRoute.path, - }, - { + }) + } + if (isAllowedResource("daemonsets")) { + routes.push({ title: DaemonSets, component: DaemonSets, url: daemonSetsURL({ query }), path: daemonSetsRoute.path, - }, - { + }) + } + if (isAllowedResource("statefulsets")) { + routes.push({ title: StatefulSets, component: StatefulSets, url: statefulSetsURL({ query }), path: statefulSetsRoute.path, - }, - { + }) + } + if (isAllowedResource("jobs")) { + routes.push({ title: Jobs, component: Jobs, url: jobsURL({ query }), path: jobsRoute.path, - }, - { + }) + } + if (isAllowedResource("cronjobs")) { + routes.push({ title: CronJobs, component: CronJobs, url: cronJobsURL({ query }), path: cronJobsRoute.path, - }, - ] + }) + } + return routes; }; render() { @@ -80,4 +94,4 @@ export class Workloads extends React.Component { ) } -} \ No newline at end of file +} diff --git a/dashboard/client/components/app.tsx b/dashboard/client/components/app.tsx index 516c2d4903bb..b07d72749c56 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 = clusterURL(); + const homeUrl = configStore.isClusterAdmin ? clusterURL() : workloadsURL(); return ( diff --git a/dashboard/client/components/item-object-list/item-list-layout.tsx b/dashboard/client/components/item-object-list/item-list-layout.tsx index 06cda8804517..eb5dac108cd0 100644 --- a/dashboard/client/components/item-object-list/item-list-layout.tsx +++ b/dashboard/client/components/item-object-list/item-list-layout.tsx @@ -114,10 +114,15 @@ export class ItemListLayout extends React.Component { const { store, dependentStores, isClusterScoped } = this.props; const stores = [store, ...dependentStores]; if (!isClusterScoped) stores.push(namespaceStore); - await Promise.all(stores.map(store => store.loadAll())); - const subscriptions = stores.map(store => store.subscribe()); + try { + await Promise.all(stores.map(store => store.loadAll())); + const subscriptions = stores.map(store => store.subscribe()); + + subscriptions.forEach(dispose => dispose()); // unsubscribe all + } catch(error) { + console.log("catched", error) + } await when(() => this.isUnmounting); - subscriptions.forEach(dispose => dispose()); // unsubscribe all } componentWillUnmount() { diff --git a/dashboard/client/components/layout/sidebar.tsx b/dashboard/client/components/layout/sidebar.tsx index e005ecd87c5c..5cd77c6746bb 100644 --- a/dashboard/client/components/layout/sidebar.tsx +++ b/dashboard/client/components/layout/sidebar.tsx @@ -28,6 +28,7 @@ import { crdStore } from "../+custom-resources/crd.store"; import { CrdList, crdResourcesRoute, crdRoute, crdURL } from "../+custom-resources"; import { CustomResources } from "../+custom-resources/custom-resources"; import { navigation } from "../../navigation"; +import { isAllowedResource } from "../../api/rbac" const SidebarContext = React.createContext({ pinned: false }); type SidebarContextValue = { @@ -43,7 +44,7 @@ interface Props { @observer export class Sidebar extends React.Component { async componentDidMount() { - if (!crdStore.isLoaded) crdStore.loadAll() + if (!crdStore.isLoaded && isAllowedResource('customresourcedefinitions')) crdStore.loadAll() } renderCustomResources() { @@ -71,7 +72,7 @@ export class Sidebar extends React.Component { render() { const { toggle, isPinned, className } = this.props; - const { isClusterAdmin, allowedResources } = configStore; + const { allowedResources } = configStore; const query = namespaceStore.getContextParams(); return ( @@ -91,19 +92,21 @@ export class Sidebar extends React.Component {
Cluster} icon={} /> Nodes} icon={} /> { /> { /> { /> { /> } text={Namespaces} /> } @@ -165,7 +173,7 @@ export class Sidebar extends React.Component { /> cluster.canI({ resource: resource, - verb: "list" + verb: "list", + namespace: namespaces[0] })) ) return resources @@ -55,6 +74,7 @@ class ConfigRoute extends LensApi { public async routeConfig(request: LensApiRequest) { const { params, response, cluster} = request + const namespaces = await getAllowedNamespaces(cluster) const data = { clusterName: cluster.contextName, lensVersion: getAppVersion(), @@ -62,8 +82,8 @@ class ConfigRoute extends LensApi { kubeVersion: cluster.version, chartsEnabled: true, isClusterAdmin: cluster.isAdmin, - allowedResources: await getAllowedResources(cluster), - allowedNamespaces: await getAllowedNamespaces(cluster) + allowedResources: await getAllowedResources(cluster, namespaces), + allowedNamespaces: namespaces }; this.respondJson(response, data)