Commit Graph

89 Commits

Author SHA1 Message Date
curo1305 7271eeb53c test(06.2-01): add xfail stubs for ADMIN-06 audit enrichment + daily exports (Task 3)
- test_audit_log_includes_user_handle: user_handle and actor_handle in audit items (D-11)
- test_audit_log_filter_by_handle: user_handle query param filters entries (D-12)
- test_audit_log_filter_unknown_handle: unknown handle returns empty list, not 422 (D-12)
- test_daily_exports_list: daily-exports listing endpoint returns {items} (D-15)
- test_daily_export_download: daily export download returns CSV bytes with Content-Disposition (D-16)
2026-05-31 11:58:05 +02:00
curo1305 bbf5355edb test(06.2-01): add xfail stubs for cloud-delete document tests (Task 2)
- test_delete_cloud_document_propagates: cloud backend delete_object called for non-minio docs (D-01)
- test_delete_cloud_document_failure: structured JSON error when provider raises (D-03)
- test_delete_cloud_remove_only: remove_only=true skips cloud delete, removes DB row only (D-02)
2026-05-31 11:57:55 +02:00
curo1305 ecdeffb63d test(06.2-01): add xfail stubs for SHARE-03 permission tests (Task 1)
- test_share_create_with_permission: POST /api/shares permission field (SHARE-03, D-08, D-10)
- test_share_patch_permission: PATCH /api/shares/{id} permission change (SHARE-03, D-09)
- test_share_patch_idor: PATCH by non-owner returns 404 IDOR protection (SHARE-03, T-IDOR)
2026-05-31 11:54:52 +02:00
curo1305 7be48266ae docs(06.2): capture phase context + fix admin user creation 500
- Phase 6.2 CONTEXT.md: cloud-delete propagation, SHARE-03/05, audit
  log CSV export fix, daily export UI, user handle display
- Fix: admin create_user missing session.flush() before write_audit_log
  caused FK violation on PostgreSQL (silent on SQLite)
- Regression test: test_create_user_writes_audit_log in test_admin_api.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 11:00:45 +02:00
curo1305 ce4dc55e4f test(6.1): add SHARE-03 and SHARE-05 Nyquist gap tests
- test_share_default_permission_view: asserts permission='view' in POST
  response and owner's GET /api/shares list (SHARE-03)
- test_share_indicator_in_owner_list: asserts is_shared flips True in
  owner's GET /api/documents after sharing (SHARE-05)

All 14 phase tests now pass (9 shares + 5 audit).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 23:38:06 +02:00
curo1305 451fff1e4d test(6.1): add audit filter behavioral test (ADMIN-06 SC3)
Verifies event_type filter returns only matching entries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 23:30:05 +02:00
curo1305 57784f9f80 fix(6.1): close WR-01/WR-02 code review findings in test_audit.py
WR-01: extend nested metadata_ forbidden-key check to all 4 keys
WR-02: assert no forbidden fields in CSV export body (D-15)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 23:24:26 +02:00
curo1305 21ea3bf169 chore: merge executor worktree (06.1-01 shares tests) 2026-05-30 23:16:38 +02:00
curo1305 9973f42f98 feat(6.1-01): replace 7 xfail stubs with real share tests (SHARE-01..05)
- Remove all 7 @pytest.mark.xfail decorators and pytest.xfail() calls
- Remove unused 'import os'
- Add module-level pytestmark = pytest.mark.asyncio
- Add _make_doc() helper: creates uploaded Document row via ORM
- test_share_success: POST grants share, recipient sees doc in /received
- test_share_handle_not_found: unknown handle returns 404
- test_shared_with_me: metadata fields present, extracted_text absent (T-04-04-03)
- test_share_no_quota_impact: recipient used_bytes stays 0 (T-04-04-04)
- test_revoke_share: DELETE 204, doc gone from recipient /received
- test_share_revoke_wrong_owner_404: IDOR protection, 404 not 403 (T-04-04-02)
- test_share_duplicate: second share of same doc+recipient returns 409
- All 7 tests verified passing in Docker (pytest 9.0.3)
2026-05-30 23:12:24 +02:00
curo1305 bda123db8d feat(6.1-02): promote test_audit.py stubs to real tests (ADMIN-06)
- Replace all 4 @pytest.mark.xfail stubs with real assertions
- Add _seed_audit() helper calling write_audit_log() + commit
- test_audit_log_viewer: verifies paginated JSON shape and total >= 1
- test_audit_log_no_doc_content: asserts no filename/extracted_text in items
- test_audit_log_regular_user_403: asserts 403 for regular users
- test_audit_log_export_csv: asserts text/csv content-type and CSV header line
- Remove unused 'import os'
- Add pytestmark = pytest.mark.asyncio at module level
2026-05-30 23:10:14 +02:00
curo1305 b7df9719c2 feat(6.1-01): add second_auth_user fixture to conftest.py
- Add @pytest_asyncio.fixture second_auth_user with handle prefix 'user2_'
- Creates User + Quota row following the same pattern as auth_user
- Returns {user, token, headers} dict shape for use in sharing tests
2026-05-30 23:09:39 +02:00
curo1305 bf7d86184d fix(documents): normalize UUID to undashed hex in raw SQL quota UPDATE
str(uuid) returns dashed format (xxxx-xxxx-…) which mismatches SQLite's
CHAR(32) storage (undashed hex). Replace with .replace('-', '') so the
WHERE clause matches in both SQLite (tests) and PostgreSQL (production).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 18:57:02 +02:00
curo1305 bd765f69bf test(phase-1): add Nyquist validation tests — STORE-07 concurrent put, fix confirm UUID
- Add test_concurrent_put_objects to test_storage.py (STORE-07: verifies no
  per-instance lock blocks concurrent MinIO workers via asyncio.gather)
- Remove @pytest.mark.xfail from test_confirm_endpoint; test now passes on
  SQLite after uuid format fix in api/documents.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 18:56:58 +02:00
curo1305 b1a136b5be fix(05-12): resolve 3 critical code review findings
CR-01: add `except HTTPException: raise` before broad except in
stream_document_content — prevents 503 (reconnect prompt) from being
swallowed and replaced with misleading 502

CR-02: move pre-flight credential checks BEFORE Redis setex in
oauth_initiate — no orphan state tokens written for unconfigured providers;
also adds onedrive_tenant_id to OneDrive pre-flight condition (WR-02)

CR-03: add CLOUD_CREDS_KEY to celery-worker environment in docker-compose.yml
— worker cannot decrypt cloud credentials without this key; every cloud
document task was silently failing at runtime

WR-03: assert Redis store empty after 400 pre-flight responses in both
new tests — confirms no token leak on misconfigured-provider requests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 18:04:09 +02:00
curo1305 10175ee4b5 fix(05-12): close 3 UAT gaps — OAuth 400 preflight, 502 cloud fallback, upload hint
- oauth_initiate: pre-flight check returns 400 with env-var hint when
  GOOGLE_CLIENT_ID/SECRET or ONEDRIVE_CLIENT_ID/SECRET are not configured,
  preventing opaque MSAL/OAuth library 500 errors on misconfigured servers
- stream_document_content: broad except-clause catches non-CloudConnectionError
  exceptions and returns 502 with user-friendly message (was raw 500)
- docker-compose.yml: add volumes: - ./backend:/app to celery-worker so code
  changes are picked up by docker compose restart without a rebuild
- CloudStorageView: upload hint paragraph directs users to navigate into a
  cloud folder; no DropZone added (no folder context at overview level)
- 3 new backend tests pass; 2 existing tests patched with credential monkeypatch;
  full suite: 293 passed, 0 new failures, 1 pre-existing (test_extract_docx)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 17:55:08 +02:00
curo1305 54ef3357ba fix(05): cloud API path param, root sentinel, webdav creds in list, upload path
cloud.py: list_connections now decrypts and surfaces server_url +
connection_username for nextcloud/webdav providers; folder route uses
{folder_id:path} to handle slashes; translates "root" sentinel to "".
nextcloud_backend.py: skip parent directory entry in PROPFIND Depth:1 results.
webdav_backend.py: add cloud_folder + original_filename params to
upload_object so files land in the user's chosen folder with their real name.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 11:58:01 +02:00
curo1305 34f012b4e8 fix(05): resolve 5 critical code review findings
CR-01: add Field(min_length=1) to UserDeleteConfirm.admin_password
CR-02: add folder ownership check in PATCH /documents/{id} — prevents IDOR
        when folder_id belongs to another user
CR-03: add min_length=1, max_length=255, and path-separator validator to
        DocumentPatch.filename — prevents empty and path-traversal filenames
CR-04: fetchDocumentContent now throws on non-ok responses instead of
        silently returning the error Response
CR-05: object URL revoke in DocumentView uses pagehide + load events with
        120s fallback instead of unreliable 60s blind timer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 11:51:54 +02:00
curo1305 aafd552a1e fix(05-09): set storage_backend='minio' in test_celery_task_uses_user_provider
Cloud-aware routing added in 05-09 checks doc.storage_backend; MagicMock
attribute is truthy and != 'minio', so the test was entering the cloud branch
without any mock for get_storage_backend_for_document. Regression: test passed
before 05-09 when _run() had no cloud routing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 11:43:48 +02:00
curo1305 390a693ec6 feat(05-11): add UserDeleteConfirm model + admin password verification in delete_user
- Import verify_password from services.auth
- Add UserDeleteConfirm Pydantic model (admin_password field)
- delete_user handler now requires body; fails fast with 403 on wrong password
- All existing SEC-09 cloud/MinIO purge logic and audit log unchanged
- Three new tests pass: 204 on correct pw, 403 on wrong pw, 422 on no body
2026-05-30 11:37:59 +02:00
curo1305 8727592bff test(05-11): add failing tests for delete_user password verification
- test_delete_user_correct_password: 204 on correct admin password
- test_delete_user_wrong_password: 403 on wrong password, user survives
- test_delete_user_no_body: 422 when no body provided (Pydantic validation)
2026-05-30 11:37:12 +02:00
curo1305 87de148a59 feat(05-10): OAuth fetch + Nextcloud edit fix + Edit on ERROR + text overflow
- client.js: add initiateOAuth() and getConnectionConfig() helpers
- SettingsCloudTab: replace window.location.href with initiateOAuth() + fetch/JWT
- SettingsCloudTab: add Edit button to ACTIVE and ERROR blocks for non-OAuth providers
- SettingsCloudTab: wrap ConfirmBlock in w-full overflow-hidden div
- CloudCredentialModal: add existing prop, edit-mode pre-population via /config endpoint
- CloudCredentialModal: add showAdvanced + customEndpoint for Nextcloud custom paths
- ConfirmBlock: add break-words class to message paragraph
- cloud.py: add GET /api/cloud/connections/{id}/config endpoint (non-secret fields)
2026-05-30 11:30:13 +02:00
curo1305 e2e499b8b1 feat(05-10): oauth_initiate returns 200 JSON {url} instead of 302 redirect
- Remove response_class=RedirectResponse from @router.get decorator
- Replace both RedirectResponse(status_code=302) returns with JSONResponse({url})
- Frontend can now inject Bearer header before navigating to OAuth URL (T-05-10-01)
- Update test_connect_google_drive to expect 200 JSON (regression fix)
2026-05-30 11:24:33 +02:00
curo1305 9b6d3f91d4 test(05-10): add failing tests for OAuth initiate JSON URL return 2026-05-30 11:23:38 +02:00
curo1305 6d094d17f0 feat(05-09): PATCH /documents/{id} endpoint + cloud-aware Celery re-analyze
- Add DocumentPatch Pydantic model with filename and folder_id optional fields
- Add PATCH /api/documents/{doc_id} endpoint: ownership guard, model_fields_set
  to distinguish absent vs null folder_id, returns updated metadata dict
- Update _run() in document_tasks.py to use get_storage_backend_for_document
  for non-MinIO backends instead of hardcoded MinIO path
- CloudConnectionError caught in cloud path: returns extract_failed status
- Update test to use pure unit mocks (no PostgreSQL) for _run() cloud routing
- All 3 plan tests pass; 23 test_cloud.py tests pass
2026-05-30 11:16:01 +02:00
curo1305 9bc056100c test(05-09): add failing tests for PATCH /documents/{id} and cloud-aware re-analyze
- test_patch_document_filename: expects 200 with updated filename (PATCH endpoint missing → 405)
- test_patch_document_wrong_owner: expects 404 for non-owner (PATCH endpoint missing → 405)
- test_reanalyze_cloud_document_routes_to_cloud_backend: expects cloud backend called for nextcloud docs
2026-05-30 11:13:31 +02:00
curo1305 f1a7f52616 fix(security): bump python-multipart>=0.0.27 and PyMuPDF>=1.26.7 — pip-audit findings 2026-05-29 09:14:27 +02:00
curo1305 d84e38acca test(05-06): promote 11 integration test stubs to real passing tests
- test_connect_google_drive: OAuth initiate redirects to Google (Redis mocked)
- test_oauth_callback_valid_state: valid state + mocked Flow.fetch_token → 302 (CLOUD-01)
- test_oauth_callback_invalid_state: invalid state → error redirect (CLOUD-01)
- test_webdav_connect_validates: localhost URL → 422 (D-17 SSRF)
- test_credentials_enc_not_exposed: credentials_enc absent from response (CLOUD-02, SEC-08)
- test_cloud_upload_no_presigned: cloud upload returns no upload_url (CLOUD-03)
- test_connection_status_display: ACTIVE status in list response (CLOUD-04)
- test_invalid_grant_sets_requires_reauth: 503 on invalid_grant (CLOUD-05)
- test_disconnect_deletes_credentials: DELETE 204 + DB row gone (CLOUD-06)
- test_admin_cannot_see_credentials: admin gets 403 (SEC-08 IDOR)
- test_cross_user_idor: wrong-owner delete → 404 (SEC-08 IDOR)

Also fix CloudConnectionOut.id field validator to accept UUID objects from ORM
(Rule 1: Bug - UUID id caused pydantic validation error on list_connections)

All 20 cloud tests PASSED; full suite: 282 passed, 1 pre-existing failure
2026-05-29 07:51:02 +02:00
curo1305 096bb48116 test(05-06): promote 4 unit test stubs to real passing tests
- test_credential_round_trip: verifies HKDF AES-256-GCM round-trip (CLOUD-02)
- test_ssrf_validation: parametrized over 6 URLs (5 blocked, 1 public) (D-17)
- test_ssrf_link_local: explicit link-local 169.254.x.x check (D-17)
- test_factory_returns_correct_backend: mock-based factory test (CLOUD-07)
All 4 tests PASSED; no xfail decorators remaining on these tests
2026-05-29 07:47:33 +02:00
curo1305 d7d6382d49 feat(05-06): extend upload and content-proxy endpoints for cloud backends
- Add POST /api/documents/upload multipart endpoint with target_backend form field
- Cloud backends (google_drive, onedrive, nextcloud, webdav) use direct put_object()
- MinIO path generates presigned PUT URL (unchanged flow)
- Cloud uploads do NOT touch quota (D-11: separate backend)
- GET /api/documents/{id}/content now uses get_storage_backend_for_document
- CloudConnectionError from any cloud op raises HTTPException(503) with safe message
- target_backend validated against _CLOUD_PROVIDERS allowlist (T-05-06-01)
- Import CloudConnectionError with fallback stub for envs without google-auth deps
2026-05-29 07:45:28 +02:00
curo1305 d85a09719e feat(05-05): add cloud credential cleanup on admin user deletion (SEC-09)
- Import CloudConnection and get_storage_backend_for_document into admin.py
- In delete_user: query all CloudConnection rows for the target user before MinIO cleanup
- For each connection: query documents with matching storage_backend, call delete_object
  best-effort (catch + ignore exceptions — same pattern as MinIO cleanup)
- Explicit session.delete(conn) for each CloudConnection row before user row deletion
- session.flush() after connection deletes to order SQL before user DELETE
- write_audit_log(event_type="cloud.credentials_purged") with providers list metadata
- Cloud cleanup runs BEFORE existing MinIO cleanup: credentials still available to build
  cloud backend instances for delete_object calls (SEC-09)
- No orphaned credentials_enc rows after account deletion (SEC-09 satisfied)
2026-05-29 00:59:10 +02:00
curo1305 f509c37611 feat(05-05): register cloud and users routers in main.py
- Added import and app.include_router(cloud_router) for Phase 5 cloud endpoints
- Added app.include_router(cloud_users_router) for /api/users/me/default-storage
- Both routers registered after the Phase 4 audit router
- All 6 cloud routes + default-storage route visible in app.routes
2026-05-29 00:45:14 +02:00
curo1305 2424f52eee feat(05-05): implement cloud.py — all 7 cloud connection management endpoints
- GET /api/cloud/oauth/initiate/{provider}: generates state token (secrets.token_urlsafe(32)),
  stores in Redis with TTL 1800, redirects to Google Drive or OneDrive OAuth URL
- GET /api/cloud/oauth/callback/{provider}: validates state (single-use Redis key), exchanges
  code for tokens, encrypts credentials, upserts CloudConnection, audit log, redirects to
  {settings.frontend_url}/settings?cloud_connected={provider}; on error redirects with cloud_error=
- POST /api/cloud/connections/webdav: SSRF validates URL, tests health_check, encrypts creds,
  upserts CloudConnection, returns CloudConnectionOut (credentials_enc excluded)
- GET /api/cloud/connections: returns {"items": [CloudConnectionOut]} — credentials_enc never exposed
- DELETE /api/cloud/connections/{connection_id}: returns 404 for wrong-owner (prevents enumeration)
- GET /api/cloud/folders/{provider}/{folder_id}: TTL-cached folder listing via get_cloud_folders_cached
- PATCH /api/users/me/default-storage: updates User.default_storage_backend
- _call_cloud_op helper: transparent token refresh + REQUIRES_REAUTH on invalid_grant
- All endpoints use Depends(get_regular_user) — admin gets 403 (D-18, D-19)
2026-05-29 00:40:08 +02:00
curo1305 a9ea33dd18 feat(05-04): fix storage factory to dispatch nextcloud to NextcloudBackend
- Previously both 'nextcloud' and 'webdav' providers were dispatched to WebDAVBackend
- Now 'nextcloud' uses NextcloudBackend (has list_folder); 'webdav' uses WebDAVBackend
- Both share identical constructor signature (server_url, username, password)
- Removes type: ignore[import] concern on nextcloud_backend — module now exists
2026-05-28 21:12:27 +02:00
curo1305 1b9573f398 feat(05-04): implement NextcloudBackend extending WebDAVBackend
- NextcloudBackend subclasses WebDAVBackend; inherits all 7 StorageBackend methods
- SSRF guard fully inherited: NextcloudBackend("http://10.0.0.1/dav", ...) raises ValueError
- stores self._username for Nextcloud path convention context
- list_folder(folder_path: str = "") async method added — lists via client.list() +
  client.info() wrapped in asyncio.to_thread(), returns [{id, name, is_dir, size}, ...]
- validate_cloud_url called before every asyncio.to_thread() call in list_folder (D-17)
- health_check overrides parent to use client.check("") for Nextcloud root probe
2026-05-28 21:11:12 +02:00
curo1305 bcb887e61d feat(05-03): implement OneDriveBackend — Microsoft Graph StorageBackend
- CloudConnectionError imported from google_drive_backend (shared exception type)
- CHUNK_SIZE = 10 * 1024 * 1024 (10 MB — above Graph 4 MB limit, Pitfall 6)
- All 7 StorageBackend methods implemented as async coroutines
- Resumable upload sessions (createUploadSession) used for ALL uploads
- _ensure_valid_token() checks expiry with 60s buffer, calls _refresh_token() if expired
- _refresh_token() wraps msal.ConfidentialClientApplication in asyncio.to_thread()
- invalid_grant → CloudConnectionError(reason='invalid_grant') per D-06 / B2 design
- presigned_get_url and generate_presigned_put_url raise NotImplementedError (D-14)
- delete_object silently ignores 404 (no-op per StorageBackend contract)
- Backend is stateless — no DB writes (B2 design)
2026-05-28 21:10:56 +02:00
curo1305 311dfa1513 feat(05-04): implement WebDAVBackend with SSRF guard and asyncio wrapping
- All 7 StorageBackend methods implemented as async coroutines
- validate_cloud_url() called in __init__ (SSRF at construct time) and before
  every asyncio.to_thread() call (D-17 defense-in-depth / T-05-04-01, T-05-04-02)
- _make_path() builds "docuvault/{user_id}/{document_id}{ext}" with urllib.parse.quote
  encoding on path segments (RESEARCH.md Pitfall 2)
- presigned_get_url and generate_presigned_put_url raise NotImplementedError (D-14)
- All webdavclient3 sync calls (upload_to, download_from, clean, info, check, mkdir)
  wrapped in asyncio.to_thread() per MinIOBackend pattern
- delete_object silently ignores missing file exceptions (StorageBackend ABC contract)
2026-05-28 21:09:25 +02:00
curo1305 337ee8ef11 feat(05-03): implement GoogleDriveBackend — Google Drive v3 StorageBackend
- CloudConnectionError(reason=) defined in this module — token_expired | invalid_grant
- All 7 StorageBackend methods implemented as async coroutines
- Every sync googleapiclient call wrapped in asyncio.to_thread() (Pitfall 7)
- cache_discovery=False on build() prevents /tmp directory traversal (T-05-03-05)
- presigned_get_url and generate_presigned_put_url raise NotImplementedError (D-14)
- HttpError 401 raises CloudConnectionError(reason='token_expired')
- HttpError 400 with 'invalid_grant' raises CloudConnectionError(reason='invalid_grant')
- HttpError 404 on delete_object is silently swallowed (no-op per contract)
- Backend is stateless — no DB writes (B2 design, D-05/D-06)
2026-05-28 21:07:26 +02:00
curo1305 c406ab1081 test(05-04): add failing RED tests for WebDAVBackend and NextcloudBackend
- Structure tests: all 7 methods async, proper subclassing
- SSRF guard tests: localhost/127.x/10.x/192.168.x/169.254.x raise ValueError
- NotImplementedError tests for presigned methods
- _make_path path construction and percent-encoding tests
- NextcloudBackend subclass, list_folder, inherited SSRF guard
2026-05-28 21:07:18 +02:00
curo1305 4efe7c1376 test(05-03): add RED phase tests for GoogleDriveBackend and OneDriveBackend
- 32 failing tests covering all 7 StorageBackend methods on both backends
- Verifies CloudConnectionError reason attribute (token_expired / invalid_grant)
- Verifies CHUNK_SIZE == 10 MB (Pitfall 6 prevention)
- Verifies shared CloudConnectionError import across backends
- Verifies _ensure_valid_token skips refresh on non-expired tokens
- Verifies _ensure_valid_token raises CloudConnectionError on invalid_grant
2026-05-28 21:06:14 +02:00
curo1305 fb803795fa feat(05-02): implement cloud_cache.py and extend storage factory
- cloud_cache.py: module-level TTLCache(maxsize=1000, ttl=60) singleton with
  threading.Lock for concurrent access safety (RESEARCH.md Pattern 8 / D-16)
- get_cloud_folders_cached(): async function; calls fetch_fn OUTSIDE the lock
  to avoid blocking the event loop during cloud API calls
- invalidate_provider_cache(): removes all cache entries for a user+provider prefix
- storage/__init__.py: adds get_storage_backend_for_document() async factory
  — returns MinIOBackend for minio docs; queries CloudConnection (scoped to user.id),
  decrypts credentials, and lazy-imports cloud backends to avoid circular imports
  — raises HTTPException(503) if connection missing or not ACTIVE (T-05-02-04)
2026-05-28 21:00:48 +02:00
curo1305 976d2ca2de feat(05-02): implement cloud_utils.py — SSRF validation and HKDF credential encryption
- validate_cloud_url(): blocks RFC-1918 (10.x, 172.16.x, 192.168.x), loopback (127.x),
  link-local (169.254.x), IPv6 loopback (::1), ULA (fc00::/7), and 'localhost' string;
  resolves DNS via socket.getaddrinfo BEFORE IP check (anti-DNS-rebinding per D-17)
- _derive_fernet_key(): creates fresh HKDF-SHA256 instance per call (AlreadyFinalized
  pitfall avoided per RESEARCH.md Pitfall 3); uses user_id as salt for per-user isolation
- encrypt_credentials(): Fernet-encrypts JSON-serialised credentials dict; returns str
- decrypt_credentials(): decrypts Fernet token back to original dict
- [Rule 1 - Bug] Fixed test_allows_public_https to use 8.8.8.8 IP (cloud.example.com
  does not resolve in offline CI environments)
2026-05-28 20:58:40 +02:00
curo1305 7fdffddfc1 test(05-02): add failing RED tests for cloud_utils, cloud_cache, and factory
- 11 SSRF validation tests (validate_cloud_url) covering RFC-1918, loopback, link-local, localhost, IPv6
- 7 HKDF credential encryption/decryption round-trip tests (encrypt_credentials, decrypt_credentials)
- 9 TTLCache singleton tests (maxsize=1000, ttl=60, thread-safe lock, get/invalidate helpers)
- 2 storage factory import tests (get_storage_backend_for_document importable)
2026-05-28 20:57:25 +02:00
curo1305 b53ea863dd feat(05-01): add Phase 5 cloud fixtures to conftest.py
Appends 4 new fixtures to backend/tests/conftest.py:
- mock_google_drive_creds: Google OAuth credential dict (access/refresh token, expiry)
- mock_onedrive_creds: OneDrive MSAL credential dict
- mock_webdav_client: MagicMock with upload_to/download_from/list/check methods
- cloud_connection_factory: async factory that creates CloudConnection ORM rows

All existing fixtures and tests unaffected; pytest collection errors = 0.
2026-05-28 20:51:41 +02:00
curo1305 231dfcd987 test(05-01): create test_cloud.py with 15 Phase 5 xfail stubs
All 15 stubs decorated with @pytest.mark.xfail(strict=False) covering
CLOUD-01..07, D-17 SSRF (test_ssrf_validation parametrized + test_ssrf_link_local),
and SEC-08/IDOR (test_admin_cannot_see_credentials, test_cross_user_idor).
pytest tests/test_cloud.py exits 0 with 19 xfailed (19 = 15 stubs + 4 parametrize variants).
2026-05-28 20:49:18 +02:00
curo1305 a052ed4528 feat(05-01): add Phase 5 cloud storage packages and config settings
- Add 6 new packages to requirements.txt: cryptography>=41.0.0,
  google-auth-oauthlib>=1.3.1, google-api-python-client>=2.196.0,
  msal>=1.36.0, webdavclient3>=3.14.7, cachetools>=5.3.0
- Add 8 new Settings fields to config.py: cloud_creds_key,
  google_client_id/secret, onedrive_client_id/secret/tenant_id,
  backend_url (frontend_url already present from Phase 2)
- Append cloud storage section to .env.example
2026-05-28 20:48:38 +02:00
curo1305 87a32b7ee8 feat(phase-4): complete UX redesign — FileManagerView, FolderTreeItem, test suite, and all Phase 4 fixes
Adds the unified file manager view (Windows Explorer-style), collapsible
folder tree sidebar item, full vitest test suite (55 tests, 4 files), and
commits all Phase 4 backend/frontend fixes that were staged but uncommitted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 17:10:52 +02:00
curo1305 8e6005cb73 feat(phase-4): Task 2 — SEC-08 CloudConnectionOut, SEC-09 delete-user cleanup, admin audit writes
- Add CloudConnectionOut Pydantic model (SEC-08): credentials_enc deliberately excluded
- Implement DELETE /api/admin/users/{id} (SEC-09): collects user docs, deletes MinIO
  objects best-effort before DB delete; audit log written within same transaction
- Add write_audit_log calls to: create_user (admin.user_created), update_user_status
  (admin.user_deactivated/admin.user_activated), update_user_quota (admin.quota_changed),
  update_ai_config (admin.ai_provider_assigned), delete_user (admin.user_deleted)
- Add Request param to all admin state-changing handlers for IP extraction
- Fix test_admin_impersonation_not_found: accept 405 in addition to 404/422
  (expected: DELETE /users/{id} exists now, so GET returns 405 — no impersonation
  route still satisfied, just a different HTTP status for non-existent method)
2026-05-25 21:51:34 +02:00
curo1305 e451b16f8f feat(phase-4): Task 1 — audit log backfill in auth.py and documents.py (D-13)
- Add write_audit_log import to auth.py and documents.py
- auth.py: login success (auth.login), login failure (auth.login_failed, no PII),
  logout (auth.logout), logout-all (auth.sign_out_all), change-password
  (auth.password_changed), TOTP enable (auth.totp_enrolled), TOTP disable
  (auth.totp_revoked), backup code used (auth.backup_code_used)
- documents.py: upload confirm (document.uploaded, size+backend only),
  document delete (document.deleted, size only — no filename/extracted_text)
- Add request: Request param to change_password, disable_totp, confirm_upload, delete_document
2026-05-25 21:48:15 +02:00
curo1305 2a0df32e92 feat(phase-4-05): PATCH /api/auth/me/preferences for pdf_open_mode (DOC-01)
- Add PreferencesUpdate Pydantic model with Literal['in_app', 'new_tab'] validation
- Add GET /api/auth/me/preferences — returns current pdf_open_mode
- Add PATCH /api/auth/me/preferences — validates + stores + returns updated value
- Both endpoints use get_current_user (admin can set own prefs, D-10)
- Add 7 preference tests: default GET, in_app, new_tab, invalid 422, persist,
  and two unauthenticated 401 tests
2026-05-25 18:50:52 +02:00
curo1305 f89f787656 feat(phase-4-06): Celery daily audit export task + beat schedule (D-17)
- Create backend/tasks/audit_tasks.py: audit_log_daily_export task
  queries yesterday's AuditLog rows, writes CSV, uploads to MinIO
  audit-logs bucket via put_object_raw(bucket='audit-logs', ...)
- All imports deferred inside _run_daily_export() to prevent circular imports
- celery_app.py: add crontab import, beat entry at midnight UTC,
  tasks.audit_tasks.* routed to documents queue
2026-05-25 18:50:50 +02:00