KAHIBARO
Discord Login Register

14.5. Admin Permissions

Why Admin Permissions Exist

In most real applications not every authenticated user should be allowed to do everything. Some actions are dangerous or sensitive, for example:

These actions are usually reserved for administrators.

Admin permissions are a specific case of authorization. You decide which users are "admins" and you enforce that only they can call certain endpoints or perform certain operations.

Typical user groups:

RoleTypical abilities
GuestRead public data only
UserManage own data, create content
ModeratorModerate user content, ban users, but not manage the system
AdminManage users, permissions, settings, critical operations
Super adminFull system control, including other admins

Admin permissions are usually modeled on top of your existing role or permission system, not as a separate concept.

Key rule: Never rely only on the frontend to hide admin features. Every admin-only capability must also be protected on the backend.

If the JavaScript can see it, a user can usually trigger it. The backend must always check permissions independently.


Representing Admin Permissions

You need a way to store the information that a user is an admin and to use it in authorization checks.

Common ways to store admin status

1. Role field on the user

The most common solution is to store a "role" field on the user.

Example user table:

ColumnTypeExample value
idint42
emailtextalice@example.com
passwordhash$2b$12$...
roletext"user" or "admin"
is_activebooleantrue

You can define a small set of allowed values: "user", "admin", "super_admin".

In code, this becomes very simple:

python
class Role(str, Enum):
    USER = "user"
    ADMIN = "admin"
    SUPER_ADMIN = "super_admin"

Then each user has a role column that uses one of these.

2. Explicit boolean flag

Instead of a text role, you can have fields like:

Example:

idemailis_adminis_staff
1admin@example.comtruetrue
2staff@example.comfalsetrue
3user@example.comfalsefalse

This is easy if your app is small. For complex systems with many roles, this becomes hard to maintain.

3. Role table and many-to-many relation

For more flexibility, you can have:

Then you can give multiple roles to one user. Admin is just one of the roles.

Example rows:

roles:

idname
1user
2admin
3editor

user_roles:

user_idrole_id

| 1 | 2 | user 1 is admin
| 1 | 1 | and user
| 2 | 1 | user 2 is only user

This fits naturally into a permission system where admin is a role with special rights.

4. Permission-based admin

In a permission-based system, admin can be:

For example:

PermissionAdmin has it?
user.createyes
user.deleteyes
product.updateyes
system.settings.updateyes

You can define that:

Important: Avoid mixing too many patterns. Pick a primary model, for example a role field or a role table, and build on that. Adding extra flags everywhere often leads to inconsistent behavior.


Protecting Admin Endpoints

Admin permissions matter most when you protect backend routes that can change a lot of data or configuration.

Example admin-only endpoints

Typical admin endpoints in a REST API:

All these need additional protection beyond "user is logged in".

Basic admin check pattern

Conceptually, admin protection is:

  1. Authenticate the user.
  2. Load the user record.
  3. Check if the user is admin.
  4. If not, return 403 Forbidden.
  5. If yes, continue the handler.

Example in pseudocode:

pseudo
function require_admin(request):
    user = authenticate(request)
    if user is None:
        return 401 Unauthorized
    if not user.is_admin:
        return 403 Forbidden
    return user

Then each admin endpoint uses require_admin:

pseudo
function delete_user_endpoint(request, user_id):
    admin = require_admin(request)
    if is_error(admin):
        return admin   # 401 or 403
    delete_user_by_id(user_id)
    return 204 No Content

FastAPI-style example

Assume you already have authentication and a get_current_user dependency:

python
from fastapi import Depends, HTTPException, status
def require_admin(current_user = Depends(get_current_user)):
    if current_user.role != "admin":
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Admin privileges required",
        )
    return current_user

Then an admin route:

python
from fastapi import APIRouter
router = APIRouter(prefix="/admin", tags=["admin"])
@router.get("/users")
def list_all_users(admin = Depends(require_admin)):
    # admin is the current user with admin role
    return get_all_users()

Points to notice:

Never trust client claims like "role": "admin" sent directly from the browser. The backend must validate admin status from a secure source, such as a signed token or database lookup.

Using JWTs with admin roles

If you use JWTs, you usually store the user's role inside the token payload:

json
{
  "sub": "42",
  "email": "admin@example.com",
  "role": "admin",
  "exp": 1729614941
}

The important part:

Then your get_current_user (or similar) reads the role from the JWT, and require_admin checks that the role is "admin".

If your system lets users change role in the database while tokens are still valid, you might want to:

Admin Areas vs Regular Areas

It is very common to separate admin functionality into an "Admin Area" both in the API and in the frontend.

URL separation

You can group admin endpoints under a specific prefix:

Examples:

Regular endpointAdmin endpoint
GET /api/v1/productsPOST /api/v1/admin/products
GET /api/v1/orders/meGET /api/v1/admin/orders
PATCH /api/v1/users/mePATCH /api/v1/admin/users/{user_id}
DELETE /api/v1/orders/{order_id}POST /api/v1/admin/orders/{id}/refund

This makes your API more understandable:

Separate admin frontend

Often you have:

The admin frontend might be a separate Single Page Application that calls only admin API routes.

Even if you use separate domains or paths for the UI, you must still protect the admin API routes with proper authorization on the backend.


Common Admin Pitfalls

Admin permissions are powerful and often poorly implemented. Here are typical mistakes and how to avoid them.

Relying on the frontend

Bad pattern:

Problem:

Always implement permission checks on the server side.

Relying on a client-provided header or field

Bad pattern:

http
DELETE /admin/users/123
X-User-Role: admin

If your backend uses the X-User-Role header as the source of truth, then any client can become an admin by sending that header.

Correct approach:

Missing check inside business logic

Sometimes you protect the endpoint but forget to protect the internal service function.

Example:

python
def delete_user(user_id: int):
    # Dangerous operation
    db.delete(User).where(User.id == user_id)

If you call delete_user from an admin endpoint, you might think you are safe. But later someone might reuse delete_user from a non-admin endpoint by mistake.

Safer pattern:

python
def delete_user_as_admin(current_user, user_id: int):
    if not current_user.is_admin:
        raise PermissionError("Admin required")
    db.delete(User).where(User.id == user_id)

Or require admin in the function signature:

python
def delete_user_as_admin(admin_user: AdminUser, user_id: int):
    # Type or class ensures this is admin
    ...

In small projects you can keep checks in the route handlers. In larger systems it is often better to put permission checks inside the service layer too.

Super admin and self-lockout

If you only have one admin user or one "super admin" and you remove its admin role, you can lock yourself out of the system.

Common mistake:

To avoid this:

Example logic:

pseudo
if current_user.id == target_user.id and current_user.role == "super_admin":
    if count_super_admins() == 1:
        disallow("Cannot remove last super admin")

Insecure "temporary" admin logic

Developers sometimes add temporary shortcuts during development, for example:

python
def require_admin(user):
    # TODO: remove this in production
    if user.email.endswith("@mycompany.com"):
        return user
    if user.role != "admin":
        raise Forbidden

If you forget to remove this before production, anyone who can register with a @mycompany.com email becomes admin.

Avoid these shortcuts entirely, or guard them with strict feature flags or environment variables that are not enabled in production.


Auditing and Logging Admin Actions

Admin actions are high impact. You usually want to know:

This is important for:

What to log

You can log at least:

Simple example:

text
2026-08-27T12:00:34Z admin_id=1 action=DELETE_USER target_user_id=123 ip=192.168.1.10
2026-08-27T12:05:10Z admin_id=1 action=UPDATE_SETTINGS key=max_login_attempts value=10

In code, you might have a helper:

python
def log_admin_action(admin_user, action: str, target: str = ""):
    logger.info(
        "admin_action",
        extra={
            "admin_id": admin_user.id,
            "action": action,
            "target": target,
        },
    )

Then in an endpoint:

python
@router.delete("/users/{user_id}")
def delete_user(user_id: int, admin = Depends(require_admin)):
    delete_user_by_id(user_id)
    log_admin_action(admin, action="DELETE_USER", target=f"user_id={user_id}")
    return Response(status_code=204)

Rule: Any operation that changes many records or system configuration should be logged with the admin identity and action details.


Designing Admin Permissions Safely

Admin permissions are about scope and limits. Full "god mode" admin might not always be necessary.

Levels of admin

You can define several admin types:

Admin typeDescription
Content adminManage posts, comments, media
Support adminView user accounts, reset passwords, but not delete users
Finance adminView and manage payments and invoices
System adminManage configuration, roles, and other admins

This is often easier to manage with roles and permissions, not just a single boolean is_admin.

Examples of permission strings:

Then you can give:

Admin is then just a role that has many permissions.

Scoping admin actions

Sometimes admin should have power but still not unlimited.

Examples:

Design with the principle of least privilege:

Admin actions that need confirmation

Some operations are so dangerous that even an admin should have an extra confirmation or step, for example:

Possible safeguards:

These workflows may be complex but they greatly reduce the risk of accidental catastrophe.


Simple Example: Admin Protection in a Task API

To connect concepts with a concrete example, imagine a simple task management API.

Data model

python
class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    email = Column(String, unique=True, nullable=False)
    password_hash = Column(String, nullable=False)
    role = Column(String, nullable=False, default="user")  # "user" or "admin"
class Task(Base):
    __tablename__ = "tasks"
    id = Column(Integer, primary_key=True)
    owner_id = Column(Integer, ForeignKey("users.id"), nullable=False)
    title = Column(String, nullable=False)
    description = Column(Text)

Permissions

Endpoints

Implementation idea

python
def get_tasks(current_user):
    if current_user.role == "admin":
        return db.query(Task).all()
    else:
        return db.query(Task).filter(Task.owner_id == current_user.id).all()

And admin-only endpoint:

python
def require_admin(current_user):
    if current_user.role != "admin":
        raise Forbidden("Admin privileges required")
    return current_user
def list_users(admin):
    return db.query(User).all()

In a real FastAPI application you would connect these with dependencies, but the core idea is:

Summary

Admin permissions are a special type of authorization that protect the most powerful and sensitive operations in your backend.

Key points to remember:

If you design admin permissions carefully, you reduce the risk from both attackers and innocent mistakes, while still giving administrators the tools they need to manage the system.

Views: 7

Comments

Please login to add a comment.

Don't have an account? Register now!