Repository navigation
fix(dash0): send a session that lands on an unusable org to one it can use - #346
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pickAccessibleOrg mirrors the backend's login-time resolution — super admin keeps the URL's org, then the URL's org when it is a membership, then the session's own org (last accessed), then organizations[0], then null for a user with no organization at all. Pure and unit-tested, so the two redirect call sites that follow can never disagree and bounce a user between them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… to the URL's The login page's already-authenticated effect resolved its destination from the URL's org. That org is only a hint: /orgs/<slug>/login is reachable from a marketing link, a bookmark, or the "Try the live demo" button offered on every org's login page. A visitor who is not a member of that org landed on "Permission Denied", whose only button links back to the same org. It now resolves through pickAccessibleOrg, with the session fields it reads in the dependency list so it evaluates against the session applyLoginResponse just stored. A session with no organization at all goes to /no-org. This also closes the second half of the demo race spec 2026-09-07-02 left open: the button path. The tightened E2E test failed 5 runs out of 5 before the fix (the visitor reached /orgs/demo, then a replace() to /orgs/test overrode it) and passes after it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d-end 403 The org layout switched the session's token when the URL named another org the user belongs to, and let everything else "fall through to the normal 403 handling": every child query answered 403, QueryErrorView rendered "Permission Denied", and that card's only button linked back to the very same org. A bookmark to an org you were removed from, or a link pasted from a colleague's org, had no way out but editing the URL by hand. It now redirects to the org pickAccessibleOrg names (or /no-org when there is none), with a toast saying which org was refused and which is being shown. The destination is the org ROOT, never the current sub-path: check, status page and incident uids do not carry across organizations. Children are gated behind the redirect the same way the switch case gates them, so no org-scoped request is fired — and refused — on the way out. Super admins are untouched, and a member merely lacking a role for one page still gets Permission Denied. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- npx tsc -b: clean - bun run test:unit: 1505 passed (92 files), incl. locale parity + translated defaults for the four new org.accessRedirect.toast keys - make build-dash0 + bun run lint: 42 errors, byte-for-byte the pre-existing base; the touched files show the same 2 errors / 12 warnings as their baseline versions, so no new finding was introduced - Playwright (side-car SP_RUNMODE=test server on :4010, postgres): accessible-org-redirect + demo-account + the 14 auth/org specs — 90 passed. The one failure, confirm-registration-no-org, is the documented "Registration is not enabled" environment limitation create-org.spec.ts already calls out, unrelated to this change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes the audit's blocking finding: a 2FA login flashed through /no-org. The new login-page effect (spec §B) picks the destination from auth.organizations. Every login shape carried that list except 2FA: completeLoginAfter2FA built its LoginResponse without the field, so applyLoginResponse fell back to /auth/me — and that await ends React's auto-batch, committing a render where the session is authenticated with an empty list. The effect read "no organization at all" and sent an ordinary TOTP user to /no-org, which has no "you do have orgs" guard, before the ordinary post-login navigation corrected the URL. Fixed at both layers: - Backend (root cause): completeLoginAfter2FA now returns Organizations from getOrganizationsForUser, exactly as the invitation-accept path does, in both its org and no-org branches. The list is decorative enough that a failure to read it is logged and swallowed rather than failing an otherwise valid login — the same call it already makes for last_active_at. The spec expected no backend change, but it also asked the implementer to verify this shape, and it did not hold. - Client (defence in depth): applyLoginResponse holds isLoading up across the /auth/me fallback, and the login effect gates on it. isLoading is the session's existing "still resolving" signal rather than a new global flag, and it is raised in the same batch as setUser so no render ever exposes an authenticated session with a half-built org list. That also makes the org layout's twin branch ($org.tsx), which already gated on !auth.isLoading, correct for this window for free — both call sites now key on one signal. Tests: login_organizations_test.go asserts the 2FA response carries the user's WHOLE list (two memberships, so an echo of the signed-into org would not pass), on both branches. e2e/login-2fa.spec.ts gains a test that records pushState/replaceState navigations — the existing tests wait for a URL merely not containing "/login", which /no-org satisfies, and a final-URL assertion cannot see a redirect that a later one corrects. Verified failing pre-fix with "/dash0/no-org" in the trail while the other three 2FA tests stayed green. Cosmetic: accessible-org-redirect.spec.ts's seed helper now skips through base.skip rather than mixing in the fixtures test object. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fclairamb
enabled auto-merge (squash)
September 8, 2026 06:03
The wrong-org live-socket regression test used "park a non-member on a
foreign org" as its vehicle for getting a wrong-org token onto a page
that dials the WebSocket. Spec 2026-09-08-01 §C deliberately makes that
state unreachable — a non-member is now redirected to an org they can
use — so the socket connected fine and the sidebar dot never reached
the terminal "disabled" state.
Re-point the test at the wrong-org state that IS still reachable, and
was always the likelier production cause: a genuine *member* of the
URL's org whose token is scoped to another of their orgs, for whom the
automatic `switchOrg()` re-mint fails. $org.tsx drops its loading gate
in exactly that case ("better to render and let per-request 403s
surface than to hang"), so the page mounts holding the other org's
token — the same (token-org × target-org) mismatch the server answers
with a 4403 (backend twin: realtimews TestServe_TwoOrgMemberMatchesREST).
The end-to-end coverage is unchanged: real cookie, real upgrade, real
4403, no hello, no subscribed, dot on "disabled". Added assertions pin
the vehicle itself down — the switch-org call was attempted and refused,
and the page stayed on the URL's org — so the test cannot degrade into
passing vacuously on a page that never held a wrong-org token.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Merged
fclairamb
added a commit
that referenced
this pull request
Sep 8, 2026
release-please renders one line per squashed PR; #346 carried three distinct user-visible fixes. Folds in the prose written alongside them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closed
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.
Spec
2026-09-08-01-redirect-to-an-accessible-org.The bug
On production v0.26.0, entering the live demo from
solidping.io/dash0/orgs/default/loginleft the visitor in/orgs/default— an org the demo user is not a member of. Every request 403s, the layout shows "Permission Denied", and that card's only button links back to the same org. A dead end with no way into the demo.Two paths produced it. The "Try the live demo" button raced its own sign-in:
login()flipsisAuthenticated, the already-authenticated effect fires with the URL's org andreplaces over the in-flight navigation to the demo. Spec 2026-09-07-02 silenced that effect for the?demoflag and left the button path with it. Separately, a returning visitor still holding a demo session hit the same wall with no race at all, just by opening a plain login link.The existing E2E caught neither: it waited for
/orgs/[^/]+/, which the login page's own URL already matches, then asserted a banner that renders on every org page.The general problem underneath
Any session can land on an org URL it cannot use — a bookmark to an org you were removed from, a colleague's link, a slug from an old email. The layout already re-mints the token when the URL org is one of yours; when it is not, the comment said "non-members fall through to the normal 403 handling". Nothing ever sent you somewhere usable, even though the client already holds the full list.
What changed
lib/accessible-org.ts— one pure, unit-tested helper both call sites share, so the two redirects cannot disagree: super admin → URL org; URL org if you're a member; else the session's org (which is "last accessed", since every login and switch re-mints the token); elseorganizations[0]; else/no-org. This mirrors what the backend already does at login time inresolveDefaultOrg.The audit caught a regression this introduced
The independent audit found that
completeLoginAfter2FAwas the one login path returning no organization list, soapplyLoginResponsefell back to/auth/me— and thatawaitends React's auto-batch, committing a render withisAuthenticated,!isLoadingand an empty list. The new effect read that as "belongs to no org" and bounced TOTP users through/no-org. Fixed at both layers: the 2FA response now carries the list like every other login shape, andisLoadingis held up across the fallback so no render exposes a half-built session.The existing 2FA E2E could not have caught it — it waits for a URL merely not containing
/login, which/no-orgsatisfies. The new test recordspushState/replaceStateinstead, because a final-URL assertion cannot see a redirect that a later one corrects.Verification
test@test.comturned out to be a superadmin, so the spec's suggested "sign in as the test user" would have asserted nothing; the general E2E provisions a real ordinary member and keeps the superadmin as the "URL org kept" variant.test:unit1505 passing; 27/27 Playwright across the three affected specs.🤖 Generated with Claude Code