From c18b8f3850647d9ac604bbe1345b4c59dc6884ad Mon Sep 17 00:00:00 2001 From: zhi Date: Tue, 17 Mar 2026 13:02:46 +0000 Subject: [PATCH] feat(P9.6): block story/feature and maintenance/release task creation via general create endpoints --- app/api/routers/milestones.py | 5 ++++- app/api/routers/tasks.py | 19 ++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/app/api/routers/milestones.py b/app/api/routers/milestones.py index 27ee997..0ef296d 100644 --- a/app/api/routers/milestones.py +++ b/app/api/routers/milestones.py @@ -147,9 +147,12 @@ def create_milestone_task(project_id: int, milestone_id: int, task_data: schemas ms_status = milestone.status.value if hasattr(milestone.status, 'value') else milestone.status if ms_status in ("undergoing", "completed", "closed"): raise HTTPException(status_code=400, detail=f"Cannot add items to a milestone that is '{ms_status}'") - # P3.6 / §5: freeze prevents adding new feature story tasks + # P9.6: feature story tasks must come from propose accept, not direct creation task_type = task_data.model_dump(exclude_unset=True).get("task_type", "") task_subtype = task_data.model_dump(exclude_unset=True).get("task_subtype", "") + if task_type == "story" and task_subtype == "feature": + raise HTTPException(status_code=400, detail="Feature story tasks can only be created via propose accept, not direct creation") + # P3.6 / §5: freeze prevents adding new feature story tasks (redundant after P9.6 but kept as defense-in-depth) if ms_status == "freeze" and task_type == "story" and task_subtype == "feature": raise HTTPException(status_code=400, detail="Cannot add feature story tasks after milestone is frozen") diff --git a/app/api/routers/tasks.py b/app/api/routers/tasks.py index abcbf64..f59fd12 100644 --- a/app/api/routers/tasks.py +++ b/app/api/routers/tasks.py @@ -52,7 +52,17 @@ TASK_SUBTYPE_MAP = { ALLOWED_TASK_TYPES = set(TASK_SUBTYPE_MAP.keys()) -def _validate_task_type_subtype(task_type: str | None, task_subtype: str | None): +"""P9.6 — type+subtype combos that may NOT be created via general create endpoints. + feature story → must come from propose accept + release maintenance → must come from controlled milestone/release flow +""" +RESTRICTED_TYPE_SUBTYPES = { + ("story", "feature"), + ("maintenance", "release"), +} + + +def _validate_task_type_subtype(task_type: str | None, task_subtype: str | None, *, allow_restricted: bool = False): if task_type is None: return if task_type not in ALLOWED_TASK_TYPES: @@ -60,6 +70,13 @@ def _validate_task_type_subtype(task_type: str | None, task_subtype: str | None) allowed = TASK_SUBTYPE_MAP.get(task_type, set()) if task_subtype and task_subtype not in allowed: raise HTTPException(status_code=400, detail=f'Invalid task_subtype for {task_type}: {task_subtype}') + # P9.6: block restricted combos unless explicitly allowed (e.g. propose accept, internal create) + if not allow_restricted and (task_type, task_subtype) in RESTRICTED_TYPE_SUBTYPES: + raise HTTPException( + status_code=400, + detail=f"Cannot create {task_type}/{task_subtype} task via general create. " + f"Use the appropriate workflow (propose accept / milestone release setup)." + ) def _notify_user(db, user_id, ntype, title, message=None, entity_type=None, entity_id=None):