Skip to content

feat(api/v2): gate admin routes by feature + instance admin#2817

Merged
kolaente merged 5 commits into
mainfrom
feat-v2-admin-gate
Jun 2, 2026
Merged

feat(api/v2): gate admin routes by feature + instance admin#2817
kolaente merged 5 commits into
mainfrom
feat-v2-admin-gate

Conversation

@tink-bot

Copy link
Copy Markdown
Collaborator

What this implements

The admin + license gate for /api/v2 (which did not exist yet) and the first gated resource, GET /api/v2/admin/projects (AdminProjectList).

  • Gate (pkg/routes/routes.go): reuses the existing v1 middleware functions unchangedRequireFeature(license.FeatureAdminPanel) (pkg/routes/feature_gate.go) and RequireInstanceAdmin() (pkg/routes/admin_gate.go). Both serve 404 on failure, so gated routes are indistinguishable from unregistered ones. v1 semantics preserved exactly.
  • Resource (pkg/routes/api/v2/admin_projects.go): GET /api/v2/admin/projects lists every project on the instance (archived included), behind the gate. Standard v2 list handler mirroring labels.go — type-asserts DoReadAll's result to []*models.Project with an ok check, wraps in NewPaginated, routes errors through translateDomainError. No permission re-check in the handler (the gate owns authorization; AdminProjectList.ReadAll already returns all projects).
  • Model docs (pkg/models/project.go): added doc:/readOnly: tags to the shared Project struct so it documents correctly as a v2 schema (the first time Project is exposed via v2). Inert for v1 (swaggo/XORM/govalidator ignore the tags).
  • Tests (pkg/webtests/huma_admin_test.go, TestHumaAdminProjects): non-admin -> 404; admin without the feature -> 404; admin with the feature -> 200 list (asserts a non-owned project Importing from Trello does not work properly #6 and an archived project Special characters in VIKUNJA_DATABASE_PASSWORD #22 appear); unauthenticated -> 401. RFC 9457 error body is not re-asserted (covered globally in TestHuma_ErrorShapeIsRFC9457).

Plan: plans/v2-phase1-pr3-admin-license-gate.md

Key architectural DECISION (the plan left this open)

Where does the gate live? The plan posed two options: (a) an Echo sub-group under /api/v2/admin carrying the existing middlewares, with admin Huma operations mounted onto it; (b) a Huma-side middleware/resolver.

Recon of pkg/modules/humaecho5/humaecho5.go showed the adapter mounts a Huma API onto a single Echo router/group; per-operation Echo middleware is not threaded through Handle. NewWithGroup could mount a second Huma API on a gated /api/v2/admin sub-group — but that splits the OpenAPI spec: admin operations would no longer appear in the main /api/v2/openapi.json / Scalar docs, and would need their own spec-serving wiring.

Chosen approach: a single Huma API, with the gate applied as a path-scoped Echo middleware on the shared /api/v2 group (gateV2AdminRoutes). It composes the two existing middleware functions and fires only when the request path starts with /api/v2/admin, otherwise passing through. It is registered after SetupTokenMiddleware() so the authenticated user is available to RequireInstanceAdmin (which re-reads is_admin from the DB).

Tradeoffs:

  • (+) Reuses the exact existing check functions — no duplicated license/admin logic.
  • (+) Keeps one Huma API, so admin routes stay in the unified v2 spec + docs.
  • (+) Same check order as v1 (feature, then admin) and same 404 on failure; verified by the test.
  • (-) Path-prefix matching in middleware rather than Echo's native group routing. It is a deliberate, documented trade to avoid splitting the spec. If a future maintainer prefers spec-splitting or a Huma-native resolver, the swap is localized to registerAPIRoutesV2.

Flag for reviewers: please confirm the path-scoped-middleware choice over a separate gated Huma API. This is the load-bearing call and the riskiest part of the PR.

Test output

TestHumaAdminProjects — all 4 subtests pass. mage lint clean. (Run directly via go test -run '^TestHumaAdminProjects$' ./pkg/webtests/ because mage test:filter passes -short, which pkg/webtests/TestMain uses to skip the whole package.)


Autonomous first-pass implementation (v2 Phase-1 pattern PR). Draft for review.

@github-actions github-actions Bot added area/api REST API surface, endpoint design, request/response shapes area/auth Login, OIDC, LDAP, SAML, TOTP/2FA, PKCE, SCIM, sessions, password area/permissions Sharing, link sharing, roles, access control, assignee roles labels May 31, 2026
@github-actions

github-actions Bot commented May 31, 2026

Copy link
Copy Markdown

Preview Deployment

Preview deployments for this PR are available at:

URL Tag Commit
https://pr-2817.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:pr-2817 latest
https://sha-6076102d21bd69931a170f861bc1118259efadc7.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-6076102d21bd69931a170f861bc1118259efadc7 6076102
https://sha-3d6608cac727aa0ca77d9a9949ac17acdae7dcdd.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-3d6608cac727aa0ca77d9a9949ac17acdae7dcdd 3d6608c
https://sha-fd1030059788721f7cd7e25813673b611f4ee139.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-fd1030059788721f7cd7e25813673b611f4ee139 fd10300
https://sha-5bbc5aa1eadc2b6c9a66d26877ddf2a80eba0189.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-5bbc5aa1eadc2b6c9a66d26877ddf2a80eba0189 5bbc5aa

The preview environment will start automatically on first visit. Subsequent pushes to this PR will update the pr-2817 image — the preview picks up the new version on restart. The per-commit URLs point to a specific version and will not change.

Run locally with Docker
docker pull ghcr.io/go-vikunja/vikunja:pr-2817
docker run -p 3456:3456 ghcr.io/go-vikunja/vikunja:pr-2817

Last updated for commit 6076102

@tink-bot

Copy link
Copy Markdown
Collaborator Author

auggie review

@augmentcode

augmentcode Bot commented May 31, 2026

Copy link
Copy Markdown
🤖 Augment PR Summary

Summary: Adds an admin/license gate for v2 admin routes and introduces the first gated v2 admin endpoint.

Changes:

  • Added a path-scoped Echo middleware to apply the existing v1 admin-panel feature gate + instance-admin gate to /api/v2/admin/* (returning 404 on failure).
  • Added GET /api/v2/admin/projects to list all projects on the instance (including archived), wrapped in the standard v2 paginated envelope.
  • Enhanced models.Project with doc/readOnly struct tags so it generates correct v2 OpenAPI schema when exposed via Huma.
  • Added webtests covering unauthenticated (401), non-admin (404), admin-without-feature (404), and admin-with-feature (200) behaviors for the new endpoint.

Technical Notes: The gate reuses the existing v1 middleware unchanged to preserve semantics (feature check first, then admin check) and keep admin endpoints in the unified v2 OpenAPI spec.

🤖 Was this summary useful? React with 👍 or 👎

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

Review completed. 4 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread pkg/routes/routes.go Outdated
// The admin gate must run after the token middleware (it reads the
// authenticated user from the JWT claims) and is scoped by path so only
// /api/v2/admin/* is gated.
a.Use(gateV2AdminRoutes())

@augmentcode augmentcode Bot May 31, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

pkg/routes/routes.go:419: gateV2AdminRoutes() is registered before setupRateLimit/setupMetricsMiddleware, so requests rejected by the admin/license gate won’t be rate-limited or measured (unlike v1 admin routes which sit behind the parent group’s rate limit). Was that intentional given RequireInstanceAdmin does a DB read per request?

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Implemented in fce8de0 — reordered the v2 middleware so the admin gate runs after setupRateLimit and setupMetricsMiddleware (still after the token middleware): token, rate limit, metrics, gate. A flood to /api/v2/admin/* is now rate-limited and measured before the per-request RequireInstanceAdmin DB read.

Comment thread pkg/webtests/huma_admin_test.go
Comment thread pkg/webtests/huma_admin_test.go Outdated
assert.Contains(t, body, `"total":`)
// Project 6 is owned by user6, not shared with user1 — the admin list
// surfaces it regardless of ownership.
assert.Contains(t, body, `"id":6`)

@augmentcode augmentcode Bot May 31, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

pkg/webtests/huma_admin_test.go:81: assert.Contains(body, "\"id\":6") can false-pass (e.g. it also matches "id":60) and doesn’t guarantee the ID appears in the items list. A structured JSON assertion would make this test stricter and less brittle.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Implemented in 0eb4350 — replaced the substring check with a structured assertion that unmarshals the paginated envelope and confirms id 6 (and 22) appear in items.

Comment thread pkg/webtests/huma_admin_test.go Outdated
t.Run("admin without the feature gets 404", func(t *testing.T) {
e, err := setupTestEnv()
require.NoError(t, err)
license.ResetForTests()

@augmentcode augmentcode Bot May 31, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

pkg/webtests/huma_admin_test.go:57: this subtest calls license.ResetForTests() without a deferred reset, so future early exits or parallelization could let license state bleed into other tests. Keeping the same set/defer ResetForTests() pattern as the other subtests would make isolation clearer.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Implemented in 1c2b2db — switched to the sibling pattern (license.SetForTests(...) + defer license.ResetForTests()) so license state cannot bleed into other tests.

@tink-bot tink-bot added the area/api-v2 Huma-backed /api/v2 API surface label May 31, 2026
@kolaente kolaente marked this pull request as ready for review June 1, 2026 11:51
@tink-bot tink-bot force-pushed the feat-v2-admin-gate branch from 0d79624 to 78e38d9 Compare June 1, 2026 12:34
@kolaente kolaente added this pull request to the merge queue Jun 1, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Jun 1, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Jun 1, 2026
kolaente added 5 commits June 2, 2026 09:17
Add the admin + license gate for /api/v2 and ship the first gated
resource, GET /api/v2/admin/projects (AdminProjectList).

The gate reuses the existing v1 middleware functions unchanged —
RequireFeature(license.FeatureAdminPanel) and RequireInstanceAdmin(),
both of which serve 404 on failure. Rather than splitting the single
v2 Huma API into a separate gated sub-group (which would split the
OpenAPI spec and drop admin operations from /api/v2/openapi.json), the
gate is applied as a path-scoped Echo middleware on the shared /api/v2
group, firing only for /api/v2/admin/* and after the token middleware.
This preserves v1's 404-not-403 semantics and keeps admin routes in the
unified v2 spec and Scalar docs.

AdminProjectList lists every project on the instance (archived
included), behind the gate. Adds doc:/readOnly: tags to the shared
Project model so it documents correctly as a v2 schema.

Tests in pkg/webtests/huma_admin_test.go (TestHumaAdminProjects) cover
all three personas: non-admin -> 404, admin without feature -> 404,
admin with feature -> 200 list, plus unauthenticated -> 401.
@tink-bot tink-bot force-pushed the feat-v2-admin-gate branch from 78e38d9 to bf64abc Compare June 2, 2026 07:18
@kolaente kolaente enabled auto-merge June 2, 2026 07:18
@kolaente kolaente added this pull request to the merge queue Jun 2, 2026
Merged via the queue into main with commit 774d884 Jun 2, 2026
66 of 78 checks passed
@kolaente kolaente deleted the feat-v2-admin-gate branch June 2, 2026 07:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/api REST API surface, endpoint design, request/response shapes area/api-v2 Huma-backed /api/v2 API surface area/auth Login, OIDC, LDAP, SAML, TOTP/2FA, PKCE, SCIM, sessions, password area/permissions Sharing, link sharing, roles, access control, assignee roles

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants