Search code examples
pythonfastapidepends

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?


Solution

  • 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)