Skip to content

Add admin-consent status GET and ConsentGrant provenance #1936

Description

@serguei-gorokhov

Parent: #1930

What to build

GET /v1/consent/{deployment_id}/admin-consent completes the verb set — the Admin Panel can show consent state without mutating (grant and withdraw were the only observers before). Gated exactly like the writers: hasExplicitAdminAccess before any resolution (fail-closed on unconfigured admin rules; a per-request key can never call it), 404 on unknown deployment, and a non-declaring app is a legitimate {"consented":false} answer — reads inform, the writers' 400 is a semantic-error guard.

The stored record becomes a ConsentGrant envelope — the approved snapshot plus server-stamped provenance (grantedBy from the authenticated admin context, grantedAt from the injectable clock) — same record, same key, same ADMIN_CONSENT type, no new storage. The provenance sits outside the client-echoed Consent document, so nothing stampable rides the GET→POST round-trip and the content-binding compare is untouched.

Tri-state semantics, load-bearing:

  • consented: true — a record exists AND its snapshot equals the current declaration: live right now, exactly what the request-time gate enforces; the GET and the resolver share one describeAdminConsent implementation and cannot drift.
  • consented: false, stale: true — a grant exists but the declaration changed since; the last approval's provenance and snapshot stay visible for the panel's re-approve view; nothing resolves at runtime.
  • consented: false (rest absent) — withdrawn or never granted.

The user-consent path (acceptConsent, buildConsent, verifyUserConsent, USER_CONSENT storage) shows zero diff — its who/when are structural.

Also folded into this story (the story ships as five commits):

  • Legacy-record leniency (security review): ConsentGrant ignores unknown properties (the stored-DTO convention), so a record written by a pre-envelope commit — a bare Consent body at the same key — deserializes with consent == null and flows into the already-built fail-closed path (stale, nothing resolves, withdrawal works) instead of 400-ing every user call and 500-ing the GET.
  • Single-sourced gate predicate (code review): isAdminConsented calls one zero-allocation matches(stored, declaration) predicate rather than delegating through the presentation DTO — no DTO on the per-request hot path, gate semantics can never inherit presentation-driven changes; the provenance-non-participation test now re-stubs with a different grantedBy/grantedAt and asserts the yes stands.
  • OpenAPI handling (docs pair): the tri-state contract is stated in the annotation response description and JavaDoc; a hand-edit of the committed spec was added then deliberately reverted — the committed OpenAPI spec stays purely generator-owned. Pipeline gotcha flagged for the team: because description is a MANUAL_PREFERRED_FIELD, changing an annotation's response description never propagates to a node that already carries one.

Acceptance criteria

  • GET returns the tri-state status for a declaring app (live / stale with provenance / clean false) and {"consented":false} for a non-declaring app; unknown deployment → 404.
  • Gated exactly like the writers: non-admin → 403 fail-closed on unconfigured admin rules, before any resolution.
  • Provenance is server-stamped only — no client-supplied path to storage; provenance fields never participate in the content binding (pinned by test).
  • A legacy bare-Consent record deserializes fail-closed: nothing resolves, GET works, withdrawal works (pinned by unit test).
  • The user-consent path shows zero diff (regression).
  • checkOpenApiDiff clean; committed spec untouched.

Blocked by

#1935

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    • Status
      No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions