KAHIBARO
Discord Login Register

14.1. Users and Permissions

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:

In code and in the database, users are usually represented by a user record and a user identifier.

Example user table:

ColumnTypeExamplePurpose
idUUID / int42 or 9b3d-...Primary key, internal identifier
emailstringalice@example.comLogin / contact
password_hashstring$2b$12$...Authentication, not authorization
is_activebooleantrueCan user log in?
created_attimestamp2026-08-01 10:30Audit / 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:

You can think of a permission as a simple sentence:

“User may ACTION RESOURCE in SCOPE.”

Examples:

To make this manageable in code and database, it is common to represent permissions using machine friendly strings like:

Or with structured data:

json
{
  "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:

ColumnTypeMeaning
is_adminboolCan perform admin actions
can_postboolCan create posts
can_commentboolCan add comments

Example:

python
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:

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:

json
{
  "id": 42,
  "email": "alice@example.com",
  "permissions": [
    "post.create",
    "post.update.self",
    "comment.create",
    "comment.delete.self"
  ]
}

In code:

python
def has_permission(user, permission: str) -> bool:
    return permission in user.permissions

Checking:

python
if not has_permission(current_user, "post.create"):
    raise HTTPException(status_code=403, detail="Not allowed to create posts")

This model is:

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:

  1. Does a user have permission for this type of resource?
  2. 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:

Check:

python
def can_read_products(user):
    return "product.read" in user.permissions

Instance-Level Permissions (Ownership)

Often, you want rules like:

In that case, you combine a generic permission with a resource ownership check.

Example database tables:

users

idemail
1alice@example.com
2bob@example.com

posts

idtitleuser_id
10"Hello"1
11"Bye"2

A common rule:

Check in code:

python
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 False

This 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:

Example pattern:

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:

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

ResourceActionPermission stringNotes
postcreatepost.createCreate a new post
postreadpost.readRead any published post
postupdate selfpost.update.selfEdit own posts
postupdate anypost.update.anyModerator or admin
postdelete selfpost.delete.selfDelete own posts
postdelete anypost.delete.anyModerator or admin
commentcreatecomment.createAdd comments
commentdelete selfcomment.delete.selfDelete own comments
commentdelete anycomment.delete.anyModerator

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

idnamedescription
1post.createCreate new posts
2post.update.selfUpdate own posts
3post.update.anyUpdate any posts

user_permissions

user_idpermission_id
11
12
21
23

This gives you:

Example query to get a user’s permissions:

sql
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:

json
{
  "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:

python
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:

python
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:

python
@app.post("/posts")
def create_post(post_in: PostCreate, user: User = Depends(get_current_user)):
    require_permission(user, "post.create")
    # create post

Combine Permission and Ownership Checks

Example with FastAPI style:

python
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 update

This 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:

These often:

Example service account:

json
{
  "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:

python
if current_user.type == "service" and "user.update" in current_user.permissions:
    # maybe restrict some actions further

Keep 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:

Resources

Actions

Permissions

ResourceActionPermissionWho gets it
taskcreatetask.createAll regular users, admins
taskread selftask.read.selfAll regular users
taskread anytask.read.anyAdmins
taskupdate selftask.update.selfAll regular users
taskupdate anytask.update.anyAdmins
taskdelete selftask.delete.selfAll regular users
taskdelete anytask.delete.anyAdmins
userlistuser.listAdmins only
userread selfuser.read.selfEvery user
userupdate selfuser.update.selfEvery user

Example Checks

List user’s own tasks:

python
@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:

python
@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:

python
@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 update

This small example shows how user records and permission strings combine to form a clear and testable authorization layer.


Good Practices for Users and Permissions

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

Comments

Please login to add a comment.

Don't have an account? Register now!