This document explains how access control works in the system.
Access is controlled at three levels:
- System Roles - Global or work-type-scoped admin/approver access
- Memberships - Department or division membership with work type scoping
- Request-Level - Checkout locks for concurrent edit prevention
To avoid confusion, we use consistent naming throughout the codebase:
| Term | Meaning |
|---|---|
is_super_admin |
User has SUPER_ADMIN role (global admin access) |
is_worktype_admin |
User is admin for a specific work type (SUPER_ADMIN OR WORKTYPE_ADMIN) |
is_budget_admin() |
Convenience function to check budget work type admin status |
Important distinctions:
user_ctx.is_super_admin- Checks if user is a global super adminperms.is_worktype_admin- Checks if user is admin for this specific work type
A super admin is always a worktype admin (for all work types), but a worktype admin is NOT a super admin.
System roles are stored in the UserRole model and managed via Admin → Users.
Full access to everything:
- All departments, all work types
- All admin pages (system config AND work type admin)
- All approval queues
- Can finalize/unfinalize requests
Code check: user_ctx.is_super_admin or is_super_admin()
Admin access for a specific work type:
- See all departments for that work type
- Access admin pages for that work type only (
/admin/budget/) - Budget Admin can configure: Expense Accounts, Approval Groups
- Cannot access system config pages (
/admin/- users, departments, divisions, etc.) - Cannot access other work types
Example: "Budget Admin" can manage all budgets but not contracts.
Code check: is_worktype_admin(user_ctx, work_type_id) or is_budget_admin(user_ctx)
Can review lines routed to specific approval groups:
- Appears in approver dashboard
- Can approve/reject/request info on lines
- Scoped to one or more approval groups
Code check: user_ctx.approval_group_ids contains the relevant group ID
# Check if user is a super admin (respects beta testing role overrides)
is_super_admin() -> bool
# Check actual database role (ignores beta testing overrides)
# Use this only for checking if override is allowed
_has_super_admin_role() -> bool@dataclass
class UserContext:
user_id: str
user: User | None
roles: tuple[str, ...]
is_super_admin: bool # True if SUPER_ADMIN role
approval_group_ids: Set[int] # Approval groups user can review# Check if user is admin for a specific work type
is_worktype_admin(user_ctx: UserContext, work_type_id: int) -> bool
# Convenience: Check if user is budget admin
is_budget_admin(user_ctx: UserContext, work_type_id: int | None = None) -> bool@dataclass
class PortfolioPerms:
can_view: bool # Can see the portfolio
can_edit: bool # Can edit draft requests
can_create_primary: bool # Can create primary request
can_create_supplementary: bool # Can create supplementary
is_worktype_admin: bool # Is admin for THIS work type
@dataclass
class WorkItemPerms:
can_view: bool
can_edit: bool
can_submit: bool
can_add_lines: bool
can_delete: bool
can_checkout: bool
can_checkin: bool
can_request_info: bool
can_respond_to_info: bool
is_worktype_admin: bool # Is admin for THIS work type
is_draft: bool
is_checked_out: bool
is_checked_out_by_current_user: boolMemberships grant department/division access and are scoped by:
- Event Cycle - Access is per-event (SMF2027, MAGStock 2027, etc.)
- Work Type - Access is per-work-type (Budget, Contracts, Supply)
Direct access to one department:
DepartmentMembership
├── user_id
├── department_id
├── event_cycle_id
├── can_view (general flag)
├── can_edit (general flag)
├── is_department_head (informational)
└── work_type_access[] ← Per-work-type permissions
├── BUDGET: can_view=True, can_edit=True
├── CONTRACT: can_view=False, can_edit=False
└── SUPPLY: can_view=True, can_edit=False
Access to ALL departments in a division:
DivisionMembership
├── user_id
├── division_id
├── event_cycle_id
├── can_view
├── can_edit
├── is_division_head
└── work_type_access[] ← Applies to all departments in division
Division membership is useful for:
- Division heads who oversee multiple departments
- Cross-department roles
Important: Just having a membership doesn't grant work type access.
A user must have explicit work type access:
# Check if user can view budgets for TechOps
membership = DepartmentMembership.query.filter_by(
user_id=user.id,
department_id=techops.id,
event_cycle_id=smf2027.id,
).first()
can_view_budget = membership.can_view_work_type(budget_work_type.id)This allows:
- Budget-only access (most common)
- Contracts access for specific people only
- View-only access for oversight roles
Most routes use context builders that handle permission checks:
from app.routes.work.helpers import get_portfolio_context, require_portfolio_view
@work_bp.get("/<event>/<dept>/budget")
def portfolio_landing(event, dept):
ctx = get_portfolio_context(event, dept) # Builds context
perms = require_portfolio_view(ctx) # Aborts 403 if no access
# ... user has access, continuefrom app.routes.admin_final.helpers import require_budget_admin
@admin_final_bp.get("/budget/")
def budget_admin_home():
user_ctx = get_user_ctx()
require_budget_admin(user_ctx) # Aborts 403 if not budget admin
# ... user is budget admin, continue# In a membership context
if membership.can_view_work_type(work_type_id):
# Show the work type
if membership.can_edit_work_type(work_type_id):
# Allow editingAdmin → Users → Edit User
- Check "Super Admin" for full access
- Check work type admin boxes for work-type-scoped admin
- Check approval group boxes for approver access
Admin → Departments → [Department] → Members
Or: Admin → Divisions → [Division] → Members
Each membership form shows:
- General permissions (legacy, informational)
- Work Type Access table with View/Edit checkboxes per work type
Give them:
- Department membership with BUDGET work type access (view + edit)
- No CONTRACT work type access
Give them:
- Division membership with appropriate work type access
Give them:
- APPROVER role for the relevant approval group(s)
- They don't need department membership (approvers see lines routed to their group)
Give them:
- Department membership, is_department_head=True
- BUDGET work type access (view + edit)
- No CONTRACT work type access (or view-only if needed for awareness)
Give them:
- WORKTYPE_ADMIN role for BUDGET work type
- They can access
/admin/budget/and configure Expense Accounts + Approval Groups - They CANNOT access
/admin/(users, departments, divisions, etc.)
SUPER_ADMIN (global)
├─ Can access /admin/ (system config)
├─ Can access all /admin/{worktype}/ pages
├─ Treated as worktype admin for ALL work types
└─ Can use beta testing role override
WORKTYPE_ADMIN (scoped to work type)
├─ Can access /admin/{worktype}/ for their work type
├─ Can configure: Expense Accounts, Approval Groups (budget-specific config)
├─ CANNOT access /admin/ (system config: users, departments, divisions)
├─ Admin for that work type only
└─ Equivalent to dept membership + admin powers for that work type
APPROVER (scoped to approval groups)
├─ Can review lines routed to their groups
├─ Appears in approval dashboard
└─ No admin access
Department/Division Membership
├─ Can view/edit portfolios (per work type access)
├─ Can create/submit requests
└─ Scoped by event cycle and work type
Permission-related changes are logged:
- User role changes (via UserRole)
- Membership changes (via DepartmentMembership, DivisionMembership)
Check config_audit_log table for history.