diff --git a/packages/core/src/common/cluster/create-request-namespace-list-permissions.injectable.ts b/packages/core/src/common/cluster/create-request-namespace-list-permissions.injectable.ts index 1aca3dbe6e..b3e8875dc1 100644 --- a/packages/core/src/common/cluster/create-request-namespace-list-permissions.injectable.ts +++ b/packages/core/src/common/cluster/create-request-namespace-list-permissions.injectable.ts @@ -39,18 +39,12 @@ const createRequestNamespaceListPermissionsInjectable = getInjectable({ const { resourceRules } = status; - return (resource) => { - const rules = resourceRules.filter(({ - apiGroups = ["*"], resources = ["*"], - }) => { - const isAboutRelevantApiGroup = apiGroups.includes("*") || apiGroups.includes(resource.group); - const isAboutResource = resources.includes("*") || resources.includes(resource.apiName); - - return isAboutRelevantApiGroup && isAboutResource; - }); - - return rules.some(({ verbs }) => verbs.includes("*") || verbs.includes("list")); - }; + return (resource) => ( + resourceRules + .filter(({ apiGroups = ["*"] }) => apiGroups.includes("*") || apiGroups.includes(resource.group)) + .filter(({ resources = ["*"] }) => resources.includes("*") || resources.includes(resource.apiName)) + .some(({ verbs }) => verbs.includes("*") || verbs.includes("list")) + ); } catch (error) { logger.error(`[AUTHORIZATION-NAMESPACE-REVIEW]: failed to create subject rules review`, { namespace, error }); diff --git a/packages/core/src/common/cluster/request-namespace-list-permissions.test.ts b/packages/core/src/common/cluster/request-namespace-list-permissions.test.ts index c62f69ca8e..194b5ce7e5 100644 --- a/packages/core/src/common/cluster/request-namespace-list-permissions.test.ts +++ b/packages/core/src/common/cluster/request-namespace-list-permissions.test.ts @@ -3,334 +3,225 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { V1SubjectRulesReviewStatus } from "@kubernetes/client-node"; +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; +import type { AuthorizationV1Api, V1SubjectRulesReviewStatus } from "@kubernetes/client-node"; import type { DiContainer } from "@ogre-tools/injectable"; +import type { IncomingMessage } from "http"; +import { anyObject } from "jest-mock-extended"; import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; -import type { RequestNamespaceListPermissionsFor } from "./request-namespace-list-permissions.injectable"; -import requestNamespaceListPermissionsForInjectable from "./request-namespace-list-permissions.injectable"; +import { cast } from "../../test-utils/cast"; +import type { KubeApiResource } from "../rbac"; +import type { RequestNamespaceListPermissions } from "./create-request-namespace-list-permissions.injectable"; +import createRequestNamespaceListPermissionsInjectable from "./create-request-namespace-list-permissions.injectable"; -const createStubProxyConfig = (statusResponse: Promise<{ body: { status: V1SubjectRulesReviewStatus }}>) => ({ - makeApiClient: () => ({ - createSelfSubjectRulesReview: (): Promise<{ body: { status: V1SubjectRulesReviewStatus }}> => statusResponse, - }), -}); +interface TestCase { + description: string; + status: V1SubjectRulesReviewStatus; + expected: boolean; +} describe("requestNamespaceListPermissions", () => { let di: DiContainer; - let requestNamespaceListPermissions: RequestNamespaceListPermissionsFor; + let createSelfSubjectRulesReviewMock: AsyncFnMock; + let requestNamespaceListPermissions: RequestNamespaceListPermissions; beforeEach(() => { di = getDiForUnitTesting(); - requestNamespaceListPermissions = di.inject(requestNamespaceListPermissionsForInjectable); + + const createRequestNamespaceListPermissions = di.inject(createRequestNamespaceListPermissionsInjectable); + + createSelfSubjectRulesReviewMock = asyncFn(); + + requestNamespaceListPermissions = createRequestNamespaceListPermissions(cast({ + createSelfSubjectRulesReview: createSelfSubjectRulesReviewMock, + })); }); - describe("when api returns incomplete data", () => { - it("returns truthy function", async () => { - const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( - new Promise((resolve) => resolve({ - body: { - status: { - incomplete: true, - resourceRules: [], - nonResourceRules: [], - }, - }, - })), - ) as any); + describe("when a request for list permissions in a namespace has been started", () => { + let request: ReturnType; - const permissionCheck = await requestPermissions("irrelevant-namespace"); - - expect(permissionCheck({ - apiName: "pods", - group: "", - kind: "Pod", - namespaced: true, - })).toBeTruthy(); + beforeEach(() => { + request = requestNamespaceListPermissions("irrelevant-namespace"); }); - }); - describe("when api rejects", () => { - it("returns truthy function", async () => { - const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( - new Promise((resolve, reject) => reject("unknown error")), - ) as any); - - const permissionCheck = await requestPermissions("irrelevant-namespace"); - - expect(permissionCheck({ - apiName: "pods", - group: "", - kind: "Pod", - namespaced: true, - })).toBeTruthy(); + it("should request the creation of a SelfSubjectRulesReview", () => { + expect(createSelfSubjectRulesReviewMock).toBeCalledWith(anyObject({ + spec: { + namespace: "irrelevant-namespace", + }, + })); }); - }); - describe("when first resourceRule has all permissions for everything", () => { - it("return truthy function", async () => { - const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( - new Promise((resolve) => resolve({ - body: { - status: { - incomplete: false, - resourceRules: [ - { - apiGroups: ["*"], - verbs: ["*"], - }, - { - apiGroups: ["*"], - verbs: ["get"], - }, - ], - nonResourceRules: [], + ([ + { + description: "incomplete data", + status: { + incomplete: true, + resourceRules: [], + nonResourceRules: [], + }, + expected: true, + }, + { + description: "first resourceRule has all permissions for everything", + status: { + incomplete: false, + resourceRules: [ + { + apiGroups: ["*"], + verbs: ["*"], }, - }, - })), - ) as any); + { + apiGroups: ["*"], + verbs: ["get"], + }, + ], + nonResourceRules: [], + }, + expected: true, + }, + { + description: "first resourceRule has list permissions for everything", + status: { + incomplete: false, + resourceRules: [ + { + apiGroups: ["*"], + verbs: ["list"], + }, + { + apiGroups: ["*"], + verbs: ["get"], + }, + ], + nonResourceRules: [], + }, + expected: true, + }, + { + description: "first resourceRule has list permissions for asked resource", + status: { + incomplete: false, + resourceRules: [ + { + apiGroups: ["some-api-group"], + resources: ["some-kind"], + verbs: ["list"], + }, + { + apiGroups: ["*"], + verbs: ["get"], + }, + ], + nonResourceRules: [], + }, + expected: true, + }, + { + description: "last resourceRule has all permissions for everything", + status: { + incomplete: false, + resourceRules: [ + { + apiGroups: ["*"], + verbs: ["get"], + }, + { + apiGroups: ["*"], + verbs: ["*"], + }, + ], + nonResourceRules: [], + }, + expected: true, + }, + { + description: "last resourceRule has list permissions for asked resource", + status: { + incomplete: false, + resourceRules: [ + { + apiGroups: ["*"], + verbs: ["get"], + }, + { + apiGroups: ["some-api-group"], + resources: ["some-kind"], + verbs: ["list"], + }, + ], + nonResourceRules: [], + }, + expected: true, + }, + { + description: "resourceRules has matching resource without list verb", + status: { + incomplete: false, + resourceRules: [ + { + apiGroups: ["some-api-group"], + resources: ["some-kind"], + verbs: ["get"], + }, + ], + nonResourceRules: [], + }, + expected: false, + }, + { + description: "resourceRules has no matching resource with list verb", + status: { + incomplete: false, + resourceRules: [ + { + apiGroups: [""], + resources: ["services"], + verbs: ["list"], + }, + ], + nonResourceRules: [], + }, + expected: false, + }, + ] as TestCase[]).forEach(({ description, status, expected }) => { + describe(`when api returns ${description}`, () => { + beforeEach(async () => { + await createSelfSubjectRulesReviewMock.resolve({ + body: { + status, + spec: {}, + }, + response: null as unknown as IncomingMessage, + }); + }); - const permissionCheck = await requestPermissions("irrelevant-namespace"); + it(`allows the request to complete, and 'canListResource' will return ${expected}`, async () => { + const canListResource = await request; - expect(permissionCheck({ - apiName: "pods", - group: "", - kind: "Pod", - namespaced: true, - })).toBeTruthy(); + expect(canListResource(someKubeResource)).toBe(expected); + }); + }); }); - }); - describe("when first resourceRule has list permissions for everything", () => { - it("return truthy function", async () => { - const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( - new Promise((resolve) => resolve({ - body: { - status: { - incomplete: false, - resourceRules: [ - { - apiGroups: ["*"], - verbs: ["list"], - }, - { - apiGroups: ["*"], - verbs: ["get"], - }, - ], - nonResourceRules: [], - }, - }, - })), - ) as any); + describe("when api rejects", () => { + beforeEach(async () => { + await createSelfSubjectRulesReviewMock.reject(new Error("unknown error")); + }); - const permissionCheck = await requestPermissions("irrelevant-namespace"); + it("allows the request to complete, and 'canListResource' will return true", async () => { + const canListResource = await request; - expect(permissionCheck({ - apiName: "pods", - group: "", - kind: "Pod", - namespaced: true, - })).toBeTruthy(); - }); - }); - - describe("when first resourceRule has list permissions for asked resource", () => { - it("return truthy function", async () => { - const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( - new Promise((resolve) => resolve({ - body: { - status: { - incomplete: false, - resourceRules: [ - { - apiGroups: [""], - resources: ["pods"], - verbs: ["list"], - }, - { - apiGroups: ["*"], - verbs: ["get"], - }, - ], - nonResourceRules: [], - }, - }, - })), - ) as any); - - const permissionCheck = await requestPermissions("irrelevant-namespace"); - - expect(permissionCheck({ - apiName: "pods", - group: "", - kind: "Pod", - namespaced: true, - })).toBeTruthy(); - }); - }); - - describe("when last resourceRule has all permissions for everything", () => { - it("return truthy function", async () => { - const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( - new Promise((resolve) => resolve({ - body: { - status: { - incomplete: false, - resourceRules: [ - { - apiGroups: ["*"], - verbs: ["get"], - }, - { - apiGroups: ["*"], - verbs: ["*"], - }, - ], - nonResourceRules: [], - }, - }, - })), - ) as any); - - const permissionCheck = await requestPermissions("irrelevant-namespace"); - - expect(permissionCheck({ - apiName: "pods", - group: "", - kind: "Pod", - namespaced: true, - })).toBeTruthy(); - }); - }); - - describe("when last resourceRule has list permissions for everything", () => { - it("return truthy function", async () => { - const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( - new Promise((resolve) => resolve({ - body: { - status: { - incomplete: false, - resourceRules: [ - { - apiGroups: ["*"], - verbs: ["get"], - }, - { - apiGroups: ["*"], - verbs: ["list"], - }, - ], - nonResourceRules: [], - }, - }, - })), - ) as any); - - const permissionCheck = await requestPermissions("irrelevant-namespace"); - - expect(permissionCheck({ - apiName: "pods", - group: "", - kind: "Pod", - namespaced: true, - })).toBeTruthy(); - }); - }); - - describe("when last resourceRule has list permissions for asked resource", () => { - it("return truthy function", async () => { - const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( - new Promise((resolve) => resolve({ - body: { - status: { - incomplete: false, - resourceRules: [ - { - apiGroups: ["*"], - verbs: ["get"], - }, - { - apiGroups: [""], - resources: ["pods"], - verbs: ["list"], - }, - ], - nonResourceRules: [], - }, - }, - })), - ) as any); - - const permissionCheck = await requestPermissions("irrelevant-namespace"); - - expect(permissionCheck({ - apiName: "pods", - group: "", - kind: "Pod", - namespaced: true, - })).toBeTruthy(); - }); - }); - - describe("when resourceRules has matching resource without list verb", () => { - it("return falsy function", async () => { - const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( - new Promise((resolve) => resolve({ - body: { - status: { - incomplete: false, - resourceRules: [ - { - apiGroups: [""], - resources: ["pods"], - verbs: ["get"], - }, - ], - nonResourceRules: [], - }, - }, - })), - ) as any); - - const permissionCheck = await requestPermissions("irrelevant-namespace"); - - expect(permissionCheck({ - apiName: "pods", - group: "", - kind: "Pod", - namespaced: true, - })).toBeFalsy(); - }); - }); - - describe("when resourceRules has no matching resource with list verb", () => { - it("return falsy function", async () => { - const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( - new Promise((resolve) => resolve({ - body: { - status: { - incomplete: false, - resourceRules: [ - { - apiGroups: [""], - resources: ["services"], - verbs: ["list"], - }, - ], - nonResourceRules: [], - }, - }, - })), - ) as any); - - const permissionCheck = await requestPermissions("irrelevant-namespace"); - - expect(permissionCheck({ - apiName: "pods", - group: "", - kind: "Pod", - namespaced: true, - })).toBeFalsy(); + expect(canListResource(someKubeResource)).toBe(true); + }); }); }); }); + +const someKubeResource: KubeApiResource = { + apiName: "some-kind", + group: "some-api-group", + kind: "SomeKind", + namespaced: true, +};