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

fix: resolve SelfSubjectRulesReview globs

Signed-off-by: Andreas Hippler <andreas.hippler@goto.com>
This commit is contained in:
Andreas Hippler 2022-11-21 21:20:02 +01:00
parent 3b31afecbe
commit dc3893cfc8
7 changed files with 144 additions and 25 deletions

View File

@ -390,12 +390,6 @@ const scenarios = [
sidebarItemTestId: "sidebar-item-link-for-service-accounts", sidebarItemTestId: "sidebar-item-link-for-service-accounts",
}, },
{
expectedSelector: "h5.title",
parentSidebarItemTestId: "sidebar-item-link-for-user-management",
sidebarItemTestId: "sidebar-item-link-for-roles",
},
{ {
expectedSelector: "h5.title", expectedSelector: "h5.title",
parentSidebarItemTestId: "sidebar-item-link-for-user-management", parentSidebarItemTestId: "sidebar-item-link-for-user-management",
@ -405,7 +399,7 @@ const scenarios = [
{ {
expectedSelector: "h5.title", expectedSelector: "h5.title",
parentSidebarItemTestId: "sidebar-item-link-for-user-management", parentSidebarItemTestId: "sidebar-item-link-for-user-management",
sidebarItemTestId: "sidebar-item-link-for-role-bindings", sidebarItemTestId: "sidebar-item-link-for-roles",
}, },
{ {
@ -417,7 +411,7 @@ const scenarios = [
{ {
expectedSelector: "h5.title", expectedSelector: "h5.title",
parentSidebarItemTestId: "sidebar-item-link-for-user-management", parentSidebarItemTestId: "sidebar-item-link-for-user-management",
sidebarItemTestId: "sidebar-item-link-for-pod-security-policies", sidebarItemTestId: "sidebar-item-link-for-role-bindings",
}, },
{ {

View File

@ -8,8 +8,15 @@ import { AuthorizationV1Api } from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import type { Logger } from "../logger"; import type { Logger } from "../logger";
import loggerInjectable from "../logger.injectable"; import loggerInjectable from "../logger.injectable";
import type { KubeApiResource } from "../rbac";
export type RequestNamespaceResources = (namespace: string) => Promise<string[]>; /**
* Requests the permissions for actions on the kube cluster
* @param namespace The namespace of the resources
* @param availableResources List of available resources in the cluster to resolve glob values fir api groups
* @returns list of allowed resources names
*/
export type RequestNamespaceResources = (namespace: string, availableResources: KubeApiResource[]) => Promise<string[]>;
/** /**
* @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
@ -25,12 +32,7 @@ const authorizationNamespaceReview = ({ logger }: Dependencies): AuthorizationNa
const api = proxyConfig.makeApiClient(AuthorizationV1Api); const api = proxyConfig.makeApiClient(AuthorizationV1Api);
/** return async (namespace, availableResources) => {
* Requests the permissions for actions on the kube cluster
* @param namespace The namespace of the resources
* @returns list of allowed resources
*/
return async (namespace: string): Promise<string[]> => {
try { try {
const { body } = await api.createSelfSubjectRulesReview({ const { body } = await api.createSelfSubjectRulesReview({
apiVersion: "authorization.k8s.io/v1", apiVersion: "authorization.k8s.io/v1",
@ -41,12 +43,27 @@ const authorizationNamespaceReview = ({ logger }: Dependencies): AuthorizationNa
const resources = new Set<string>(); const resources = new Set<string>();
body.status?.resourceRules.forEach(resourceRule => { body.status?.resourceRules.forEach(resourceRule => {
if (resourceRule.verbs.some(verb => ["*", "list"].includes(verb))) { if (!resourceRule.verbs.some(verb => ["*", "list"].includes(verb)) || !resourceRule.resources) {
resourceRule.resources?.forEach(resource => resources.add(resource)); return;
} }
});
resources.delete("*"); const apiGroups = resourceRule.apiGroups;
if (resourceRule.resources.length === 1 && resourceRule.resources[0] === "*" && apiGroups) {
if (apiGroups[0] === "*") {
availableResources.forEach(resource => resources.add(resource.apiName));
} else {
availableResources.forEach((apiResource)=> {
if (apiGroups.includes(apiResource.group || "")) {
resources.add(apiResource.apiName);
}
});
}
} else {
resourceRule.resources.forEach(resource => resources.add(resource));
}
});
return [...resources]; return [...resources];
} catch (error) { } catch (error) {

View File

@ -26,6 +26,7 @@ import type { Logger } from "../logger";
import type { BroadcastMessage } from "../ipc/broadcast-message.injectable"; import type { BroadcastMessage } from "../ipc/broadcast-message.injectable";
import type { LoadConfigfromFile } from "../kube-helpers/load-config-from-file.injectable"; import type { LoadConfigfromFile } from "../kube-helpers/load-config-from-file.injectable";
import type { RequestNamespaceResources } from "./authorization-namespace-review.injectable"; import type { RequestNamespaceResources } from "./authorization-namespace-review.injectable";
import type { RequestListApiResources } from "./list-api-resources.injectable";
export interface ClusterDependencies { export interface ClusterDependencies {
readonly directoryForKubeConfigs: string; readonly directoryForKubeConfigs: string;
@ -36,6 +37,7 @@ export interface ClusterDependencies {
createKubectl: (clusterVersion: string) => Kubectl; createKubectl: (clusterVersion: string) => Kubectl;
createAuthorizationReview: (config: KubeConfig) => CanI; createAuthorizationReview: (config: KubeConfig) => CanI;
createAuthorizationNamespaceReview: (config: KubeConfig) => RequestNamespaceResources; createAuthorizationNamespaceReview: (config: KubeConfig) => RequestNamespaceResources;
createListApiResources: (cluster: Cluster) => RequestListApiResources;
createListNamespaces: (config: KubeConfig) => ListNamespaces; createListNamespaces: (config: KubeConfig) => ListNamespaces;
createVersionDetector: (cluster: Cluster) => VersionDetector; createVersionDetector: (cluster: Cluster) => VersionDetector;
broadcastMessage: BroadcastMessage; broadcastMessage: BroadcastMessage;
@ -477,6 +479,7 @@ export class Cluster implements ClusterModel, ClusterState {
const proxyConfig = await this.getProxyKubeconfig(); const proxyConfig = await this.getProxyKubeconfig();
const canI = this.dependencies.createAuthorizationReview(proxyConfig); const canI = this.dependencies.createAuthorizationReview(proxyConfig);
const requestNamespaceResources = this.dependencies.createAuthorizationNamespaceReview(proxyConfig); const requestNamespaceResources = this.dependencies.createAuthorizationNamespaceReview(proxyConfig);
const listApiResources = this.dependencies.createListApiResources(this);
this.isAdmin = await canI({ this.isAdmin = await canI({
namespace: "kube-system", namespace: "kube-system",
@ -488,7 +491,7 @@ export class Cluster implements ClusterModel, ClusterState {
resource: "*", resource: "*",
}); });
this.allowedNamespaces = await this.getAllowedNamespaces(proxyConfig); this.allowedNamespaces = await this.getAllowedNamespaces(proxyConfig);
this.allowedResources = await this.getAllowedResources(requestNamespaceResources); this.allowedResources = await this.getAllowedResources(listApiResources, requestNamespaceResources);
this.ready = true; this.ready = true;
} }
@ -670,14 +673,23 @@ export class Cluster implements ClusterModel, ClusterState {
} }
} }
protected async getAllowedResources(requestNamespaceResources: RequestNamespaceResources) { protected async getAllowedResources(listApiResources:RequestListApiResources, requestNamespaceResources: RequestNamespaceResources) {
try { try {
if (!this.allowedNamespaces.length) { if (!this.allowedNamespaces.length) {
return []; return [];
} }
const resources = apiResources.filter((resource) => this.resourceAccessStatuses.get(resource) === undefined); const unknownResources = new Map<string, KubeApiResource>(apiResources.map(resource => ([resource.apiName, resource])));
const unknownResources = new Map<string, KubeApiResource>(resources.map(resource => ([resource.apiName, resource])));
const availableResources = await listApiResources();
const availableResourcesNames = new Set(availableResources.map(apiResource => apiResource.apiName));
[...unknownResources.values()].map(unknownResource => {
if (!availableResourcesNames.has(unknownResource.apiName)) {
this.resourceAccessStatuses.set(unknownResource, false);
unknownResources.delete(unknownResource.apiName);
}
});
if (unknownResources.size > 0) { if (unknownResources.size > 0) {
const apiLimit = plimit(5); // 5 concurrent api requests const apiLimit = plimit(5); // 5 concurrent api requests
@ -687,7 +699,7 @@ export class Cluster implements ClusterModel, ClusterState {
return; return;
} }
const namespaceResources = await requestNamespaceResources(namespace); const namespaceResources = await requestNamespaceResources(namespace, availableResources);
for (const resourceName of namespaceResources) { for (const resourceName of namespaceResources) {
const unknownResource = unknownResources.get(resourceName); const unknownResource = unknownResources.get(resourceName);

View File

@ -0,0 +1,91 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type {
V1APIGroupList,
V1APIResourceList,
V1APIVersions,
} from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable";
import type { K8sRequest } from "../../main/k8s-request.injectable";
import k8SRequestInjectable from "../../main/k8s-request.injectable";
import type { Logger } from "../logger";
import loggerInjectable from "../logger.injectable";
import type { KubeApiResource, KubeResource } from "../rbac";
import type { Cluster } from "./cluster";
import plimit from "p-limit";
export type RequestListApiResources = () => Promise<KubeApiResource[]>;
/**
* @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster
*/
export type ListApiResources = (cluster: Cluster) => RequestListApiResources;
interface Dependencies {
logger: Logger;
k8sRequest: K8sRequest;
}
const listApiResources = ({ k8sRequest, logger }: Dependencies): ListApiResources => {
return (cluster) => {
const clusterRequest = (path: string) => k8sRequest(cluster, path);
const apiLimit = plimit(5);
return async () => {
const resources: KubeApiResource[] = [];
try {
const resourceListGroups:{ group:string;path:string }[] = [];
await Promise.all(
[
clusterRequest("/api").then((response:V1APIVersions)=>response.versions.forEach(version => resourceListGroups.push({ group:version, path:`/api/${version}` }))),
clusterRequest("/apis").then((response:V1APIGroupList) => response.groups.forEach(group => {
const preferredVersion = group.preferredVersion?.groupVersion;
if (preferredVersion) {
resourceListGroups.push({ group:group.name, path:`/apis/${preferredVersion}` });
}
})),
],
);
await Promise.all(
resourceListGroups.map(({ group, path }) => apiLimit(async () => {
const apiResources:V1APIResourceList = await clusterRequest(path);
if (apiResources.resources) {
resources.push(
...apiResources.resources.filter(resource => resource.verbs.includes("list")).map((resource) => ({
apiName: resource.name as KubeResource,
kind: resource.kind,
group,
})),
);
}
}),
),
);
} catch (error) {
logger.error(`[LIST-API-RESOURCES]: failed to list api resources: ${error}`);
}
return resources;
};
};
};
const listApiResourcesInjectable = getInjectable({
id: "list-api-resources",
instantiate: (di) => {
const k8sRequest = di.inject(k8SRequestInjectable);
const logger = di.inject(loggerInjectable);
return listApiResources({ k8sRequest, logger });
},
});
export default listApiResourcesInjectable;

View File

@ -20,7 +20,8 @@ import directoryForTempInjectable from "../../common/app-paths/directory-for-tem
import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable"; import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable";
import kubectlBinaryNameInjectable from "../kubectl/binary-name.injectable"; import kubectlBinaryNameInjectable from "../kubectl/binary-name.injectable";
import kubectlDownloadingNormalizedArchInjectable from "../kubectl/normalized-arch.injectable"; import kubectlDownloadingNormalizedArchInjectable from "../kubectl/normalized-arch.injectable";
import { apiResourceRecord } from "../../common/rbac"; import { apiResourceRecord, apiResources } from "../../common/rbac";
import listApiResourcesInjectable from "../../common/cluster/list-api-resources.injectable";
console = new Console(process.stdout, process.stderr); // fix mockFS console = new Console(process.stdout, process.stderr); // fix mockFS
@ -42,6 +43,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(authorizationNamespaceReviewInjectable, () => () => () => Promise.resolve(Object.keys(apiResourceRecord))); di.override(authorizationNamespaceReviewInjectable, () => () => () => Promise.resolve(Object.keys(apiResourceRecord)));
di.override(listApiResourcesInjectable, () => () => () => Promise.resolve(apiResources));
di.override(listNamespacesInjectable, () => () => () => Promise.resolve([ "default" ])); di.override(listNamespacesInjectable, () => () => () => Promise.resolve([ "default" ]));
di.override(createContextHandlerInjectable, () => (cluster) => ({ di.override(createContextHandlerInjectable, () => (cluster) => ({
restartServer: jest.fn(), restartServer: jest.fn(),

View File

@ -13,6 +13,7 @@ import { createClusterInjectionToken } from "../../common/cluster/create-cluster
import authorizationReviewInjectable from "../../common/cluster/authorization-review.injectable"; import authorizationReviewInjectable from "../../common/cluster/authorization-review.injectable";
import createAuthorizationNamespaceReview from "../../common/cluster/authorization-namespace-review.injectable"; import createAuthorizationNamespaceReview from "../../common/cluster/authorization-namespace-review.injectable";
import listNamespacesInjectable from "../../common/cluster/list-namespaces.injectable"; import listNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
import createListApiResourcesInjectable from "../../common/cluster/list-api-resources.injectable";
import loggerInjectable from "../../common/logger.injectable"; import loggerInjectable from "../../common/logger.injectable";
import detectorRegistryInjectable from "../cluster-detectors/detector-registry.injectable"; import detectorRegistryInjectable from "../cluster-detectors/detector-registry.injectable";
import createVersionDetectorInjectable from "../cluster-detectors/create-version-detector.injectable"; import createVersionDetectorInjectable from "../cluster-detectors/create-version-detector.injectable";
@ -30,6 +31,7 @@ const createClusterInjectable = getInjectable({
createContextHandler: di.inject(createContextHandlerInjectable), createContextHandler: di.inject(createContextHandlerInjectable),
createAuthorizationReview: di.inject(authorizationReviewInjectable), createAuthorizationReview: di.inject(authorizationReviewInjectable),
createAuthorizationNamespaceReview: di.inject(createAuthorizationNamespaceReview), createAuthorizationNamespaceReview: di.inject(createAuthorizationNamespaceReview),
createListApiResources: di.inject(createListApiResourcesInjectable),
createListNamespaces: di.inject(listNamespacesInjectable), createListNamespaces: di.inject(listNamespacesInjectable),
logger: di.inject(loggerInjectable), logger: di.inject(loggerInjectable),
detectorRegistry: di.inject(detectorRegistryInjectable), detectorRegistry: di.inject(detectorRegistryInjectable),

View File

@ -29,6 +29,7 @@ const createClusterInjectable = getInjectable({
createAuthorizationReview: () => { throw new Error("Tried to access back-end feature in front-end."); }, createAuthorizationReview: () => { throw new Error("Tried to access back-end feature in front-end."); },
createAuthorizationNamespaceReview: () => { throw new Error("Tried to access back-end feature in front-end."); }, createAuthorizationNamespaceReview: () => { throw new Error("Tried to access back-end feature in front-end."); },
createListNamespaces: () => { throw new Error("Tried to access back-end feature in front-end."); }, createListNamespaces: () => { throw new Error("Tried to access back-end feature in front-end."); },
createListApiResources: ()=> { throw new Error("Tried to access back-end feature in front-end."); },
detectorRegistry: undefined as never, detectorRegistry: undefined as never,
createVersionDetector: () => { throw new Error("Tried to access back-end feature in front-end."); }, createVersionDetector: () => { throw new Error("Tried to access back-end feature in front-end."); },
}; };