row level security for FastAPI framework

Overview

Row Level Permissions for FastAPI

Build Status

While 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).

An extremely simple and incomplete example:

from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer
from fastapi_permissions import configure_permissions, Allow, Deny
from pydantic import BaseModel

app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")

class Item(BaseModel):
    name: str
    owner: str

    def __acl__(self):
        return [
            (Allow, Authenticated, "view"),
            (Allow, "role:admin", "edit"),
            (Allow, f"user:{self.owner}", "delete"),
        ]

class User(BaseModel):
    name: str

    def principals(self):
        return [f"user:{self.name}"]

def get_current_user(token: str = Depends(oauth2_scheme)):
    ...

def get_active_user_principals(user:User = Depends(get_current_user)):
    ...

def get_item(item_identifier):
    ...

# Permission is already wrapped in Depends()
Permission = configure_permissions(get_active_user_principals)

@app.get("/item/{item_identifier}")
async def show_item(item: Item=Permission("view", get_item)):
    return [{"item": item}]

For a better example install fastapi_permissions source in an virtual environment (see further below), and start a test server:

(permissions) $ uvicorn fastapi_permissions.example:app --reload

Visit http://127.0.0.1:8000/docs to 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 file fastapi_permissions/example.py

Why not use Scopes?

For 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.

While scopes are tied only to the state of the user, fastapi_permissions also 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, but fastapi_permissions offers a method to define these constraints in a single place.

There 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...

Long Story Short: Use scopes until you need something different.

Concepts

Since fastapi_permissions heavely derived from the Pyramid framework, I strongly suggest to take a look at its security documentation if anything is unclear to you.

The system depends on a couple of concepts not found in FastAPI:

  • resources: objects that provide an access controll list
  • access controll lists: a list of rules defining which principal has what permission
  • principal: an identifier of a user or his/her associated groups/roles
  • permission: an identifier (string) for an action on an object

resources & access controll lists

A 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:

  1. an action: fastapi_permissions.Allow or fastapi_permissions.Deny
  2. a principal: e.g. "role:admin" or "user:bob"
  3. a permission or a tuple thereof: e.g. "edit" or ("view", "delete")

Examples:

from fastapi_permissions import Allow, Deny, Authenticated, Everyone

class StaticAclResource:
    __acl__ =  [
        (Allow, Everyone, "view"),
        (Allow, "role:user", "share")
    ]

class DynamicAclResource:
    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 in the order provided in the list. This makes some complex configurations simple, but can sometimes be a pain in the lower back…

The two principals Everyone and Authenticated will be discussed in short time.

users & principal identifiers

You 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:

Example:

def get_active_principals(user: User = Depends(get_current_user)):
    if user:
        # user is logged in
        principals = [Everyone, Authenticated]
        principals.extend(getattr(user, "principals", []))
    else:
        # user is not logged in
        principals = [Everyone]
    return principals

special principals

There are two special principals that also help providing access controll lists: Everyone and Authenticated.

The 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.

permissions

A 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.

Usage

There are some things you must provide before using the permissions system:

  • a callable (FastApi dependency) that returns the principal of the logged in (active) user
  • a resource with an access controll list

Configuring the permissions system

Simple configuration with some defaults:

from fastapi_permissions import configure_permissions

# must be provided
def get_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 denied
    • defaults to fastapi_permissions.permission_exception
from fastapi_permissions import configure_permissions

# must be provided
def get_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 operation

To 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

from fastapi_permissions import configure_permissions, Allow

# must be provided
def get_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("/")
async def root(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:

from fastapi_permissions import configure_permissions, Allow

# must be provided
def get_active_principals(...):
    """ returns the principals of the current logged in user"""
    ...

# fetches a resource from the database
def get_item(item_id: int):
    """ returns a resource from the database

    The 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}")
async def show_item(item:Item=Permission("view", get_item)):
    return {"item": item}

helper functions

Sometimes you might want to check permissions inside a function and not as the definition of a path operation:

With 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/.

from fastapi_permissions import (
    has_permission, Allow, All, Everyone, Authenticated
)

user_principals == [Everyone, Authenticated, "role:owner", "user:bob"]
apple_acl == [(Allow, "role:owner", All)]

if has_permission(user_principals, "eat", apple_acl):
    print "Yum!"

The 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:

from fastapi_permissions import list_permissions, Allow, All

user_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 of fastapi_permissions.All.

How it works

The 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.

Wait. I didn't tell you about the latter one?

The 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).

The 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 ;-).

Or 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.

(F.)A.Q.

Permission check on collection of resources

How 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 environment

There 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/activate

Then you can test any changes locally with make test. This will stop on the first error and not report coverage.

(permissions) $ make test

If you can also run all tests and get a coverage report with

(permissions) $ make coverage

And when ready to test everything as an installed package (bonus point if using make clean before)

(permissions) $ make tox

Thanks

  • Sebastián Ramírez, for creating FastAPI
  • William, for fixing my stupid bug
Comments
  • Allow permission check on collection of resources

    Allow permission check on collection of resources

    If I'm not mistaken permissions can only be checked on a single resource. When implementing a bulk api it would be very convenient to be able to check permissions on a list of resources all at once:

    def update_items(items: List[Item]=Permission("edit", get_items), body: ...):
        ...
    
    wontfix 
    opened by cmgreen210 4
  • Permissions on Data

    Permissions on Data

    I am new to fast API, and I searching to implement permissions on the data level. I was reading the documentation of fastapi-permissions but did find what I need. Can we Implement permissions on data level? (example, I can give user A to see an item with id 1, and give the User B access to edit item with id:2 and 4)

    Thank you.

    opened by imadmoussa1 3
  • Convert non iterable permissions to set, rather than just strings

    Convert non iterable permissions to set, rather than just strings

    On my system I want to use an enum for permissions:

    class Permissions(Enum):
        create = "CREATE"
        read = "READ"
        update = "UPDATE"
        delete = "DELETE
    

    However, the permissions doesnt get converted to a set on line 181.

    opened by mcleantom 0
  • Project maintenance and future?

    Project maintenance and future?

    This project seems to be fairly popular and I was unable to find a better alternative. However, seeing that the last commit in this project was almost two years ago, issues stay unanswered, PRs not reviewed and not merged, I'd consider this project no longer being actively maintained. For that reason I am reluctant to use it, and I'm sure others are as well.

    What can we as a community do to help maintain this project?

    opened by pmlk 0
  • Can't access pydantic principals

    Can't access pydantic principals

    I create a pydantic model following the example described in package readme:

            class SuperUser(BaseModel):    
              username: Optional[str]
    
               def principals(self):
                  return ["group:admin"]
    

    But when I call get_active_principals function, it does not append the principal prop from superuser class...I think pydantic does not allow this.

    opened by Master-Y0da 0
  • Use polymorphic principals

    Use polymorphic principals

    Implements polymorphic principals using dataclasses instead of colon-delimited strings, resolving #6. This offers the advantage of more clear semantic separation between the method and value of a principal, whilst still permitting the user to define their own principals.

    The implementation is mostly backwards-compatible, except where outlined. A version number bump will be required on the subsequent release if this PR is merged.

    Introduces a new Principal base class, from which the new default principals of UserPrincipal, RolePrincipal, and ActionPrincipal are defined. These denote the semantics of "is the user x", "has the role x", and "can do action x" respectively.

    Slightly changes the behaviour of list_permissions: instead of having a reserved magic string (permissions:*) to denote the default value in the returned dict, it instead returns a PermissionSet. The PermissionSet is a subclass of dict, which exposes a default attribute to specify what the default permission is, for the given user and resource. Any logic that previously depended on the permissions:* string will need to be rewritten to check the value of the default attribute instead, making this a (slightly) breaking change.

    Since the principals are no longer strings, the example app's show_items route (/items/) will return json objects containing the "method" and "value" fields for permissionsslightly, instead of the previous string representation. This is due to pydantic's clever handling of dataclasses. Note in particular that the permissions:* magic string will not be returned, and so additional logic would need to be implemented for anyone who depends upon this string being returned.

    Also adds some additional tests for the subtle case in which a "deny all" entry in the ACL precedes an "allow all".

    opened by eddsalkield 1
Releases(0.2.6)
  • 0.2.6(Aug 4, 2020)

    0.2.6 - OpenAPI Tests, Aug. 2020

    • Changed the list based ACLs in the example app to catch errors in the test
    • Added tests for the OpenAPI specification of the example app This is a reaction to the "List Fix" problems I had and that were finally resolved by William.
    Source code(tar.gz)
    Source code(zip)
  • 0.2.5(Jul 29, 2020)

Owner
Holger Frey
Twitter: @holgerfrey
Holger Frey
A simple api written in python/fastapi that serves movies from a cassandra table.

A simple api written in python/fastapi that serves movies from a cassandra table. 1)clone the repo 2)rename sample_global_config_.py to global_config.

Sreeraj 1 Aug 26, 2021
🤪 FastAPI + Vue构建的Mall项目后台管理

Mall项目后台管理 前段时间学习Vue写了一个移动端项目 https://www.charmcode.cn/app/mall/home 然后教程到此就结束了, 我就总感觉少点什么,计划自己着手写一套后台管理。 相关项目 移动端Mall项目源码(Vue构建): https://github.com/

王小右 131 Jan 01, 2023
The base to start an openapi project featuring: SQLModel, Typer, FastAPI, JWT Token Auth, Interactive Shell, Management Commands.

The base to start an openapi project featuring: SQLModel, Typer, FastAPI, JWT Token Auth, Interactive Shell, Management Commands.

Bruno Rocha 251 Jan 09, 2023
OpenAPI generated FastAPI server

OpenAPI generated FastAPI server This Python package is automatically generated by the OpenAPI Generator project: API version: 1.0.0 Build package: or

microbo 1 Oct 31, 2021
Deploy an inference API on AWS (EC2) using FastAPI Docker and Github Actions

Deploy an inference API on AWS (EC2) using FastAPI Docker and Github Actions To learn more about this project: medium blog post The goal of this proje

Ahmed BESBES 60 Dec 17, 2022
Piccolo Admin provides a simple yet powerful admin interface on top of Piccolo tables

Piccolo Admin Piccolo Admin provides a simple yet powerful admin interface on top of Piccolo tables - allowing you to easily add / edit / filter your

188 Jan 09, 2023
Publish Xarray Datasets via a REST API.

Xpublish Publish Xarray Datasets via a REST API. Serverside: Publish a Xarray Dataset through a rest API ds.rest.serve(host="0.0.0.0", port=9000) Clie

xarray-contrib 106 Jan 06, 2023
FastAPI Admin Dashboard based on FastAPI and Tortoise ORM.

FastAPI ADMIN 中文文档 Introduction FastAPI-Admin is a admin dashboard based on fastapi and tortoise-orm. FastAPI-Admin provide crud feature out-of-the-bo

long2ice 1.6k Dec 31, 2022
Async and Sync wrapper client around httpx, fastapi, date stuff

lazyapi Async and Sync wrapper client around httpx, fastapi, and datetime stuff. Motivation This library is forked from an internal project that works

2 Apr 19, 2022
A complete end-to-end machine learning portal that covers processes starting from model training to the model predicting results using FastAPI.

Machine Learning Portal Goal Application Workflow Process Design Live Project Goal A complete end-to-end machine learning portal that covers processes

Shreyas K 39 Nov 24, 2022
Example projects built using Piccolo.

Piccolo examples Here are some example Piccolo projects. Tutorials headless blog fastapi Build a documented API with an admin in minutes! Live project

15 Nov 23, 2022
MLServer

MLServer An open source inference server to serve your machine learning models. ⚠️ This is a Work in Progress. Overview MLServer aims to provide an ea

Seldon 341 Jan 03, 2023
API & Webapp to answer questions about COVID-19. Using NLP (Question Answering) and trusted data sources.

This open source project serves two purposes. Collection and evaluation of a Question Answering dataset to improve existing QA/search methods - COVID-

deepset 329 Nov 10, 2022
a lightweight web framework based on fastapi

start-fastapi Version 2021, based on FastAPI, an easy-to-use web app developed upon Starlette Framework Version 2020 中文文档 Requirements python 3.6+ (fo

HiKari 71 Dec 30, 2022
fastapi-crud-sync

Developing and Testing an API with FastAPI and Pytest Syncronous Example Want to use this project? Build the images and run the containers: $ docker-c

59 Dec 11, 2022
volunteer-database

This is the official CSM (Crowd source medical) database The What Now? We created this in light of the COVID-19 pandemic to allow volunteers to work t

32 Jun 21, 2022
Backend logic implementation for realworld with awesome FastAPI

Backend logic implementation for realworld with awesome FastAPI

Nik 2.2k Jan 08, 2023
First API using FastApi

First API using FastApi Made this Simple Api to store and Retrive Student Data of My College Ncc-Bim To View All the endpoits Visit /docs To Run Local

Sameer Joshi 2 Jun 21, 2022
I'm curious if pydantic + fast api can be sensibly used with DDD + hex arch methodology

pydantic-ddd-exploration I'm curious if pydantic + fast api can be sensibly used with DDD + hex arch methodology Prerequisites nix direnv (nix-env -i

Olgierd Kasprowicz 2 Nov 17, 2021
📦 Autowiring dependency injection container for python 3

Lagom - Dependency injection container What Lagom is a dependency injection container designed to give you "just enough" help with building your depen

Steve B 146 Dec 29, 2022