34
Criteria Total
31
MET
3
PARTIAL
0
MISSING
7
Deviations
91%
Full Coverage
Changes in this commit — SPLITa
6 Security Hardening Items — All MET
Fixed Deviation from Previous Review
EXPOSE 8080 → 8081 (LOW severity, now resolved). Previous Dockerfile used EXPOSE 8080 which diverged from the architecture spec (DocStore = port 8081). Corrected in this commit.
New Criteria — AC-029 to AC-034 (this commit)
MET
PARTIAL
MISSING
New in this review
ID Criterion Status Evidence
AC-029 NEW API responses must not expose internal storage_key MET domain/document.rs:43-84DocumentResponse struct excludes storage_key; From<Document> copies only safe fields. domain/version.rs:17-45VersionResponse 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-122METADATA_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-122METADATA_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
Carried-over Criteria — AC-001 to AC-028 (unchanged)
Show 28 carried-over criteria — 25 MET · 3 PARTIAL
ID Criterion Status Evidence (summary)
AC-001All tables use schema docstoreMETmigrations/001_init.sql:5CREATE SCHEMA IF NOT EXISTS docstore; all tables as docstore.*
AC-002RLS on all tables with tenant_id isolationMETmigrations/001_init.sql:100-120ENABLE ROW LEVEL SECURITY with USING (tenant_id = current_setting('app.current_tenant_id')::uuid)
AC-003tenant_id from JWT onlyMETapi/middleware.rsClaims struct; no tenant_id in any request body
AC-004All endpoints require valid JWTMETAll handlers take AuthContext extractor; returns 401 on invalid/missing token
AC-005Write ops: editor/tenant-admin/super-admin onlyMETapi/roles.rs:6WRITE_ROLES; enforced in all mutation handlers
AC-006Audit API: tenant-admin/super-admin onlyMETapi/roles.rs:9AUDIT_ROLES; enforced in list_audit_by_entity. LOW: audit.rs has local copy
AC-007Audit trail for all mutationsPARTIALNot audited: TagService.assign_to_document() and remove_from_document() — no audit.create() calls
AC-008Audit log schema: full fieldsMETmigrations/002_audit_log.sqlentity_type, entity_id, action, actor_id, correlation_id, details (JSONB)
AC-009RLS on audit_logMETmigrations/002_audit_log.sql:30-33; test_audit_tenant_isolation verifies cross-tenant isolation
AC-010CloudEvents v1.0 for all state changesPARTIALNo events for TagService.assign_to_document() / remove_from_document(); no TAG_ASSIGNED/TAG_REMOVED constants
AC-011CloudEvents include tenantid extensionMETevents/producer.rs:17tenantid: String field in CloudEvent
AC-012Events on events.docstore topicMETconfig.rskafka_topic defaults to events.docstore
AC-013JWT: RS256 + iss/aud/nbf/leewayMETapi/middleware.rs:55-71Algorithm::RS256, validate_exp, validate_nbf, configurable leeway
AC-014JWT expiry validated on every requestMETapi/middleware.rs:64validation.validate_exp = true
AC-015Soft delete onlyMETrepository/document.rs:151-176 — sets deleted_at/status='deleted'; no DELETE FROM in document/folder repos
AC-016X-Correlation-Id propagated or generatedMETapi/auth.rs:51-56 — reads header or generates UUID v4; stored in audit_log
AC-017GET /health — 200 with service infoMETmain.rs:90{status, service, version}
AC-018GET /ready — DB probe, 503 on failureMETmain.rs:91 — runs SELECT 1, returns 503 if DB unavailable
AC-019Dockerfile multi-stage, correct port, non-rootMET FIXEDSuperseded by AC-034. Previous LOW deviation (EXPOSE 8080) resolved in this commit.
AC-020All config from environment variablesMETconfig.rsAppConfig::from_env(); no hardcoded production secrets
AC-021All timestamps in UTC (TIMESTAMPTZ)METAll migrations use TIMESTAMPTZ; Rust types use DateTime<Utc>
AC-022Parameterized queries onlyMETAll repos use sqlx::query_as with .bind()
AC-023Documents CRUD at /api/v1/documentsMETmain.rs:93-112 — POST/GET/GET{id}/PATCH{id}/DELETE{id}
AC-024Folders CRUD at /api/v1/foldersMETmain.rs:114-124 — full CRUD
AC-025Versioning at /api/v1/documents/{id}/versionsMETmain.rs:126-138 — POST/GET/GET{version_id}; auto-increment
AC-026Tags API + assign/removeMETmain.rs:139-150 — full tag CRUD + assign/remove routes
AC-027Audit query at /api/v1/audit/entity/{id}METmain.rs:151-154; tenant-scoped, paginated, role-restricted
AC-028No production secrets in source codePARTIALRSA 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 criteria25 MET · 3 PARTIAL
Active Deviations — 7 total (0 HIGH · 2 MEDIUM · 5 LOW)
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