Files
kite/.planning/phases/09-admin-panel-rearchitecture/09-01-PLAN.md
T
curo1305andClaude Sonnet 4.6 ba30ada87a docs(09): create phase 9 plan — Admin Panel Rearchitecture
5 plans across 4 waves covering ADMIN-08..12, CODE-06, CODE-09:

- 09-01 (W1): backend GET /api/admin/overview + 8 ADMIN-11 tests
- 09-02 (W1): AdminLayout + AdminSidebar + AdminOverviewView + getAdminOverview API
- 09-03 (W2): extract 4 admin tab components to standalone views
- 09-04 (W3): nested /admin route + to.matched.some guard + D-08/D-09/D-10 redirects + Tailwind safelist (sky+amber) + login redirect + delete AdminView/tabs
- 09-05 (W4): CODE-09 comment purge for Phase 9 + retroactive Phase 8 backend sub-packages + human UAT checkpoint

All locked decisions D-01..D-16 mapped to tasks; safelist corrected to include sky (OneDrive) and amber (audit badges) per RESEARCH.md Pattern 7.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 15:34:31 +02:00

192 lines
15 KiB
Markdown

---
phase: 09-admin-panel-rearchitecture
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- backend/api/admin/overview.py
- backend/api/admin/__init__.py
- backend/tests/test_admin_overview.py
autonomous: true
requirements:
- ADMIN-11
user_setup: []
must_haves:
truths:
- "GET /api/admin/overview returns 200 for admin caller with keys user_count, total_storage_bytes, doc_status, recent_audit"
- "GET /api/admin/overview returns 401 or 403 for non-admin / unauthenticated callers"
- "Overview response body contains no occurrence of password_hash, credentials_enc, or extracted_text"
- "recent_audit list has at most 10 entries"
artifacts:
- path: "backend/api/admin/overview.py"
provides: "GET /api/admin/overview aggregate endpoint"
contains: "router = APIRouter()"
- path: "backend/api/admin/__init__.py"
provides: "overview_router registration on admin parent router"
contains: "from api.admin.overview import router as overview_router"
- path: "backend/tests/test_admin_overview.py"
provides: "ADMIN-11 endpoint + security invariant tests"
contains: "def test_overview_no_sensitive_fields"
key_links:
- from: "backend/api/admin/overview.py"
to: "backend/api/audit.py"
via: "import _build_filtered_query_with_handles, _audit_to_dict_with_handles"
pattern: "from api.audit import"
- from: "backend/api/admin/__init__.py"
to: "backend/api/admin/overview.py"
via: "include_router(overview_router)"
pattern: "include_router\\(overview_router\\)"
---
<objective>
Add a new admin overview backend endpoint at `GET /api/admin/overview` that returns aggregated platform stats (user count, total storage, document status breakdown) plus the 10 most recent audit-log entries in a single response payload, and register it on the admin parent router. Backed by `backend/tests/test_admin_overview.py` covering ADMIN-11 + security invariants.
Purpose: ADMIN-11 requires an admin overview that combines stats + recent audit entries; D-02 mandates a new `overview.py` sub-module inside `backend/api/admin/`; D-03 requires the audit rows inline (single HTTP call, no `/api/audit` round-trip from the overview view).
Output: `backend/api/admin/overview.py` (new), `backend/api/admin/__init__.py` (modified — one import + one include_router call), `backend/tests/test_admin_overview.py` (new — 3 promoted tests).
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/phases/09-admin-panel-rearchitecture/09-CONTEXT.md
@.planning/phases/09-admin-panel-rearchitecture/09-RESEARCH.md
@.planning/phases/09-admin-panel-rearchitecture/09-PATTERNS.md
@CLAUDE.md
@backend/api/admin/__init__.py
@backend/api/admin/users.py
@backend/api/audit.py
@backend/tests/test_admin_api.py
<interfaces>
From backend/api/audit.py:
- `_build_filtered_query_with_handles(start, end, user_uuid, event_type)` — returns a SQLAlchemy `Select` statement joining `AuditLog` with `users` for owner_handle + target_user_handle. Pass `None` for all four args to get the unfiltered base query; chain `.limit(10)` for the most recent 10.
- `_audit_to_dict_with_handles(audit_log_row, owner_handle, target_user_handle, ip)` — returns a safe dict for one audit entry; the security-audited whitelist serializer reused for ADMIN-06.
From backend/db/models.py:
- `User` (fields used: `id`, `role` literal `'user'` vs `'admin'`)
- `Quota` (fields used: `used_bytes`)
- `Document` (fields used: `status`)
- `AuditLog`
From backend/deps/auth.py:
- `get_current_admin` — FastAPI dependency that raises 401 (no token) or 403 (non-admin token).
From backend/api/admin/__init__.py (current state):
- `router = APIRouter(prefix="/api/admin", tags=["admin"])` — parent aggregator; sub-routers MUST carry NO prefix per the "NO prefix" WHY comment at the top of the file (do not remove that comment in this plan).
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Create Wave 0 test stubs for the overview endpoint</name>
<files>backend/tests/test_admin_overview.py</files>
<read_first>
- backend/tests/test_admin_api.py (copy `make_admin_user` fixture verbatim + `admin_client` fixture pattern; reuse `FakeRedis` import from `tests.test_auth_api`)
- backend/tests/test_audit.py (audit fixture seeding pattern for `recent_audit` assertion)
- backend/tests/conftest.py (existing `async_client` and `db_session` fixtures)
- backend/api/audit.py (signature of `_build_filtered_query_with_handles` so test data covers a query that returns at least one row)
</read_first>
<behavior>
- Test 1 `test_overview_requires_admin`: unauthenticated GET /api/admin/overview returns 401 or 403.
- Test 2 `test_overview_non_admin_forbidden`: regular-user JWT returns 403.
- Test 3 `test_overview_returns_expected_keys`: admin GET returns 200 with all four top-level keys present (`user_count`, `total_storage_bytes`, `doc_status`, `recent_audit`).
- Test 4 `test_overview_user_count_excludes_admins`: seed one admin + two `role='user'` users; `user_count == 2`.
- Test 5 `test_overview_total_storage_sums_quotas`: seed quotas (1024 + 2048); `total_storage_bytes == 3072`.
- Test 6 `test_overview_doc_status_groups_by_status`: seed documents with statuses `ready`, `ready`, `processing`, `failed`; `doc_status == {"ready": 2, "processing": 1, "failed": 1}`.
- Test 7 `test_overview_recent_audit_limit_10`: seed 15 audit entries; `len(recent_audit) == 10`; entries ordered newest first.
- Test 8 `test_overview_no_sensitive_fields`: response body string contains none of `password_hash`, `credentials_enc`, `extracted_text`, `totp_secret`, `api_key_enc`.
</behavior>
<action>Create `backend/tests/test_admin_overview.py` with the 8 tests listed in `<behavior>`. Reuse `make_admin_user` and `admin_client` patterns from `test_admin_api.py` verbatim (copy fixtures into this file or import from `tests.test_admin_api` if existing test files do that — match the prevailing pattern in `test_admin_ai_config.py`). All 8 tests SHOULD be marked `@pytest.mark.xfail(strict=True, reason="ADMIN-11: overview endpoint not yet implemented")` initially so Task 2 can flip them to passing. Each test uses `pytest.mark.asyncio`. Use the `FakeRedis` import from `tests.test_auth_api` (matches existing admin tests). The total_storage test seeds Quota rows directly; the doc_status test seeds Document rows directly; the recent_audit test seeds AuditLog rows directly via the existing `AuditLog` ORM model — do not call audit-write helpers (that introduces coupling). Body-string scan in `test_overview_no_sensitive_fields` uses `resp.text` (full raw JSON string).</action>
<verify>
<automated>cd backend && pytest tests/test_admin_overview.py -v</automated>
</verify>
<acceptance_criteria>
- File `backend/tests/test_admin_overview.py` exists.
- `grep -c "^async def test_" backend/tests/test_admin_overview.py` returns 8.
- All 8 tests show XFAIL (expected fail) in pytest output since the endpoint does not exist yet — `pytest tests/test_admin_overview.py -v` exits 0.
- File imports `pytest`, `pytest_asyncio`, `uuid`, `httpx.AsyncClient`, and `from db.models import User, Quota, Document, AuditLog`.
- Every test function carries both `@pytest.mark.asyncio` and `@pytest.mark.xfail(strict=True, reason="ADMIN-11: ...")`.
</acceptance_criteria>
<done>Wave 0 test scaffolds exist for ADMIN-11; all 8 tests are xfail and pytest exits 0.</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Implement overview.py endpoint and register on admin router</name>
<files>backend/api/admin/overview.py, backend/api/admin/__init__.py</files>
<read_first>
- backend/api/admin/users.py (full file — exact `router = APIRouter()` pattern, dependency wiring `_admin: User = Depends(get_current_admin)`, scalar/aggregate query idioms with `func.count`)
- backend/api/admin/__init__.py (current 24 lines — preserve the WHY comment about prefix per D-16; pattern for `include_router` ordering)
- backend/api/audit.py (signature and body of `_build_filtered_query_with_handles` and `_audit_to_dict_with_handles` — see lines around the audit-log listing endpoint; confirm the helper returns aliased handle columns the dict serializer expects)
- backend/db/models.py (`User.role`, `Quota.used_bytes`, `Document.status`)
- backend/tests/test_admin_overview.py (the 8 xfail tests this implementation must satisfy)
</read_first>
<behavior>
- Endpoint path `/overview` declared on a sub-router with NO prefix; aggregator carries `/api/admin` so full URL is `/api/admin/overview`.
- Handler is `async def get_overview` returning `dict` with exact keys `user_count`, `total_storage_bytes`, `doc_status`, `recent_audit`.
- `user_count` is `SELECT count(id) FROM users WHERE role = 'user'`.
- `total_storage_bytes` is `SELECT sum(used_bytes) FROM quotas` coerced to `0` when NULL.
- `doc_status` is `{status: count}` from `SELECT status, count(id) FROM documents GROUP BY status`.
- `recent_audit` is the result of `_build_filtered_query_with_handles(None, None, None, None).limit(10)` mapped via `_audit_to_dict_with_handles`.
- Response never includes raw user records, raw audit rows, password_hash, credentials_enc, totp_secret, api_key_enc, or extracted_text.
</behavior>
<action>Create `backend/api/admin/overview.py`. Imports: `from __future__ import annotations`; `from fastapi import APIRouter, Depends`; `from sqlalchemy import func, select`; `from sqlalchemy.ext.asyncio import AsyncSession`; `from db.models import Document, Quota, User`; `from deps.auth import get_current_admin`; `from deps.db import get_db`; `from api.audit import _build_filtered_query_with_handles, _audit_to_dict_with_handles`. Declare `router = APIRouter()` with NO prefix (single WHY comment: "NO prefix — parent `__init__.py` carries `/api/admin` (D-02)"). Declare `@router.get("/overview")` async function `get_overview(session: AsyncSession = Depends(get_db), _admin: User = Depends(get_current_admin)) -> dict`. Compute user_count via `await session.scalar(select(func.count(User.id)).where(User.role == "user"))` (default to 0 if None). Compute total_storage_bytes via `await session.scalar(select(func.sum(Quota.used_bytes)))` (default 0). Compute doc_status via `(await session.execute(select(Document.status, func.count(Document.id)).group_by(Document.status))).all()` and convert to dict. Compute recent_audit by calling `_build_filtered_query_with_handles(None, None, None, None).limit(10)`, executing, then mapping each row tuple via `_audit_to_dict_with_handles(*row)` — confirm the row unpacking shape against the audit.py source (the existing list endpoint already does this — copy that exact iteration). Return the dict with the four keys. Modify `backend/api/admin/__init__.py`: add `from api.admin.overview import router as overview_router` alongside existing imports (preserve the docstring WHY comment); append `router.include_router(overview_router)` after the existing three `include_router` calls. Then remove the `@pytest.mark.xfail` decorators from all 8 tests in `backend/tests/test_admin_overview.py` so they execute as real tests.</action>
<verify>
<automated>cd backend && pytest tests/test_admin_overview.py -v -x</automated>
</verify>
<acceptance_criteria>
- File `backend/api/admin/overview.py` exists.
- `grep -c "router = APIRouter()" backend/api/admin/overview.py` returns 1.
- `grep -E "APIRouter\\(prefix=" backend/api/admin/overview.py` returns no match (NO prefix on sub-router).
- `grep -c "from api.audit import _build_filtered_query_with_handles" backend/api/admin/overview.py` returns 1.
- `grep -c "_admin: User = Depends(get_current_admin)" backend/api/admin/overview.py` returns 1.
- `grep -c "overview_router" backend/api/admin/__init__.py` returns 2 (one import, one include_router).
- `grep -c "xfail" backend/tests/test_admin_overview.py` returns 0 (all xfails removed).
- `pytest backend/tests/test_admin_overview.py -v` reports 8 passed, 0 failed, 0 xfailed.
- `pytest backend/tests/test_admin_api.py backend/tests/test_audit.py -v` continues to pass (no regression in existing admin/audit tests).
- Endpoint module is discoverable: `python -c "from api.admin.overview import router; print(router.routes[0].path)"` prints `/overview`.
</acceptance_criteria>
<done>GET /api/admin/overview returns 200 for admins with the four-key payload; all 8 test_admin_overview tests pass; existing admin and audit test suites stay green.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| browser → /api/admin/overview | admin JWT crosses; aggregate stats + audit rows returned |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-09-01-01 | Elevation of Privilege | GET /api/admin/overview | mitigate | `_admin: User = Depends(get_current_admin)` raises 401/403 for non-admin tokens; covered by `test_overview_requires_admin` + `test_overview_non_admin_forbidden` |
| T-09-01-02 | Information Disclosure | GET /api/admin/overview response | mitigate | Response is hand-rolled dict with only aggregate counts + whitelisted `_audit_to_dict_with_handles` rows; no `password_hash`/`credentials_enc`/`extracted_text`/`totp_secret`/`api_key_enc` ever serialized; covered by `test_overview_no_sensitive_fields` (raw body grep) |
| T-09-01-03 | Tampering (Reusable code) | `_build_filtered_query_with_handles` import | accept | Cross-module import from sibling `api/audit.py` is a stable existing helper (already security-audited and tested); risk: future audit.py refactor breaks the import — accepted because the import path is documented in the WHY comment and an ImportError fails loud on startup |
</threat_model>
<verification>
- `cd backend && pytest tests/test_admin_overview.py tests/test_admin_api.py tests/test_audit.py -v` → all pass, zero failures, zero xfails.
- `cd backend && pytest -v` → no regressions across the full suite.
- `cd backend && python -c "from api.admin import router; paths=[r.path for r in router.routes]; assert '/api/admin/overview' in paths or any('/overview' in p for p in paths), paths"` → endpoint registered under the `/api/admin` prefix.
- `cd backend && bandit -r api/admin/overview.py` → zero HIGH findings.
</verification>
<success_criteria>
- ADMIN-11 backend slice deliverable: `GET /api/admin/overview` returns the four-key aggregate payload to admin callers; non-admin/unauthenticated callers get 401/403; response body never contains sensitive fields.
- Sub-router carries NO prefix (constraint preserved per existing Phase 8 invariant).
- All 8 dedicated tests pass; broader admin + audit suites stay green.
</success_criteria>
<output>
Create `.planning/phases/09-admin-panel-rearchitecture/09-01-SUMMARY.md` when done. Include: files created, files modified, tests passing count, and confirmation that no sensitive fields are present in the overview response.
</output>