Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
c728de7
test(auth): bind anonymous proof to exact session resource
seonghobae Aug 16, 2026
0bf3f3d
feat(auth): bind anonymous proof to exact session resource
seonghobae Aug 16, 2026
cddef86
feat(auth): expose anonymous session authorization
seonghobae Aug 16, 2026
ed6978b
style(auth): apply rustfmt to anonymous authorization tests
seonghobae Aug 16, 2026
d2beee7
test(auth): pin anonymous denial precedence
seonghobae Aug 16, 2026
16e7283
docs(auth): explain anonymous session authorization
seonghobae Aug 16, 2026
00d7000
merge(main): reconcile anonymous session authorization after #85
seonghobae Aug 16, 2026
26b6e58
feat(auth): authorize anonymous commands from loaded session
cursoragent Aug 16, 2026
458d21c
feat(auth): apply session commands only after anonymous authorization
cursoragent Aug 16, 2026
6f1701f
fix(auth): classify anonymous commands from loaded records
cursoragent Aug 16, 2026
e438aa9
feat(participant): persist anonymous assessment identity
cursoragent Aug 16, 2026
53ae118
docs(traceability): name Active PR #118 for participant persist
cursoragent Aug 16, 2026
708fb5c
revert(participant): leave persist/reload on Active PR #114
cursoragent Aug 16, 2026
937dabf
fix(auth): tell the truth about supplied command records
cursoragent Aug 16, 2026
80ceb23
fix(auth): satisfy clippy doc-markdown on command timeline
cursoragent Aug 16, 2026
9df20ea
fix(auth): stop claiming command records were store-loaded
cursoragent Aug 16, 2026
858b628
docs(traceability): name honesty successor Active PR #144
cursoragent Aug 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/target/
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ All notable product and architecture changes are recorded here. Releases use imm
## Unreleased

### Added
- Anonymous session command authorization compares the verified actor to the supplied participant tenant/owner and session reference, then applies a lifecycle command only after that check. The command entry point does not accept a caller-built resource scope and does not claim those aggregates were store-loaded. The lower-level exact-resource check remains available for callers that already hold a stored `ResourceScope`. Participant persist/reload remains Active PR #133.
- Scoring-job cancel and lease-expiry fallback classification lock the current row until the caller transaction ends, so concurrent workers cannot rewrite terminal or unleased evidence.
- PostgreSQL operational-store readiness probe classifies the supported major version and write-readiness, and fails closed when a caller-declared required relation is missing.
- PostgreSQL scoring-job cancellation: queued, leased, or retry-scheduled work becomes cancelled without transferring a fence, exact replay is idempotent, and completed or quarantined evidence cannot be rewritten.
Expand Down
2 changes: 2 additions & 0 deletions docs/TRACEABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ Still-Target logical modules/adapters include remaining product aggregate persis

**Active PR** #76 data-rights processing-start persistence is not protected-main truth until an unchanged reviewed/check-clean head is integrated. Identity-verified requests persist an immutable operation identity and processing-start time under `FOR UPDATE` so later lifecycle composition cannot race the classified row. Dependent-system execution remains outside this slice.

**Active PR** #86 anonymous-session resource authorization, plus follow-up #104, #118, #135, and honesty successor #144 that compare the verified actor to the supplied participant tenant/owner and session and apply a lifecycle command only after that check, is not protected-main truth until an unchanged reviewed/check-clean head is integrated. The command entry point does not accept a caller-built `ResourceScope` and does not claim the aggregates were store-loaded. Persist/reload of `assessment_participant` and append-only identity-link history remains Active PR #133. HTTP transport remains outside this slice.

## 5. ADR traceability by concern

| Concern | Governing ADR(s) |
Expand Down
13 changes: 13 additions & 0 deletions docs/adr/0003-keyverse-identity-and-anonymous-participation.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ The mapping between operational and research identities is stored in a restricte

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. This slice is an as-built library: `authorize_anonymous_session_command` / `apply_anonymous_session_command` compare the verified actor to the supplied `ParticipantRecord` and `AssessmentSession`. They do not accept a caller-built `ResourceScope`. They do not prove those records were loaded from the store. Persist/reload of `assessment_participant` remains Active PR #133. HTTP transport remains Target.

Fail-closed classification order is a product contract: trusted server time, exclusive expiry, supplied-participant tenant, session/participant ownership, actor participant, then session identity. Named tests: `anonymous_command_authorization_fails_closed_for_zero_or_expired_server_time`, `anonymous_command_authorization_rejects_compound_foreign_tenant_and_inconsistent_loaded_pair_as_cross_tenant`, and `anonymous_command_authorization_rejects_actor_when_loaded_participant_and_session_agree`.

Trusted server time and exclusive authenticator validity follow NIST SP 800-63-4 (Temoshok et al., 2025). That publication does not specify the tenant-then-owner-then-session error order.

The lower-level `authorize_anonymous_session(actor, resource, now)` check remains for callers that already hold a stored assessment-session `ResourceScope`. It is not sufficient by itself for a command against a different loaded session.

## Invariants

1. Core anonymous assessment does not require a Keyverse account.
Expand All @@ -61,6 +69,7 @@ The product does not rely on blanket PII masking that destroys operational utili

- token validation and audience-confusion tests;
- cross-tenant authorization tests;
- anonymous command-path tests that classify tenant before ownership and leave the session unmutated on authorization failure;
- anonymous-to-account linking replay and conflict tests;
- research-release joinability tests;
- account deletion/export end-to-end tests.
Expand All @@ -74,3 +83,7 @@ The product does not rely on blanket PII masking that destroys operational utili
## Reversal conditions

Revisit if Keyverse cannot meet a deployment's residency or federation requirements. A replacement must remain OIDC-compatible and preserve subject-mapping semantics without moving credentials into the product database.

## References

Temoshok, D., Proud-Madruga, D., Choong, Y.-Y., Galluzzo, R., Gupta, S., LaSalle, C., Lefkovitz, N., & Regenscheid, A. (2025). *Digital identity guidelines* (NIST Special Publication 800-63-4). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-63-4
2 changes: 1 addition & 1 deletion docs/architecture/ERD.md
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ The target ERD deliberately includes several logical entities that are not yet p
- `data_rights_request` and `data_rights_propagation_state` are the first durable export/deletion slice. Physical `migrations/0003_data_rights_propagation.sql` stores requested-state identity plus one local outbox event per dependent system; verification, processing, completion, and dependent-system execution remain Target.
- `item_delivery_event` reflects the already-merged `src/item_delivery.rs` domain primitive; durable persistence/API orchestration is still Target.
- `consent_ledger` and `consent_event` persist the already-merged `src/consent.rs` append-only ledger. Physical persistence is carried by Active PR #49 (`migrations/0005_consent_lifecycle.sql`); HTTP consent transport and derived snapshot tables remain Target.
- `participant_identity_link` is the persistence target accepted by ADR-0020. The current `src/participant.rs` `keyverse_subject_ref` field is an application-domain first-link projection, not the future mutable persistence source of truth.
- `participant_identity_link` is the persistence target accepted by ADR-0020. The current `src/participant.rs` `keyverse_subject_ref` field is an application-domain first-link projection, not the future mutable persistence source of truth. Active PR #133 persists `assessment_participant` plus append-only link history; it is not protected-main truth until integrated. Prefer that head over #114 or #124.
- `longitudinal_enrollment`, `longitudinal_observation_record`, and `temporal_analysis_submission` make the ADR-0008 Commons-owned Gyeot/TEPP orchestration boundary explicit. No TEPP analytical kernel is duplicated here.
- `integration_outbox`, `integration_delivery_attempt`, `integration_inbox`, and `integration_consumption` reflect `src/integration.rs` domain semantics. Outbox/inbox/delivery-attempt tables are on protected main; `integration_consumption` pending/processing/completed/quarantined persistence and expire-and-reclaim of a crashed processing claim exist only on this Active PR until merged.

Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/SECURITY_AND_DATA.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ sharing_token_audience/expiry if used

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 supplied `ParticipantRecord` argument. The command gate does not prove those records were store-loaded. Active PR #133 persists the `assessment_participant` row and append-only identity-link history; prefer that head over #114 or #124. Tenant is not taken from an untrusted body field, a caller-invented `ResourceScope`, or an implicit default.
- Public opaque identifiers are identifiers, not authorization capabilities.
- Research steward, instrument publisher, participant result owner, and identity administrator are distinct authorities.
- A sharing link, if introduced, must be revocable, scoped to an exact resource/audience, expire by default, and not reveal raw responses unless explicitly permitted by the participant and product policy.
Expand Down
10 changes: 10 additions & 0 deletions docs/architecture/UML.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,8 @@ Export cannot complete with a deletion-retention exception. Exact terminal repla

## 6. Anonymous assessment happy-path sequence

This sequence is target transport. The as-built command gate compares supplied records and does not perform the load.

```mermaid
sequenceDiagram
autonumber
Expand All @@ -295,6 +297,12 @@ sequenceDiagram
DB-->>A: session_ref + pinned instrument version
A-->>C: session resource + item-delivery contract

C->>A: activate session
A->>DB: load assessment_participant + assessment_session
A->>A: authorize anonymous command from loaded records

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.

Target sequence already loads on the previous arrow. from loaded records still reads as if the command gate performed or proved the load. The as-built disclaimer on line 281 is honest; this mermaid step should say supplied records so the two layers stay distinct.

A->>DB: atomically state=Active
A-->>C: activation accepted

loop each presented item / response
A->>DB: append ItemDeliveryEvent(sequence, item version, payload digest)
P->>C: answer presented item
Expand All @@ -305,6 +313,8 @@ sequenceDiagram
end

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
A-->>C: completion accepted / scoring pending

Expand Down
Loading
Loading