├── .gitignore ├── .travis.yml ├── CHANGES.md ├── MANIFEST.in ├── Makefile ├── README.md ├── README.rst ├── miracle ├── __init__.py └── acl.py ├── requirements-dev.txt ├── setup.cfg ├── setup.py ├── tests └── acl_test.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # ===[ APP ]=== # 2 | 3 | # ===[ PYTHON PACKAGE ]=== # 4 | /build/ 5 | /dist/ 6 | /MANIFEST 7 | /*.egg/ 8 | /*.egg-info/ 9 | 10 | # ===[ OTHER ]=== # 11 | 12 | # IDE Projects 13 | .idea 14 | .nbproject 15 | .project 16 | *.sublime-project 17 | 18 | # Temps 19 | *~ 20 | *.tmp 21 | *.bak 22 | *.swp 23 | *.kate-swp 24 | *.DS_Store 25 | Thumbs.db 26 | 27 | # Utils 28 | /.tox/ 29 | .sass-cache/ 30 | .coverage 31 | 32 | # Generated 33 | __pycache__ 34 | *.py[cod] 35 | *.pot 36 | *.mo 37 | 38 | # Runtime 39 | /*.log 40 | /*.pid 41 | 42 | # ===[ EXCLUDES ]=== # 43 | !.gitkeep 44 | !.htaccess 45 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: linux 2 | sudo: false 3 | language: python 4 | 5 | matrix: 6 | include: 7 | - python: 2.7 8 | env: TOXENV=py 9 | - python: 3.4 10 | env: TOXENV=py 11 | - python: 3.5 12 | env: TOXENV=py 13 | - python: 3.6 14 | env: TOXENV=py 15 | - python: 3.7-dev 16 | env: TOXENV=py 17 | - python: pypy 18 | env: TOXENV=py 19 | - python: pypy3 20 | env: TOXENV=py 21 | install: 22 | - pip install tox 23 | cache: 24 | - pip 25 | script: 26 | - tox 27 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | v0.0.3, 2013.01.08 5 | ------------------ 6 | 7 | New: 8 | 9 | * acl.revoke_all() 10 | 11 | v0.0.2, 2013.01.03 12 | ------------------ 13 | 14 | New: 15 | 16 | * acl.add_roles() to add multiple roles 17 | * acl.clear() method 18 | 19 | Fixed: 20 | 21 | * acl.del_*() does proper clean-up 22 | 23 | v0.0.1, 2013.12.31 24 | ------------------ 25 | 26 | Initial release 27 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.md *.rst 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | 3 | SHELL := /bin/bash 4 | 5 | # Package 6 | .PHONY: clean 7 | clean: 8 | @rm -rf build/ dist/ *.egg-info/ README.md README.rst 9 | #README.md: $(shell find * -type f -name '*.py' -o -name '*.j2') $(wildcard misc/_doc/**) 10 | # @python misc/_doc/README.py | j2 --format=json -o README.md misc/_doc/README.md.j2 11 | README.rst: README.md 12 | @pandoc -f markdown -t rst -o README.rst README.md 13 | 14 | .PHONY: build publish-test publish 15 | build: README.rst 16 | @./setup.py build sdist bdist_wheel 17 | publish-test: README.rst 18 | @twine upload --repository pypitest dist/* 19 | publish: README.rst 20 | @twine upload dist/* 21 | 22 | 23 | .PHONY: test test-tox test-docker test-docker-2.6 24 | test: 25 | @nosetests 26 | test-tox: 27 | @tox 28 | test-docker: 29 | @docker run --rm -it -v `pwd`:/src themattrix/tox 30 | test-docker-2.6: # temporary, since `themattrix/tox` has faulty 2.6 31 | @docker run --rm -it -v $(realpath .):/app mrupgrade/deadsnakes:2.6 bash -c 'cd /app && pip install -e . && pip install nose argparse && nosetests' 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/kolypto/py-miracle.png?branch=master)](https://travis-ci.org/kolypto/py-miracle) 2 | [![Pythons](https://img.shields.io/badge/python-2.7%20%7C%203.4%E2%80%933.7%20%7C%20pypy-blue.svg)](.travis.yml) 3 | 4 | Miracle 5 | ======= 6 | 7 | Miracle is an ACL for Python that was designed to be well-structuted, 8 | simple yet exhaustive. It uses *permissions* defined on *resources*, and *roles* are granted with the access to them. 9 | 10 | To be a universal tool, it does not include any special cases, 11 | does not force you to persist and does not insist on any formats or conventions. 12 | 13 | Maximum flexibility and total control. Enjoy! :) 14 | 15 | Highlights: 16 | 17 | * Inspired by [miracle](https://github.com/kolypto/nodejs-miracle/) for NodeJS ; 18 | * Simple core 19 | * No restrictions on authorization entities 20 | * Unit-tested 21 | 22 | 23 | 24 | 25 | 26 | 27 | Table of Contents 28 | ================= 29 | 30 | * Installation 31 | * Define The Structure 32 | * Acl 33 | * Create 34 | * add_role(role) 35 | * add_roles(roles) 36 | * add_resource(resource) 37 | * add_permission(resource, permission) 38 | * add(structure) 39 | * Remove 40 | * remove_role(role) 41 | * remove_resource(resource) 42 | * remove_permission(resource, permission) 43 | * clear() 44 | * Get 45 | * get_roles() 46 | * get_resources() 47 | * get_permissions(resource) 48 | * get() 49 | * Export and Import 50 | * Authorize 51 | * Grant Permissions 52 | * grant(role, resource, permission) 53 | * grants(grants) 54 | * revoke(role, resource, permission) 55 | * revoke_all(role[, resource]) 56 | * Check Permissions 57 | * check(role, resource, permission) 58 | * check_any(roles, resource, permission) 59 | * check_all(roles, resource, permission) 60 | * Show Grants 61 | * which_permissions(role, resource) 62 | * which_permissions_any(roles, resource) 63 | * which_permissions_all(roles, resource) 64 | * which(role) 65 | * which_any(roles) 66 | * which_all(roles) 67 | * show() 68 | 69 | 70 | 71 | 72 | 73 | 74 | Installation 75 | ============ 76 | 77 | With pip: 78 | 79 | ```bash 80 | $ pip install miracle-acl 81 | ``` 82 | 83 | Define The Structure 84 | ==================== 85 | 86 | Acl 87 | --- 88 | To start using miracle, instantiate the `Acl` object: 89 | 90 | ```python 91 | from miracle import Acl 92 | acl = Acl() 93 | ``` 94 | 95 | The `Acl` object keeps track of your *resources* and *permissions* defined on them, handles *grants* over *roles* and 96 | provides utilities to manage them. When configured, you can check the access against the defined state. 97 | 98 | Create 99 | ------ 100 | 101 | Methods from this section allow you to build the *structure*: list of roles, resources and permissions. 102 | 103 | It's not required that you have the structure defined before you start granting the access: the `grant()` method 104 | implicitly creates all resources and permissions that were not previously defined. 105 | 106 | Start with defining the *resources* and *permissions* on them, then you can grant a *role* with the access to some 107 | permissions on a resource. 108 | 109 | For roles, resources & permissions, any hashable objects will do. 110 | 111 | ### `add_role(role)` 112 | Define a role. 113 | 114 | * `role`: the role to define. 115 | 116 | The role will have no permissions granted, but will appear in `get_roles()`. 117 | 118 | ```python 119 | acl.add_role('admin') 120 | acl.get_roles() # -> {'admin'} 121 | ``` 122 | 123 | ### `add_roles(roles)` 124 | Define multiple roles 125 | 126 | * `roles`: An iterable of roles 127 | 128 | ```python 129 | acl.add_roles(['admin', 'root']) 130 | acl.get_roles() # -> {'admin', 'root'} 131 | ``` 132 | 133 | ### `add_resource(resource)` 134 | Define a resource. 135 | 136 | * `resources`: the resource to define. 137 | 138 | The resource will have no permissions defined but will appear in `get_resources()`. 139 | 140 | ```python 141 | acl.add_resource('blog') 142 | acl.get_resources() # -> {'blog'} 143 | ``` 144 | 145 | ### `add_permission(resource, permission)` 146 | Define a permission on a resource. 147 | 148 | * `resource`: the resource to define the permission on. 149 | Is created if was not previously defined. 150 | * `permission`: the permission to define. 151 | 152 | The defined permission is not granted to anyone, but will appear in `get_permissions(resource)`. 153 | 154 | ```python 155 | acl.add_permission('blog', 'post') 156 | acl.get_permissions('blog') # -> {'post'} 157 | ``` 158 | 159 | ### `add(structure)` 160 | Define the whole resource/permission structure with a single dict. 161 | 162 | * `structure`: a dict that maps resources to an iterable of permissions. 163 | 164 | ```python 165 | acl.add({ 166 | 'blog': ['post'], 167 | 'page': {'create', 'read', 'update', 'delete'}, 168 | }) 169 | ``` 170 | 171 | Remove 172 | ------ 173 | 174 | ### `remove_role(role)` 175 | Remove the role and its grants. 176 | 177 | * `role`: the role to remove. 178 | 179 | ```python 180 | acl.remove_role('admin') 181 | ``` 182 | 183 | ### `remove_resource(resource)` 184 | Remove the resource along with its grants and permissions. 185 | 186 | * `resource`: the resource to remove. 187 | 188 | ```python 189 | acl.remove_resource('blog') 190 | ``` 191 | 192 | ### `remove_permission(resource, permission)` 193 | Remove the permission from a resource. 194 | 195 | * `resource`: the resource to remove the permission from. 196 | * `permission`: the permission to remove. 197 | 198 | The resource is not implicitly removed: it remains with an empty set of permissions. 199 | 200 | ```python 201 | acl.remove_permission('blog', 'post') 202 | ``` 203 | 204 | ### `clear()` 205 | Remove all roles, resources, permissions and grants. 206 | 207 | Get 208 | --- 209 | 210 | ### `get_roles()` 211 | Get the set of defined roles. 212 | 213 | ```python 214 | acl.get_roles() # -> {'admin', 'anonymous', 'registered'} 215 | ``` 216 | 217 | ### `get_resources()` 218 | Get the set of defined resources, including those with empty permissions set. 219 | 220 | ```python 221 | acl.get_resources() # -> {'blog', 'page', 'article'} 222 | ``` 223 | 224 | ### `get_permissions(resource)` 225 | Get the set of permissions for a resource. 226 | 227 | * `resource`: the resource to get the permissions for. 228 | 229 | ```python 230 | acl.get_permissions('page') # -> {'create', 'read', 'update', 'delete'} 231 | ``` 232 | 233 | ### `get()` 234 | Get the *structure*: hash of all resources mapped to their permissions. 235 | 236 | Returns a dict: `{ resource: set(permission,...), ... }`. 237 | 238 | ```python 239 | acl.get() # -> { blog: {'post'}, page: {'create', ...} } 240 | ``` 241 | 242 | 243 | 244 | Export and Import 245 | ----------------- 246 | The `Acl` class is picklable: 247 | 248 | ```python 249 | acl = miracle.Acl() 250 | save = acl.__getstate__() 251 | 252 | #... 253 | 254 | acl = miracle.Acl() 255 | acl.__setstate__(save) 256 | ``` 257 | 258 | 259 | 260 | 261 | 262 | Authorize 263 | ========= 264 | 265 | Grant Permissions 266 | ----------------- 267 | 268 | ### `grant(role, resource, permission)` 269 | Grant a permission over resource to the specified role. 270 | 271 | * `role`: The role to grant the access to 272 | * `resource`: The resource to grant the access over 273 | * `permission`: The permission to grant with 274 | 275 | Roles, resources and permissions are implicitly created if missing. 276 | 277 | ```python 278 | acl.grant('admin', 'blog', 'delete') 279 | acl.grant('anonymous', 'page', 'view') 280 | ``` 281 | 282 | ### `grants(grants)` 283 | Add a structure of grants to the Acl. 284 | 285 | * `grants`: A hash in the following form: `{ role: { resource: set(permission) } }`. 286 | 287 | ```python 288 | acl.grants({ 289 | 'admin': { 290 | 'blog': ['post'], 291 | }, 292 | 'anonymous': { 293 | 'page': ['view'] 294 | } 295 | }) 296 | ``` 297 | 298 | ### `revoke(role, resource, permission)` 299 | Revoke a permission over a resource from the specified role. 300 | 301 | ```python 302 | acl.revoke('anonymous', 'page', 'view') 303 | acl.revoke('user', 'account', 'delete') 304 | ``` 305 | 306 | ### `revoke_all(role[, resource])` 307 | Revoke all permissions from the specified role for all resources. 308 | If the optional `resource` argument is provided - removes all permissions from the specified resource. 309 | 310 | ```python 311 | acl.revoke_all('anonymous', 'page') # revoke all permissions from a single resource 312 | acl.revoke_all('anonymous') # revoke permissions from all resources 313 | ``` 314 | 315 | 316 | 317 | Check Permissions 318 | ----------------- 319 | 320 | ### `check(role, resource, permission)` 321 | Test whether the given role has access to the resource with the specified permission. 322 | 323 | * `role`: The role to check 324 | * `resource`: The protected resource 325 | * `permission`: The required permission 326 | 327 | Returns a boolean. 328 | 329 | ```python 330 | acl.check('admin', 'blog') # True 331 | acl.check('anonymous', 'page', 'delete') # -> False 332 | ``` 333 | 334 | ### `check_any(roles, resource, permission)` 335 | Test whether *any* of the given roles have access to the resource with the specified permission. 336 | 337 | * `roles`: An iterable of roles. 338 | 339 | When no roles are provided, returns False. 340 | 341 | ### `check_all(roles, resource, permission)` 342 | Test whether *all* of the given roles have access to the resource with the specified permission. 343 | 344 | * `roles`: An iterable of roles. 345 | 346 | When no roles are provided, returns False. 347 | 348 | 349 | 350 | Show Grants 351 | ----------- 352 | 353 | ### which_permissions(role, resource) 354 | List permissions that the provided role has over the resource: 355 | 356 | ```python 357 | acl.which_permissions('admin', 'blog') # -> {'post'} 358 | ``` 359 | 360 | ### which_permissions_any(roles, resource) 361 | List permissions that any of the provided roles have over the resource: 362 | 363 | ```python 364 | acl.which_permissions_any(['anonymous', 'registered'], 'page') # -> {'view'} 365 | ``` 366 | 367 | ### which_permissions_all(roles, resource) 368 | List permissions that all of the provided roles have over the resource: 369 | 370 | ```python 371 | acl.which_permissions_all(['anonymous', 'registered'], 'page') # -> {'view'} 372 | ``` 373 | 374 | 375 | 376 | ### `which(role)` 377 | Collect grants that the provided role has: 378 | 379 | ```python 380 | acl.which('admin') # -> { blog: {'post'} } 381 | ``` 382 | 383 | ### `which_any(roles)` 384 | Collect grants that any of the provided roles have (union). 385 | 386 | ```python 387 | acl.which(['anonymous', 'registered']) # -> { page: ['view'] } 388 | ``` 389 | 390 | ### `which_all(roles)` 391 | Collect grants that all of the provided roles have (intersection): 392 | 393 | ```python 394 | acl.which(['anonymous', 'registered']) # -> { page: ['view'] } 395 | ``` 396 | 397 | 398 | 399 | ### `show()` 400 | Get all current grants. 401 | 402 | Returns a dict `{ role: { resource: set(permission) } }`. 403 | 404 | ```python 405 | acl.show() # -> { admin: { blog: ['post'] } } 406 | ``` 407 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | `Build Status `__ 2 | `Pythons <.travis.yml>`__ 3 | 4 | Miracle 5 | ======= 6 | 7 | Miracle is an ACL for Python that was designed to be well-structuted, 8 | simple yet exhaustive. It uses *permissions* defined on *resources*, and 9 | *roles* are granted with the access to them. 10 | 11 | To be a universal tool, it does not include any special cases, does not 12 | force you to persist and does not insist on any formats or conventions. 13 | 14 | Maximum flexibility and total control. Enjoy! :) 15 | 16 | Highlights: 17 | 18 | - Inspired by `miracle `__ 19 | for NodeJS ; 20 | - Simple core 21 | - No restrictions on authorization entities 22 | - Unit-tested 23 | 24 | Table of Contents 25 | ================= 26 | 27 | - Define The Structure 28 | 29 | - Acl 30 | - Create 31 | 32 | - add_role(role) 33 | - add_roles(roles) 34 | - add_resource(resource) 35 | - add_permission(resource, permission) 36 | - add(structure) 37 | 38 | - Remove 39 | 40 | - remove_role(role) 41 | - remove_resource(resource) 42 | - remove_permission(resource, permission) 43 | - clear() 44 | 45 | - Get 46 | 47 | - get_roles() 48 | - get_resources() 49 | - get_permissions(resource) 50 | - get() 51 | 52 | - Export and Import 53 | 54 | - Authorize 55 | 56 | - Grant Permissions 57 | 58 | - grant(role, resource, permission) 59 | - grants(grants) 60 | - revoke(role, resource, permission) 61 | - revoke_all(role[, resource]) 62 | 63 | - Check Permissions 64 | 65 | - check(role, resource, permission) 66 | - check_any(roles, resource, permission) 67 | - check_all(roles, resource, permission) 68 | 69 | - Show Grants 70 | 71 | - which_permissions(role, resource) 72 | - which_permissions_any(roles, resource) 73 | - which_permissions_all(roles, resource) 74 | - which(role) 75 | - which_any(roles) 76 | - which_all(roles) 77 | - show() 78 | 79 | Define The Structure 80 | ==================== 81 | 82 | Acl 83 | --- 84 | 85 | To start using miracle, instantiate the ``Acl`` object: 86 | 87 | .. code:: python 88 | 89 | from miracle import Acl 90 | acl = Acl() 91 | 92 | The ``Acl`` object keeps track of your *resources* and *permissions* 93 | defined on them, handles *grants* over *roles* and provides utilities to 94 | manage them. When configured, you can check the access against the 95 | defined state. 96 | 97 | Create 98 | ------ 99 | 100 | Methods from this section allow you to build the *structure*: list of 101 | roles, resources and permissions. 102 | 103 | It’s not required that you have the structure defined before you start 104 | granting the access: the ``grant()`` method implicitly creates all 105 | resources and permissions that were not previously defined. 106 | 107 | Start with defining the *resources* and *permissions* on them, then you 108 | can grant a *role* with the access to some permissions on a resource. 109 | 110 | For roles, resources & permissions, any hashable objects will do. 111 | 112 | ``add_role(role)`` 113 | ~~~~~~~~~~~~~~~~~~ 114 | 115 | Define a role. 116 | 117 | - ``role``: the role to define. 118 | 119 | The role will have no permissions granted, but will appear in 120 | ``get_roles()``. 121 | 122 | .. code:: python 123 | 124 | acl.add_role('admin') 125 | acl.get_roles() # -> {'admin'} 126 | 127 | ``add_roles(roles)`` 128 | ~~~~~~~~~~~~~~~~~~~~ 129 | 130 | Define multiple roles 131 | 132 | - ``roles``: An iterable of roles 133 | 134 | .. code:: python 135 | 136 | acl.add_roles(['admin', 'root']) 137 | acl.get_roles() # -> {'admin', 'root'} 138 | 139 | ``add_resource(resource)`` 140 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 141 | 142 | Define a resource. 143 | 144 | - ``resources``: the resource to define. 145 | 146 | The resource will have no permissions defined but will appear in 147 | ``get_resources()``. 148 | 149 | .. code:: python 150 | 151 | acl.add_resource('blog') 152 | acl.get_resources() # -> {'blog'} 153 | 154 | ``add_permission(resource, permission)`` 155 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 156 | 157 | Define a permission on a resource. 158 | 159 | - ``resource``: the resource to define the permission on. Is created if 160 | was not previously defined. 161 | - ``permission``: the permission to define. 162 | 163 | The defined permission is not granted to anyone, but will appear in 164 | ``get_permissions(resource)``. 165 | 166 | .. code:: python 167 | 168 | acl.add_permission('blog', 'post') 169 | acl.get_permissions('blog') # -> {'post'} 170 | 171 | ``add(structure)`` 172 | ~~~~~~~~~~~~~~~~~~ 173 | 174 | Define the whole resource/permission structure with a single dict. 175 | 176 | - ``structure``: a dict that maps resources to an iterable of 177 | permissions. 178 | 179 | .. code:: python 180 | 181 | acl.add({ 182 | 'blog': ['post'], 183 | 'page': {'create', 'read', 'update', 'delete'}, 184 | }) 185 | 186 | Remove 187 | ------ 188 | 189 | ``remove_role(role)`` 190 | ~~~~~~~~~~~~~~~~~~~~~ 191 | 192 | Remove the role and its grants. 193 | 194 | - ``role``: the role to remove. 195 | 196 | .. code:: python 197 | 198 | acl.remove_role('admin') 199 | 200 | ``remove_resource(resource)`` 201 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 202 | 203 | Remove the resource along with its grants and permissions. 204 | 205 | - ``resource``: the resource to remove. 206 | 207 | .. code:: python 208 | 209 | acl.remove_resource('blog') 210 | 211 | ``remove_permission(resource, permission)`` 212 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 213 | 214 | Remove the permission from a resource. 215 | 216 | - ``resource``: the resource to remove the permission from. 217 | - ``permission``: the permission to remove. 218 | 219 | The resource is not implicitly removed: it remains with an empty set of 220 | permissions. 221 | 222 | .. code:: python 223 | 224 | acl.remove_permission('blog', 'post') 225 | 226 | ``clear()`` 227 | ~~~~~~~~~~~ 228 | 229 | Remove all roles, resources, permissions and grants. 230 | 231 | Get 232 | --- 233 | 234 | ``get_roles()`` 235 | ~~~~~~~~~~~~~~~ 236 | 237 | Get the set of defined roles. 238 | 239 | .. code:: python 240 | 241 | acl.get_roles() # -> {'admin', 'anonymous', 'registered'} 242 | 243 | ``get_resources()`` 244 | ~~~~~~~~~~~~~~~~~~~ 245 | 246 | Get the set of defined resources, including those with empty permissions 247 | set. 248 | 249 | .. code:: python 250 | 251 | acl.get_resources() # -> {'blog', 'page', 'article'} 252 | 253 | ``get_permissions(resource)`` 254 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 255 | 256 | Get the set of permissions for a resource. 257 | 258 | - ``resource``: the resource to get the permissions for. 259 | 260 | .. code:: python 261 | 262 | acl.get_permissions('page') # -> {'create', 'read', 'update', 'delete'} 263 | 264 | .. _get-1: 265 | 266 | ``get()`` 267 | ~~~~~~~~~ 268 | 269 | Get the *structure*: hash of all resources mapped to their permissions. 270 | 271 | Returns a dict: ``{ resource: set(permission,...), ... }``. 272 | 273 | .. code:: python 274 | 275 | acl.get() # -> { blog: {'post'}, page: {'create', ...} } 276 | 277 | Export and Import 278 | ----------------- 279 | 280 | The ``Acl`` class is picklable: 281 | 282 | .. code:: python 283 | 284 | acl = miracle.Acl() 285 | save = acl.__getstate__() 286 | 287 | #... 288 | 289 | acl = miracle.Acl() 290 | acl.__setstate__(save) 291 | 292 | Authorize 293 | ========= 294 | 295 | Grant Permissions 296 | ----------------- 297 | 298 | ``grant(role, resource, permission)`` 299 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 300 | 301 | Grant a permission over resource to the specified role. 302 | 303 | - ``role``: The role to grant the access to 304 | - ``resource``: The resource to grant the access over 305 | - ``permission``: The permission to grant with 306 | 307 | Roles, resources and permissions are implicitly created if missing. 308 | 309 | .. code:: python 310 | 311 | acl.grant('admin', 'blog', 'delete') 312 | acl.grant('anonymous', 'page', 'view') 313 | 314 | ``grants(grants)`` 315 | ~~~~~~~~~~~~~~~~~~ 316 | 317 | Add a structure of grants to the Acl. 318 | 319 | - ``grants``: A hash in the following form: 320 | ``{ role: { resource: set(permission) } }``. 321 | 322 | .. code:: python 323 | 324 | acl.grants({ 325 | 'admin': { 326 | 'blog': ['post'], 327 | }, 328 | 'anonymous': { 329 | 'page': ['view'] 330 | } 331 | }) 332 | 333 | ``revoke(role, resource, permission)`` 334 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 335 | 336 | Revoke a permission over a resource from the specified role. 337 | 338 | .. code:: python 339 | 340 | acl.revoke('anonymous', 'page', 'view') 341 | acl.revoke('user', 'account', 'delete') 342 | 343 | ``revoke_all(role[, resource])`` 344 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 345 | 346 | Revoke all permissions from the specified role for all resources. If the 347 | optional ``resource`` argument is provided - removes all permissions 348 | from the specified resource. 349 | 350 | .. code:: python 351 | 352 | acl.revoke_all('anonymous', 'page') # revoke all permissions from a single resource 353 | acl.revoke_all('anonymous') # revoke permissions from all resources 354 | 355 | Check Permissions 356 | ----------------- 357 | 358 | ``check(role, resource, permission)`` 359 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 360 | 361 | Test whether the given role has access to the resource with the 362 | specified permission. 363 | 364 | - ``role``: The role to check 365 | - ``resource``: The protected resource 366 | - ``permission``: The required permission 367 | 368 | Returns a boolean. 369 | 370 | .. code:: python 371 | 372 | acl.check('admin', 'blog') # True 373 | acl.check('anonymous', 'page', 'delete') # -> False 374 | 375 | ``check_any(roles, resource, permission)`` 376 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 377 | 378 | Test whether *any* of the given roles have access to the resource with 379 | the specified permission. 380 | 381 | - ``roles``: An iterable of roles. 382 | 383 | When no roles are provided, returns False. 384 | 385 | ``check_all(roles, resource, permission)`` 386 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 387 | 388 | Test whether *all* of the given roles have access to the resource with 389 | the specified permission. 390 | 391 | - ``roles``: An iterable of roles. 392 | 393 | When no roles are provided, returns False. 394 | 395 | Show Grants 396 | ----------- 397 | 398 | which_permissions(role, resource) 399 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 400 | 401 | List permissions that the provided role has over the resource: 402 | 403 | .. code:: python 404 | 405 | acl.which_permissions('admin', 'blog') # -> {'post'} 406 | 407 | which_permissions_any(roles, resource) 408 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 409 | 410 | List permissions that any of the provided roles have over the resource: 411 | 412 | .. code:: python 413 | 414 | acl.which_permissions_any(['anonymous', 'registered'], 'page') # -> {'view'} 415 | 416 | which_permissions_all(roles, resource) 417 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 418 | 419 | List permissions that all of the provided roles have over the resource: 420 | 421 | .. code:: python 422 | 423 | acl.which_permissions_all(['anonymous', 'registered'], 'page') # -> {'view'} 424 | 425 | ``which(role)`` 426 | ~~~~~~~~~~~~~~~ 427 | 428 | Collect grants that the provided role has: 429 | 430 | .. code:: python 431 | 432 | acl.which('admin') # -> { blog: {'post'} } 433 | 434 | ``which_any(roles)`` 435 | ~~~~~~~~~~~~~~~~~~~~ 436 | 437 | Collect grants that any of the provided roles have (union). 438 | 439 | .. code:: python 440 | 441 | acl.which(['anonymous', 'registered']) # -> { page: ['view'] } 442 | 443 | ``which_all(roles)`` 444 | ~~~~~~~~~~~~~~~~~~~~ 445 | 446 | Collect grants that all of the provided roles have (intersection): 447 | 448 | .. code:: python 449 | 450 | acl.which(['anonymous', 'registered']) # -> { page: ['view'] } 451 | 452 | ``show()`` 453 | ~~~~~~~~~~ 454 | 455 | Get all current grants. 456 | 457 | Returns a dict ``{ role: { resource: set(permission) } }``. 458 | 459 | .. code:: python 460 | 461 | acl.show() # -> { admin: { blog: ['post'] } } 462 | -------------------------------------------------------------------------------- /miracle/__init__.py: -------------------------------------------------------------------------------- 1 | from .acl import Acl 2 | -------------------------------------------------------------------------------- /miracle/acl.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | 4 | class Acl(object): 5 | def __init__(self): 6 | #: Set of defined roles 7 | self._roles = set() 8 | 9 | #: Resources & Permissions: { resource: set(permission) } 10 | self._structure = defaultdict(set) 11 | 12 | #: Grants: set( (role, resource, permission) ) 13 | self._grants = set() 14 | 15 | #region Add 16 | 17 | def add_role(self, role): 18 | """ Define a role. 19 | 20 | Existing roles are not overwritten nor duplicated. 21 | 22 | :param role: Role to define. 23 | Any hashable object will do. 24 | :type role: str 25 | :rtype: Acl 26 | """ 27 | self._roles.add(role) 28 | return self 29 | 30 | def add_roles(self, roles): 31 | """ Define multiple roles 32 | 33 | Existing roles are not overwritten nor duplicated. 34 | 35 | :param roles: The roles to define 36 | :type roles: list(str) 37 | :rtype: Acl 38 | """ 39 | self._roles.update(set(roles)) 40 | return self 41 | 42 | def add_resource(self, resource): 43 | """ Define a resource. 44 | 45 | Existing resources are not overwritten nor duplicated 46 | 47 | :param resource: Resource to define. 48 | For now, it will have an empty set of permissions 49 | :type resource: str 50 | :rtype: Acl 51 | """ 52 | if resource not in self._structure: 53 | self._structure[resource] = set() 54 | return self 55 | 56 | def add_permission(self, resource, permission): 57 | """ Define permission on a resource 58 | 59 | The resource is created if missing. 60 | Existing permissions are not overwritten nor duplicated 61 | 62 | :param resource: Resource to define the permission on. 63 | :type resource: str 64 | :param permission: Permission to define. 65 | :type permission: str 66 | :rtype: Acl 67 | """ 68 | self._structure[resource].add(permission) 69 | return self 70 | 71 | def add(self, structure): 72 | """ Define the whole structure of resources and permissions 73 | 74 | :param structure: A dict {resource: [permissions]} 75 | :type structure: dict(list(str)) 76 | :rtype: Acl 77 | """ 78 | for resource, permissions in structure.items(): 79 | for permission in permissions: 80 | self._structure[resource].add(permission) 81 | return self 82 | 83 | #endregion 84 | 85 | #region Delete 86 | 87 | def clear(self): 88 | """ Clear the Acl completely 89 | 90 | This removes all roles, resources, permissions ans grants 91 | 92 | :rtype: Acl 93 | """ 94 | self._roles.clear() 95 | self._structure.clear() 96 | self._grants.clear() 97 | return self 98 | 99 | def del_role(self, role): 100 | """ Remove a role. The grants remain. 101 | 102 | Undefined roles are silently ignored 103 | 104 | :param role: Role to remove 105 | :type role: str 106 | :rtype: Acl 107 | """ 108 | self._roles.discard(role) 109 | self._grants = set([x for x in self._grants if x[0] != role]) 110 | return self 111 | 112 | def del_resource(self, resource): 113 | """ Remove a resource and its permissions. The grants remain. 114 | 115 | Undefined resources are silently ignored 116 | 117 | :param resource: Resource to remove 118 | :type resource: str 119 | :rtype: Acl 120 | """ 121 | if resource in self._structure: 122 | del self._structure[resource] 123 | self._grants = set([x for x in self._grants if x[1] != resource]) 124 | return self 125 | 126 | def del_permission(self, resource, permission): 127 | """ Remove a permission from the resource. The grants remain. 128 | 129 | Undefined resources and permissions are silently ignored 130 | 131 | :param resource: The resource to remove the permission from 132 | :type resource: str 133 | :param permission: Permission to remove 134 | :type permission: str 135 | :rtype: Acl 136 | """ 137 | if resource in self._structure: 138 | self._structure[resource].discard(permission) 139 | self._grants = set([x for x in self._grants if x[2] != permission]) 140 | return self 141 | 142 | #endregion 143 | 144 | #region Get 145 | 146 | def get_roles(self): 147 | """ Get the set of roles. 148 | 149 | :rtype: set(str) 150 | """ 151 | return set(self._roles) 152 | 153 | def get_resources(self): 154 | """ Get the set of resources 155 | 156 | :rtype: set(str) 157 | """ 158 | return set(self._structure.keys()) 159 | 160 | def get_permissions(self, resource): 161 | """ Get the set of permissions on a resource 162 | 163 | :param resource: The resource to list the permissions for 164 | :type resource: str 165 | :rtype: set(str) 166 | """ 167 | if resource not in self._structure: 168 | return set() 169 | return set(self._structure[resource]) 170 | 171 | def get(self): 172 | """ Get the whole structure of resources and permissions 173 | 174 | Returns { resource: set(permission) } 175 | 176 | :rtype: dict(set(str)) 177 | """ 178 | ret = {} 179 | for resource, permissions in self._structure.items(): 180 | ret[resource] = set(permissions) 181 | return ret 182 | 183 | #endregion 184 | 185 | #region Grant Permissions 186 | 187 | def grant(self, role, resource, permission): 188 | """ Grant a permission over resource to the role. 189 | 190 | Missing entities are added to the structure 191 | 192 | :param role: The role to grant the access to 193 | :type role: str 194 | :param resource: The resource to grant the access over 195 | :type resource: str 196 | :param permission: The permission to grant with 197 | :type permission: str 198 | :rtype: Acl 199 | """ 200 | self.add_role(role) 201 | self.add_resource(resource) 202 | self.add_permission(resource, permission) 203 | self._grants.add((role, resource, permission)) 204 | return self 205 | 206 | def grants(self, grants): 207 | """ Add a structure of grants to the Acl 208 | 209 | Input: { role: { resource: set(permissions) } } 210 | 211 | :param grants: Grants structure to add 212 | :type grants: dict(dict(set(str))) 213 | :rtype: Acl 214 | """ 215 | for role, gs in grants.items(): 216 | self.add_role(role) 217 | for resource, permissions in gs.items(): 218 | self.add_resource(resource) 219 | for permission in permissions: 220 | self.add_permission(resource, permission) 221 | self._grants.add((role, resource, permission)) 222 | return self 223 | 224 | def revoke(self, role, resource, permission): 225 | """ Revoke a permission over a resource from the specified role. 226 | 227 | :param role: The role to modify 228 | :type role: str 229 | :param resource: The resource to modify 230 | :type resource: str 231 | :param permission: The permission to revoke 232 | :type permission: str 233 | :rtype: Acl 234 | """ 235 | self._grants.discard((role, resource, permission)) 236 | return self 237 | 238 | def revoke_all(self, role, resource=None): 239 | """ Revoke all permissions from the specified role [over the specified resource] 240 | 241 | :param role: The role to revoke all permissions from 242 | :type role: str 243 | :param resource: The resource to revoke the permissions from. Optional: revokes from all resources 244 | :type resource: str 245 | :rtype: Acl 246 | """ 247 | self._grants = { g for g in self._grants if not (g[0] == role and (resource is None or g[1] == resource)) } 248 | return self 249 | 250 | #endregion 251 | 252 | #region Check 253 | 254 | def check(self, role, resource, permission): 255 | """ Test whether the given role has access to the resource with the specified permission. 256 | 257 | :param role: The role to check the access for 258 | :type role: str 259 | :param resource: The resource to check the access for 260 | :type resource: str 261 | :param permission: The permission to check the access with 262 | :type permission: str 263 | :rtype: bool 264 | """ 265 | return (role, resource, permission) in self._grants 266 | 267 | def check_any(self, roles, resource, permission): 268 | """ Test whether ANY of the given roles have access to the resource with the specified permission. 269 | 270 | :param roles: Roles collection to check the access for 271 | :type roles: list(str) 272 | :param resource: The resource to check the access for 273 | :type resource: str 274 | :param permission: The permission to check the access with 275 | :type permission: str 276 | :rtype: bool 277 | """ 278 | # No roles 279 | if not roles: 280 | return False 281 | 282 | # Any 283 | return any((role, resource, permission) in self._grants for role in roles) 284 | 285 | def check_all(self, roles, resource, permission): 286 | """ Test whether ALL of the given roles have access to the resource with the specified permission. 287 | 288 | :param roles: Roles collection to check the access for 289 | :type roles: list(str) 290 | :param resource: The resource to check the access for 291 | :type resource: str 292 | :param permission: The permission to check the access with 293 | :type permission: str 294 | :rtype: bool 295 | """ 296 | # No roles 297 | if not roles: 298 | return False 299 | 300 | # all 301 | return all((role, resource, permission) in self._grants for role in roles) 302 | 303 | #endregion 304 | 305 | #region Show Grants 306 | 307 | def which_permissions(self, role, resource): 308 | """ List permissions that the provided role has over the resource 309 | 310 | :param role: The role to list permissions for 311 | :type role: str 312 | :rtype: set(str) 313 | """ 314 | return {permission for r, res, permission in self._grants if r == role and res == resource} 315 | 316 | def which_permissions_any(self, roles, resource): 317 | """ List permissions that any of the provided roles have over the resource 318 | 319 | :param roles: Roles to list permissions for 320 | :type roles: list(str) 321 | :rtype: set(str) 322 | """ 323 | if not roles: 324 | return {} 325 | 326 | # Collect permissions per role 327 | roles = set(roles) 328 | return {permission for r, res, permission in self._grants if r in roles and res == resource} 329 | 330 | def which_permissions_all(self, roles, resource): 331 | """ List permissions that all of the provided roles have over the resource 332 | 333 | :param roles: Roles to list permissions for 334 | :type roles: list(str) 335 | :rtype: set(str) 336 | """ 337 | if not roles: 338 | return {} 339 | roles = set(roles) 340 | 341 | # Collect permissions per role 342 | ppr = { 343 | role: set( 344 | permission 345 | for r, res, permission in self._grants 346 | if r == role and res == resource) 347 | for role in roles 348 | } 349 | 350 | # Intersect them 351 | return set.intersection(*ppr.values()) 352 | 353 | def which(self, role): 354 | """ Collect grants that the provided role has 355 | 356 | Returns: { resource: set(permission) } 357 | 358 | :param role: The role to show the grants for 359 | :type role: str 360 | :rtype: dict(set(str)) 361 | """ 362 | ret = defaultdict(set) 363 | for (r, resource, permission) in self._grants: 364 | if r == role: 365 | ret[resource].add(permission) 366 | return dict(ret) 367 | 368 | def which_any(self, roles): 369 | """ Collect grants that ANY of the provided roles have 370 | 371 | Returns: { resource: set(permission) } 372 | 373 | :param roles: The roles to show the grants for 374 | :type roles: list(str) 375 | :rtype: dict(set(str)) 376 | """ 377 | # No roles 378 | roles = set(roles) 379 | if not roles: 380 | return {} 381 | 382 | # Union 383 | ret = defaultdict(set) 384 | for (r, resource, permission) in self._grants: 385 | if r in roles: 386 | ret[resource].add(permission) 387 | return dict(ret) 388 | 389 | def which_all(self, roles): 390 | """ Collect grants that ALL of the provided roles have 391 | 392 | Returns: { resource: set(permission) } 393 | 394 | :param roles: The roles to show the grants for 395 | :type roles: list(str) 396 | :rtype: dict(set(str)) 397 | """ 398 | # No roles 399 | roles = set(roles) 400 | if not roles: 401 | return {} 402 | 403 | # Collect grants for each role 404 | grants = {role: self.which(role) for role in roles} 405 | 406 | # Start with the first one 407 | if not grants: 408 | return dict() 409 | ret = grants.popitem()[1] 410 | 411 | # Intersect them 412 | for role, gs in grants.items(): 413 | for resource in list(ret.keys()): 414 | if resource in gs: 415 | ret[resource] &= gs[resource] 416 | else: 417 | del ret[resource] 418 | 419 | # Finish 420 | return dict(ret) 421 | 422 | def show(self): 423 | """ Show all current grants 424 | 425 | Returns: { role: { resource: set(permission) } } 426 | 427 | :rtype: dict(dict(set(str)) 428 | """ 429 | ret = defaultdict(lambda: defaultdict(set)) 430 | for (role, resource, permission) in self._grants: 431 | ret[role][resource].add(permission) 432 | return dict(ret) 433 | 434 | #endregion 435 | 436 | #region Export & Import 437 | 438 | def __getstate__(self): 439 | return { 440 | 'roles': self.get_roles(), 441 | 'struct': self.get(), 442 | 'grants': self.show() 443 | } 444 | 445 | def __setstate__(self, state): 446 | self.add_roles(state['roles']) 447 | self.add(state['struct']) 448 | self.grants(state['grants']) 449 | return self 450 | 451 | #endregion 452 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | wheel 2 | nose 3 | j2cli 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | """ Flexible role-based authorization solution that is a pleasure to use """ 3 | 4 | from setuptools import setup, find_packages 5 | 6 | setup( 7 | # http://pythonhosted.org/setuptools/setuptools.html 8 | name='miracle-acl', 9 | version='0.0.4-1', 10 | author='Mark Vartanyan', 11 | author_email='kolypto@gmail.com', 12 | 13 | url='https://github.com/kolypto/py-miracle', 14 | license='MIT', 15 | description=__doc__, 16 | long_description=open('README.md').read(), 17 | long_description_content_type='text/markdown', 18 | keywords=['acl', 'rbac', 'authorization'], 19 | 20 | packages=find_packages(), 21 | scripts=[], 22 | entry_points={}, 23 | 24 | install_requires=[ 25 | ], 26 | extras_require={}, 27 | include_package_data=True, 28 | test_suite='nose.collector', 29 | 30 | platforms='any', 31 | classifiers=[ 32 | # https://pypi.python.org/pypi?%3Aaction=list_classifiers 33 | 'Development Status :: 5 - Production/Stable', 34 | 'Intended Audience :: Developers', 35 | 'Natural Language :: English', 36 | 'Programming Language :: Python :: 2', 37 | 'Programming Language :: Python :: 3', 38 | 'Operating System :: OS Independent' 39 | ], 40 | ) 41 | -------------------------------------------------------------------------------- /tests/acl_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import miracle 3 | 4 | 5 | class TestAclStructure(unittest.TestCase): 6 | def test_roles(self): 7 | """ add_role(), add_roles(), list_roles(), del_role() """ 8 | acl = miracle.Acl() 9 | 10 | # Add roles 11 | acl.add_role('root') 12 | acl.add_role('superadmin') 13 | acl.add_role('superadmin') # does not replace or fail 14 | acl.add_role('user') 15 | acl.add_role('poweruser') 16 | acl.add_roles(['n00b', 'poweruser']) 17 | 18 | # Test roles 19 | self.assertSetEqual(acl.get_roles(), {'root','superadmin','user','poweruser','n00b'}) 20 | 21 | # Del roles 22 | acl.del_role('poweruser') 23 | acl.del_role('poweruser') # does not fail 24 | acl.del_role('n00b') 25 | 26 | # Test roles 27 | self.assertSetEqual(acl.get_roles(), {'root','superadmin','user'}) 28 | 29 | def test_resources(self): 30 | """ add_resource(), list_resource(), del_resource() """ 31 | acl = miracle.Acl() 32 | 33 | # Add resources 34 | acl.add_resource('user') 35 | acl.add_resource('page') 36 | acl.add_resource('page') # does not replace or fail 37 | acl.add_resource('news') 38 | acl.add_resource('blog') 39 | 40 | # Test resources 41 | self.assertSetEqual(acl.get_resources(), {'user', 'page', 'news', 'blog'}) 42 | 43 | # Delete resources 44 | acl.del_resource('news') 45 | acl.del_resource('news') # does not fail 46 | acl.del_resource('blog') 47 | 48 | # Test resources 49 | self.assertSetEqual(acl.get_resources(), {'user', 'page'}) 50 | 51 | def test_permissions(self): 52 | """ add_permission(), list_permissions(), del_permission() """ 53 | acl = miracle.Acl() 54 | 55 | # Add permissions 56 | acl.add_permission('user', 'create') # silently creates a resource 57 | acl.add_permission('user', 'create') # does not replace or fail 58 | acl.add_permission('user', 'read') 59 | acl.add_permission('user', 'write') 60 | acl.add_permission('post', 'read') 61 | acl.add_permission('post', 'create') 62 | acl.add_permission('log', 'delete') 63 | 64 | # Test resources 65 | self.assertSetEqual(acl.get_resources(), {'user', 'post', 'log'}) 66 | 67 | # Test permissions on resources 68 | self.assertSetEqual(acl.get_permissions('404'), set()) # empty ok 69 | self.assertSetEqual(acl.get_permissions('user'), {'create','read','write'}) 70 | self.assertSetEqual(acl.get_permissions('post'), {'read', 'create'}) 71 | self.assertSetEqual(acl.get_permissions('log'), {'delete'}) 72 | 73 | # Del permissions 74 | acl.del_permission('user', 'write') 75 | acl.del_permission('post', 'create') 76 | acl.del_permission('post', 'create') # does not fail 77 | 78 | # Test resources 79 | self.assertSetEqual(acl.get_resources(), {'user', 'post', 'log'}) 80 | 81 | # Test permissions on resources 82 | self.assertSetEqual(acl.get_permissions('404'), set()) 83 | self.assertSetEqual(acl.get_permissions('user'), {'create', 'read'}) 84 | self.assertSetEqual(acl.get_permissions('post'), {'read'}) 85 | self.assertSetEqual(acl.get_permissions('log'), {'delete'}) 86 | 87 | def test_structure(self): 88 | """ add(), list() """ 89 | acl = miracle.Acl() 90 | 91 | # Add 92 | acl.add({ 93 | '/article': {'create','edit'}, 94 | '/profile': ['edit'], 95 | }) 96 | acl.add({ 97 | '/article': ['vote'], 98 | }) 99 | 100 | # list() 101 | self.assertDictEqual( 102 | acl.get(), 103 | { 104 | '/article': {'create','edit','vote'}, 105 | '/profile': {'edit'} 106 | } 107 | ) 108 | 109 | # lit() must produce a copy 110 | l = acl.get() 111 | l['/lol'] = 'a' 112 | l['/article'].add('lol') 113 | 114 | # Test: should not be modified 115 | self.assertDictEqual( 116 | acl.get(), 117 | { 118 | '/article': {'create', 'edit', 'vote'}, 119 | '/profile': {'edit'} 120 | } 121 | ) 122 | 123 | # Test resources 124 | self.assertSetEqual(acl.get_resources(), {'/article', '/profile'}) 125 | 126 | # Test permissions on resources 127 | self.assertSetEqual(acl.get_permissions('/article'), {'create', 'edit', 'vote'}) # empty ok 128 | self.assertSetEqual(acl.get_permissions('/profile'), {'edit'}) # empty ok 129 | 130 | def test_grant(self): 131 | """ grant(), grants(), revoke(), revoke_all(), show() """ 132 | acl = miracle.Acl() 133 | acl.grant('root', '/admin', 'enter') 134 | acl.grant('root', '/admin', 'enter') # dupe 135 | acl.grant('root', '/article', 'edit') 136 | acl.grants({ 137 | 'user': { 138 | '/article': ['view'], 139 | '/admin': ['kill'] 140 | } 141 | }) 142 | acl.revoke('user', '/admin', 'kill') 143 | acl.revoke('user', '/admin', 'kill') # dupe 144 | 145 | # Structure 146 | self.assertSetEqual(acl.get_roles(), {'root', 'user'}) 147 | self.assertDictEqual( acl.get(), { 148 | '/admin': {'enter','kill'}, # 'kill' remains, though revoked 149 | '/article': {'view','edit'} 150 | }) 151 | 152 | # Grants 153 | self.assertDictEqual(acl.show(), { 154 | 'root': { 155 | '/admin': {'enter'}, 156 | '/article': {'edit'} 157 | }, 158 | 'user': { 159 | '/article':{'view'} 160 | } 161 | }) 162 | 163 | # revoke_all() 164 | acl.revoke_all('root', '/article') 165 | self.assertDictEqual(acl.show(), { 166 | 'root': { 167 | '/admin': {'enter'} 168 | }, 169 | 'user': { 170 | '/article':{'view'} 171 | } 172 | }) 173 | 174 | acl.revoke_all('root') 175 | self.assertDictEqual(acl.show(), { 176 | 'user': { 177 | '/article':{'view'} 178 | } 179 | }) 180 | 181 | def test_check(self): 182 | """ check(), check_any(), check_all() ; which(), which_any(), which_all() """ 183 | acl = miracle.Acl() 184 | acl.grant('root', '/admin', 'enter') 185 | acl.grant('admin', '/admin', 'enter') 186 | acl.grant('root', '/user', 'edit') 187 | acl.grant('root', '/user', 'delete') 188 | acl.grant('root', '/user', 'show') 189 | acl.grant('admin', '/user', 'show') 190 | acl.grant('admin', '/user', 'edit') 191 | acl.grant('user', '/user', 'show') 192 | acl.add_role('nobody') 193 | 194 | # which() 195 | self.assertDictEqual(acl.which('???'), {}) 196 | self.assertDictEqual(acl.which('root'), { 197 | '/admin': {'enter'}, 198 | '/user': {'show','edit','delete'} 199 | }) 200 | self.assertDictEqual(acl.which('admin'), { 201 | '/admin': {'enter'}, 202 | '/user': {'show','edit'} 203 | }) 204 | self.assertDictEqual(acl.which('user'), { 205 | '/user': {'show'} 206 | }) 207 | self.assertDictEqual(acl.which('nobody'), {}) 208 | 209 | # which_any() 210 | self.assertDictEqual(acl.which_any([]), {}) 211 | self.assertDictEqual(acl.which_any(['root', 'user']), acl.which('root')) 212 | self.assertDictEqual(acl.which_any(['admin', 'user']), acl.which('admin')) 213 | self.assertDictEqual(acl.which_any(['user']), acl.which('user')) 214 | self.assertDictEqual(acl.which_any(['user', 'nobody']), acl.which('user')) 215 | 216 | # which_all() 217 | self.assertDictEqual(acl.which_all([]), {}) 218 | self.assertDictEqual(acl.which_all(['root', 'admin']), acl.which('admin')) 219 | self.assertDictEqual(acl.which_all(['admin', 'user']), acl.which('user')) 220 | self.assertDictEqual(acl.which_all(['root', 'nobody']), acl.which('nobody')) 221 | self.assertDictEqual(acl.which_all(['user', 'root', 'nobody']), acl.which('nobody')) 222 | 223 | # which_permissions() 224 | self.assertEqual(acl.which_permissions('???', '/admin'), set()) 225 | self.assertEqual(acl.which_permissions('root', '/???'), set()) 226 | self.assertEqual(acl.which_permissions('root', '/admin'), {'enter'}) 227 | self.assertEqual(acl.which_permissions('root', '/user'), {'edit', 'delete', 'show'}) 228 | self.assertEqual(acl.which_permissions('user', '/user'), {'show'}) 229 | 230 | # which_permissions_any() 231 | self.assertEqual(acl.which_permissions_any([], '/admin'), {}) 232 | self.assertEqual(acl.which_permissions_any(['root', 'user'], '/user'), acl.which_permissions('root', '/user')) 233 | self.assertEqual(acl.which_permissions_any(['admin', 'user'], '/user'), acl.which_permissions('admin', '/user')) 234 | self.assertEqual(acl.which_permissions_any(['user'], '/user'), acl.which_permissions('user', '/user')) 235 | self.assertEqual(acl.which_permissions_any(['user', 'nobody'], '/user'), acl.which_permissions('user', '/user')) 236 | 237 | # which_permissions_all() 238 | self.assertEqual(acl.which_permissions_all([], '/admin'), {}) 239 | self.assertEqual(acl.which_permissions_all(['root', 'user'], '/user'), acl.which_permissions('user', '/user')) 240 | self.assertEqual(acl.which_permissions_all(['admin', 'user'], '/user'), acl.which_permissions('user', '/user')) 241 | self.assertEqual(acl.which_permissions_all(['user'], '/user'), acl.which_permissions('user', '/user')) 242 | self.assertEqual(acl.which_permissions_all(['user', 'nobody'], '/user'), acl.which_permissions('nobody', '/user')) 243 | 244 | # check() 245 | self.assertTrue(acl.check('root', '/admin', 'enter')) 246 | self.assertFalse(acl.check('???', '/admin', 'enter')) # unknown role 247 | self.assertFalse(acl.check('root', '/???', 'enter')) # unknown resource 248 | self.assertFalse(acl.check('root', '/admin', '???')) # unknown permission 249 | self.assertTrue(acl.check('user', '/user', 'show')) 250 | 251 | # check_any() 252 | self.assertFalse(acl.check_any([], '/user', 'show')) 253 | self.assertTrue(acl.check_any(['root'], '/user', 'show')) 254 | self.assertTrue(acl.check_any(['root','user'], '/user', 'show')) 255 | self.assertTrue(acl.check_any(['root','user'], '/admin', 'enter')) 256 | self.assertTrue(acl.check_any(['root','user'], '/user', 'delete')) 257 | self.assertFalse(acl.check_any(['admin','user'], '/user', 'delete')) 258 | 259 | # check_all() 260 | self.assertFalse(acl.check_all([], '/user', 'show')) 261 | self.assertTrue(acl.check_all(['root','user'], '/user', 'show')) 262 | self.assertFalse(acl.check_all(['root','user'], '/admin', 'enter')) 263 | self.assertFalse(acl.check_all(['root','user'], '/user', 'delete')) 264 | self.assertFalse(acl.check_all(['root','user'], '/user', 'delete')) 265 | self.assertFalse(acl.check_all(['root','admin'], '/user', 'delete')) 266 | self.assertTrue(acl.check_all(['root','admin'], '/user', 'edit')) 267 | 268 | def test_pickle(self): 269 | """ __getstate__(), __setstate__() """ 270 | acl = miracle.Acl() 271 | acl.grant('root', '/admin', 'enter') 272 | acl.grant('user', '/user', 'show') 273 | acl.grant('author', '/article', 'post') 274 | 275 | self.assertDictEqual(acl.__getstate__(), { 276 | 'roles': {'root','user','author'}, 277 | 'struct': { 278 | '/admin': {'enter'}, 279 | '/user': {'show'}, 280 | '/article': {'post'} 281 | }, 282 | 'grants': { 283 | 'root': { '/admin': {'enter'} }, 284 | 'user': { '/user': {'show'} }, 285 | 'author': { '/article': {'post'} } 286 | } 287 | }) 288 | 289 | acl2 = miracle.Acl() 290 | acl2.__setstate__(acl.__getstate__()) 291 | 292 | self.assertDictEqual( 293 | acl .__getstate__(), 294 | acl2.__getstate__(), 295 | ) 296 | 297 | def test_del(self): 298 | """ del_*() does not remove grants """ 299 | acl = miracle.Acl() 300 | acl.grants({ 301 | 'root': { 302 | 'a': ['anything'], 303 | 'b': ['everything'] 304 | }, 305 | 'admin': { 306 | 'b': ['something'], 307 | }, 308 | 'nobody': { 309 | 'a': {'nothing'}, 310 | 'c': {'nothing'} 311 | } 312 | }) 313 | 314 | acl.del_permission('a', 'anything') 315 | acl.del_role('root') 316 | acl.del_resource('c') 317 | 318 | self.assertDictEqual(acl.show(), { 319 | 'admin': { 320 | 'b': {'something'}, 321 | }, 322 | 'nobody': { 323 | 'a': {'nothing'} 324 | } 325 | }) 326 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist=py{27,34,35,36,37},pypy,pypy3 3 | skip_missing_interpreters=True 4 | 5 | [testenv] 6 | deps=-rrequirements-dev.txt 7 | commands= 8 | nosetests {posargs:tests/} 9 | whitelist_externals=make 10 | 11 | [testenv:dev] 12 | deps=-rrequirements-dev.txt 13 | usedevelop=True 14 | --------------------------------------------------------------------------------