Skip to content

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

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

fix(dashboard-api): support Hydra-only deployments and pre-Ory user migration#3224
AdaAibaby wants to merge 3 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:

  • GetUserEmailsByUserIDs — bulk email lookup by user ID via default team
  • FindUserIDByEmail — reverse lookup: email → user_id via default team

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

  • GetTeamsTeamIDMembers: falls back to GetUserEmailsByUserIDs when Ory admin call fails
  • PostTeamsTeamIDMembers: falls back to FindUserIDByEmail when Ory admin call fails

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

  • bootstrapUserWithIdentity: 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 Ory identity for this issuer. If both conditions hold, reuses the existing user_id — preserving API keys, templates, and team memberships across the auth migration.
  • resolveProfile: same DB fallback pattern for single-user profile resolution.

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

Returns user_id alongside id and slug in the bootstrap response so callers can read the canonical public.users id.

Safety

The email-match merge only fires when:

  1. No user_identities row exists for (oidc_iss, oidc_sub) — new identity
  2. A user with the matching email exists in public.teams (default team)
  3. That user has zero existing user_identities rows for this issuer

Condition 3 prevents email-based account takeover for users already linked to Ory.

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

@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 implements fallback mechanisms to the local database when the Ory admin API is unavailable, ensuring that user profile lookups, email retrievals, and team member additions can still succeed. It also adds logic to preserve pre-Ory user accounts by reusing their user IDs during bootstrap. The review feedback highlights three main issues: first, database errors during the pre-Ory user migration lookup are silently ignored, which could lead to duplicate account creation under transient DB failures; second, using an untyped gin.H map in the bootstrap handler bypasses compile-time type safety and OpenAPI contracts; and third, the case-insensitive email lookup in the database will cause full table scans unless a functional index is created on lower(email).

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.

Comment thread packages/dashboard-api/internal/handlers/utils_team_provisioning.go Outdated
Comment thread packages/dashboard-api/internal/handlers/admin_users_bootstrap.go Outdated
Comment thread packages/db/pkg/auth/profiles.go

@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: e0d66563e2

ℹ️ 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/db/pkg/auth/profiles.go Outdated
Comment thread packages/dashboard-api/internal/handlers/utils_team_provisioning.go Outdated

@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: e289eb47f5

ℹ️ 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 Outdated
Comment on lines +21 to +22
WHERE ut.user_id = ANY($1::uuid[])
AND ut.is_default = true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fail or cover SSO members in DB profile fallback

When Ory is unavailable for an SSO-managed team, this fallback only reads emails through default-team memberships. enrollSSOMember inserts SSO memberships with is_default=false and SSO users have no default team, so those members are absent from profiles and the handler silently skips them while returning 200, making the member list incomplete exactly in the outage/Hydra-only path; fail closed for SSO teams or use another email source.

Useful? React with 👍 / 👎.

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.

follow-up

AdaAibaby pushed a commit to AdaAibaby/infra that referenced this pull request Jul 10, 2026
SSO-backed identities (IdentityOrganizationID != uuid.Nil) must go
through enrollSSOMember so the user lands in the org's auto-join team.
Previously the email-match migration reused the pre-Ory personal default
team and returned early, silently skipping SSO team enrollment entirely.

Gate the migration on ssoOrgID == uuid.Nil: pre-Ory users authenticating
via a plain OIDC issuer still get their account preserved; SSO-backed
identities fall through to the normal enrollSSOMember path and get a
fresh account in the org team. A follow-up can handle the case where a
pre-Ory SSO user also needs their old account merged.

Addresses Codex P2 review comment on PR e2b-dev#3224.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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
@AdaAibaby AdaAibaby force-pushed the fix/dashboard-api-ory-hydra-only-support branch from 8b565b4 to afa0510 Compare July 10, 2026 09:58
@AdaAibaby

Copy link
Copy Markdown
Contributor Author

Reopening with clean commit history.

@AdaAibaby AdaAibaby closed this Jul 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant