Files
query-orchestration/plans/mutable.metadata.plan.combo.v4.review.codex.md
T
Jay Brown 17fc813823 Merged in feature/mutable-metadata1 (pull request #221)
M1, M2 and M3 complete

* M1, M2 and M3 complete

* review changes

* docs

* docs
2026-04-16 23:11:26 +00:00

6.4 KiB

Mutable Metadata v4 Review (Codex)

Overall assessment: v4 is meaningfully simpler than the earlier versions, but it is not ready to implement as written. The biggest problems are one real security hole that the plan explicitly accepts, one schema-versioning bug that can produce invalid lineage state, and several plan/tracking inconsistencies that will cause implementation drift.

Findings

1. Critical: per-client isolation is still not actually enforced for /custom-metadata/*

Refs: requirements 5-19, plan 35, 711-718, 721-726, tracking 165-166.

Why this matters: the plan explicitly accepts that anyone with custom-metadata:get or custom-metadata:post can read or write any document's custom metadata if they know the UUID. That is a direct mismatch with the feature's per-client framing in the requirements, and it hard-codes "we deploy one stack per client" as the only security boundary. It also makes the plan internally inconsistent: the reset section says requests should 404 when the caller's client scope does not include the document, but no such client-scope check is actually designed.

Current-code cross-check: this is consistent with the current first-segment-only Permit mapping in internal/cognitoauth/mapping.go and with the existing "no tenant isolation" note for client_user in cmd/auth_related/permit.setup/permit_policies.yaml.

Recommendation: either add document/client ownership enforcement now, or keep the new metadata read/write endpoints behind super_admin until object-scoped auth exists. Do not ship a new per-client feature while explicitly depending on deployment topology for isolation.

2. High: schema version creation can leave multiple active versions in one lineage

Refs: plan 407-449, 1086, 1399, tracking 131-135, 200, 410.

Why this matters: POST /super-admin/custom-schemas/{schemaId}/versions is keyed by an arbitrary parent schema ID, and the plan only supersedes that parent. If v2 is active and someone creates v3 from v1, the flow inserts v3 and marks v1 superseded again, but v2 remains active. The same bug lets a caller create a new version from a retired schema and silently rewrite its lifecycle to superseded.

That breaks the plan's own "current active version" model and makes default list behavior, assignment behavior, and admin UX ambiguous.

Recommendation: require the parent to be the latest active version, or independently supersede the currently active version for the (client_id, name) lineage inside the same transaction. Add explicit negative tests for creating from superseded and retired parents.

4. Medium: several API response contracts are not backed by the planned service/query layer

Refs: plan 558-566 vs. 1081 and tracking 280/334; plan 625-640 vs. 1091-1100 and tracking 381; plan 930-942 vs. tracking 249-260.

Why this matters: the plan promises response fields that the service/query design never schedules retrieval for.

Examples:

  • PATCH /super-admin/documents/{id}/schema returns schemaName and schemaVersion, but AssignSchema(...) returns only error, and the tracking doc does not add a follow-up lookup.
  • The reset response example includes customSchemaId and hasCustomMetadata, but ResetMetadataResult does not.
  • GET /custom-metadata returns schemaName, but the metadata-query work only fetches metadata rows plus documents.custom_schema_id; nothing in tracking fetches the schema name.

Recommendation: either shrink the responses to fields the service already owns, or make the method return types and query tasks match the API contract before implementation starts.

5. Medium: the actor/createdBy design is likely to regress the API contract and audit readability

Refs: plan 381, 866, 1422, tracking 189 and 329.

Why this matters: the plan repeatedly says handlers should call cognitoauth.GetUserSubject, which returns the Cognito sub, not an email or display identity; see internal/cognitoauth/auth.go. But the examples still show email-like createdBy / resetBy values, and the current API commonly models createdBy as an email-shaped field; see serviceAPIs/queryAPI.yaml and serviceAPIs/queryAPI.yaml.

If implemented literally, new endpoints will expose opaque subject IDs in fields named like human-readable actor fields. That is a contract change and makes logs/UI less useful.

Recommendation: decide explicitly whether these fields are generic actor IDs or human-readable identities. If they are IDs, rename/document them that way. If they are meant to stay user-facing, source them from a claim/helper that returns the intended display value.

6. Medium: the tracking doc contradicts itself on the trigger set

Refs: tracking 230-245 vs. 501.

Why this matters: section 2.2 correctly adds the new v4 Trigger 2 (trg_prevent_legacy_extraction_on_custom_document), but the cross-milestone reminder says "Do NOT add Trigger 2 or Trigger 3." A developer following the reminder literally can remove the very trigger the plan depends on to close the mutual-exclusivity race.

Recommendation: rewrite the reminder to name the forbidden legacy triggers explicitly, e.g. "Do NOT add v3's old client-match trigger or schema-consistency trigger."

7. Medium: the tracking doc has at least one behavior mismatch and one missing concurrency test

Refs: plan 459-460, 1284, tracking 55, 200, 392-393.

Why this matters:

  • Milestone 1 expects the schema list call to show both v1 and v2 statuses, but the plan says includeAllVersions defaults to false. That test description is wrong unless it explicitly calls the list endpoint with includeAllVersions=true.
  • The plan says reset and metadata writes share the same parent-row lock shape and therefore serialize correctly, but tracking only covers reset-vs-reset and reset-vs-assign. It does not cover reset-vs-SetDocumentMetadata.

Recommendation: make the list-test query parameters explicit and add a reset-vs-metadata-write concurrency test.