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:
parent
3b31afecbe
commit
dc3893cfc8
@ -390,12 +390,6 @@ const scenarios = [
|
||||
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",
|
||||
parentSidebarItemTestId: "sidebar-item-link-for-user-management",
|
||||
@ -405,7 +399,7 @@ const scenarios = [
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
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",
|
||||
parentSidebarItemTestId: "sidebar-item-link-for-user-management",
|
||||
sidebarItemTestId: "sidebar-item-link-for-pod-security-policies",
|
||||
sidebarItemTestId: "sidebar-item-link-for-role-bindings",
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@ -8,8 +8,15 @@ import { AuthorizationV1Api } from "@kubernetes/client-node";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { Logger } from "../logger";
|
||||
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
|
||||
@ -25,12 +32,7 @@ const authorizationNamespaceReview = ({ logger }: Dependencies): AuthorizationNa
|
||||
|
||||
const api = proxyConfig.makeApiClient(AuthorizationV1Api);
|
||||
|
||||
/**
|
||||
* 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[]> => {
|
||||
return async (namespace, availableResources) => {
|
||||
try {
|
||||
const { body } = await api.createSelfSubjectRulesReview({
|
||||
apiVersion: "authorization.k8s.io/v1",
|
||||
@ -41,12 +43,27 @@ const authorizationNamespaceReview = ({ logger }: Dependencies): AuthorizationNa
|
||||
const resources = new Set<string>();
|
||||
|
||||
body.status?.resourceRules.forEach(resourceRule => {
|
||||
if (resourceRule.verbs.some(verb => ["*", "list"].includes(verb))) {
|
||||
resourceRule.resources?.forEach(resource => resources.add(resource));
|
||||
if (!resourceRule.verbs.some(verb => ["*", "list"].includes(verb)) || !resourceRule.resources) {
|
||||
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];
|
||||
} catch (error) {
|
||||
|
||||
@ -26,6 +26,7 @@ import type { Logger } from "../logger";
|
||||
import type { BroadcastMessage } from "../ipc/broadcast-message.injectable";
|
||||
import type { LoadConfigfromFile } from "../kube-helpers/load-config-from-file.injectable";
|
||||
import type { RequestNamespaceResources } from "./authorization-namespace-review.injectable";
|
||||
import type { RequestListApiResources } from "./list-api-resources.injectable";
|
||||
|
||||
export interface ClusterDependencies {
|
||||
readonly directoryForKubeConfigs: string;
|
||||
@ -36,6 +37,7 @@ export interface ClusterDependencies {
|
||||
createKubectl: (clusterVersion: string) => Kubectl;
|
||||
createAuthorizationReview: (config: KubeConfig) => CanI;
|
||||
createAuthorizationNamespaceReview: (config: KubeConfig) => RequestNamespaceResources;
|
||||
createListApiResources: (cluster: Cluster) => RequestListApiResources;
|
||||
createListNamespaces: (config: KubeConfig) => ListNamespaces;
|
||||
createVersionDetector: (cluster: Cluster) => VersionDetector;
|
||||
broadcastMessage: BroadcastMessage;
|
||||
@ -477,6 +479,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
const proxyConfig = await this.getProxyKubeconfig();
|
||||
const canI = this.dependencies.createAuthorizationReview(proxyConfig);
|
||||
const requestNamespaceResources = this.dependencies.createAuthorizationNamespaceReview(proxyConfig);
|
||||
const listApiResources = this.dependencies.createListApiResources(this);
|
||||
|
||||
this.isAdmin = await canI({
|
||||
namespace: "kube-system",
|
||||
@ -488,7 +491,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
resource: "*",
|
||||
});
|
||||
this.allowedNamespaces = await this.getAllowedNamespaces(proxyConfig);
|
||||
this.allowedResources = await this.getAllowedResources(requestNamespaceResources);
|
||||
this.allowedResources = await this.getAllowedResources(listApiResources, requestNamespaceResources);
|
||||
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 {
|
||||
if (!this.allowedNamespaces.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const resources = apiResources.filter((resource) => this.resourceAccessStatuses.get(resource) === undefined);
|
||||
const unknownResources = new Map<string, KubeApiResource>(resources.map(resource => ([resource.apiName, resource])));
|
||||
const unknownResources = new Map<string, KubeApiResource>(apiResources.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) {
|
||||
const apiLimit = plimit(5); // 5 concurrent api requests
|
||||
@ -687,7 +699,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
return;
|
||||
}
|
||||
|
||||
const namespaceResources = await requestNamespaceResources(namespace);
|
||||
const namespaceResources = await requestNamespaceResources(namespace, availableResources);
|
||||
|
||||
for (const resourceName of namespaceResources) {
|
||||
const unknownResource = unknownResources.get(resourceName);
|
||||
|
||||
91
src/common/cluster/list-api-resources.injectable.ts
Normal file
91
src/common/cluster/list-api-resources.injectable.ts
Normal 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;
|
||||
@ -20,7 +20,8 @@ import directoryForTempInjectable from "../../common/app-paths/directory-for-tem
|
||||
import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable";
|
||||
import kubectlBinaryNameInjectable from "../kubectl/binary-name.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
|
||||
|
||||
@ -42,6 +43,7 @@ describe("create clusters", () => {
|
||||
di.override(broadcastMessageInjectable, () => async () => {});
|
||||
di.override(authorizationReviewInjectable, () => () => () => Promise.resolve(true));
|
||||
di.override(authorizationNamespaceReviewInjectable, () => () => () => Promise.resolve(Object.keys(apiResourceRecord)));
|
||||
di.override(listApiResourcesInjectable, () => () => () => Promise.resolve(apiResources));
|
||||
di.override(listNamespacesInjectable, () => () => () => Promise.resolve([ "default" ]));
|
||||
di.override(createContextHandlerInjectable, () => (cluster) => ({
|
||||
restartServer: jest.fn(),
|
||||
|
||||
@ -13,6 +13,7 @@ import { createClusterInjectionToken } from "../../common/cluster/create-cluster
|
||||
import authorizationReviewInjectable from "../../common/cluster/authorization-review.injectable";
|
||||
import createAuthorizationNamespaceReview from "../../common/cluster/authorization-namespace-review.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 detectorRegistryInjectable from "../cluster-detectors/detector-registry.injectable";
|
||||
import createVersionDetectorInjectable from "../cluster-detectors/create-version-detector.injectable";
|
||||
@ -30,6 +31,7 @@ const createClusterInjectable = getInjectable({
|
||||
createContextHandler: di.inject(createContextHandlerInjectable),
|
||||
createAuthorizationReview: di.inject(authorizationReviewInjectable),
|
||||
createAuthorizationNamespaceReview: di.inject(createAuthorizationNamespaceReview),
|
||||
createListApiResources: di.inject(createListApiResourcesInjectable),
|
||||
createListNamespaces: di.inject(listNamespacesInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
detectorRegistry: di.inject(detectorRegistryInjectable),
|
||||
|
||||
@ -29,6 +29,7 @@ const createClusterInjectable = getInjectable({
|
||||
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."); },
|
||||
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,
|
||||
createVersionDetector: () => { throw new Error("Tried to access back-end feature in front-end."); },
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user