Skip to content

feat(dashboard-api): add admin endpoint for listing tiers - #3399

Draft
tvi wants to merge 2 commits into
mainfrom
feat/dashboard-api-admin-tiers
Draft

feat(dashboard-api): add admin endpoint for listing tiers#3399
tvi wants to merge 2 commits into
mainfrom
feat/dashboard-api-admin-tiers

Conversation

@tvi

@tvi tvi commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Problem

public.tiers is not reachable over HTTP. Admin clients that display a team's tier, filter teams by tier, or offer a tier change have no way to obtain the set of tiers to choose from — every other admin surface is already served by dashboard-api, so this is the one gap that forces a direct database connection.

Nothing in the repo queried public.tiers before this change: the existing "tier" queries all read the denormalized teams.tier text column or the team_limits view, so there was no model or query to extend.

Changes

  • GET /admin/tiers in spec/openapi-dashboard.yml, secured with AdminApiKeyAuth (X-Admin-Token) like the other /admin/* routes. Returns {"tiers": [{"id", "name"}]} ordered by name.
  • ListTiers sqlc query under packages/db/pkg/dashboard/sql_queries/tiers/, and the handler in internal/handlers/admin_tiers.go.
  • The response projects only id and name. Tiers carry resource limits (max_vcpu, max_ram_mb, concurrent_instances, disk entitlements, …) that a tier picker has no business seeing, and keeping them out means adding a column doesn't silently widen the payload.
  • Empty result serializes as [], not null.

Regenerating api.gen.go also moves it from oapi-codegen v2.7.0 to v2.7.1, which accounts for most of the diff in that file. The version is what go.mod already pins and what every other package's generated file was built with — packages/dashboard-api had drifted behind because the root make generate target doesn't include it.

Testing

go test ./packages/dashboard-api/... (testcontainers Postgres), plus go vet and npx @redocly/cli lint spec/openapi-dashboard.yml — the only new lint warning is operation-operationId, which all 26 existing operations also emit.

Three layers:

  • admin_tiers_test.go — DB-backed: ordering by name across seeded and inserted tiers, and the empty-table case serializing as {"tiers":[]}. The fixture clones the migration-seeded tier through jsonb_populate_record so adding a column to public.tiers doesn't break it.
  • admin_tiers_route_test.go — drives the generated router behind the spec's request validator: rejects a missing and a wrong token with 401, serves the tier list with a valid one. Calling the handler function directly cannot catch a route registered without security, which is the failure mode that matters for /admin/*. This test also has to reproduce the status promotion main.go performs, since the validator surfaces auth failures as 400 and the authenticator's 401 has to win.
  • route_conflict_test.go — asserts every admin route declares AdminApiKeyAuth and does not accept a team API key.

I checked the auth tests aren't vacuous by temporarily dropping security: from the new path and regenerating: both the route test and the spec assertion fail, and the endpoint starts answering unauthenticated requests with 200.

tvi added 2 commits July 26, 2026 15:13
Admin clients that render or edit a team's tier need the set of tiers to
choose from, and public.tiers was not reachable over HTTP.

GET /admin/tiers is admin-token authenticated and returns {id, name}
ordered by name.

Regenerating api.gen.go also picks up oapi-codegen v2.7.1, which the
checked-in file had drifted behind.
Calling the handler directly can't catch a route registered without
security, which is the failure mode that matters for /admin/*. Exercise
the generated router behind the spec-driven validator instead, including
the status promotion main.go relies on to turn the validator's 400 into
the 401 the authenticator wrote.
@cursor

cursor Bot commented Jul 26, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Read-only admin listing behind existing X-Admin-Token auth; no changes to tier assignment or limits enforcement.

Overview
Adds GET /admin/tiers so admin clients can load platform tier id/name pairs for pickers and filters without hitting the database directly. The route uses AdminApiKeyAuth (X-Admin-Token) like other /admin/* endpoints and returns tiers sorted by name, exposing only id and name (not limit columns). A sqlc ListTiers query backs the handler; OpenAPI and generated server code are updated (including oapi-codegen v2.7.1 on api.gen.go). Tests cover ordering, empty [] responses, admin-token auth on the real router, and OpenAPI checks that admin routes require admin auth and reject team API keys.

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

@codecov

codecov Bot commented Jul 26, 2026

Copy link
Copy Markdown

❌ 5 Tests Failed:

Tests completed Failed Passed Skipped
3512 5 3507 9
View the top 3 failed test(s) by shortest run time
github.com/e2b-dev/infra/tests/integration/internal/tests/envd::TestListDir/depth_1_lists_root_directory
Stack Traces | 0.01s run time
=== RUN   TestListDir/depth_1_lists_root_directory
=== PAUSE TestListDir/depth_1_lists_root_directory
=== CONT  TestListDir/depth_1_lists_root_directory
    filesystem_test.go:96: 
        	Error Trace:	.../tests/envd/filesystem_test.go:96
        	Error:      	Received unexpected error:
        	            	unavailable: 502 Bad Gateway
        	Test:       	TestListDir/depth_1_lists_root_directory
--- FAIL: TestListDir/depth_1_lists_root_directory (0.01s)
github.com/e2b-dev/infra/tests/integration/internal/tests/envd::TestListDir/depth_2_lists_first_level_of_subdirectories_(in_this_case_the_root_directory)
Stack Traces | 0.01s run time
=== RUN   TestListDir/depth_2_lists_first_level_of_subdirectories_(in_this_case_the_root_directory)
=== PAUSE TestListDir/depth_2_lists_first_level_of_subdirectories_(in_this_case_the_root_directory)
=== CONT  TestListDir/depth_2_lists_first_level_of_subdirectories_(in_this_case_the_root_directory)
    filesystem_test.go:96: 
        	Error Trace:	.../tests/envd/filesystem_test.go:96
        	Error:      	Received unexpected error:
        	            	unavailable: 502 Bad Gateway
        	Test:       	TestListDir/depth_2_lists_first_level_of_subdirectories_(in_this_case_the_root_directory)
--- FAIL: TestListDir/depth_2_lists_first_level_of_subdirectories_(in_this_case_the_root_directory) (0.01s)
github.com/e2b-dev/infra/tests/integration/internal/tests/envd::TestListDir/depth_0_lists_only_root_directory
Stack Traces | 0.02s run time
=== RUN   TestListDir/depth_0_lists_only_root_directory
=== PAUSE TestListDir/depth_0_lists_only_root_directory
=== CONT  TestListDir/depth_0_lists_only_root_directory
    filesystem_test.go:96: 
        	Error Trace:	.../tests/envd/filesystem_test.go:96
        	Error:      	Received unexpected error:
        	            	unavailable: 502 Bad Gateway
        	Test:       	TestListDir/depth_0_lists_only_root_directory
--- FAIL: TestListDir/depth_0_lists_only_root_directory (0.02s)
github.com/e2b-dev/infra/tests/integration/internal/tests/envd::TestListDir
Stack Traces | 6.26s run time
=== RUN   TestListDir
=== PAUSE TestListDir
=== CONT  TestListDir
--- FAIL: TestListDir (6.26s)
View the full list of 1 ❄️ flaky test(s)
github.com/e2b-dev/infra/tests/integration/internal/tests/envd::TestListDir/depth_3_lists_all_directories_and_files

Flake rate in main: 30.77% (Passed 9 times, Failed 4 times)

Stack Traces | 0.01s run time
=== RUN   TestListDir/depth_3_lists_all_directories_and_files
=== PAUSE TestListDir/depth_3_lists_all_directories_and_files
=== CONT  TestListDir/depth_3_lists_all_directories_and_files
    filesystem_test.go:96: 
        	Error Trace:	.../tests/envd/filesystem_test.go:96
        	Error:      	Received unexpected error:
        	            	unavailable: 502 Bad Gateway
        	Test:       	TestListDir/depth_3_lists_all_directories_and_files
--- FAIL: TestListDir/depth_3_lists_all_directories_and_files (0.01s)

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

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

LGTM. Straightforward, well-tested addition that mirrors existing admin endpoint patterns exactly.

What was reviewed: the new GET /admin/tiers route wiring (spec, generated router, handler), the ListTiers sqlc query, and the auth/route-level tests. Confirmed the endpoint is gated by AdminApiKeyAuth (not the team ApiKeyAuth) via route_conflict_test.go, and that the response only projects id/name from public.tiers, excluding resource-limit columns.

Extended reasoning...

Overview

The PR adds a single new read-only admin endpoint, GET /admin/tiers, following the exact pattern of existing /admin/* routes in packages/dashboard-api. Changes span: the OpenAPI spec (new path + schemas), the generated api.gen.go (routing plumbing plus an unrelated oapi-codegen v2.7.0→v2.7.1 version bump that the author explains is just catching this package up to what every other generated file already uses), a new sqlc query (ListTiers) reading id, name from public.tiers ordered by name, and a new handler (admin_tiers.go) that maps the rows into the response DTO.

Security risks

The endpoint is read-only, requires AdminApiKeyAuth (X-Admin-Token), and deliberately excludes tier resource-limit fields (max_vcpu, max_ram_mb, etc.) from the response, projecting only id and name. route_conflict_test.go explicitly asserts the route requires the admin scheme and does not accept a team API key, guarding against the route being reachable without the intended auth. No user input is taken (no path/query params), so there's no injection surface. I don't see any security concerns here.

Level of scrutiny

This is a low-risk, additive, read-only admin endpoint that follows an established pattern almost line-for-line (compare to admin_auth_provider_profiles.go and its route tests). It doesn't touch production request-serving paths for sandboxes/orchestration, doesn't modify existing behavior, and the generated-code diff is mechanical. A light-touch review is appropriate here.

Other factors

Test coverage is strong for the size of the change: DB-backed ordering/empty-list tests, a route-level test that exercises the real request validator + authenticator (specifically to catch the "route registered without security" failure mode), and a spec-level assertion that the route declares the right security scheme. The author also states they verified the auth tests aren't vacuous by temporarily removing security: and confirming the tests fail. No outstanding reviewer comments exist on the PR (only a Cursor bot summary placeholder). The bug hunting system found no issues, and my own reading of the diff didn't surface anything additional.

@tvi
tvi marked this pull request as draft July 26, 2026 20:27
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.

1 participant