fix(api/v2): expose v2-only token route groups via the routes endpoint

This commit is contained in:
kolaente 2026-06-08 15:14:34 +02:00 committed by kolaente
parent 74510bb00a
commit 4a558fc57a
2 changed files with 48 additions and 5 deletions

View File

@ -29,8 +29,8 @@ var apiTokenRoutes = map[string]APITokenRoute{}
// apiTokenRoutesV2 holds /api/v2 routes under the same (group, permission)
// keys as v1, so a token granted e.g. labels.read_one authorises both
// versions. The frontend token UI still reads only apiTokenRoutes;
// CanDoAPIRoute consults both tables.
// versions. CanDoAPIRoute consults both tables; GetAPITokenRoutes (the /routes
// exposure the frontend reads) merges v2-only groups so they're discoverable.
var apiTokenRoutesV2 = map[string]APITokenRoute{}
func init() {
@ -346,10 +346,30 @@ func CollectRoutesForAPITokenUsage(route echo.RouteInfo, requiresJWT bool) {
}
// GetAPITokenRoutes exposes the registered scoped-token routes so tests
// and the /api/v1/routes handler share a single source of truth.
// GetAPITokenRoutes exposes the registered scoped-token routes for the /routes
// handler and tests. v1 is the base; v2-only groups and permissions (a v2-only
// resource like time-entries has no v1 counterpart) are merged in so tokens can
// discover and grant them. Shared (group, permission) keys keep their v1 entry —
// CanDoAPIRoute authorises both versions off the same key regardless.
func GetAPITokenRoutes() map[string]APITokenRoute {
return apiTokenRoutes
merged := make(map[string]APITokenRoute, len(apiTokenRoutes))
for group, perms := range apiTokenRoutes {
merged[group] = make(APITokenRoute, len(perms))
for perm, rd := range perms {
merged[group][perm] = rd
}
}
for group, perms := range apiTokenRoutesV2 {
if merged[group] == nil {
merged[group] = make(APITokenRoute)
}
for perm, rd := range perms {
if merged[group][perm] == nil {
merged[group][perm] = rd
}
}
}
return merged
}
// GetAvailableAPIRoutesForToken returns a list of all API routes which are available for token usage.

View File

@ -147,6 +147,29 @@ func TestCollectRoutes_TimeEntriesV2(t *testing.T) {
assert.Equal(t, "DELETE", te["delete"].Method)
}
// TestGetAPITokenRoutes_ExposesV2Only verifies the /routes payload merges
// v2-only groups (time-entries has no v1 counterpart) so token clients can
// discover and grant them, without mutating the v1 table itself.
func TestGetAPITokenRoutes_ExposesV2Only(t *testing.T) {
apiTokenRoutes = make(map[string]APITokenRoute)
apiTokenRoutesV2 = make(map[string]APITokenRoute)
CollectRoutesForAPITokenUsage(echo.RouteInfo{Method: "GET", Path: "/api/v1/labels"}, true)
CollectRoutesForAPITokenUsage(echo.RouteInfo{Method: "GET", Path: "/api/v2/time-entries"}, true)
routes := GetAPITokenRoutes()
_, hasLabels := routes["labels"]
assert.True(t, hasLabels, "v1 groups stay exposed")
te, hasTE := routes["time-entries"]
require.True(t, hasTE, "v2-only time-entries must be exposed via /routes")
assert.Equal(t, "GET", te["read_all"].Method)
_, v1HasTE := apiTokenRoutes["time-entries"]
assert.False(t, v1HasTE, "the merge must not mutate the v1 table")
}
// TestGetRouteDetail_V2Verbs verifies the v2 verb mapping: POST→create,
// PUT/PATCH→update. v1 inverts POST and PUT so we need a separate mapping
// path.