├── tests ├── __init__.py ├── test_scenario_flat.py ├── test_scenario_hierarchy.py └── test_scenario_constraints.py ├── setup.py ├── pyproject.toml ├── Pipfile ├── .github └── FUNDING.yml ├── LICENSE ├── setup.cfg ├── .gitignore ├── README.md └── rbac └── __init__.py /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | setuptools.setup() 4 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | 8 | [dev-packages] 9 | pytest = "*" 10 | 11 | [requires] 12 | python_version = "3.9" 13 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: muratgozel 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Murat Gözel 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. 22 | -------------------------------------------------------------------------------- /tests/test_scenario_flat.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from rbac import RBAC, RBACConfigurationError, RBACAuthorizationError 3 | 4 | def test_scenario_flat(): 5 | rbac = RBAC() 6 | assert isinstance(rbac, RBAC) == True 7 | 8 | jr_editor = rbac.create_role('jr_editor') 9 | assert jr_editor.name == 'jr_editor' 10 | assert jr_editor.children == None 11 | 12 | article = rbac.create_domain('article') 13 | assert article.name == 'article' 14 | 15 | create = rbac.create_permission('c') 16 | assert create.name == 'c' 17 | read = rbac.create_permission('r') 18 | update = rbac.create_permission('u') 19 | delete = rbac.create_permission('d') 20 | 21 | jr_editor.add_permission(create, article) 22 | jr_editor.add_permission(read, article) 23 | 24 | subject = rbac.create_subject('some_int_or_str') 25 | assert subject.identifier == 'some_int_or_str' 26 | subject.authorize(jr_editor) 27 | 28 | with pytest.raises(RBACAuthorizationError): 29 | rbac.go('some_int_or_str', article, 'u') 30 | 31 | with pytest.raises(RBACAuthorizationError): 32 | rbac.go('some_int_or_str', article, create) 33 | 34 | rbac.lock() 35 | 36 | assert rbac.go('some_int_or_str', article, create) is None 37 | -------------------------------------------------------------------------------- /tests/test_scenario_hierarchy.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from rbac import RBAC, RBACConfigurationError, RBACAuthorizationError 3 | 4 | def test_scenario_hierarchy(): 5 | rbac = RBAC() 6 | 7 | jr_editor = rbac.create_role('jr_editor') 8 | editor = rbac.create_role('editor', children=jr_editor) 9 | it = rbac.create_role('it', children=(jr_editor, editor)) 10 | 11 | article = rbac.create_domain('article') 12 | service_conf = rbac.create_domain('service_conf') 13 | 14 | create = rbac.create_permission('c') 15 | read = rbac.create_permission('r') 16 | update = rbac.create_permission('u') 17 | delete = rbac.create_permission('d') 18 | 19 | jr_editor.add_permission(read, article) 20 | editor.add_permission(create, article) 21 | it.add_permission((create, read, update, delete), service_conf) 22 | 23 | john = rbac.create_subject(1) 24 | john.authorize(jr_editor) 25 | jack = rbac.create_subject(2) 26 | jack.authorize(editor) 27 | mark = rbac.create_subject(3) 28 | mark.authorize(it) 29 | 30 | rbac.lock() 31 | 32 | assert rbac.go(2, article, read) is None 33 | assert rbac.go(1, article, read) is None 34 | assert rbac.go(3, article, create) is None 35 | assert rbac.go(3, service_conf, create) is None 36 | 37 | with pytest.raises(RBACAuthorizationError): 38 | rbac.go(1, article, create) 39 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = py-rbac 3 | version = 20.12.3 4 | license_file = LICENSE 5 | author = Murat Gözel 6 | author_email = murat@gozel.com.tr 7 | description = Python implementation of the NIST model for role based access control (RBAC). 8 | long_description = file: README.md 9 | long_description_content_type = text/markdown 10 | keywords = rbac, access control, role based access control 11 | url = https://github.com/muratgozel/py-rbac 12 | project_urls = 13 | Source = https://github.com/muratgozel/py-rbac 14 | Documentation = https://github.com/muratgozel/py-rbac 15 | Bug Reports = https://github.com/muratgozel/py-rbac/issues 16 | Funding = https://ko-fi.com/muratgozel 17 | classifiers = 18 | Intended Audience :: Developers 19 | License :: OSI Approved :: MIT License 20 | Natural Language :: English 21 | Operating System :: MacOS :: MacOS X 22 | Operating System :: POSIX 23 | Operating System :: POSIX :: BSD 24 | Operating System :: POSIX :: Linux 25 | Operating System :: Microsoft :: Windows 26 | Programming Language :: Python 27 | Programming Language :: Python :: 3 28 | Programming Language :: Python :: 3 :: Only 29 | Programming Language :: Python :: 3.6 30 | Programming Language :: Python :: 3.7 31 | Programming Language :: Python :: 3.8 32 | Programming Language :: Python :: 3.9 33 | 34 | [options] 35 | packages = rbac 36 | -------------------------------------------------------------------------------- /tests/test_scenario_constraints.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from rbac import RBAC, RBACConfigurationError, RBACAuthorizationError 3 | 4 | def test_scenario_constraints(): 5 | class Article(): 6 | """docstring for Article.""" 7 | 8 | def __init__(self, id, author): 9 | self.id = id 10 | self.author = author 11 | 12 | rbac = RBAC() 13 | 14 | jr_editor = rbac.create_role('jr_editor', max_subjects=1) 15 | editor = rbac.create_role('editor', children=jr_editor, max_subjects=10) 16 | chief = rbac.create_role('chief', children=(jr_editor, editor), inherit=False, max_subjects=1) 17 | 18 | article = rbac.create_domain(Article) 19 | 20 | create = rbac.create_permission('c') 21 | read = rbac.create_permission('r') 22 | update = rbac.create_permission('u') 23 | delete = rbac.create_permission('d') 24 | 25 | jr_editor.add_permission(read, article) 26 | jr_editor.add_permission(delete, article, match_domain_prop='author') 27 | editor.add_permission(create, article) 28 | 29 | john = rbac.create_subject(1) 30 | john.authorize(jr_editor) 31 | another_john = rbac.create_subject(2) 32 | another_john.authorize(jr_editor) 33 | 34 | with pytest.raises(RBACConfigurationError): 35 | rbac.lock() 36 | 37 | another_john.revoke(rbac.get_role_by_name('jr_editor')) 38 | 39 | assert rbac.lock() is None 40 | 41 | rbac.unlock() 42 | 43 | jack = rbac.create_subject(3) 44 | jack.authorize(editor) 45 | brad = rbac.create_subject(4) 46 | brad.authorize(chief) 47 | 48 | rbac.lock() 49 | 50 | some_article = Article(28372, 1) 51 | 52 | assert rbac.go(1, some_article, delete) is None 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # py-rbac spesific 2 | Pipfile.lock 3 | 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 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # py-rbac 2 | Python implementation of the NIST model for role based access control (RBAC). 3 | 4 | ![PyPI](https://img.shields.io/pypi/v/py-rbac) 5 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/py-rbac) 6 | 7 | [The NIST model][95961bd8] proposes four level of role based access control implementation: 8 | 1. **Flat** 9 | - users acquire permissions through roles 10 | - must support many-to-many user-role assignment 11 | - must support many-to-many permission-role assignment 12 | - must support user-role assignment review 13 | - users can use permissions of multiple roles simultaneously 14 | 2. **Hierarchical** 15 | - Flat + 16 | - must support role hierarchy (partial order) 17 | - arbitrary hierarchies 18 | - limited hierarchies 19 | 3. **Constrained** 20 | - Hierarchical + 21 | - must enforce separation of duties (SOD) 22 | - arbitrary hierarchies 23 | - limited hierarchies 24 | 4. **Symmetric** 25 | - Constrained + 26 | - must support permission-role review with performance effectively comparable to user-role review 27 | - arbitrary hierarchies 28 | - limited hierarchies 29 | 30 | This library supports Level 1, 2 and 3. 31 | 32 | ## Usage 33 | I've tried to explain the usage based on levels but the library is flexible enough to 34 | use any feature freely without thinking about levels. 35 | 36 | ### Install 37 | Through pip: 38 | ```sh 39 | pip install py-rbac 40 | ``` 41 | 42 | ### Flat Scenario 43 | This is the simplest scenario an mostly used I think. Let's configure it first: 44 | ```py 45 | from rbac import RBAC 46 | 47 | rbac = RBAC() 48 | 49 | # a role for junior editors 50 | jr_editor = rbac.create_role('jr_editor') 51 | 52 | # a domain or resource is also an object 53 | article = rbac.create_domain('article') 54 | 55 | # create permissions 56 | create = rbac.create_permission('c') 57 | read = rbac.create_permission('r') 58 | update = rbac.create_permission('u') 59 | delete = rbac.create_permission('d') 60 | 61 | # give junior a read permission for articles 62 | jr_editor.add_permission(read, article) 63 | 64 | # lets create a subject. a user or a third party client 65 | subject = rbac.create_subject('some_int_or_str') 66 | 67 | # our subject is new in the job 68 | subject.authorize(jr_editor) 69 | 70 | # lock rbac configuration 71 | # this validates the entire structure of our configuration 72 | # will sense more meaning as we use advanced features below 73 | rbac.lock() 74 | ``` 75 | After your application executed some code and is about respond client's request: 76 | ```py 77 | # check if the client is allowed to... 78 | rbac.go('some_int_or_str', article, create) 79 | # this will raise an exception since we didn't give a create permission to our junior 80 | # raised RBACAuthorizationError 81 | ``` 82 | 83 | ### Hierarchical Scenario 84 | In this example, there are hierarchical relationships between roles. Each role 85 | inherits its children roles and permissions. (Inheriting can be disabled but 86 | review this scenario first.) Configure and lock as always: 87 | ```py 88 | from rbac import RBAC 89 | 90 | rbac = RBAC() 91 | 92 | jr_editor = rbac.create_role('jr_editor') 93 | editor = rbac.create_role('editor', children=jr_editor) 94 | it = rbac.create_role('it', children=(jr_editor, editor)) 95 | 96 | article = rbac.create_domain('article') 97 | service_conf = rbac.create_domain('service_conf') 98 | 99 | create = rbac.create_permission('c') 100 | read = rbac.create_permission('r') 101 | update = rbac.create_permission('u') 102 | delete = rbac.create_permission('d') 103 | 104 | jr_editor.add_permission(read, article) 105 | editor.add_permission(create, article) 106 | it.add_permission((create, read, update, delete), service_conf) 107 | 108 | john = rbac.create_subject(1) 109 | john.authorize(jr_editor) 110 | jack = rbac.create_subject(2) 111 | jack.authorize(editor) 112 | mark = rbac.create_subject(3) 113 | mark.authorize(it) 114 | 115 | rbac.lock() 116 | ``` 117 | Run: 118 | ```py 119 | assert rbac.go(2, article, read) is None 120 | assert rbac.go(1, article, read) is None 121 | assert rbac.go(3, article, create) is None 122 | assert rbac.go(3, service_conf, create) is None 123 | 124 | # will raise 125 | try: 126 | rbac.go(1, article, create) 127 | except RBACAuthorizationError as e: 128 | raise 129 | ``` 130 | ### Constrained Scenario 131 | In this scenario, there are constraints. Constraints restricts the authorization 132 | flow as they are being applied through RBAC objects. Configure and lock: 133 | ```py 134 | from rbac import RBAC 135 | 136 | # an article from our application! we use this as our domain 137 | class Article(): 138 | """docstring for Article.""" 139 | 140 | def __init__(self, id, author): 141 | self.id = id 142 | self.author = author 143 | 144 | rbac = RBAC() 145 | 146 | # we can only allow one person to be assigned to this role 147 | jr_editor = rbac.create_role('jr_editor', max_subjects=1) 148 | # we may have editors up to 10 149 | editor = rbac.create_role('editor', children=jr_editor, max_subjects=10) 150 | # a chief role for one person but it won't inherit its children permissions 151 | chief = rbac.create_role('chief', children=(jr_editor, editor), inherit=False, max_subjects=1) 152 | 153 | # we use Article object as input to our RBACDomain but why an object? 154 | # specifically for the match_domain_prop constraint. 155 | article = rbac.create_domain(Article) 156 | 157 | # as usual 158 | create = rbac.create_permission('c') 159 | read = rbac.create_permission('r') 160 | update = rbac.create_permission('u') 161 | delete = rbac.create_permission('d') 162 | 163 | # our junior can read and create articles... 164 | jr_editor.add_permission((create, read), article) 165 | # ... but can only remove the ones which he/she wrote 166 | # match_domain_prop constraint indicates that the article instance property 167 | # "author" should match with the at-then id of the subject. 168 | jr_editor.add_permission(delete, article, match_domain_prop='author') 169 | editor.add_permission(create, article) 170 | 171 | # defining 2 jrs... hmmm 172 | john = rbac.create_subject(1) 173 | john.authorize(jr_editor) 174 | another_john = rbac.create_subject(2) 175 | another_john.authorize(jr_editor) 176 | 177 | # this will raise because our jr role can have 1 jrs max. 178 | try: 179 | rbac.lock() 180 | except RBACConfigurationError as e: 181 | raise 182 | 183 | # ok then, fire another_john! 184 | another_john.revoke(rbac.get_role_by_name('jr_editor')) 185 | 186 | # now locked. 187 | assert rbac.lock() is None 188 | 189 | # or unblock and add 2 more subject: 190 | rbac.unlock() 191 | jack = rbac.create_subject(3) 192 | jack.authorize(editor) 193 | brad = rbac.create_subject(4) 194 | brad.authorize(chief) 195 | 196 | rbac.lock() 197 | ``` 198 | Our API received a request about deleting some article: 199 | ```py 200 | some_article = Article(28372, 1) 201 | # our junior john trying to delete an article 202 | # the library will match the john's id which is 1 with the article's author and 203 | # allow the operation if they match. 204 | assert rbac.go(1, some_article, delete) is None 205 | ``` 206 | 207 | ## Versioning 208 | This library uses calendar versioning. 209 | 210 | [95961bd8]: https://csrc.nist.gov/CSRC/media/Publications/conference-paper/2000/07/26/the-nist-model-for-role-based-access-control-towards-a-unified-/documents/sandhu-ferraiolo-kuhn-00.pdf "The NIST model for role based access control" 211 | 212 | Thanks for watching 🐬 213 | 214 | [![ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/F1F1RFO7) 215 | 216 | ## Contribution 217 | This project uses pipenv to manage its dependencies. The only dependency it has 218 | is the pytest package which is used in development. 219 | 220 | 1. Clone the repository. 221 | 2. Run `pipenv install` 222 | 3. Make updates. 223 | 4. Run `pytest` under `pipenv shell` 224 | 5. Run `git push origin master` and create a pull request. 225 | -------------------------------------------------------------------------------- /rbac/__init__.py: -------------------------------------------------------------------------------- 1 | class RBACError(Exception): 2 | """docstring for RBACError.""" 3 | pass 4 | 5 | class RBACConfigurationError(RBACError): 6 | """docstring for RBACConfigurationError.""" 7 | 8 | def __init__(self, msg): 9 | self.msg = msg 10 | 11 | class RBACAuthorizationError(RBACError): 12 | """docstring for RBACAuthorizationError.""" 13 | 14 | def __init__(self, msg): 15 | self.msg = msg 16 | 17 | class RBACDomain(): 18 | """docstring for RBACDomain.""" 19 | 20 | def __init__(self, value): 21 | self.value = value 22 | name = self.value.__class__.__name__ 23 | self.name = value if name == 'str' else value.__name__ if name == 'type' else name 24 | 25 | class RBACPermission(): 26 | """docstring for RBACPermission.""" 27 | 28 | def __init__(self, name): 29 | self.name = name 30 | 31 | class PermissionAggregate(): 32 | """docstring for PermissionAggregate.""" 33 | 34 | def __init__(self, permission: RBACPermission, domain: RBACDomain, match_domain_prop=None): 35 | self.permission = permission 36 | self.domain = domain 37 | self.match_domain_prop = match_domain_prop 38 | 39 | class RBACRole(): 40 | """docstring for RBACRole.""" 41 | 42 | properties = ( 43 | 'children', 44 | 'inherit', 45 | 'max_subjects', 46 | 'max_permissions' 47 | ) 48 | 49 | def __init__(self, name, d): 50 | for k in self.properties: 51 | setattr(self, k, None) 52 | 53 | for k, v in d.items(): 54 | if k in self.properties: 55 | if k == 'children' and v is not None: 56 | if type(v) is not list and type(v) is not tuple and not isinstance(v, RBACRole): 57 | raise RBACConfigurationError('Children can be an instance of RBACRole or a list of RBACRoles') 58 | children = [v] if isinstance(v, RBACRole) else v 59 | setattr(self, k, children) 60 | else: 61 | setattr(self, k, v) 62 | 63 | self.name = name 64 | self.permission_aggregates = [] 65 | 66 | def add_permission(self, permission: RBACPermission, domain: RBACDomain, match_domain_prop=None): 67 | if type(permission) is list or type(permission) is tuple: 68 | for p in permission: 69 | self.add_permission(p, domain, match_domain_prop) 70 | return 71 | 72 | if type(domain) is list or type(domain) is tuple: 73 | for d in domain: 74 | self.add_permission(permission, d, match_domain_prop) 75 | return 76 | 77 | if not isinstance(permission, RBACPermission): 78 | raise RBACConfigurationError('Invalid permission object.') 79 | 80 | aggregate = PermissionAggregate(permission, domain, match_domain_prop) 81 | 82 | self.permission_aggregates.append(aggregate) 83 | 84 | class RBACSubject(): 85 | """docstring for RBACSubject.""" 86 | 87 | def __init__(self, identifier, max_roles=None): 88 | self.identifier = identifier 89 | self.max_roles = max_roles 90 | self.authorizations = [] 91 | 92 | def authorize(self, role: RBACRole): 93 | if not isinstance(role, RBACRole): 94 | raise RBACConfigurationError('The argument "role" should be a RBACRole object.') 95 | 96 | self.authorizations.append(role) 97 | 98 | def revoke(self, role: RBACRole): 99 | authorizations = [] 100 | for r in self.authorizations: 101 | if r.name != role.name: 102 | authorizations.append(r) 103 | self.authorizations = authorizations 104 | 105 | class RBAC(): 106 | """docstring for RBAC.""" 107 | 108 | def __init__(self): 109 | self._roles = [] 110 | self._subjects = [] 111 | self._domains = [] 112 | self._permissions = [] 113 | self._state = 'OPEN' 114 | 115 | def create_role(self, name, **kwargs): 116 | if self.is_locked(): 117 | raise RBACConfigurationError('RBAC is locked.') 118 | 119 | r = RBACRole(name, kwargs) 120 | self._roles.append(r) 121 | return r 122 | 123 | def create_domain(self, domain: any): 124 | if self.is_locked(): 125 | raise RBACConfigurationError('RBAC is locked.') 126 | 127 | d = RBACDomain(domain) 128 | self._domains.append(d) 129 | return d 130 | 131 | def create_permission(self, name): 132 | if self.is_locked(): 133 | raise RBACConfigurationError('RBAC is locked.') 134 | 135 | p = RBACPermission(name) 136 | self._permissions.append(p) 137 | return p 138 | 139 | def create_subject(self, identifier, max_roles=None): 140 | if self.is_locked(): 141 | raise RBACConfigurationError('RBAC is locked.') 142 | 143 | s = RBACSubject(identifier, max_roles) 144 | self._subjects.append(s) 145 | return s 146 | 147 | def get_role_by_name(self, name): 148 | for r in self._roles: 149 | if r.name == name: 150 | return r 151 | 152 | def get_subject_by_id(self, id): 153 | for s in self._subjects: 154 | if s.identifier == id: 155 | return s 156 | 157 | def get_role_family(self, role, recursive_calls=0): 158 | if recursive_calls > 1000: 159 | raise RBACConfigurationError('Possible inifinte loop. Please check the hierarchy of the roles.') 160 | 161 | result = [] 162 | if role.children is not None and role.inherit is not False: 163 | for c in role.children: 164 | result = result + [c] + self.get_role_family(c, recursive_calls+1) 165 | 166 | return result 167 | 168 | def lock(self): 169 | self.validate() 170 | self._state = 'LOCKED' 171 | 172 | def is_locked(self): 173 | return True if self._state == 'LOCKED' else False 174 | 175 | def unlock(self): 176 | self._state = 'OPEN' 177 | 178 | def validate(self): 179 | # validate roles constraints 180 | for r in self._roles: 181 | if r.max_subjects is not None: 182 | count_subjects = 0 183 | for s in self._subjects: 184 | if r in s.authorizations: 185 | count_subjects += 1 186 | if count_subjects > r.max_subjects: 187 | raise RBACConfigurationError(f'The role "{r.name}" can not have more than {r.max_subjects} subjects.') 188 | 189 | if r.max_permissions is not None: 190 | if len(r.permission_aggregates) > r.max_permissions: 191 | raise RBACConfigurationError(f'The role "{r.name}" can not have more than {r.max_permissions} permissions.') 192 | 193 | # validate subject constraints 194 | for s in self._subjects: 195 | if s.max_roles is not None: 196 | count_roles = len(s.authorizations) 197 | if count_roles > s.max_roles: 198 | raise RBACConfigurationError(f'The subject "{s.identifier}" can not have more than {s.max_roles} roles.') 199 | 200 | def go(self, subject: RBACSubject, domain: RBACDomain, permission: RBACPermission): 201 | if self.is_locked() is False: 202 | raise RBACAuthorizationError('RBAC is not in the lock mode.') 203 | 204 | subject_id = subject if not isinstance(subject, RBACSubject) else subject.identifier 205 | verified_subject = None 206 | for s in self._subjects: 207 | if getattr(s, 'identifier') == subject_id: 208 | verified_subject = s 209 | if verified_subject is None: 210 | raise RBACAuthorizationError('Unrecognized subject.') 211 | 212 | domain_name = domain if not isinstance(domain, RBACDomain) else domain.name 213 | verified_domain = None 214 | for d in self._domains: 215 | if getattr(d, 'name') == domain_name: 216 | verified_domain = d 217 | elif type(d.value) == type and isinstance(domain, d.value): 218 | verified_domain = d 219 | if verified_domain is None: 220 | raise RBACAuthorizationError('Unrecognized domain.') 221 | 222 | permission_name = permission if not isinstance(permission, RBACPermission) else permission.name 223 | verified_permission = None 224 | for p in self._permissions: 225 | if getattr(p, 'name') == permission_name: 226 | verified_permission = p 227 | if verified_permission is None: 228 | raise RBACAuthorizationError('Unrecognized permission.') 229 | 230 | subject_roles = verified_subject.authorizations 231 | if len(subject_roles) == 0: 232 | raise RBACAuthorizationError('Subject has no role.') 233 | subject_roles_family = [] + subject_roles 234 | for r in subject_roles: 235 | subject_roles_family = subject_roles_family + self.get_role_family(r) 236 | subject_roles_family_filtered = [] 237 | for r in subject_roles_family: 238 | duplicate = False 239 | for rf in subject_roles_family_filtered: 240 | if rf.name == r.name: 241 | duplicate = True 242 | break 243 | if duplicate is False: 244 | subject_roles_family_filtered.append(r) 245 | 246 | match = False 247 | for r in subject_roles_family_filtered: 248 | permission_aggregates = r.permission_aggregates 249 | if len(permission_aggregates) == 0: 250 | continue 251 | 252 | for pa in permission_aggregates: 253 | if pa.domain.name == verified_domain.name and pa.permission.name == verified_permission.name: 254 | if pa.match_domain_prop is not None: 255 | if verified_subject.identifier == getattr(domain, pa.match_domain_prop): 256 | match = True 257 | else: 258 | match = False 259 | else: 260 | match = True 261 | break 262 | 263 | if match is True: 264 | break 265 | 266 | if match is False: 267 | raise RBACAuthorizationError('Not authorized.') 268 | --------------------------------------------------------------------------------