Skip to content

fix(auth): classify anonymous commands from loaded records - #118

Closed
cursor[bot] wants to merge 13 commits into
mainfrom
cursor/bc-6eb6e789-610b-4198-b90b-b400721ff1d3-0958
Closed

fix(auth): classify anonymous commands from loaded records#118
cursor[bot] wants to merge 13 commits into
mainfrom
cursor/bc-6eb6e789-610b-4198-b90b-b400721ff1d3-0958

Conversation

@cursor

@cursor cursor Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

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:

#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.

seonghobae and others added 11 commits August 16, 2026 10:41
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>
cursoragent and others added 2 commits August 16, 2026 15:30
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>
#114 already owns assessment_participant plus append-only identity-link
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>
@seonghobae seonghobae changed the title feat(auth): load stored participant before anonymous commands fix(auth): classify anonymous commands from loaded records Aug 16, 2026
@seonghobae
seonghobae self-requested a review August 16, 2026 15:31

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. 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.
  2. SECURITY_AND_DATA / ADR speak of a loaded assessment_participant row. Persist/reload remains #114.
  3. ADR cites NIST SP 800-63-4 for the tenant-then-owner error order. That publication covers authenticator validity, not this classification contract.
  4. UML only gates Complete and over-indents the new arrows inside the item-delivery loop visually. Tests exercise Activate.
  5. Command fixtures create the session at 20_000 under a proof that expires at 2_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.

Open in Web View Automation 

Sent by Cursor Automation: Fix Issues

Comment on lines +130 to +136
/// 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +161 to +171
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);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"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.

Comment thread docs/architecture/UML.md
Comment on lines +307 to +310
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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"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,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

@seonghobae seonghobae closed this Aug 16, 2026
cursor Bot pushed a commit that referenced this pull request Aug 17, 2026
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>
cursor Bot pushed a commit that referenced this pull request Aug 17, 2026
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>
cursor Bot pushed a commit that referenced this pull request Aug 18, 2026
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>
seonghobae added a commit that referenced this pull request Aug 18, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants