Skip to content

Commit

Permalink
fix: resolve SelfSubjectRulesReview globs
Browse files Browse the repository at this point in the history
Signed-off-by: Andreas Hippler <[email protected]>
  • Loading branch information
ahippler committed Nov 21, 2022
1 parent ed33d64 commit be9d0cf
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 17 deletions.
39 changes: 28 additions & 11 deletions src/common/cluster/authorization-namespace-review.injectable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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",
Expand All @@ -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) {
Expand Down
22 changes: 17 additions & 5 deletions src/common/cluster/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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",
Expand All @@ -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;
}

Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down
90 changes: 90 additions & 0 deletions src/common/cluster/list-api-resources.injectable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/

import type {
V1APIGroupList,
V1APIResourceList,
} 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 response = (await clusterRequest("/apis")) as V1APIGroupList;

const requests: Promise<void>[] = [];

if (!response.groups) {
return resources;
}

for (const group of response.groups) {
const endPoint = group.preferredVersion?.groupVersion;

if (endPoint === undefined) {
continue;
}

requests.push(apiLimit(async () => {
const apiResources = (await clusterRequest(`/apis/${endPoint}`)) as V1APIResourceList;

if (apiResources.resources) {
resources.push(
...apiResources.resources.map((resource) => ({
apiName: resource.name as KubeResource,
group: group.name,
kind: resource.kind,
})),
);
}
}));
}

await Promise.all(requests);
} 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;
4 changes: 3 additions & 1 deletion src/main/__test__/cluster.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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(),
Expand Down
2 changes: 2 additions & 0 deletions src/main/create-cluster/create-cluster.injectable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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),
Expand Down
1 change: 1 addition & 0 deletions src/renderer/create-cluster/create-cluster.injectable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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."); },
};
Expand Down

0 comments on commit be9d0cf

Please sign in to comment.