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
FastAPI framework plugins

Plugins for FastAPI framework, high performance, easy to learn, fast to code, ready for production fastapi-plugins FastAPI framework plugins Cache Mem

RES 239 Dec 28, 2022
TODO aplication made with Python's FastAPI framework and Hexagonal Architecture

FastAPI Todolist Description Todolist aplication made with Python's FastAPI framework and Hexagonal Architecture. This is a test repository for the pu

Giovanni Armane 91 Dec 31, 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
Example app using FastAPI and JWT

FastAPI-Auth Example app using FastAPI and JWT virtualenv -p python3 venv source venv/bin/activate pip3 install -r requirements.txt mv config.yaml.exa

Sander 28 Oct 25, 2022
FastAPI Learning Example,对应中文视频学习教程:https://space.bilibili.com/396891097

视频教学地址 中文学习教程 1、本教程每一个案例都可以独立跑,前提是安装好依赖包。 2、本教程并未按照官方教程顺序,而是按照实际使用顺序编排。 Video Teaching Address FastAPI Learning Example 1.Each case in this tutorial c

null 381 Dec 11, 2022
🤪 FastAPI + Vue构建的Mall项目后台管理

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

王小右 131 Jan 1, 2023
Backend, modern REST API for obtaining match and odds data crawled from multiple sites. Using FastAPI, MongoDB as database, Motor as async MongoDB client, Scrapy as crawler and Docker.

Introduction Apiestas is a project composed of a backend powered by the awesome framework FastAPI and a crawler powered by Scrapy. This project has fo

Fran Lozano 54 Dec 13, 2022
Backend Skeleton using FastAPI and Sqlalchemy ORM

Backend API Skeleton Based on @tiangolo's full stack postgres template, with some things added, some things removed, and some things changed. This is

David Montague 18 Oct 31, 2022
FastAPI on Google Cloud Run

cloudrun-fastapi Boilerplate for running FastAPI on Google Cloud Run with Google Cloud Build for deployment. For all documentation visit the docs fold

Anthony Corletti 139 Dec 27, 2022
FastAPI + Django experiment

django-fastapi-example This is an experiment to demonstrate one potential way of running FastAPI with Django. It won't be actively maintained. If you'

Jordan Eremieff 78 Jan 3, 2023
Auth for use with FastAPI

FastAPI Auth Pluggable auth for use with FastAPI Supports OAuth2 Password Flow Uses JWT access and refresh tokens 100% mypy and test coverage Supports

David Montague 95 Jan 2, 2023
FastAPI Boilerplate

FastAPI Boilerplate Features SQlAlchemy session Custom user class Top-level dependency Dependencies for specific permissions Celery SQLAlchemy for asy

Hide 417 Jan 7, 2023
Minimal example utilizing fastapi and celery with RabbitMQ for task queue, Redis for celery backend and flower for monitoring the celery tasks.

FastAPI with Celery Minimal example utilizing FastAPI and Celery with RabbitMQ for task queue, Redis for Celery backend and flower for monitoring the

Grega Vrbančič 371 Jan 1, 2023
A simple docker-compose app for orchestrating a fastapi application, a celery queue with rabbitmq(broker) and redis(backend)

fastapi - celery - rabbitmq - redis -> Docker A simple docker-compose app for orchestrating a fastapi application, a celery queue with rabbitmq(broker

Kartheekasasanka Kaipa 83 Dec 19, 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

null 59 Dec 11, 2022
JSON-RPC server based on fastapi

Description JSON-RPC server based on fastapi: https://fastapi.tiangolo.com Motivation Autogenerated OpenAPI and Swagger (thanks to fastapi) for JSON-R

null 199 Dec 30, 2022
FastAPI Skeleton App to serve machine learning models production-ready.

FastAPI Model Server Skeleton Serving machine learning models production-ready, fast, easy and secure powered by the great FastAPI by Sebastián Ramíre

null 268 Jan 1, 2023
python fastapi example connection to mysql

Quickstart Then run the following commands to bootstrap your environment with poetry: git clone https://github.com/xiaozl/fastapi-realworld-example-ap

null 55 Dec 15, 2022
The template for building scalable web APIs based on FastAPI, Tortoise ORM and other.

FastAPI and Tortoise ORM. Powerful but simple template for web APIs w/ FastAPI (as web framework) and Tortoise-ORM (for working via database without h

prostomarkeloff 95 Jan 8, 2023