├── tests ├── conftest.py ├── test_ordering.py ├── test_context.py ├── test_proxy.py └── test_acl.py ├── MANIFEST.in ├── rbac ├── __init__.py ├── context.py ├── proxy.py └── acl.py ├── .travis.yml ├── tox.ini ├── examples ├── acl.py ├── context.py └── proxy.py ├── setup.py ├── LICENSE ├── .gitignore └── README.rst /tests/conftest.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include MANIFEST.in -------------------------------------------------------------------------------- /rbac/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | """Simple RBAC 4 | 5 | This is a simple role based access control utility in Python. 6 | """ 7 | 8 | __all__ = ["acl", "context", "proxy"] 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | python: 4 | - "2.7" 5 | - "3.3" 6 | - "3.4" 7 | - "3.5" 8 | install: pip install tox-travis coveralls 9 | script: tox 10 | after_success: coveralls 11 | branches: 12 | only: 13 | - master 14 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 2.5 3 | envlist = py27,py33,py34,py35 4 | 5 | [testenv] 6 | deps = 7 | pytest 8 | pytest-cov 9 | flake8 10 | commands = 11 | flake8 12 | py.test --cov={envsitepackagesdir}/rbac --cov-append {posargs} 13 | -------------------------------------------------------------------------------- /examples/acl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import rbac.acl 4 | 5 | 6 | # create access control list 7 | acl = rbac.acl.Registry() 8 | 9 | # add roles 10 | acl.add_role("member") 11 | acl.add_role("student", ["member"]) 12 | acl.add_role("teacher", ["member"]) 13 | acl.add_role("junior-student", ["student"]) 14 | 15 | # add resources 16 | acl.add_resource("course") 17 | acl.add_resource("senior-course", ["course"]) 18 | 19 | # set rules 20 | acl.allow("member", "view", "course") 21 | acl.allow("student", "learn", "course") 22 | acl.allow("teacher", "teach", "course") 23 | acl.deny("junior-student", "learn", "senior-course") 24 | 25 | # use acl to check permission 26 | if acl.is_allowed("student", "view", "course"): 27 | print("Students chould view courses.") 28 | else: 29 | print("Students chould not view courses.") 30 | 31 | # use acl to check permission again 32 | if acl.is_allowed("junior-student", "learn", "senior-course"): 33 | print("Junior students chould learn senior courses.") 34 | else: 35 | print("Junior students chould not learn senior courses.") 36 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | with open('README.rst') as readme: 4 | next(readme) 5 | long_description = ''.join(readme).strip() 6 | 7 | setup( 8 | name='simple-rbac', 9 | version='0.1.1', 10 | description='A simple role based access control utility', 11 | long_description=long_description, 12 | keywords='rbac permission acl access-control', 13 | author='Jiangge Zhang', 14 | author_email='tonyseek@gmail.com', 15 | url='http://github.tonyseek.com/simple-rbac/', 16 | license='MIT License', 17 | packages=['rbac'], 18 | zip_safe=False, 19 | platforms=['Any'], 20 | classifiers=[ 21 | 'Programming Language :: Python', 22 | 'Programming Language :: Python :: 2', 23 | 'Programming Language :: Python :: 3', 24 | 'License :: OSI Approved :: MIT License', 25 | 'Operating System :: OS Independent', 26 | 'Development Status :: 4 - Beta', 27 | 'Intended Audience :: Developers', 28 | 'Topic :: Security', 29 | 'Topic :: Software Development :: Libraries :: Python Modules' 30 | ], 31 | ) 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012 TonySeek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/python 2 | 3 | ### Python ### 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *,cover 50 | .hypothesis/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv/ 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | 93 | # Rope project settings 94 | .ropeproject 95 | 96 | # End of https://www.gitignore.io/api/python 97 | -------------------------------------------------------------------------------- /examples/context.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from rbac.acl import Registry 4 | from rbac.context import IdentityContext, PermissionDenied 5 | 6 | 7 | # ----------------------------------------------- 8 | # build the access control list and add the rules 9 | # ----------------------------------------------- 10 | 11 | acl = Registry() 12 | context = IdentityContext(acl) 13 | 14 | acl.add_role("staff") 15 | acl.add_role("editor", parents=["staff"]) 16 | acl.add_role("bad man", parents=["staff"]) 17 | acl.add_resource("article") 18 | 19 | acl.allow("staff", "view", "article") 20 | acl.allow("editor", "edit", "article") 21 | acl.deny("bad man", None, "article") 22 | 23 | 24 | # ------------- 25 | # to be a staff 26 | # ------------- 27 | 28 | @context.set_roles_loader 29 | def first_load_roles(): 30 | yield "staff" 31 | 32 | 33 | print("* Now you are %s." % ", ".join(context.load_roles())) 34 | 35 | 36 | @context.check_permission("view", "article", message="can not view") 37 | def article_page(): 38 | return "" 39 | 40 | 41 | # use it as `decorator` 42 | @context.check_permission("edit", "article", message="can not edit") 43 | def edit_article_page(): 44 | return "" 45 | 46 | 47 | if article_page() == "": 48 | print("You could view the article page.") 49 | 50 | try: 51 | edit_article_page() 52 | except PermissionDenied as exception: 53 | print("You could not edit the article page, ") 54 | print("the exception said: '%s'." % exception.kwargs['message']) 55 | 56 | try: 57 | # use it as `with statement` 58 | with context.check_permission("edit", "article"): 59 | pass 60 | except PermissionDenied: 61 | print("Maybe it's because you are not a editor.") 62 | 63 | 64 | # -------------- 65 | # to be a editor 66 | # -------------- 67 | 68 | @context.set_roles_loader 69 | def second_load_roles(): 70 | yield "editor" 71 | 72 | 73 | print("* Now you are %s." % ", ".join(context.load_roles())) 74 | 75 | if edit_article_page() == "": 76 | print("You could edit the article page.") 77 | 78 | 79 | # --------------- 80 | # to be a bad man 81 | # --------------- 82 | 83 | @context.set_roles_loader 84 | def third_load_roles(): 85 | yield "bad man" 86 | 87 | 88 | print("* Now you are %s." % ", ".join(context.load_roles())) 89 | 90 | try: 91 | article_page() 92 | except PermissionDenied as exception: 93 | print("You could not view the article page,") 94 | print("the exception said: '%s'." % exception.kwargs['message']) 95 | 96 | # use it as `nonzero` 97 | if not context.check_permission("view", "article"): 98 | print("Oh! A bad man could not view the article page.") 99 | 100 | # use it as `check function` 101 | try: 102 | context.check_permission("edit", "article").check() 103 | except PermissionDenied as exception: 104 | print("Yes, of course, a bad man could not edit the article page too.") 105 | -------------------------------------------------------------------------------- /tests/test_ordering.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import pytest 4 | 5 | import rbac.acl 6 | import rbac.context 7 | 8 | 9 | class _FunctionProxy(object): 10 | def __init__(self, fn, evaluated_roles, role_idx=0): 11 | self.fn = fn 12 | self.role_idx = role_idx 13 | self.evaluated_roles = evaluated_roles 14 | 15 | def __call__(self, *args, **kwargs): 16 | role = args[self.role_idx] 17 | self.evaluated_roles.append(role) 18 | return self.fn.__call__(*args, **kwargs) 19 | 20 | 21 | @pytest.fixture 22 | def acl(): 23 | return rbac.acl.Registry() 24 | 25 | 26 | @pytest.fixture 27 | def context(acl): 28 | return rbac.context.IdentityContext(acl) 29 | 30 | 31 | @pytest.fixture 32 | def evaluated_roles(): 33 | return [] 34 | 35 | 36 | def test_role_evaluation_order_preserved(acl, context, evaluated_roles): 37 | # decorate acl.is_allowed so we can track role evaluation order 38 | setattr(acl, 'is_allowed', _FunctionProxy(acl.is_allowed, evaluated_roles)) 39 | 40 | # add roles as a list in the expected order (1 through 10) 41 | acl.add_resource('my_resource') 42 | roles = [str(i) for i in range(10)] 43 | for i, role in enumerate(roles): 44 | acl.add_role(role) 45 | context.set_roles_loader(lambda: roles) 46 | 47 | # allow only the final role to avoid short-circuiting 48 | acl.allow(roles[9], 'view', 'my_resource') 49 | context.has_permission('view', 'my_resource') 50 | 51 | # check that the roles were evaluated in order 52 | assert evaluated_roles == roles 53 | 54 | 55 | def test_short_circuit_skip_deny(acl, context, evaluated_roles): 56 | """ If no remaining role could grant access, don't bother checking """ 57 | # track which roles are evaluated 58 | setattr(acl, 'is_allowed', _FunctionProxy(acl.is_allowed, evaluated_roles)) 59 | 60 | acl.add_resource('the dinosaurs') 61 | roles = ['tourist', 'scientist', 'intern'] 62 | for role in roles: 63 | acl.add_role(role) 64 | context.set_roles_loader(lambda: roles) 65 | # explicitly deny one role and don't allow any permissions to others 66 | acl.deny('intern', 'feed', 'the dinosaurs') 67 | context.has_permission('feed', 'the dinosaurs') 68 | 69 | # no roles checked, since all are deny-only 70 | assert evaluated_roles == [] 71 | 72 | acl.allow('scientist', 'study', 'the dinosaurs') 73 | context.has_permission('feed', 'the dinosaurs') 74 | 75 | # since scientist is no longer deny-only, 76 | # only the intern check will be skipped 77 | assert evaluated_roles == ['tourist', 'scientist'] 78 | 79 | 80 | def test_short_circuit_skip_allow(acl, context, evaluated_roles): 81 | """Once one role is passed, shouldn't other roles should not be checked.""" 82 | # track which roles have their assertion function evaluated 83 | assertion = _FunctionProxy(lambda *args, **kwargs: args[1] == '3', 84 | evaluated_roles, role_idx=1) 85 | 86 | acl.add_resource('my_resource') 87 | roles = [str(i) for i in range(10)] 88 | for i, role in enumerate(roles): 89 | acl.add_role(role) 90 | acl.allow(role, 'view', 'my_resource', assertion=assertion) 91 | context.set_roles_loader(lambda: roles) 92 | context.has_permission('view', 'my_resource') 93 | 94 | # since role '3' was allowed, 'allowed' isn't checked on any role 95 | assert evaluated_roles == roles[0:4] 96 | -------------------------------------------------------------------------------- /rbac/context.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import functools 4 | 5 | 6 | __all__ = ["IdentityContext", "PermissionDenied"] 7 | 8 | 9 | class PermissionContext(object): 10 | """A context of decorator to check the permission.""" 11 | 12 | def __init__(self, checker, exception=None, **exception_kwargs): 13 | self._check = checker 14 | self.in_context = False 15 | self.exception = exception or PermissionDenied 16 | self.exception_kwargs = exception_kwargs 17 | 18 | def __call__(self, wrapped): 19 | def wrapper(*args, **kwargs): 20 | with self: 21 | return wrapped(*args, **kwargs) 22 | return functools.update_wrapper(wrapper, wrapped) 23 | 24 | def __enter__(self): 25 | self.in_context = True 26 | self.check() 27 | return self 28 | 29 | def __exit__(self, exception_type, exception, traceback): 30 | self.in_context = False 31 | 32 | def __bool__(self): 33 | return bool(self._check()) 34 | 35 | def __nonzero__(self): 36 | return self.__bool__() 37 | 38 | def check(self): 39 | if not self._check(): 40 | raise self.exception(**self.exception_kwargs) 41 | return True 42 | 43 | 44 | class IdentityContext(object): 45 | """A context of identity, providing the enviroment to control access.""" 46 | 47 | def __init__(self, acl, roles_loader=None): 48 | self.acl = acl 49 | self.set_roles_loader(roles_loader) 50 | 51 | def set_roles_loader(self, role_loader): 52 | """Set a callable object (such as a function) which could return a 53 | iteration to provide all roles of current context user. 54 | 55 | Example: 56 | >>> @context.set_roles_loader 57 | ... def load_roles(): 58 | ... user = request.context.current_user 59 | ... for role in user.roles: 60 | ... yield role 61 | """ 62 | self.load_roles = role_loader 63 | 64 | def check_permission(self, operation, resource, 65 | assertion_kwargs=None, **exception_kwargs): 66 | """A context to check the permission. 67 | 68 | The keyword arguments would be stored into the attribute `kwargs` of 69 | the exception `PermissionDenied`. 70 | 71 | If the key named `exception` is existed in the `kwargs`, it will be 72 | used instead of the `PermissionDenied`. 73 | 74 | The return value of this method could be use as a decorator, a with 75 | context enviroment or a boolean-like value. 76 | """ 77 | exception = exception_kwargs.pop("exception", PermissionDenied) 78 | checker = functools.partial(self._docheck, 79 | operation=operation, resource=resource, 80 | **assertion_kwargs or {}) 81 | return PermissionContext(checker, exception, **exception_kwargs) 82 | 83 | def has_permission(self, *args, **kwargs): 84 | return bool(self.check_permission(*args, **kwargs)) 85 | 86 | def has_roles(self, role_groups): 87 | had_roles = frozenset(self.load_roles()) 88 | return any(all(role in had_roles for role in role_group) 89 | for role_group in role_groups) 90 | 91 | def _docheck(self, operation, resource, **assertion_kwargs): 92 | had_roles = self.load_roles() 93 | role_list = list(had_roles) 94 | assert len(role_list) == len(set(role_list)) # duplicate role check 95 | return self.acl.is_any_allowed(role_list, operation, resource, 96 | **assertion_kwargs) 97 | 98 | 99 | class PermissionDenied(Exception): 100 | """The exception for denied access request.""" 101 | 102 | def __init__(self, message="", **kwargs): 103 | super(PermissionDenied, self).__init__(message) 104 | self.kwargs = kwargs 105 | self.kwargs['message'] = message 106 | -------------------------------------------------------------------------------- /rbac/proxy.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import functools 4 | import collections 5 | 6 | 7 | __all__ = ["dummy_factory", "model_role_factory", "model_resource_factory", 8 | "RegistryProxy"] 9 | 10 | # identity tuple 11 | identity = collections.namedtuple("identity", ["type", "cls", "id"]) 12 | role_identity = functools.partial(identity, "role-model") 13 | resource_identity = functools.partial(identity, "resource-model") 14 | 15 | 16 | def GetFullName(m): 17 | return "%s.%s" % (m.__module__, m.__name__) 18 | 19 | 20 | def DummyFactory(acl, obj): 21 | return obj 22 | 23 | 24 | # inline functions 25 | getfullname = GetFullName 26 | dummy_factory = DummyFactory 27 | 28 | 29 | def _model_identity_factory(obj, identity_maker, identity_adder): 30 | if not hasattr(obj, "id"): 31 | return obj 32 | 33 | if isinstance(obj, type): 34 | # make a identity tuple for the "class" 35 | identity = identity_maker(getfullname(obj), None) 36 | # register into access control list 37 | identity_adder(identity) 38 | else: 39 | # make a identity tuple for the "instance" and the "class" 40 | class_fullname = getfullname(obj.__class__) 41 | identity = identity_maker(class_fullname, obj.id) 42 | identity_type = identity_maker(class_fullname, None) 43 | # register into access control list 44 | identity_adder(identity, parents=[identity_type]) 45 | 46 | return identity 47 | 48 | 49 | def model_role_factory(acl, obj): 50 | """A factory to create a identity tuple from a model class or instance.""" 51 | return _model_identity_factory(obj, role_identity, acl.add_role) 52 | 53 | 54 | def model_resource_factory(acl, obj): 55 | """A factory to create a identity tuple from a model class or instance.""" 56 | return _model_identity_factory(obj, resource_identity, acl.add_resource) 57 | 58 | 59 | class RegistryProxy(object): 60 | """A proxy of the access control list. 61 | 62 | This proxy could use two factory function to create the role identity 63 | object and the resource identity object automatic. 64 | 65 | A example for the factory function: 66 | >>> def role_factory(acl, input_role): 67 | >>> role = ("my-role", str(input_role)) 68 | >>> acl.add_role(role) 69 | >>> return role 70 | """ 71 | 72 | def __init__(self, acl, role_factory=dummy_factory, 73 | resource_factory=model_resource_factory): 74 | self.acl = acl 75 | self.make_role = functools.partial(role_factory, self.acl) 76 | self.make_resource = functools.partial(resource_factory, self.acl) 77 | 78 | def add_role(self, role, parents=[]): 79 | role = self.make_role(role) 80 | parents = [self.make_role(parent) for parent in parents] 81 | return self.acl.add_role(role, parents) 82 | 83 | def add_resource(self, resource, parents=[]): 84 | resource = self.make_resource(resource) 85 | parents = [self.make_resource(parent) for parent in parents] 86 | return self.acl.add_resource(resource, parents) 87 | 88 | def allow(self, role, operation, resource, assertion=None): 89 | role = self.make_role(role) 90 | resource = self.make_resource(resource) 91 | return self.acl.allow(role, operation, resource, assertion) 92 | 93 | def deny(self, role, operation, resource, assertion=None): 94 | role = self.make_role(role) 95 | resource = self.make_resource(resource) 96 | return self.acl.deny(role, operation, resource, assertion) 97 | 98 | def is_allowed(self, role, operation, resource, **assertion_kwargs): 99 | role = self.make_role(role) 100 | resource = self.make_resource(resource) 101 | return self.acl.is_allowed(role, operation, 102 | resource, **assertion_kwargs) 103 | 104 | def is_any_allowed(self, roles, operation, resource, **assertion_kwargs): 105 | roles = [self.make_role(role) for role in roles] 106 | resource = self.make_resource(resource) 107 | return self.acl.is_any_allowed(roles, operation, 108 | resource, **assertion_kwargs) 109 | 110 | def __getattr__(self, attr): 111 | return getattr(self.acl, attr) 112 | -------------------------------------------------------------------------------- /examples/proxy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from sqlalchemy import create_engine, Column, Integer, String, ForeignKey 4 | from sqlalchemy.orm import sessionmaker, relationship 5 | from sqlalchemy.ext.declarative import declarative_base 6 | from rbac.acl import Registry 7 | from rbac.proxy import RegistryProxy 8 | from rbac.context import IdentityContext, PermissionDenied 9 | 10 | 11 | engine = create_engine('sqlite:///:memory:', echo=False) 12 | Session = sessionmaker(bind=engine) 13 | ModelBase = declarative_base() 14 | 15 | 16 | class ResourceMixin(object): 17 | 18 | def __eq__(self, other): 19 | return hasattr(other, "id") and self.id == other.id 20 | 21 | def __hash__(self): 22 | return hash(self.id) 23 | 24 | 25 | class User(ResourceMixin, ModelBase): 26 | """User Model""" 27 | 28 | __tablename__ = "user" 29 | id = Column(Integer, primary_key=True) 30 | name = Column(String, unique=True, nullable=False) 31 | roles = Column(String, nullable=False, default="") 32 | 33 | def get_roles(self): 34 | return self.roles.split(",") 35 | 36 | def set_roles(self, roles): 37 | self.roles = ",".join(roles) 38 | 39 | 40 | class Message(ResourceMixin, ModelBase): 41 | """Message Model""" 42 | 43 | __tablename__ = "post" 44 | id = Column(Integer, primary_key=True) 45 | content = Column(String, nullable=False) 46 | owner_id = Column(ForeignKey(User.id), nullable=False) 47 | owner = relationship(User, uselist=False, lazy="joined") 48 | 49 | 50 | def main(): 51 | # current context user 52 | current_user = None 53 | 54 | # create a access control list 55 | acl = RegistryProxy(Registry()) 56 | identity = IdentityContext(acl, lambda: current_user.get_roles()) 57 | 58 | # registry roles and resources 59 | acl.add_role("staff") 60 | acl.add_role("admin") 61 | acl.add_resource(Message) 62 | 63 | def check(acl, role, operation, resource): 64 | return db.query(Message).get(resource.id).owner is current_user 65 | 66 | is_message_owner = check 67 | acl.allow("staff", "create", Message) 68 | acl.allow("staff", "edit", Message, assertion=is_message_owner) 69 | acl.allow("admin", "edit", Message) 70 | 71 | db = Session() 72 | ModelBase.metadata.create_all(engine) 73 | 74 | tonyseek = User(name="tonyseek") 75 | tonyseek.set_roles(["staff"]) 76 | tom = User(name="tom") 77 | tom.set_roles(["staff"]) 78 | admin = User(name="admin") 79 | admin.set_roles(["admin"]) 80 | db.add_all([tonyseek, tom, admin]) 81 | db.commit() 82 | 83 | @identity.check_permission("create", Message) 84 | def create_message(content): 85 | message = Message(content=content, owner=current_user) 86 | db.add(message) 87 | db.commit() 88 | print("%s has craeted a message: '%s'." % ( 89 | current_user.name.capitalize(), content)) 90 | 91 | def edit_message(content, new_content): 92 | message = db.query(Message).filter_by(content=content).one() 93 | 94 | if not identity.check_permission("edit", message): 95 | print("%s tried to edit the message '%s' but he will fail." % ( 96 | current_user.name.capitalize(), content)) 97 | else: 98 | print("%s will edit the message '%s'." % ( 99 | current_user.name.capitalize(), content)) 100 | 101 | with identity.check_permission("edit", message): 102 | message.content = new_content 103 | db.commit() 104 | 105 | print("The message '%s' has been edit by %s," % ( 106 | content, current_user.name.capitalize())) 107 | print("the new content is '%s'" % new_content) 108 | 109 | # tonyseek signed in and create a message 110 | current_user = tonyseek 111 | create_message("Please open the door.") 112 | 113 | # tom signed in and edit tonyseek's message 114 | current_user = tom 115 | try: 116 | edit_message("Please open the door.", "Please don't open the door.") 117 | except PermissionDenied: 118 | print("Oh, the operation has been denied.") 119 | 120 | # tonyseek signed in and edit his message 121 | current_user = tonyseek 122 | edit_message("Please open the door.", "Please don't open the door.") 123 | 124 | # admin signed in and edit tonyseek's message 125 | current_user = admin 126 | edit_message("Please don't open the door.", "Please open the window.") 127 | 128 | 129 | if __name__ == "__main__": 130 | main() 131 | -------------------------------------------------------------------------------- /tests/test_context.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import pytest 4 | 5 | import rbac.acl 6 | import rbac.context 7 | 8 | 9 | @pytest.fixture 10 | def acl(): 11 | # create context 12 | acl = rbac.acl.Registry() 13 | # self.denied_error = rbac.context.PermissionDenied 14 | 15 | # register roles and resources 16 | acl.add_role('staff') 17 | acl.add_role('editor', parents=['staff']) 18 | acl.add_role('badguy', parents=['staff']) 19 | acl.add_resource('article') 20 | 21 | # add rules 22 | acl.allow('staff', 'view', 'article') 23 | acl.allow('editor', 'edit', 'article') 24 | acl.deny('badguy', None, 'article') 25 | 26 | return acl 27 | 28 | 29 | @pytest.fixture 30 | def context(acl): 31 | return rbac.context.IdentityContext(acl) 32 | 33 | 34 | @pytest.fixture 35 | def role_provider(acl, context): 36 | class singleton(object): 37 | def to_be_staff(self): 38 | @context.set_roles_loader 39 | def load_roles(): 40 | yield 'staff' 41 | 42 | yield 0 43 | 44 | def to_be_editor(self): 45 | @context.set_roles_loader 46 | def load_roles_0(): 47 | yield 'editor' 48 | 49 | yield 0 50 | 51 | @context.set_roles_loader 52 | def load_roles_1(): 53 | yield 'staff' 54 | yield 'editor' 55 | 56 | yield 1 57 | 58 | def to_be_badguy(self): 59 | @context.set_roles_loader 60 | def load_roles_0(): 61 | yield 'badguy' 62 | 63 | yield 0 64 | 65 | @context.set_roles_loader 66 | def load_roles_1(): 67 | yield 'staff' 68 | yield 'badguy' 69 | 70 | yield 1 71 | 72 | @context.set_roles_loader 73 | def load_roles_2(): 74 | yield 'editor' 75 | yield 'badguy' 76 | 77 | yield 2 78 | 79 | @context.set_roles_loader 80 | def load_roles_3(): 81 | yield 'staff' 82 | yield 'editor' 83 | yield 'badguy' 84 | 85 | yield 3 86 | 87 | def assert_call(self, view_article, edit_article): 88 | for _ in self.to_be_staff(): 89 | assert view_article() 90 | with pytest.raises(rbac.context.PermissionDenied): 91 | edit_article() 92 | 93 | for _ in self.to_be_editor(): 94 | assert view_article() 95 | assert edit_article() 96 | 97 | for _ in self.to_be_badguy(): 98 | with pytest.raises(rbac.context.PermissionDenied): 99 | view_article() 100 | with pytest.raises(rbac.context.PermissionDenied): 101 | edit_article() 102 | 103 | return singleton() 104 | 105 | 106 | def test_decorator(acl, context, role_provider): 107 | @context.check_permission('view', 'article') 108 | def view_article(): 109 | return True 110 | 111 | @context.check_permission('edit', 'article') 112 | def edit_article(): 113 | return True 114 | 115 | role_provider.assert_call(view_article, edit_article) 116 | 117 | 118 | def test_with_statement(acl, context, role_provider): 119 | def view_article(): 120 | with context.check_permission('view', 'article'): 121 | return True 122 | 123 | def edit_article(): 124 | with context.check_permission('edit', 'article'): 125 | return True 126 | 127 | role_provider.assert_call(view_article, edit_article) 128 | 129 | 130 | def test_check_function(acl, context, role_provider): 131 | check_view = context.check_permission('view', 'article').check 132 | check_edit = context.check_permission('edit', 'article').check 133 | role_provider.assert_call(check_view, check_edit) 134 | 135 | 136 | def test_nonzero(acl, context, role_provider): 137 | check_view = context.check_permission('view', 'article') 138 | check_edit = context.check_permission('edit', 'article') 139 | 140 | for _ in role_provider.to_be_staff(): 141 | assert bool(check_view) 142 | assert not bool(check_edit) 143 | 144 | for _ in role_provider.to_be_editor(): 145 | assert bool(check_view) 146 | assert bool(check_edit) 147 | 148 | for _ in role_provider.to_be_badguy(): 149 | assert not bool(check_view) 150 | assert not bool(check_edit) 151 | -------------------------------------------------------------------------------- /tests/test_proxy.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import pytest 4 | 5 | import rbac.acl 6 | import rbac.proxy 7 | 8 | 9 | # ----------- 10 | # Mock Models 11 | # ----------- 12 | 13 | class BaseModel(object): 14 | """The mock model base.""" 15 | 16 | storage = {} 17 | 18 | def __init__(self): 19 | self.storage[self.__class__.__name__, str(self.id)] = self 20 | return self 21 | 22 | @classmethod 23 | def query(cls, id): 24 | return cls.storage[cls.__name__, str(id)] 25 | 26 | 27 | class Role(BaseModel): 28 | """The mock role model.""" 29 | 30 | def __init__(self, name): 31 | self.name = name 32 | super(Role, self).__init__() 33 | 34 | @property 35 | def id(self): 36 | return self.name 37 | 38 | 39 | class Group(BaseModel): 40 | """The group model, a mock resource model.""" 41 | 42 | def __init__(self, name): 43 | self.name = name 44 | super(Group, self).__init__() 45 | 46 | @property 47 | def id(self): 48 | return self.name 49 | 50 | 51 | class Post(BaseModel): 52 | """The post model, a mock resource model.""" 53 | 54 | def __init__(self, title, author): 55 | self.title = title 56 | self.author = author 57 | super(Post, self).__init__() 58 | 59 | @property 60 | def id(self): 61 | return self.title 62 | 63 | 64 | @pytest.fixture 65 | def proxy(): 66 | acl = rbac.acl.Registry() 67 | 68 | # create a acl and give it a proxy 69 | proxy = rbac.proxy.RegistryProxy( 70 | acl, role_factory=rbac.proxy.model_role_factory, 71 | resource_factory=rbac.proxy.model_resource_factory) 72 | 73 | # create roles 74 | proxy.add_role(Role('staff')) 75 | proxy.add_role(Role('editor'), [ 76 | Role.query('staff'), 77 | ]) 78 | proxy.add_role(Role('manager'), [ 79 | Role.query('staff'), 80 | Role.query('editor'), 81 | ]) 82 | 83 | # create rules 84 | proxy.allow(Role.query('staff'), 'create', Post) 85 | proxy.allow(Role.query('editor'), 'edit', Post) 86 | proxy.deny(Role.query('manager'), 'edit', Post) 87 | proxy.allow(Role.query('staff'), 'join', Group) 88 | 89 | return proxy 90 | 91 | 92 | def test_undefined_models(proxy): 93 | visitor = Role('visitor') 94 | manager = Role.query('manager') 95 | staff = Role.query('staff') 96 | public_post = Post('This is public', 'Tom') 97 | 98 | proxy.allow(visitor, 'edit', public_post) 99 | proxy.deny(manager, 'edit', public_post) 100 | 101 | assert proxy.is_allowed(visitor, 'edit', public_post) 102 | assert not proxy.is_allowed(visitor, 'move', public_post) 103 | assert not proxy.is_allowed(manager, 'edit', public_post) 104 | assert not proxy.is_allowed(staff, 'edit', public_post) 105 | 106 | 107 | def test_rules(proxy): 108 | post = Post('Special Post', 'nobody') 109 | group = Group('Special Group') 110 | 111 | for role in [Role.query('staff'), Role.query('editor')]: 112 | assert proxy.is_allowed(role, 'create', Post) 113 | assert proxy.is_allowed(role, 'create', post) 114 | assert proxy.is_allowed(role, 'join', Group) 115 | assert proxy.is_allowed(role, 'join', group) 116 | 117 | manager = Role.query('manager') 118 | assert not proxy.is_allowed(manager, 'edit', Post) 119 | assert not proxy.is_allowed(manager, 'edit', post) 120 | assert proxy.is_allowed(manager, 'join', Group) 121 | assert proxy.is_allowed(manager, 'join', group) 122 | 123 | 124 | def test_recreate(proxy): 125 | BaseModel.storage.clear() 126 | 127 | for role in ['staff', 'editor', 'manager']: 128 | r = Role(role) 129 | del r 130 | 131 | test_rules(proxy) 132 | 133 | 134 | def test_owner_assertion(proxy): 135 | data = {'current_user': 'tom'} 136 | staff = Role.query('staff') 137 | 138 | def staff_is_owner_assertion(acl, role, operation, resource): 139 | return Post.query(resource.id).author == data['current_user'] 140 | 141 | proxy.allow(staff, 'edit', Post, staff_is_owner_assertion) 142 | 143 | post = Post("Tony's Post", 'tony') 144 | assert not proxy.is_allowed(staff, 'edit', post) 145 | data['current_user'] = 'tony' 146 | assert proxy.is_allowed(staff, 'edit', post) 147 | 148 | 149 | def test_is_any_allowed(proxy): 150 | proxy.add_role(Role('nobody')) 151 | 152 | no_allowed = ['staff', 'nobody'] 153 | no_allowed_one = ['staff'] 154 | 155 | one_allowed = ['staff', 'editor', 'nobody'] 156 | one_allowed_only = ['editor'] 157 | 158 | one_denied = ['staff', 'nobody', 'manager'] 159 | one_denied_with_allowed = ['staff', 'editor', 'manager'] 160 | 161 | def test_result(roles): 162 | return proxy.is_any_allowed( 163 | (Role.query(r) for r in roles), 'edit', Post) 164 | 165 | for roles in (no_allowed, no_allowed_one): 166 | assert not test_result(roles) 167 | 168 | for roles in (one_allowed, one_allowed_only): 169 | assert test_result(roles) 170 | 171 | for roles in (one_denied, one_denied_with_allowed): 172 | assert not test_result(roles) 173 | -------------------------------------------------------------------------------- /tests/test_acl.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import pytest 4 | 5 | import rbac.acl 6 | import rbac.proxy 7 | 8 | 9 | @pytest.fixture(params=[ 10 | lambda: rbac.acl.Registry(), 11 | lambda: rbac.proxy.RegistryProxy(rbac.acl.Registry()), 12 | ], ids=['registry', 'registry_proxy']) 13 | def acl(request): 14 | # create acl registry from parametrized factory 15 | acl = request.param() 16 | 17 | # add roles 18 | acl.add_role('user') 19 | acl.add_role('actived_user', parents=['user']) 20 | acl.add_role('writer', parents=['actived_user']) 21 | acl.add_role('manager', parents=['actived_user']) 22 | acl.add_role('editor', parents=['writer', 'manager']) 23 | acl.add_role('super') 24 | 25 | # add resources 26 | acl.add_resource('comment') 27 | acl.add_resource('post') 28 | acl.add_resource('news', parents=['post']) 29 | acl.add_resource('infor', parents=['post']) 30 | acl.add_resource('event', parents=['news']) 31 | 32 | # set super permission 33 | acl.allow('super', None, None) 34 | 35 | return acl 36 | 37 | 38 | def test_allow(acl): 39 | # add allowed rules 40 | acl.allow('actived_user', 'view', 'news') 41 | acl.allow('writer', 'new', 'news') 42 | 43 | # test 'view' operation 44 | roles = ['actived_user', 'writer', 'manager', 'editor'] 45 | 46 | for role in roles: 47 | for resource in ['news', 'event']: 48 | assert acl.is_allowed(role, 'view', resource) 49 | for resource in ['post', 'infor']: 50 | assert not acl.is_allowed(role, 'view', resource) 51 | 52 | for resource in ['news', 'event']: 53 | assert acl.is_any_allowed(roles, 'view', resource) 54 | for resource in ['post', 'infor']: 55 | assert not acl.is_any_allowed(roles, 'view', resource) 56 | 57 | for resource in ['post', 'news', 'infor', 'event']: 58 | assert not acl.is_allowed('user', 'view', resource) 59 | assert acl.is_allowed('super', 'view', resource) 60 | assert acl.is_allowed('super', 'new', resource) 61 | assert acl.is_any_allowed(['user', 'super'], 'view', resource) 62 | 63 | # test 'new' operation 64 | roles = ['writer', 'editor'] 65 | 66 | for role in roles: 67 | for resource in ['news', 'event']: 68 | assert acl.is_allowed(role, 'new', resource) 69 | for resource in ['post', 'infor']: 70 | assert not acl.is_allowed(role, 'new', resource) 71 | 72 | for resource in ['news', 'event']: 73 | assert acl.is_any_allowed(roles, 'new', resource) 74 | for resource in ['post', 'infor']: 75 | assert not acl.is_any_allowed(roles, 'new', resource) 76 | 77 | roles = ['user', 'manager'] 78 | 79 | for role in roles: 80 | for resource in ['news', 'event', 'post', 'infor']: 81 | assert not acl.is_allowed(role, 'new', resource) 82 | for resource in ['news', 'event', 'post', 'infor']: 83 | assert not acl.is_any_allowed(roles, 'new', resource) 84 | 85 | 86 | def test_deny(acl): 87 | # add allowed rule and denied rule 88 | acl.allow('actived_user', 'new', 'comment') 89 | acl.deny('manager', 'new', 'comment') 90 | 91 | # test allowed rules 92 | roles = ['actived_user', 'writer'] 93 | 94 | for role in roles: 95 | assert acl.is_allowed(role, 'new', 'comment') 96 | 97 | assert acl.is_any_allowed(roles, 'new', 'comment') 98 | 99 | # test denied rules 100 | roles = ['manager', 'editor'] 101 | 102 | for role in roles: 103 | assert not acl.is_allowed(role, 'new', 'comment') 104 | 105 | assert not acl.is_any_allowed(roles, 'new', 'comment') 106 | 107 | 108 | def test_undefined(acl): 109 | # test denied undefined rule 110 | roles = ['user', 'actived_user', 'writer', 'manager', 'editor'] 111 | 112 | for resource in ['comment', 'post', 'news', 'infor', 'event']: 113 | for role in roles: 114 | assert not acl.is_allowed(role, 'x', resource) 115 | assert not acl.is_allowed(role, '', resource) 116 | assert not acl.is_allowed(role, None, resource) 117 | assert not acl.is_any_allowed(roles, 'x', resource) 118 | assert not acl.is_any_allowed(roles, '', resource) 119 | assert not acl.is_any_allowed(roles, None, resource) 120 | 121 | # test `None` defined rule 122 | for resource in ['comment', 'post', 'news', 'infor', 'event', None]: 123 | for op in ['undefined', 'x', '', None]: 124 | assert acl.is_allowed('super', op, resource) 125 | 126 | 127 | def test_assertion(acl): 128 | # set up assertion 129 | db = {'newsid': 1} 130 | 131 | def check(acl, role, operation, resource): 132 | return db['newsid'] == 10 133 | 134 | assertion = check 135 | 136 | # set up rules 137 | acl.add_role('writer2', parents=['writer']) 138 | acl.allow('writer', 'edit', 'news', assertion) 139 | acl.allow('manager', 'edit', 'news') 140 | 141 | # test while assertion is invalid 142 | assert not acl.is_allowed('writer', 'edit', 'news') 143 | assert not acl.is_allowed('writer2', 'edit', 'news') 144 | assert acl.is_allowed('manager', 'edit', 'news') 145 | assert acl.is_allowed('editor', 'edit', 'news') 146 | 147 | # test while assertion is valid 148 | db['newsid'] = 10 149 | assert acl.is_allowed('writer', 'edit', 'news') 150 | assert acl.is_allowed('editor', 'edit', 'news') 151 | assert acl.is_allowed('manager', 'edit', 'news') 152 | 153 | 154 | def test_is_any_allowed(acl): 155 | pass # TODO: create a test 156 | -------------------------------------------------------------------------------- /rbac/acl.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import itertools 4 | 5 | 6 | __all__ = ["Registry"] 7 | 8 | 9 | class Registry(object): 10 | """The registry of access control list.""" 11 | 12 | def __init__(self): 13 | self._roles = {} 14 | self._resources = {} 15 | self._allowed = {} 16 | self._denied = {} 17 | 18 | # to allow additional short circuiting, track roles that only 19 | # ever deny access 20 | self._denial_only_roles = set() 21 | self._children = {} 22 | 23 | def add_role(self, role, parents=[]): 24 | """Add a role or append parents roles to a special role. 25 | 26 | All added roles should be hashable. 27 | (http://docs.python.org/glossary.html#term-hashable) 28 | """ 29 | self._roles.setdefault(role, set()) 30 | self._roles[role].update(parents) 31 | for p in parents: 32 | self._children.setdefault(p, set()) 33 | self._children[p].add(role) 34 | 35 | # all roles start as deny-only (unless one of its parents 36 | # isn't deny-only) 37 | if not parents or self._roles_are_deny_only(parents): 38 | self._denial_only_roles.add(role) 39 | 40 | def add_resource(self, resource, parents=[]): 41 | """Add a resource or append parents resources to a special resource. 42 | 43 | All added resources should be hashable. 44 | (http://docs.python.org/glossary.html#term-hashable) 45 | """ 46 | self._resources.setdefault(resource, set()) 47 | self._resources[resource].update(parents) 48 | 49 | def allow(self, role, operation, resource, assertion=None): 50 | """Add a allowed rule. 51 | 52 | The added rule will allow the role and its all children roles to 53 | operate the resource. 54 | """ 55 | assert not role or role in self._roles 56 | assert not resource or resource in self._resources 57 | self._allowed[role, operation, resource] = assertion 58 | 59 | # since we just allowed a permission, role and any children aren't 60 | # denied-only 61 | for r in itertools.chain([role], get_family(self._children, role)): 62 | self._denial_only_roles.discard(r) 63 | 64 | def deny(self, role, operation, resource, assertion=None): 65 | """Add a denied rule. 66 | 67 | The added rule will deny the role and its all children roles to 68 | operate the resource. 69 | """ 70 | assert not role or role in self._roles 71 | assert not resource or resource in self._resources 72 | self._denied[role, operation, resource] = assertion 73 | 74 | def is_allowed(self, role, operation, resource, check_allowed=True, 75 | **assertion_kwargs): 76 | """Check the permission. 77 | 78 | If the access is denied, this method will return False; if the access 79 | is allowed, this method will return True; if there is not any rule 80 | for the access, this method will return None. 81 | """ 82 | assert not role or role in self._roles 83 | assert not resource or resource in self._resources 84 | 85 | roles = set(get_family(self._roles, role)) 86 | operations = {None, operation} 87 | resources = set(get_family(self._resources, resource)) 88 | 89 | def DefaultAssertion(*args, **kwargs): 90 | return True 91 | 92 | is_allowed = None 93 | default_assertion = DefaultAssertion 94 | 95 | for permission in itertools.product(roles, operations, resources): 96 | if permission in self._denied: 97 | assertion = self._denied[permission] or default_assertion 98 | if assertion(self, role, operation, resource, 99 | **assertion_kwargs): 100 | return False # denied by rule immediately 101 | 102 | if check_allowed and permission in self._allowed: 103 | assertion = self._allowed[permission] or default_assertion 104 | if assertion(self, role, operation, resource, 105 | **assertion_kwargs): 106 | is_allowed = True # allowed by rule 107 | 108 | return is_allowed 109 | 110 | def is_any_allowed(self, roles, operation, resource, **assertion_kwargs): 111 | """Check the permission with many roles.""" 112 | is_allowed = None # no matching rules 113 | for i, role in enumerate(roles): 114 | # if access not yet allowed and all remaining roles could 115 | # only deny access, short-circuit and return False 116 | if not is_allowed and self._roles_are_deny_only(roles[i:]): 117 | return False 118 | 119 | check_allowed = not is_allowed 120 | 121 | # if another role gave access, 122 | # don't bother checking if this one is allowed 123 | is_current_allowed = self.is_allowed(role, operation, resource, 124 | check_allowed=check_allowed, 125 | **assertion_kwargs) 126 | if is_current_allowed is False: 127 | return False # denied by rule 128 | elif is_current_allowed is True: 129 | is_allowed = True 130 | return is_allowed 131 | 132 | def _roles_are_deny_only(self, roles): 133 | return all(r in self._denial_only_roles for r in roles) 134 | 135 | 136 | def get_family(all_parents, current): 137 | """Iterate current object and its all parents recursively.""" 138 | yield current 139 | for parent in get_parents(all_parents, current): 140 | yield parent 141 | yield None 142 | 143 | 144 | def get_parents(all_parents, current): 145 | """Iterate current object's all parents.""" 146 | for parent in all_parents.get(current, []): 147 | yield parent 148 | for grandparent in get_parents(all_parents, parent): 149 | yield grandparent 150 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | |Build Status| |Coverage Status| |PyPI Version| |Wheel Status| 2 | 3 | Simple RBAC 4 | =========== 5 | 6 | This is a simple role based access control utility in Python. 7 | 8 | Quick Start 9 | ----------- 10 | 11 | 1. Install Simple RBAC 12 | ~~~~~~~~~~~~~~~~~~~~~~ 13 | 14 | :: 15 | 16 | pip install simple-rbac 17 | 18 | 2. Create a Access Control List 19 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 20 | 21 | :: 22 | 23 | import rbac.acl 24 | 25 | acl = rbac.acl.Registry() 26 | 27 | 3. Register Roles and Resources 28 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 29 | 30 | :: 31 | 32 | acl.add_role("member") 33 | acl.add_role("student", ["member"]) 34 | acl.add_role("teacher", ["member"]) 35 | acl.add_role("junior-student", ["student"]) 36 | 37 | acl.add_resource("course") 38 | acl.add_resource("senior-course", ["course"]) 39 | 40 | 4. Add Rules 41 | ~~~~~~~~~~~~ 42 | 43 | :: 44 | 45 | acl.allow("member", "view", "course") 46 | acl.allow("student", "learn", "course") 47 | acl.allow("teacher", "teach", "course") 48 | acl.deny("junior-student", "learn", "senior-course") 49 | 50 | 5. Use It to Check Permission 51 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 52 | 53 | :: 54 | 55 | if acl.is_allowed("student", "view", "course"): 56 | print("Students chould view courses.") 57 | else: 58 | print("Students chould not view courses.") 59 | 60 | if acl.is_allowed("junior-student", "learn", "senior-course"): 61 | print("Junior students chould learn senior courses.") 62 | else: 63 | print("Junior students chould not learn senior courses.") 64 | 65 | Custom Role and Resource Class 66 | ------------------------------ 67 | 68 | It’s not necessary to use string as role object and resource object like 69 | "Quick Start". You could define role class and resource class of 70 | yourself, such as a database mapped model in SQLAlchemy. 71 | 72 | Whatever which role class and resource class you will use, it must 73 | implement ``__hash__`` method and ``__eq__`` method to be `hashable`_. 74 | 75 | Example 76 | ~~~~~~~ 77 | 78 | :: 79 | 80 | class Role(db.Model): 81 | """The role.""" 82 | 83 | id = db.Column(db.Integer, primary_key=True) 84 | screen_name = db.Column(db.Unicode, nullable=False, unique=True) 85 | 86 | def __hash__(self): 87 | return hash("ROLE::%d" % self.id) 88 | 89 | def __eq__(self, other): 90 | return self.id == other.id 91 | 92 | 93 | class Resource(db.Model): 94 | """The resource.""" 95 | 96 | id = db.Column(db.Integer, primary_key=True) 97 | screen_name = db.Column(db.Unicode, nullable=False, unique=True) 98 | 99 | def __hash__(self): 100 | return hash("RESOURCE::%d" % self.id) 101 | 102 | def __eq__(self, other): 103 | return self.id == other.id 104 | 105 | Of course, You could use the built-in hashable types too, such as tuple, 106 | namedtuple, frozenset and more. 107 | 108 | Use the Identity Context Check Your Permission 109 | ---------------------------------------------- 110 | 111 | Obviously, the work of checking permission is a cross-cutting concern. 112 | The module named ``rbac.context``, our ``IdentityContext``, provide some 113 | ways to make our work neater. 114 | 115 | 1. Create the Context Manager 116 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 117 | 118 | :: 119 | 120 | acl = Registry() 121 | context = IdentityContext(acl) 122 | 123 | 2. Set a Loader 124 | ~~~~~~~~~~~~~~~ 125 | 126 | The loader should load the roles of current user. 127 | 128 | :: 129 | 130 | from myapp import get_current_user 131 | 132 | @context.set_roles_loader 133 | def second_load_roles(): 134 | user = get_current_user() 135 | yield "everyone" 136 | for role in user.roles: 137 | yield str(role) 138 | 139 | 3. Protect Your Action 140 | ~~~~~~~~~~~~~~~~~~~~~~ 141 | 142 | Now you could protect your action from unauthorized access. As you 143 | please, you could choose many ways to check the permission, including 144 | python ``decorator``, python ``with statement`` or simple method 145 | calling. 146 | 147 | Decorator 148 | ^^^^^^^^^ 149 | 150 | :: 151 | 152 | @context.check_permission("view", "article", message="can't view") 153 | def article_page(): 154 | return "your-article" 155 | 156 | With Statement 157 | ^^^^^^^^^^^^^^ 158 | 159 | :: 160 | 161 | def article_page(): 162 | with context.check_permission("view", "article", message="can't view"): 163 | return "your-article" 164 | 165 | Simple Method Calling 166 | ^^^^^^^^^^^^^^^^^^^^^ 167 | 168 | :: 169 | 170 | def article_page(): 171 | context.check_permission("view", "article", message="can't view").check() 172 | return "your-article" 173 | 174 | Exception Handler and Non-Zero Checking 175 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 176 | 177 | Whatever which way you choosen, a exception 178 | ``rbac.context.PermissionDenied`` will be raised while a unauthorized 179 | access happening. The keyword arguments sent to the 180 | ``context.check_permission`` will be set into a attirbute named 181 | ``kwargs`` of the exception. You could get those data in your exception 182 | handler. 183 | 184 | :: 185 | 186 | @context.check_permission("view", "article", message="can not view") 187 | def article_page(): 188 | return "your-article" 189 | 190 | try: 191 | print article_page() 192 | except PermissionDenied as exception: 193 | print "The access has been denied, you %s" % exception.kwargs['message'] 194 | 195 | If you don’t want to raise the exception but only check the access is 196 | allowed or not, you could use the checking like a boolean value. 197 | 198 | :: 199 | 200 | if not context.check_permission("view", "article"): 201 | print "Oh! the access has been denied." 202 | 203 | is_allowed = bool(context.check_permission("view", "article")) 204 | 205 | .. _hashable: http://docs.python.org/glossary.html#term-hashable 206 | 207 | 208 | .. |Build Status| image:: https://img.shields.io/travis/tonyseek/simple-rbac.svg?style=flat 209 | :target: https://travis-ci.org/tonyseek/simple-rbac 210 | :alt: Build Status 211 | .. |Coverage Status| image:: https://img.shields.io/coveralls/tonyseek/simple-rbac.svg?style=flat 212 | :target: https://coveralls.io/r/tonyseek/simple-rbac 213 | :alt: Coverage Status 214 | .. |Wheel Status| image:: https://img.shields.io/pypi/wheel/simple-rbac.svg?style=flat 215 | :target: https://warehouse.python.org/project/simple-rbac 216 | :alt: Wheel Status 217 | .. |PyPI Version| image:: https://img.shields.io/pypi/v/simple-rbac.svg?style=flat 218 | :target: https://pypi.python.org/pypi/simple-rbac 219 | :alt: PyPI Version 220 | --------------------------------------------------------------------------------