Skip to content

Commit

Permalink
Merge pull request #3 from ucsd-ets/user-course-validation
Browse files Browse the repository at this point in the history
AWSED user and course enrollment validation
  • Loading branch information
arjunghoshal authored Jan 31, 2024
2 parents a68e300 + 5d6ac42 commit 482fe15
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 14 deletions.
19 changes: 18 additions & 1 deletion src/dsmlp/app/validator.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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,
Expand Down
6 changes: 4 additions & 2 deletions src/dsmlp/ext/awsed.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
return ListTeamsResponse(teams=teams)
1 change: 1 addition & 0 deletions src/dsmlp/plugin/awsed.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class ListTeamsResponse:
@dataclass
class UserResponse:
uid: int
enrollments: List[str]


class AwsedClient(metaclass=ABCMeta):
Expand Down
141 changes: 131 additions & 10 deletions tests/app/test_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
))
Expand All @@ -27,6 +27,9 @@ def test_log_request_details(self):
"username": "system:kube-system"
},
"object": {
"metadata": {
"labels": {}
},
"spec": {
"containers": [{}]
},
Expand All @@ -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(
{
Expand All @@ -50,6 +91,9 @@ def test_pod_security_context(self):
},
"namespace": "user1",
"object": {
"metadata": {
"labels": {}
},
"spec": {
"securityContext": {
"runAsUser": 1
Expand All @@ -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(
{
Expand All @@ -83,6 +127,9 @@ def test_security_context(self):
},
"namespace": "user1",
"object": {
"metadata": {
"labels": {}
},
"spec": {
"securityContext": {
"runAsUser": 1
Expand Down Expand Up @@ -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(
{
Expand All @@ -125,6 +172,9 @@ def test_deny_security_context(self):
},
"namespace": "user2",
"object": {
"metadata": {
"labels": {}
},
"spec": {
"securityContext": {"runAsUser": 3},
"containers": []
Expand All @@ -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(
{
Expand All @@ -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"))

Expand All @@ -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",
Expand All @@ -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(
{
Expand All @@ -200,6 +294,9 @@ def test_deny_pod_security_context(self):
"namespace": "user2",
"object": {
"kind": "Pod",
"metadata": {
"labels": {}
},
"spec": {
"securityContext": {"runAsUser": 2},
"containers": [
Expand Down Expand Up @@ -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(
{
Expand All @@ -239,6 +336,9 @@ def test_deny_init_container(self):
"namespace": "user2",
"object": {
"kind": "Pod",
"metadata": {
"labels": {}
},
"spec": {
"containers": [{}],
"initContainers": [
Expand Down Expand Up @@ -277,6 +377,9 @@ def test_deny_pod_security_context2(self):
"namespace": "user10",
"object": {
"kind": "Pod",
"metadata": {
"labels": {}
},
"spec": {
"containers": [{}]
}
Expand Down Expand Up @@ -304,6 +407,9 @@ def test_deny_team_gid(self):
"namespace": "user10",
"object": {
"kind": "Pod",
"metadata": {
"labels": {}
},
"spec": {
"securityContext": {"runAsGroup": 2},
"containers": [{}]
Expand Down Expand Up @@ -333,6 +439,9 @@ def test_deny_pod_fsGroup(self):
"namespace": "user10",
"object": {
"kind": "Pod",
"metadata": {
"labels": {}
},
"spec": {
"securityContext": {"fsGroup": 2},
"containers": [{}]
Expand Down Expand Up @@ -362,6 +471,9 @@ def test_deny_pod_supplemental_groups(self):
"namespace": "user10",
"object": {
"kind": "Pod",
"metadata": {
"labels": {}
},
"spec": {
"securityContext": {"supplementalGroups": [2]},
"containers": [{}]
Expand Down Expand Up @@ -391,6 +503,9 @@ def test_deny_container_run_as_group(self):
"namespace": "user10",
"object": {
"kind": "Pod",
"metadata": {
"labels": {}
},
"spec": {
"containers": [
{
Expand Down Expand Up @@ -422,6 +537,9 @@ def test_allow_gid_0_and_100a(self):
"namespace": "user10",
"object": {
"kind": "Pod",
"metadata": {
"labels": {}
},
"spec": {
"securityContext": {"runAsGroup": 0},
"containers": [
Expand Down Expand Up @@ -485,6 +603,9 @@ def test_log_allowed_requests(self):
},
"namespace": "user10",
"object": {
"metadata": {
"labels": {}
},
"spec": {
"containers": [{}]
}
Expand Down
2 changes: 1 addition & 1 deletion tests/fakes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 482fe15

Please sign in to comment.