fix(dashboard-api): support Hydra-only deployments and pre-Ory user migration#3252
fix(dashboard-api): support Hydra-only deployments and pre-Ory user migration#3252AdaAibaby wants to merge 6 commits into
Conversation
Two new methods on authdb.Client that query public.teams/users_teams directly, avoiding the Ory admin API: - GetUserEmailsByUserIDs: bulk user_id → email via default-team record - FindUserIDsByEmail: email → []user_id via default-team record; returns all matches so callers can detect and reject ambiguous lookups Also adds a functional index on lower(t.email) so the case-insensitive WHERE clause stays O(log n) on large deployments.
…igration Problem 1 — Ory admin API unavailable (fixes e2b-dev#3222): GetTeamsTeamIDMembers and PostTeamsTeamIDMembers fall back to DB email lookups when the Ory admin SDK call fails, so member list and add-member work in Hydra-only or CDN-proxied deployments. Problem 2 — pre-Ory users get a blank account on first login (fixes e2b-dev#3223): bootstrapUser checks for an existing user by email when no user_identities row exists for the incoming OIDC sub. If exactly one unambiguous match is found and that user has no existing identity for this issuer, the existing user_id is reused — preserving API keys, templates, and team memberships. Gate is skipped for SSO-backed issuers (ssoOrgID != uuid.Nil) so enrollSSOMember is not bypassed. Also returns user_id in AdminUserBootstrapResponse (OpenAPI spec updated) and enforces the SSO-org membership check in the DB fallback path of PostTeamsTeamIDMembers so that a Ory outage cannot bypass SSO gates.
provisioning/bootstrap_test.go: - TestBootstrapOIDCUser_ExternalIDFailureKeepsUserProvisioned: non-fatal backfill does not abort provisioning - TestBootstrapOIDCUser_ReRunBackfillsExternalID: second login sets external_id when first was a Hydra-only deployment - TestBootstrapOIDCUser_PreOryUserEmailMatchReusesExistingAccount - TestBootstrapOIDCUser_EmailMatchBlockedIfUserAlreadyLinked - TestBootstrapOIDCUser_PreOryUserSecondLoginUsesIdentityLookup - TestBootstrapOIDCUser_ConcurrentEmailMatchOnlyFirstSubMerges handlers/ory_fallback_test.go: - TestPostAdminUsersBootstrap_ResponseIncludesUserID - TestGetTeamsTeamIDMembers_OryUnavailableFallsBackToDBEmail - TestPostTeamsTeamIDMembers_OryUnavailableFallsBackToDBEmailLookup - TestPostTeamsTeamIDMembers_OryUnavailableUnknownEmailReturns404
There was a problem hiding this comment.
Code Review
This pull request introduces database-backed fallback mechanisms for user email and ID lookups when the Ory identity service is unreachable, ensuring robustness in team member management. It also implements a secure migration path for pre-Ory users by matching and reusing existing accounts based on email, complete with row locking to prevent concurrent bootstrap races and checks to prevent account hijacking. Additionally, the admin user bootstrap endpoint now returns the internal user ID, and a database index was added to optimize email queries. There are no review comments to address.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: afa0510b05
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
…y race When the email-match migration path sets profile.UserID to a pre-existing account, candidateUserID points to a real user rather than the ephemeral UUID allocated for this request. If a concurrent bootstrap wins the UpsertPublicIdentity race, the original DeletePublicUser guard would cascade-delete the migrated user's account (API keys, team memberships). Fix: record freshUserID before the switch so the deletion is skipped when candidateUserID is a pre-existing account rather than the fresh allocation. Also add a comment to the GetTeamsTeamIDMembers DB fallback noting that SSO members (is_default=false) are absent in degraded mode; this is an acceptable known limitation of the fallback path.
|
Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: shaolila.
|
Problem
Two related issues affect self-hosted deployments using Hydra without Kratos, or with a CDN that blocks
/admin/paths:1. Ory admin API calls fail silently (fixes #3222)
GetTeamsTeamIDMembersandPostTeamsTeamIDMemberscall the Ory admin SDK to resolve user profiles (email, name). In Hydra-only deployments the Kratos identity admin endpoint does not exist. When Hydra is behind a reverse proxy (e.g. Nginx, Cloudflare) the/admin/*paths are commonly blocked. Both cases cause the handlers to error out — members list returns empty, add-member returns 500.2. Pre-Ory users get a new empty account on first login (fixes #3223)
When migrating from a non-Ory auth system, existing users have no
user_identitiesrow.bootstrapUserWithIdentityfinds no(oidc_iss, oidc_sub)match and creates a fresh user + team, making all their existing API keys and template sandboxes invisible.Changes
packages/db/pkg/auth/profiles.go(new)Two helper methods on
authdb.Clientthat querypublic.teams/public.users_teamsdirectly, avoiding the Ory admin API:GetUserEmailsByUserIDs— bulk email lookup by user ID via default teamFindUserIDsByEmail— email →[]user_idvia default team; returns all matches so callers can detect and reject ambiguous lookupsAlso adds a functional index on
lower(t.email)so the case-insensitive WHERE clause stays O(log n) on large deployments.packages/dashboard-api/internal/handlers/team_members.goGetTeamsTeamIDMembers: falls back toGetUserEmailsByUserIDswhen Ory admin call failsPostTeamsTeamIDMembers: falls back toFindUserIDsByEmailwhen Ory admin call fails; enforces SSO-org membership check even on the DB fallback pathpackages/dashboard-api/internal/provisioning/bootstrap.goAdds a
defaultcase to the identity lookup switch. When nouser_identitiesrow exists for the incoming OIDC sub, checks whether an existing user's default-team email matches the OIDC email and that user has no existing identity for this issuer. If both conditions hold, reuses the existinguser_id— preserving API keys, templates, and team memberships across the auth migration.Gate is skipped for SSO-backed issuers (
ssoOrgID != uuid.Nil) soenrollSSOMemberis not bypassed.packages/dashboard-api/internal/handlers/admin_users_bootstrap.goReturns
user_idalongsideidandslugin the bootstrap response (OpenAPI spec updated).Safety
The email-match merge only fires when:
user_identitiesrow exists for(oidc_iss, oidc_sub)— new identitypublic.teams(default team)user_identitiesrows for this issuerssoOrgID == uuid.Nil)Conditions 3 and 4 prevent email-based account takeover and SSO auto-join bypass.
Testing
/admin/*user_identitiesrow is written for future logins