fix(auth): classify anonymous commands from loaded records - #118
fix(auth): classify anonymous commands from loaded records#118cursor[bot] wants to merge 13 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>
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>
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>
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>
There was a problem hiding this comment.
Review
Do not merge draft #118 at 708fb5c until an independent current-head approval and required checks pass. This automation does not approve its own lineage.
The command-path comparison is fail-closed when the caller actually passes store-loaded records. There is no allow-bug in authorize_anonymous_session_command / apply_anonymous_session_command. Unauthorized apply leaves SessionState::Created. #104 residuals (tenant-first order, dead ResourceScope map_err, CHANGELOG overclaim) are fixed in code and CHANGELOG.
Remaining work is contract honesty and test realism, not a tenant-isolation bypass:
- Rustdoc and the command-test module header still say the function does not accept a caller-invented tenant/owner/session. The signature accepts caller-built aggregates. CHANGELOG is the honest statement.
- SECURITY_AND_DATA / ADR speak of a loaded
assessment_participantrow. Persist/reload remains #114. - ADR cites NIST SP 800-63-4 for the tenant-then-owner error order. That publication covers authenticator validity, not this classification contract.
- UML only gates Complete and over-indents the new arrows inside the item-delivery loop visually. Tests exercise Activate.
- Command fixtures create the session at
20_000under a proof that expires at2_000.
Prefer this head over #104. Do not add HTTP Activate or a second 0021 here. Next after #118+#114: HTTP session command that verifies the #84/#108 credential, loads the #114 participant and #109 session, then calls apply_anonymous_session_command.
A successor on this run will correct the rustdoc/ADR/UML/SECURITY wording and the command-path timeline.
Sent by Cursor Automation: Fix Issues
| /// The function compares the actor to those loaded records. It does **not** accept a | ||
| /// caller-invented tenant, owner, or session reference, and it does not build a | ||
| /// [`ResourceScope`] that a transport could invent. For example, a proof for | ||
| /// `session_alpha` / `participant_alpha` in `tenant_alpha` is allowed only when the loaded | ||
| /// participant is that same person in that same tenant and the loaded session is `session_alpha` | ||
| /// owned by that person. A session owned by `participant_beta`, or `session_beta` owned by the | ||
| /// same person, is denied. |
There was a problem hiding this comment.
This sentence overclaims. The function accepts caller-built ParticipantRecord and AssessmentSession values. It compares actor fields to those supplied records and does not accept a ResourceScope. It does not prove the records were loaded from the store. Persist/reload of assessment_participant remains Active PR #114. Match CHANGELOG: the command entry point refuses a caller-built resource scope; it does not make invented aggregates impossible.
| if actor.tenant_ref() != participant.tenant_ref() { | ||
| return Err(AnonymousResourceAuthorizationError::CrossTenantDenied); | ||
| } | ||
| if session.participant_ref() != participant.participant_ref() | ||
| || actor.participant_ref() != participant.participant_ref() | ||
| { | ||
| return Err(AnonymousResourceAuthorizationError::OwnerMismatch); | ||
| } | ||
| if actor.session_ref() != session.session_ref() { | ||
| return Err(AnonymousResourceAuthorizationError::SessionMismatch); | ||
| } |
There was a problem hiding this comment.
Classification order is now tenant-first, which fixes the #104 compound foreign-tenant + inconsistent-pair OwnerMismatch. After #114, the HTTP adapter must load assessment_participant by stored identity and pass that row. Echoing the proof into ParticipantRecord::new_anonymous makes this tenant check tautological.
|
|
||
| Keyverse claims establish authenticated subject and coarse scopes. Psychometrics Commons performs resource-level decisions for instrument administration, result ownership, research roles, data export, deletion, and release approval. A Keyverse administrator is not automatically a Psychometrics Commons research data steward. | ||
|
|
||
| Anonymous session commands are a product-owned gate after the short-lived proof has already been verified. Transports that loaded `assessment_participant` and `assessment_session` must call `authorize_anonymous_session_command` / `apply_anonymous_session_command`. Those functions compare the verified actor to the loaded tenant, participant, and session references. They do not accept a caller-built `ResourceScope`. Fail-closed classification order is trusted server time, exclusive expiry, loaded-participant tenant, loaded session/participant ownership, actor participant, then session identity (National Institute of Standards and Technology, 2025). |
There was a problem hiding this comment.
NIST SP 800-63-4 supports trusted-time / exclusive authenticator validity. It does not specify this product's tenant-then-owner-then-session error order. Keep the APA 7 reference for authenticator validity. Name the command-path tests as the error-order contract. State as-built library vs target HTTP (assessment_participant row is #114).
| Rules: | ||
|
|
||
| - Tenant context for state-changing requests is derived from authenticated authorization, not an untrusted body field or implicit default. | ||
| - Tenant context for state-changing requests is derived from authenticated authorization or, for an anonymous session command, from the loaded `assessment_participant` row. It is not taken from an untrusted body field, a caller-invented `ResourceScope`, or an implicit default. |
There was a problem hiding this comment.
"loaded assessment_participant row" is target language. This slice has no persist/reload. Tenant comes from the ParticipantRecord argument. Align with TRACEABILITY: the row is Active PR #114.
| C->>A: complete session | ||
| A->>DB: load assessment_participant + assessment_session | ||
| A->>A: authorize anonymous command from loaded records | ||
| A->>DB: atomically state=Completed + freeze ResponseSnapshot + outbox scoring request |
There was a problem hiding this comment.
These arrows are over-indented after the item-delivery end, so readers treat authorize as part of item delivery. Dedent to the same level as W->>DB. Show the same load+authorize on Activate, which is the command the tests actually apply.
| //! | ||
| //! A transport must load the participant and assessment session from the product store, | ||
| //! then ask this boundary whether the already-verified anonymous session may command | ||
| //! that exact loaded session. Callers do not invent the resource tenant or owner. |
There was a problem hiding this comment.
"Callers do not invent the resource tenant or owner" is a process rule, not a type-system guarantee. Callers can still construct both aggregates from the proof. Say the tests pass supplied records and the transport must load them.
| participant_ref, | ||
| &published_release(), | ||
| "ko-KR", | ||
| 20_000, |
There was a problem hiding this comment.
created_at_unix_ms = 20_000 under a proof that expires at 2_000 with now = 1_500 is not a realistic authorized command. The published release is bound at 10_200. Use a session created after publication and before exclusive expiry (for example create 10_300, now 11_000, exclusive expiry 12_000).
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>
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>
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>
* 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>


Superseded
This anonymous command-authorization slice is fully contained in the verified successor chain #135 → #144 → #159. Do not merge this predecessor.
Fresh ancestry evidence:
708fb5cf1f231b634b328c9905b0c8d81d24bfe85ee49d86b858079cb83fcd5fe6a88932cb56395dis ahead of fix(auth): stop claiming command records were store-loaded #144 by 3 commits, behind by 0.#159 retains the supplied-record command authorization behavior and tightens documentation honesty/current persist-lane pointers. #159 remains Draft and subject to exact-head CI/security/review gates. Closing this PR does not promote successor behavior to protected-main truth.