Skip to content

Commit

Permalink
fix: getAllowedResources for all namespaces using SelfSubjectRulesReview
Browse files Browse the repository at this point in the history
Signed-off-by: Andreas Hippler <[email protected]>
  • Loading branch information
ahippler committed Nov 18, 2022
1 parent 1b6f64c commit 6124e0c
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 19 deletions.
56 changes: 56 additions & 0 deletions src/common/cluster/authorization-namespace-review.injectable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/

import type { KubeConfig } from "@kubernetes/client-node";
import { AuthorizationV1Api } from "@kubernetes/client-node";
import logger from "../logger";
import { getInjectable } from "@ogre-tools/injectable";

export type NamespaceResources = (namespace: string) => Promise<string[]>;

/**
* @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster
*/
export function authorizationNamespaceReview(proxyConfig: KubeConfig): NamespaceResources {
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[]> => {
try {
const { body } = await api.createSelfSubjectRulesReview({
apiVersion: "authorization.k8s.io/v1",
kind: "SelfSubjectRulesReview",
spec: { namespace },
});

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));
}
});

resources.delete("*");

return [...resources];
} catch (error) {
logger.error(`[AUTHORIZATION-NAMESPACE-REVIEW]: failed to create subject rules review: ${error}`, { namespace });

return [];
}
};
}

const authorizationNamespaceReviewInjectable = getInjectable({
id: "authorization-namespace-review",
instantiate: () => authorizationNamespaceReview,
});

export default authorizationNamespaceReviewInjectable;
48 changes: 29 additions & 19 deletions src/common/cluster/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import assert from "assert";
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 { NamespaceResources } from "./authorization-namespace-review.injectable";

export interface ClusterDependencies {
readonly directoryForKubeConfigs: string;
Expand All @@ -34,6 +35,7 @@ export interface ClusterDependencies {
createContextHandler: (cluster: Cluster) => ClusterContextHandler;
createKubectl: (clusterVersion: string) => Kubectl;
createAuthorizationReview: (config: KubeConfig) => CanI;
createAuthorizationNamespaceReview: (config: KubeConfig) => NamespaceResources;
createListNamespaces: (config: KubeConfig) => ListNamespaces;
createVersionDetector: (cluster: Cluster) => VersionDetector;
broadcastMessage: BroadcastMessage;
Expand Down Expand Up @@ -474,6 +476,7 @@ export class Cluster implements ClusterModel, ClusterState {
private async refreshAccessibility(): Promise<void> {
const proxyConfig = await this.getProxyKubeconfig();
const canI = this.dependencies.createAuthorizationReview(proxyConfig);
const getNamespaceResources = this.dependencies.createAuthorizationNamespaceReview(proxyConfig);

this.isAdmin = await canI({
namespace: "kube-system",
Expand All @@ -485,7 +488,7 @@ export class Cluster implements ClusterModel, ClusterState {
resource: "*",
});
this.allowedNamespaces = await this.getAllowedNamespaces(proxyConfig);
this.allowedResources = await this.getAllowedResources(canI);
this.allowedResources = await this.getAllowedResources(getNamespaceResources);
this.ready = true;
}

Expand Down Expand Up @@ -667,32 +670,39 @@ export class Cluster implements ClusterModel, ClusterState {
}
}

protected async getAllowedResources(canI: CanI) {
protected async getAllowedResources(getNamespaceResources: NamespaceResources) {
try {
if (!this.allowedNamespaces.length) {
return [];
}

const resources = apiResources.filter((resource) => this.resourceAccessStatuses.get(resource) === undefined);
const apiLimit = plimit(5); // 5 concurrent api requests
const requests = [];

for (const apiResource of resources) {
requests.push(apiLimit(async () => {
for (const namespace of this.allowedNamespaces.slice(0, 10)) {
if (!this.resourceAccessStatuses.get(apiResource)) {
const result = await canI({
resource: apiResource.apiName,
group: apiResource.group,
verb: "list",
namespace,
});

this.resourceAccessStatuses.set(apiResource, result);
const unknownResources = new Map<string, KubeApiResource>(resources.map(resource => ([resource.apiName, resource])));

if (unknownResources.size > 0) {
const apiLimit = plimit(5); // 5 concurrent api requests

await Promise.all(this.allowedNamespaces.map(namespace => apiLimit(async () => {
if (unknownResources.size === 0) {
return;
}

const namespaceResources = await getNamespaceResources(namespace);

for (const resourceName of namespaceResources) {
const unknownResource = unknownResources.get(resourceName);

if (unknownResource) {
this.resourceAccessStatuses.set(unknownResource, true);
unknownResources.delete(resourceName);
}
}
}));
})));

for (const forbiddenResource of unknownResources.values()) {
this.resourceAccessStatuses.set(forbiddenResource, false);
}
}
await Promise.all(requests);

return apiResources
.filter((resource) => this.resourceAccessStatuses.get(resource))
Expand Down
3 changes: 3 additions & 0 deletions src/main/__test__/cluster.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { getDiForUnitTesting } from "../getDiForUnitTesting";
import type { CreateCluster } from "../../common/cluster/create-cluster-injection-token";
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
import authorizationReviewInjectable from "../../common/cluster/authorization-review.injectable";
import authorizationNamespaceReviewInjectable from "../../common/cluster/authorization-namespace-review.injectable";
import listNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
import type { ClusterContextHandler } from "../context-handler/context-handler";
Expand All @@ -19,6 +20,7 @@ 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";

console = new Console(process.stdout, process.stderr); // fix mockFS

Expand All @@ -39,6 +41,7 @@ describe("create clusters", () => {
di.override(normalizedPlatformInjectable, () => "darwin");
di.override(broadcastMessageInjectable, () => async () => {});
di.override(authorizationReviewInjectable, () => () => () => Promise.resolve(true));
di.override(authorizationNamespaceReviewInjectable, () => () => () => Promise.resolve(Object.keys(apiResourceRecord)));
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 @@ -11,6 +11,7 @@ import createKubectlInjectable from "../kubectl/create-kubectl.injectable";
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
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 loggerInjectable from "../../common/logger.injectable";
import detectorRegistryInjectable from "../cluster-detectors/detector-registry.injectable";
Expand All @@ -28,6 +29,7 @@ const createClusterInjectable = getInjectable({
createKubectl: di.inject(createKubectlInjectable),
createContextHandler: di.inject(createContextHandlerInjectable),
createAuthorizationReview: di.inject(authorizationReviewInjectable),
createAuthorizationNamespaceReview: di.inject(createAuthorizationNamespaceReview),
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 @@ -27,6 +27,7 @@ const createClusterInjectable = getInjectable({
createKubectl: () => { throw new Error("Tried to access back-end feature in front-end.");},
createContextHandler: () => undefined as never,
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."); },
detectorRegistry: undefined as never,
createVersionDetector: () => { throw new Error("Tried to access back-end feature in front-end."); },
Expand Down

0 comments on commit 6124e0c

Please sign in to comment.