Problem:
I am building a FastAPI based API and need to design a role-based permission system for authorization. Users can have one of three roles: Admin, Developer, and Operator. These roles are hierarchical:
- Operator has basic permissions.
- Developer inherits Operator permissions and has additional capabilities.
- Admin inherits Developer permissions and has full access.
I want a require_role dependency that enforces role-based authorization for my endpoints. This system should be scalable across multiple microservices and easy to extend when adding new roles in the future.
Extra information:
- Roles must be shared between microservices and the frontend.
- The system should follow a hierarchy where higher roles inherit lower-level permissions.
- New roles may be added occasionally, but I don’t need a full-fledged RBAC system.
Example:
An implementation example to illustration the use case can be:
class Role:
OPERATOR
DEVELOPER
ADMIN
class User(Model):
username: str
role: Role
def get_current_user() -> User:
# to be replaced with actual authentication logic (e.g., JWT, OAuth, etc.)
return User(username="john_doe", role=Role.DEVELOPER)
# Dependency to enforce role-based access
def require_role(min_role: Role) -> Callable[[User], None]:
def role_checker(user: User = Depends(get_current_user)):
# some logic here to verify if the user is authorized or not
return role_checker
# Admins, developers and operators can access this resource
@app.get("/operator-endpoint")
async def operator_endpoint(user: User = Depends(require_role(Role.OPERATOR))):
return {"message": "Accessible by operators and higher roles"}
# Admins and developers can access this resource
@app.get("/developer-endpoint")
async def developer_endpoint(user: User = Depends(require_role(Role.DEVELOPER))):
return {"message": "Accessible by developers and higher roles"}
# Only admins can access this resource
@app.get("/admin-endpoint")
async def admin_endpoint(user: User = Depends(require_role(Role.ADMIN))):
return {"message": "Accessible by admins only"}
First approach: Integer-Based Roles
- Use an integer
Enumto represent role weights, allowing authorization checks via simple comparisons (role >= required_role). - The
Enumwould be shared across microservices and redefined in the frontend. - Adding new roles requires extending the
Enum.
An example of this approach implementation would be:
class Role(Enum):
OPERATOR = 1
DEVELOPER = 2
ADMIN = 3
class User(Model):
username: str
role: int
def require_role(min_role: int) -> Callable[[User], None]:
def role_checker(user: User = Depends(get_current_user)):
if user.role < min_role:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions",
)
return role_checker
Second approach: Database-Backed Roles
- Use a
Roledatabase table to store roles and their weights. - The User table has a foreign key referencing the
Roletable. - Adding new roles requires inserting a new row into the
Roletable.
An example of this approach implementation would be:
class Role(Model):
id: int #primary key
weight: int #an int to reflect thee role weight in order to verify authorization
class User(Model):
username: str
role_id: int #foreign key to Role table
def require_role(min_role: int) -> Callable[[User], None]:
def role_checker(user: User = Depends(get_current_user)):
if user.role.weight < min_role:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions",
)
return role_checker
Question:
- Considering maintainability, scalability (occasional role adding), performance and my use case, which approach is better: integer-based roles or database-backed roles?