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
This commit is contained in:
@@ -156,7 +156,7 @@ def freeze_milestone(
|
|||||||
- Caller must have ``freeze milestone`` permission.
|
- Caller must have ``freeze milestone`` permission.
|
||||||
"""
|
"""
|
||||||
check_project_role(db, current_user.id, project_id, min_role="mgr")
|
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)
|
ms = _get_milestone_or_404(db, project_id, milestone_id)
|
||||||
|
|
||||||
@@ -222,7 +222,7 @@ def start_milestone(
|
|||||||
- Caller must have ``start milestone`` permission.
|
- Caller must have ``start milestone`` permission.
|
||||||
"""
|
"""
|
||||||
check_project_role(db, current_user.id, project_id, min_role="mgr")
|
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)
|
ms = _get_milestone_or_404(db, project_id, milestone_id)
|
||||||
|
|
||||||
@@ -314,7 +314,7 @@ def close_milestone(
|
|||||||
- Caller must have ``close milestone`` permission.
|
- Caller must have ``close milestone`` permission.
|
||||||
"""
|
"""
|
||||||
check_project_role(db, current_user.id, project_id, min_role="mgr")
|
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)
|
ms = _get_milestone_or_404(db, project_id, milestone_id)
|
||||||
current = _ms_status_value(ms)
|
current = _ms_status_value(ms)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from sqlalchemy import func as sa_func
|
|||||||
|
|
||||||
from app.core.config import get_db
|
from app.core.config import get_db
|
||||||
from app.api.deps import get_current_user_or_apikey
|
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 import models
|
||||||
from app.models.propose import Propose, ProposeStatus
|
from app.models.propose import Propose, ProposeStatus
|
||||||
from app.models.milestone import Milestone, MilestoneStatus
|
from app.models.milestone import Milestone, MilestoneStatus
|
||||||
@@ -161,8 +161,7 @@ def accept_propose(
|
|||||||
if propose_status != "open":
|
if propose_status != "open":
|
||||||
raise HTTPException(status_code=400, detail="Only open proposes can be accepted")
|
raise HTTPException(status_code=400, detail="Only open proposes can be accepted")
|
||||||
|
|
||||||
# TODO: check 'accept propose' permission once P2 lands
|
check_permission(db, current_user.id, project_id, "propose.accept")
|
||||||
check_project_role(db, current_user.id, project_id, min_role="mgr")
|
|
||||||
|
|
||||||
# Validate milestone
|
# Validate milestone
|
||||||
milestone = db.query(Milestone).filter(
|
milestone = db.query(Milestone).filter(
|
||||||
@@ -236,8 +235,7 @@ def reject_propose(
|
|||||||
if propose_status != "open":
|
if propose_status != "open":
|
||||||
raise HTTPException(status_code=400, detail="Only open proposes can be rejected")
|
raise HTTPException(status_code=400, detail="Only open proposes can be rejected")
|
||||||
|
|
||||||
# TODO: check 'reject propose' permission once P2 lands
|
check_permission(db, current_user.id, project_id, "propose.reject")
|
||||||
check_project_role(db, current_user.id, project_id, min_role="mgr")
|
|
||||||
|
|
||||||
propose.status = ProposeStatus.REJECTED
|
propose.status = ProposeStatus.REJECTED
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -266,8 +264,7 @@ def reopen_propose(
|
|||||||
if propose_status != "rejected":
|
if propose_status != "rejected":
|
||||||
raise HTTPException(status_code=400, detail="Only rejected proposes can be reopened")
|
raise HTTPException(status_code=400, detail="Only rejected proposes can be reopened")
|
||||||
|
|
||||||
# TODO: check 'reopen rejected propose' permission once P2 lands
|
check_permission(db, current_user.id, project_id, "propose.reopen")
|
||||||
check_project_role(db, current_user.id, project_id, min_role="mgr")
|
|
||||||
|
|
||||||
propose.status = ProposeStatus.OPEN
|
propose.status = ProposeStatus.OPEN
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from app.schemas import schemas
|
|||||||
from app.services.webhook import fire_webhooks_sync
|
from app.services.webhook import fire_webhooks_sync
|
||||||
from app.models.notification import Notification as NotificationModel
|
from app.models.notification import Notification as NotificationModel
|
||||||
from app.api.deps import get_current_user_or_apikey
|
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
|
from app.services.activity import log_activity
|
||||||
|
|
||||||
router = APIRouter(tags=["Tasks"])
|
router = APIRouter(tags=["Tasks"])
|
||||||
@@ -320,8 +320,14 @@ def transition_task(
|
|||||||
if task.assignee_id and current_user.id != task.assignee_id:
|
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")
|
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
|
# P5.6: reopen from completed/closed -> open
|
||||||
if new_status == "open" and old_status in ("completed", "closed"):
|
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
|
# Clear finished_on on reopen so lifecycle timestamps are accurate
|
||||||
task.finished_on = None
|
task.finished_on = None
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,18 @@ DEFAULT_PERMISSIONS = [
|
|||||||
("milestone.read", "View milestones", "milestone"),
|
("milestone.read", "View milestones", "milestone"),
|
||||||
("milestone.write", "Edit milestones", "milestone"),
|
("milestone.write", "Edit milestones", "milestone"),
|
||||||
("milestone.delete", "Delete 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/Permission management
|
||||||
("role.manage", "Manage roles and permissions", "admin"),
|
("role.manage", "Manage roles and permissions", "admin"),
|
||||||
# User management
|
# User management
|
||||||
|
|||||||
Reference in New Issue
Block a user