Building Access Permissions into Your API

Microservices-oriented architecture has evolved from a buzzword to standard architecture practice for building large-scale applications. Applications are designed as blocks of independently deployable services and communicate with each other via lightweight mechanisms.
Microservices offer a lot of advantages, including flexible language selection, functionality-based scalability, independent deployment, and independent product development teams. They can, however, bring challenges as well. One of those challenges is authentication and authorization of resources.
In a monolithic application, a single user session can be used to authenticate and authorize the user. With independent microservices, the user context is not shared among them. That means that authentication and authorization need to be handled in each microservice, causing duplication of the auth logic. The duplicated code can distract developers and can be difficult to maintain and monitor.
If an authorization mechanism is implemented as a global shared provider, it will help remove duplication, freeing the development team to focus on the business logic.
In this tutorial, you will be building REST API endpoints in Python and adding resource access permissions to these endpoints using Cerbos.
Building a RESTful API
You will be building RESTful APIs on Python with permission management. To get started with this tutorial, make sure you have the following prerequisites:
In this tutorial, you will be creating REST APIs for the TODO service. The APIs will be built using Flask web framework.
Initialize the API by importing the Flask module into your application:
1 2 3 4 5 6 7 |
```python from flask import Flask from flask_restful import Resource, Api, reqparse app = Flask(__name__) api = Api(app) ``` |
The API you’re building will have Create, Update, Read, and Delete REST API endpoints.
Now add the API endpoints for the TODO resource:
1 2 3 4 5 6 7 8 9 10 11 12 |
```python from flask import Flask from flask_restful import Resource, Api, reqparse app = Flask(__name__) api = Api(app) class TODO(Resource): pass api.add_resource(TODO, '/todo') ``` |
Here you’ve added a TODO class that will contain all the necessary methods to handle the request to the provided routes. The line api.add_resource(TODO, '/todo')
is to define the routes from which the API will be accessed.
Once you have the resource, add the methods to handle the GET, POST, PUT, EDIT requests:
1 2 3 4 5 6 7 |
```python class TODO(Resource): def get(self): return {'data': 'data'}, 200 api.add_resource(TODO, '/todo') ``` |
Before running the Python file, make sure to install the Flask
and flask_restful
dependencies:
1 2 3 4 |
``` > pip3 install Flask > pip3 install flask_restful ``` |
Now you can start the Flask server by running the main.py
file:
1 2 3 |
``` > python3 main.go ``` |
This will start the Flask
app in the default port 5000
. To check the GET /todo
API, use the curl command:
1 2 3 4 |
``` > curl http://127.0.0.1:5000/todo #=> {"data": "data"} ``` |
Now that you have the server up and running, add the remaining endpoints create, update, delete
including authentication
.
Authentication
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
```python class TODO(Resource): @app.route("/todo/<string:id>") def get(id): if not TODO.__authenticate(request.headers["token"]): return {'message': 'API key invalid'}, 401 return {'data': id}, 200 def post(self): if not TODO.__authenticate(request.headers['TOKEN']): return {'message': 'API key invalid'}, 401 return {'message': 'success'}, 201 @app.route("/todo/<string:id>", methods = ['PUT']) def put(id): if not TODO.__authenticate(request.headers['TOKEN']): return {'message': 'API key invalid'}, 401 return {'data': id}, 200 @app.route("/todo/<string:id>", methods = ['DELETE']) def delete(self): if not TODO.__authenticate(request.headers['TOKEN']): return {'message': 'API key invalid'}, 401 return {'message': id}, 200 def __authenticate(token): if token in Tokens: return True else: return False api.add_resource(TODO, '/todo') ``` |
The API key for the users is managed in a dict
. Create a new file resources.py
with the API tokens. This file is imported by the main file to check if the token is valid.
1 2 3 4 5 6 7 8 |
```python # format => { "token": "userid" } Tokens = { "user1SecretToken": "1", "user2SecretToken": "2", "user3SecretToken": "3" } ``` |
Here you can see that an authentication mechanism is added to check if the API key provided by the user is valid. If the token is invalid, the user will receive the message “API key invalid.”
The next step is to add an authorization mechanism in the API. You will need to set up the following authorization policies.
Authorization
GET
- User with
admin
role can get their TODO or any other user’s TODO. - User with
user
role can get only their TODO and cannot get other users’ TODO.
CREATE
- User with
user
oradmin
role can create a TODO for self.
UPDATE
- User with
admin
role can update their TODO or any other user’s TODO. - User with
user
role can update only their TODO and cannot update other users’ TODO.
DELETE
- User with
admin
role can delete any TODO. - User with
user
role cannot delete any TODO.
I will be using Cerbos for authorization of the user requests. Cerbos helps you to authorize by defining context-aware access control policies for your application resources.
There are three steps to add managing your permissions using Cerbos:
- Deploy and run Cerbos
- Define your policies
- Check permissions
Run Cerbos
Create a policies directory where all the policies will be stored:
1 2 3 |
``` mkdir -p cerbos-bin/policies ``` |
Start the Cerbos server using the container image.
1 2 3 |
``` docker run --rm --name cerbos -d -v $(pwd)/cerbos-bin/policies:/policies -p 3592:3592 ghcr.io/cerbos/cerbos:0.4.0 ``` |
This will start the Cerbos server in port 3592
. Navigate to the port (http://localhost:3592/), where you can see documentation about the Cerbos API. You can also try out the API from the web page.
Define Your Policies
The policies will be defined in a user-friendly YAML format. Not only developers, but any non-technical person can go through the YAML file and understand the defined policies, which are found in a central place and are easy to manage.
Create a file derived_roles.yml
inside the cerbos-bin/policies
directory:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
```yml --- apiVersion: "api.cerbos.dev/v1" derived_roles: name: todo_derived_roles definitions: - name: admin parentRoles: ["admin"] - name: user_that_owns_the_record parentRoles: ["user"] condition: match: expr: request.resource.attr.user == request.principal.id - name: any_user parentRoles: ["user"] ``` |
There are three derived roles created to which the policies will be attached. Create a file resource_policy.yml
to store the resource policies:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
```yml --- apiVersion: api.cerbos.dev/v1 resourcePolicy: version: "default" importDerivedRoles: - todo_derived_roles resource: todo rules: - actions: ["*"] effect: EFFECT_ALLOW roles: - admin - actions: ["create", "view", "update"] derivedRoles: - user_that_owns_the_record effect: EFFECT_ALLOW - actions: ["view"] derivedRoles: - any_user effect: EFFECT_ALLOW condition: match: expr: request.resource.attr.public == "true" ``` |
Check Permissions
The final step is to check the permissions in the API endpoint to validate if the user is authorized to access the TODO resource.
Create a generic Python file that will handle the permission check. To check the permission, make an API call to the Cerbos server with data about the user, their attributes, and the role of the user in a JSON format. Modify the get
method to include the permissions check using Cerbos:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
```python from flask import Flask, request from flask_restful import Resource, Api, reqparse from resources import Tokens, TodoItems, Users import cerbos import uuid def get(id): if not TODO.__authenticate(request.headers["token"]): return {'message': 'API key invalid'}, 401 # Initialize cerbos client client = cerbos.Client(host=CERBOS_URL) # initialize the resource instance which is being accessed todo_request = cerbos.ResourceInstance( attr=TodoItems[id] ) # Initialize the principal userId = Tokens[request.headers["token"]] user = Users[userId] principal = cerbos.Principal( id=userId, roles=[user["role"]], attr=user, ) # Check the user is permitted to access the resource if not TODO.check(client, principal, "view", todo_request): return { 'data': 'Not authorized' }, 401 return {'data': id}, 200 def check( client: cerbos.Client, principal: cerbos.Principal, action: str, resource: cerbos.ResourceInstance, ): # Build the Cerbos request request = cerbos.CheckResourceSetRequest( request_id=str(uuid.uuid4()), actions=[action], principal=principal, resource=cerbos.ResourceSet( kind="todo", instances={resource.attr["id"]: resource} ), ) try: # Make a Cerbos request response = client.check_resource_set(request) # Check whether the Cerbos response indicates that this action is allowed return response.is_allowed(resource.attr["id"], action) except cerbos.ClientException as e: print(f"Request failed: {e.msg}") ``` |
> Note: The generic implementation for Cerbos check and calls can be found in the GitHub repo.
In the resource.py
file, add the Users
resource and the TodoItems
resource.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
```python TodoItems = { "1": { "id": "1", "user": "1", "title": "learn python", "status": "completed", "public": "false" }, "2": { "id": "2", "user": "1", "title": "learn flask", "status": "in_progress", "public": "false" }, "3": { "id": "3", "user": "1", "title": "learn cerbos", "status": "in_progress", "public": "false" }, "4": { "id": "4", "user": "2", "title": "learn cerbos", "status": "in_progress", "public": "false" }, "5": { "id": "5", "user": "1", "title": "learn cerbos", "status": "in_progress", "public": "true" } } Users = { "1": { 'role': 'user', }, "2": { 'role': 'user', }, "3": { 'role': 'admin', } } ``` |
These resources would ideally be in a database, but for simplicity, I am adding them to a file.
Now try to access the TODO resource with the token of a user. I am using Postman to make the requests.
When User 2 Tries to Access Resource 4
Resource 4 belongs to user 2, so the user should be able to access the resource:
Note: Clicking images opens a new link to a larger size.
When User 2 Tries to Access Resource 5
Resource 5 belongs to user 1 but is a public resource. In the policy file, I have defined the policy to “view” public resources to everyone, so users will be able to access it:
When User 2 Tries to Access Resource 3
Resource 3 belongs to user 1 and is not a public resource, so the user will not be able to access it:
Similarly, you can add the check to all the remaining endpoints and validate the authorization of the resources. The complete code can be found in the GitHub repo. You can play around by changing the policies and see how it changes the resource access permission.
Conclusion
While designing your API, permission management is a key functionality to keep in mind for the security of the resources. If any permission checks are missing, you will be compromising the data of your platform. It’s important to have a proper permission check for each resource, but you also don’t want to spend a lot of time thinking about permissions while developing each API. The permission check should be easy to implement and easy to test.
Using an external provider like Cerbos helps to centralize permission management in an easy-to-understand format. This solves the access permissions problem for developers and non-technical users alike.