feat(auth): authorize anonymous commands from loaded session - #104
feat(auth): authorize anonymous commands from loaded session#104cursor[bot] wants to merge 9 commits into
Conversation
Keep the session-authorization adapter beside the landed account-link module.
Derive the assessment-session resource from the stored participant tenant and the loaded session so a transport cannot invent a matching scope and then command a different session. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Keep the loaded session unchanged when the proof is expired or names a different session, and still fail closed on illegal lifecycle transitions. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
There was a problem hiding this comment.
Command authorization on this head still has two contract holes that will fail a careful coverage or error-order review.
authorize_anonymous_session_commandchecks loaded session/participant ownership before tenant. A foreign-tenant participant plus a session owned by someone else returnsOwnerMismatchand neverCrossTenantDenied, which contradicts the command rustdoc order.ResourceScope::participant_owned(...).map_err(|_| SessionMismatch)is unreachable from validatedParticipantRecord/AssessmentSessionconstructors. If it ever fired it would mislabelInvalidReference/ResourceOwnershipMismatch. That arm is the 100% branch-coverage hole.- CHANGELOG overclaims that a transport cannot invent a matching scope. That is true only for the new command/apply entry points.
authorize_anonymous_sessionstill takes a caller-builtResourceScope. - UML, SECURITY_AND_DATA, and ADR-0003 were not updated for the new public command API.
Successor #118 compares the actor to loaded records directly (tenant first), adds the missing command-path tests, persists/reloads anonymous assessment_participant so tenant is not reconstructed from the proof, and updates those viewpoints.
Do not merge this head. Independent non-author approval and green required checks remain merge gates on whichever successor is integrated.
Sent by Cursor Automation: Fix Issues
| if !actor.is_valid_at(now_unix_ms) { | ||
| return Err(AnonymousResourceAuthorizationError::Expired); | ||
| } | ||
| if session.participant_ref() != participant.participant_ref() { |
There was a problem hiding this comment.
This loaded-pair check runs before tenant. A tenant_beta participant plus a session owned by another participant returns OwnerMismatch and never reaches CrossTenantDenied. Classify actor.tenant_ref() != participant.tenant_ref() first so the documented command error order holds.
| participant.participant_ref(), | ||
| session.session_ref(), | ||
| ) | ||
| .map_err(|_| AnonymousResourceAuthorizationError::SessionMismatch)?; |
There was a problem hiding this comment.
This map_err(|_| SessionMismatch) arm is dead: participant_owned fails only on invalid refs or a tenant-only kind, and kind is hardcoded to AssessmentSession while the refs already passed ParticipantRecord / AssessmentSession constructors. Do not add a test that tries to hit it. Compare actor fields to the loaded records (or expect after documenting that validated aggregates cannot fail construction).
| ## Unreleased | ||
|
|
||
| ### Added | ||
| - Anonymous session command authorization builds the assessment-session resource from the loaded participant tenant/owner and loaded session reference, then applies a lifecycle command only after that check, so a transport cannot invent a matching scope and then command a different stored session. |
There was a problem hiding this comment.
"a transport cannot invent a matching scope" is true only for authorize_anonymous_session_command / apply_anonymous_session_command. authorize_anonymous_session still accepts a caller-built ResourceScope. Say that the command entry point does not take a caller scope, and keep the lower-level exact-resource check separately.
|
Closing as a proven superseded predecessor. Fresh compare shows #104 head |
history on migration 0021. This successor keeps the #104 command-auth contract fix and does not open a colliding anonymous-only persist slice. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
history on migration 0021. This successor keeps the #104 command-auth contract fix and does not open a colliding anonymous-only persist slice. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
* test(auth): bind anonymous proof to exact session resource * feat(auth): bind anonymous proof to exact session resource * feat(auth): expose anonymous session authorization * style(auth): apply rustfmt to anonymous authorization tests * test(auth): pin anonymous denial precedence * docs(auth): explain anonymous session authorization * feat(auth): authorize anonymous commands from loaded session Derive the assessment-session resource from the stored participant tenant and the loaded session so a transport cannot invent a matching scope and then command a different session. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * feat(auth): apply session commands only after anonymous authorization Keep the loaded session unchanged when the proof is expired or names a different session, and still fail closed on illegal lifecycle transitions. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * fix(auth): classify anonymous commands from loaded records Compare the verified actor to the loaded participant tenant and session instead of rebuilding a ResourceScope. Tenant mismatch is reported before ownership so a foreign-tenant inconsistent pair cannot hide as OwnerMismatch. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * feat(participant): persist anonymous assessment identity Store tenant, participant reference, anonymous status, and creation time so command authorization can load the participant instead of rebuilding it from the proof. Exact replay is idempotent; tenant or time rebinding fails closed; linked participants stay out of this slice. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * docs(traceability): name Active PR #118 for participant persist Record the opened successor so architecture views do not leave the assessment-participant slice unlabeled. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * revert(participant): leave persist/reload on Active PR #114 history on migration 0021. This successor keeps the #104 command-auth contract fix and does not open a colliding anonymous-only persist slice. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * fix(auth): tell the truth about supplied command records Command authorization compares the verified actor to supplied participant and session values. It does not accept a ResourceScope and does not claim those aggregates were store-loaded. Align rustdoc, ADR-0003, SECURITY_AND_DATA, UML Activate, TRACEABILITY, and CHANGELOG. Use a session created after publication and before exclusive proof expiry. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * fix(auth): satisfy clippy doc-markdown on command timeline Backtick the publication and exclusive-expiry instants in the command authorization fixtures, and name honesty successor #135 in TRACEABILITY. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * fix(auth): stop claiming command records were store-loaded and named superseded #114 as the persist landing. The gate compares supplied records only; persist/reload remains Active PR #133. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * docs(traceability): name honesty successor Active PR #144 Point command-authorization honesty at the opened successor so reviewers do not treat #135 as the landing head. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * fix(auth): name persist landing #147 and drop leftover load claims The command gate still compares supplied records. Docs and the architecture contract now name Active PR #147 for persist/reload, forbid the leftover #133 pointer, and stop saying the gate authorized from loaded records. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * docs(traceability): name honesty successor Active PR #159 Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * fix(auth): name persist landing #158 instead of superseded #147 Identity persist/reload landing moved to #158. Keep the command gate honest about supplied records and stop pinning the leftover #147 pointer. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * fix(auth): drop closed #158 persist landing and loaded names Name persist/reload as Target, forbid the closed pointer, and call command-test records supplied. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * docs(traceability): name honesty successor Active PR #225 Record the opened successor so architecture views do not leave the command-auth honesty slice labeled as closed #159. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * fix(auth): keep anonymous authorization after rebase onto main Preserve account-link and anonymous-credential modules beside the session-command authorization entry point so this honesty head stays mergeable without claiming participant persist on this branch. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * docs(traceability): drop merged Active PR leftovers after rebase After rebasing onto 0c695b9, exclusive outbox leases, observation clocks/membership, and claim-next scoring-job poll are protected-main truth. Keep #225 as the Active PR for supplied-record anonymous command authorization. Persist/reload of assessment_participant remains Target. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> --------- Co-authored-by: Seongho Bae <me@seonghobae.me> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>


Why
PR #86 adds
authorize_anonymous_session, but a transport can still build a matchingResourceScopefrom the anonymous proof and then command a different loaded assessment session. Tenant lives on the participant record, not on the session aggregate, so the resource must be derived from stored records.What
authorize_anonymous_session_commandthat builds the assessment-session resource from the loaded participant tenant/owner and loaded session reference.Architecture / scope
This completes the product-side anonymous command gate from ADR-0003 without changing Keyverse credentials, HTTP transport, persistence, or scoring. Callers must still verify the short-lived proof before constructing
AnonymousSessionContext.Verification
cargo test --test anonymous_session_command_authorization --test anonymous_resource_authorizationcargo test --test documentation_architecture_contract --test traceability_active_pr_contractcargo clippy --all-targets -- -D warningscargo doc --no-depsIndependent non-author approval and required checks on the unchanged exact head remain merge gates.
Stacked on the #86 anonymous-session authorization head after the #85 account-link merge.