14.1. Users and Permissions
Table of Contents
Understanding Users and Permissions
In authorization, everything starts with who is acting and what they are allowed to do. This chapter focuses on users and permissions as the raw building blocks of authorization. Later chapters will cover more elaborate schemes like roles and advanced access control, so here we will stay close to the basics and focus on what is unique to users and permissions themselves.
Users as Subjects in Your System
In authorization, a user is a subject: someone or something that tries to perform an action on a resource.
Common subjects:
- Human users
- Service accounts (for other services or background jobs)
- API clients (e.g. a third party integration)
In code and in the database, users are usually represented by a user record and a user identifier.
Example user table:
| Column | Type | Example | Purpose |
|---|---|---|---|
| id | UUID / int | 42 or 9b3d-... | Primary key, internal identifier |
| string | alice@example.com | Login / contact | |
| password_hash | string | $2b$12$... | Authentication, not authorization |
| is_active | boolean | true | Can user log in? |
| created_at | timestamp | 2026-08-01 10:30 | Audit / info |
Authentication tells you “This is user #42”. Authorization then needs to decide “What is user #42 allowed to do?”.
What Are Permissions?
A permission describes an allowed action on a type of resource, optionally within some scope.
Typical pieces of a permission:
- Action:
read,create,update,delete,list,approve, etc. - Resource type:
user,order,product,invoice,comment, etc. - Scope: which instances, which organization, which tenant, which project, etc.
You can think of a permission as a simple sentence:
“User mayACTIONRESOURCEinSCOPE.”
Examples:
read:articleupdate:orderdelete:commentapprove:paymentread:report:department:finance
To make this manageable in code and database, it is common to represent permissions using machine friendly strings like:
user.readuser.updateproduct.createinvoice.refund
Or with structured data:
{
"action": "read",
"resource": "user",
"scope": "self"
}Important rule: A permission is always about an action on a resource. If your permission name does not clearly imply an action and a resource, it will quickly become confusing.
Basic Permission Models
Simple Boolean Flags
The simplest form is a boolean flag on the user:
| Column | Type | Meaning |
|---|---|---|
| is_admin | bool | Can perform admin actions |
| can_post | bool | Can create posts |
| can_comment | bool | Can add comments |
Example:
if not current_user.can_post:
raise HTTPException(status_code=403, detail="Posting not allowed")This works for very small applications, but it does not scale:
- Hard to add new permissions without adding new columns
- Combinations become messy
- Not flexible per resource instance
Use it only for very small features or extremely stable rules, like is_superuser.
List of Permission Strings
A flexible approach is to give each user a list of permission strings.
Example:
{
"id": 42,
"email": "alice@example.com",
"permissions": [
"post.create",
"post.update.self",
"comment.create",
"comment.delete.self"
]
}In code:
def has_permission(user, permission: str) -> bool:
return permission in user.permissionsChecking:
if not has_permission(current_user, "post.create"):
raise HTTPException(status_code=403, detail="Not allowed to create posts")This model is:
- Easy to understand
- Easy to store (e.g. separate table or JSON field)
- Easy to check in code
You will later see how to combine this with roles, but the underlying idea of a list of permission strings is the same.
Permissions and Resources
Permissions become meaningful when tied to resources. Two big questions:
- Does a user have permission for this type of resource?
- Does a user have permission for this specific instance of the resource?
Type-Level Permissions
Type-level permissions care only about the kind of resource, not which instance.
Examples:
product.readuser.listreport.generate
Check:
def can_read_products(user):
return "product.read" in user.permissionsInstance-Level Permissions (Ownership)
Often, you want rules like:
- User can edit only their own profile
- User can update only their own posts
- User can view only orders from their organization
In that case, you combine a generic permission with a resource ownership check.
Example database tables:
users
| id | |
|---|---|
| 1 | alice@example.com |
| 2 | bob@example.com |
posts
| id | title | user_id |
|---|---|---|
| 10 | "Hello" | 1 |
| 11 | "Bye" | 2 |
A common rule:
- User needs permission
post.update.self - And must own the post (
post.user_id == current_user.id)
Check in code:
def can_update_post(user, post):
if "post.update.any" in user.permissions:
return True # e.g. moderator
if "post.update.self" in user.permissions and post.user_id == user.id:
return True
return FalseThis simple pattern combines permissions with ownership, a very common requirement in real applications.
Designing Permissions
Designing permissions is a bit like designing a public API. If you are not careful, it can become hard to maintain.
Define Actions Clearly
Pick a small, consistent set of actions and reuse them across resources.
Common actions:
createreadorviewupdateoreditdeletelistexportapprove/rejectassign
Example pattern:
user.readuser.updateuser.deleteorder.readorder.updateorder.refund
Try to avoid inventing new verbs for the same concept. For example, do not mix view, read, and see for the same action.
Design rule: Use a consistent naming convention for permissions, for example:
<resource>.<action>[.<scope>]
Example: post.update.self, order.read.any.
Keep Permissions Close to Use Cases
Do not create permissions for every tiny method or field. Instead, think in use cases.
Example, comment system:
- View a post and its comments:
post.read - Add a comment:
comment.create - Edit own comment:
comment.update.self - Delete comments for moderation:
comment.delete.any
This is enough. You probably do not need comment.update.text_only or comment.update.author_only unless your product really needs it.
Example Permission Matrix
A matrix can help you think about which permissions you need.
Example: Blogging system
| Resource | Action | Permission string | Notes |
|---|---|---|---|
| post | create | post.create | Create a new post |
| post | read | post.read | Read any published post |
| post | update self | post.update.self | Edit own posts |
| post | update any | post.update.any | Moderator or admin |
| post | delete self | post.delete.self | Delete own posts |
| post | delete any | post.delete.any | Moderator or admin |
| comment | create | comment.create | Add comments |
| comment | delete self | comment.delete.self | Delete own comments |
| comment | delete any | comment.delete.any | Moderator |
Once this is defined, your code becomes more structured and readable.
Storing Permissions
You will usually store permissions in the database rather than hardcoding everything.
Separate Permission Table
A normalized design:
permissions
| id | name | description |
|---|---|---|
| 1 | post.create | Create new posts |
| 2 | post.update.self | Update own posts |
| 3 | post.update.any | Update any posts |
user_permissions
| user_id | permission_id |
|---|---|
| 1 | 1 |
| 1 | 2 |
| 2 | 1 |
| 2 | 3 |
This gives you:
- A central place to see all permissions
- Ability to add or remove permissions without changing database schema
- Ability to attach permissions directly to users
Example query to get a user’s permissions:
SELECT p.name
FROM permissions p
JOIN user_permissions up ON p.id = up.permission_id
WHERE up.user_id = 1;You can cache the resulting list in memory or in a token for faster checks.
Embedding Permissions in Tokens
In many backend APIs, you include permissions inside a JWT access token after login. For example:
{
"sub": "42",
"email": "alice@example.com",
"permissions": [
"post.create",
"post.update.self",
"comment.create"
],
"exp": 1724760000
}Then each request does not need to hit the database to check permissions, it can read them from the token. Just remember that changes to permissions might not take effect until old tokens expire.
Checking Permissions in Code
Centralize Permission Checks
You should avoid sprinkling string checks all over the code without structure.
Bad:
if "post.update.self" in current_user.permissions or "post.update.any" in current_user.permissions:
# editing logic
else:
raise HTTPException(403)Better, define helper functions:
def has_permission(user, permission: str) -> bool:
return permission in user.permissions
def require_permission(user, permission: str):
if not has_permission(user, permission):
raise HTTPException(status_code=403, detail="Forbidden")Use it:
@app.post("/posts")
def create_post(post_in: PostCreate, user: User = Depends(get_current_user)):
require_permission(user, "post.create")
# create postCombine Permission and Ownership Checks
Example with FastAPI style:
from fastapi import Depends, HTTPException
def can_edit_post(user, post) -> bool:
if "post.update.any" in user.permissions:
return True
if "post.update.self" in user.permissions and post.user_id == user.id:
return True
return False
@app.put("/posts/{post_id}")
def update_post(
post_id: int,
post_in: PostUpdate,
user: User = Depends(get_current_user),
):
post = get_post_by_id(post_id)
if post is None:
raise HTTPException(status_code=404, detail="Post not found")
if not can_edit_post(user, post):
raise HTTPException(status_code=403, detail="Not allowed to edit this post")
# perform updateThis keeps your authorization logic clear and testable.
Service Accounts and Non Human Users
Not all users are people. A common pattern is service accounts or API clients.
Examples:
- Background worker that sends emails
- Script that runs reports nightly
- Third party app that uses your API
These often:
- Do not have a password but use tokens or keys
- Have very specific permissions
Example service account:
{
"id": 1001,
"name": "report-generator",
"type": "service",
"permissions": [
"report.generate",
"report.read"
]
}In code, you can treat them like any other user, but possibly with an extra flag:
if current_user.type == "service" and "user.update" in current_user.permissions:
# maybe restrict some actions furtherKeep the same permission concepts so your authorization logic is consistent.
Example: Designing Users and Permissions for a Simple API
Imagine you are building a Task Management API with:
- Users
- Tasks owned by users
- Admins who can see and manage all tasks
Resources
usertask
Actions
create,read,update,delete,list
Permissions
| Resource | Action | Permission | Who gets it |
|---|---|---|---|
| task | create | task.create | All regular users, admins |
| task | read self | task.read.self | All regular users |
| task | read any | task.read.any | Admins |
| task | update self | task.update.self | All regular users |
| task | update any | task.update.any | Admins |
| task | delete self | task.delete.self | All regular users |
| task | delete any | task.delete.any | Admins |
| user | list | user.list | Admins only |
| user | read self | user.read.self | Every user |
| user | update self | user.update.self | Every user |
Example Checks
List user’s own tasks:
@app.get("/tasks")
def list_my_tasks(user: User = Depends(get_current_user)):
require_permission(user, "task.read.self")
return get_tasks_by_user_id(user.id)Admin listing all users:
@app.get("/admin/users")
def list_users(user: User = Depends(get_current_user)):
require_permission(user, "user.list")
return get_all_users()Updating a task:
@app.put("/tasks/{task_id}")
def update_task(
task_id: int,
task_in: TaskUpdate,
user: User = Depends(get_current_user),
):
task = get_task_by_id(task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
if "task.update.any" in user.permissions:
pass # ok, admin
elif "task.update.self" in user.permissions and task.owner_id == user.id:
pass # ok, owner
else:
raise HTTPException(status_code=403, detail="Not allowed to update this task")
# perform updateThis small example shows how user records and permission strings combine to form a clear and testable authorization layer.
Good Practices for Users and Permissions
- Least privilege:
Give users only the permissions they really need. - Centralized logic:
Keep permission names and checks in a few places, not scattered everywhere. - Consistent naming:
Stick to a naming pattern likeresource.action.scope. - Test your rules:
Write tests for permission logic, especially when you add or change permissions. - Plan for growth:
Start simple, but design so you can later introduce roles or more advanced models without changing everything.
Key statement: Authorization is easiest to maintain when you treat permissions as a clear, stable contract between your business rules and your code.
Later chapters will build on this foundation to introduce role based access control and more advanced authorization models, but the core concepts of users and permissions remain the same.
Views: 8
KAHIBARO