From 3afbbc2a88dcb6c27fca9ceb5d5362dd4c68be26 Mon Sep 17 00:00:00 2001 From: zhi Date: Tue, 17 Mar 2026 15:03:48 +0000 Subject: [PATCH] feat(P2.1): register 9 new permissions (milestone/task/propose actions) + wire check_permission in all action endpoints - Add milestone.freeze/start/close, task.close/reopen_closed/reopen_completed, propose.accept/reject/reopen to DEFAULT_PERMISSIONS - Replace placeholder check_project_role with check_permission in proposes.py accept/reject/reopen - Replace freeform permission strings with dotted names in milestone_actions.py - Add task.close and task.reopen_* permission checks in tasks.py transition endpoint - Admin role auto-inherits all new permissions via init_wizard --- app/api/routers/milestone_actions.py | 6 +++--- app/api/routers/proposes.py | 11 ++++------- app/api/routers/tasks.py | 8 +++++++- app/init_wizard.py | 12 ++++++++++++ 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/app/api/routers/milestone_actions.py b/app/api/routers/milestone_actions.py index a307f6e..86387e8 100644 --- a/app/api/routers/milestone_actions.py +++ b/app/api/routers/milestone_actions.py @@ -156,7 +156,7 @@ def freeze_milestone( - Caller must have ``freeze milestone`` permission. """ check_project_role(db, current_user.id, project_id, min_role="mgr") - check_permission(db, current_user.id, project_id, "freeze milestone") + check_permission(db, current_user.id, project_id, "milestone.freeze") ms = _get_milestone_or_404(db, project_id, milestone_id) @@ -222,7 +222,7 @@ def start_milestone( - Caller must have ``start milestone`` permission. """ check_project_role(db, current_user.id, project_id, min_role="mgr") - check_permission(db, current_user.id, project_id, "start milestone") + check_permission(db, current_user.id, project_id, "milestone.start") ms = _get_milestone_or_404(db, project_id, milestone_id) @@ -314,7 +314,7 @@ def close_milestone( - Caller must have ``close milestone`` permission. """ check_project_role(db, current_user.id, project_id, min_role="mgr") - check_permission(db, current_user.id, project_id, "close milestone") + check_permission(db, current_user.id, project_id, "milestone.close") ms = _get_milestone_or_404(db, project_id, milestone_id) current = _ms_status_value(ms) diff --git a/app/api/routers/proposes.py b/app/api/routers/proposes.py index a2829ca..cddcba8 100644 --- a/app/api/routers/proposes.py +++ b/app/api/routers/proposes.py @@ -6,7 +6,7 @@ from sqlalchemy import func as sa_func from app.core.config import get_db from app.api.deps import get_current_user_or_apikey -from app.api.rbac import check_project_role, is_global_admin +from app.api.rbac import check_project_role, check_permission, is_global_admin from app.models import models from app.models.propose import Propose, ProposeStatus from app.models.milestone import Milestone, MilestoneStatus @@ -161,8 +161,7 @@ def accept_propose( if propose_status != "open": raise HTTPException(status_code=400, detail="Only open proposes can be accepted") - # TODO: check 'accept propose' permission once P2 lands - check_project_role(db, current_user.id, project_id, min_role="mgr") + check_permission(db, current_user.id, project_id, "propose.accept") # Validate milestone milestone = db.query(Milestone).filter( @@ -236,8 +235,7 @@ def reject_propose( if propose_status != "open": raise HTTPException(status_code=400, detail="Only open proposes can be rejected") - # TODO: check 'reject propose' permission once P2 lands - check_project_role(db, current_user.id, project_id, min_role="mgr") + check_permission(db, current_user.id, project_id, "propose.reject") propose.status = ProposeStatus.REJECTED db.commit() @@ -266,8 +264,7 @@ def reopen_propose( if propose_status != "rejected": raise HTTPException(status_code=400, detail="Only rejected proposes can be reopened") - # TODO: check 'reopen rejected propose' permission once P2 lands - check_project_role(db, current_user.id, project_id, min_role="mgr") + check_permission(db, current_user.id, project_id, "propose.reopen") propose.status = ProposeStatus.OPEN db.commit() diff --git a/app/api/routers/tasks.py b/app/api/routers/tasks.py index f59fd12..068371f 100644 --- a/app/api/routers/tasks.py +++ b/app/api/routers/tasks.py @@ -14,7 +14,7 @@ from app.schemas import schemas from app.services.webhook import fire_webhooks_sync from app.models.notification import Notification as NotificationModel from app.api.deps import get_current_user_or_apikey -from app.api.rbac import check_project_role, ensure_can_edit_task +from app.api.rbac import check_project_role, check_permission, ensure_can_edit_task from app.services.activity import log_activity router = APIRouter(tags=["Tasks"]) @@ -320,8 +320,14 @@ def transition_task( if task.assignee_id and current_user.id != task.assignee_id: raise HTTPException(status_code=403, detail="Only the assigned user can complete this task") + # P5.5: closing a task requires 'task.close' permission + if new_status == "closed": + check_permission(db, current_user.id, task.project_id, "task.close") + # P5.6: reopen from completed/closed -> open if new_status == "open" and old_status in ("completed", "closed"): + perm_name = "task.reopen_completed" if old_status == "completed" else "task.reopen_closed" + check_permission(db, current_user.id, task.project_id, perm_name) # Clear finished_on on reopen so lifecycle timestamps are accurate task.finished_on = None diff --git a/app/init_wizard.py b/app/init_wizard.py index 5268685..f227036 100644 --- a/app/init_wizard.py +++ b/app/init_wizard.py @@ -109,6 +109,18 @@ DEFAULT_PERMISSIONS = [ ("milestone.read", "View milestones", "milestone"), ("milestone.write", "Edit milestones", "milestone"), ("milestone.delete", "Delete milestones", "milestone"), + # Milestone actions + ("milestone.freeze", "Freeze milestone scope", "milestone"), + ("milestone.start", "Start milestone execution", "milestone"), + ("milestone.close", "Close / abort milestone", "milestone"), + # Task actions + ("task.close", "Close / cancel a task", "task"), + ("task.reopen_closed", "Reopen a closed task", "task"), + ("task.reopen_completed", "Reopen a completed task", "task"), + # Propose actions + ("propose.accept", "Accept a propose into a milestone", "propose"), + ("propose.reject", "Reject a propose", "propose"), + ("propose.reopen", "Reopen a rejected propose", "propose"), # Role/Permission management ("role.manage", "Manage roles and permissions", "admin"), # User management