Files
resolutionflow/backend/app/api/endpoints/target_lists.py
chihlasm e05472615b feat: tenant isolation Phase 3 — audit_logs, tree_shares, remaining RLS
P3-A: Add account_id to audit_logs model + migration (backfill via user_id →
  users.account_id). log_audit() gains optional account_id param with fallback
  SELECT to avoid churn across 40 call sites.

P3-B: Add account_id to tree_shares model + migration (backfill via created_by
  → users.account_id). TreeShare constructor updated in trees.py.

P3-C: Enable RLS on 6 remaining tables: step_ratings, step_usage_log,
  target_lists, session_shares, audit_logs, tree_shares.

P3-D: Drop team_id from target_lists — endpoint, schema, and model now use
  account_id as the sole isolation key.

P3-E: Append Phase 3 RLS isolation tests for all 6 tables.

test_target_lists.py: fix cross-account test to use Account model (not Team)
and set account_id on new User.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 07:02:35 +00:00

116 lines
3.9 KiB
Python

"""Target lists CRUD endpoints."""
from typing import Annotated
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_current_active_user, get_db, require_engineer_or_admin
from app.models.target_list import TargetList
from app.models.user import User
from app.schemas.target_list import TargetListCreate, TargetListUpdate, TargetListResponse
router = APIRouter(prefix="/target-lists", tags=["target-lists"])
@router.get("/", response_model=list[TargetListResponse])
async def list_target_lists(
current_user: Annotated[User, Depends(get_current_active_user)],
db: Annotated[AsyncSession, Depends(get_db)],
):
"""List all target lists for the current user's account."""
result = await db.execute(
select(TargetList)
.where(TargetList.account_id == current_user.account_id)
.order_by(TargetList.name)
)
return result.scalars().all()
@router.post("/", response_model=TargetListResponse, status_code=201)
async def create_target_list(
data: TargetListCreate,
current_user: Annotated[User, Depends(get_current_active_user)],
db: Annotated[AsyncSession, Depends(get_db)],
_: None = Depends(require_engineer_or_admin),
):
"""Create a new target list for the current account."""
target_list = TargetList(
account_id=current_user.account_id,
created_by=current_user.id,
name=data.name,
description=data.description,
targets=[t.model_dump() for t in data.targets],
)
db.add(target_list)
await db.commit()
await db.refresh(target_list)
return target_list
@router.get("/{list_id}", response_model=TargetListResponse)
async def get_target_list(
list_id: UUID,
current_user: Annotated[User, Depends(get_current_active_user)],
db: Annotated[AsyncSession, Depends(get_db)],
):
result = await db.execute(
select(TargetList).where(
TargetList.id == list_id,
TargetList.account_id == current_user.account_id,
)
)
target_list = result.scalar_one_or_none()
if not target_list:
raise HTTPException(status_code=404, detail="Target list not found")
return target_list
@router.put("/{list_id}", response_model=TargetListResponse)
async def update_target_list(
list_id: UUID,
data: TargetListUpdate,
current_user: Annotated[User, Depends(get_current_active_user)],
db: Annotated[AsyncSession, Depends(get_db)],
_: None = Depends(require_engineer_or_admin),
):
result = await db.execute(
select(TargetList).where(
TargetList.id == list_id,
TargetList.account_id == current_user.account_id,
)
)
target_list = result.scalar_one_or_none()
if not target_list:
raise HTTPException(status_code=404, detail="Target list not found")
update_fields = data.model_fields_set
if "name" in update_fields and data.name is not None:
target_list.name = data.name
if "description" in update_fields:
target_list.description = data.description
if "targets" in update_fields and data.targets is not None:
target_list.targets = [t.model_dump() for t in data.targets]
await db.commit()
await db.refresh(target_list)
return target_list
@router.delete("/{list_id}", status_code=204)
async def delete_target_list(
list_id: UUID,
current_user: Annotated[User, Depends(get_current_active_user)],
db: Annotated[AsyncSession, Depends(get_db)],
_: None = Depends(require_engineer_or_admin),
):
result = await db.execute(
select(TargetList).where(
TargetList.id == list_id,
TargetList.account_id == current_user.account_id,
)
)
target_list = result.scalar_one_or_none()
if not target_list:
raise HTTPException(status_code=404, detail="Target list not found")
await db.delete(target_list)
await db.commit()