63 lines
6.4 KiB
Markdown
63 lines
6.4 KiB
Markdown
|
|
# 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](./mutable.metadata.requirments.md), 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](/Users/jaybrown/dev/clients/aarete/query-orchestration.2/internal/cognitoauth/mapping.go:3) and with the existing "no tenant isolation" note for `client_user` in [cmd/auth_related/permit.setup/permit_policies.yaml](/Users/jaybrown/dev/clients/aarete/query-orchestration.2/cmd/auth_related/permit.setup/permit_policies.yaml:181).
|
||
|
|
|
||
|
|
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](/Users/jaybrown/dev/clients/aarete/query-orchestration.2/internal/cognitoauth/auth.go:133). 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](/Users/jaybrown/dev/clients/aarete/query-orchestration.2/serviceAPIs/queryAPI.yaml:3960) and [serviceAPIs/queryAPI.yaml](/Users/jaybrown/dev/clients/aarete/query-orchestration.2/serviceAPIs/queryAPI.yaml:4719).
|
||
|
|
|
||
|
|
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.
|