Files
Budget_Tool_Martinhal/backend/app/permissions.py
T

96 lines
3.9 KiB
Python
Raw Normal View History

2026-10-29 20:40:02 +00:00
"""Who may do what with a department's budget.
Everyone signed in can view every budget. Changing and approving is limited to the budget's own
department, following the workflow the administrator set up for it. Upper Management only views,
downloads and emails budgets and reports, in every department.
"""
from fastapi import HTTPException
from .models import BudgetYear, Department, User, WorkflowStep
from .security import ROLES
def rank(user: User) -> int:
return ROLES.index(user.role)
def is_admin(user: User) -> bool:
return user.role == "admin"
def is_management(user: User) -> bool:
return user.role == "management"
def member(user: User, department_id: int) -> bool:
return user.department_id == department_id
def can_edit_department(user: User, department_id: int) -> bool:
"""Create, import and edit draft budgets of the department."""
return is_admin(user) or (member(user, department_id) and rank(user) >= ROLES.index("editor"))
def steps_of(department: Department) -> list[WorkflowStep]:
return sorted(department.steps, key=lambda s: (s.position, s.id))
def can_approve_step(user: User, step: WorkflowStep | None, department_id: int) -> bool:
if step is None:
# No workflow configured: the department's approvers sign off in one step.
return member(user, department_id) and rank(user) >= ROLES.index("approver")
if step.approver_mode == "users":
return user.id in (step.approver_ids or []) and not is_management(user)
return member(user, department_id) and rank(user) >= ROLES.index("approver")
def current_step(budget: BudgetYear) -> WorkflowStep | None:
steps = steps_of(budget.department)
if budget.status != "in_review" or not steps:
return None
index = min(budget.current_step or 0, len(steps) - 1)
return steps[index]
def can_reopen(user: User, budget: BudgetYear) -> bool:
return is_admin(user) or (member(user, budget.department_id) and rank(user) >= ROLES.index("approver"))
def pending_reopen(budget: BudgetYear):
return next((r for r in budget.reopen_requests if r.status == "pending"), None)
def allowed_actions(user: User, budget: BudgetYear) -> dict:
if budget.archived:
# a replaced version: read-only for everyone; administrators may still delete it
return {"edit": False, "submit": False, "approve": False, "return": False, "reopen": False,
"request_reopen": False, "decide_reopen": False, "upload": False, "delete": is_admin(user)}
dept = budget.department_id
editor = can_edit_department(user, dept)
approver = budget.status == "in_review" and can_approve_step(user, current_step(budget), dept)
return {
"edit": budget.status == "draft" and editor,
"submit": budget.status == "draft" and editor,
"approve": approver,
"return": approver or (budget.status == "in_review" and is_admin(user)),
"reopen": budget.status == "approved" and can_reopen(user, budget),
# anyone may ask for an approved budget to be reopened (approvers reopen it straight away)
"request_reopen": budget.status == "approved" and pending_reopen(budget) is None and not is_management(user),
"decide_reopen": budget.status == "approved" and pending_reopen(budget) is not None and can_reopen(user, budget),
"upload": budget.status == "draft" and editor,
# Deleting a budget removes it and its reports for everyone, so only administrators may, in any status.
"delete": is_admin(user),
}
def require_action(user: User, budget: BudgetYear, action: str) -> None:
if not allowed_actions(user, budget)[action]:
if action == "edit" and budget.status != "draft":
raise HTTPException(409, "year_not_draft")
raise HTTPException(403, "forbidden")
def require_department_editor(user: User, department_id: int) -> None:
if not can_edit_department(user, department_id):
raise HTTPException(403, "forbidden")