Files
kite/.planning/STATE.md
T
curo1305 3b84626da9 docs(05-02): complete shared cloud utilities plan
- 05-02-SUMMARY.md: full plan summary with TDD gate compliance, deviation docs, threat surface scan
- STATE.md: advanced to plan 26/32 (81%), updated session log, added 4 key decisions
- ROADMAP.md: marked 05-02 complete (2/8 Phase 5 plans done)
2026-05-28 21:04:03 +02:00

175 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
gsd_state_version: 1.0
milestone: v1.0
milestone_name: milestone
current_phase: 5
status: executing
last_updated: "2026-05-28T19:30:00.000Z"
progress:
total_phases: 5
completed_phases: 4
total_plans: 32
completed_plans: 26
percent: 81
---
# Project State
**Project:** DocuVault
**Status:** Phase 5 Planned — Ready to execute
**Current Phase:** 5
**Last Updated:** 2026-05-28
## Phase Status
| Phase | Name | Status |
|---|---|---|
| 1 | Infrastructure Foundation | ✓ Complete |
| 2 | Users & Authentication | ✓ Complete (5/5 plans) |
| 3 | Document Migration & Multi-User Isolation | ✓ Complete (5/5 plans, UAT passed, security gate passed) |
| 4 | Folders, Sharing, Quotas & Document UX | ✓ Complete (9/9 plans, UAT 14/15 passed, 1 bug fixed) |
| 5 | Cloud Storage Backends | Not Started |
## Current Position
**Phase:** 05-cloud-storage-backends — Not started
**Plan:** 0/TBD
**Progress:** [████████░░] 78%
## Performance Metrics
| Metric | Value |
|---|---|
| Phases complete | 1 / 5 |
| Requirements mapped | 54 / 54 |
| Plans written | 5 (Phase 1) |
| Plans complete | 10 (5 Phase 1 + 5 Phase 2) |
## Accumulated Context
### Key Decisions
| Decision | Rationale |
|---|---|
| PostgreSQL + MinIO | Multi-user quotas and horizontal scaling require shared, consistent state |
| HKDF per-user key derivation | Single Fernet key would be catastrophic on leak — must be derived before first credential is stored |
| Presigned MinIO URL flow | FastAPI handles metadata only; bytes never pass through the API layer |
| Atomic PostgreSQL quota UPDATE | Never perform quota arithmetic in Python between two DB statements |
| JWT in httpOnly cookie | Refresh token in httpOnly cookie; access token in Pinia memory only — never localStorage |
| Refresh token family revocation | RFC 9700 — reuse of a rotated token revokes entire family and alerts user |
| BackgroundTasks replacement | FastAPI BackgroundTasks is per-instance; replace with Celery+Redis or pgqueuer before horizontal scale |
| AuditLog metadata_ ORM attribute | `metadata` is reserved on DeclarativeBase; ORM attribute is `metadata_` with `name="metadata"` kwarg to avoid silent collision |
| documents.user_id nullable Phase 1 | D-03 — no auth in Phase 1; Phase 2 migration adds NOT NULL after auth lands |
| groups stub table Phase 1 | D-02 — groups is a v2 feature; table created now for schema completeness, no rows until Phase 2+ |
| SEQUENCES grants in migration | GRANT USAGE/SELECT on sequences required for audit_log.id autoincrement nextval() by docuvault_app |
| Admin impersonation excluded | Explicit architectural exclusion — no endpoint or UI pathway; violates privacy-first core value |
| user_id as refresh token family proxy | No separate family_id column; user_id serves as family per RFC 9700 — simpler schema |
| pwdlib over passlib | pwdlib actively maintained with clean Argon2Hasher API; passlib unmaintained |
| TOTP replay TTL=90s | valid_window=1 covers ±30s (90s total) — TTL matches window |
| HIBP fail-open | Network errors return False + log warning; auth never blocked by external service |
| Two-DSN PostgreSQL strategy | DATABASE_URL (docuvault_app, DML only) + DATABASE_MIGRATE_URL (docuvault_migrate, DDL only); celery-worker gets only DATABASE_URL |
| MinIO healthcheck via mc ready local | curl removed from MinIO Docker image since Oct 2023; mc is the correct in-container healthcheck tool |
| pydantic-settings v2 SettingsConfigDict | SettingsConfigDict API used (not deprecated class Config form) for env var config |
| async_client fixture name | Distinct from legacy sync `client` fixture to avoid collision; both coexist until Plan 05 |
| xfail(strict=False) for Wave 0 | All pre-implementation scaffolds use strict=False so unexpected passes don't break CI |
| StorageBackend ABC + factory mirrors ai/ pattern | 5 abstract methods; get_storage_backend() factory; MinIOBackend wraps all sync Minio SDK calls in asyncio.to_thread() |
| Explicit localhost string block in validate_cloud_url | hostname == "localhost" blocked before DNS resolution — OS-agnostic (getaddrinfo("localhost") behaviour varies by OS) |
| Fresh HKDF instance per _derive_fernet_key call | cryptography library raises AlreadyFinalized on 2nd .derive() call; always create new HKDF(...) instance — never cache |
| Lazy import of cloud backends in get_storage_backend_for_document | Avoids circular imports at module load time; backends imported inside function body with type: ignore[import] until Plans 05-03..05-05 create them |
| Fetch-outside-lock async cache pattern | get_cloud_folders_cached acquires lock to check cache, releases lock, awaits fetch_fn, re-acquires lock to write — prevents event loop blocking on cache miss |
| STORE-02 key enforced in code | MinIOBackend.put_object constructs {user_id}/{document_id}/{uuid4()}{ext}; no filename parameter — only extension passes through |
| null-user D-03 sentinel | services/storage.save_upload uses user_id="null-user" in Phase 1 (no auth); Phase 2 replaces with str(current_user.id) |
| load_settings flat-file Phase 1 | users.ai_provider/ai_model columns cannot be populated until Phase 2; settings remain flat-file JSON for Phase 1 |
| Deferred Celery import in /password-reset | send_reset_email.delay called via from tasks.email_tasks import send_reset_email inside handler body — same circular-import fix as document_tasks |
| TOTP QR code as otpauth:// link | No QR library installed; plan permits manual secret display for MVP; functional flow complete without rendered QR image |
| ConfirmBlock no acknowledgment checkbox | ConfirmBlock handles message + button pair; BackupCodesDisplay owns its separate acknowledgment checkbox — no overlap |
| ADMIN-07 enforced by omission | No impersonation endpoint exists; AST check + test_admin_impersonation_not_found verify absence; violates privacy-first core value |
| _user_to_dict() whitelist for admin responses | Explicit field whitelist prevents accidental password_hash/credentials_enc leakage from admin endpoints |
| Quota warning is 200 not 4xx | Below-usage limit change is applied; warning=True advisory field returned — not a rejection |
| AdminQuotasTab fetches quotas per-user via Promise.allSettled | adminListUsers() does not include quota fields; per-user endpoint parallelized; failed quotas filtered silently |
| Temp password via crypto.getRandomValues | Browser-native CSPRNG; no external library; always satisfies AUTH-01 strength rules |
| batch_alter_table for NOT NULL in migration 0003 | SQLite requires batch_alter_table for ALTER COLUMN; transparent passthrough on PostgreSQL — enables SQLite CI test runs |
| MinIO step in migration 0003 gated on MINIO_ENDPOINT | Migration skips MinIO deletions when env var absent; enables safe SQLite test runs per T-03-02 |
| raising=False for Phase 3 MinIO mock fixtures | mock_minio_presigned + mock_minio_stat patch methods that don't exist until Plan 03-02; raising=False pre-installs them |
| Dual MinIO client (internal + public) | Presigned URL HMAC signature must be computed with browser-visible hostname (localhost:9000); using internal Docker client (minio:9000) causes browser signature mismatch |
| Wave 2 user_id=None guard | upload-url sets user_id=None + object_key "null-user/" prefix; confirm skips quota when user_id is None; Plan 03-03 removes both guards |
| SQLite quota xfail(strict=False) | SQLite stores UUID as CHAR(32) without dashes; raw SQL WHERE user_id = :uid never matches str(uuid) dashed format — test-env limitation, not code defect |
| Celery mock required in /confirm tests | extract_and_classify.delay() connects to Redis; monkeypatch blocks it in unit tests; MagicMock pattern established for all confirm endpoint tests |
| get_regular_user raises 403 for admin | Admin is authenticated but must not access document content; 401 would falsely imply unauthenticated — 403 is correct for role rejection |
| Cross-user doc access returns 404 not 403 | Combining "not found" and "wrong owner" into 404 prevents attacker from learning which doc IDs exist for other users (D-16, T-03-11) |
| CASE WHEN replaces GREATEST in quota decrement | SQLite lacks GREATEST scalar function; CASE WHEN used_bytes > :delta THEN used_bytes - :delta ELSE 0 END is semantically equivalent and SQLite-compatible |
| load_topics_for_user uses or_(user_id == x, user_id.is_(None)) | SQLAlchemy is_(None) not == None; or_() combines system topics and user's own topics for namespace-scoped query (D-17, DOC-04) |
| AI-suggested topics go in user namespace | classifier passes user_id=doc.user_id to create_topic; AI-suggested topics are per-user not system-wide (D-11) |
| Celery task signature unchanged for ai_provider | Task receives only document_id; ai_provider/ai_model resolved inside _run via session.get(User, doc.user_id) — prevents broker injection (T-03-19) |
| _DEFAULT_SYSTEM_PROMPT in classifier.py | System prompt env var is optional; hardcoded fallback kept in classifier module not config.py (D-13) |
| Default AI provider is ollama/llama3.2 | Code defaults; overridable via DEFAULT_AI_PROVIDER / DEFAULT_AI_MODEL env vars (D-15) |
| /settings route kept as static placeholder | SettingsView shows admin-managed card; route not removed to avoid UX regression (Risk 6) |
| Plain anchor in quota rejection block | <a href="/settings"> used instead of <router-link> to avoid import dependency in upload component |
| uploadProgress entries owned by parent | Store does not clear uploadProgress map entries after upload; DropZone/parent clears on row dismiss |
| fetchQuota silent catch in auth store | Silent catch keeps last-known values; QuotaBar owns loadFailed state and hides on error (UI-SPEC) |
| XHR PUT progress range 590 | 5 + Math.round(pct * 0.85) maps XHR 0-100 → visual 5-90; remaining 10% covers confirm + enqueue |
| FTS stubs carry both xfail and skipif(INTEGRATION) | skipif fires first in non-INTEGRATION runs (tests appear SKIPPED); xfail catches failures when INTEGRATION=1 — both decorators required |
| Wave 0 stubs: single-line body only | All Phase 4 stubs: body is only pytest.xfail("not implemented yet") — no assertion code; strict=False so xpass never breaks CI |
| GIN index via op.execute() raw SQL | Alembic autogenerate cannot round-trip expression indexes; raw SQL with comment prevents re-creation on every --autogenerate run (issue #1390) |
| put_object_raw not in StorageBackend ABC | audit-logs bucket is MinIO-only; local/WebDAV backends have no audit concept; MinIOBackend-only method |
| write_audit_log uses session.flush() | D-14: caller owns the transaction; flush queues the audit entry without committing — commit remains caller's responsibility |
| Breadcrumb uses iterative Python parent-walk | Not WITH RECURSIVE — ensures SQLite unit tests pass; cycle guard (visited set) prevents infinite loop on malformed data |
| document_move_router is a separate APIRouter | PATCH /api/documents/{id}/folder placed in folders.py not documents.py; separate router with /api/documents prefix avoids circular import |
| FTS plainto_tsquery wrapped in try/except | SQLite silently degrades to unfiltered results when plainto_tsquery unavailable; PostgreSQL works fully — no unit test breakage |
| Share IDOR: DELETE returns 404 not 403 | Prevents share ID enumeration; attacker cannot learn which share IDs exist for other users (T-04-04-02) |
| /received before /{share_id} in router | Path parameter conflict: FastAPI routes /received as /{share_id}="received" if DELETE is defined first — ordering enforced by comment |
| No quota touch in shares.py | Recipient's quota is never modified by share operations (T-04-04-04); sharing is metadata-only from quota's perspective |
| login_failed audit metadata_=None | No email, no hash, no PII in login failure audit events — T-04-07-01 threat mitigation |
| document audit metadata whitelist | document.uploaded contains only size_bytes and storage_backend; document.deleted contains only size_bytes — no filename, no extracted_text |
| CloudConnectionOut whitelist pattern | Pydantic model with exactly the safe fields; credentials_enc absent by omission — SEC-08 safe-by-default |
| admin.user_deleted flush before delete | audit write flushed (session.flush()) while user FK still valid; session.delete(user) follows — preserves audit FK integrity |
| test_admin_impersonation 405 acceptable | DELETE /users/{id} causes GET to return 405 not 422; both mean no GET impersonation endpoint; test updated to accept {404, 405, 422} |
### Open Questions
- Verify cloud SDK minor versions on PyPI before Phase 5 pinning
### Workflow Changes (2026-05-25)
Two mandatory cross-cutting gates added to all phases going forward:
**1. Test gate** — every plan must leave `pytest -v` passing with zero failures. Every new function/endpoint/component requires at least one test. All security-invariant negative tests (wrong owner, admin block, token replay) must exist and pass.
**2. Security gate** — a security agent runs after every plan execution and is a blocking requirement before phase advancement. It:
- Runs `bandit -r backend/`, `pip audit`, `npm audit --audit-level=high`
- Checks for path traversal, IDOR, SSRF, timing attacks, mass assignment, token replay
- Verifies admin endpoints never return `password_hash`, `credentials_enc`, or document content
- Fixes issues directly (full edit access) rather than deferring
**3. Bug fix rule** — all fixes: root cause only, ≤50 lines, regression test required, no workarounds.
See CLAUDE.md "Testing Protocol" and "Security Protocol" sections for full detail.
### Blockers
None.
## Session Continuity
_Updated at each phase transition._
| Field | Value |
|---|---|
| Last session | 2026-05-25 — Phase 3 UAT complete (10/10); security gate passed (3 fixes: bandit B324, Referrer-Policy, IDOR on /topics/suggest); test fix for test_lmstudio.py import |
| Last session | 2026-05-25 — Phase 4 context gathered (4 areas: folder nav, sharing, PDF proxy, audit log) |
| Last session | 2026-05-25 — Phase 4 UI-SPEC approved (6 dimensions: 2 PASS clean, 3 FLAG non-blocking, 0 BLOCK) |
| Last session | 2026-05-25 — Phase 4 plans created (9 plans, 7 waves) + verification passed (0 blockers, 2 warnings) |
| Last session | 2026-05-25 — Plan 04-01 executed: 30 Wave 0 xfail stubs across 5 test files; 39 xfailed total, zero new failures |
| Last session | 2026-05-25 — Plan 04-02 executed: migration 0004 (pdf_open_mode, GIN FTS index, audit-logs bucket) + MinIOBackend.put_object_raw(); 122 tests pass |
| Last session | 2026-05-25 — Plan 04-03 executed: write_audit_log() helper (flush-not-commit, never-raises) + FOLD-01..05 folder API + document sort/FTS/move; 122 pass, 0 new failures |
| Last session | 2026-05-25 — Plan 04-04 executed: Sharing API (SHARE-01..05) — grant/list/received/revoke with IDOR protection; 7 xfailed, zero new failures |
| Last session | 2026-05-28 — Phase 4 UAT complete (14/15 passed, 1 bug found + fixed: duplicate folder on creation); sidebar collapsible folder tree added; Phase 4 marked complete |
| Last session | 2026-05-28 — Phase 5 UI-SPEC approved (6/6 dimensions passed; 2 revision rounds: Cancel label → context-specific, text-lg → text-xl) |
| Last session | 2026-05-28 — Phase 5 planned (8 plans, 7 waves); verification passed (4 blockers → resolved: D-05 API-layer refresh path, SEC-09 cloud cleanup, frontend_url config, RESEARCH resolved markers) |
| Last session | 2026-05-28 — Plan 05-01 executed: Wave 0 Nyquist scaffold — 19 xfail stubs in test_cloud.py, 4 cloud fixtures in conftest.py, 6 package pins, 8 config settings; 172 passed / 43 xfailed |
| Last session | 2026-05-28 — Plan 05-02 executed: cloud_utils.py (SSRF+HKDF), cloud_cache.py (TTLCache), storage factory extended; 199 passed / 43 xfailed / 1 pre-existing failure |
| Next action | Execute Plan 05-03: GoogleDriveBackend + OneDriveBackend (all 7 StorageBackend methods) |
| Pending decisions | None |
| Resume file | `.planning/phases/05-cloud-storage-backends/05-03-PLAN.md` |