detached 546e1a6

refactor(auth): move context keys to domain and fix review feedback

commit
546e1a6fd866f39918bb079202d71b9d5d30159c
author
bnema · · 2026-01-28 21:55 UTC
parent
e32c0a4
signature
unsigned
8 files · +56 -31
M internal/adapters/in/cli/auth.go +11 -5
@@ -656,7 +656,8 @@ func createAuthServiceForCLI(cfg *cliConfig, log zerowrap.Logger) (*auth.Service
656656
657657 // newAuthStatusCmd creates the auth status command.
658658 func newAuthStatusCmd() *cobra.Command {
659- return &cobra.Command{
659+ var remoteNameFlag string
660+ cmd := &cobra.Command{
660661 Use: "status",
661662 Short: "Check authentication session status",
662663 Long: `Verify if stored authentication session is still valid.
@@ -667,21 +668,26 @@ Examples:
667668 gordon auth status Check status of active remote
668669 gordon auth status --remote prod Check specific remote`,
669670 RunE: func(cmd *cobra.Command, args []string) error {
670- return runAuthStatus()
671+ return runAuthStatus(remoteNameFlag)
671672 },
672673 }
674+ cmd.Flags().StringVar(&remoteNameFlag, "remote", "", "Check specific remote")
675+ return cmd
673676 }
674677
675678 // runAuthStatus checks authentication status for the active remote.
676-func runAuthStatus() error {
679+func runAuthStatus(remoteNameArg string) error {
677680 // Load remotes config
678681 config, err := remote.LoadRemotes("")
679682 if err != nil {
680683 return fmt.Errorf("failed to load remotes: %w", err)
681684 }
682685
683- // Determine active remote
684- remoteName := config.Active
686+ // Determine remote name
687+ remoteName := remoteNameArg
688+ if remoteName == "" {
689+ remoteName = config.Active
690+ }
685691 if remoteName == "" {
686692 return fmt.Errorf("no active remote configured. Use 'gordon remote use <name>' or --remote flag")
687693 }
@@ -656,7 +656,8 @@ func createAuthServiceForCLI(cfg *cliConfig, log zerowrap.Logger) (*auth.Service
656 656
657 // newAuthStatusCmd creates the auth status command.657 // newAuthStatusCmd creates the auth status command.
658 func newAuthStatusCmd() *cobra.Command {658 func newAuthStatusCmd() *cobra.Command {
659- return &cobra.Command{659+ var remoteNameFlag string
660+ cmd := &cobra.Command{
660 Use: "status",661 Use: "status",
661 Short: "Check authentication session status",662 Short: "Check authentication session status",
662 Long: `Verify if stored authentication session is still valid.663 Long: `Verify if stored authentication session is still valid.
@@ -667,21 +668,26 @@ Examples:
667 gordon auth status Check status of active remote668 gordon auth status Check status of active remote
668 gordon auth status --remote prod Check specific remote`,669 gordon auth status --remote prod Check specific remote`,
669 RunE: func(cmd *cobra.Command, args []string) error {670 RunE: func(cmd *cobra.Command, args []string) error {
670- return runAuthStatus()671+ return runAuthStatus(remoteNameFlag)
671 },672 },
672 }673 }
674+ cmd.Flags().StringVar(&remoteNameFlag, "remote", "", "Check specific remote")
675+ return cmd
673 }676 }
674 677
675 // runAuthStatus checks authentication status for the active remote.678 // runAuthStatus checks authentication status for the active remote.
676-func runAuthStatus() error {679+func runAuthStatus(remoteNameArg string) error {
677 // Load remotes config680 // Load remotes config
678 config, err := remote.LoadRemotes("")681 config, err := remote.LoadRemotes("")
679 if err != nil {682 if err != nil {
680 return fmt.Errorf("failed to load remotes: %w", err)683 return fmt.Errorf("failed to load remotes: %w", err)
681 }684 }
682 685
683- // Determine active remote686+ // Determine remote name
684- remoteName := config.Active687+ remoteName := remoteNameArg
688+ if remoteName == "" {
689+ remoteName = config.Active
690+ }
685 if remoteName == "" {691 if remoteName == "" {
686 return fmt.Errorf("no active remote configured. Use 'gordon remote use <name>' or --remote flag")692 return fmt.Errorf("no active remote configured. Use 'gordon remote use <name>' or --remote flag")
687 }693 }
M internal/adapters/in/http/admin/handler.go +7 -0
@@ -1085,6 +1085,13 @@ func (h *Handler) handleAttachmentsConfigDelete(w http.ResponseWriter, r *http.R
10851085 func (h *Handler) handleAuthVerify(w http.ResponseWriter, r *http.Request) {
10861086 ctx := r.Context()
10871087
1088+ // Only allow GET method
1089+ if r.Method != http.MethodGet {
1090+ w.Header().Set("Allow", http.MethodGet)
1091+ h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
1092+ return
1093+ }
1094+
10881095 // Call use case to get auth status
10891096 status, err := h.authSvc.GetAuthStatus(ctx)
10901097 if err != nil {
@@ -1085,6 +1085,13 @@ func (h *Handler) handleAttachmentsConfigDelete(w http.ResponseWriter, r *http.R
1085 func (h *Handler) handleAuthVerify(w http.ResponseWriter, r *http.Request) {1085 func (h *Handler) handleAuthVerify(w http.ResponseWriter, r *http.Request) {
1086 ctx := r.Context()1086 ctx := r.Context()
1087 1087
1088+ // Only allow GET method
1089+ if r.Method != http.MethodGet {
1090+ w.Header().Set("Allow", http.MethodGet)
1091+ h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
1092+ return
1093+ }
1094+
1088 // Call use case to get auth status1095 // Call use case to get auth status
1089 status, err := h.authSvc.GetAuthStatus(ctx)1096 status, err := h.authSvc.GetAuthStatus(ctx)
1090 if err != nil {1097 if err != nil {
M internal/adapters/in/http/admin/handler_test.go +1 -1
@@ -23,7 +23,7 @@ func testLogger() zerowrap.Logger {
2323
2424 func ctxWithScopes(scopes ...string) context.Context {
2525 ctx := context.Background()
26- return context.WithValue(ctx, ContextKeyScopes, scopes)
26+ return context.WithValue(ctx, domain.ContextKeyScopes, scopes)
2727 }
2828
2929 // Routes endpoint tests
@@ -23,7 +23,7 @@ func testLogger() zerowrap.Logger {
23 23
24 func ctxWithScopes(scopes ...string) context.Context {24 func ctxWithScopes(scopes ...string) context.Context {
25 ctx := context.Background()25 ctx := context.Background()
26- return context.WithValue(ctx, ContextKeyScopes, scopes)26+ return context.WithValue(ctx, domain.ContextKeyScopes, scopes)
27 }27 }
28 28
29 // Routes endpoint tests29 // Routes endpoint tests
M internal/adapters/in/http/admin/middleware.go +6 -15
@@ -16,16 +16,6 @@ import (
1616 "github.com/bnema/gordon/internal/domain"
1717 )
1818
19-// contextKey is a custom type for context keys to avoid collisions.
20-type contextKey string
21-
22-const (
23- // ContextKeyScopes is the context key for storing token scopes.
24- ContextKeyScopes contextKey = "admin_scopes"
25- // ContextKeySubject is the context key for storing token subject.
26- ContextKeySubject contextKey = "admin_subject"
27-)
28-
2919 // AuthMiddleware creates middleware that validates admin API authentication.
3020 // Both globalLimiter and ipLimiter can be nil to disable rate limiting.
3121 // The trustedNets parameter is used for proper IP extraction behind reverse proxies.
@@ -110,8 +100,9 @@ func AuthMiddleware(
110100 }
111101
112102 // Add claims to context for downstream handlers
113- ctx = context.WithValue(ctx, ContextKeyScopes, claims.Scopes)
114- ctx = context.WithValue(ctx, ContextKeySubject, claims.Subject)
103+ ctx = context.WithValue(ctx, domain.ContextKeyScopes, claims.Scopes)
104+ ctx = context.WithValue(ctx, domain.ContextKeySubject, claims.Subject)
105+ ctx = context.WithValue(ctx, domain.TokenClaimsKey, claims)
115106
116107 next.ServeHTTP(w, r.WithContext(ctx))
117108 })
@@ -125,7 +116,7 @@ func RequireScope(resource, action string) func(http.Handler) http.Handler {
125116 ctx := r.Context()
126117
127118 // Get scopes from context
128- scopes, ok := ctx.Value(ContextKeyScopes).([]string)
119+ scopes, ok := ctx.Value(domain.ContextKeyScopes).([]string)
129120 if !ok {
130121 sendForbidden(w, "no scopes in context")
131122 return
@@ -144,13 +135,13 @@ func RequireScope(resource, action string) func(http.Handler) http.Handler {
144135
145136 // GetSubject retrieves the authenticated subject from the context.
146137 func GetSubject(ctx context.Context) string {
147- subject, _ := ctx.Value(ContextKeySubject).(string)
138+ subject, _ := ctx.Value(domain.ContextKeySubject).(string)
148139 return subject
149140 }
150141
151142 // GetScopes retrieves the token scopes from the context.
152143 func GetScopes(ctx context.Context) []string {
153- scopes, _ := ctx.Value(ContextKeyScopes).([]string)
144+ scopes, _ := ctx.Value(domain.ContextKeyScopes).([]string)
154145 return scopes
155146 }
156147
@@ -16,16 +16,6 @@ import (
16 "github.com/bnema/gordon/internal/domain"16 "github.com/bnema/gordon/internal/domain"
17 )17 )
18 18
19-// contextKey is a custom type for context keys to avoid collisions.
20-type contextKey string
21-
22-const (
23- // ContextKeyScopes is the context key for storing token scopes.
24- ContextKeyScopes contextKey = "admin_scopes"
25- // ContextKeySubject is the context key for storing token subject.
26- ContextKeySubject contextKey = "admin_subject"
27-)
28-
29 // AuthMiddleware creates middleware that validates admin API authentication.19 // AuthMiddleware creates middleware that validates admin API authentication.
30 // Both globalLimiter and ipLimiter can be nil to disable rate limiting.20 // Both globalLimiter and ipLimiter can be nil to disable rate limiting.
31 // The trustedNets parameter is used for proper IP extraction behind reverse proxies.21 // The trustedNets parameter is used for proper IP extraction behind reverse proxies.
@@ -110,8 +100,9 @@ func AuthMiddleware(
110 }100 }
111 101
112 // Add claims to context for downstream handlers102 // Add claims to context for downstream handlers
113- ctx = context.WithValue(ctx, ContextKeyScopes, claims.Scopes)103+ ctx = context.WithValue(ctx, domain.ContextKeyScopes, claims.Scopes)
114- ctx = context.WithValue(ctx, ContextKeySubject, claims.Subject)104+ ctx = context.WithValue(ctx, domain.ContextKeySubject, claims.Subject)
105+ ctx = context.WithValue(ctx, domain.TokenClaimsKey, claims)
115 106
116 next.ServeHTTP(w, r.WithContext(ctx))107 next.ServeHTTP(w, r.WithContext(ctx))
117 })108 })
@@ -125,7 +116,7 @@ func RequireScope(resource, action string) func(http.Handler) http.Handler {
125 ctx := r.Context()116 ctx := r.Context()
126 117
127 // Get scopes from context118 // Get scopes from context
128- scopes, ok := ctx.Value(ContextKeyScopes).([]string)119+ scopes, ok := ctx.Value(domain.ContextKeyScopes).([]string)
129 if !ok {120 if !ok {
130 sendForbidden(w, "no scopes in context")121 sendForbidden(w, "no scopes in context")
131 return122 return
@@ -144,13 +135,13 @@ func RequireScope(resource, action string) func(http.Handler) http.Handler {
144 135
145 // GetSubject retrieves the authenticated subject from the context.136 // GetSubject retrieves the authenticated subject from the context.
146 func GetSubject(ctx context.Context) string {137 func GetSubject(ctx context.Context) string {
147- subject, _ := ctx.Value(ContextKeySubject).(string)138+ subject, _ := ctx.Value(domain.ContextKeySubject).(string)
148 return subject139 return subject
149 }140 }
150 141
151 // GetScopes retrieves the token scopes from the context.142 // GetScopes retrieves the token scopes from the context.
152 func GetScopes(ctx context.Context) []string {143 func GetScopes(ctx context.Context) []string {
153- scopes, _ := ctx.Value(ContextKeyScopes).([]string)144+ scopes, _ := ctx.Value(domain.ContextKeyScopes).([]string)
154 return scopes145 return scopes
155 }146 }
156 147
M internal/adapters/in/http/admin/middleware_test.go +2 -2
@@ -340,7 +340,7 @@ func TestRequireScope(t *testing.T) {
340340
341341 req := httptest.NewRequest(http.MethodGet, "/admin/routes", nil)
342342 // Add scopes to context
343- ctx := context.WithValue(req.Context(), ContextKeyScopes, tt.scopes)
343+ ctx := context.WithValue(req.Context(), domain.ContextKeyScopes, tt.scopes)
344344 req = req.WithContext(ctx)
345345
346346 rec := httptest.NewRecorder()
@@ -352,7 +352,7 @@ func TestRequireScope(t *testing.T) {
352352 }
353353
354354 func TestHasAccess(t *testing.T) {
355- ctx := context.WithValue(context.Background(), ContextKeyScopes, []string{"admin:routes:read", "admin:secrets:*"})
355+ ctx := context.WithValue(context.Background(), domain.ContextKeyScopes, []string{"admin:routes:read", "admin:secrets:*"})
356356
357357 assert.True(t, HasAccess(ctx, "routes", "read"))
358358 assert.False(t, HasAccess(ctx, "routes", "write"))
@@ -340,7 +340,7 @@ func TestRequireScope(t *testing.T) {
340 340
341 req := httptest.NewRequest(http.MethodGet, "/admin/routes", nil)341 req := httptest.NewRequest(http.MethodGet, "/admin/routes", nil)
342 // Add scopes to context342 // Add scopes to context
343- ctx := context.WithValue(req.Context(), ContextKeyScopes, tt.scopes)343+ ctx := context.WithValue(req.Context(), domain.ContextKeyScopes, tt.scopes)
344 req = req.WithContext(ctx)344 req = req.WithContext(ctx)
345 345
346 rec := httptest.NewRecorder()346 rec := httptest.NewRecorder()
@@ -352,7 +352,7 @@ func TestRequireScope(t *testing.T) {
352 }352 }
353 353
354 func TestHasAccess(t *testing.T) {354 func TestHasAccess(t *testing.T) {
355- ctx := context.WithValue(context.Background(), ContextKeyScopes, []string{"admin:routes:read", "admin:secrets:*"})355+ ctx := context.WithValue(context.Background(), domain.ContextKeyScopes, []string{"admin:routes:read", "admin:secrets:*"})
356 356
357 assert.True(t, HasAccess(ctx, "routes", "read"))357 assert.True(t, HasAccess(ctx, "routes", "read"))
358 assert.False(t, HasAccess(ctx, "routes", "write"))358 assert.False(t, HasAccess(ctx, "routes", "write"))
M internal/adapters/in/http/middleware/auth.go +3 -5
@@ -14,11 +14,9 @@ import (
1414 "github.com/bnema/gordon/internal/domain"
1515 )
1616
17-// contextKey is a type for context keys used by this package.
18-type contextKey string
19-
20-// TokenClaimsKey is the context key for storing token claims.
21-const TokenClaimsKey contextKey = "tokenClaims"
17+// TokenClaimsKey is context key for storing token claims.
18+// Using domain key for consistency across all auth flows.
19+const TokenClaimsKey = domain.TokenClaimsKey
2220
2321 // RegistryAuth middleware provides Docker Registry authentication.
2422 func RegistryAuth(username, password string, log zerowrap.Logger) func(http.Handler) http.Handler {
@@ -14,11 +14,9 @@ import (
14 "github.com/bnema/gordon/internal/domain"14 "github.com/bnema/gordon/internal/domain"
15 )15 )
16 16
17-// contextKey is a type for context keys used by this package.17+// TokenClaimsKey is context key for storing token claims.
18-type contextKey string18+// Using domain key for consistency across all auth flows.
19-19+const TokenClaimsKey = domain.TokenClaimsKey
20-// TokenClaimsKey is the context key for storing token claims.
21-const TokenClaimsKey contextKey = "tokenClaims"
22 20
23 // RegistryAuth middleware provides Docker Registry authentication.21 // RegistryAuth middleware provides Docker Registry authentication.
24 func RegistryAuth(username, password string, log zerowrap.Logger) func(http.Handler) http.Handler {22 func RegistryAuth(username, password string, log zerowrap.Logger) func(http.Handler) http.Handler {
M internal/domain/auth.go +24 -0
@@ -2,6 +2,7 @@
22 package domain
33
44 import (
5+ "context"
56 "fmt"
67 "strings"
78 "time"
@@ -273,3 +274,26 @@ type AuthStatus struct {
273274 ExpiresAt int64 // Unix timestamp, 0 if never expires
274275 IssuedAt int64 // Unix timestamp
275276 }
277+
278+// Context keys for auth-related domain concerns.
279+type authContextKey string
280+
281+const (
282+ // TokenClaimsKey is the context key for storing TokenClaims.
283+ // Allows use cases and adapters to access validated token claims from context.
284+ TokenClaimsKey authContextKey = "token_claims"
285+ // ContextKeyScopes is the context key for storing token scopes.
286+ ContextKeyScopes authContextKey = "token_scopes"
287+ // ContextKeySubject is the context key for storing token subject.
288+ ContextKeySubject authContextKey = "token_subject"
289+)
290+
291+// GetTokenClaims extracts TokenClaims from context.
292+// Returns nil if claims are not present or type assertion fails.
293+func GetTokenClaims(ctx context.Context) *TokenClaims {
294+ claims, ok := ctx.Value(TokenClaimsKey).(*TokenClaims)
295+ if !ok {
296+ return nil
297+ }
298+ return claims
299+}
@@ -2,6 +2,7 @@
2 package domain2 package domain
3 3
4 import (4 import (
5+ "context"
5 "fmt"6 "fmt"
6 "strings"7 "strings"
7 "time"8 "time"
@@ -273,3 +274,26 @@ type AuthStatus struct {
273 ExpiresAt int64 // Unix timestamp, 0 if never expires274 ExpiresAt int64 // Unix timestamp, 0 if never expires
274 IssuedAt int64 // Unix timestamp275 IssuedAt int64 // Unix timestamp
275 }276 }
277+
278+// Context keys for auth-related domain concerns.
279+type authContextKey string
280+
281+const (
282+ // TokenClaimsKey is the context key for storing TokenClaims.
283+ // Allows use cases and adapters to access validated token claims from context.
284+ TokenClaimsKey authContextKey = "token_claims"
285+ // ContextKeyScopes is the context key for storing token scopes.
286+ ContextKeyScopes authContextKey = "token_scopes"
287+ // ContextKeySubject is the context key for storing token subject.
288+ ContextKeySubject authContextKey = "token_subject"
289+)
290+
291+// GetTokenClaims extracts TokenClaims from context.
292+// Returns nil if claims are not present or type assertion fails.
293+func GetTokenClaims(ctx context.Context) *TokenClaims {
294+ claims, ok := ctx.Value(TokenClaimsKey).(*TokenClaims)
295+ if !ok {
296+ return nil
297+ }
298+ return claims
299+}
M internal/usecase/auth/service.go +2 -3
@@ -13,7 +13,6 @@ import (
1313 "github.com/google/uuid"
1414 "golang.org/x/crypto/bcrypt"
1515
16- "github.com/bnema/gordon/internal/adapters/in/http/middleware"
1716 "github.com/bnema/gordon/internal/boundaries/out"
1817 "github.com/bnema/gordon/internal/domain"
1918 )
@@ -465,8 +464,8 @@ func (s *Service) GetAuthStatus(ctx context.Context) (*domain.AuthStatus, error)
465464 }
466465
467466 // Extract claims from context (set by AdminAuth middleware)
468- claims, ok := ctx.Value(middleware.TokenClaimsKey).(*domain.TokenClaims)
469- if !ok || claims == nil {
467+ claims := domain.GetTokenClaims(ctx)
468+ if claims == nil {
470469 return &domain.AuthStatus{Valid: false}, nil
471470 }
472471
@@ -13,7 +13,6 @@ import (
13 "github.com/google/uuid"13 "github.com/google/uuid"
14 "golang.org/x/crypto/bcrypt"14 "golang.org/x/crypto/bcrypt"
15 15
16- "github.com/bnema/gordon/internal/adapters/in/http/middleware"
17 "github.com/bnema/gordon/internal/boundaries/out"16 "github.com/bnema/gordon/internal/boundaries/out"
18 "github.com/bnema/gordon/internal/domain"17 "github.com/bnema/gordon/internal/domain"
19 )18 )
@@ -465,8 +464,8 @@ func (s *Service) GetAuthStatus(ctx context.Context) (*domain.AuthStatus, error)
465 }464 }
466 465
467 // Extract claims from context (set by AdminAuth middleware)466 // Extract claims from context (set by AdminAuth middleware)
468- claims, ok := ctx.Value(middleware.TokenClaimsKey).(*domain.TokenClaims)467+ claims := domain.GetTokenClaims(ctx)
469- if !ok || claims == nil {468+ if claims == nil {
470 return &domain.AuthStatus{Valid: false}, nil469 return &domain.AuthStatus{Valid: false}, nil
471 }470 }
472 471

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close