{ "info": { "author": "Holger Frey", "author_email": "mail@holgerfrey.de", "bugtrack_url": null, "classifiers": [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: Freely Distributable", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3 :: Only", "Topic :: Internet :: WWW/HTTP :: HTTP Servers" ], "description": "Row Level Permissions for FastAPI\n=================================\n\nWhile trying out the excellent [FastApi][] framework there was one peace missing for me: an easy, declarative way to define permissions of users (and roles/groups) on resources. Since I reall love the way [Pyramid][] handles this, I re-implemented and adapted the system for FastApi (well, you might call it a blatant rip-off).\n\n\nAn extremely simple and incomplete example:\n-------------------------------------------\n\n```python\nfrom fastapi import Depends, FastAPI\nfrom fastapi.security import OAuth2PasswordBearer\nfrom fastapi_permissions import configure_permissions, Allow, Deny\nfrom pydantic import BaseModel\n\napp = FastAPI()\noauth2_scheme = OAuth2PasswordBearer(tokenUrl=\"/token\")\n\nclass Item(BaseModel):\n name: str\n owner: str\n\n def __acl__(self):\n return [\n (Allow, Authenticated, \"view\"),\n (Allow, \"role:admin\", \"edit\"),\n (Allow, f\"user:{self.owner}\", \"delete\"),\n ]\n\nclass User(BaseModel):\n name: str\n\n def principals(self):\n return [f\"user:{self.name}\"]\n\ndef get_current_user(token: str = Depends(oauth2_scheme)):\n ...\n\ndef get_active_user_principals(user:User = Depends(get_current_user)):\n ...\n\ndef get_item(item_identifier):\n ...\n\n# Permission is already wrapped in Depends()\nPermission = configure_permissions(get_active_user_principals)\n\n@app.get(\"/item/{item_identifier}\")\nasync def show_item(item: Item=Permission(\"view\", get_item)):\n return [{\"item\": item}]\n```\n\nFor a better example install ```fastapi_permissions``` source in an virtual environment (see further below), and start a test server:\n\n```\n(permissions) $ uvicorn fastapi_permissions.example:app --reload\n```\n\nVisit to try it out. There are two users available: \"bob\" and \"alice\", both have the password \"secret\".\n\nThe example is derived from the FastApi examples, so it should be familiar. New / added stuff is marked with comments in the source file `fastapi_permissions/example.py`\n\n\nWhy not use Scopes?\n-------------------\n\nFor most applications the use of [scopes][] to determine the rights of a user is sufficient enough. So if scopes fit your application, please use them - they are already a part of the FastAPI framework.\n\nWhile scopes are tied only to the state of the user, `fastapi_permissions` also\ntake the state of the requested resource into account.\n\nLet's take an scientific paper as an example: depending on the state of the submission process (like \"draft\", \"submitted\", \"peer review\" or \"published\") different users should have different permissions on viewing, editing or retracting. This could be acomplished with custom code in the path definition functions, but `fastapi_permissions` offers a method to define these constraints in a single place.\n\nThere is a second case, where `fastapi_permissions` might be the right addition to your app: If your brain is wired / preconditioned like mine to such a permission model - e.g. exposed for a long time to [Pyramid][]...\n\nLong Story Short: Use [scopes][] until you need something different.\n\n\nConcepts\n--------\n\nSince `fastapi_permissions` heavely derived from the [Pyramid][] framework, I strongly suggest to take a look at its [security documentation][pyramid_security] if anything is unclear to you.\n\nThe system depends on a couple of concepts not found in FastAPI:\n\n- **resources**: objects that provide an *access controll list*\n- **access controll lists**: a list of rules defining which *principal* has what *permission*\n- **principal**: an identifier of a user or his/her associated groups/roles\n- **permission**: an identifier (string) for an action on an object\n\n### resources & access controll lists\n\nA resource provides an access controll list via it's ```__acl__``` attribute. It can either be an property of an object or a callable. Each entry in the list is a tuple containing three values:\n\n1. an action: ```fastapi_permissions.Allow``` or ```fastapi_permissions.Deny```\n2. a principal: e.g. \"role:admin\" or \"user:bob\"\n3. a permission or a tuple thereof: e.g. \"edit\" or (\"view\", \"delete\")\n\nExamples:\n\n```python\nfrom fastapi_permissions import Allow, Deny, Authenticated, Everyone\n\nclass StaticAclResource:\n __acl__ = [\n\t\t(Allow, Everyone, \"view\"),\n (Allow, \"role:user\", \"share\")\n ]\n\nclass DynamicAclResource:\n def __acl__(self):\n return [\n\t\t(Allow, Authenticated, \"view\"),\n (Allow, \"role:user\", \"share\"),\n (Allow, f\"user:{self.owner}\", \"edit\"),\n ]\n\n# in contrast to pyramid, resources might be access conroll list themselves\n# this can save some typing:\n\nAclResourceAsList = [(Allow, Everyone, \"view\"), (Deny, \"role:troll\", \"edit\")]\n```\n\nYou don't need to add any \"deny-all-clause\" at the end of the access controll list, this is automagically implied. All entries in a ACL are checked in *the order provided in the list*. This makes some complex configurations simple, but can sometimes be a pain in the lower back\u2026\n\nThe two principals ```Everyone``` and ```Authenticated``` will be discussed in short time.\n\n### users & principal identifiers\n\nYou **must provide** a function that returns the principals of the current active user. The principals is just a list of strings, identifying the user and groups/roles the user belongs to:\n\nExample:\n\n```python\ndef get_active_principals(user: User = Depends(get_current_user)):\n if user:\n # user is logged in\n principals = [Everyone, Authenticated]\n principals.extend(getattr(user, \"principals\", []))\n else:\n # user is not logged in\n principals = [Everyone]\n return principals\n```\n\n#### special principals\n\nThere are two special principals that also help providing access controll lists: ```Everyone``` and ```Authenticated```.\n\nThe ```Everyone``` principal should be added regardless of any other defined principals or login status, ```Authenticated``` should only be added for a user that is logged in.\n\n### permissions\n\nA permission is just a string that represents an action to be performed on a resource. Just make something up.\n\nAs with the special principals, there is a special permission that is usable as a wildcard: ```fastapi_permisssions.All```.\n\n\nUsage\n-----\n\nThere are some things you must provide before using the permissions system:\n\n- a callable ([FastApi dependency][dependency]) that returns the principal of the logged in (active) user\n- a resource with an access controll list\n\n### Configuring the permissions system\n\nSimple configuration with some defaults:\n\n```python\nfrom fastapi_permissions import configure_permissions\n\n# must be provided\ndef get_active_principals(...):\n \"\"\" returns the principals of the current logged in user\"\"\"\n ...\n\npermission = configure_permissions(get_active_principals)\n```\n\nOne configuration option is available:\n\n- permission_exception:\n\t- this exception will be raised if a permission is denied\n\t- defaults to fastapi_permissions.permission_exception\n\n```python\nfrom fastapi_permissions import configure_permissions\n\n# must be provided\ndef get_active_principals(...):\n \"\"\" returns the principals of the current logged in user\"\"\"\n ...\n\npermission = configure_permissions(\n get_active_principals,\n permission_exception\n\n)\n```\n\n### using permissions in path operation\n\nTo use access controll in a path operation, you call the perviously configured function with a permission and the resource. If the permission is granted, the requested resource the permission is checked on will be returned, or in this case, the acl list\n\n```python\nfrom fastapi_permissions import configure_permissions, Allow\n\n# must be provided\ndef get_active_principals(...):\n \"\"\" returns the principals of the current logged in user\"\"\"\n ...\n\nexample_acl = [(Allow \"role:user\", \"view\")]\n\n# Permission is already wrapped in Depends()\nPermission = configure_permissions(get_active_principals)\n\n@app.get(\"/\")\nasync def root(acls:list=Permission(\"view\", example_acl)):\n return {\"OK\"}\n```\n\nInstead of using an access controll list directly, you can also provide a dependency function that might fetch a resource from a database, the resouce should provide its access controll list via the `__acl__` attribute:\n\n```python\nfrom fastapi_permissions import configure_permissions, Allow\n\n# must be provided\ndef get_active_principals(...):\n \"\"\" returns the principals of the current logged in user\"\"\"\n ...\n\n# fetches a resource from the database\ndef get_item(item_id: int):\n \"\"\" returns a resource from the database\n\n The resource provides an access controll list via its \"__acl__\" attribute.\n \"\"\"\n ...\n\n# Permission is alredy wrapped in Depends()\nPermission = configure_permissions(get_active_principals)\n\n@app.get(\"/item/{item_id}\")\nasync def show_item(item:Item=permission(\"view\", get_item)):\n return {\"item\": item}\n```\n\n### helper functions\n\nSometimes you might want to check permissions inside a function and not as the definition of a path operation:\n\nWith ```has_permission(user_principals, permission, resource)``` you can preform the permission check programatically. The function signature can easily be remebered with something like \"John eat apple?\". The result will be either ```True``` or ```False```, so no need for try/except blocks \\o/.\n\n```python\nfrom fastapi_permissions import (\n has_permission, Allow, All, Everyone, Authenticated\n)\n\nuser_principals == [Everyone, Authenticated, \"role:owner\", \"user:bob\"]\napple_acl == [(Allow, \"role:owner\", All)]\n\nif has_permission(user_principals, \"eat\", apple_acl):\n print \"Yum!\"\n```\n\nThe other function provided is ```list_permissions(user_principals, resource)``` this will return a dict of all available permissions and a boolean value if the permission is granted or denied:\n\n```python\nfrom fastapi_permissions import list_permissions, Allow, All\n\nuser_principals == [Everyone, Authenticated, \"role:owner\", \"user:bob\"]\napple_acl == [(Allow, \"role:owner\", All)]\n\nprint(list_permissions(user_principals, apple_acl))\n{\"permissions:*\": True}\n```\n\nPlease note, that ```\"permissions:*\"``` is the string representation of ```fastapi_permissions.All```.\n\n\nHow it works\n============\n\nThe main work is done in the ```has_permissions()``` function, but the most interesting ones (at least for me) are the ```configure_permissions()``` and ```permission_dependency_factory()``` functions.\n\nWait. I didn't tell you about the latter one?\n\nThe ```permission()``` thingy used in the path operation definition before is actually the mentioned ```permission_dependency_factory()```. The ```configure_permissions()``` function just provisiones it with some default values using ```functools.partial```. This reduces the function signature from ```permission_dependency_factory(permission, resource, active_principals_func, permission_exception)``` down to ```partial_function(permission, resource)```.\n\nThe ```permission_dependency_factory``` returns another function with the signature ```permission_dependency(Depends(resource), Depends(active_principals_func))```. This is the acutal signature, that ```Depends()``` uses in the path operation definition to search and inject the dependencies. The rest is just some closure magic ;-).\n\nOr in other words: to have a nice API, the ```Depends()``` in the path operation function should only have a function signature for retrieving the active user and the resource. On the other side, when writing the code, I wanted to only specifiy the parts relevant to the path operation function: the resource and the permission. The rest is just on how to make it work.\n\nDev & Test virtual environment\n------------------------------\n\nTesting and development should be done with a virtual environment.\n\n```\n$ git clone https://github.com/holgi/fastapi-permissions.git\n$ cd fastapi-permissions\n$ python3 -m venv .venv --prompt permissions\n$ source .venv/bin/activate\n(permissions) $ pip install -U pip\n```\n\nDevelopment requires flit to be installed:\n\n```\n(permissions) $ pip install flit\n(permissions) $ flit install --pth-file\n```\n\nThen you can test any changes locally with ```make test```. This will stop\non the first error and not report coverage.\n\n```\n(permissions) $ make test\n```\n\nIf you can also run all tests and get a coverage report with\n\n```\n(permissions) $ make coverage\n```\n\nAnd when ready to test everything as an installed package (bonus point if\nusing ```make clean``` before)\n\n```\n(permissions) $ make tox\n```\n\n\n\n\n[FastApi]: https://fastapi.tiangolo.com/\n[dependency]: https://fastapi.tiangolo.com/tutorial/dependencies/first-steps/\n[pyramid]: https://trypyramid.com\n[pyramid_security]: https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/security.html\n[scopes]: https://fastapi.tiangolo.com/tutorial/security/oauth2-scopes/\n", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/holgi/fastapi-permissions", "keywords": "", "license": "", "maintainer": "", "maintainer_email": "", "name": "fastapi_permissions", "package_url": "https://pypi.org/project/fastapi_permissions/", "platform": "", "project_url": "https://pypi.org/project/fastapi_permissions/", "project_urls": { "Homepage": "https://github.com/holgi/fastapi-permissions" }, "release_url": "https://pypi.org/project/fastapi_permissions/0.2.1/", "requires_dist": [ "fastapi >= 0.33.0", "pyjwt; extra == \"dev\"", "passlib[bcrypt]; extra == \"dev\"", "fastapi[all]; extra == \"dev\"", "black; extra == \"test\"", "flake8; extra == \"test\"", "flake8-comprehensions; extra == \"test\"", "pytest >=4.0.0; extra == \"test\"", "pytest-cov; extra == \"test\"", "pytest-mock; extra == \"test\"", "pytest-asyncio; extra == \"test\"", "tox; extra == \"test\"" ], "requires_python": ">=3.6", "summary": "Row Level Permissions for FastAPI", "version": "0.2.1" }, "last_serial": 5622883, "releases": { "0.0.1": [ { "comment_text": "", "digests": { "md5": "03df3119402c2ca859a97f927c48e9fb", "sha256": "41115955680f9b87c14a9cbd8694c1bb1408d670b616ade6a03cef46a72f8d5c" }, "downloads": -1, "filename": "fastapi_permissions-0.0.1-py3-none-any.whl", "has_sig": false, "md5_digest": "03df3119402c2ca859a97f927c48e9fb", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 13591, "upload_time": "2019-07-21T18:48:49", "url": "https://files.pythonhosted.org/packages/e4/59/ec84d5b0c71d5923bde082cd3d860edc5994f9a1fee5269d8530f4d7467b/fastapi_permissions-0.0.1-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "d88de38d5a6fc346db542b6cadfd61ac", "sha256": "bb766a26016fb58fb12d964c655a3b7122096616a356d056c1b200caf29ccb8e" }, "downloads": -1, "filename": "fastapi_permissions-0.0.1.tar.gz", "has_sig": false, "md5_digest": "d88de38d5a6fc346db542b6cadfd61ac", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 12963, "upload_time": "2019-07-21T18:49:01", "url": "https://files.pythonhosted.org/packages/74/43/22ab601781bbd8ca795d6f2e0baa318c9cd107a8c8903f7fc51cd5765781/fastapi_permissions-0.0.1.tar.gz" } ], "0.1.0": [ { "comment_text": "", "digests": { "md5": "c3b133bd387ac664303863f6d6146756", "sha256": "6a0ccf765fc2821588573a52fa0f110ae69ee7f6ffe91127fc633c2860c9e659" }, "downloads": -1, "filename": "fastapi_permissions-0.1.0-py3-none-any.whl", "has_sig": false, "md5_digest": "c3b133bd387ac664303863f6d6146756", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 13593, "upload_time": "2019-07-21T18:50:52", "url": "https://files.pythonhosted.org/packages/a7/7e/c8b5aab4e151b240bae36563ffda14faeb01b78bb24dd2070151a706d26b/fastapi_permissions-0.1.0-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "35e2cd0a724ca20397c6846b0647528d", "sha256": "c4f623c7c24b922a93da6b1d166b6f44d51a94b46216fa6e99715800c1967d70" }, "downloads": -1, "filename": "fastapi_permissions-0.1.0.tar.gz", "has_sig": false, "md5_digest": "35e2cd0a724ca20397c6846b0647528d", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 12962, "upload_time": "2019-07-21T18:50:56", "url": "https://files.pythonhosted.org/packages/e1/88/ce2defea116b6fe3ae91b3f9bacbc306c1b9d57bd50d13ed767956d4deb3/fastapi_permissions-0.1.0.tar.gz" } ], "0.2.0": [ { "comment_text": "", "digests": { "md5": "01b8819c7cb7d88bff776b5f2e8158c6", "sha256": "d6a46fdaf8ce0ddf670664a519b0a709b67facdb89d618825eeb6fa2c04a41de" }, "downloads": -1, "filename": "fastapi_permissions-0.2.0-py3-none-any.whl", "has_sig": false, "md5_digest": "01b8819c7cb7d88bff776b5f2e8158c6", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 21559, "upload_time": "2019-08-01T20:25:12", "url": "https://files.pythonhosted.org/packages/e6/b8/96fd5e41a16bc461e5f464c512ebbebcb418d2641231d527252824555c27/fastapi_permissions-0.2.0-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "f0986b1e454a9c25a234ca01e291de78", "sha256": "6c405df81d2b279fcebc5f436cd92adb9488bfeb4d9cea7fc174fc7203ab4fb2" }, "downloads": -1, "filename": "fastapi_permissions-0.2.0.tar.gz", "has_sig": false, "md5_digest": "f0986b1e454a9c25a234ca01e291de78", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 14082, "upload_time": "2019-08-01T20:25:18", "url": "https://files.pythonhosted.org/packages/e9/78/20cf081d0274441c4674450f7e4d431ff9f06c613b02900e4d90c607b9a2/fastapi_permissions-0.2.0.tar.gz" } ], "0.2.1": [ { "comment_text": "", "digests": { "md5": "82ac103a9c9ca890ede232eed9d0bc28", "sha256": "e21a1c797d769506336b76d85bbf6dfcf3b616117f1e99e9764dc20f3ddec895" }, "downloads": -1, "filename": "fastapi_permissions-0.2.1-py3-none-any.whl", "has_sig": false, "md5_digest": "82ac103a9c9ca890ede232eed9d0bc28", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 21592, "upload_time": "2019-08-02T09:41:41", "url": "https://files.pythonhosted.org/packages/75/53/7e7bd84849b9be045727fac7c8ed0392414b1ad5be9112dba6a45264a21b/fastapi_permissions-0.2.1-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "264589d57c88923c002597015b2d7263", "sha256": "1429092f2985696287181829408f9a2663dc5576cd61dda872a9362004f05889" }, "downloads": -1, "filename": "fastapi_permissions-0.2.1.tar.gz", "has_sig": false, "md5_digest": "264589d57c88923c002597015b2d7263", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 14269, "upload_time": "2019-08-02T09:41:44", "url": "https://files.pythonhosted.org/packages/5d/9f/596b15e5ee340c08b240194afee010c577c7bdd8bff84502796c352b58ae/fastapi_permissions-0.2.1.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "82ac103a9c9ca890ede232eed9d0bc28", "sha256": "e21a1c797d769506336b76d85bbf6dfcf3b616117f1e99e9764dc20f3ddec895" }, "downloads": -1, "filename": "fastapi_permissions-0.2.1-py3-none-any.whl", "has_sig": false, "md5_digest": "82ac103a9c9ca890ede232eed9d0bc28", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 21592, "upload_time": "2019-08-02T09:41:41", "url": "https://files.pythonhosted.org/packages/75/53/7e7bd84849b9be045727fac7c8ed0392414b1ad5be9112dba6a45264a21b/fastapi_permissions-0.2.1-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "264589d57c88923c002597015b2d7263", "sha256": "1429092f2985696287181829408f9a2663dc5576cd61dda872a9362004f05889" }, "downloads": -1, "filename": "fastapi_permissions-0.2.1.tar.gz", "has_sig": false, "md5_digest": "264589d57c88923c002597015b2d7263", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 14269, "upload_time": "2019-08-02T09:41:44", "url": "https://files.pythonhosted.org/packages/5d/9f/596b15e5ee340c08b240194afee010c577c7bdd8bff84502796c352b58ae/fastapi_permissions-0.2.1.tar.gz" } ] }