14.5. Admin Permissions
Table of Contents
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:
- Deleting users
- Refunding payments
- Changing roles and permissions
- Viewing all customer data
- Managing system configuration
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:
| Role | Typical abilities |
|---|---|
| Guest | Read public data only |
| User | Manage own data, create content |
| Moderator | Moderate user content, ban users, but not manage the system |
| Admin | Manage users, permissions, settings, critical operations |
| Super admin | Full 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:
| Column | Type | Example value |
|---|---|---|
| id | int | 42 |
| text | alice@example.com | |
| password | hash | $2b$12$... |
| role | text | "user" or "admin" |
| is_active | boolean | true |
You can define a small set of allowed values: "user", "admin", "super_admin".
In code, this becomes very simple:
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:
is_admin(boolean)is_staff(boolean)
Example:
| id | is_admin | is_staff | |
|---|---|---|---|
| 1 | admin@example.com | true | true |
| 2 | staff@example.com | false | true |
| 3 | user@example.com | false | false |
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:
userstablerolestable (withname="admin","user","moderator")user_rolestable that connects users to roles
Then you can give multiple roles to one user. Admin is just one of the roles.
Example rows:
roles:
| id | name |
|---|---|
| 1 | user |
| 2 | admin |
| 3 | editor |
user_roles:
| user_id | role_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:
- a role that has all permissions, or
- a shortcut flag
is_super_adminthat bypasses other checks.
For example:
| Permission | Admin has it? |
|---|---|
user.create | yes |
user.delete | yes |
product.update | yes |
system.settings.update | yes |
You can define that:
- Admin role automatically has all permissions.
- Or when checking permissions, if
user.is_administrue, you allow all operations.
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:
POST /admin/userscreate new usersGET /admin/userslist all usersDELETE /admin/users/{user_id}delete any userPOST /admin/productscreate productsGET /admin/statsview system statisticsPATCH /admin/settingschange application configuration
All these need additional protection beyond "user is logged in".
Basic admin check pattern
Conceptually, admin protection is:
- Authenticate the user.
- Load the user record.
- Check if the user is admin.
- If not, return
403 Forbidden. - If yes, continue the handler.
Example in pseudocode:
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:
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 ContentFastAPI-style example
Assume you already have authentication and a get_current_user dependency:
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_userThen an admin route:
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:
- The route does not trust the frontend.
- The route does not check
X-Admin: trueheaders or anything the client sets. - It only trusts the authenticated user record from the database or token.
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:
{
"sub": "42",
"email": "admin@example.com",
"role": "admin",
"exp": 1729614941
}The important part:
- The JWT is signed by your backend.
- The client cannot modify the payload without breaking the signature.
- When the backend verifies the token, it can safely read
"role": "admin".
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:
- Either keep JWT lifetimes short.
- Or load the user from the database and trust the database role, not the token role.
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:
- Regular API:
/api/v1/... - Admin API:
/api/v1/admin/...
Examples:
| Regular endpoint | Admin endpoint |
|---|---|
GET /api/v1/products | POST /api/v1/admin/products |
GET /api/v1/orders/me | GET /api/v1/admin/orders |
PATCH /api/v1/users/me | PATCH /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:
- Everything under
/adminis clearly sensitive. - You can apply special middleware or rate limiting to
/admin/*. - You can log admin actions separately.
Separate admin frontend
Often you have:
- Public frontend:
https://example.com/ - Admin dashboard:
https://admin.example.com/orhttps://example.com/admin/
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:
- Hide the "Delete user" button in the UI for non-admins.
- Do not enforce any admin check in the backend.
Problem:
- A non-admin user can still send an HTTP
DELETE /admin/users/123request manually with curl, Postman, or the browser console. - Because the backend does not check admin role, the request succeeds.
Always implement permission checks on the server side.
Relying on a client-provided header or field
Bad pattern:
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:
- Ignore any role fields coming from the client.
- Only use roles that come from a trusted source:
- The user record in your database, or
- The claims in a verified JWT token.
Missing check inside business logic
Sometimes you protect the endpoint but forget to protect the internal service function.
Example:
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:
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:
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:
- Admin A removes its own admin role by accident.
- There is no other admin.
- No one can promote any user to admin again.
To avoid this:
- Have at least two accounts with super admin rights.
- Or disallow:
- Removing the last admin role.
- An admin from removing its own admin role without confirmation.
Example logic:
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:
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:
- Which admin did what
- When they did it
- From where (IP, device)
This is important for:
- Debugging mistakes
- Detecting malicious admin behavior
- Compliance and regulations (for example GDPR, financial systems)
What to log
You can log at least:
admin_user_idaction(for example"DELETE_USER")target(for example"user_id=123")timestampip_address
Simple example:
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=10In code, you might have a helper:
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:
@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 type | Description |
|---|---|
| Content admin | Manage posts, comments, media |
| Support admin | View user accounts, reset passwords, but not delete users |
| Finance admin | View and manage payments and invoices |
| System admin | Manage 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:
user.readuser.writeuser.deletesettings.updatepayment.refundrole.assign
Then you can give:
- Support admin:
user.read,user.write,password.reset - System admin:
settings.update,role.assign, everything else
Admin is then just a role that has many permissions.
Scoping admin actions
Sometimes admin should have power but still not unlimited.
Examples:
- Support admin can see last 4 digits of a credit card only, not the full number.
- Content admin can delete posts but not delete user accounts.
- Admin can see full logs but not raw passwords (you should not store them anyway).
Design with the principle of least privilege:
- Give the minimum rights needed to do the job.
- Split powerful actions into several smaller permissions.
Admin actions that need confirmation
Some operations are so dangerous that even an admin should have an extra confirmation or step, for example:
- Deleting the entire database
- Disabling login for all users
- Changing encryption keys
Possible safeguards:
- Second admin confirmation
- Extra password prompt for the admin
- Time lock, for example request now and execute after some delay
- Email notification to all super admins when such actions happen
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
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
- Regular users:
- Can create tasks with
owner_id = their own id - Can read and edit only their own tasks
- Admin users:
- Can read and edit any task
- Can list all tasks
Endpoints
GET /tasks:- Regular user: see only own tasks.
- Admin: see all tasks.
DELETE /tasks/{task_id}:- Regular user: delete only own tasks.
- Admin: delete any task.
GET /admin/users:- Admin only: list all users.
Implementation idea
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:
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:
- Use the user's role.
- Change behavior for admin versus non-admin.
- For admin-only zones, completely reject non-admins with
403 Forbidden.
Summary
Admin permissions are a special type of authorization that protect the most powerful and sensitive operations in your backend.
Key points to remember:
- Store admin status securely, usually with roles or permissions.
- Never trust the frontend or client-sent role data.
- Protect admin endpoints by:
- Authenticating the user.
- Verifying their admin role or permissions.
- Returning
403 Forbiddenwhen they lack rights. - Group admin features logically, often under
/adminprefixes and in separate dashboards. - Log admin actions for auditing and debugging.
- Use least privilege, split admin responsibilities into roles when necessary.
- Add extra safeguards for extremely dangerous actions.
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
KAHIBARO