Repository navigation
fix(events): check stream identity against the stored account - #214
Merged
Merged
Conversation
The live identity check behind GET /schemas/{schema}/events looked up
the auth store with the raw token subject. Login and OAuth mint
`sub = "user:<username>"` while the store is keyed by the bare
username, so no local account was ever found. Tenant-scoped callers
fell through to the external-subject branch and were refused with
403 "Stream authorization is no longer valid." before any event, and
callers without a tenant chain were treated as external subjects, so
deactivation and role changes never closed their streams.
Resolve the subject with `user_id_from_sub`, as Cedar, ownership and
creator membership already do. Platform administrators bypass the
tenant-scope middleware, so their `tenant_chain` is the flat
login-time membership set; the check now applies the same rule
through a shared `is_tenant_scoped` helper instead of treating that
set as an active tenant and expecting a scoped role. Expected roles
come from a pure `expected_roles` built on the middleware's
`active_membership_roles`.
The stream fixture now uses the production subject shape, and new
tests drive real password login through the PASETO middleware and
tenant scope: admins with and without memberships, a single-membership
member, and a multi-membership scoped owner open streams, while
membership removal, scoped role demotion, deactivation and global
role changes still close streams and refuse reconnects. Waits for a
close are bounded so a regression fails instead of hanging. The same
tests fail on v0.48.0, so this predates 0.49.0.
Refresh, `/auth/me`, and entity streams now map a token to its stored account through `account_username`, beside `refresh`, so the stream check cannot drift from the routes that already resolved the account correctly. The helper strips the `user:` subject prefix exactly as the Cedar principal and ownership checks do; every token the server mints sets `username` to that same value. Refs #207
A single-membership member and a multi-membership scoped owner now each receive an `entity.updated` change on a stream opened with a login token, and the owner's write goes through its scoped grant. Refs #207
rrrodzilla
force-pushed
the
fix/stream-identity-check
branch
from
October 6, 2026 21:54
0a5ef1c to
67fee1e
Compare
This was referenced Oct 6, 2026
rrrodzilla
added a commit
that referenced
this pull request
Oct 6, 2026
Bump the crates changed since v0.49.0 by a patch: schema-forge-cli 0.49.1, schema-forge-acton 0.47.1, schema-forge-backend 0.21.1 and schema-forge-mssql 0.8.1, with the internal version pins on the mssql backend and the CLI to match. Date the changelog section. The release carries the entity event stream fix for login tokens and tenant members (#207, #214), the system-schema check in `policies validate`, and the acton-service 0.46.0 build (#217).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #207
Summary
GET /api/v1/forge/schemas/{schema}/eventsrefused every login-minted token that carries a tenant chain with 403Stream authorization is no longer valid.before sending any event. The live identity check (identity_validinroutes/events.rs) calledauth_store.get_user(&claims.sub), but password and OAuth login mintsub = "user:<username>"while the auth store is keyed by the bare username. No local account was ever found, so:X-Active-Tenant) fell into the external-subject branch, which refuses any non-empty tenant chain;A second defect sat behind the first: the tenant-scope middleware passes platform-admin claims through unscoped, so an admin's
tenant_chainis the flat login-time membership set. The check still treated its last entry as an active tenant and expected a scoped role on top of the admin's roles. That kept an admin with a creator membership (for example the admin who created the first organization undercreator_role = "owner") refused even after the subject fix.Fix
routes/auth.rsgainsaccount_username(&Claims), besiderefresh, as events: tenant users get 403 and the admin stream skips the account re-check, because the stream looks users up by the prefixed token subject #207 suggests. Refresh,/auth/me, and the stream (identity_validand the handler'slocal_account) all resolve the stored account through it. It strips theuser:prefix withuser_id_from_sub, the mapping Cedar, ownership and creator membership already use. Every token the server mints setsusernameto that same value, so refresh and/auth/mebehave as before.Ok(None), no local account, empty chain) is now decided from the resolved username, so it applies only to tokens that really have no User row.tenant_scope::is_tenant_scoped(platform admins bypass tenancy) andtenant_scope::active_membership_roles(pure;add_active_membership_rolenow wraps it). The stream compares the token's roles with a pureexpected_roles(global, tenant_roles, active)and only walks memberships and hierarchy for tenant-scoped callers.docs/events.mdstates that platform admins get account status and role rechecks only. CHANGELOG entry under Unreleased/Fixed, after the fix(cli): validate custom policies against the system schemas #213 entry.Tests
New tests in
crates/schema-forge-acton/tests/entity_events.rsdrive the production path: realPOST /auth/login, thePasetoAuthmiddlewareserveinstalls, thentenant_scope. Accounts are seeded the way invite acceptance seeds them.login_tokens_open_streams_for_admins_and_tenant_members: platform admin with no memberships, platform admin with a creator owner membership, single-membership member, and a multi-membership user with scopedownerroles underX-Active-Tenantfor each org, all open. The member receives anentity.updatedchange on its stream, and the scoped owner renames its org through its scoped grant and receives that change.login_streams_close_and_stay_refused_after_membership_changes: removing a membership, or demoting a scoped owner to member, closes the open stream withclosed/authorization_changedand refuses reconnects with the old token. The user's other, unchanged scoped grant still opens.login_streams_close_and_stay_refused_after_account_changes: deactivating an admin, or removingplatform_admin, closes the open stream the same way and refuses reconnects.account_usernamemapsuser:<name>to the stored account;expected_rolesadds only the active membership's role; platform admins are never tenant-scoped.These cover the three tests #207 lists. The existing fixture now uses the production subject (
user:alice). That alone makes the existing tenancy and deactivation tests fail without the fix, so they exercise what production sends. Twoactorassertions now expectuser:alice, which is what production has always emitted. Waits for a close are bounded at 10 s (the idle recheck is at most 5 s), so a regression fails instead of streaming keep-alives until the runner kills it.Without the fix: the three new tests and
membership_removal_closes_stream_and_active_tenant_cannot_be_impersonatedplusscoped_membership_role_allows_stream_and_role_revocation_closes_itfail, andidle_keep_alive_and_live_account_revocationnever ends (killed after 875 s).Regression?
No. The same three login-path tests, ported to a v0.48.0 checkout, fail the same way: the member stream is refused with 403, the admin holding a membership is refused, and the deactivated admin's stream stays open. The subject lookup has been wrong since events shipped in 0.48.0 (#206). 0.49.0 changed the role comparison but not the lookup.
Why a 0.48 admin could stream while a 0.49 admin could not: it depends on whether the admin holds a tenant membership, not on how the admin was created.
serve --admin-userandschemaforge bootstrap-adminboth callshared_auth::bootstrap_admin_with_display_name, which writes the same bare username and["platform_admin"]with no memberships; only the display name differs. An admin with no membership logs in with an empty tenant chain and lands in the external-subject branch, which opens the stream but skips every recheck. 0.49.0 added[schema_forge.tenancy] creator_role; an admin who creates an organization undercreator_role = "owner"gains an owner membership, logs in with a non-empty chain, and is refused. Thefounder@example.govfixture models that admin.Verification
cargo nextest run -p schema-forge-cli -p schema-forge-acton --features schema-forge-cli/oauth,schema-forge-cli/sse(the CI SurrealDB job): 1267 passed, 4 skipped (Postgres-only, ignored)cargo clippy -p schema-forge-acton --features postgres,graphql,oauth,sse --all-targets -- -D warningsandcargo clippy -p schema-forge-cli --features oauth,sse --all-targets -- -D warnings: cleancargo test --doc -p schema-forge-acton --features graphql,oauth,sse: cleanevents_httpnamespace would race the existing Postgres events fixture.