storage_key from all API responses; all handlers in api/documents.rs and api/versions.rs updatedvalidate_metadata()) and service update path"unavailable" string instead of raw DB error messages for unauthenticated callerslog_audit() now returns AppResult<()>, all callers use .await? across all 4 services (document, folder, tag, version)cc-debian12:nonroot base, EXPOSE 8081 (corrected), explicit binary name ods-docstore, migrations copiedEXPOSE 8080 which diverged from the architecture spec (DocStore = port 8081). Corrected in this commit.
| ID | Criterion | Status | Evidence |
|---|---|---|---|
| AC-029 NEW | API responses must not expose internal storage_key |
MET |
domain/document.rs:43-84 — DocumentResponse struct excludes storage_key; From<Document> copies only safe fields.
domain/version.rs:17-45 — VersionResponse same pattern.
All API handlers updated: api/documents.rs lines 21,35,70,91; api/versions.rs lines 24,37,51.
Tests: test_document_response_excludes_storage_key, test_version_response_excludes_storage_key
|
| AC-030 NEW | Metadata JSON max nesting depth of 5 | MET |
domain/document.rs:97-122 — METADATA_MAX_DEPTH=5, json_depth() recursive traversal, errors if depth > 5.
Applied in CreateDocument.validate() and DocumentService.update() (lines 86-93).
Tests: test_metadata_too_deep (6 levels → error), test_metadata_exactly_at_limits (5 levels → ok)
|
| AC-031 NEW | Metadata JSON max 50 total keys | MET |
domain/document.rs:113-122 — METADATA_MAX_KEYS=50, json_total_keys() recursive count, errors if keys > 50.
Tests: test_metadata_too_many_keys (51 → error), test_metadata_exactly_at_limits (50 → ok)
|
| AC-032 NEW | /ready must not leak DB error details to unauthenticated callers | MET |
api/health.rs:33-37 — Err branch logs via tracing::error! and returns ReadyResponse { database: "unavailable" }.
Test: test_ready_error_response_hides_details — asserts field is "unavailable" and does not contain "error", "password", or "connection"
|
| AC-033 NEW | Audit log write failures must fail the mutation | MET |
Previously if let Err(e) = self.audit.create(...) { warn!(...) } across all services — now:
DocumentService.log_audit() returns AppResult<()>, callers use .await? (lines 49, 103, 130, 161).
FolderService, TagService, VersionService same pattern.
Structural test: test_audit_failure_propagation_design — compiler enforces via type system
|
| AC-034 NEW | Dockerfile: distroless, non-root, correct port, explicit binary | MET FIX |
Line 7: FROM gcr.io/distroless/cc-debian12:nonroot (no shell, no OS tools).
Line 12: EXPOSE 8081 (corrected from 8080).
Line 13: USER nonroot:nonroot.
Line 8: COPY .../ods-docstore /usr/local/bin/service (explicit binary, not glob).
Line 9: COPY .../migrations /app/migrations (available at runtime).
|
| 6 new criteria | 6 MET | ||
| ID | Criterion | Status | Evidence (summary) |
|---|---|---|---|
| AC-001 | All tables use schema docstore | MET | migrations/001_init.sql:5 — CREATE SCHEMA IF NOT EXISTS docstore; all tables as docstore.* |
| AC-002 | RLS on all tables with tenant_id isolation | MET | migrations/001_init.sql:100-120 — ENABLE ROW LEVEL SECURITY with USING (tenant_id = current_setting('app.current_tenant_id')::uuid) |
| AC-003 | tenant_id from JWT only | MET | api/middleware.rs — Claims struct; no tenant_id in any request body |
| AC-004 | All endpoints require valid JWT | MET | All handlers take AuthContext extractor; returns 401 on invalid/missing token |
| AC-005 | Write ops: editor/tenant-admin/super-admin only | MET | api/roles.rs:6 — WRITE_ROLES; enforced in all mutation handlers |
| AC-006 | Audit API: tenant-admin/super-admin only | MET | api/roles.rs:9 — AUDIT_ROLES; enforced in list_audit_by_entity. LOW: audit.rs has local copy |
| AC-007 | Audit trail for all mutations | PARTIAL | Not audited: TagService.assign_to_document() and remove_from_document() — no audit.create() calls |
| AC-008 | Audit log schema: full fields | MET | migrations/002_audit_log.sql — entity_type, entity_id, action, actor_id, correlation_id, details (JSONB) |
| AC-009 | RLS on audit_log | MET | migrations/002_audit_log.sql:30-33; test_audit_tenant_isolation verifies cross-tenant isolation |
| AC-010 | CloudEvents v1.0 for all state changes | PARTIAL | No events for TagService.assign_to_document() / remove_from_document(); no TAG_ASSIGNED/TAG_REMOVED constants |
| AC-011 | CloudEvents include tenantid extension | MET | events/producer.rs:17 — tenantid: String field in CloudEvent |
| AC-012 | Events on events.docstore topic | MET | config.rs — kafka_topic defaults to events.docstore |
| AC-013 | JWT: RS256 + iss/aud/nbf/leeway | MET | api/middleware.rs:55-71 — Algorithm::RS256, validate_exp, validate_nbf, configurable leeway |
| AC-014 | JWT expiry validated on every request | MET | api/middleware.rs:64 — validation.validate_exp = true |
| AC-015 | Soft delete only | MET | repository/document.rs:151-176 — sets deleted_at/status='deleted'; no DELETE FROM in document/folder repos |
| AC-016 | X-Correlation-Id propagated or generated | MET | api/auth.rs:51-56 — reads header or generates UUID v4; stored in audit_log |
| AC-017 | GET /health — 200 with service info | MET | main.rs:90 — {status, service, version} |
| AC-018 | GET /ready — DB probe, 503 on failure | MET | main.rs:91 — runs SELECT 1, returns 503 if DB unavailable |
| AC-019 | Dockerfile multi-stage, correct port, non-root | MET FIXED | Superseded by AC-034. Previous LOW deviation (EXPOSE 8080) resolved in this commit. |
| AC-020 | All config from environment variables | MET | config.rs — AppConfig::from_env(); no hardcoded production secrets |
| AC-021 | All timestamps in UTC (TIMESTAMPTZ) | MET | All migrations use TIMESTAMPTZ; Rust types use DateTime<Utc> |
| AC-022 | Parameterized queries only | MET | All repos use sqlx::query_as with .bind() |
| AC-023 | Documents CRUD at /api/v1/documents | MET | main.rs:93-112 — POST/GET/GET{id}/PATCH{id}/DELETE{id} |
| AC-024 | Folders CRUD at /api/v1/folders | MET | main.rs:114-124 — full CRUD |
| AC-025 | Versioning at /api/v1/documents/{id}/versions | MET | main.rs:126-138 — POST/GET/GET{version_id}; auto-increment |
| AC-026 | Tags API + assign/remove | MET | main.rs:139-150 — full tag CRUD + assign/remove routes |
| AC-027 | Audit query at /api/v1/audit/entity/{id} | MET | main.rs:151-154; tenant-scoped, paginated, role-restricted |
| AC-028 | No production secrets in source code | PARTIAL | RSA test key pair in src/api/middleware.rs and tests/e2e/common.rs — #[cfg(test)], not in release binary, but committed to git history |
| 28 carried-over criteria | 25 MET · 3 PARTIAL | ||
| Severity | Description | Spec Reference | Since |
|---|---|---|---|
| MEDIUM |
Tag operations not audited.
TagService.assign_to_document() and remove_from_document() perform no audit.create() calls. Every mutation must be logged.
|
CLAUDE.md: "Audit trail: log who/when/what for every mutation" | 2d9eca2 |
| MEDIUM |
Tag assign/remove emit no CloudEvents.
No TAG_ASSIGNED or TAG_REMOVED constants. assign_to_document() and remove_from_document() produce no events.
|
architecture.md: "Event-First — Every state change emits a CloudEvent" | 2d9eca2 |
| LOW |
api/audit.rs defines its own const AUDIT_ROLES instead of importing crate::api::roles::AUDIT_ROLES. Values are identical now but will silently diverge if the canonical definition is updated.
|
src/api/roles.rs — canonical definition since 15e9eb5 |
15e9eb5 |
| LOW |
RSA-2048 test key pair (TEST_RSA_PRIVATE_KEY, TEST_RSA_PUBLIC_KEY) committed to source. Test-only (#[cfg(test)]), not in release binary, but private key material is in git history.
|
business-rules.md: "No secrets in source code or Docker images" | 15e9eb5 |
| LOW |
Contract tests (Pact) not implemented. Listed as pending in tasks/todo.md.
|
CLAUDE.md Checklist: "Write API contract tests (Pact) FIRST" | initial |
| LOW |
Redpanda consumer not implemented. Only event production in src/events/producer.rs. May be intentional for P1 phase scope.
|
CLAUDE.md Checklist: "Implement Redpanda producer (CloudEvents) + tests" | initial |
| LOW | Integration tests use direct DATABASE_URL, not testcontainers. Global rules require testcontainers for portability and isolation. | ~/.claude/CLAUDE.md: "Real PostgreSQL + real Redpanda (testcontainers)" | initial |
| 7 deviations | 0 HIGH · 2 MEDIUM · 5 LOW | No deviation blocks compliance | |