Files
kite/.planning/milestones/v0.2-phases/09-admin-panel-rearchitecture/09-01-PLAN.md
T
curo1305andClaude Sonnet 4.6 123ae5b29b chore: archive v0.2 phase directories to milestones/v0.2-phases/
Moves phases 08–11 execution artifacts from .planning/phases/ to
.planning/milestones/v0.2-phases/ to keep .planning/phases/ clean
for the next milestone.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 14:34:52 +02:00

15 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, user_setup, must_haves
phase plan type wave depends_on files_modified autonomous requirements user_setup must_haves
09-admin-panel-rearchitecture 01 execute 1
backend/api/admin/overview.py
backend/api/admin/__init__.py
backend/tests/test_admin_overview.py
true
ADMIN-11
truths artifacts key_links
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
path provides contains
backend/api/admin/overview.py GET /api/admin/overview aggregate endpoint router = APIRouter()
path provides contains
backend/api/admin/__init__.py overview_router registration on admin parent router from api.admin.overview import router as overview_router
path provides contains
backend/tests/test_admin_overview.py ADMIN-11 endpoint + security invariant tests def test_overview_no_sensitive_fields
from to via pattern
backend/api/admin/overview.py backend/api/audit.py import _build_filtered_query_with_handles, _audit_to_dict_with_handles from api.audit import
from to via pattern
backend/api/admin/__init__.py backend/api/admin/overview.py include_router(overview_router) include_router(overview_router)
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).

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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 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).
Task 1: Create Wave 0 test stubs for the overview endpoint backend/tests/test_admin_overview.py - 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) - 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`. Create `backend/tests/test_admin_overview.py` with the 8 tests listed in ``. 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). cd backend && pytest tests/test_admin_overview.py -v - 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: ...")`. Wave 0 test scaffolds exist for ADMIN-11; all 8 tests are xfail and pytest exits 0. Task 2: Implement overview.py endpoint and register on admin router backend/api/admin/overview.py, backend/api/admin/__init__.py - 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) - 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).order_by(AuditLog.created_at.desc()).limit(10)` mapped via `_audit_to_dict_with_handles` — the `.order_by(AuditLog.created_at.desc())` must be chained before `.limit(10)` to guarantee newest-first ordering that the test asserts. - Response never includes raw user records, raw audit rows, password_hash, credentials_enc, totp_secret, api_key_enc, or extracted_text. 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).order_by(AuditLog.created_at.desc()).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. cd backend && pytest tests/test_admin_overview.py -v -x - 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`. 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.

<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>
- `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.

<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>
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.