Teresa N. Fontanella De Santis
Posted on April 23, 2022
Nowadays, tons of APIs (both external and internal) are created and used every day. With methods like authentication/firewall restriction, we can identify who can invoke the methods, or restrict from where is trying to access. But, can we identify and authorize given users to invoke some methods rather than others? In the following tutorial we will cover how to authorize different users to execute certain REST API methods in an easy and straightforward way.
Situation
In this case, we have an Items REST API implemented on Python 3.10 with FastAPI framework. It allows to list all items and get, create and delete an item. All of these operations must be performed by authenticated users. For sake of simplicity, the following users can be used for authentication:
User | Password | Role |
---|---|---|
alice | secret2 | Admin |
johndoe | secret | User |
The application consists of the files: main.py, utils.py and requirements.txt, with the following code.
main.py
utils.py
requirements.txt
You can create a conda environment, install required packages and run the api with:
conda create -n itemsapi pip
conda activate itemsapi
pip install -r requirements.txt
python3 -m uvicorn main:app --reload
After the API is up and running, let's follow these steps to test it:
- Open http://127.0.0.0:8000/docs url in browser.
- Click on "Authorize" and login with username and password (as per Users table shown before).
- To get all items list, select on /items GET API method. Then, click on "Try out" button and on "Execute" button.
- To delete the item with id = 1, select on /item DELETE API method, click on "Try out" and execute the method with item_id = 1. The response is 204 and item is deleted successfully.
Goal
Although only registered users (johndoe and alice in this case) can perform items actions, all of them are able to delete items. As per their roles, alice should be able to delete items, but johndoe shouldn't. To achieve this we will implement authorization at REST API method level, in an easy and extensible way with Casbin.
Implementation
Overview
Casbin is an open source authorization library with support for many models (like Access Control Lists or ACLs, Role Based Access Control or RBAC, Restful, etc) and with implementations on several programming languages (ie: Python, Go, Java, Rust, Ruby, etc).
It consists of two configuration files:
- A model file: a CONF file (with
.conf
extension) which specifies the authorization model to be applied (in this case we will use Restful one) - A policy file: a CSV file that list API methods permissions for each user.
Steps
1) Install casbin python package with pip
pip install casbin
2) Define Conf policy
Create new file called model.conf with the following content:
You can find more details about Casbin model syntax on the documentation
3) Define Policy file
Create a new CSV file called policy.csv and paste the following:
Each row is an allowed permissions with the following values: the second column is the user, the third is the API resource or url, and the last one is a set of allowed methods. In this case, alice will have access to list create and delete items, while johndoe may list and create items but not delete them.
4) Update Python API code to enforce authorization.
In the main.py file, with following lines:
import casbin
...
async def get_current_user_authorization(req: Request, curr_user: User = Depends(get_current_active_user)):
e = casbin.Enforcer("model.conf", "policy.csv")
sub = curr_user.username
obj = req.url.path
act = req.method
if not(e.enforce(sub, obj, act)):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Method not authorized for this user")
return curr_user
It imports the casbin module and create a new authorization function that read the configuration files with casbin.Enforcer
and enforce the user has the required permission.
Then, on the defined API methods, change the old method get_current_active_user
with the new get_current_user_authorization
@app.get("/items/{item_id}")
async def read_item(item_id: int, req: Request, curr_user: User = Depends(get_current_user_authorization)):
return items_dao.get_item(item_id)
@app.post("/items/")
async def create_item(item: Item, req: Request, curr_user: User = Depends(get_current_user_authorization)):
answer = items_dao.create_item(item)
if not(answer):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Item with given id already exists")
else:
return answer
@app.delete("/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_item(item_id: int, req: Request, curr_user: User = Depends(get_current_user_authorization)):
items_dao.delete_item(item_id)
return Response(status_code=status.HTTP_204_NO_CONTENT)
Test
- Start the updated API
- Open http://127.0.0.0:8000/docs url in browser.
- Click on "Authorize" and login with "johndoe".
- Try to delete item with id=1. It will be rejected with a 401 Unauthorized error.
- Logout from that user. Then login with "alice".
- Try to delete item with id=1. The request works fine, returns 204 and item is deleted.
Conclusion
On this post we saw how to use Casbin to implement authorization on REST APIs. Keep in consideration that this example can be extended combining with other authorization models like RBAC, and only changing the model and policy configuration files.
Posted on April 23, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.