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.
- Elevated permission user (admin, root or superuser)
- 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:
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:
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 :
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
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
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.