Skip to content

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

Open
ben-fornefeld wants to merge 1 commit into
mainfrom
ory-bootstrap
Open

feat(dashboard-api): add OIDC admin user bootstrap endpoint#2841
ben-fornefeld wants to merge 1 commit 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

High Risk
Touches admin-authenticated user/identity provisioning and issuer validation; mistakes could create wrong identities, bypass issuer policy, or break concurrent first-login bootstrap.

Overview
Adds an admin-token POST /admin/users/bootstrap path so OIDC dashboards can provision a user from issuer, subject, email, and optional display name, returning the same default team payload as the existing Supabase bootstrap. The legacy POST /admin/users/{userId}/bootstrap flow is unchanged in role but documented as Supabase-only.

Provisioning now ties public users to public.user_identities (lookup by issuer/subject, upsert identity, reconcile concurrent bootstraps by keeping the canonical user and dropping orphan rows), then creates or reuses a default team and billing sink as before. oidc_issuer is restricted to configured JWT issuers (or ORY_ISSUER_URL when the profile provider is Ory-only) so arbitrary issuers cannot be planted. Identity upsert on conflict no longer reassigns user_id, only refreshes metadata and returns the stored id.

A small router regression test confirms static .../users/bootstrap and parametric .../users/:userId/bootstrap coexist without misrouting.

Reviewed by Cursor Bugbot for commit 82a6e8b. 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

❌ 5 Tests Failed:

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

Flake rate in main: 42.83% (Passed 734 times, Failed 550 times)

Stack Traces | 91.5s run time
=== RUN   TestSandboxListPaginationRunningLargerLimit
    sandbox_list_test.go:327: Created sandbox 1/12: igkmkgkd5ssfpj9mld8cn
    sandbox_list_test.go:327: Created sandbox 2/12: ispbwghznis182hqf2if3
    sandbox_list_test.go:327: Created sandbox 3/12: irm13v4o3yzfkm19jpv0f
    sandbox_list_test.go:327: Created sandbox 4/12: itad8w63fkzgrnaheb8ii
    sandbox_list_test.go:327: Created sandbox 5/12: ij24o71ayu54c5fift60j
    sandbox_list_test.go:327: Created sandbox 6/12: igxptvguy366frq7xid4g
    sandbox_list_test.go:327: Created sandbox 7/12: i9f0qf1zs0kvfe20olxmu
    sandbox_list_test.go:327: Created sandbox 8/12: ifvr3lnra8mwixuty2ujk
    sandbox_list_test.go:327: Created sandbox 9/12: iuhb6a9bw1xw77p0jh4lo
    sandbox_list_test.go:327: Created sandbox 10/12: izznaufi1km3zrgodh4x0
    sandbox_list_test.go:327: Created sandbox 11/12: i0j0gb0ncfnhkn1nxtinh
    sandbox_list_test.go:327: Created sandbox 12/12: ihe5toouj3qcggevny0yy
    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 (91.49s)
github.com/e2b-dev/infra/tests/integration/internal/tests/api/sandboxes::TestUpdateNetworkConfig

Flake rate in main: 64.64% (Passed 732 times, Failed 1338 times)

Stack Traces | 29.4s run time
=== RUN   TestUpdateNetworkConfig
=== PAUSE TestUpdateNetworkConfig
=== CONT  TestUpdateNetworkConfig
--- FAIL: TestUpdateNetworkConfig (29.38s)
github.com/e2b-dev/infra/tests/integration/internal/tests/api/sandboxes::TestUpdateNetworkConfig/pause_resume_preserves_rules

Flake rate in main: 42.33% (Passed 714 times, Failed 524 times)

Stack Traces | 0.89s run time
=== RUN   TestUpdateNetworkConfig/pause_resume_preserves_rules
Executing command curl in sandbox if1326zl6trgkkeuwd4zu
    sandbox_network_out_test.go:68: Command [curl] output: event:{start:{pid:1397}}
    sandbox_network_out_test.go:68: Command [curl] output: event:{data:{stdout:"HTTP/2 302 \r\nx-content-type-options: nosniff\r\nlocation: https://dns.google/\r\ndate: Sat, 30 May 2026 01:10:52 GMT\r\ncontent-type: text/html; charset=UTF-8\r\nserver: HTTP server (unknown)\r\ncontent-length: 216\r\nx-xss-protection: 0\r\nx-frame-options: SAMEORIGIN\r\nalt-svc: h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000\r\n\r\n"}}
    sandbox_network_out_test.go:68: Command [curl] output: event:{end:{exited:true status:"exit status 0"}}
    sandbox_network_out_test.go:68: Command [curl] completed successfully in sandbox if1326zl6trgkkeuwd4zu
Executing command curl in sandbox if1326zl6trgkkeuwd4zu
    sandbox_network_update_test.go:368: Command [curl] output: event:{start:{pid:1398}}
    sandbox_network_update_test.go:368: 
        	Error Trace:	.../api/sandboxes/sandbox_network_out_test.go:78
        	            				.../api/sandboxes/sandbox_network_update_test.go:87
        	            				.../api/sandboxes/sandbox_network_update_test.go:368
        	Error:      	"failed to execute command curl in sandbox if1326zl6trgkkeuwd4zu: invalid_argument: protocol error: incomplete envelope: unexpected EOF" does not contain "failed with exit code"
        	Test:       	TestUpdateNetworkConfig/pause_resume_preserves_rules
        	Messages:   	Expected connection failure message
--- FAIL: TestUpdateNetworkConfig/pause_resume_preserves_rules (0.89s)
github.com/e2b-dev/infra/tests/integration/internal/tests/orchestrator::TestSandboxMemoryIntegrity

Flake rate in main: 57.81% (Passed 727 times, Failed 996 times)

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

Flake rate in main: 57.92% (Passed 717 times, Failed 987 times)

Stack Traces | 187s 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:1254}}
Executing command bash in sandbox im88er0y0e369fabi8pow (user: root)
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stdout:"Total memory: 985 MB\nUsed memory before tmpfs mount: 190 MB\nFree memory before tmpfs mount: 794 MB\nMemory to use in integrity test (60% of free, min 64MB): 476 MB\n"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"476+0 records in\n476+0 records out\n499122176 bytes (499 MB, 476 MiB) copied, 2.05416 s, 243 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=476\"\n\tUser time (seconds): 0.00\n\tSystem time (seconds): 2.04\n\tPercent of CPU this job got: 99%\n\tElapsed (wall clock) time (h:mm:ss or m:ss): 0:02.06\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): 2700\n\tAverage resident set size (kbytes): 0\n\tMajor (requiring I/O) page faults: 2\n\tMinor (reclaiming a frame) page faults: 345\n\tVoluntary context switches: 3\n\tInvoluntary context switches: 8\n\tSwaps: 0\n\tFile system inputs: 176\n\tFile system outputs: 0\n\tSocket me"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"ssages 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: 672 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 irs9ckwpnavkfqq7ok00o
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
    sandbox_memory_integrity_test.go:80: Command [bash] output: event:{start:{pid:1270}}
Executing command bash in sandbox ioonb30a4oi7i8okx83re (user: root)
    sandbox_memory_integrity_test.go:80: Command [bash] output: event:{data:{stdout:"2ae00635aa7979098c901ab87217a44724fffc0623247ed5c2eb10e6a6553911\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 irs9ckwpnavkfqq7ok00o
Executing command bash in sandbox ioonb30a4oi7i8okx83re (user: root)
    sandbox_memory_integrity_test.go:80: Command [bash] output: event:{start:{pid:1274}}
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (user: root)
Executing command bash in sandbox irs9ckwpnavkfqq7ok00o (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 irs9ckwpnavkfqq7ok00o: 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 (187.02s)

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