Skip to content

feat(dashboard-api): add OIDC admin user bootstrap endpoint#2841

Merged
ben-fornefeld merged 2 commits into
mainfrom
ory-bootstrap
Jun 1, 2026
Merged

feat(dashboard-api): add OIDC admin user bootstrap endpoint#2841
ben-fornefeld merged 2 commits into
mainfrom
ory-bootstrap

Conversation

@ben-fornefeld
Copy link
Copy Markdown
Member

@ben-fornefeld ben-fornefeld commented May 28, 2026

Summary

  • Add admin-token POST /admin/users/bootstrap for OIDC user provisioning (issuer, subject, email, optional name).
  • Upsert public.users and public.user_identities before default team creation, with issuer allow-listing aligned to the Ory profile resolver.
  • Add route conflict regression test for sibling POST /admin/users/bootstrap and POST /admin/users/{userId}/bootstrap.

Stack

Stacks on #2840 (Ory user profile provider). Merge #2840 first, then rebase this onto main or merge the stack.

Dashboard Ory sign-in (dashboard.full-stack PR #342) depends on this PR landing.

@cursor
Copy link
Copy Markdown

cursor Bot commented May 28, 2026

PR Summary

Medium Risk
Touches admin-authenticated user provisioning and OIDC identity storage; mitigated by issuer allow-listing, transactional identity handling, and broad tests, but still security-sensitive.

Overview
Adds an admin-token POST /admin/users/bootstrap path so OIDC dashboards can provision users from issuer, subject, email, and optional name without going through the legacy Supabase user-id bootstrap. The handler validates required OIDC fields, runs bootstrapOIDCUser, and returns the same team resolve payload as other bootstrap flows.

Provisioning now ties public.user_identities to public.users and default team creation in one transactional path, with issuer allow-listing (configured JWT issuers / Ory URL) so arbitrary issuers cannot be planted. UpsertPublicIdentity returns the canonical user_id on conflict instead of overwriting an existing mapping, and concurrent bootstraps reconcile to a single identity and default team. JWT identity resolution reads from the primary DB to avoid replica lag right after bootstrap.

Reviewed by Cursor Bugbot for commit b8c1c57. Bugbot is set up for automated code reviews on this repo. Configure here.

Copy link
Copy Markdown
Contributor

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

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 a new admin endpoint /admin/users/bootstrap to bootstrap users authenticated via generic OIDC providers. It adds the corresponding request schema, handler implementation, and database query updates to safely handle concurrent bootstrap requests by resolving identity conflicts and cleaning up orphan user records. Comprehensive unit tests have also been added to verify the routing, OIDC issuer validation, and concurrent bootstrap behavior. No review comments were provided, and there are no critical findings or feedback to report.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 28, 2026

❌ 4 Tests Failed:

Tests completed Failed Passed Skipped
2706 4 2702 7
View the full list of 4 ❄️ flaky test(s)
github.com/e2b-dev/infra/tests/integration/internal/tests/api/sandboxes::TestSandboxListPaginationRunningLargerLimit

Flake rate in main: 42.01% (Passed 791 times, Failed 573 times)

Stack Traces | 94.3s run time
=== RUN   TestSandboxListPaginationRunningLargerLimit
    sandbox_list_test.go:327: Created sandbox 1/12: i21b8bqbsngtk7xchscjs
    sandbox_list_test.go:327: Created sandbox 2/12: iq24qvsfp5pfjgxa0mf8c
    sandbox_list_test.go:327: Created sandbox 3/12: ith7hecgbe7oejb4i86mv
    sandbox_list_test.go:327: Created sandbox 4/12: iozd3z4e1kpotq4f9buhk
    sandbox_list_test.go:327: Created sandbox 5/12: i0zj46oun1sf7yoleprcp
    sandbox_list_test.go:327: Created sandbox 6/12: i4cp11ye0h3rxs6d8wtoz
    sandbox_list_test.go:327: Created sandbox 7/12: i835q86y5tr7q6n8j0d7j
    sandbox_list_test.go:327: Created sandbox 8/12: iexpobpu8od93o85vtmrx
    sandbox_list_test.go:327: Created sandbox 9/12: iojhexb5cc5r4wone5gj4
    sandbox_list_test.go:327: Created sandbox 10/12: i2pycyxm85hhe17op493r
    sandbox_list_test.go:327: Created sandbox 11/12: ifvabtnhxjyg0nvg8ix7q
    sandbox_list_test.go:327: Created sandbox 12/12: ixdrw7iozuwnkwxsmdoyz
    sandbox_list_test.go:330: 
        	Error Trace:	.../api/sandboxes/sandbox_list_test.go:340
        	            				.../hostedtoolcache/go/1.26.3.../src/runtime/asm_amd64.s:1771
        	Error:      	"[]" should have 12 item(s), but has 0
    sandbox_list_test.go:330: 
        	Error Trace:	.../api/sandboxes/sandbox_list_test.go:330
        	Error:      	Condition never satisfied
        	Test:       	TestSandboxListPaginationRunningLargerLimit
--- FAIL: TestSandboxListPaginationRunningLargerLimit (94.26s)
github.com/e2b-dev/infra/tests/integration/internal/tests/orchestrator::TestSandboxMemoryIntegrity

Flake rate in main: 56.90% (Passed 784 times, Failed 1035 times)

Stack Traces | 60.8s run time
=== RUN   TestSandboxMemoryIntegrity
=== PAUSE TestSandboxMemoryIntegrity
=== CONT  TestSandboxMemoryIntegrity
    sandbox_memory_integrity_test.go:27: Build completed successfully
--- FAIL: TestSandboxMemoryIntegrity (60.82s)
github.com/e2b-dev/infra/tests/integration/internal/tests/orchestrator::TestSandboxMemoryIntegrity/tmpfs_hash

Flake rate in main: 57.00% (Passed 774 times, Failed 1026 times)

Stack Traces | 195s run time
=== RUN   TestSandboxMemoryIntegrity/tmpfs_hash
=== PAUSE TestSandboxMemoryIntegrity/tmpfs_hash
=== CONT  TestSandboxMemoryIntegrity/tmpfs_hash
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{start:{pid:1263}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stdout:"Total memory: 985 MB\nUsed memory before tmpfs mount: 191 MB\nFree memory before tmpfs mount: 793 MB\nMemory to use in integrity test (60% of free, min 64MB): 475 MB\n"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"475+0 records in\n475+0 records out\n498073600 bytes (498 MB, 475 MiB) copied, 1.84608 s, 270 MB/s\n"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"\tCommand being timed: \"dd if=/dev/urandom of=/mnt/testfile bs=1M count=475\"\n\tUser time (seconds): 0.00\n\tSystem time (seconds): 1.81\n\tPercent of CPU this job got: 98%\n\tElapsed (wall clock) time (h:mm:ss or m:ss): 0:01.85\n\tAverage shared text size (kbytes): 0\n\tAverage unshared data size (kbytes): 0\n\tAverage stack size (kbytes): 0\n\tAverage total size (kbytes): 0\n\tMaximum resident set size (kbytes): 2632\n\tAverage resident set size (kbytes): 0\n\tMajor (requiring I/O) page faults: 3\n\tMinor (reclaiming a frame) page faults: 343\n\tVoluntary context switches: 4\n\tInvoluntary context switches: 7\n\tSwaps: 0\n\tFile system inputs: 176\n\tFile system outputs: 0\n\tSocket messages sent: 0\n\tSocket messages received: 0\n\tSignals delivered: 0\n\tPage size (bytes): 4096\n\tExit status: 0\n"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stdout:"Used memory after tmpfs mount and file fill: 671 MB\n"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{end:{exited:true  status:"exit status 0"}}
    sandbox_memory_integrity_test.go:70: Command [bash] completed successfully in sandbox ihr1exeajbdpj7rn147im
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
    sandbox_memory_integrity_test.go:80: Command [bash] output: event:{start:{pid:1279}}
Executing command bash in sandbox i6tkyqvhv60ae8ezlojth (user: root)
    sandbox_memory_integrity_test.go:80: Command [bash] output: event:{data:{stdout:"980fb363b777c352fccf4eb5c597316e1df67a4cecacf3591a1d01a0f586b293\n"}}
    sandbox_memory_integrity_test.go:80: Command [bash] output: event:{end:{exited:true  status:"exit status 0"}}
    sandbox_memory_integrity_test.go:80: Command [bash] completed successfully in sandbox ihr1exeajbdpj7rn147im
    sandbox_memory_integrity_test.go:80: Command [bash] output: event:{start:{pid:1282}}
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
Executing command bash in sandbox ihr1exeajbdpj7rn147im (user: root)
    sandbox_memory_integrity_test.go:110: 
        	Error Trace:	.../tests/orchestrator/sandbox_memory_integrity_test.go:81
        	            				.../hostedtoolcache/go/1.26.3.../src/runtime/asm_amd64.s:1771
        	Error:      	Received unexpected error:
        	            	failed to execute command bash in sandbox ihr1exeajbdpj7rn147im: unavailable: HTTP status 502 Bad Gateway
    sandbox_memory_integrity_test.go:110: 
        	Error Trace:	.../tests/orchestrator/sandbox_memory_integrity_test.go:78
        	            				.../tests/orchestrator/sandbox_memory_integrity_test.go:110
        	Error:      	Condition never satisfied
        	Test:       	TestSandboxMemoryIntegrity/tmpfs_hash
--- FAIL: TestSandboxMemoryIntegrity/tmpfs_hash (195.05s)
github.com/e2b-dev/infra/tests/integration/internal/tests/proxies::TestSandboxAutoResumeViaProxy

Flake rate in main: 42.71% (Passed 778 times, Failed 580 times)

Stack Traces | 12.4s run time
=== RUN   TestSandboxAutoResumeViaProxy
=== PAUSE TestSandboxAutoResumeViaProxy
=== CONT  TestSandboxAutoResumeViaProxy
    auto_resume_test.go:116: 
        	Error Trace:	.../tests/proxies/auto_resume_test.go:116
        	Error:      	Received unexpected error:
        	            	Get "http://localhost:3002": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
        	Test:       	TestSandboxAutoResumeViaProxy
--- FAIL: TestSandboxAutoResumeViaProxy (12.40s)

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Copy link
Copy Markdown

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

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: e6222cb0dd

ℹ️ 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 on lines +171 to +175
canonicalUserID, err := authTxDB.UpsertPublicIdentity(ctx, authqueries.UpsertPublicIdentityParams{
OidcIss: identity.Issuer,
OidcSub: identity.Subject,
UserID: candidateUserID,
})
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 Resolve freshly bootstrapped identities from primary

When AUTH_DB_READ_REPLICA_CONNECTION_STRING points at a lagging replica, this endpoint can return 200 after inserting the (iss, sub) row on the primary, but the next OIDC-authenticated request still resolves identities through authDB.Read in packages/auth/pkg/auth/service.go:77. That makes the bootstrap flow intermittently fail with 401 until replication catches up; the OIDC login path needs to read the newly created identity from primary or otherwise wait/invalidate before reporting success.

Useful? React with 👍 / 👎.

@ben-fornefeld ben-fornefeld force-pushed the ory-bootstrap branch 5 times, most recently from ee666da to 6fef570 Compare May 29, 2026 22:00
Base automatically changed from ory-userprofile to main May 30, 2026 00:54
Add POST /admin/users/bootstrap for dashboard Ory sign-in provisioning,
with issuer allow-listing, identity upsert concurrency handling, and a
route conflict regression test.
@ben-fornefeld ben-fornefeld merged commit 6a7a59e into main Jun 1, 2026
51 checks passed
@ben-fornefeld ben-fornefeld deleted the ory-bootstrap branch June 1, 2026 21:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants