"""What my team owes, security-wise — ``/api/v1/me/team/systems``. A lead had 团队资产 / 团队云资产 / 团队入离职 / 团队申请 and no way to see that a report of theirs owns a system with a confirmed vulnerability outstanding. The gap was found the honest way: the platform owner opened 「我负责的系统」 as themselves, saw it empty, and asked why their report's TiCard bug was not visible anywhere. Scope is the manager chain, resolved through the canonical ``subordinate_person_ids`` — never a hand-rolled BFS, which is the bug class ``scripts/check_subordinate_helpers.py`` exists to prevent. No role gate: being someone's manager IS the authorisation, exactly as it is for the other team surfaces. """ from __future__ import annotations from typing import Any from fastapi import APIRouter, Depends, HTTPException, Request from sqlalchemy.ext.asyncio import AsyncSession from app.auth.models import AuthUser from app.core.coordinators import bounty_coordinator as bounty from app.core.coordinators import system_registry_coordinator as registry from app.core.database import get_db from app.core.dependencies import get_current_user from app.core.person_scope import SubordinateScope, subordinate_person_ids from app.core.response import success_response from app.identity.person_models import IdentityPerson router = APIRouter(prefix="/api/v1/me/team", tags=["system-registry"]) async def _owned_by_my_reports( db: AsyncSession, user_id: Any, ) -> list[dict[str, Any]]: """Systems whose PRIMARY owner reports to ``user_id``, each carrying its owner's display name. Shared by the list and the detail endpoint so the two can never disagree about what this lead may see — the detail endpoint's authorisation is exactly this set.""" from sqlalchemy import select reports = await subordinate_person_ids( db, user_id, scope=SubordinateScope.MANAGED, ) if not reports: return [] people = { p.id: (p.display_name or p.work_email) for p in ( await db.execute( select(IdentityPerson).where(IdentityPerson.id.in_(reports)), ) ).scalars().all() } items: list[dict[str, Any]] = [] for person_id in reports: for system in await registry.systems_owned_by(db, person_id): if not system["is_primary_owner"]: # The backup owner is not the accountable party; showing # them here would double-count the same system under two # people and make "who owes this" ambiguous. continue items.append({**system, "owner_name": people.get(person_id, "")}) return items @router.get("/systems/findings/{code}") async def team_finding_detail( code: str, request: Request, user: AuthUser = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """One finding's progress, for a lead whose report owns the system. 404 rather than 403 for a code outside that scope: a lead must not be able to probe whether an arbitrary report code exists. Read-only by design. Assignment is the security desk's call, and a nudge would be a new employee-facing push — neither belongs to a lead, so this answers 「进展如何」 and stops there. """ items = await _owned_by_my_reports(db, user.id) detail = await bounty.finding_detail_for_systems( db, code, [item["id"] for item in items], ) if detail is None: raise HTTPException(status_code=404, detail="FINDING_NOT_FOUND") by_id = {item["id"]: item for item in items} system = by_id.get(detail["system_id"], {}) detail["system_name"] = system.get("name", "") detail["owner_name"] = system.get("owner_name", "") return success_response(detail, request=request) @router.get("/systems") async def team_systems( request: Request, user: AuthUser = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Every system owned by someone who reports to the caller, with the open security work outstanding on it. Includes the whole subtree, not just direct reports: a system owned two levels down is still the lead's problem when it goes unanswered. Leavers are excluded by the resolver's default scope — a departing report's systems belong to the offboarding blocker, not here. """ items = await _owned_by_my_reports(db, user.id) if not items: return success_response( {"items": [], "totals": {"systems": 0, "open_findings": 0}}, request=request, ) findings = await bounty.open_findings_by_system( db, [item["id"] for item in items], ) for item in items: item["findings"] = findings.get(item["id"], []) return success_response( { "items": sorted( items, key=lambda i: (-len(i["findings"]), i["name"]), ), "totals": { "systems": len(items), "open_findings": sum(len(i["findings"]) for i in items), }, }, request=request, ) @router.get("/systems/findings/{code}/attachments/{attachment_id}/url") async def team_finding_attachment_url( code: str, attachment_id: str, request: Request, user: AuthUser = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Download one attachment on a finding sitting on my team's system. 404 for everything that is not allowed, the same as the detail endpoint: a lead must not be able to distinguish 「不在你范围内」 from 「不存在」 by the status code. """ items = await _owned_by_my_reports(db, user.id) url = await bounty.attachment_url_for_systems( db, code, [item["id"] for item in items], attachment_id, ) if url is None: raise HTTPException(status_code=404, detail="ATTACHMENT_NOT_FOUND") return success_response({"url": url}, request=request)