Runbook: Adding a New Admin Permission
This runbook covers how to add a new permission to the Flask permission system, assign it to roles, and use it in routes and templates. FastAPI does not have a granular permission system yet — see the note at the end.
Overview
Admin permissions in Flask are:
- Defined as string constants in the
Permissionclass (admin_permissions.py) - Stored in the
admin_permissiondatabase table - Assigned to
AdminRolerecords (many-to-many) - Checked via the
@authcheck()decorator on routes and{% perm_check %}in templates
The permission table is auto-synced on app startup — adding a constant to the class is enough to create the database record. You never write a migration for a new permission.
Step 1: Add the Permission Constant
Open seedtrust_flask/seedtrust/models/admin_permissions.py and add your permission to the appropriate category class.
class Permission: class Payments: MAKE_PAYMENTS = "make_payments" VIEW_PAYMENT_HISTORY = "view_payment_history" MY_NEW_PERMISSION = "my_new_permission" # Add hereNaming conventions:
- Use
SCREAMING_SNAKE_CASEfor the constant name - Use
snake_casefor the string value - Group by feature area (Payments, Banking, Case, Disbursements, etc.)
- Use verb + noun:
VIEW_,EDIT_,CREATE_,DELETE_,DOWNLOAD_,UPLOAD_
Step 2: Sync the Database
AdminPermission.sync_permissions() runs automatically when Flask starts. It compares the Permission class constants against the admin_permission table and:
- Adds any new constants as new rows
- Removes rows for constants that no longer exist in the class
To verify the sync worked:
Start the Flask app (uv run st run flask) and check the admin permissions list in the database or admin UI. Your new permission should appear.
If you need to sync without restarting the full app, call AdminPermission.sync_permissions() from a Flask shell:
cd seedtrust_flaskflask shell>>> from seedtrust.models.admin_permissions import AdminPermission>>> AdminPermission.sync_permissions()Step 3: Assign to Roles
New permissions are not assigned to any role by default. Assign via the admin UI or a one-time script.
Via admin UI: Navigate to Admin → Roles, select the appropriate role(s), and check your new permission in the permissions list.
Common role assignments:
| Role | Typical Permissions |
|---|---|
| Payment Manager | All payment + banking permissions |
| Escrow Specialist | Case, DR, document permissions |
| Sales Manager | Case view, reporting permissions |
| Super Admin | All permissions |
If you’re unsure which roles should have the permission, ask the team lead. Assigning too broadly is a security issue; assigning too narrowly blocks legitimate users.
Step 4: Protect a Flask Route
Use the @authcheck() decorator on the route function:
from seedtrust.utils.auth import authcheckfrom seedtrust.models.admin_permissions import Permission
@blueprint.route("/my-feature")@authcheck(user_types=["admin"], perm_check=Permission.MyCategory.MY_NEW_PERMISSION)def my_route(): # g.admin is the current admin user # g.admin.has_permission(Permission.MyCategory.MY_NEW_PERMISSION) is also available ...For API endpoints within Flask, use @api_authcheck() instead — it returns a JSON 403 instead of redirecting:
from seedtrust.utils.auth import api_authcheck
@api_blueprint.route("/my-api-endpoint")@api_authcheck(user_types=["admin"], perm_check=Permission.MyCategory.MY_NEW_PERMISSION)def my_api_endpoint(): ...Do not use inline g.admin.has_permission() checks as the primary guard in route handlers. Always use the decorator — inline checks are inconsistent and easy to miss in code review. Inline checks are acceptable for conditional logic within an already-protected route (e.g., “show extra fields if user has this permission”).
Code:
seedtrust_flask/seedtrust/utils/auth.py
Step 5: Conditionally Render UI in Templates
Use the {% perm_check %} Jinja2 tag to hide UI elements for users without the permission:
{% perm_check Permission.MyCategory.MY_NEW_PERMISSION %} <button>Do the thing</button>{% endperm_check %}
{# Negation is also supported #}{% perm_check not Permission.MyCategory.MY_NEW_PERMISSION %} <p>You don't have access to this feature.</p>{% endperm_check %}Permission is available in all Flask templates without import. The tag simply evaluates to an empty string for users without the permission — the enclosed HTML is not rendered.
Code:
seedtrust_flask/seedtrust/permission_extension.py
Multiple Permission Requirements
To require any one of several permissions (OR logic):
@authcheck( user_types=["admin"], perm_check=[ Permission.Payments.MAKE_PAYMENTS, Permission.Payments.VIEW_PAYMENT_HISTORY, ])To require the user to have any permission in a category (useful for broad access gates):
@authcheck(user_types=["admin"], category_check=Permission.Banking)Testing Your Permission
- Log in as an admin without the permission — confirm the route returns 403 or redirects
- Assign the permission to a test role, log in as an admin with that role — confirm access
- Check the template — confirm the UI element is hidden/shown correctly
FastAPI: No Granular Permissions Yet
FastAPI endpoints currently do not enforce granular Admin permissions. Any authenticated Admin can access any FastAPI Admin endpoint.
If your new FastAPI endpoint requires a specific permission:
- Query the
AdminRole/AdminPermissiontables manually in your endpoint - Document that you’ve done so in a comment
- Add a TODO noting it should use the permission system once one is built
This is a known gap and a planned improvement as FastAPI grows.
See also: Authentication & Permissions