Skip to content

Commit d761d67

Browse files
committed
Lock JWT-claims call shape and document GetEntryClaims posture
Add `TestGetEntryClaims_PassesJWTClaimsToService` so the JWT-bearing call shape (four mock matchers: ctx + entryType + name + jwtClaims) is covered. The existing table test uses three matchers and would silently break if the handler started passing a third option; the new case locks the contract in place. Also expand the `getEntryClaims` godoc to call out the authorization model explicitly: role gate in middleware, JWT-subset check in the service layer (mirrors the matching PUT), and the anonymous-mode short-circuit. Note that the nil-claims-to-{} normalisation is dead code in authz mode (publish forbids empty claims per auth.md §6 and the gate denies them per §4) — so future readers don't assume it's load-bearing for the authz path.
1 parent fdb4793 commit d761d67

2 files changed

Lines changed: 48 additions & 1 deletion

File tree

internal/api/v1/entries.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,21 @@ type entryClaimsResponse struct {
192192
Claims map[string]any `json:"claims"`
193193
}
194194

195-
// getEntryClaims handles GET /v1/entries/{type}/{name}/claims
195+
// getEntryClaims handles GET /v1/entries/{type}/{name}/claims.
196+
//
197+
// Authorization model:
198+
// - manageEntries role gate runs in middleware (see routes.go).
199+
// - Service-layer JWT subset check denies cross-team reads (403) when authz
200+
// is enabled, mirroring the matching PUT.
201+
// - Anonymous mode (no JWT in context): the handler skips WithJWTClaims, the
202+
// gate short-circuits, and any caller reads any entry's claims. Intended,
203+
// but worth knowing if you ever run partial-anonymous deployments.
204+
//
205+
// The response envelope `{"claims": {...}}` is always a non-nil JSON object —
206+
// the impl normalises a missing/nil claims blob to `map[string]any{}`. Under
207+
// authz, that branch is unreachable in practice (publish forbids empty claims
208+
// per auth.md §6, and the gate denies empty-claim rows per §4); the
209+
// normalisation is for the auth-off / synced-source case.
196210
//
197211
// @Summary Get entry claims
198212
// @Description Get the claims for a published entry name. Claims are stored at the

internal/api/v1/entries_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,39 @@ func TestGetEntryClaims(t *testing.T) {
629629
}
630630
}
631631

632+
// TestGetEntryClaims_PassesJWTClaimsToService verifies the handler plumbs JWT
633+
// claims through to the service when they're present in the request context.
634+
// Without this case, the table test above would let a regression slip — its
635+
// mock expectations use exactly three matchers (ctx + 2 options) and would
636+
// fail at runtime if the handler started passing a third option silently.
637+
func TestGetEntryClaims_PassesJWTClaimsToService(t *testing.T) {
638+
t.Parallel()
639+
640+
ctrl := gomock.NewController(t)
641+
t.Cleanup(ctrl.Finish)
642+
643+
mockSvc := mocks.NewMockRegistryService(ctrl)
644+
// Four matchers: ctx + WithEntryType + WithName + WithJWTClaims.
645+
mockSvc.EXPECT().GetEntryClaims(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
646+
Return(map[string]any{"org": "acme", "team": "platform"}, nil)
647+
648+
router := Router(mockSvc, nil)
649+
req, err := http.NewRequest(http.MethodGet, "/entries/server/test%2Fserver/claims", nil)
650+
require.NoError(t, err)
651+
req = req.WithContext(auth.ContextWithClaims(
652+
req.Context(), jwt.MapClaims{"org": "acme", "team": "platform"},
653+
))
654+
655+
rr := httptest.NewRecorder()
656+
router.ServeHTTP(rr, req)
657+
658+
assert.Equal(t, http.StatusOK, rr.Code)
659+
660+
var resp entryClaimsResponse
661+
require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &resp))
662+
assert.Equal(t, map[string]any{"org": "acme", "team": "platform"}, resp.Claims)
663+
}
664+
632665
// mustMarshal is a test helper that marshals v to JSON or panics.
633666
func mustMarshal(v any) []byte {
634667
b, err := json.Marshal(v)

0 commit comments

Comments
 (0)