Skip to content

RBAC:Python

Python으로 RBAC 구현하는 샘플.

rbac.py Code

# -*- coding: utf-8 -*-

from typing import Any, Dict

from recc.rbac.rbac_domain import RbacDomain
from recc.rbac.rbac_exception import RbacKeyError
from recc.rbac.rbac_permission import RbacPermission
from recc.rbac.rbac_role import RbacRole
from recc.rbac.rbac_subject import RbacSubject


class Rbac:
    def __init__(self):
        self._domains: Dict[Any, RbacDomain] = dict()
        self._roles: Dict[Any, RbacRole] = dict()
        self._permissions: Dict[Any, RbacPermission] = dict()
        self._subjects: Dict[Any, RbacSubject] = dict()

    def create_domain(self, key: Any) -> RbacDomain:
        if key in self._domains:
            raise RbacKeyError(f"Already exists domain-key: {key}")
        domain = RbacDomain(self, key)
        self._domains[key] = domain
        return domain

    def create_role(self, key: Any) -> RbacRole:
        if key in self._roles:
            raise RbacKeyError(f"Already exists role-key: {key}")
        role = RbacRole(self, key)
        self._roles[key] = role
        return role

    def create_permission(self, key: Any) -> RbacPermission:
        if key in self._permissions:
            raise RbacKeyError(f"Already exists permission-key: {key}")
        perm = RbacPermission(self, key)
        self._permissions[key] = perm
        return perm

    def create_subject(self, key: Any) -> RbacSubject:
        if key in self._subjects:
            raise RbacKeyError(f"Already exists subject-key: {key}")
        subject = RbacSubject(self, key)
        self._subjects[key] = subject
        return subject

rbac_domain.py Code

# -*- coding: utf-8 -*-

from typing import Any, Dict, Optional

from recc.rbac.rbac_exception import RbacKeyError, RbacMismatchContextError
from recc.rbac.rbac_permission import RbacPermission
from recc.rbac.rbac_role import RbacRole
from recc.rbac.rbac_subject import RbacSubject


class RbacDomain:
    def __init__(self, rbac, key: Any, parent: Optional["RbacDomain"] = None):
        self._rbac = rbac
        self._key = key
        self._parent = parent
        self._subdomains: Dict[Any, RbacDomain] = dict()
        self._subjects: Dict[RbacSubject, RbacRole] = dict()

    def __repr__(self) -> str:
        return f"RbacDomain[rbac={id(self._rbac)},key={self._key}]"

    def __str__(self) -> str:
        return self._key

    @property
    def context(self):
        return self._rbac

    def create_subdomain(self, key: Any) -> "RbacDomain":
        if key in self._subdomains:
            raise RbacKeyError(f"Already exists subdomain-key: {key}")
        project = RbacDomain(self._rbac, key, self)
        self._subdomains[key] = project
        return project

    def add_subject(self, subject: RbacSubject, role: RbacRole) -> None:
        if self.context != subject.context:
            raise RbacMismatchContextError
        if self.context != role.context:
            raise RbacMismatchContextError
        self._subjects[subject] = role

    def test_permission(
        self,
        subject: RbacSubject,
        *permission: RbacPermission,
        inherit=True,
    ) -> None:
        if self.context != subject.context:
            raise RbacMismatchContextError
        for p in permission:
            if self.context != p.context:
                raise RbacMismatchContextError

        if subject in self._subjects:
            role = self._subjects[subject]
            for p in permission:
                role.test_permission(p)
        elif inherit and self._parent is not None:
            self._parent.test_permission(subject, *permission, inherit=inherit)
        else:
            raise RbacKeyError(f"Not found subject: {str(subject)}")

    def has_permission(
        self,
        subject: RbacSubject,
        *permissions: RbacPermission,
        inherit=True,
    ) -> bool:
        try:
            self.test_permission(subject, *permissions, inherit=inherit)
        except:  # noqa
            return False
        else:
            return True

rbac_exception.py Code

# -*- coding: utf-8 -*-


class RbacError(Exception):
    def __init__(self, *args):
        super().__init__(*args)


class RbacKeyError(RbacError):
    def __init__(self, *args):
        super().__init__(*args)


class RbacMismatchContextError(RbacError):
    def __init__(self, *args):
        super().__init__(*args)


class RbacPermissionError(RbacError):
    def __init__(self, *args):
        super().__init__(*args)

rbac_permission.py Code

# -*- coding: utf-8 -*-

from typing import Any


class RbacPermission:
    def __init__(self, rbac, key: Any):
        self._rbac = rbac
        self._key = key

    def __repr__(self) -> str:
        return f"RbacPermission[rbac={id(self._rbac)},key={self._key}]"

    def __str__(self) -> str:
        return self._key

    @property
    def context(self):
        return self._rbac

    @property
    def key(self) -> Any:
        return self._key

rbac_role.py Code

# -*- coding: utf-8 -*-

from typing import Any, Set

from recc.rbac.rbac_exception import RbacMismatchContextError, RbacPermissionError
from recc.rbac.rbac_permission import RbacPermission


class RbacRole:
    def __init__(self, rbac, key: Any):
        self._rbac = rbac
        self._key = key
        self._allows: Set[RbacPermission] = set()
        self._denies: Set[RbacPermission] = set()

    def __repr__(self) -> str:
        return f"RbacRole[rbac={id(self._rbac)},key={self._key}]"

    def __str__(self) -> str:
        return self._key

    @property
    def context(self):
        return self._rbac

    @property
    def key(self) -> Any:
        return self._key

    def allow_permission(self, *permissions: RbacPermission) -> None:
        for p in permissions:
            if self.context != p.context:
                raise RbacMismatchContextError

        for p in permissions:
            self._allows.add(p)

    def deny_permission(self, *permissions: RbacPermission) -> None:
        for p in permissions:
            if self.context != p.context:
                raise RbacMismatchContextError

        for p in permissions:
            self._denies.add(p)

    def test_allow_permission(self, permission: RbacPermission) -> None:
        if self.context != permission.context:
            raise RbacMismatchContextError

        if permission not in self._allows:
            raise RbacPermissionError(f"Not allowed permission: {str(permission)}")

    def test_deny_permission(self, permission: RbacPermission) -> None:
        if self.context != permission.context:
            raise RbacMismatchContextError

        if permission in self._denies:
            raise RbacPermissionError(f"Denied permission: {str(permission)}")

    def test_permission(self, permission: RbacPermission) -> None:
        if self.context != permission.context:
            raise RbacMismatchContextError

        self.test_allow_permission(permission)
        self.test_deny_permission(permission)

rbac_subject.py Code

# -*- coding: utf-8 -*-

from typing import Any


class RbacSubject:
    def __init__(self, rbac, key: Any):
        self._rbac = rbac
        self._key = key

    def __repr__(self) -> str:
        return f"RbacSubject[rbac={id(self._rbac)},key={self._key}]"

    def __str__(self) -> str:
        return self._key

    @property
    def context(self):
        return self._rbac

    @property
    def key(self) -> Any:
        return self._key

TestCase

# -*- coding: utf-8 -*-

from unittest import TestCase, main

from recc.rbac.rbac import Rbac
from recc.rbac.rbac_exception import RbacMismatchContextError


class RbacTestCase(TestCase):
    def test_basic_scenario(self):
        rbac = Rbac()

        # Permissions
        view = rbac.create_permission("view")
        edit = rbac.create_permission("edit")

        # Roles
        admin = rbac.create_role("admin")
        guest = rbac.create_role("guest")

        # Role to Permission
        admin.allow_permission(edit, view)
        guest.allow_permission(view)

        # Users
        user1 = rbac.create_subject("user1")
        user2 = rbac.create_subject("user2")
        user3 = rbac.create_subject("user3")

        # Groups
        group1 = rbac.create_domain("group1")

        # Projects
        project1 = group1.create_subdomain("project1")
        project2 = group1.create_subdomain("project2")

        # Group members
        group1.add_subject(user1, admin)
        group1.add_subject(user2, guest)
        self.assertTrue(group1.has_permission(user1, view))
        self.assertTrue(group1.has_permission(user1, edit))
        self.assertTrue(group1.has_permission(user2, view))
        self.assertFalse(group1.has_permission(user2, edit))
        self.assertFalse(group1.has_permission(user3, view))
        self.assertFalse(group1.has_permission(user3, edit))

        # Project members
        project1.add_subject(user1, guest)
        project1.add_subject(user2, admin)
        self.assertTrue(project1.has_permission(user1, view))
        self.assertFalse(project1.has_permission(user1, edit))
        self.assertTrue(project1.has_permission(user2, view))
        self.assertTrue(project1.has_permission(user2, edit))
        self.assertFalse(project1.has_permission(user3, view))
        self.assertFalse(project1.has_permission(user3, edit))

        project2.add_subject(user3, guest)
        self.assertTrue(project2.has_permission(user1, view))
        self.assertTrue(project2.has_permission(user1, edit))
        self.assertTrue(project2.has_permission(user2, view))
        self.assertFalse(project2.has_permission(user2, edit))
        self.assertTrue(project2.has_permission(user3, view))
        self.assertFalse(project2.has_permission(user3, edit))

        self.assertFalse(project2.has_permission(user1, view, inherit=False))
        self.assertFalse(project2.has_permission(user1, edit, inherit=False))
        self.assertFalse(project2.has_permission(user2, view, inherit=False))
        self.assertFalse(project2.has_permission(user2, edit, inherit=False))
        self.assertTrue(project2.has_permission(user3, view, inherit=False))
        self.assertFalse(project2.has_permission(user3, edit, inherit=False))

    def test_context_mismatch(self):
        rbac1 = Rbac()
        view1 = rbac1.create_permission("view")
        role1 = rbac1.create_role("role")

        rbac2 = Rbac()
        view2 = rbac2.create_permission("view")
        role2 = rbac2.create_role("role")

        with self.assertRaises(RbacMismatchContextError):
            role2.allow_permission(view1)
        with self.assertRaises(RbacMismatchContextError):
            role1.allow_permission(view2)


if __name__ == "__main__":
    main()

See also