RBAC

What is Role based Access Control (RBAC)

Most of the CRUD apps, require some level of role based access control.

You may have at least two types of users.

  1. Elevated permission user (admin, root or superuser)
  2. Normal user aka everyone else ;)

More likely you have more levels in between.

This means only the users with specific role can access certain API endpoints or operations e.g. Allow everyone the GET operation, but only admin can DELETE. Some levels in-between can create/update etc.

Code

Following code assumes your User model has a role attribute. It is better to have a default value so that every user created starts with lowest level, even if role is not assigned when creating.

Let us first define the RoleChecker class as follows:

1
2
3
4
5
6
7
8
9
class RoleChecker:
    def __init__(self, allowed_roles: List):
        self.allowed_roles = allowed_roles

    def __call__(self, user: User = Depends(get_current_active_user)):
        if user.role not in self.allowed_roles:
            logger.debug(f"User with role {user.role} not in {self.allowed_roles}")
            raise HTTPException(status_code=403, detail="Operation not permitted")

Then in your routes file use it as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
allow_create_resource = RoleChecker(["admin"])

@router.post(
    "/some-resource/",
    response_model=schemas.MyResource,
    status_code=201,
    dependencies=[Depends(allow_create_resource)],
)
def add_resource(resource: schemas.ResourceCreate, db: Session = Depends(get_db)):
    # Some validation like resource does not already exist
    # Create the resource
    pass

Sometimes you want to allow multiple roles to perform certain operation. That is why, RoleChecker takes a list of roles like :

1
allow_create_resource = RoleChecker(["admin", "manager"])

Learning (Or how I got here)

If you came here just looking for solution, you can stop reading now.

Read on, to know how I reached the solution, things I tried (and failed)

(Sometimes such details give you an idea for something you may want in the future)

As you may know, you can get the current user details in the API via Dependency Injection via user: User = Depends(get_current_user) See the documentation

So easy first attempt was on the lines of

1
2
if user.role not 'admin':
    raise HTTPException(status_code=403, detail="Operation not permitted")

I extended the above to user.role not in ["admin", "manager"] to allow multiple roles to perform that operation.

It works for “proof of concept”, but we cant be adding similar code everywhere

Then I created

1
2
3
def verify_role(required_role: List, user: User = Depends(get_current_active_user)):
    if user.role not in required_role:
        raise HTTPException(status_code=403, detail="Operation not permitted")

I needed to pass the list of roles to the function. Unfortunately I could not call this via Depends. I kept getting Depends has no attribute ... error.

Also, I need to call this from the router decorator function as dependencies=[Depends(my_func)] rather than in the function param like user: User = Depends(get_current_user)

Finally another user pointed me to this section of the documentation, and that was that. 🎉

Thanks

I'm grateful for Marcelo aka Kludex and Danny Rohde on FastAPI gitter for the ideas and help.