diff --git a/src/dsmlp/app/validator.py b/src/dsmlp/app/validator.py index cb0749e..d3869f0 100644 --- a/src/dsmlp/app/validator.py +++ b/src/dsmlp/app/validator.py @@ -1,6 +1,6 @@ from dataclasses import dataclass import json -from typing import List, Optional +from typing import Dict, List, Optional from dataclasses_json import dataclass_json from dsmlp.plugin.awsed import AwsedClient, UnsuccessfulRequest @@ -43,10 +43,16 @@ class PodSpec: initContainers: Optional[List[Container]] = None securityContext: Optional[PodSecurityContext] = None +@dataclass_json +@dataclass +class ObjectMeta: + labels: Dict[str, str] + @dataclass_json @dataclass class Object: + metadata: ObjectMeta spec: PodSpec @@ -120,17 +126,28 @@ def validate_pod(self, request: Request): # if 'k8s-sync' in namespace.labels: user = self.awsed.describe_user(username) + if not user: + raise ValidationFailure(f"namespace: no AWSEd user found with username {username}") allowed_uid = user.uid + allowed_courses = user.enrollments team_response = self.awsed.list_user_teams(username) allowed_gids = [team.gid for team in team_response.teams] allowed_gids.append(0) allowed_gids.append(100) + metadata = request.object.metadata spec = request.object.spec + self.validate_course_enrollment(allowed_courses, metadata.labels) self.validate_pod_security_context(allowed_uid, allowed_gids, spec.securityContext) self.validate_containers(allowed_uid, allowed_gids, spec) + def validate_course_enrollment(self, allowed_courses: List[str], labels: Dict[str, str]): + if not 'dsmlp/course' in labels: + return + if not labels['dsmlp/course'] in allowed_courses: + raise ValidationFailure(f"metadata.labels: dsmlp/course must be in range {allowed_courses}") + def validate_pod_security_context( self, authorized_uid: int, diff --git a/src/dsmlp/ext/awsed.py b/src/dsmlp/ext/awsed.py index 2f86af3..82d855e 100644 --- a/src/dsmlp/ext/awsed.py +++ b/src/dsmlp/ext/awsed.py @@ -15,7 +15,9 @@ def __init__(self): def describe_user(self, username: str) -> UserResponse: usrResultJson = self.client.describe_user(username) - return UserResponse(uid=usrResultJson.uid) + if not usrResultJson: + return None + return UserResponse(uid=usrResultJson.uid, enrollments=usrResultJson.enrollments) def list_user_teams(self, username: str) -> ListTeamsResponse: usrTeams = self.client.list_teams(username) @@ -24,4 +26,4 @@ def list_user_teams(self, username: str) -> ListTeamsResponse: for team in usrTeams.teams: teams.append(TeamJson(gid=team.gid)) - return ListTeamsResponse(teams=teams) \ No newline at end of file + return ListTeamsResponse(teams=teams) diff --git a/src/dsmlp/plugin/awsed.py b/src/dsmlp/plugin/awsed.py index 623a3c8..93e6901 100644 --- a/src/dsmlp/plugin/awsed.py +++ b/src/dsmlp/plugin/awsed.py @@ -16,6 +16,7 @@ class ListTeamsResponse: @dataclass class UserResponse: uid: int + enrollments: List[str] class AwsedClient(metaclass=ABCMeta): diff --git a/tests/app/test_validator.py b/tests/app/test_validator.py index 754c40d..d9aac05 100644 --- a/tests/app/test_validator.py +++ b/tests/app/test_validator.py @@ -12,7 +12,7 @@ def setup_method(self) -> None: self.logger = FakeLogger() self.awsed_client = FakeAwsedClient() - 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)] )) @@ -27,6 +27,9 @@ def test_log_request_details(self): "username": "system:kube-system" }, "object": { + "metadata": { + "labels": {} + }, "spec": { "containers": [{}] }, @@ -38,8 +41,46 @@ 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"])) + + 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=[])) response = self.when_validate( { @@ -50,6 +91,9 @@ def test_pod_security_context(self): }, "namespace": "user1", "object": { + "metadata": { + "labels": {} + }, "spec": { "securityContext": { "runAsUser": 1 @@ -72,7 +116,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=[])) response = self.when_validate( { @@ -83,6 +127,9 @@ def test_security_context(self): }, "namespace": "user1", "object": { + "metadata": { + "labels": {} + }, "spec": { "securityContext": { "runAsUser": 1 @@ -114,7 +161,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( { @@ -125,6 +172,9 @@ def test_deny_security_context(self): }, "namespace": "user2", "object": { + "metadata": { + "labels": {} + }, "spec": { "securityContext": {"runAsUser": 3}, "containers": [] @@ -143,7 +193,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( { @@ -154,11 +204,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")) @@ -172,10 +225,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", @@ -184,11 +278,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( { @@ -200,6 +294,9 @@ def test_deny_pod_security_context(self): "namespace": "user2", "object": { "kind": "Pod", + "metadata": { + "labels": {} + }, "spec": { "securityContext": {"runAsUser": 2}, "containers": [ @@ -227,7 +324,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( { @@ -239,6 +336,9 @@ def test_deny_init_container(self): "namespace": "user2", "object": { "kind": "Pod", + "metadata": { + "labels": {} + }, "spec": { "containers": [{}], "initContainers": [ @@ -277,6 +377,9 @@ def test_deny_pod_security_context2(self): "namespace": "user10", "object": { "kind": "Pod", + "metadata": { + "labels": {} + }, "spec": { "containers": [{}] } @@ -304,6 +407,9 @@ def test_deny_team_gid(self): "namespace": "user10", "object": { "kind": "Pod", + "metadata": { + "labels": {} + }, "spec": { "securityContext": {"runAsGroup": 2}, "containers": [{}] @@ -333,6 +439,9 @@ def test_deny_pod_fsGroup(self): "namespace": "user10", "object": { "kind": "Pod", + "metadata": { + "labels": {} + }, "spec": { "securityContext": {"fsGroup": 2}, "containers": [{}] @@ -362,6 +471,9 @@ def test_deny_pod_supplemental_groups(self): "namespace": "user10", "object": { "kind": "Pod", + "metadata": { + "labels": {} + }, "spec": { "securityContext": {"supplementalGroups": [2]}, "containers": [{}] @@ -391,6 +503,9 @@ def test_deny_container_run_as_group(self): "namespace": "user10", "object": { "kind": "Pod", + "metadata": { + "labels": {} + }, "spec": { "containers": [ { @@ -422,6 +537,9 @@ def test_allow_gid_0_and_100a(self): "namespace": "user10", "object": { "kind": "Pod", + "metadata": { + "labels": {} + }, "spec": { "securityContext": {"runAsGroup": 0}, "containers": [ @@ -485,6 +603,9 @@ def test_log_allowed_requests(self): }, "namespace": "user10", "object": { + "metadata": { + "labels": {} + }, "spec": { "containers": [{}] } diff --git a/tests/fakes.py b/tests/fakes.py index f6984e7..6ec0684 100644 --- a/tests/fakes.py +++ b/tests/fakes.py @@ -27,7 +27,7 @@ def describe_user(self, username: str) -> UserResponse: try: return self.users[username] except KeyError: - raise UnsuccessfulRequest() + return None def add_user(self, username, user: UserResponse): self.users[username] = user