Is your feature request related to a problem? Please describe.
I'm hitting a bit of a brick wall with FastAPI at the moment. Without the ability to ask the framework to resolve a dependency, I find it really hard to make some of the helper libraries I want to write work.
What I'd really like to do, for example, is something like:
@app.get('/')
def root(perms=security.has_permission('foo')):
return {'perms': perms}
Where security is something like:
class Security:
def __init__(self, credentials, authenticate, ...):
self.credentials = credentials
self.authenticate = authenticate
...
def has_permission(self, *names):
async def permissions_(resolve=Depends(DependencyResolver)):
creds = await resolve(self.credentials)
user = await resolve(self.authenticate, credentials=creds)
...
has_perms= await resolve(self.user_has_perm, user=user)
return names
return params.Depends(permissions_, use_cache=True)
security = Security(credentials=HTTPBasic(), ...)
Describe the solution you'd like
I'd like to have the DependencyResolver dependency as shown above. resolvewould do the dance to decide if something is async or not, and give priority to any extra parameters passed in.
Describe alternatives you've considered
Sure, I can build closures that nest and nest and nest, but Python function calls are expensive, so I'd prefer not to. I have a feeling some use cases may actually not be possible at all.
If I worked up a PR to add this support, would it be welcome or likely to be rejected?
I think I understand the code example you provided, but I'm not 100% sure. Some questions that might help make it more concrete:
Security initialized to produce the security object used as the default value in the root function?~ Whoops, I missed it at the bottom of the Security class definition, I see it now.authenticate? What is self.user_has_perm? What is HTTPBasic()? (Are they callables? Do they potentially have other dependencies in their signatures?I know you aren't necessarily intending to focus on that example specifically, but I think clarifying the above questions might make it easier to understand the intended scope of your feature request.
The biggest issue I foresee with this (beyond the potentially heavy refactors to the solve_dependencies function it might require) would be that it could make it hard / impossible to determine the required dependencies prior to executing the request, which could mean trouble for generated openapi specs, and guarantees around what may be required for an endpoint call to "succeed".
Personally, I would be weakly opposed to including additional functionality into the main fastapi library that makes it easier to (accidentally) have your generated openapi spec get out of sync with the (potentially dynamically determined) requirements of the endpoint -- I think the current system imposes a sort of type-safety for your API. Obviously it is possible to get around this now by making direct use of the request in your dependencies, but I have found that usually that is less of a challenge than determining how to get all the dependencies injected directly into the function signature.
But I think the current design helps push implementations in a more statically-analyzable direction (which I personally tend to prefer).
(Either way, I wouldn't be opposed in principle to refactors that made it easier to implement similar functionality in a third party package.)
For what it's worth, this isn't the first request I've seen for pulling out the dependency injection part of the framework to make it easier to reuse the dependency resolution functionality, so either way it may be worth thinking about more seriously. (Though I would recommend anyone wanting such functionality look into this package as an alternative, at least in the short term.)
As an example to address point 3. in my comment above, I think this could be achieved more simply without changes to fastapi if you were to refactor your self.authenticate and self.credentials to return objects (constructed using dependency injection) that are capable of performing checks given other inputs, rather than being callables that require dependencies to be injected that then perform the check inside their function body:
from fastapi import Depends, params
class Security:
def __init__(self, get_credentials, get_authenticator, get_permissions_checker):
self.get_credentials = get_credentials
self.get_authenticator = get_authenticator
self.get_permissions_checker = get_permissions_checker
def has_permission(self, *names):
async def get_user(creds=Depends(self.get_credentials), authenticator = Depends(self.get_authenticator)):
return authenticator.authenticate(credentials=creds)
async def permissions_(user = Depends(get_user), permissions_checker = Depends(self.get_permissions_checker)):
return await permissions_checker.check(names=names, user=user)
return params.Depends(permissions_, use_cache=True)
security = Security(get_credentials=HTTPBasic(), ...)
Okay, answering the questions first:
2) authenticate is a dependency that needs to be resolved by calling it, like any other dependency. self.user_has_perm is another dependency, much like authenticate, that would be passed to Security.__init__. HTTPBasic is a fastapi.security.HTTPBasic, but it's basically another dependency that needs resolving by FastAPI's machinery.
3) Sure, I'm happy with anything that allows the security policy to be abstracted away and not copy/pasted between each project. The aim here is to make it easy to set up different security policies: how do you extract credentials from a request? how are those credentials authenticated? how do you check what permissions the authenticated user has?
I'm not sure these all need to end in the the OpenAPI spec, but I do see that being an issue.
With respect to other packages, I already have some prior art in this area.
The problem with trying to build this all statically in the way that FastAPI does is that you end up with a huge chain of very tightly coupled closures, which seems to often defeat the important intention of dependency injection, for me, which is specifying what is required rather than how it is obtained.
Your example in https://github.com/tiangolo/fastapi/issues/578#issuecomment-536700133 looks like where I was heading, but the explosion of closures and nested function calls in that makes me nervous about performance.
I have to no hard numbers to confirm that nervousness, but function calls in Python are relatively expensive, and so I'd be interested in finding ways to keep them to a minimum...
With regard to performance, I think that building it dynamically like this just means the functions know they were defined inside the closure; given references to the function, I don't think the presence of the closure contributes to increased lookup or execution times during calls. (If I understand correctly, what's happening is basically a very high-level version of monomorphization.)
I think the await resolves in your example may actually lead to a larger performance overhead -- by placing the dependencies in the signatures, FastAPI is able to solve the whole set of dependencies necessary in a single pass, whereas I think with this design you'd require another pass for each call to the resolve function.
If you really wanted to optimize for performance, I think (though I'm not sure) you'd be better off eliminating the use of a class/instance attributes entirely. I hacked together a functioning version of your example using as close as I could to your example code, and a more performant-in-theory (I think) function-based approach:
Click to expand
from base64 import b64encode
import time
from typing import Tuple
from starlette.requests import Request
from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
from starlette.testclient import TestClient
from fastapi import Depends, params, HTTPException, FastAPI
from fastapi.security import HTTPBasic, HTTPBasicCredentials
# Shared
def authenticate(credentials: HTTPBasicCredentials) -> str:
if credentials.username == "user1":
if credentials.password != "password1":
raise HTTPException(HTTP_401_UNAUTHORIZED)
return credentials.username
credentials = HTTPBasic()
user_perms = {"user1": ["perms1"]}
# Approach 1
class DependencyResolver:
def __init__(self):
pass
async def __call__(self, func, *args, **kwargs):
return func(*args, **kwargs)
class Security:
def __init__(self, credentials, authenticate):
self.credentials = credentials
self.authenticate = authenticate
def user_has_perm(self, names: Tuple[str, ...], user: str):
if user == "user1" and all(x in ('perms1',) for x in names):
return True
return False
def has_permission(self, *names):
async def permissions_(request: Request, resolve=Depends(DependencyResolver)):
creds = await self.credentials(request=request)
user = await resolve(self.authenticate, credentials=creds)
has_perms = await resolve(self.user_has_perm, names=names, user=user)
if not has_perms:
raise HTTPException(HTTP_403_FORBIDDEN)
return names
return params.Depends(permissions_, use_cache=True)
security1 = Security(credentials=credentials, authenticate=authenticate)
s1_dependency = security1.has_permission("perms1")
# Approach 2
def security(credentials, authenticate):
def has_permission(*names):
async def get_user(creds=Depends(credentials)):
return authenticate(credentials=creds)
def user_has_perm(user=Depends(get_user)):
perms = user_perms.get(user, [])
if all(x in perms for x in names):
return True
return False
def permissions_(has_permissions=Depends(user_has_perm)):
if not has_permissions:
raise HTTPException(HTTP_403_FORBIDDEN)
return names
return params.Depends(permissions_, use_cache=True)
return has_permission
security2 = security(credentials=credentials, authenticate=authenticate)
s2_dependency = security2("perms1")
# Performance
app = FastAPI()
@app.get("/1")
def endpoint(security=s1_dependency):
pass
@app.get("/2")
def endpoint(security=s2_dependency):
pass
def benchmark_endpoint(endpoint):
client = TestClient(app)
t0 = time.time()
for _ in range(100):
response = client.get(endpoint, headers={"Authorization": f"basic {b64encode(b'user1:password1').decode()}"})
assert response.status_code == 200
response = client.get(endpoint, headers={"Authorization": f"basic {b64encode(b'user1:password2').decode()}"})
assert response.status_code == 401
t1 = time.time()
return t1 - t0
for _ in range(2):
# Warmup
benchmark_endpoint("/1")
benchmark_endpoint("/2")
total1 = 0
total2 = 0
for _ in range(20):
total1 += benchmark_endpoint("/1")
total2 += benchmark_endpoint("/2")
print("/1", total1)
print("/2", total2)
"""
/1 12.597245454788208 # class-based
/2 12.48971962928772 # function-based
"""
The function-based approach was consistently about 0.5-1% faster on my computer. I didn't test it, but my suspicion is that, if adapted properly, the approach I suggested above would land somewhere in between these two performance-wise.
Either way, I don't think there's too much overhead being added by the closures; I think the difference is negligible for practical purposes.
Finally got some time to come back to this, what does monomorphization mean to you?
Okay, next problem: what if any of the things passed in to security (credentials, authenticate, etc) can be either defs or async defs? I guess I could manually use is_coroutine_callable on each one, but that feels pretty horrible...
@cjw296 By “monomorphization” I just meant that a specialized version of the function is created (based on the inputs from the containing scope), but once built, calling the specialized function doesn’t have any extra overhead. This is in contrast with approaches where calling the dependency function would require extra lookup/function setup overhead on every call.
For example, checking if the input function is a coroutine function on every call (rather than preprocessing it once, when the dependency is created), would be an example of the “non-monomorphized” approach. (I recognize that technically the analogy may not be 100% perfect, but I think it makes sense.)
If I recall correctly, fastapi actually does do the iscoroutinefunction check on every call to a dependency, and I think as a result there is some room for performance improvements. (But it may cause trouble with the dependency overrides provider capabilities.)
@cjw296 FastAPI handles converting def to async def for you, so as long as you put the function inside a Depends, you shouldn't need to worry about it at all. In particular, I think the above code would work as is if you changed out the def functions for async def or vice versa.
Right, but when you say "changed out the def functions for async def or vice versa", that means I have to do some real contortions in def security to cater for each part of the auth chain being either async or sync.
@cjw296 It seems like what you need is just to use scopes.
They are made for OAuth2, and they integrate well with the docs when you use OAuth2 schemes. But you can still use it for HTTPBasic.
"scopes" are generic enough that you could implement different things with them, including app-based permissions. If you want to make them independent of any other scopes you might want to use, you can "namespace" them, e.g. perms:read, perms:write.
This is a working self-contained example, if you login with the user chris, as that user has permissions to read and execute, you will be able to use those 2 endpoints. But not the one to write. And with user david, you will be able to read and write, but not execute:
from fastapi import FastAPI, Security, Depends, HTTPException
from fastapi.security import HTTPBasic, HTTPBasicCredentials, SecurityScopes
from starlette import status
app = FastAPI()
http_basic_scheme = HTTPBasic()
users = {
"david": {"username": "david", "password": "secret1", "perms": ["read", "write"]},
"chris": {"username": "chris", "password": "secret2", "perms": ["read", "execute"]},
}
def security(
security_scopes: SecurityScopes,
credentials: HTTPBasicCredentials = Depends(http_basic_scheme),
):
user = users.get(credentials.username)
if not user:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Not authenticated"
)
if not user["password"] == credentials.password:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Not authenticated"
)
for scope in security_scopes.scopes:
if scope not in user["perms"]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions"
)
return user
@app.get("/")
def read(user: dict = Security(security, scopes=["read"])):
return {"user": user}
@app.post("/")
def write(user: dict = Security(security, scopes=["write"])):
return {"user": user}
@app.post("/execute")
def execute(user: dict = Security(security, scopes=["execute"])):
return {"user": user}
No need for closures, extra classes, or anything else.
Perhaps, but the one thing I don't like about FastAPI is the encouragement to copy and paste non-trivial amounts of code around between projects. I've consistently found all the auth stuff really hard to follow every time I come to it, to the point where it's stopped me using FastAPI for anything real. I really want to find a good story for abstracting stuff into libraries such that "auth from an OAauth2 bounce, or a simple header-based token like drf, check against active directory for groups and a local database for permissions mapped to those groups" becomes a few lines of config/code, not a mammoth bought of cognitive load that bounces me off somewhere else.
...so, I really want to explore the patterns for abstracting configurable dependencies into libraries :-)
I've also had pushback from a front-end dev I work with and respect highly (@romgain on here...) that using scopes to cover the full range of permissions might be overloading the intention of scopes, particularly in their use case of issuing tokens for third party apps.
Are there examples available of FastAPI apps that use Google or GitHub as their source of user identity (ie: using OAuth2 to get a token from them to identify a user in a FastAPI app?)
Any JWT examples out there?
(and sorry, I'm aware this is (ab)using this issue for discussion, please let me know if there's a better place for this discussion)
About abstractions, yeah, sure, it would be nice to have better abstractions, but sadly we don't have yet a one-size-fits-all for these things, so there's no way to create just the "right" abstraction yet.
About JWT, yeah, there's an example in the docs.
About integration and other flows with OAuth2, that's still missing, that it will come to the docs eventually. Hopefully, that will also lead to some common patterns that can be abstracted away...
Now, for the issue at hand, I guess the specific issue about the parameterizable dependency could be solved by a dependency class in your use case, and that way you don't have to use scopes. So, may we close this issue now?