Skip to content

feat(auth): authorize anonymous commands from loaded session - #104

Closed
cursor[bot] wants to merge 9 commits into
mainfrom
cursor/bc-4bf57153-a70b-40ff-a1a4-0318985bffd6-bb46
Closed

feat(auth): authorize anonymous commands from loaded session#104
cursor[bot] wants to merge 9 commits into
mainfrom
cursor/bc-4bf57153-a70b-40ff-a1a4-0318985bffd6-bb46

Conversation

@cursor

@cursor cursor Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Why

PR #86 adds authorize_anonymous_session, but a transport can still build a matching ResourceScope from 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

  • Keep the feat(auth): bind anonymous proof to exact session resource #86 exact-session resource authorization boundary.
  • Add authorize_anonymous_session_command that builds the assessment-session resource from the loaded participant tenant/owner and loaded session reference.
  • Reject a session owned by another loaded participant, a different session for the same owner, a foreign participant tenant, and zero/expired server time in documented order.
  • Record the slice in CHANGELOG and TRACEABILITY as active work, not protected-main truth.

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_authorization
  • cargo test --test documentation_architecture_contract --test traceability_active_pr_contract
  • cargo clippy --all-targets -- -D warnings
  • cargo doc --no-deps

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

Open in Web View Automation 

seonghobae and others added 8 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>

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

Command authorization on this head still has two contract holes that will fail a careful coverage or error-order review.

  1. authorize_anonymous_session_command checks loaded session/participant ownership before tenant. A foreign-tenant participant plus a session owned by someone else returns OwnerMismatch and never CrossTenantDenied, which contradicts the command rustdoc order.
  2. ResourceScope::participant_owned(...).map_err(|_| SessionMismatch) is unreachable from validated ParticipantRecord / AssessmentSession constructors. If it ever fired it would mislabel InvalidReference / ResourceOwnershipMismatch. That arm is the 100% branch-coverage hole.
  3. CHANGELOG overclaims that a transport cannot invent a matching scope. That is true only for the new command/apply entry points. authorize_anonymous_session still takes a caller-built ResourceScope.
  4. 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.

Open in Web View Automation 

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() {

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 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)?;

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

Comment thread CHANGELOG.md
## 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.

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.

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

cursor Bot pushed a commit that referenced this pull request Aug 16, 2026
#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>

Copy link
Copy Markdown
Contributor

Closing as a proven superseded predecessor. Fresh compare shows #104 head 458d21c5 is the merge base/ancestor of #159 head 5ee49d86; #159 carries the anonymous command authorization line forward and removes stale store-load claims/persist pointers, naming #158 as the actual persistence landing while preserving the supplied-record authorization boundary. #159 explicitly prefers this head over #104.

@seonghobae seonghobae closed this Aug 16, 2026
cursor Bot pushed a commit that referenced this pull request Aug 17, 2026
#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>
cursor Bot pushed a commit that referenced this pull request Aug 17, 2026
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>
cursor Bot pushed a commit that referenced this pull request Aug 18, 2026
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 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