fix: Add missing API endpoints and validation improvements
- Add cancel_sprint and delete_sprint endpoints to sprints.py - Add unassign_issue endpoint to issues.py - Add remove_issue_from_sprint endpoint to sprints.py - Add CRUD methods: remove_sprint_from_issues, unassign, remove_from_sprint - Add validation to prevent closed issues in active/planned sprints - Add authorization tests for SSE events endpoint - Fix IDOR vulnerabilities in agents.py and projects.py - Add Syndarix models migration (0004) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,7 @@ from app.core.exceptions import (
|
||||
from app.crud.syndarix.agent_instance import agent_instance as agent_instance_crud
|
||||
from app.crud.syndarix.issue import issue as issue_crud
|
||||
from app.crud.syndarix.project import project as project_crud
|
||||
from app.crud.syndarix.sprint import sprint as sprint_crud
|
||||
from app.models.syndarix.enums import IssuePriority, IssueStatus, SyncStatus
|
||||
from app.models.user import User
|
||||
from app.schemas.common import (
|
||||
@@ -200,6 +201,21 @@ async def create_issue(
|
||||
field="assigned_agent_id",
|
||||
)
|
||||
|
||||
# Validate sprint if provided (IDOR prevention)
|
||||
if issue_in.sprint_id:
|
||||
sprint = await sprint_crud.get(db, id=issue_in.sprint_id)
|
||||
if not sprint:
|
||||
raise NotFoundError(
|
||||
message=f"Sprint {issue_in.sprint_id} not found",
|
||||
error_code=ErrorCode.NOT_FOUND,
|
||||
)
|
||||
if sprint.project_id != project_id:
|
||||
raise ValidationException(
|
||||
message="Sprint does not belong to this project",
|
||||
error_code=ErrorCode.VALIDATION_ERROR,
|
||||
field="sprint_id",
|
||||
)
|
||||
|
||||
try:
|
||||
issue = await issue_crud.create(db, obj_in=issue_in)
|
||||
logger.info(
|
||||
@@ -470,6 +486,21 @@ async def update_issue(
|
||||
field="assigned_agent_id",
|
||||
)
|
||||
|
||||
# Validate sprint if being updated (IDOR prevention)
|
||||
if issue_in.sprint_id is not None:
|
||||
sprint = await sprint_crud.get(db, id=issue_in.sprint_id)
|
||||
if not sprint:
|
||||
raise NotFoundError(
|
||||
message=f"Sprint {issue_in.sprint_id} not found",
|
||||
error_code=ErrorCode.NOT_FOUND,
|
||||
)
|
||||
if sprint.project_id != project_id:
|
||||
raise ValidationException(
|
||||
message="Sprint does not belong to this project",
|
||||
error_code=ErrorCode.VALIDATION_ERROR,
|
||||
field="sprint_id",
|
||||
)
|
||||
|
||||
try:
|
||||
updated_issue = await issue_crud.update(db, db_obj=issue, obj_in=issue_in)
|
||||
logger.info(
|
||||
@@ -693,6 +724,78 @@ async def assign_issue(
|
||||
)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/projects/{project_id}/issues/{issue_id}/assignment",
|
||||
response_model=IssueResponse,
|
||||
summary="Unassign Issue",
|
||||
description="""
|
||||
Remove agent/human assignment from an issue.
|
||||
|
||||
**Authentication**: Required (Bearer token)
|
||||
**Authorization**: Project owner or superuser
|
||||
|
||||
This clears both agent and human assignee fields.
|
||||
|
||||
**Rate Limit**: 60 requests/minute
|
||||
""",
|
||||
operation_id="unassign_issue",
|
||||
)
|
||||
@limiter.limit(f"{60 * RATE_MULTIPLIER}/minute")
|
||||
async def unassign_issue(
|
||||
request: Request,
|
||||
project_id: UUID,
|
||||
issue_id: UUID,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> Any:
|
||||
"""
|
||||
Remove assignment from an issue.
|
||||
|
||||
Clears both assigned_agent_id and human_assignee fields.
|
||||
"""
|
||||
# Verify project access
|
||||
await verify_project_ownership(db, project_id, current_user)
|
||||
|
||||
# Get existing issue
|
||||
issue = await issue_crud.get(db, id=issue_id)
|
||||
if not issue:
|
||||
raise NotFoundError(
|
||||
message=f"Issue {issue_id} not found",
|
||||
error_code=ErrorCode.NOT_FOUND,
|
||||
)
|
||||
|
||||
# Verify issue belongs to project (IDOR prevention)
|
||||
if issue.project_id != project_id:
|
||||
raise NotFoundError(
|
||||
message=f"Issue {issue_id} not found in project {project_id}",
|
||||
error_code=ErrorCode.NOT_FOUND,
|
||||
)
|
||||
|
||||
# Unassign the issue
|
||||
updated_issue = await issue_crud.unassign(db, issue_id=issue_id)
|
||||
|
||||
if not updated_issue:
|
||||
raise NotFoundError(
|
||||
message=f"Issue {issue_id} not found",
|
||||
error_code=ErrorCode.NOT_FOUND,
|
||||
)
|
||||
|
||||
logger.info(f"User {current_user.email} unassigned issue {issue_id}")
|
||||
|
||||
# Get full details for response
|
||||
issue_data = await issue_crud.get_with_details(db, issue_id=issue_id)
|
||||
|
||||
return _build_issue_response(
|
||||
updated_issue,
|
||||
project_name=issue_data.get("project_name") if issue_data else None,
|
||||
project_slug=issue_data.get("project_slug") if issue_data else None,
|
||||
sprint_name=issue_data.get("sprint_name") if issue_data else None,
|
||||
assigned_agent_type_name=issue_data.get("assigned_agent_type_name")
|
||||
if issue_data
|
||||
else None,
|
||||
)
|
||||
|
||||
|
||||
# ===== Issue Sync Endpoint =====
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user