Authorization logic shouldn't be scattered across your Python
services. In this article, we'll build a complete authorization
service using Open Policy Agent (OPA) and opa-python-client,
from writing your first Rego policy to integrating it with FastAPI.
Why Another Authorization Article?
If you've built more than one backend service, you've probably written
code like this:
if current_user.role == "admin":
return True
if current_user.id == document.owner_id:
return True
raise HTTPException(status_code=403, detail="Forbidden")
Initially, this seems perfectly reasonable.
Then your application grows.
Now you need to support:
- Multiple user roles
- Organization-level permissions
- Temporary permissions
- Country-specific regulations
- Customer-specific rules
- Feature flags
- Auditable authorization decisions
Soon, every service has its own authorization implementation.
Business rules become duplicated.
Changing one permission requires modifying multiple services.
Testing authorization becomes difficult.
This is exactly the problem Open Policy Agent (OPA) solves.
What is Open Policy Agent?
Open Policy Agent is an open-source policy engine.
Instead of embedding authorization logic inside your application, your
application asks OPA a question.
Can Alice read this document?
OPA evaluates your policies written in Rego and returns either:
true
or
false
Your application no longer needs to know how authorization works.
It simply enforces the decision.
Architecture
+------------------+
| FastAPI App |
+------------------+
|
|
Authorization Request
|
▼
+------------------+
| opa-python-client|
+------------------+
|
HTTP API
|
▼
+------------------+
| OPA |
+------------------+
|
Rego Policies
|
Data Documents
Running OPA
docker run \
--name opa \
-p 8181:8181 \
openpolicyagent/opa:latest \
run --server
Installing the Python Client
pip install opa-python-client
Writing Our First Policy
Create authz.rego
package authz
default allow := false
allow if {
input.user.role == "admin"
}
allow if {
input.user.id == input.document.owner
}
Uploading Policies
from opa import OPAClient
client = OPAClient("http://localhost:8181")
with open("authz.rego") as file:
client.create_policy(
policy_name="authz",
policy=file.read(),
)
Evaluating Authorization
decision = client.check_policy(
"authz/allow",
{
"user": {
"id": 10,
"role": "customer"
},
"document": {
"owner": 10
}
}
)
print(decision)
Expected output:
True
Dynamic Data
client.update_data(
"users",
{
"admins": [
"alice",
"bob",
"charlie"
]
}
)
package authz
default allow := false
allow if {
input.user.name in data.users.admins
}
FastAPI Integration
from fastapi import FastAPI, HTTPException
from opa import AsyncOPAClient
app = FastAPI()
client = AsyncOPAClient("http://localhost:8181")
@app.get("/documents/{document_id}")
async def get_document(document_id: int):
payload = {
"user": {
"id": 15,
"role": "customer"
},
"document": {
"owner": 15
}
}
allowed = await client.check_policy(
"authz/allow",
payload
)
if not allowed:
raise HTTPException(
status_code=403,
detail="Forbidden"
)
return {
"id": document_id,
"title": "My Document"
}
Production Recommendations
- Enable HTTPS
- Configure request timeouts
- Use mutual TLS where appropriate
- Store policies in Git
- Test Rego policies
- Separate policies from data
- Monitor policy evaluation latency
Why I Built opa-python-client
The goal of opa-python-client is to provide a clean, Pythonic,
production-ready interface for interacting with Open Policy Agent.
Resources
- GitHub: https://github.com/Turall/OPA-python-client
- Open Policy Agent: https://www.openpolicyagent.org/
Top comments (0)