From 4abd69d65551d52c76b0ab5e6774c0f3260a3732 Mon Sep 17 00:00:00 2001 From: Juntao Wang <37624318+DaoDaoNoCode@users.noreply.github.com> Date: Fri, 10 Mar 2023 02:34:08 +0800 Subject: [PATCH] Check user permissions before promoting data science projects (#991) * Use access token to promote projects * update to use access token * Use pass-through API and selfSubjectAccessReview to check the user permissions * lint * address comments --- backend/src/routes/api/k8s/pass-through.ts | 55 ++++++++++++------- backend/src/routes/api/namespaces/index.ts | 2 +- .../routes/api/namespaces/namespaceUtils.ts | 49 ++++++++++++++++- 3 files changed, 82 insertions(+), 24 deletions(-) diff --git a/backend/src/routes/api/k8s/pass-through.ts b/backend/src/routes/api/k8s/pass-through.ts index c836f9b7fb..f0b3458622 100644 --- a/backend/src/routes/api/k8s/pass-through.ts +++ b/backend/src/routes/api/k8s/pass-through.ts @@ -1,7 +1,11 @@ -import { FastifyRequest } from 'fastify'; import https, { RequestOptions } from 'https'; -import { K8sResourceCommon, K8sStatus, KubeFastifyInstance } from '../../../types'; -import { DEV_MODE, USER_ACCESS_TOKEN } from '../../../utils/constants'; +import { + K8sResourceCommon, + K8sStatus, + KubeFastifyInstance, + OauthFastifyRequest, +} from '../../../types'; +import { DEV_MODE } from '../../../utils/constants'; import { getDirectCallOptions } from '../../../utils/directCallUtils'; type PassThroughData = { @@ -10,13 +14,29 @@ type PassThroughData = { url: string; }; -const isK8sStatus = (data: Record): data is K8sStatus => data.kind === 'Status'; +export const isK8sStatus = (data: unknown): data is K8sStatus => + (data as K8sStatus).kind === 'Status'; const setupRequest = async ( fastify: KubeFastifyInstance, - request: FastifyRequest<{ Headers: { [USER_ACCESS_TOKEN]: string } }>, + request: OauthFastifyRequest, data: PassThroughData, -): Promise<{ url: string; requestOptions: RequestOptions }> => { +): Promise => { + const { method, url } = data; + + const requestOptions = await getDirectCallOptions(fastify, request, url); + + return { + ...requestOptions, + method, + }; +}; + +export const passThrough = ( + fastify: KubeFastifyInstance, + request: OauthFastifyRequest, + data: PassThroughData, +): Promise => { const { method, url } = data; // TODO: Remove when bug is fixed - https://issues.redhat.com/browse/HAC-1825 @@ -33,26 +53,22 @@ const setupRequest = async ( safeURL = queryParams.join('?'); } - const requestOptions = await getDirectCallOptions(fastify, request, url); - - return { + const updatedData = { + ...data, url: safeURL, - requestOptions: { - ...requestOptions, - method, - }, }; + return safeURLPassThrough(fastify, request, updatedData); }; -export const passThrough = ( +export const safeURLPassThrough = ( fastify: KubeFastifyInstance, - request: FastifyRequest<{ Headers: { [USER_ACCESS_TOKEN]: string } }>, + request: OauthFastifyRequest, data: PassThroughData, -): Promise => { - const { method, requestData } = data; +): Promise => { + const { method, requestData, url } = data; return new Promise((resolve, reject) => { - setupRequest(fastify, request, data).then(({ url, requestOptions }) => { + setupRequest(fastify, request, data).then((requestOptions) => { if (requestData) { requestOptions.headers = { ...requestOptions.headers, @@ -74,7 +90,7 @@ export const passThrough = ( data += chunk; }) .on('end', () => { - let parsedData: K8sResourceCommon | K8sStatus; + let parsedData: T | K8sStatus; try { parsedData = JSON.parse(data); } catch (e) { @@ -83,7 +99,6 @@ export const passThrough = ( parsedData = { kind: 'Status', apiVersion: 'v1', - metadata: {}, status: 'Failure', message: data, reason: 'NotFound', diff --git a/backend/src/routes/api/namespaces/index.ts b/backend/src/routes/api/namespaces/index.ts index bf4ff45715..7628c5347d 100644 --- a/backend/src/routes/api/namespaces/index.ts +++ b/backend/src/routes/api/namespaces/index.ts @@ -13,7 +13,7 @@ export default async (fastify: KubeFastifyInstance): Promise => { const context = parseInt(contextAsString) as NamespaceApplicationCase; - return applyNamespaceChange(fastify, name, context); + return applyNamespaceChange(fastify, request, name, context); }, ); }; diff --git a/backend/src/routes/api/namespaces/namespaceUtils.ts b/backend/src/routes/api/namespaces/namespaceUtils.ts index 2a10f49fa4..05b1a7dc7f 100644 --- a/backend/src/routes/api/namespaces/namespaceUtils.ts +++ b/backend/src/routes/api/namespaces/namespaceUtils.ts @@ -1,10 +1,40 @@ -import { PatchUtils } from '@kubernetes/client-node'; +import { PatchUtils, V1SelfSubjectAccessReview } from '@kubernetes/client-node'; import { NamespaceApplicationCase } from './const'; -import { KubeFastifyInstance } from '../../../types'; +import { K8sStatus, KubeFastifyInstance, OauthFastifyRequest } from '../../../types'; import { createCustomError } from '../../../utils/requestUtils'; +import { isK8sStatus, safeURLPassThrough } from '../k8s/pass-through'; -export const applyNamespaceChange = ( +const checkNamespacePermission = ( fastify: KubeFastifyInstance, + request: OauthFastifyRequest, + name: string, +): Promise => { + const kc = fastify.kube.config; + const cluster = kc.getCurrentCluster(); + const selfSubjectAccessReviewObject: V1SelfSubjectAccessReview = { + apiVersion: 'authorization.k8s.io/v1', + kind: 'SelfSubjectAccessReview', + spec: { + resourceAttributes: { + group: 'project.openshift.io', + resource: 'projects', + subresource: '', + verb: 'update', + name, + namespace: name, + }, + }, + }; + return safeURLPassThrough(fastify, request, { + url: `${cluster.server}/apis/authorization.k8s.io/v1/selfsubjectaccessreviews`, + method: 'POST', + requestData: JSON.stringify(selfSubjectAccessReviewObject), + }); +}; + +export const applyNamespaceChange = async ( + fastify: KubeFastifyInstance, + request: OauthFastifyRequest, name: string, context: NamespaceApplicationCase, ): Promise<{ applied: boolean }> => { @@ -17,6 +47,19 @@ export const applyNamespaceChange = ( ); } + const selfSubjectAccessReview = await checkNamespacePermission(fastify, request, name); + if (isK8sStatus(selfSubjectAccessReview)) { + throw createCustomError( + selfSubjectAccessReview.reason, + selfSubjectAccessReview.message, + selfSubjectAccessReview.code, + ); + } + if (!selfSubjectAccessReview.status.allowed) { + fastify.log.error(`Unable to access the namespace, ${selfSubjectAccessReview.status.reason}`); + throw createCustomError('Forbidden', "You don't have the access to update the namespace", 403); + } + let labels = {}; switch (context) { case NamespaceApplicationCase.DSG_CREATION: