Skip to content
Advertisement

Secured endpoints in FastApi using enum

In my app, I want to apply access to a given endpoint based on a role, which is an enum. The way it all works is that a logged in (authorized) user, wants to get access to some resources, or create a new user etc…, then his jwt token is decoded, so we can see his roles (enum). I’m going to create 3 functions (permission_user, permission_admin, permission_manager) that read the roles of the user and based on it, give access or not. I know that I could create 6 functions (permutations), such as permission_user_and_manager, but I want to solve this in a more professional way. I would like to do something based on:

@app.get("/users") #example endpoint
def fetch_users(is_auth: bool = Depends(permission_admin or permission_manager)
.
.

Unfortunately it doesn’t work, do you know any solutions?

Advertisement

Answer

I would supply the value as another dependency which will return a 403 if the enum is not an appropriate value. I would expect a separate dependency that handles the actual auth and returns an enum value for the permissions (e.g. something like AuthRole).

def admin_permissions(auth_role: AuthRole = Depends(get_auth_role)):
    if auth_role!= AuthRole.ADMIN:
        raise HTTPException(
            status_code=403, 
            detail="User must be an admin to perform this action"
        )

In your definition of the endpoint route, you can specify this method as a depends that must be performed before the call happens. You could also apply this to an ApiRouter class to avoid duplication.

@app.get("/users", dependencies=[Depends(admin_permissions)])
def fetch_users():
    ...

Now you will only enter the body of fetch_users if the admin_permissions dependency does not raise the 403 response code.

If you want to parameterize this further, you can use an advanced dependency that uses a class instances __call__ method to perform the work. Then you can provide multiple roles that are acceptable instead of just one. That would look something like this:

class AuthChecker:
    def __init__(self, *roles: AuthRole) -> None:
        self.roles = roles

    def __call__(self, auth_role: AuthRole = Depends(get_auth_role)):
        if auth_role not in self.roles:
            roles_values = " or ".join([role.value for role in self.roles])
            raise HTTPException(
                status_code=403,
                detail=f"User must be in role {roles_values} to perform this action"
            )


@app.get("/users", dependencies=[Depends(AuthChecker(AuthRole.ADMIN, AuthRole.MANAGER))])
def fetch_users():
    return "users"

Full example to play with:

from enum import Enum
import uvicorn as uvicorn
from fastapi import FastAPI, Header, Depends, HTTPException

app = FastAPI()

class AuthRole(Enum):
    ADMIN = "admin"
    MANAGER = "manager"
    NORMAL = "normal"

def get_auth_role(auth_role: AuthRole = Header()) -> AuthRole:
    return auth_role

class AuthChecker:
    def __init__(self, *roles: AuthRole) -> None:
        self.roles = roles

    def __call__(self, auth_role: AuthRole = Depends(get_auth_role)):
        if auth_role not in self.roles:
            roles_values = " or ".join([role.value for role in self.roles])
            raise HTTPException(
                status_code=403,
                detail=f"User must be in role {roles_values} to perform this action"
            )

@app.get("/admin", dependencies=[Depends(AuthChecker(AuthRole.ADMIN, AuthRole.MANAGER))])
def admin(auth_role: AuthRole = Depends(get_auth_role)):
    return auth_role


if __name__ == "__main__":
    uvicorn.run(app, host="127.0.0.1", port=8000)
User contributions licensed under: CC BY-SA
9 People found this is helpful
Advertisement