Skip to content

Commit ae87fa3

Browse files
authored
[ModelRegistry] ensure RunAsUser and RunAsGroup are not set explicitly (#226)
updates! 4813f2b updates! 20cd457 updates! b126825 updates! 809cca7
1 parent 69a5123 commit ae87fa3

File tree

4 files changed

+192
-1
lines changed

4 files changed

+192
-1
lines changed

tests/model_registry/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class ModelRegistryEndpoints:
3131
"authProvider": "redhat-ods-applications-auth-provider",
3232
"gateway": {"grpc": {"tls": {}}, "rest": {"tls": {}}},
3333
}
34-
DB_RESOURCES_NAME: str = "model-registry-db"
34+
DB_RESOURCES_NAME: str = "db-model-registry"
3535
MR_DB_IMAGE_DIGEST: str = (
3636
"public.ecr.aws/docker/library/mysql@sha256:9de9d54fecee6253130e65154b930978b1fcc336bcc86dfd06e89b72a2588ebe"
3737
)

tests/model_registry/scc/__init__.py

Whitespace-only changes.
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import pytest
2+
from typing import Self
3+
from simple_logger.logger import get_logger
4+
from _pytest.fixtures import FixtureRequest
5+
6+
from ocp_resources.namespace import Namespace
7+
from ocp_resources.pod import Pod
8+
from ocp_resources.deployment import Deployment
9+
from ocp_resources.resource import NamespacedResource
10+
from tests.model_registry.scc.utils import (
11+
get_uid_from_namespace,
12+
validate_pod_security_context,
13+
KEYS_TO_VALIDATE,
14+
validate_containers_pod_security_context,
15+
)
16+
from utilities.constants import DscComponents
17+
from tests.model_registry.constants import MR_NAMESPACE, MODEL_DICT, MR_INSTANCE_NAME
18+
19+
from kubernetes.dynamic import DynamicClient
20+
from ocp_utilities.infra import get_pods_by_name_prefix
21+
22+
LOGGER = get_logger(name=__name__)
23+
24+
25+
@pytest.fixture(scope="class")
26+
def model_registry_scc_namespace(model_registry_namespace: Namespace):
27+
mr_annotations = model_registry_namespace.instance.metadata.annotations
28+
return {
29+
"seLinuxOptions": mr_annotations.get("openshift.io/sa.scc.mcs"),
30+
"uid-range": mr_annotations.get("openshift.io/sa.scc.uid-range"),
31+
}
32+
33+
34+
@pytest.fixture(scope="class")
35+
def model_registry_resource(
36+
request: FixtureRequest, admin_client: DynamicClient, model_registry_namespace: Namespace
37+
) -> NamespacedResource:
38+
if request.param["kind"] == Deployment:
39+
return Deployment(name=MR_INSTANCE_NAME, namespace=model_registry_namespace.name, ensure_exists=True)
40+
elif request.param["kind"] == Pod:
41+
pods = get_pods_by_name_prefix(
42+
client=admin_client, pod_prefix=MR_INSTANCE_NAME, namespace=model_registry_namespace.name
43+
)
44+
if len(pods) != 1:
45+
pytest.fail(
46+
"Expected one model registry pod. Found: {[{pod.name: pod.status} for pod in pods] if pods else None}"
47+
)
48+
return pods[0]
49+
else:
50+
raise AssertionError(f"Invalid resource: {request.param['kind']}. Valid options: Deployment and Pod")
51+
52+
53+
@pytest.mark.parametrize(
54+
"model_registry_namespace, updated_dsc_component_state_scope_class, registered_model",
55+
[
56+
pytest.param(
57+
{"namespace_name": MR_NAMESPACE},
58+
{
59+
"component_patch": {
60+
DscComponents.MODELREGISTRY: {
61+
"managementState": DscComponents.ManagementState.MANAGED,
62+
"registriesNamespace": MR_NAMESPACE,
63+
},
64+
},
65+
},
66+
MODEL_DICT,
67+
),
68+
],
69+
indirect=True,
70+
)
71+
@pytest.mark.usefixtures("model_registry_namespace", "updated_dsc_component_state_scope_class", "registered_model")
72+
class TestModelRegistrySecurityContextValidation:
73+
@pytest.mark.parametrize(
74+
"model_registry_resource",
75+
[
76+
pytest.param({"kind": Deployment}),
77+
],
78+
indirect=["model_registry_resource"],
79+
)
80+
def test_model_registry_deployment_security_context_validation(
81+
self: Self,
82+
model_registry_resource: NamespacedResource,
83+
model_registry_namespace: Namespace,
84+
):
85+
"""
86+
Validate that model registry deployment does not set runAsUser/runAsGroup
87+
"""
88+
error = []
89+
for container in model_registry_resource.instance.spec.template.spec.containers:
90+
if not all([True for key in KEYS_TO_VALIDATE if not container.get(key)]):
91+
error.append({container.name: container.securityContext})
92+
93+
if error:
94+
pytest.fail(
95+
f"{model_registry_resource.name} {model_registry_resource.kind} containers expected to not "
96+
f"set {KEYS_TO_VALIDATE}, actual: {error}"
97+
)
98+
99+
@pytest.mark.parametrize(
100+
"model_registry_resource",
101+
[
102+
pytest.param({"kind": Pod}),
103+
],
104+
indirect=["model_registry_resource"],
105+
)
106+
def test_model_registry_pod_security_context_validation(
107+
self: Self,
108+
model_registry_resource: NamespacedResource,
109+
model_registry_scc_namespace: dict[str, str],
110+
):
111+
"""
112+
Validate that model registry pod gets runAsUser/runAsGroup from openshift and the values matches namespace
113+
annotations
114+
"""
115+
ns_uid = get_uid_from_namespace(namespace_scc=model_registry_scc_namespace)
116+
pod_spec = model_registry_resource.instance.spec
117+
errors = validate_pod_security_context(
118+
pod_security_context=pod_spec.securityContext,
119+
namespace_scc=model_registry_scc_namespace,
120+
model_registry_pod=model_registry_resource,
121+
ns_uid=ns_uid,
122+
)
123+
errors.extend(
124+
validate_containers_pod_security_context(model_registry_pod=model_registry_resource, namespace_uid=ns_uid)
125+
)
126+
if errors:
127+
pytest.fail(
128+
f"{model_registry_resource.name} {model_registry_resource.kind} pod security context validation failed"
129+
f" with error: {errors}"
130+
)

tests/model_registry/scc/utils.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from typing import Any
2+
from simple_logger.logger import get_logger
3+
4+
from ocp_resources.pod import Pod
5+
from ocp_resources.resource import NamespacedResource
6+
7+
KEYS_TO_VALIDATE = ["runAsGroup", "runAsUser"]
8+
9+
LOGGER = get_logger(name=__name__)
10+
11+
12+
def get_uid_from_namespace(namespace_scc: dict[str, str]) -> str:
13+
return namespace_scc["uid-range"].split("/")[0]
14+
15+
16+
def validate_pod_security_context(
17+
pod_security_context: dict[str, Any],
18+
namespace_scc: dict[str, str],
19+
model_registry_pod: NamespacedResource,
20+
ns_uid: str,
21+
) -> list[str]:
22+
"""
23+
Check model registry pod, ensure the security context values are being set by openshift
24+
"""
25+
errors = []
26+
pod_selinux_option = pod_security_context.get("seLinuxOptions", {}).get("level")
27+
if pod_selinux_option != namespace_scc["seLinuxOptions"]:
28+
errors.append(
29+
f"selinux option from pod {model_registry_pod.name} {pod_selinux_option},"
30+
f" namespace: {namespace_scc['seLinuxOptions']}"
31+
)
32+
if pod_security_context.get("fsGroup") != int(ns_uid):
33+
errors.append(
34+
f"UID-range from pod {model_registry_pod.name} {pod_security_context.get('fsGroup')}, namespace: {ns_uid}"
35+
)
36+
return errors
37+
38+
39+
def validate_containers_pod_security_context(model_registry_pod: Pod, namespace_uid: str) -> list[str]:
40+
"""
41+
Check all the containers of model registry pod, ensure the security context values are being set by openshift
42+
"""
43+
errors = []
44+
containers = model_registry_pod.instance.spec.containers
45+
for container in containers:
46+
expected_value = {
47+
"runAsUser": int(namespace_uid) + 1 if "sidecar" in container.args else int(namespace_uid),
48+
"runAsGroup": int(namespace_uid) + 1 if "sidecar" in container.args else None,
49+
}
50+
51+
for key in KEYS_TO_VALIDATE:
52+
if container.securityContext.get(key) == expected_value[key]:
53+
LOGGER.info(
54+
f"For container: {container.name}, {key} validation: {expected_value[key]} completed successfully"
55+
)
56+
else:
57+
errors.append(
58+
f"For {container.name}, expected key {key} value: {expected_value[key]},"
59+
f" actual: {container.securityContext.get(key)}"
60+
)
61+
return errors

0 commit comments

Comments
 (0)