Add RBAC and resource-level permissions to a FastAPI app
Short answer: To add RBAC and resource-level permissions to a FastAPI app, verify the JWT in a dependency and call await permit.check() with the user, an action, and a resource type or instance. Use it when roles must change without a redeploy, or when users hold rights on single objects. Don't use it when one fixed role check in a single service is enough.
| Fact | Value |
|---|---|
| Question this page answers | How do I add fine-grained authorization, RBAC plus resource-level permissions, to a FastAPI app? |
| Models used | RBAC for top-level roles and ReBAC for roles on a single post. ABAC is also supported with a container PDP. |
| PDP | A container PDP at http://localhost:7766 in this tutorial, or the Cloud PDP. |
| SDK | permit on PyPI, asyncio client. The code in this tutorial is written against version 3.0.0, which requires Python 3.10 or later. |
| Policy changes | You change roles and permissions in the Permit dashboard. The FastAPI code and the deployment stay the same. |
| Free tier | Community plan, labeled "Free Forever" on the Permit.io pricing page: MAU 1000, Tenants 20, Authorization Queries No Limit, Environments 3, PDP Instances No Limit. |
| Audit | Each check appears on the Audit Log screen of the Permit dashboard. |
| Open source | The Edge PDP (permitio/PDP), OPAL, cedar-agent, the Permit SDKs, and the Permit CLI are open source. See Open-source fallback. |
Build a FastAPI app for a blogging platform that registers users in Permit.io and allows only users with the Author role to create posts. This tutorial is for Python backend developers who want to enforce Permit.io policies from FastAPI endpoints.
When you finish, your FastAPI app has two endpoints:
| Endpoint | What it does |
|---|---|
POST /register | Syncs a user to Permit.io and assigns the user the Reader role in the default tenant |
POST /posts | Calls permit.check() and returns 403 unless the user has permission to create a Post |
Prerequisites
- A Permit.io account. See Create a Permit.io account.
- Python 3 with FastAPI and the Uvicorn server installed.
- Node.js and npm, to install the Permit CLI.
- Docker, to run the policy decision point (PDP) container.
1. Configure the policy in Permit
Create the blogging platform policy with the Permit CLI. If your environment already has a policy with a Post resource and an Author role that can create posts, skip to 2. Get your API key.
Install the Permit CLI
The Permit CLI creates policies and runs the PDP from your terminal. Install the CLI with npm:
npm install -g @permitio/cli
Run permit to confirm that the CLI is installed.
Sign in with the Permit CLI
Authenticate the CLI with your Permit.io account:
permit login
The command opens a browser window where you sign in. After you sign in, the CLI uses your default environment. To use a different environment, run permit env select and choose the environment.
Apply the blogging platform template
Permit CLI templates create a policy with predefined resources, roles, and rules. To see the available templates, run permit env template list. The template source files are in the Permit CLI repository.

Apply the blogging-platform template to your environment:
permit env template apply --template blogging-platform
The CLI prints a success message when the template is applied.
Review the policy in the Policy Editor
In the Permit dashboard, select your project and open the Policy screen.

The blogging-platform template creates:
| Policy element | What the template defines |
|---|---|
| Resources | Post (with a premium boolean attribute) and Comment, each with create, read, update, and delete actions |
| Roles | Admin (all actions), Author (create and read posts, read comments), Reader (create and read comments), and Premium Reader (read posts and comments) |
| Relationship | A Post is the parent of its Comment instances. An Author of a post instance becomes a Moderator of the comments on that post. This rule is relationship-based access control (ReBAC). |
| Resource set | Free Post contains posts where premium is false. Readers can read free posts. This rule is attribute-based access control (ABAC). |
This tutorial uses one rule from the policy: the Author role can create a Post, and the Reader role cannot. To change which role can perform an action, check or clear the box in the Policy Editor.
2. Get your API key
Your FastAPI app and the PDP authenticate with Permit.io with your environment API key. Copy the API key of the environment where you applied the template. See Get your API key.
Anyone with the environment API key can change that environment's policy through the Permit API. Load the key from an environment variable, and don't commit it.
3. Run the PDP
The PDP evaluates each permit.check() call against your policy. Start a PDP container with the Permit CLI:
permit pdp run
The command starts the PDP in Docker and prints the container ID and name. The PDP listens on port 7766, so your app connects to it at http://localhost:7766.

The Free Post resource set is an ABAC rule, and the Cloud PDP doesn't evaluate ABAC rules, so run the container PDP for this policy. To run the container with docker run instead, or to check that the PDP is healthy, see Run the PDP.
4. Build the FastAPI app
Install the Python SDK
In your FastAPI project, install the Permit Python SDK:
pip install permit
The permit package installs Pydantic with email validation, which the request models in this tutorial use. This tutorial uses the asyncio version of the SDK, which matches FastAPI's async endpoints. For all SDK options, see Check permissions with the Python SDK (asyncio).
Initialize the Permit client in main.py
Create a file named main.py with the following code:
# main.py
from fastapi import FastAPI, Request, status
from permit import Permit
from pydantic import BaseModel, EmailStr, ValidationError
import os
from fastapi.responses import JSONResponse
app = FastAPI()
class UserIn(BaseModel):
email: EmailStr
first_name: str
last_name: str
permit = Permit(
token=os.getenv("PERMIT_API_KEY"),
pdp=os.getenv("PDP_URL"), # Replace with your PDP URL
)
The UserIn Pydantic model validates the body of registration requests. The Permit client reads two environment variables:
| Variable | Value |
|---|---|
PERMIT_API_KEY | Your environment API key from 2. Get your API key |
PDP_URL | The PDP address from 3. Run the PDP: http://localhost:7766 |
Add the /register endpoint
Add the following code to main.py. The register endpoint syncs the user to Permit.io with permit.api.users.sync(), then assigns the user the Reader role in the default tenant with permit.api.users.assign_role().
# main.py
@app.post("/register")
async def register(user_in: UserIn):
try:
user = await permit.api.users.sync(
{
"key": user_in.email,
"email": user_in.email,
"first_name": user_in.first_name,
"last_name": user_in.last_name,
}
)
# Assign role as part of registration
role_assignment = await permit.api.users.assign_role({
"user": user_in.email,
"role": "Reader",
"tenant": "default",
})
return {
"message": "User registered and role assigned",
"user": user,
"role_assignment": role_assignment
}
except ValidationError as e:
return JSONResponse(content=e.errors(), status_code=422)
except Exception as e:
return JSONResponse(content={"error": str(e)}, status_code=500)
The user's email address is the user key in Permit.io. Your app passes the same key to permit.check().
Protect the /posts endpoint with permit.check()
Add the following code to main.py. The posts endpoint asks the PDP whether the user in the request body can create a Post, and returns 403 when the PDP denies the request.
# main.py
# Posts model
class Posts(BaseModel):
user: str
# post creation endpoint
@app.post("/posts")
async def posts(request: Request):
action = 'create'
resource = 'Post'
try:
data = await request.json()
check = Posts(**data)
except ValidationError as e:
return JSONResponse(content=e.errors(), status_code=422)
except Exception as e:
print("Error in posts:", str(e))
return JSONResponse(content={"error": str(e)}, status_code=500)
try:
permitted = await permit.check(check.user, action, resource)
except Exception as e:
print("Error during permission check:", str(e))
return JSONResponse(content={"error": "Permission check failed"}, status_code=500)
if permitted:
return {"message": "User is permitted"}
else:
return JSONResponse(content={"message": "User is not permitted"}, status_code=403)
permit.check() takes the user key, the action, and the resource type, and returns True or False. To protect other endpoints, such as commenting or editing, change the action and resource values.
In a production app, take the user key from your authenticated session. This example reads the user key from the request body so that you can test the endpoint with curl.
Start the FastAPI app
Set the environment variables and start the app with Uvicorn, replacing <YOUR_API_KEY> with your API key:
export PERMIT_API_KEY=<YOUR_API_KEY>
export PDP_URL=http://localhost:7766
uvicorn main:app --port 8000
The app listens on http://localhost:8000.
5. Test the permission check
Register two users, give one of them the Author role, and confirm that the PDP allows only that user to create a post.
Register two users
In a second terminal, register John and Emma:
curl -X POST http://localhost:8000/register \
-H "Content-Type: application/json" \
-d '{"email": "john@example.com", "first_name": "John", "last_name": "Doe"}'
curl -X POST http://localhost:8000/register \
-H "Content-Type: application/json" \
-d '{"email": "emma@example.com", "first_name": "Emma", "last_name": "Den"}'
Each request returns "message": "User registered and role assigned", the synced user under user, and the role assignment under role_assignment, with "role": "Reader" and "tenant": "default". Both users appear in the Directory screen of the Permit dashboard.
Assign John the Author role
Both users have the Reader role, which can't create posts. Give John the Author role in the Permit dashboard:
- Open the Directory screen and select
john@example.comto open the Edit User panel. - Under Permissions Per Tenant, select the Default Tenant.
- In Top Level Access, add the Author role.
- Click Save.

For other ways to assign roles, including the API and SDK, see Sync users.
Check that John can create a post and Emma can't
Send a POST /posts request for John:
curl -X POST http://localhost:8000/posts \
-H "Content-Type: application/json" \
-d '{"user": "john@example.com"}'
The PDP allows the request because John has the Author role. The app returns HTTP 200 with "message": "User is permitted" in the JSON body.
Send the same request for Emma:
curl -X POST http://localhost:8000/posts \
-H "Content-Type: application/json" \
-d '{"user": "emma@example.com"}'
The PDP denies the request because Emma has only the Reader role. The app returns HTTP 403 with "message": "User is not permitted" in the JSON body.
Each check also appears in the Audit Log screen of the Permit dashboard, with the user, action, resource, and decision.
6. Verify JWTs and add resource-level permissions
This section adds a FastAPI dependency that verifies a JWT and calls permit.check() for the route. The sub claim must equal the user key in Permit, which is the email address in this tutorial. The routes use the /api/posts prefix, so they sit next to the /posts route from step 4. They return plain dictionaries.
Verify the token in a dependency
Install PyJWT:
pip install pyjwt
Add current_user to main.py. It returns the sub claim of a valid token and raises 401 when the token is invalid:
# main.py
import os
import uuid
import jwt
from fastapi import Depends, HTTPException, Request
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
bearer = HTTPBearer()
def current_user(creds: HTTPAuthorizationCredentials = Depends(bearer)) -> str:
try:
claims = jwt.decode(creds.credentials, os.environ["JWT_SECRET"], algorithms=["HS256"])
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail="Invalid token")
return claims["sub"]
This example verifies tokens signed with a shared secret. If your identity provider signs tokens with a key pair, pass its public key to jwt.decode() and list the matching algorithm.
Add a dependency that checks permissions
require() returns a dependency for one action and resource type. When the route has a post_id path parameter, the check names that post, so the PDP evaluates the user's roles on that post as well as the top-level roles:
# main.py
def require(action: str, resource_type: str):
async def dependency(request: Request, user: str = Depends(current_user)) -> str:
resource = {"type": resource_type, "tenant": "default"}
if "post_id" in request.path_params:
resource["key"] = request.path_params["post_id"]
if not await permit.check(user, action, resource):
raise HTTPException(status_code=403, detail="Forbidden")
return user
return dependency
Protect the routes and assign the author role
The create route checks the top-level create permission, registers the new post as a resource instance, and assigns the creator the Author role on that post only. The update and delete routes check the specific post:
# main.py
@app.post("/api/posts", status_code=201)
async def create_post(user: str = Depends(require("create", "Post"))):
post_id = str(uuid.uuid4())
await permit.api.resource_instances.create(
{"key": post_id, "resource": "Post", "tenant": "default"}
)
# The creator becomes the Author of this post only
await permit.api.role_assignments.assign(
{"user": user, "role": "Author", "resource_instance": f"Post:{post_id}", "tenant": "default"}
)
return {"id": post_id}
@app.put("/api/posts/{post_id}")
async def update_post(post_id: str, user: str = Depends(require("update", "Post"))):
return {"message": f"Post {post_id} updated"}
@app.delete("/api/posts/{post_id}")
async def delete_post(post_id: str, user: str = Depends(require("delete", "Post"))):
return {"message": f"Post {post_id} deleted"}
The blogging platform template gives the Admin role every action on Post, and it gives the Author role on a post the delete action. To make delete an Admin-only action, clear delete for the Author role on a Post instance in the Policy Editor. The change takes effect without a redeploy.
Test the routes with signed tokens
Stop the app. Set JWT_SECRET in the same shell as PERMIT_API_KEY and PDP_URL, then start the app again. Create a token for John, who has the Author role from step 5:
export JWT_SECRET=$(openssl rand -hex 32)
JOHN=$(python -c "import jwt, os; print(jwt.encode({'sub': 'john@example.com'}, os.environ['JWT_SECRET'], algorithm='HS256'))")
curl -s -X POST http://localhost:8000/api/posts -H "Authorization: Bearer $JOHN"
The response is {"id":"<POST_ID>"}. Create a token for Emma the same way, with sub set to emma@example.com, and send it to PUT /api/posts/<POST_ID>. Emma has the Reader role and no role on John's post, so the response is 403. The same request with John's token returns {"message":"Post <POST_ID> updated"}. A request with an invalid token returns 401.
Frequently asked questions
How do I add RBAC and resource-level permissions to a FastAPI app?
Verify the JWT in a dependency, then call await permit.check(user, action, resource) in a second dependency that each route declares. Use a resource type for RBAC checks and a resource with a key for checks on a single object. Section 6 shows both dependencies and three routes.
What is a resource-level permission in Permit?
A role that applies to one resource instance, such as one post, instead of to every resource of that type. In this tutorial the creator of a post gets the Author role on that post only. This is relationship-based access control (ReBAC). See ReBAC overview.
Does the Permit Python SDK support asyncio?
Yes. The permit package used here is the asyncio client, so permit.check() and permit.api calls use await inside FastAPI's async routes. Version 3.0.0 requires Python 3.10 or later. See Check permissions with the Python SDK (asyncio).
Do I need to redeploy when a role or permission changes?
No. Roles, permissions, and role assignments live in Permit. Change them in the Policy Editor or the Directory, and the next permit.check() call uses the updated policy. Updates reach a PDP in the background, so repeat the request if you see the previous result.
Should I use Permit.io or a FastAPI-only approach?
Use a dependency with your own role table when the rules live in one service and change only with a code release. Use Permit.io when roles change without a redeploy, several services share the rules, or you need an audit trail and a dashboard. The require() dependency in section 6 is the only place that changes in your FastAPI code.
What does the free tier include?
The Community plan on the Permit.io pricing page lists MAU 1000, Tenants 20, Authorization Queries No Limit, and PDP Instances No Limit.
Next steps
- Check permissions with the Python SDK (asyncio): SDK installation, configuration, and more
permit.check()examples. - Check permissions with permit.check(): check against tenants, resource instances, and attributes.
- Build RBAC policies: create roles and permissions for your own resources.
- Build ABAC policies: write rules based on user and resource attributes, like the Free Post resource set.