feat(P9.6): block story/feature and maintenance/release task creation via general create endpoints
This commit is contained in:
@@ -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
|
ms_status = milestone.status.value if hasattr(milestone.status, 'value') else milestone.status
|
||||||
if ms_status in ("undergoing", "completed", "closed"):
|
if ms_status in ("undergoing", "completed", "closed"):
|
||||||
raise HTTPException(status_code=400, detail=f"Cannot add items to a milestone that is '{ms_status}'")
|
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_type = task_data.model_dump(exclude_unset=True).get("task_type", "")
|
||||||
task_subtype = task_data.model_dump(exclude_unset=True).get("task_subtype", "")
|
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":
|
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")
|
raise HTTPException(status_code=400, detail="Cannot add feature story tasks after milestone is frozen")
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,17 @@ TASK_SUBTYPE_MAP = {
|
|||||||
ALLOWED_TASK_TYPES = set(TASK_SUBTYPE_MAP.keys())
|
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:
|
if task_type is None:
|
||||||
return
|
return
|
||||||
if task_type not in ALLOWED_TASK_TYPES:
|
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())
|
allowed = TASK_SUBTYPE_MAP.get(task_type, set())
|
||||||
if task_subtype and task_subtype not in allowed:
|
if task_subtype and task_subtype not in allowed:
|
||||||
raise HTTPException(status_code=400, detail=f'Invalid task_subtype for {task_type}: {task_subtype}')
|
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):
|
def _notify_user(db, user_id, ntype, title, message=None, entity_type=None, entity_id=None):
|
||||||
|
|||||||
Reference in New Issue
Block a user