Package: fastapi_permissions
Row Level Permissions for FastAPIWhile trying out the excellentFastApiframework 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 wayPyramidhandles this, I re-implemented and adapted the system for FastApi (well, you might call it a blatant rip-off).An extremely simple and incomplete example:fromfastapiimportDepends,FastAPIfromfastapi.securityimportOAuth2PasswordBearerfromfastapi_permissionsimportconfigure_permissions,Allow,DenyfrompydanticimportBaseModelapp=FastAPI()oauth2_scheme=OAuth2PasswordBearer(tokenUrl="/token")classItem(BaseModel):name:strowner:strdef__acl__(self):return[(Allow,Authenticated,"view"),(Allow,"role:admin","edit"),(Allow,f"user:{self.owner}","delete"),]classUser(BaseModel):name:strdefprincipals(self):return[f"user:{self.name}"]defget_current_user(token:str=Depends(oauth2_scheme)):...defget_active_user_principals(user:User=Depends(get_current_user)):...defget_item(item_identifier):...# Permission is already wrapped in Depends()Permission=configure_permissions(get_active_user_principals)@app.get("/item/{item_identifier}")asyncdefshow_item(item:Item=Permission("view",get_item)):return[{"item":item}]For a better example installfastapi_permissionssource in an virtual environment (see further below), and start a test server:(permissions) $ uvicorn fastapi_permissions.example:app --reloadVisithttp://127.0.0.1:8000/docsto try it out. There are two users available: "bob" and "alice", both have the password "secret".The example is derived from the FastApi examples, so it should be familiar. New / added stuff is marked with comments in the source filefastapi_permissions/example.pyWhy not use Scopes?For most applications the use ofscopesto 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.While scopes are tied only to the state of the user,fastapi_permissionsalso
take the state of the requested resource into account.Let'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, butfastapi_permissionsoffers a method to define these constraints in a single place.There is a second case, wherefastapi_permissionsmight 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 toPyramid...Long Story Short: Usescopesuntil you need something different.ConceptsSincefastapi_permissionsheavely derived from thePyramidframework, I strongly suggest to take a look at itssecurity documentationif anything is unclear to you.The system depends on a couple of concepts not found in FastAPI:resources: objects that provide anaccess controll listaccess controll lists: a list of rules defining whichprincipalhas whatpermissionprincipal: an identifier of a user or his/her associated groups/rolespermission: an identifier (string) for an action on an objectresources & access controll listsA 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:an action:fastapi_permissions.Alloworfastapi_permissions.Denya principal: e.g. "role:admin" or "user:bob"a permission or a tuple thereof: e.g. "edit" or ("view", "delete")Examples:fromfastapi_permissionsimportAllow,Deny,Authenticated,EveryoneclassStaticAclResource:__acl__=[(Allow,Everyone,"view"),(Allow,"role:user","share")]classDynamicAclResource:def__acl__(self):return[(Allow,Authenticated,"view"),(Allow,"role:user","share"),(Allow,f"user:{self.owner}","edit"),]# in contrast to pyramid, resources might be access conroll list themselves# this can save some typing:AclResourceAsList=[(Allow,Everyone,"view"),(Deny,"role:troll","edit")]You 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 inthe order provided in the list. This makes some complex configurations simple, but can sometimes be a pain in the lower back…The two principalsEveryoneandAuthenticatedwill be discussed in short time.users & principal identifiersYoumust providea 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:Example:defget_active_principals(user:User=Depends(get_current_user)):ifuser:# user is logged inprincipals=[Everyone,Authenticated]principals.extend(getattr(user,"principals",[]))else:# user is not logged inprincipals=[Everyone]returnprincipalsspecial principalsThere are two special principals that also help providing access controll lists:EveryoneandAuthenticated.TheEveryoneprincipal should be added regardless of any other defined principals or login status,Authenticatedshould only be added for a user that is logged in.permissionsA permission is just a string that represents an action to be performed on a resource. Just make something up.As with the special principals, there is a special permission that is usable as a wildcard:fastapi_permisssions.All.UsageThere are some things you must provide before using the permissions system:a callable (FastApi dependency) that returns the principal of the logged in (active) usera resource with an access controll listConfiguring the permissions systemSimple configuration with some defaults:fromfastapi_permissionsimportconfigure_permissions# must be provideddefget_active_principals(...):""" returns the principals of the current logged in user"""...# Permission is already wrapped in Depends()Permission=configure_permissions(get_active_principals)One configuration option is available:permission_exception:this exception will be raised if a permission is denieddefaults to fastapi_permissions.permission_exceptionfromfastapi_permissionsimportconfigure_permissions# must be provideddefget_active_principals(...):""" returns the principals of the current logged in user"""...# Permission is already wrapped in Depends()Permission=configure_permissions(get_active_principals,permission_exception)using permissions in path operationTo 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 listfromfastapi_permissionsimportconfigure_permissions,Allow# must be provideddefget_active_principals(...):""" returns the principals of the current logged in user"""...example_acl=[(Allow,"role:user","view")]# Permission is already wrapped in Depends()Permission=configure_permissions(get_active_principals)@app.get("/")asyncdefroot(acls:list=Permission("view",example_acl)):return{"OK"}Instead 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:fromfastapi_permissionsimportconfigure_permissions,Allow# must be provideddefget_active_principals(...):""" returns the principals of the current logged in user"""...# fetches a resource from the databasedefget_item(item_id:int):""" returns a resource from the databaseThe resource provides an access controll list via its "__acl__" attribute."""...# Permission is alredy wrapped in Depends()Permission=configure_permissions(get_active_principals)@app.get("/item/{item_id}")asyncdefshow_item(item:Item=Permission("view",get_item)):return{"item":item}helper functionsSometimes you might want to check permissions inside a function and not as the definition of a path operation:Withhas_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 eitherTrueorFalse, so no need for try/except blocks \o/.fromfastapi_permissionsimport(has_permission,Allow,All,Everyone,Authenticated)user_principals==[Everyone,Authenticated,"role:owner","user:bob"]apple_acl==[(Allow,"role:owner",All)]ifhas_permission(user_principals,"eat",apple_acl):print"Yum!"The other function provided islist_permissions(user_principals, resource)this will return a dict of all available permissions and a boolean value if the permission is granted or denied:fromfastapi_permissionsimportlist_permissions,Allow,Alluser_principals==[Everyone,Authenticated,"role:owner","user:bob"]apple_acl==[(Allow,"role:owner",All)]print(list_permissions(user_principals,apple_acl)){"permissions:*":True}Please note, that"permissions:*"is the string representation offastapi_permissions.All.How it worksThe main work is done in thehas_permissions()function, but the most interesting ones (at least for me) are theconfigure_permissions()andpermission_dependency_factory()functions.Wait. I didn't tell you about the latter one?Thepermission()thingy used in the path operation definition before is actually the mentionedpermission_dependency_factory(). Theconfigure_permissions()function just provisiones it with some default values usingfunctools.partial. This reduces the function signature frompermission_dependency_factory(permission, resource, active_principals_func, permission_exception)down topartial_function(permission, resource).Thepermission_dependency_factoryreturns another function with the signaturepermission_dependency(Depends(resource), Depends(active_principals_func)). This is the acutal signature, thatDepends()uses in the path operation definition to search and inject the dependencies. The rest is just some closure magic ;-).Or in other words: to have a nice API, theDepends()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.(F.)A.Q.Permission check on collection of resourcesHow to use the library with something like this:List[Item]=Permission("edit", get_items).
The question was actually issue #3 and I have written a longer answer in the issue, please have a look there.Dev & Test virtual environmentThere is an easy to use make command for setting up a virtual environment, installing the required packages and installing the project in an editable way.$ git clone https://github.com/holgi/fastapi-permissions.git
$ cd fastapi-permissions
$ make devenv
$ source .venv/bin/activateThen you can test any changes locally withmake test. This will stop
on the first error and not report coverage.(permissions) $ make testIf you can also run all tests and get a coverage report with(permissions) $ make coverageAnd when ready to test everything as an installed package (bonus point if
usingmake cleanbefore)(permissions) $ make toxThanksSebastián Ramírez, for creating FastAPIWilliam, for fixing my stupid bug