From afaaef8daedad464758c7afa9fe7ee4092acdc6e Mon Sep 17 00:00:00 2001 From: Mohit Gupta Date: Tue, 5 Dec 2023 22:56:39 +0000 Subject: [PATCH] adds checks for apiVersion for resources (#13) --- icekube/icekube.py | 2 +- icekube/models/__init__.py | 65 +++++++------------- icekube/models/base.py | 37 +++++++---- icekube/models/cluster.py | 15 ++--- icekube/models/clusterrole.py | 4 ++ icekube/models/clusterrolebinding.py | 17 +++-- icekube/models/group.py | 7 +-- icekube/models/namespace.py | 7 --- icekube/models/node.py | 4 +- icekube/models/pod.py | 12 ++-- icekube/models/policyrule.py | 2 +- icekube/models/role.py | 4 ++ icekube/models/rolebinding.py | 4 ++ icekube/models/secret.py | 5 +- icekube/models/securitycontextconstraints.py | 9 ++- icekube/models/serviceaccount.py | 7 +-- icekube/models/signer.py | 3 +- icekube/models/user.py | 7 +-- icekube/neo4j.py | 20 +----- 19 files changed, 106 insertions(+), 125 deletions(-) delete mode 100644 icekube/models/namespace.py diff --git a/icekube/icekube.py b/icekube/icekube.py index 3cd56f6..6c15d1f 100644 --- a/icekube/icekube.py +++ b/icekube/icekube.py @@ -44,7 +44,7 @@ def enumerate_resource_kind( ignore = [] with get_driver().session() as session: - cluster = Cluster(name=context_name(), version=kube_version()) + cluster = Cluster(apiVersion="N/A", name=context_name(), version=kube_version()) cmd, kwargs = create(cluster) session.run(cmd, **kwargs) diff --git a/icekube/models/__init__.py b/icekube/models/__init__.py index 1cd3030..0bd21b6 100644 --- a/icekube/models/__init__.py +++ b/icekube/models/__init__.py @@ -1,52 +1,33 @@ -from typing import List, Type - +from icekube.models import ( + clusterrole, + clusterrolebinding, + group, + pod, + role, + rolebinding, + secret, + securitycontextconstraints, + serviceaccount, + user, +) from icekube.models.api_resource import APIResource from icekube.models.base import Resource from icekube.models.cluster import Cluster -from icekube.models.clusterrole import ClusterRole -from icekube.models.clusterrolebinding import ClusterRoleBinding -from icekube.models.group import Group -from icekube.models.namespace import Namespace -from icekube.models.pod import Pod -from icekube.models.role import Role -from icekube.models.rolebinding import RoleBinding -from icekube.models.secret import Secret -from icekube.models.securitycontextconstraints import ( - SecurityContextConstraints, -) -from icekube.models.serviceaccount import ServiceAccount from icekube.models.signer import Signer -from icekube.models.user import User - -enumerate_resource_kinds: List[Type[Resource]] = [ - ClusterRole, - ClusterRoleBinding, - Namespace, - Pod, - Role, - RoleBinding, - Secret, - SecurityContextConstraints, - ServiceAccount, -] - - -# plurals: Dict[str, Type[Resource]] = {x.plural: x for x in enumerate_resource_kinds} - __all__ = [ "APIResource", "Cluster", - "ClusterRole", - "ClusterRoleBinding", - "Group", - "Namespace", - "Pod", - "Role", - "RoleBinding", - "Secret", - "SecurityContextConstraints", - "ServiceAccount", "Signer", - "User", + "Resource", + "clusterrole", + "clusterrolebinding", + "group", + "pod", + "role", + "rolebinding", + "secret", + "securitycontextconstraints", + "serviceaccount", + "user", ] diff --git a/icekube/models/base.py b/icekube/models/base.py index a8c6cfd..0bc5b56 100644 --- a/icekube/models/base.py +++ b/icekube/models/base.py @@ -13,6 +13,13 @@ logger = logging.getLogger(__name__) +def api_group(api_version: str) -> str: + if "/" in api_version: + return api_version.split("/")[0] + # When the base APIGroup is "" + return "" + + class Resource(BaseModel): apiVersion: str = Field(default=...) kind: str = Field(default=...) @@ -20,6 +27,7 @@ class Resource(BaseModel): plural: str = Field(default=...) namespace: Optional[str] = Field(default=None) raw: Optional[str] = Field(default=None) + supported_api_groups: List[str] = Field(default_factory=list) def __new__(cls, **kwargs): kind_class = cls.get_kind_class( @@ -91,19 +99,24 @@ def get_value(field): @classmethod def get_kind_class(cls, apiVersion: str, kind: str) -> Type[Resource]: - subclasses = {x.__name__: x for x in cls.__subclasses__()} - try: - return subclasses[kind] - except KeyError: - return cls + for subclass in cls.__subclasses__(): + if subclass.__name__ != kind: + continue + + supported = subclass.model_fields["supported_api_groups"].default + if not isinstance(supported, list): + continue + + if api_group(apiVersion) not in supported: + continue + + return subclass + + return cls @property def api_group(self) -> str: - if "/" in self.apiVersion: - return self.apiVersion.split("/")[0] - else: - # When the base APIGroup is "" - return "" + return api_group(self.apiVersion) @property def resource_definition_name(self) -> str: @@ -206,12 +219,10 @@ def relationships( logger.debug( f"Generating {'initial' if initial else 'second'} set of relationships", ) - from icekube.neo4j import mock - relationships: List[RELATIONSHIP] = [] if self.namespace is not None: - ns = mock(Resource, name=self.namespace, kind="Namespace") + ns = Resource(name=self.namespace, kind="Namespace") relationships += [ ( self, diff --git a/icekube/models/cluster.py b/icekube/models/cluster.py index 97c3275..9d47166 100644 --- a/icekube/models/cluster.py +++ b/icekube/models/cluster.py @@ -8,29 +8,26 @@ class Cluster(Resource): kind: str = "Cluster" apiVersion: str = "N/A" plural: str = "clusters" + supported_api_groups: List[str] = ["N"] def __repr__(self) -> str: return f"Cluster(name='{self.name}', version='{self.version}')" @property - def unique_identifiers(self) -> Dict[str, str]: + def db_labels(self) -> Dict[str, str]: return { - "name": self.name, - "kind": self.kind, - "apiVersion": self.apiVersion, + **self.unique_identifiers, + "plural": self.plural, + "version": self.version, } - @property - def db_labels(self) -> Dict[str, str]: - return {**self.unique_identifiers, "version": self.version} - def relationships( self, initial: bool = True, ) -> List[RELATIONSHIP]: relationships = super().relationships() - query = "MATCH (src) WHERE NOT src:Cluster " + query = "MATCH (src) WHERE NOT src.apiVersion = 'N/A' " relationships += [((query, {}), "WITHIN_CLUSTER", self)] diff --git a/icekube/models/clusterrole.py b/icekube/models/clusterrole.py index c3b53d7..fa77d6b 100644 --- a/icekube/models/clusterrole.py +++ b/icekube/models/clusterrole.py @@ -11,6 +11,10 @@ class ClusterRole(Resource): rules: List[PolicyRule] = Field(default_factory=list) + supported_api_groups: List[str] = [ + "rbac.authorization.k8s.io", + "authorization.openshift.io", + ] @root_validator(pre=True) def inject_rules(cls, values): diff --git a/icekube/models/clusterrolebinding.py b/icekube/models/clusterrolebinding.py index bc57da0..67cf631 100644 --- a/icekube/models/clusterrolebinding.py +++ b/icekube/models/clusterrolebinding.py @@ -9,7 +9,6 @@ from icekube.models.role import Role from icekube.models.serviceaccount import ServiceAccount from icekube.models.user import User -from icekube.neo4j import find_or_mock, get_cluster_object, mock from icekube.relationships import Relationship from pydantic import root_validator from pydantic.fields import Field @@ -19,6 +18,8 @@ def get_role( role_ref: Dict[str, Any], namespace: Optional[str] = None, ) -> Union[ClusterRole, Role]: + from icekube.neo4j import find_or_mock + role_ref["kind"] = role_ref.get("kind", "ClusterRole") if role_ref["kind"] == "ClusterRole": return find_or_mock(ClusterRole, name=role_ref["name"]) @@ -48,8 +49,7 @@ def get_subjects( results.append(Group(name=subject["name"])) elif subject["kind"] == "ServiceAccount": results.append( - mock( - ServiceAccount, + ServiceAccount( name=subject["name"], namespace=subject.get("namespace", namespace), ), @@ -63,6 +63,10 @@ def get_subjects( class ClusterRoleBinding(Resource): role: Union[ClusterRole, Role] subjects: List[Union[ServiceAccount, User, Group]] = Field(default_factory=list) + supported_api_groups: List[str] = [ + "rbac.authorization.k8s.io", + "authorization.openshift.io", + ] @root_validator(pre=True) def inject_role_and_subjects(cls, values): @@ -88,11 +92,16 @@ def relationships( (subject, Relationship.BOUND_TO, self) for subject in self.subjects ] + cluster_query = ( + "MATCH ({prefix}) WHERE {prefix}.kind =~ ${prefix}_kind ", + {"apiVersion": "N/A", "kind": "Cluster"}, + ) + if not initial: for role_rule in self.role.rules: if role_rule.contains_csr_approval: relationships.append( - (self, Relationship.HAS_CSR_APPROVAL, get_cluster_object()), + (self, Relationship.HAS_CSR_APPROVAL, cluster_query), ) for relationship, resource in role_rule.affected_resource_query(): relationships.append((self, relationship, resource)) diff --git a/icekube/models/group.py b/icekube/models/group.py index 7ecd1dd..ce8aa6c 100644 --- a/icekube/models/group.py +++ b/icekube/models/group.py @@ -1,13 +1,10 @@ from __future__ import annotations -from typing import Dict +from typing import List from icekube.models.base import Resource class Group(Resource): plural: str = "groups" - - @property - def unique_identifiers(self) -> Dict[str, str]: - return {**super().unique_identifiers, "plural": self.plural} + supported_api_groups: List[str] = ["", "user.openshift.io"] diff --git a/icekube/models/namespace.py b/icekube/models/namespace.py deleted file mode 100644 index 5cc374c..0000000 --- a/icekube/models/namespace.py +++ /dev/null @@ -1,7 +0,0 @@ -from __future__ import annotations - -from icekube.models.base import Resource - - -class Namespace(Resource): - ... diff --git a/icekube/models/node.py b/icekube/models/node.py index 5889620..c232d87 100644 --- a/icekube/models/node.py +++ b/icekube/models/node.py @@ -1,7 +1,9 @@ from __future__ import annotations +from typing import List + from icekube.models.base import Resource class Node(Resource): - ... + supported_api_groups: List[str] = [""] diff --git a/icekube/models/pod.py b/icekube/models/pod.py index 64d7858..88dab3b 100644 --- a/icekube/models/pod.py +++ b/icekube/models/pod.py @@ -9,7 +9,6 @@ from icekube.models.node import Node from icekube.models.secret import Secret from icekube.models.serviceaccount import ServiceAccount -from icekube.neo4j import mock from icekube.relationships import Relationship from pydantic import root_validator @@ -67,14 +66,14 @@ class Pod(Resource): privileged: bool hostPID: bool hostNetwork: bool + supported_api_groups: List[str] = [""] @root_validator(pre=True) def inject_service_account(cls, values): data = json.loads(values.get("raw", "{}")) sa = data.get("spec", {}).get("serviceAccountName") if sa: - values["service_account"] = mock( - ServiceAccount, + values["service_account"] = ServiceAccount( name=sa, namespace=values.get("namespace"), ) @@ -87,7 +86,7 @@ def inject_node(cls, values): data = json.loads(values.get("raw", "{}")) node = data.get("spec", {}).get("nodeName") if node: - values["node"] = mock(Node, name=node) + values["node"] = Node(name=node) else: values["node"] = None @@ -258,7 +257,10 @@ def relationships( ( self, Relationship.MOUNTS_SECRET, - mock(Secret, namespace=cast(str, self.namespace), name=secret), + Secret( # type: ignore + namespace=cast(str, self.namespace), + name=secret, + ), ), ] diff --git a/icekube/models/policyrule.py b/icekube/models/policyrule.py index bc19290..93a9d34 100644 --- a/icekube/models/policyrule.py +++ b/icekube/models/policyrule.py @@ -90,7 +90,7 @@ def affected_resource_query( "name": namespace, } else: - query_filter = {"kind": "Cluster"} + query_filter = {"apiVersion": "N/A", "kind": "Cluster"} yield ( Relationship.generate_grant("CREATE", resource), generate_query(query_filter), diff --git a/icekube/models/role.py b/icekube/models/role.py index df832de..27f2809 100644 --- a/icekube/models/role.py +++ b/icekube/models/role.py @@ -11,6 +11,10 @@ class Role(Resource): rules: List[PolicyRule] = Field(default_factory=list) + supported_api_groups: List[str] = [ + "rbac.authorization.k8s.io", + "authorization.openshift.io", + ] @root_validator(pre=True) def inject_role(cls, values): diff --git a/icekube/models/rolebinding.py b/icekube/models/rolebinding.py index 9b9aeb0..dd0d761 100644 --- a/icekube/models/rolebinding.py +++ b/icekube/models/rolebinding.py @@ -18,6 +18,10 @@ class RoleBinding(Resource): role: Union[ClusterRole, Role] subjects: List[Union[ServiceAccount, User, Group]] = Field(default_factory=list) + supported_api_groups: List[str] = [ + "rbac.authorization.k8s.io", + "authorization.openshift.io", + ] @root_validator(pre=True) def inject_role_and_subjects(cls, values): diff --git a/icekube/models/secret.py b/icekube/models/secret.py index e2a7f89..8accc92 100644 --- a/icekube/models/secret.py +++ b/icekube/models/secret.py @@ -4,7 +4,6 @@ from typing import Any, Dict, List, cast from icekube.models.base import RELATIONSHIP, Resource -from icekube.neo4j import mock from icekube.relationships import Relationship from pydantic import root_validator @@ -12,6 +11,7 @@ class Secret(Resource): secret_type: str annotations: Dict[str, Any] + supported_api_groups: List[str] = [""] @root_validator(pre=True) def remove_secret_data(cls, values): @@ -45,8 +45,7 @@ def relationships(self, initial: bool = True) -> List[RELATIONSHIP]: sa = self.annotations.get("kubernetes.io/service-account.name") if sa: - account = mock( - ServiceAccount, + account = ServiceAccount( name=sa, namespace=cast(str, self.namespace), ) diff --git a/icekube/models/securitycontextconstraints.py b/icekube/models/securitycontextconstraints.py index 0e063a5..65aae9c 100644 --- a/icekube/models/securitycontextconstraints.py +++ b/icekube/models/securitycontextconstraints.py @@ -7,7 +7,6 @@ from icekube.models.group import Group from icekube.models.serviceaccount import ServiceAccount from icekube.models.user import User -from icekube.neo4j import mock from pydantic import root_validator from pydantic.fields import Field @@ -16,6 +15,7 @@ class SecurityContextConstraints(Resource): plural: str = "securitycontextconstraints" users: List[Union[User, ServiceAccount]] = Field(default_factory=list) groups: List[Group] + supported_api_groups: List[str] = ["security.openshift.io"] @root_validator(pre=True) def inject_users_and_groups(cls, values): @@ -27,19 +27,18 @@ def inject_users_and_groups(cls, values): if user.startswith("system:serviceaccount:"): ns, name = user.split(":")[2:] values["users"].append( - mock( - ServiceAccount, + ServiceAccount( name=name, namespace=ns, ), ) else: - values["users"].append(mock(User, name=user)) + values["users"].append(User(name=user)) groups = data.get("groups", []) values["groups"] = [] for group in groups: - values["groups"].append(mock(Group, name=group)) + values["groups"].append(Group(name=group)) return values diff --git a/icekube/models/serviceaccount.py b/icekube/models/serviceaccount.py index 34edd1b..6aefb8b 100644 --- a/icekube/models/serviceaccount.py +++ b/icekube/models/serviceaccount.py @@ -5,7 +5,6 @@ from icekube.models.base import RELATIONSHIP, Resource from icekube.models.secret import Secret -from icekube.neo4j import mock from icekube.relationships import Relationship from pydantic import root_validator from pydantic.fields import Field @@ -13,6 +12,7 @@ class ServiceAccount(Resource): secrets: List[Secret] = Field(default_factory=list) + supported_api_groups: List[str] = [""] @root_validator(pre=True) def inject_secrets(cls, values): @@ -26,11 +26,10 @@ def inject_secrets(cls, values): for secret in data.get("secrets", []): values["secrets"].append( - mock( - Secret, + Secret( # type: ignore name=secret.get("name", ""), namespace=data.get("metadata", {}).get("namespace", ""), - ), + ) ) return values diff --git a/icekube/models/signer.py b/icekube/models/signer.py index e155d2d..8c9f39a 100644 --- a/icekube/models/signer.py +++ b/icekube/models/signer.py @@ -1,4 +1,4 @@ -from typing import Dict +from typing import Dict, List from icekube.models.base import Resource @@ -7,6 +7,7 @@ class Signer(Resource): apiVersion: str = "certificates.k8s.io/v1" kind: str = "Signer" plural: str = "signers" + supported_api_groups: List[str] = ["certificates.k8s.io"] def __repr__(self) -> str: return f"Signer(name={self.name})" diff --git a/icekube/models/user.py b/icekube/models/user.py index b497c9e..44ebbbe 100644 --- a/icekube/models/user.py +++ b/icekube/models/user.py @@ -1,13 +1,10 @@ from __future__ import annotations -from typing import Dict +from typing import List from icekube.models.base import Resource class User(Resource): plural: str = "users" - - @property - def unique_identifiers(self) -> Dict[str, str]: - return {**super().unique_identifiers, "plural": self.plural} + supported_api_groups: List[str] = ["", "user.openshift.io"] diff --git a/icekube/neo4j.py b/icekube/neo4j.py index 868dbe4..d4361c3 100644 --- a/icekube/neo4j.py +++ b/icekube/neo4j.py @@ -4,7 +4,7 @@ from typing import Any, Dict, Generator, List, Optional, Tuple, Type, TypeVar from icekube.config import config -from icekube.models import Cluster, Resource +from icekube.models import Resource from neo4j import BoltDriver, GraphDatabase from neo4j.io import ServiceUnavailable @@ -134,21 +134,3 @@ def find_or_mock(resource: Type[T], **kwargs: str) -> T: return next(find(resource, **kwargs)) # type: ignore except (StopIteration, IndexError, ServiceUnavailable): return resource(**kwargs) - - -def mock(resource: Type[T], **kwargs: str) -> T: - return resource(**kwargs) - - -cluster: Optional[Cluster] = None - - -def get_cluster_object() -> Cluster: - global cluster - - if cluster: - return cluster - - cluster = find_or_mock(Cluster, kind="Cluster") - - return cluster