diff --git a/src/dsmlp/app/id_validator.py b/src/dsmlp/app/id_validator.py index d409764..c99f11d 100644 --- a/src/dsmlp/app/id_validator.py +++ b/src/dsmlp/app/id_validator.py @@ -115,4 +115,17 @@ def validate_security_context( if securityContext.runAsGroup is not None and securityContext.runAsGroup not in allowed_teams: raise ValidationFailure( - f"spec.{context}.securityContext: gid must be in range {allowed_teams}") \ No newline at end of file + f"spec.{context}.securityContext: gid must be in range {allowed_teams}") + + def admission_response(self, uid, allowed, message): + return { + "apiVersion": "admission.k8s.io/v1", + "kind": "AdmissionReview", + "response": { + "uid": uid, + "allowed": allowed, + "status": { + "message": message + } + } + } \ No newline at end of file diff --git a/src/dsmlp/app/types.py b/src/dsmlp/app/types.py index 68211fb..c238850 100644 --- a/src/dsmlp/app/types.py +++ b/src/dsmlp/app/types.py @@ -1,15 +1,7 @@ + from dataclasses import dataclass -import json from typing import List, Optional, Dict - from dataclasses_json import dataclass_json -from dsmlp.plugin.awsed import AwsedClient, UnsuccessfulRequest -from dsmlp.plugin.console import Console -from dsmlp.plugin.course import ConfigProvider -from dsmlp.plugin.kube import KubeClient, NotFound -import jsonify - -from dsmlp.plugin.logger import Logger from abc import ABCMeta, abstractmethod @dataclass_json @@ -49,10 +41,16 @@ class PodSpec: securityContext: Optional[PodSecurityContext] = None priorityClassName: Optional[str] = None +@dataclass_json +@dataclass +class ObjectMeta: + labels: Dict[str, str] + @dataclass_json @dataclass class Object: + metadata: ObjectMeta spec: PodSpec diff --git a/tests/app/test_gpu_validator.py b/tests/app/test_gpu_validator.py index 6cc76ef..0e357dd 100644 --- a/tests/app/test_gpu_validator.py +++ b/tests/app/test_gpu_validator.py @@ -13,7 +13,7 @@ def setup_method(self) -> None: self.awsed_client = FakeAwsedClient() self.kube_client = FakeKubeClient() - self.awsed_client.add_user('user10', UserResponse(uid=10)) + self.awsed_client.add_user('user10', UserResponse(uid=10, enrollments=[])) self.awsed_client.add_teams('user10', ListTeamsResponse( teams=[TeamJson(gid=1000)] )) @@ -31,6 +31,9 @@ def test_no_gpus_requested(self): }, "namespace": "user10", "object": { + "metadata": { + "labels": {} + }, "kind": "Pod", "spec": { "containers": [{}] @@ -58,6 +61,9 @@ def test_quota_not_reached(self): }, "namespace": "user10", "object": { + "metadata": { + "labels": {} + }, "kind": "Pod", "spec": { "containers": [{ @@ -91,6 +97,9 @@ def test_quota_exceeded(self): }, "namespace": "user10", "object": { + "metadata": { + "labels": {} + }, "kind": "Pod", "spec": { "containers": [{ @@ -126,6 +135,9 @@ def test_sum_exceeded(self): }, "namespace": "user10", "object": { + "metadata": { + "labels": {} + }, "kind": "Pod", "spec": { "containers": [{ @@ -161,6 +173,9 @@ def test_low_priority(self): }, "namespace": "user10", "object": { + "metadata": { + "labels": {} + }, "kind": "Pod", "spec": { "containers": [{ @@ -198,6 +213,9 @@ def test_limit_exceeded(self): }, "namespace": "user10", "object": { + "metadata": { + "labels": {} + }, "kind": "Pod", "spec": { "containers": [{ diff --git a/tests/app/test_id_validator.py b/tests/app/test_id_validator.py index f54ba88..6d7404e 100644 --- a/tests/app/test_id_validator.py +++ b/tests/app/test_id_validator.py @@ -13,13 +13,12 @@ def setup_method(self) -> None: self.awsed_client = FakeAwsedClient() self.kube_client = FakeKubeClient() - self.awsed_client.add_user('user10', UserResponse(uid=10)) + self.awsed_client.add_user('user10', UserResponse(uid=10, enrollments=[])) self.awsed_client.add_teams('user10', ListTeamsResponse( teams=[TeamJson(gid=1000)] )) - + self.kube_client.add_namespace('user10', Namespace(name='user10', labels={'k8s-sync': 'true'}, gpu_quota=10)) - self.kube_client.set_existing_gpus('user10', 0) def test_log_request_details(self): self.when_validate( @@ -31,6 +30,9 @@ def test_log_request_details(self): "username": "system:kube-system" }, "object": { + "metadata": { + "labels": {} + }, "spec": { "containers": [{}] }, @@ -42,8 +44,47 @@ def test_log_request_details(self): assert_that(self.logger.messages, has_item( "INFO Allowed request username=system:kube-system namespace=user10 uid=705ab4f5-6393-11e8-b7cc-42010a800002")) + def test_course_enrollment(self): + self.awsed_client.add_user('user1', UserResponse(uid=1, enrollments=["course1"])) + self.kube_client.add_namespace('user1', Namespace(name='user1', labels={'k8s-sync': 'true'}, gpu_quota=10)) + + response = self.when_validate( + { + "request": { + "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", + "userInfo": { + "username": "user1" + }, + "namespace": "user1", + "object": { + "metadata": { + "labels": { + "dsmlp/course": "course1" + } + }, + "spec": { + "securityContext": { + "runAsUser": 1 + }, + "containers": [] + }, + } + } + } + ) + + assert_that(response, equal_to({ + "apiVersion": "admission.k8s.io/v1", + "kind": "AdmissionReview", + "response": { + "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", + "allowed": True, + "status": { + "message": "Allowed" + }}})) + def test_pod_security_context(self): - self.awsed_client.add_user('user1', UserResponse(uid=1)) + self.awsed_client.add_user('user1', UserResponse(uid=1, enrollments=[])) self.kube_client.add_namespace('user1', Namespace(name='user1', labels={'k8s-sync': 'true'}, gpu_quota=10)) response = self.when_validate( @@ -55,6 +96,9 @@ def test_pod_security_context(self): }, "namespace": "user1", "object": { + "metadata": { + "labels": {} + }, "spec": { "securityContext": { "runAsUser": 1 @@ -77,7 +121,7 @@ def test_pod_security_context(self): }}})) def test_security_context(self): - self.awsed_client.add_user('user1', UserResponse(uid=1)) + self.awsed_client.add_user('user1', UserResponse(uid=1, enrollments=[])) self.kube_client.add_namespace('user1', Namespace(name='user1', labels={'k8s-sync': 'true'}, gpu_quota=10)) response = self.when_validate( @@ -89,6 +133,9 @@ def test_security_context(self): }, "namespace": "user1", "object": { + "metadata": { + "labels": {} + }, "spec": { "securityContext": { "runAsUser": 1 @@ -120,7 +167,7 @@ def test_deny_security_context(self): but the PodSecurityContext.runAsUser doesn't belong to them. Deny the request. """ - self.awsed_client.add_user('user2', UserResponse(uid=2)) + self.awsed_client.add_user('user2', UserResponse(uid=2, enrollments=[])) response = self.when_validate( { @@ -131,6 +178,9 @@ def test_deny_security_context(self): }, "namespace": "user2", "object": { + "metadata": { + "labels": {} + }, "spec": { "securityContext": {"runAsUser": 3}, "containers": [] @@ -149,7 +199,7 @@ def test_deny_security_context(self): }}})) def test_failures_are_logged(self): - self.awsed_client.add_user('user2', UserResponse(uid=2)) + self.awsed_client.add_user('user2', UserResponse(uid=2, enrollments=[])) response = self.when_validate( { @@ -160,11 +210,14 @@ def test_failures_are_logged(self): }, "namespace": "user2", "object": { + "metadata": { + "labels": {} + }, "spec": { "containers": [], "securityContext": {"runAsUser": 3}}, }}}) - + assert_that(self.logger.messages, has_item( f"INFO Denied request username=user2 namespace=user2 reason={response['response']['status']['message']} uid=705ab4f5-6393-11e8-b7cc-42010a800002")) @@ -178,10 +231,51 @@ def test_deny_unknown_user(self): }, "namespace": "user2", "object": { + "metadata": { + "labels": {} + }, "spec": { "containers": [], - "securityContext": {"runAsUser": 3}}, + "securityContext": {"runAsUser": 2}}, }}}) + assert_that(response, equal_to({ + "apiVersion": "admission.k8s.io/v1", + "kind": "AdmissionReview", + "response": { + "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", + "allowed": False, + "status": { + "message": "namespace: no AWSEd user found with username user2" + }}})) + + def test_deny_course_enrollment(self): + """ + The user is launching a Pod, + but they are not enrolled in the course in the label "dsmlp/course". + Deny the request. + """ + self.awsed_client.add_user('user2', UserResponse(uid=2, enrollments=[])) + + response = self.when_validate( + { + "request": { + "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", + "userInfo": { + "username": "user2" + }, + "namespace": "user2", + "object": { + "metadata": { + "labels": { + "dsmlp/course": "course1" + } + }, + "spec": { + "securityContext": {"runAsUser": 2}, + "containers": [] + } + } + }}) assert_that(response, equal_to({ "apiVersion": "admission.k8s.io/v1", @@ -190,11 +284,11 @@ def test_deny_unknown_user(self): "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", "allowed": False, "status": { - "message": "Error" + "message": "metadata.labels: dsmlp/course must be in range []" }}})) def test_deny_pod_security_context(self): - self.awsed_client.add_user('user2', UserResponse(uid=2)) + self.awsed_client.add_user('user2', UserResponse(uid=2, enrollments=[])) response = self.when_validate( { @@ -206,6 +300,9 @@ def test_deny_pod_security_context(self): "namespace": "user2", "object": { "kind": "Pod", + "metadata": { + "labels": {} + }, "spec": { "securityContext": {"runAsUser": 2}, "containers": [ @@ -233,7 +330,7 @@ def test_deny_init_container(self): but the uid doesn't belong to them. Deny the request. """ - self.awsed_client.add_user('user2', UserResponse(uid=2)) + self.awsed_client.add_user('user2', UserResponse(uid=2, enrollments=[])) response = self.when_validate( { @@ -245,6 +342,9 @@ def test_deny_init_container(self): "namespace": "user2", "object": { "kind": "Pod", + "metadata": { + "labels": {} + }, "spec": { "containers": [{}], "initContainers": [ @@ -283,6 +383,9 @@ def test_deny_pod_security_context2(self): "namespace": "user10", "object": { "kind": "Pod", + "metadata": { + "labels": {} + }, "spec": { "containers": [{}] } @@ -310,6 +413,9 @@ def test_deny_team_gid(self): "namespace": "user10", "object": { "kind": "Pod", + "metadata": { + "labels": {} + }, "spec": { "securityContext": {"runAsGroup": 2}, "containers": [{}] @@ -339,6 +445,9 @@ def test_deny_pod_fsGroup(self): "namespace": "user10", "object": { "kind": "Pod", + "metadata": { + "labels": {} + }, "spec": { "securityContext": {"fsGroup": 2}, "containers": [{}] @@ -368,6 +477,9 @@ def test_deny_pod_supplemental_groups(self): "namespace": "user10", "object": { "kind": "Pod", + "metadata": { + "labels": {} + }, "spec": { "securityContext": {"supplementalGroups": [2]}, "containers": [{}] @@ -397,6 +509,9 @@ def test_deny_container_run_as_group(self): "namespace": "user10", "object": { "kind": "Pod", + "metadata": { + "labels": {} + }, "spec": { "containers": [ { @@ -428,6 +543,9 @@ def test_allow_gid_0_and_100a(self): "namespace": "user10", "object": { "kind": "Pod", + "metadata": { + "labels": {} + }, "spec": { "securityContext": {"runAsGroup": 0}, "containers": [ @@ -450,37 +568,6 @@ def test_allow_gid_0_and_100a(self): "message": "Allowed" }}})) - # no longer needed since the webhook filters for k9s-sync namespaces only - # def test_unlabelled_namespace_can_use_any_uid(self): - # self.kube.add_namespace('kube-system', Namespace(name='kube-system', labels={})) - - # response = self.when_validate( - # { - # "request": { - # "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - # "userInfo": { - # "username": "user10" - # }, - # "namespace": "kube-system", - # "object": { - # "spec": { - # "containers": [{}] - # } - # } - # } - # } - # ) - - # assert_that(response, equal_to({ - # "apiVersion": "admission.k8s.io/v1", - # "kind": "AdmissionReview", - # "response": { - # "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - # "allowed": True, - # "status": { - # "message": "Allowed" - # }}})) - def test_log_allowed_requests(self): self.when_validate( { @@ -491,6 +578,9 @@ def test_log_allowed_requests(self): }, "namespace": "user10", "object": { + "metadata": { + "labels": {} + }, "spec": { "containers": [{}] } @@ -506,4 +596,4 @@ def when_validate(self, json): validator = Validator(self.awsed_client, self.kube_client, self.logger) response = validator.validate_request(json) - return response + return response \ No newline at end of file