Skip to content

feat(backend): [DEMO] add v2 group endpoints - #7549

Draft
maverbiest wants to merge 3 commits into
v2-in-backend-experimentfrom
v2-groups-endpoint
Draft

maverbiest wants to merge 3 commits into
v2-in-backend-experimentfrom
v2-groups-endpoint

Conversation

@maverbiest

@maverbiest maverbiest commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Note

Demo, stacked on #7540. It shows what adding a v2 endpoint looks like once the backend is split into modules. #7539 adds the same endpoints as a separate service; the comparison is below.

What this does

  • New v2 module, served under /v2. It depends only on core, never on v1.
  • The groups code moves from v1 to core, so v1 and v2 share one implementation. v2 groups is just a new controller over the existing service.

The diff

  • Groups code to core: tables, types and the permission checks are pure renames. Changes to GroupManagementDatabaseService:
    • new getGroup and getMembers methods, with v1's getDetailsOfGroup built from them
    • lists are ordered by id (the only behaviour change for v1)
    • written with Exposed's DSL only: the GroupEntity/UserGroupEntity DAO classes are removed, and v1's released-data stream joins the groups table instead of looking up each row's group name through them
    • uses PSQLState.UNIQUE_VIOLATION instead of a constant from v1's SubmitModel
  • Token handling to core: v1's bearer-token setup (token validation and the logging of rejected requests) is now one shared function in core, used by both v1's and v2's security chains. That is the only change to v1's code.
  • v2 main code (~120 lines):
    • config/V2Config.kt: the /v2/** security chain
    • controller/GroupController.kt: the group endpoints
  • Tests: about 290 lines in v2, plus one app-level test checking that v1 and v2 coexist. v2's tests run against an application with core and v2 only, without v1. The shared group test constants move to core's test fixtures.

v2 group endpoints

v2 v1 equivalent Access
GET /v2/groups?name= GET /groups?name= authenticated
POST /v2/groups → 201 + Location POST /groups → 200 authenticated
GET /v2/groups/{groupId} GET /groups/{groupId} (.group) public; contact email hidden for anonymous callers
PUT /v2/groups/{groupId} PUT /groups/{groupId} authenticated; members and superusers only (checked in the service)
GET /v2/groups/{groupId}/members GET /groups/{groupId} (.users) authenticated
PUT /v2/groups/{groupId}/members/{username} PUT /groups/{groupId}/users/{username} authenticated; members and superusers only
DELETE /v2/groups/{groupId}/members/{username} DELETE /groups/{groupId}/users/{username} authenticated; members and superusers only
GET /v2/me/groups GET /user/groups authenticated

Access control in v2

  • Same mechanism as v1: Spring Security's standard rules on URLs. v2 has its own security chain for /v2/**, ordered before v1's catch-all chain. Every v2 endpoint requires a token, except the public ones the chain lists (currently only GET /v2/groups/{groupId}).
  • Accurate docs. Public endpoints are also marked with an empty @SecurityRequirements (a standard Swagger annotation), so the API docs say they need no token (Document authorization more explicitly to make it easier to test correct behavior and avoid issues #1614). In v1, the docs claim every endpoint needs a token.
  • Docs and security chain checked against each other. PublicEndpointsTest pins which endpoints the docs call public, then sends a request without a token to every v2 endpoint to check that the security chain agrees. Making an endpoint public therefore always shows up in review, and the docs can't silently disagree with the security chain.
  • CSRF protection stays enabled, as in v1. Requests with a bearer token skip it, and writes without a token get 403.
  • Data-dependent checks stay in the service, e.g. group membership.
  • Considered and dropped: chore(api): [DEMO – DO NOT MERGE] separate v2 API service with group management #7539's custom per-endpoint access annotations with an interceptor. They make access visible on every endpoint, but replace Spring Security's standard enforcement with our own code.

Compared with #7539

#7539 (separate service) This PR (module in the backend)
Deployment New image, Helm chart, ingress route and CI needed Unchanged
Table definitions and migrations Duplicated; tests run the backend's migrations to catch drift One set, in core
Shared code (auth, errors, config) Copied (~520 lines) Already in core (#7540)
Group logic Reimplemented in a second service One service shared by v1 and v2
Transactions across v1 and v2 code Not possible Possible
Load isolation, Jackson 3 for v2 alone Yes No

Things to discuss

  • Shared service. v2 inherits the service's current behaviour, including its quirks. On update it checks that the caller exists in Keycloak. When adding a member, it checks that the user exists before checking group membership, which lets non-members test whether a username exists. Fixing these in core changes v1 as well.
  • Global behaviour still in v1. Read-only mode, CORS, request-ID logging and the base OpenAPI setup still live in v1. They apply to v2 at runtime because it's one app, but v2's own tests don't cover them.
  • Role-based access. Admin-only endpoints (e.g. for seqsets) would use hasAuthority(...) rules in the security chain as v1 does, or Spring's @PreAuthorize on the method.

Testing

  • ./gradlew test: all 672 tests pass. That's the 661 existing tests plus 11 new ones: 8 for the group endpoints, 1 public-endpoints test and 2 app-level tests.
  • ./gradlew ktlintCheck is clean.
  • PublicEndpointsTest catches mismatches in both directions: it fails both when the docs annotation is removed from the public endpoint, and when the security chain makes another endpoint public.
  • Smoke test of the built jar against a fresh Postgres:
    • health is UP
    • without a token: GET /v2/groups/1 → 404 (public, the group doesn't exist), GET /v2/groups → 401, POST /v2/groups → 403, POST /groups (v1) → 403
    • /api-docs lists 29 v1 and 5 v2 paths, with security: [] only on GET /v2/groups/{groupId}

🤖 Generated with Claude Code

🚀 Preview: Add preview label to enable

Add the v2 module with its own security chain for /v2/**, per-endpoint access annotations (documented in the OpenAPI spec as x-access) and the group endpoints. The groups code moves from v1 to core so that v1 and v2 share one implementation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@claude claude Bot added the backend related to the loculus backend component label Oct 15, 2026
@maverbiest
maverbiest added this pull request to stack #7550 October 15, 2026 13:37

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend related to the loculus backend component

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants