Skip to content

fix(dashboard-api): support Hydra-only deployments and pre-Ory user migration#3252

Open
AdaAibaby wants to merge 6 commits into
e2b-dev:mainfrom
AdaAibaby:fix/dashboard-api-ory-hydra-only-support
Open

fix(dashboard-api): support Hydra-only deployments and pre-Ory user migration#3252
AdaAibaby wants to merge 6 commits into
e2b-dev:mainfrom
AdaAibaby:fix/dashboard-api-ory-hydra-only-support

Conversation

@AdaAibaby

Copy link
Copy Markdown
Contributor

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)

GetTeamsTeamIDMembers and PostTeamsTeamIDMembers call 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_identities row. bootstrapUserWithIdentity finds 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.Client that query public.teams / public.users_teams directly, avoiding the Ory admin API:

  • GetUserEmailsByUserIDs — bulk email lookup by user ID via default team
  • FindUserIDsByEmail — email → []user_id via default team; 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.

packages/dashboard-api/internal/handlers/team_members.go

  • GetTeamsTeamIDMembers: falls back to GetUserEmailsByUserIDs when Ory admin call fails
  • PostTeamsTeamIDMembers: falls back to FindUserIDsByEmail when Ory admin call fails; enforces SSO-org membership check even on the DB fallback path

packages/dashboard-api/internal/provisioning/bootstrap.go

Adds a default case to the identity lookup switch. When no user_identities row 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 existing user_id — preserving API keys, templates, and team memberships across the auth migration.

Gate is skipped for SSO-backed issuers (ssoOrgID != uuid.Nil) so enrollSSOMember is not bypassed.

packages/dashboard-api/internal/handlers/admin_users_bootstrap.go

Returns user_id alongside id and slug in the bootstrap response (OpenAPI spec updated).

Safety

The email-match merge only fires when:

  1. No user_identities row exists for (oidc_iss, oidc_sub) — new identity
  2. Exactly one user with the matching email exists in public.teams (default team)
  3. That user has zero existing user_identities rows for this issuer
  4. The OIDC issuer is not SSO-backed (ssoOrgID == uuid.Nil)

Conditions 3 and 4 prevent email-based account takeover and SSO auto-join bypass.

Testing

  • Verified on a self-hosted Hydra v2 deployment with CDN blocking /admin/*
  • Members list and add-member work via DB fallback when Ory admin is unreachable
  • Pre-Ory user logs in via Ory → existing team with API keys and templates is returned, user_identities row is written for future logins
  • Unit tests cover: non-fatal backfill, pre-Ory email match, account-takeover guard, concurrent bootstrap race, DB fallback for member list and add-member

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

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread packages/dashboard-api/internal/provisioning/bootstrap.go
Comment thread packages/dashboard-api/internal/handlers/team_members.go
adababys and others added 3 commits July 10, 2026 18:53
…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.
@cla-bot

cla-bot Bot commented Jul 15, 2026

Copy link
Copy Markdown

Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: shaolila.
This is most likely caused by a git client misconfiguration; please make sure to:

  1. check if your git client is configured with an email to sign commits git config --list | grep email
  2. If not, set it up using git config --global user.email email@example.com
  3. Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails

@cla-bot cla-bot Bot removed the cla-signed label Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants