docs: add shared module map to CLAUDE.md, SECURITY.md, planning artifacts

- CLAUDE.md: add Code Standards section with backend and frontend shared
  module maps, component architecture rules, duplication checklist, and
  no-dead-code enforcement rule
- SECURITY.md: Phase 02 + 03 security audit results (all threats CLOSED)
- .planning: update milestone audit, config, and add plan/UAT files for
  phases 01, 02-06, and 06.2-05

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-06-02 16:10:59 +02:00
parent cce70b2ef6
commit eaa3399ec0
7 changed files with 1282 additions and 215 deletions
+273 -214
View File
@@ -1,30 +1,46 @@
---
milestone: v1.0
audited: 2026-05-30
audited: "2026-05-30T00:00:00Z"
status: gaps_found
scores:
requirements: 48/54
phases_verified: 2/5
integration_blockers: 3
integration_warnings: 6
flows_complete: 2/4
requirements: 44/54
phases_verified: 2/6
integration_blockers: 4
integration_warnings: 7
flows_complete: 3/6
gaps:
requirements:
- id: "SHARE-02"
status: "partial"
phase: "4"
claimed_by_plans: ["04-01-PLAN.md", "04-04-PLAN.md"]
completed_by_plans: ["04-04-SUMMARY.md (Sharing API SHARE-01..05 in commit log)"]
verification_status: "missing"
evidence: "Grant/revocation/list work. But GET /api/documents/{id} enforces doc.user_id == current_user.id — share recipients get 404 on metadata, blocking the document detail view."
claimed_by_plans: ["04-04-PLAN.md", "06.1-01-PLAN.md"]
completed_by_plans: ["06.1-01-PLAN.md"]
verification_status: "gaps_found"
evidence: "Two distinct bugs: (1) backend/api/documents.py line 542 checks doc.user_id != current_user.id and raises 404 — share recipients get 404 on GET /api/documents/{id} despite having a valid Share record; (2) SharedView.vue accesses share.document?.original_name, share.shared_by, share.document?.created_at but /api/shares/received returns a flat object with filename/owner_handle/created_at — all metadata fields render blank."
- id: "DOC-01"
status: "partial"
phase: "4"
claimed_by_plans: ["04-01-PLAN.md"]
completed_by_plans: ["04-09-SUMMARY.md (implicit)"]
claimed_by_plans: ["04-01-PLAN.md", "04-09-PLAN.md"]
completed_by_plans: ["04-09-PLAN.md"]
verification_status: "missing"
evidence: "Owners can view document metadata and extracted text. Share recipients who navigate to /document/{id} get 404 because documents.py:542 checks ownership only, not share grants."
evidence: "Owners can view document metadata and extracted text. Share recipients cannot — documents.py:542 enforces ownership-only check, returning 404 for recipients who navigate to /document/{id}. No share-grant lookup performed before the 404."
- id: "SHARE-03"
status: "partial"
phase: "4"
claimed_by_plans: ["04-04-PLAN.md", "06.1-01-PLAN.md"]
completed_by_plans: ["06.1-01-PLAN.md"]
verification_status: "gaps_found"
evidence: "ShareCreate model has no permission field. grant_share hardcodes permission='view'. No PATCH /api/shares/{id} endpoint exists to change permission after creation. SHARE-03 requires 'owner controls permission level' — only the 'view-only default' half is satisfied. The stored permission field in the shares table cannot be changed through any API endpoint."
- id: "SHARE-05"
status: "partial"
phase: "4"
claimed_by_plans: ["04-04-PLAN.md", "06.1-01-PLAN.md"]
completed_by_plans: ["06.1-01-PLAN.md"]
verification_status: "gaps_found"
evidence: "is_shared computed per document in documents.py lines 433-445 and 498-510 (two separate DB subqueries per list request). Zero occurrences of is_shared in any .vue or .js file. DocumentCard.vue has no visual indicator for shared documents. 06.1-VALIDATION added test_share_indicator_in_owner_list which confirms is_shared=True in the API response — but no frontend component reads or renders it."
- id: "STORE-06"
status: "partial"
@@ -32,323 +48,366 @@ gaps:
claimed_by_plans: ["03-02-PLAN.md"]
completed_by_plans: ["03-02-SUMMARY.md (STORE-06)"]
verification_status: "missing"
evidence: "services/storage.delete_document() always calls MinIOBackend.delete_object() regardless of doc.storage_backend, then decrements MinIO quota. Cloud-stored documents never incremented MinIO quota (D-11), so deletion incorrectly decrements it. Actual cloud provider files are not deleted on user-initiated document delete — they become orphaned."
evidence: "MinIO path: services/storage.py:168-175 implements atomic CASE WHEN quota decrement — correct for MinIO documents. Cloud path: delete_document() calls self._backend().delete_object(doc.object_key) where _backend() always returns MinIOBackend regardless of doc.storage_backend. Cloud-stored documents: (1) MinIO delete_object gets NoSuchKey (silently swallowed); (2) MinIO quota decremented even though no quota was charged at cloud upload; (3) actual file in Google Drive / OneDrive / Nextcloud / WebDAV is never deleted. Additionally, test_delete_decrements_quota is @pytest.mark.xfail(strict=False) — ROADMAP phase gate requires INTEGRATION=1 confirmation against live PostgreSQL."
- id: "SEC-09"
status: "partial"
phase: "4"
claimed_by_plans: ["04-07-PLAN.md", "05-05-PLAN.md"]
completed_by_plans: ["04-07-SUMMARY.md (SEC-09 MinIO cleanup)", "05-05-SUMMARY.md (SEC-09 cloud cleanup)"]
verification_status: "partial — Phase 5 VERIFICATION.md confirms admin delete path; user delete path unverified"
evidence: "Admin-initiated delete (admin.py lines 522546) correctly purges CloudConnection rows before MinIO cleanup. User-initiated document delete (services/storage.delete_document) does not call get_storage_backend_for_document — cloud provider files are orphaned when a user deletes a cloud-stored document."
completed_by_plans: ["04-07-SUMMARY.md", "05-05-SUMMARY.md"]
verification_status: "partial"
evidence: "Admin-initiated account deletion (admin.py lines 518-565) correctly purges all CloudConnection rows and calls delete_user_files() before MinIO+DB cleanup — SEC-09 satisfied for the account deletion path. However, user-initiated document deletion (services/storage.delete_document) does not call get_storage_backend_for_document — cloud provider files are orphaned when a user deletes a cloud-stored document."
- id: "ADMIN-06"
status: "partial"
phase: "4"
claimed_by_plans: ["04-06-PLAN.md"]
completed_by_plans: ["04-06-SUMMARY.md (audit.py)", "04-02-SUMMARY.md (GIN index, audit-logs bucket)"]
verification_status: "missing"
evidence: "GET /api/admin/audit-log JSON viewer works end-to-end. GET /api/admin/audit-log/export: AuditLogTab.vue uses window.location.href for the export button, which does not send the Authorization: Bearer header. get_current_admin requires HTTPBearer — export always returns 403."
claimed_by_plans: ["04-06-PLAN.md", "06.1-02-PLAN.md"]
completed_by_plans: ["06.1-02-PLAN.md"]
verification_status: "gaps_found"
evidence: "GET /api/admin/audit-log JSON viewer works end-to-end. Filter behavioral tests pass (test_audit_log_filter_by_event_type added in commit 451fff1). GET /api/admin/audit-log/export: AuditLogTab.vue:191 uses window.location.href which sends no Authorization: Bearer header. get_current_admin requires HTTPBearer — CSV export always returns 403."
- id: "CLOUD-03"
status: "partial"
phase: "5"
claimed_by_plans: ["05-05-PLAN.md", "05-09-PLAN.md"]
completed_by_plans: ["05-05-SUMMARY.md (CLOUD-03)", "05-06-SUMMARY.md (CLOUD-03)"]
verification_status: "Phase 5 VERIFICATION.md: SATISFIED"
evidence: "PATCH /api/users/me/default-storage endpoint exists and is registered in main.py. updateDefaultStorage() is defined in client.js. However, no Vue component imports or calls updateDefaultStorage(). No UI selector for default backend in SettingsCloudTab.vue or any other component. Default storage (minio) can only be changed via direct API call, not through the UI."
claimed_by_plans: ["05-06-PLAN.md"]
completed_by_plans: ["05-06-SUMMARY.md"]
verification_status: "human_needed"
evidence: "PATCH /api/users/me/default-storage fully implemented (cloud.py:927, registered in main.py). updateDefaultStorage() exported from client.js:448. However, updateDefaultStorage() is never imported or called by any Vue component. SettingsCloudTab.vue renders cloud connections but has no radio/select to change the default storage backend. Users cannot change their default backend through the UI."
- id: "FOLD-01"
status: "partial"
phase: "4"
claimed_by_plans: ["04-03-PLAN.md"]
completed_by_plans: ["04-03-PLAN.md"]
verification_status: "missing"
evidence: "_folder_to_dict() in folders.py:65 returns {id, name, parent_id, user_id, created_at} — no doc_count field. FolderDeleteModal.vue:31 and FolderRow.vue:32 display folder.doc_count ?? 0. FOLD-01 requires 'delete confirms content count before proceeding' — confirmation always shows '0 documents' regardless of actual folder content."
- id: "FOLD-05"
status: "partial"
phase: "4"
claimed_by_plans: ["04-03-PLAN.md"]
completed_by_plans: ["04-03-PLAN.md"]
verification_status: "missing"
evidence: "SearchBar rendered with v-if='currentFolderId' in FileManagerView.vue — hidden at the root level (no folder selected). Users browsing the root document library have no search input. FOLD-05 requires 'Full-text search across user's documents' — search is only available when inside a folder, not at root."
integration:
- blocker: "SHARE-02 / DOC-01 — Share recipient document detail blocked"
description: "documents.py line 542: `if doc is None or doc.user_id != current_user.id: raise HTTPException(404)`. Recipients with valid share records cannot access GET /api/documents/{id} they see 404 despite being able to stream /content."
- blocker: "SHARE-02 / DOC-01 — Share recipient blocked from document detail"
description: "documents.py:542 checks doc.user_id != current_user.id HTTPException(404). No share-grant lookup performed. Recipients with valid Share records cannot access GET /api/documents/{id}, breaking the shared document detail view. SharedView.vue also accesses wrong field paths (share.document?.original_name instead of share.filename etc.) — all metadata renders blank even on the list."
- blocker: "STORE-06 / SEC-09 — Cloud document delete corrupts MinIO quota and orphans cloud files"
description: "services/storage.delete_document() calls self._backend().delete_object(doc.object_key) where _backend() always returns MinIOBackend. Cloud-stored documents: (1) MinIO delete_object silently fails (NoSuchKey); (2) MinIO quota decremented even though no quota was charged at upload; (3) actual file in Google Drive / OneDrive / Nextcloud / WebDAV is never deleted."
- blocker: "STORE-06 / SEC-09 — Cloud document delete corrupts quota and orphans files"
description: "services/storage.delete_document() calls self._backend().delete_object() where _backend() always returns MinIOBackend. Cloud-stored docs: MinIO delete_object silently fails (NoSuchKey), MinIO quota decremented unconditionally, actual cloud provider file never deleted."
- blocker: "ADMIN-06 — Audit log CSV export always returns 403"
description: "AuditLogTab.vue line 191: `window.location.href = '/api/admin/audit-log/export?${params}'`. Browser navigation strips Authorization: Bearer header. Backend requires get_current_admin (HTTPBearer). All admin CSV export clicks result in 403."
description: "AuditLogTab.vue:191 uses window.location.href for CSV export. Browser navigation strips Authorization: Bearer header. Backend endpoint requires HTTPBearer. All CSV export clicks result in 403."
- blocker: "CLOUD-03 — Default storage UI orphaned"
description: "updateDefaultStorage() exported from client.js but never called by any component. No frontend UI exists to change the default storage backend."
flows:
- name: "Recipient views shared document"
breaks_at: "GET /api/documents/{id} — ownership check excludes recipients"
- name: "Recipient views shared document detail"
breaks_at: "documents.py:542 ownership-only check"
affected_requirements: ["SHARE-02", "DOC-01"]
- name: "User deletes cloud document"
- name: "User deletes cloud-stored document"
breaks_at: "services/storage.delete_document() — MinIO backend hardcoded"
affected_requirements: ["STORE-06", "SEC-09"]
- name: "Admin exports audit log"
breaks_at: "AuditLogTab.vue window.location.href drops Bearer token"
- name: "Admin exports audit log as CSV"
breaks_at: "AuditLogTab.vue:191 window.location.href drops Bearer token"
affected_requirements: ["ADMIN-06"]
- name: "User selects default cloud storage backend"
breaks_at: "No UI component calls updateDefaultStorage()"
affected_requirements: ["CLOUD-03"]
tech_debt:
- phase: "02-users-authentication"
items:
- "Phase 2 VERIFICATION.md status=gaps_found (gap: admin JWT → 403 on documents). Gap was closed by Phase 3 (get_regular_user dep added to all /api/documents/* handlers) but no re-verification was run for Phase 2."
- phase: "01-infrastructure-foundation"
items:
- "No VERIFICATION.md — phase marked complete but never formally verified."
- "No VERIFICATION.md exists (phase not formally verified by gsd-verifier)"
- "VALIDATION.md: nyquist_compliant: true, audited 2026-05-30"
- phase: "02-users-authentication"
items:
- "VERIFICATION.md exists (gaps_found 4/5) — SC5 gap closed by Phase 3, no re-verification run"
- "No VALIDATION.md — Nyquist compliance MISSING for Phase 2"
- "4 human verification items pending: TOTP enrollment e2e, password reset email, sign out all devices, admin panel visuals"
- phase: "03-document-migration-multi-user-isolation"
items:
- "No VERIFICATION.md — phase marked complete but never formally verified."
- "No VERIFICATION.md exists (phase not formally verified)"
- "VALIDATION.md: nyquist_compliant: false, status: draft — Nyquist PARTIAL"
- "Document.user_id ORM column has nullable=True but DB has NOT NULL constraint (migration 0003 alters it) — ORM/schema drift"
- "test_delete_decrements_quota is xfail(strict=False) on SQLite — INTEGRATION=1 gate requires live PostgreSQL to confirm"
- phase: "04-folders-sharing-quotas-document-ux"
items:
- "No VERIFICATION.md — phase marked complete but never formally verified."
- phase: "04-folders-sharing-quotas-document-ux"
items:
- "SharedView.vue renders share.document?.created_at and size_bytes, but /api/shares/received returns a flat object (no nested document key) — date/size lines never render for recipients."
- phase: "03-document-migration-multi-user-isolation"
items:
- "Document.user_id ORM column has nullable=True (models.py) but DB has NOT NULL constraint (migration 0003). ORM divergence from actual schema."
- "No VERIFICATION.md exists (phase not formally verified)"
- "VALIDATION.md: nyquist_compliant: false, status: draft — Nyquist PARTIAL"
- "AdminView.vue has no frontend role guard — unauthenticated-role users who navigate to /admin see full UI (all backend calls return 403 but no redirect occurs)"
- "FOLD-01: _folder_to_dict() omits doc_count; delete confirmation always shows 0 documents"
- "FOLD-05: SearchBar hidden at root level (v-if='currentFolderId')"
- "SHARE-05: is_shared computed per document (2 DB subqueries) but never rendered in any Vue component"
- "SHARE-03: permission hardcoded 'view', no PATCH endpoint to change it"
- phase: "05-cloud-storage-backends"
items:
- "cloud.py module docstring claims all endpoints use get_regular_user but OAuth callback intentionally omits it (state-token auth). Misleading, though behavior is correct."
- "CLOUD-05 REQUIRES_REAUTH transitions work for OAuth providers (Google Drive, OneDrive). Nextcloud/WebDAV credential failures produce generic 502 — no REQUIRES_REAUTH state transition. Requirement is OAuth-specific so this is spec-compliant, but a user experience gap."
- "_doc_to_dict() omits storage_backend and folder_id — document list response cannot distinguish cloud vs local docs."
- "VERIFICATION.md: human_needed — 6 items require live cloud credentials (Google OAuth, OneDrive OAuth, live Nextcloud/WebDAV server)"
- "VALIDATION.md: nyquist_compliant: true, audited 2026-05-30"
- "CLOUD-05 REQUIRES_REAUTH transition implemented for OAuth providers only (Google Drive, OneDrive). Nextcloud/WebDAV credential failures produce generic 502 — no REQUIRES_REAUTH state for non-OAuth backends. Spec-compliant but UX gap."
- "_doc_to_dict() omits storage_backend and folder_id — document list response cannot distinguish cloud vs local documents"
- "CLOUD-03: updateDefaultStorage() exported but no UI element calls it"
- phase: "06.1-close-v1-audit-gaps"
items:
- "VERIFICATION.md: stale (written before commit 451fff1 which added audit filter test). 06.1-VALIDATION.md supersedes."
- "VALIDATION.md: nyquist_compliant: true, gaps_found: 3, gaps_resolved: 2, gaps_manual: 1"
- "STORE-06 INTEGRATION=1 gate: manual-only — requires live PostgreSQL Docker stack to confirm"
- "conftest.py WR-03: dependency_overrides not cleared on exception in async_client fixture (low-probability correctness gap)"
- phase: "all"
items:
- "REQUIREMENTS.md checkboxes are stale: many implemented requirements still show [ ]. Last updated 2026-05-21, before any execution."
- "CLAUDE.md specifies ES256 (ECDSA P-256) JWT algorithm, email_hmac deterministic index, and fgp token fingerprint binding. None are implemented HS256 used, email stored in plaintext, no fingerprint claim. These are v2 hardening items outside the 54 formal v1 REQ-IDs."
- "REQUIREMENTS.md checkboxes are stale — 22 satisfied requirements still show [ ]. Not maintained during execution."
- "CLAUDE.md specifies ES256 JWT algorithm, email_hmac deterministic index, fgp token fingerprint claim — none implemented (HS256, plaintext email, no fingerprint). Outside 54 v1 REQ-IDs; v2 hardening scope."
nyquist:
compliant_phases: ["01-infrastructure-foundation", "05-cloud-storage-backends"]
partial_phases: ["03-document-migration-multi-user-isolation", "04-folders-sharing-quotas-document-ux"]
missing_phases: ["02-users-authentication"]
compliant_phases: [1, 5, "6.1"]
partial_phases: [3, 4]
missing_phases: [2]
overall: partial
delta_2026-05-30: "Phase 1 upgraded from partial → compliant after gsd-validate-phase 1: STORE-07 concurrent test added, test_confirm_endpoint unblocked, alembic tests moved to manual-only."
---
# DocuVault v1.0 — Milestone Audit Report
**Milestone:** v1.0
**Audited:** 2026-05-30
**Status:** ⚠ gaps_found
**Score:** 48/54 requirements satisfied (89%)
**Phases audited:** 1, 2, 3, 4, 5, 6.1 (Phase 6 not started — excluded)
**Status:** ⚠ GAPS FOUND
---
## Executive Summary
All 5 phases executed and marked complete. Phase 5 was formally verified (7/7 truths confirmed). Phase 2 has a VERIFICATION.md with one gap that was closed by Phase 3. Phases 1, 3, and 4 have no VERIFICATION.md.
All 6 planned v1 phases executed and marked complete. Phase 5 formally verified (7/7 truths, human_needed). Phase 2 VERIFICATION.md status=gaps_found (4/5); gap confirmed closed by Phase 3. Phases 1, 3, 4 have no VERIFICATION.md. Phase 6.1 VALIDATION.md supersedes its stale VERIFICATION.md.
The integration checker found **3 blockers** and **6 warnings** through cross-phase wiring analysis. Six requirements are partially unsatisfied.
The integration check found **4 blockers** and **7 warnings**. Ten requirements are partially satisfied, primarily due to frontend wiring gaps and a cloud-delete path defect.
| Metric | Score |
|--------|-------|
| Requirements satisfied | 44/54 (81%) |
| Requirements partial | 10/54 (19%) |
| Requirements unsatisfied | 0/54 |
| Phases formally verified | 2/6 (Phases 2, 5) |
| Nyquist compliant | 3/6 phases |
| Test gate | 309 passed, 1 pre-existing failure (test_extract_docx — missing python-docx module; unrelated to milestone scope) |
---
## Requirements Coverage (3-Source Cross-Reference)
## Requirements Coverage
*Sources: VERIFICATION.md (where present) + SUMMARY.md requirements-completed frontmatter + REQUIREMENTS.md traceability + codebase verification*
### Satisfied (44/54)
### Phase 1 — Infrastructure Foundation (3/3 satisfied)
| Phase | REQ-IDs | Count |
|-------|---------|-------|
| 1 — Infrastructure | STORE-01, STORE-02, STORE-07 | 3 |
| 2 — Auth | AUTH-01, AUTH-02, AUTH-03, AUTH-04, AUTH-05, AUTH-06, AUTH-07, AUTH-08, SEC-01, SEC-02, SEC-03, SEC-05, SEC-06, SEC-07, ADMIN-01, ADMIN-02, ADMIN-03, ADMIN-04, ADMIN-05, ADMIN-07 | 20 |
| 3 — Documents | STORE-03, STORE-04, STORE-05, STORE-08, SEC-04, DOC-03, DOC-04, DOC-05 | 8 |
| 4 — Folders/Sharing | FOLD-02, FOLD-03, FOLD-04, SHARE-01, SHARE-04, SEC-08, DOC-02 | 7 |
| 5 — Cloud | CLOUD-01, CLOUD-02, CLOUD-04, CLOUD-05, CLOUD-06, CLOUD-07 | 6 |
| REQ-ID | Description | REQUIREMENTS.md | SUMMARY.md | VERIFICATION.md | Status |
|--------|-------------|-----------------|------------|-----------------|--------|
| STORE-01 | PostgreSQL + MinIO migration | [ ] (stale) | 01-03 ✓ | MISSING | ✅ satisfied |
| STORE-02 | MinIO key schema {user_id}/{doc_id}/{uuid4()}{ext} | [ ] (stale) | not claimed (implicit in 01-04) | MISSING | ✅ satisfied (confirmed in minio_backend.py:75) |
| STORE-07 | Stateless backend | [ ] (stale) | 01-03 ✓ | MISSING | ✅ satisfied (no BackgroundTasks, Celery used) |
**Total satisfied: 44**
### Phase 2 — Users & Authentication (20/20 satisfied)
Notable confirmations from integration check:
- **STORE-08**: Zero `BackgroundTasks` usages remain; all async work runs through Celery (`document_tasks.py`, `email_tasks.py`).
- **DOC-02**: PDF proxy chain complete — `fetchDocumentContent()` → Bearer-authenticated `GET /api/documents/{id}/content``get_storage_backend_for_document()` → byte stream → blob URL.
- **SEC-07**: `get_regular_user` raises 403 for admin role on all `/api/documents/*` endpoints — Phase 2 gap confirmed closed by Phase 3.
- **SEC-08**: `CloudConnectionOut` whitelist (provider, display_name, connected_at, status only) used at `cloud.py:637,661``credentials_enc` excluded from all responses.
*Note: Phase 2 VERIFICATION.md status=gaps_found (4/5). The gap (admin JWT → 403 on /api/documents/*) was closed in Phase 3 by adding get_regular_user dep. Effectively all Phase 2 requirements are satisfied.*
---
| REQ-ID | Description | Status |
|--------|-------------|--------|
| AUTH-01 | Register (Argon2 + HIBP) | ✅ satisfied |
| AUTH-02 | JWT session + httpOnly cookie | ✅ satisfied |
| AUTH-03 | TOTP enrollment + backup codes | ✅ satisfied |
| AUTH-04 | Login via TOTP or backup code | ✅ satisfied |
| AUTH-05 | Password reset email | ✅ satisfied |
| AUTH-06 | Sign out all devices | ✅ satisfied |
| AUTH-07 | Refresh token family revocation | ✅ satisfied |
| AUTH-08 | TOTP single-use within window | ✅ satisfied |
| SEC-01 | CSRF (SameSite=Strict + origin validation) | ✅ satisfied |
| SEC-02 | Rate limiting on auth endpoints | ✅ satisfied |
| SEC-03 | Parameterized queries / ORM only | ✅ satisfied |
| SEC-05 | Security headers (CSP, X-Frame-Options, X-Content-Type-Options) | ✅ satisfied |
| SEC-06 | Constant-time comparison for token verification | ✅ satisfied |
| SEC-07 | Admin role dep + admin blocked from doc content | ✅ satisfied (gap closed Phase 3) |
| ADMIN-01 | Admin creates user | ✅ satisfied |
| ADMIN-02 | Admin deactivates user | ✅ satisfied |
| ADMIN-03 | Admin initiates password reset | ✅ satisfied |
| ADMIN-04 | Admin adjusts user quotas | ✅ satisfied |
| ADMIN-05 | Admin assigns AI provider/model | ✅ satisfied |
| ADMIN-07 | No admin impersonation | ✅ satisfied |
### Partial (10/54) — Blockers and Warnings
### Phase 3 — Document Migration & Multi-User Isolation (8/9 satisfied)
| REQ-ID | Description | Status |
|--------|-------------|--------|
| STORE-03 | Atomic quota enforcement at upload | ✅ satisfied |
| STORE-04 | Quota bar (80%/95% warnings) | ✅ satisfied |
| STORE-05 | Upload rejected at quota limit | ✅ satisfied |
| STORE-06 | Quota decremented on document delete | ⚠️ partial — cloud docs decrement MinIO quota they never incremented; cloud provider file not deleted |
| STORE-08 | BackgroundTasks replaced with Celery | ✅ satisfied |
| SEC-04 | File access via DB lookup only | ✅ satisfied |
| DOC-03 | AI provider/model from admin-assigned DB field | ✅ satisfied |
| DOC-04 | System + per-user topic overrides | ✅ satisfied |
| DOC-05 | Classification uses user's assigned AI config | ✅ satisfied |
### Phase 4 — Folders, Sharing, Quotas & Document UX (11/15 satisfied)
| REQ-ID | Description | Status |
|--------|-------------|--------|
| FOLD-01 | Folder CRUD with count confirmation | ✅ satisfied |
| FOLD-02 | Move documents between folders | ✅ satisfied |
| FOLD-03 | Breadcrumb navigation | ✅ satisfied |
| FOLD-04 | Document list sort | ✅ satisfied |
| FOLD-05 | Full-text search via tsvector | ✅ satisfied |
| SHARE-01 | Share by user handle | ✅ satisfied |
| SHARE-02 | "Shared with me" folder; no quota for recipient | ⚠️ partial — recipient can stream /content but GET /api/documents/{id} returns 404 (ownership-only check) |
| SHARE-03 | View-only default sharing | ✅ satisfied |
| SHARE-04 | Share revocation | ✅ satisfied |
| SHARE-05 | Shared indicator in owner's list | ✅ satisfied |
| SEC-08 | credentials_enc excluded from all serializers | ✅ satisfied |
| SEC-09 | Account deletion purges cloud files | ⚠️ partial — admin delete path correct; user-initiated document delete does not purge cloud provider files |
| ADMIN-06 | Admin audit log viewer | ⚠️ partial — JSON viewer works; CSV export returns 403 (Bearer header dropped by window.location.href) |
| DOC-01 | View document metadata and extracted text | ⚠️ partial — owners: ✅; share recipients: 404 at GET /api/documents/{id} |
| DOC-02 | In-browser PDF preview (bytes proxied, no presigned URLs) | ✅ satisfied |
### Phase 5 — Cloud Storage Backends (6/7 satisfied)
| REQ-ID | Description | Status |
|--------|-------------|--------|
| CLOUD-01 | Connect OneDrive, Google Drive, Nextcloud, WebDAV | ✅ satisfied |
| CLOUD-02 | HKDF per-user credential encryption | ✅ satisfied |
| CLOUD-03 | Local and cloud storage coexist; user selects default | ⚠️ partial — coexist: ✅; select default: API exists but no UI component calls it |
| CLOUD-04 | Connection status display (ACTIVE/REQUIRES_REAUTH/ERROR) | ✅ satisfied |
| CLOUD-05 | invalid_grant transitions to REQUIRES_REAUTH | ✅ satisfied (OAuth providers; WebDAV/Nextcloud don't use OAuth) |
| CLOUD-06 | Disconnect; credentials permanently deleted from DB | ✅ satisfied |
| CLOUD-07 | StorageBackend ABC + factory | ✅ satisfied |
| REQ-ID | Phase | Severity | Root Cause |
|--------|-------|----------|------------|
| **SHARE-02** | 4 | BLOCKER | Recipients get 404 on `GET /api/documents/{id}` (ownership check only). SharedView.vue field names wrong (blank metadata display). |
| **DOC-01** | 4 | BLOCKER | Owners: ✅. Share recipients: 404 at `documents.py:542` (`doc.user_id != current_user.id`, no share-grant check). |
| **STORE-06** | 3 | BLOCKER | MinIO delete-path correct. Cloud delete-path: MinIO backend called unconditionally → quota corrupted, cloud file orphaned. |
| **SEC-09** | 4 | BLOCKER | Admin account deletion: ✅. User-initiated document delete: cloud provider file not deleted (only MinIO attempted). |
| **ADMIN-06** | 4 | BLOCKER | JSON audit viewer: ✅. CSV export: `window.location.href` drops Bearer header → 403. |
| **SHARE-03** | 4 | WARNING | `permission="view"` hardcoded. No `PATCH /api/shares/{id}` endpoint. Owner cannot change permission level. |
| **SHARE-05** | 4 | WARNING | `is_shared` computed per document (2 DB subqueries / request) but never rendered by any Vue component. |
| **CLOUD-03** | 5 | WARNING | `PATCH /api/users/me/default-storage` implemented. `updateDefaultStorage()` exported but no UI calls it. |
| **FOLD-01** | 4 | WARNING | `_folder_to_dict()` omits `doc_count`. Delete confirmation modal always shows "0 documents". |
| **FOLD-05** | 4 | WARNING | `SearchBar` hidden at root (`v-if="currentFolderId"`). Full-text search unavailable in root document library. |
---
## Phase Verification Status
| Phase | VERIFICATION.md | Status | Score | Notes |
|-------|-----------------|--------|-------|-------|
| 01 — Infrastructure Foundation | ❌ MISSING | Unverified | — | No formal verification run |
| 02 — Users & Authentication | ✅ Present | gaps_found (4/5) | 4/5 | Gap closed by Phase 3 (get_regular_user on /api/documents/*) |
| 03 — Document Migration | ❌ MISSING | Unverified | — | No formal verification run |
| 04 — Folders, Sharing, Quotas | ❌ MISSING | Unverified | — | No formal verification run |
| 05 — Cloud Storage Backends | ✅ Present | human_needed | 7/7 | 6 human UAT items (cloud credentials required) |
| Phase | VERIFICATION.md | Status | Notes |
|-------|----------------|--------|-------|
| 01 — Infrastructure Foundation | ❌ MISSING | Unverified | Phase marked complete; no formal verification run |
| 02 — Users & Authentication | ✅ exists | gaps_found (4/5) | SC5 gap (admin JWT → 403 on docs) closed by Ph3; confirmed by integration check |
| 03 — Document Migration | ❌ MISSING | Unverified | Phase marked complete; no formal verification run |
| 04 — Folders, Sharing, Quotas | ❌ MISSING | Unverified | Phase marked complete; no formal verification run |
| 05 — Cloud Storage Backends | ✅ exists | human_needed (7/7) | All must-haves verified; 6 human UAT items require live cloud credentials |
| 6.1 — Gap Closure | ✅ exists (stale) | Superseded by VALIDATION.md | 06.1-VALIDATION.md: gaps_found 3, resolved 2, manual 1 |
---
## Nyquist Compliance (Validation Coverage)
## Nyquist Coverage
| Phase | VALIDATION.md | nyquist_compliant | Action |
|-------|---------------|-------------------|--------|
| 01 — Infrastructure Foundation | ✅ exists (compliant) | ✅ true | — *(validated 2026-05-30)* |
| 02 — Users & Authentication | ❌ missing | — | `/gsd:validate-phase 2` |
| 03 — Document Migration | ✅ exists (draft) | false | `/gsd:validate-phase 3` |
| 04 — Folders, Sharing, Quotas | ✅ exists (draft) | false | `/gsd:validate-phase 4` |
| 05 — Cloud Storage Backends | ✅ exists (complete) | ✅ true | |
| 01 — Infrastructure Foundation | ✅ audited 2026-05-30 | `true` | None |
| 02 — Users & Authentication | ❌ MISSING | — | `/gsd:validate-phase 2` |
| 03 — Document Migration | ✅ exists, status: draft | `false` | `/gsd:validate-phase 3` |
| 04 — Folders, Sharing, Quotas | ✅ exists, status: draft | `false` | `/gsd:validate-phase 4` |
| 05 — Cloud Storage Backends | ✅ audited 2026-05-30 | `true` | None |
| 6.1 — Gap Closure | ✅ audited 2026-05-30 | `true` | None |
---
## Critical Blockers (3)
## Critical Blockers (5)
### BLOCKER-1 — Share Recipient Cannot View Document Metadata
### BLOCKER-1 — Share Recipient Cannot View Document Metadata (SHARE-02, DOC-01)
**Affected Requirements:** SHARE-02, DOC-01
**File:** `backend/api/documents.py` line 542
**Root cause:** `if doc is None or doc.user_id != current_user.id: raise HTTPException(404)` — no share-grant check.
**Broken flow:** SharedView.vue → click shared item → DocumentView.vue → `api.getDocument(id)` → 404 for recipient.
**Fix:** Add share-grant check to `get_document()`: if `doc.user_id != current_user.id`, query `Share` table for `(document_id=doc_id, recipient_id=current_user.id)` and allow if found.
**Broken flow:** SharedView.vue → click shared item → DocumentView.vue → `getDocument(id)` → 404 for recipient despite valid Share record.
**Secondary bug:** SharedView.vue accesses `share.document?.original_name`, `share.shared_by`, `share.document?.created_at` but `/api/shares/received` returns a flat object (`filename`, `owner_handle`, `created_at`). All metadata renders blank even on the list.
**Fix (backend):** In `get_document()`, after the ownership 404, add: check `Share` table for `(document_id=doc_id, recipient_id=current_user.id)` and allow if found.
**Fix (frontend):** In `SharedView.vue`, update field access to match flat response shape.
### BLOCKER-2 — Cloud Document Delete Corrupts Quota and Orphans Files
### BLOCKER-2 — Cloud Document Delete Corrupts Quota and Orphans Files (STORE-06, SEC-09)
**Affected Requirements:** STORE-06, SEC-09
**File:** `backend/services/storage.py` (delete_document function)
**Root cause:** `self._backend().delete_object(doc.object_key)` always uses MinIOBackend regardless of `doc.storage_backend`. Then decrements MinIO quota unconditionally.
**Broken flow:** User uploads to Google Drive (quota=0) → deletes document → `delete_object()` gets NoSuchKey on MinIO (silently swallowed) quota decremented below actual MinIO usage → actual Google Drive file never deleted.
**Fix:** Use `get_storage_backend_for_document(doc, session)` in `delete_document()` (same pattern as admin delete). Gate quota decrement on `doc.storage_backend == "minio"`.
**Impact:** Cloud-stored documents: (1) MinIO `delete_object` gets NoSuchKey (silently swallowed), (2) MinIO quota decremented below actual usage, (3) actual cloud provider file never deleted → GDPR Article 17 obligation not met for cloud storage.
**Fix:** Use `get_storage_backend_for_document(doc, session)` in `delete_document()`. Gate quota decrement on `doc.storage_backend == "minio"`.
### BLOCKER-3 — Admin Audit Log CSV Export Always Returns 403
### BLOCKER-3 — Admin Audit Log CSV Export Always Returns 403 (ADMIN-06)
**Affected Requirements:** ADMIN-06
**File:** `frontend/src/components/admin/AuditLogTab.vue` line 191
**Root cause:** `window.location.href = '/api/admin/audit-log/export?${params}'` — browser navigation strips Authorization: Bearer header. `get_current_admin` requires HTTPBearer.
**Broken flow:** Admin clicks "Export CSV" → 403 Forbidden.
**Fix:** Use `fetch()` with `Authorization: Bearer ${accessToken}` header and download the blob via `URL.createObjectURL()`, or pass the access token as a query param (less secure but simple).
**Root cause:** `window.location.href = '/api/admin/audit-log/export?${params}'` — browser navigation strips the `Authorization: Bearer` header. `get_current_admin` requires `HTTPBearer`.
**Fix:** Replace `window.location.href` with `fetch()` using `Authorization: Bearer ${accessToken}`, then create a Blob URL for download. The `fetchDocumentContent()` pattern in `client.js` is the correct model.
### BLOCKER-4 — Default Storage Backend Has No Frontend UI (CLOUD-03)
**File:** `frontend/src/components/settings/SettingsCloudTab.vue`
**Root cause:** `updateDefaultStorage()` is exported from `client.js:448` but never imported or called by any component. `SettingsCloudTab.vue` has no UI control to select a default backend.
**Fix:** Add a "Set as default" button or radio to each connected provider row in `SettingsCloudTab.vue`; wire it to `updateDefaultStorage(provider)`.
---
## Warnings (6)
## Warnings (7)
| # | Severity | Description | Requirement |
|---|----------|-------------|-------------|
| W-1 | Medium | SharedView.vue uses `share.document?.created_at` but /api/shares/received returns flat objects — date/size lines never render | SHARE-02 |
| W-2 | Medium | `updateDefaultStorage()` defined in client.js but never called; no default-backend UI selector exists | CLOUD-03 |
| W-3 | Low | `_doc_to_dict()` omits `storage_backend` and `folder_id` — list response cannot distinguish cloud vs local docs | CLOUD-03 |
| W-4 | Low | `Document.user_id` ORM column has `nullable=True` but DB has `NOT NULL` constraint (migration 0003) — ORM/schema drift | STORE-03 |
| W-5 | Low | cloud.py module docstring says all endpoints use `get_regular_user` but OAuth callback intentionally omits it | — |
| W-6 | Info | CLAUDE.md specifies ES256, email_hmac, fgp fingerprint claim — none implemented (HS256, plaintext email, no fingerprint). Outside 54 v1 REQ-IDs. | v2 scope |
| # | Description | Requirement |
|---|-------------|-------------|
| W-1 | `is_shared` computed per document (2 subqueries per list request) but no Vue component renders it | SHARE-05 |
| W-2 | SHARE-03: `permission` hardcoded to `"view"`; no `PATCH /api/shares/{id}` endpoint | SHARE-03 |
| W-3 | `_folder_to_dict()` omits `doc_count` — delete confirmation modal always shows "0 documents" | FOLD-01 |
| W-4 | `SearchBar` hidden at root level (`v-if="currentFolderId"`) — search unavailable in root library | FOLD-05 |
| W-5 | `Document.user_id` ORM column `nullable=True`; DB has `NOT NULL` constraint (migration 0003) — ORM/schema drift | STORE-03 |
| W-6 | `AdminView.vue` has no frontend role guard — regular users who navigate to `/admin` see full UI; backend returns 403 but no redirect | — |
| W-7 | CLAUDE.md specifies ES256 JWT, `email_hmac` index, `fgp` fingerprint claim — none implemented (HS256, plaintext email, no fingerprint). v2 hardening scope, outside 54 v1 REQ-IDs. | v2 |
---
## Tech Debt by Phase
## E2E Flow Results
**Phase 01:** No VERIFICATION.md written.
**Phase 02:** VERIFICATION.md status=gaps_found; Phase 3 closed the gap but no re-verification was run.
**Phase 03:** No VERIFICATION.md written. Document.user_id ORM nullable divergence.
**Phase 04:** No VERIFICATION.md written. VALIDATION.md in draft state.
**Phase 05:** CLOUD-05 REQUIRES_REAUTH transition not implemented for WebDAV/Nextcloud (spec-compliant; quality gap).
**All:** REQUIREMENTS.md checkboxes not maintained during execution — many satisfied requirements still show `[ ]`.
| Flow | Status | Break Point |
|------|--------|-------------|
| MinIO upload → quota updated → Celery AI classification | ✅ COMPLETE | — |
| Password reset → TOTP gate on next login | ✅ COMPLETE | — |
| Cloud upload → authenticated content proxy (blob URL) | ✅ COMPLETE | — |
| Share document → "Shared with me" list → recipient views detail | ❌ BROKEN | `documents.py:542` ownership-only check + SharedView field mismatch |
| User deletes cloud-stored document → files purged | ❌ BROKEN | `delete_document()` hardcodes MinIOBackend |
| Admin views audit log → exports CSV | ⚠️ PARTIAL | JSON viewer works; CSV export → 403 (no Bearer in `window.location.href`) |
---
## Integration Wiring Summary
## Integration Wiring Summary (47 connections)
| Connection | Status |
|------------|--------|
| Auth deps (get_regular_user / get_current_admin) on all protected endpoints | ✅ All wired |
| Phase 2 admin gap (admin JWT → 403 on /api/documents/*) | ✅ Closed in Phase 3 |
| Atomic quota at upload (MinIO path) | ✅ Wired |
| Atomic quota decrement at delete (MinIO path only) | ⚠️ Cloud path broken |
| Cloud document content proxy (authenticated fetch) | ✅ Wired |
| Admin delete: cloud cleanup before MinIO before DB | ✅ Wired (SEC-09) |
| User-initiated doc delete: cloud provider cleanup | ❌ Not wired (STORE-06, SEC-09) |
| Share recipient access to /content | ✅ Wired |
| Share recipient access to GET /documents/{id} | ❌ Ownership check blocks recipients |
| Auth deps (`get_regular_user` / `get_current_admin`) on all Phase 3-5 endpoints | ✅ All wired (verified across documents.py, folders.py, shares.py, cloud.py, audit.py) |
| Phase 2 admin gap (admin JWT → 403 on `/api/documents/*`) | ✅ Closed in Phase 3 by `get_regular_user` |
| Atomic quota at upload (MinIO path) | ✅ Wired (`documents.py:342-346`) |
| Atomic quota decrement at delete (MinIO path) | ✅ Wired (`services/storage.py:168-175`) |
| Atomic quota decrement at delete (cloud path) | ❌ Not wired — MinIOBackend hardcoded |
| Cloud document content proxy (authenticated fetch → blob URL) | ✅ Wired |
| Admin delete: cloud cleanup → MinIO cleanup → DB delete | ✅ Wired (`admin.py:518-565`) |
| User-initiated doc delete: cloud provider cleanup | ❌ Not wired |
| Share recipient access to `/api/documents/{id}/content` | ✅ Wired (content proxy uses `get_storage_backend_for_document`) |
| Share recipient access to `GET /api/documents/{id}` metadata | ❌ Ownership check blocks recipients |
| Admin audit log JSON viewer | ✅ Wired end-to-end |
| Admin audit log CSV export | ❌ Bearer header dropped |
| Default storage backend selection UI | ❌ Client function orphaned, no UI |
| HKDF credential encryption throughout cloud flows | ✅ Wired |
| All routers registered in main.py | ✅ Confirmed |
| Admin audit log CSV export | ❌ Bearer header dropped by `window.location.href` |
| Default storage backend selection UI | ❌ `updateDefaultStorage()` orphaned no UI calls it |
| HKDF credential encryption through all cloud flows | ✅ Wired |
| `write_audit_log()` called from documents (3), shares (2), folders (3), cloud (4) | ✅ All wired |
| All API routers registered in `main.py` | ✅ Confirmed |
| `get_storage_backend_for_document()` factory in content proxy + Celery task | ✅ Wired |
| `SHARE-05 is_shared` computed in API → frontend | ❌ Computed, never rendered |
---
## Remediation Plan
## Tech Debt Summary
3 blockers require closure phases (or targeted inline fixes). In priority order:
**Phase 01:** No VERIFICATION.md. VALIDATION.md compliant.
### Gap 1 — Share recipient metadata access (BLOCKER-1)
Affects: SHARE-02, DOC-01
Effort: Small — add share-grant check to `get_document()` in `documents.py` (~15 lines)
**Phase 02:** VERIFICATION.md gaps_found (Phase 2 SC5 closed by Phase 3). No VALIDATION.md (Nyquist MISSING for Phase 2). 4 human verification items pending.
### Gap 2 — Cloud document delete (BLOCKER-2)
Affects: STORE-06, SEC-09
Effort: Medium — refactor `delete_document()` in `services/storage.py` to use `get_storage_backend_for_document()` and conditionally decrement quota (~30 lines)
**Phase 03:** No VERIFICATION.md. VALIDATION.md draft (Nyquist PARTIAL). `Document.user_id` ORM nullable vs DB NOT NULL drift. `test_delete_decrements_quota` xfail — INTEGRATION=1 required.
### Gap 3 — Admin audit log CSV export (BLOCKER-3)
Affects: ADMIN-06
Effort: Small — change `window.location.href` to `fetch()` with Bearer header and blob download in `AuditLogTab.vue` (~20 lines)
**Phase 04:** No VERIFICATION.md. VALIDATION.md draft (Nyquist PARTIAL). AdminView.vue missing frontend role guard. Multiple UI gaps (FOLD-01, FOLD-05, SHARE-05, SHARE-03, SHARE-02 field names).
These three fixes are small enough to close as a single gap-closure phase or inline as part of `/gsd:complete-milestone v1.0` pre-work.
**Phase 05:** VERIFICATION.md human_needed (must-haves all confirmed). VALIDATION.md compliant. CLOUD-03 UI orphaned. CLOUD-05 REQUIRES_REAUTH only for OAuth providers.
**Phase 06.1:** VERIFICATION.md stale (superseded). VALIDATION.md compliant. STORE-06 manual gate pending. conftest.py WR-03 teardown gap.
**Cross-cutting:** REQUIREMENTS.md checkboxes not maintained (22 satisfied reqs still show `[ ]`). CLAUDE.md v2 hardening items (ES256, email_hmac, fgp fingerprint) not yet implemented.
---
## Remediation Guide
**Run Nyquist validation first (may close some verification gaps retroactively):**
```
/gsd:validate-phase 2
/gsd:validate-phase 3
/gsd:validate-phase 4
```
**Then insert closure phases for remaining blockers:**
```
/clear then:
/gsd:phase --insert 6.2 "Close v1 sharing + cloud-delete + CSV export gaps"
/gsd:discuss-phase 6.2
/gsd:plan-phase 6.2
/gsd:execute-phase 6.2
```
Suggested scope for Phase 6.2 (all small fixes, could ship as one phase):
| Fix | Files | Effort | REQ-IDs |
|-----|-------|--------|---------|
| Share recipient access: add share-grant check to `get_document()` | `documents.py:542` | ~15 lines | SHARE-02, DOC-01 |
| Fix SharedView.vue field names | `SharedView.vue` | ~10 lines | SHARE-02 |
| Cloud delete: use `get_storage_backend_for_document()` + gate quota decrement | `services/storage.py` | ~25 lines | STORE-06, SEC-09 |
| Audit CSV export: fetch() + Bearer + blob download | `AuditLogTab.vue:191` | ~20 lines | ADMIN-06 |
| Default storage UI: "Set as default" button in SettingsCloudTab | `SettingsCloudTab.vue` | ~30 lines | CLOUD-03 |
| Add `is_shared` indicator to DocumentCard.vue | `DocumentCard.vue` | ~15 lines | SHARE-05 |
| Add `doc_count` to `_folder_to_dict()` | `folders.py:65` | ~10 lines | FOLD-01 |
| Remove `v-if="currentFolderId"` gate from SearchBar | `FileManagerView.vue` | ~5 lines | FOLD-05 |
| Add `PATCH /api/shares/{id}` permission endpoint | `shares.py` | ~30 lines | SHARE-03 |
| Add frontend role guard to AdminView route or component | `router/index.js` or `AdminView.vue` | ~10 lines | — |
| Confirm `test_delete_decrements_quota` under INTEGRATION=1 | `test_quota.py:196` | manual | STORE-06 |
---
**Also available:**
- `cat .planning/v1.0-MILESTONE-AUDIT.md` — this report
- `/gsd:complete-milestone v1.0` — proceed with gaps noted (accept as tech debt)
---
_Audited: 2026-05-30_
_Auditor: Claude (gsd-audit-milestone)_
_Integration checker: gsd-integration-checker_
_Integration checker: gsd-integration-checker (155 tool calls, 47 connections verified)_