detached 4a71618

refactor(config): infer auth type from config instead of explicit type field

commit
4a7161851c6a1b531dc7c6c867ca230f7a8a0dad
author
bnema · · 2026-01-18 03:16 UTC
parent
3dc5982
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +55 -37
M internal/app/run.go +55 -37
@@ -7,6 +7,7 @@ import (
77 "encoding/hex"
88 "encoding/json"
99 "fmt"
10+ "net"
1011 "net/http"
1112 "os"
1213 "os/signal"
@@ -31,10 +32,9 @@ import (
3132 "gordon/internal/adapters/out/secrets"
3233 "gordon/internal/adapters/out/tokenstore"
3334
34- "golang.org/x/time/rate"
35-
3635 // Adapters - Input
3736 "gordon/internal/adapters/in/http/admin"
37+ authhandler "gordon/internal/adapters/in/http/auth"
3838 "gordon/internal/adapters/in/http/middleware"
3939 "gordon/internal/adapters/in/http/registry"
4040
@@ -127,7 +127,7 @@ type services struct {
127127 registrySvc *registrySvc.Service
128128 proxySvc *proxy.Service
129129 authSvc *auth.Service
130- tokenHandler *registry.TokenHandler
130+ authHandler *authhandler.Handler
131131 adminHandler *admin.Handler
132132 internalRegUser string
133133 internalRegPass string
@@ -301,11 +301,11 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra
301301
302302 // Create token handler for registry token endpoint
303303 if svc.authSvc != nil {
304- internalAuth := registry.InternalAuth{
304+ internalAuth := authhandler.InternalAuth{
305305 Username: svc.internalRegUser,
306306 Password: svc.internalRegPass,
307307 }
308- svc.tokenHandler = registry.NewTokenHandler(svc.authSvc, internalAuth, log)
308+ svc.authHandler = authhandler.NewHandler(svc.authSvc, internalAuth, log)
309309 }
310310
311311 // Determine env directory for admin API
@@ -568,11 +568,11 @@ func createAuthService(ctx context.Context, cfg Config, log zerowrap.Logger) (ou
568568 return nil, nil, nil
569569 }
570570
571- authType := resolveAuthType(cfg.Auth.Type)
571+ authType := resolveAuthType(cfg)
572572 backend := resolveSecretsBackend(cfg.Auth.SecretsBackend)
573573 dataDir := resolveDataDir(cfg.Server.DataDir)
574574
575- store, err := createTokenStore(authType, backend, dataDir, log)
575+ store, err := createTokenStore(backend, dataDir, log)
576576 if err != nil {
577577 return nil, nil, err
578578 }
@@ -592,11 +592,26 @@ func createAuthService(ctx context.Context, cfg Config, log zerowrap.Logger) (ou
592592 return store, authSvc, nil
593593 }
594594
595-func resolveAuthType(authType string) domain.AuthType {
596- if authType == "token" {
595+// resolveAuthType determines the auth type from config.
596+// If password_hash is configured, password auth is available (plus tokens).
597+// If only token_secret is configured, token-only mode.
598+// The explicit "type" field is deprecated but still respected for backwards compat.
599+func resolveAuthType(cfg Config) domain.AuthType {
600+ // Explicit type takes precedence (backwards compatibility)
601+ if cfg.Auth.Type == "token" {
597602 return domain.AuthTypeToken
598603 }
599- return domain.AuthTypePassword
604+ if cfg.Auth.Type == "password" {
605+ return domain.AuthTypePassword
606+ }
607+
608+ // Infer from config: password auth if password_hash is configured
609+ if cfg.Auth.PasswordHash != "" || cfg.Auth.Password != "" {
610+ return domain.AuthTypePassword
611+ }
612+
613+ // Default to token-only
614+ return domain.AuthTypeToken
600615 }
601616
602617 func resolveSecretsBackend(backend string) domain.SecretsBackend {
@@ -619,11 +634,8 @@ func resolveDataDir(dataDir string) string {
619634 return dataDir
620635 }
621636
622-func createTokenStore(authType domain.AuthType, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) (out.TokenStore, error) {
623- if authType != domain.AuthTypeToken {
624- return nil, nil
625- }
626-
637+func createTokenStore(backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) (out.TokenStore, error) {
638+ // Token store is always created since tokens work in both auth modes
627639 store, err := tokenstore.NewStore(backend, dataDir, log)
628640 if err != nil {
629641 return nil, log.WrapErr(err, "failed to create token store")
@@ -638,8 +650,16 @@ func buildAuthConfig(ctx context.Context, cfg Config, authType domain.AuthType,
638650 Username: cfg.Auth.Username,
639651 }
640652
641- switch authType {
642- case domain.AuthTypePassword:
653+ // Token config is always required (tokens work in all auth modes)
654+ secret, expiry, err := loadTokenConfig(ctx, cfg, backend, dataDir, log)
655+ if err != nil {
656+ return auth.Config{}, err
657+ }
658+ authConfig.TokenSecret = secret
659+ authConfig.TokenExpiry = expiry
660+
661+ // Password config only needed for password auth mode
662+ if authType == domain.AuthTypePassword {
643663 hash, err := loadPasswordHash(ctx, cfg, backend, dataDir, log)
644664 if err != nil {
645665 return auth.Config{}, err
@@ -648,13 +668,6 @@ func buildAuthConfig(ctx context.Context, cfg Config, authType domain.AuthType,
648668 return auth.Config{}, errAuthNotConfigured()
649669 }
650670 authConfig.PasswordHash = hash
651- case domain.AuthTypeToken:
652- secret, expiry, err := loadTokenConfig(ctx, cfg, backend, dataDir, log)
653- if err != nil {
654- return auth.Config{}, err
655- }
656- authConfig.TokenSecret = secret
657- authConfig.TokenExpiry = expiry
658671 }
659672
660673 return authConfig, nil
@@ -714,7 +727,7 @@ func loadTokenSecret(ctx context.Context, cfg Config, backend domain.SecretsBack
714727
715728 // Fall back to config-specified path via secrets backend
716729 if cfg.Auth.TokenSecret == "" {
717- return nil, fmt.Errorf("token_secret is required for token authentication; set %s environment variable or configure auth.token_secret", TokenSecretEnvVar)
730+ return nil, fmt.Errorf("token_secret is required for JWT token generation; set %s environment variable or configure auth.token_secret", TokenSecretEnvVar)
718731 }
719732
720733 secret, err := loadSecret(ctx, backend, cfg.Auth.TokenSecret, dataDir, log)
@@ -902,19 +915,21 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha
902915
903916 registryWithMiddleware := middleware.Chain(registryMiddlewares...)(registryHandler)
904917
905- // Create a mux that routes /v2/token to the token handler (no auth required)
906- // and all other /v2/* routes to the registry handler (with auth)
918+ // Create a mux that routes:
919+ // - /auth/* to auth handler (no auth required, for password login and token exchange)
920+ // - /v2/* to registry handler (with auth)
921+ // - /admin/* to admin handler (with auth)
907922 registryMux := http.NewServeMux()
908- if svc.tokenHandler != nil {
909- // Token endpoint is NOT protected by auth - it's where clients get tokens
910- // but it still needs rate limiting to prevent brute force attacks
911- tokenWithMiddleware := middleware.Chain(
923+ if svc.authHandler != nil {
924+ // Auth endpoints are NOT protected by auth - they're where clients authenticate
925+ // but still need rate limiting to prevent brute force attacks
926+ authWithMiddleware := middleware.Chain(
912927 middleware.PanicRecovery(log),
913928 middleware.RequestLogger(log),
914929 middleware.SecurityHeaders,
915930 rateLimitMiddleware,
916- )(svc.tokenHandler)
917- registryMux.Handle("/v2/token", tokenWithMiddleware)
931+ )(svc.authHandler)
932+ registryMux.Handle("/auth/", authWithMiddleware)
918933 }
919934 registryMux.Handle("/v2/", registryWithMiddleware)
920935
@@ -927,12 +942,15 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha
927942 }
928943 // Add admin auth middleware if auth is enabled
929944 if svc.authSvc != nil {
930- // Rate limiter for admin API uses per-IP rate from config
931- var adminLimiter *rate.Limiter
945+ // Create rate limiters for admin API - uses same config as registry
946+ var globalLimiter, ipLimiter out.RateLimiter
947+ var trustedNets []*net.IPNet
932948 if cfg.API.RateLimit.Enabled {
933- adminLimiter = rate.NewLimiter(rate.Limit(cfg.API.RateLimit.PerIPRPS), cfg.API.RateLimit.Burst)
949+ globalLimiter = ratelimit.NewMemoryStore(cfg.API.RateLimit.GlobalRPS, cfg.API.RateLimit.Burst, log)
950+ ipLimiter = ratelimit.NewMemoryStore(cfg.API.RateLimit.PerIPRPS, cfg.API.RateLimit.Burst, log)
951+ trustedNets = middleware.ParseTrustedProxies(cfg.API.RateLimit.TrustedProxies)
934952 }
935- adminMiddlewares = append(adminMiddlewares, admin.AuthMiddleware(svc.authSvc, adminLimiter, log))
953+ adminMiddlewares = append(adminMiddlewares, admin.AuthMiddleware(svc.authSvc, globalLimiter, ipLimiter, trustedNets, log))
936954 }
937955 adminWithMiddleware := middleware.Chain(adminMiddlewares...)(svc.adminHandler)
938956 registryMux.Handle("/admin/", adminWithMiddleware)
@@ -7,6 +7,7 @@ import (
7 "encoding/hex"7 "encoding/hex"
8 "encoding/json"8 "encoding/json"
9 "fmt"9 "fmt"
10+ "net"
10 "net/http"11 "net/http"
11 "os"12 "os"
12 "os/signal"13 "os/signal"
@@ -31,10 +32,9 @@ import (
31 "gordon/internal/adapters/out/secrets"32 "gordon/internal/adapters/out/secrets"
32 "gordon/internal/adapters/out/tokenstore"33 "gordon/internal/adapters/out/tokenstore"
33 34
34- "golang.org/x/time/rate"
35-
36 // Adapters - Input35 // Adapters - Input
37 "gordon/internal/adapters/in/http/admin"36 "gordon/internal/adapters/in/http/admin"
37+ authhandler "gordon/internal/adapters/in/http/auth"
38 "gordon/internal/adapters/in/http/middleware"38 "gordon/internal/adapters/in/http/middleware"
39 "gordon/internal/adapters/in/http/registry"39 "gordon/internal/adapters/in/http/registry"
40 40
@@ -127,7 +127,7 @@ type services struct {
127 registrySvc *registrySvc.Service127 registrySvc *registrySvc.Service
128 proxySvc *proxy.Service128 proxySvc *proxy.Service
129 authSvc *auth.Service129 authSvc *auth.Service
130- tokenHandler *registry.TokenHandler130+ authHandler *authhandler.Handler
131 adminHandler *admin.Handler131 adminHandler *admin.Handler
132 internalRegUser string132 internalRegUser string
133 internalRegPass string133 internalRegPass string
@@ -301,11 +301,11 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra
301 301
302 // Create token handler for registry token endpoint302 // Create token handler for registry token endpoint
303 if svc.authSvc != nil {303 if svc.authSvc != nil {
304- internalAuth := registry.InternalAuth{304+ internalAuth := authhandler.InternalAuth{
305 Username: svc.internalRegUser,305 Username: svc.internalRegUser,
306 Password: svc.internalRegPass,306 Password: svc.internalRegPass,
307 }307 }
308- svc.tokenHandler = registry.NewTokenHandler(svc.authSvc, internalAuth, log)308+ svc.authHandler = authhandler.NewHandler(svc.authSvc, internalAuth, log)
309 }309 }
310 310
311 // Determine env directory for admin API311 // Determine env directory for admin API
@@ -568,11 +568,11 @@ func createAuthService(ctx context.Context, cfg Config, log zerowrap.Logger) (ou
568 return nil, nil, nil568 return nil, nil, nil
569 }569 }
570 570
571- authType := resolveAuthType(cfg.Auth.Type)571+ authType := resolveAuthType(cfg)
572 backend := resolveSecretsBackend(cfg.Auth.SecretsBackend)572 backend := resolveSecretsBackend(cfg.Auth.SecretsBackend)
573 dataDir := resolveDataDir(cfg.Server.DataDir)573 dataDir := resolveDataDir(cfg.Server.DataDir)
574 574
575- store, err := createTokenStore(authType, backend, dataDir, log)575+ store, err := createTokenStore(backend, dataDir, log)
576 if err != nil {576 if err != nil {
577 return nil, nil, err577 return nil, nil, err
578 }578 }
@@ -592,11 +592,26 @@ func createAuthService(ctx context.Context, cfg Config, log zerowrap.Logger) (ou
592 return store, authSvc, nil592 return store, authSvc, nil
593 }593 }
594 594
595-func resolveAuthType(authType string) domain.AuthType {595+// resolveAuthType determines the auth type from config.
596- if authType == "token" {596+// If password_hash is configured, password auth is available (plus tokens).
597+// If only token_secret is configured, token-only mode.
598+// The explicit "type" field is deprecated but still respected for backwards compat.
599+func resolveAuthType(cfg Config) domain.AuthType {
600+ // Explicit type takes precedence (backwards compatibility)
601+ if cfg.Auth.Type == "token" {
597 return domain.AuthTypeToken602 return domain.AuthTypeToken
598 }603 }
599- return domain.AuthTypePassword604+ if cfg.Auth.Type == "password" {
605+ return domain.AuthTypePassword
606+ }
607+
608+ // Infer from config: password auth if password_hash is configured
609+ if cfg.Auth.PasswordHash != "" || cfg.Auth.Password != "" {
610+ return domain.AuthTypePassword
611+ }
612+
613+ // Default to token-only
614+ return domain.AuthTypeToken
600 }615 }
601 616
602 func resolveSecretsBackend(backend string) domain.SecretsBackend {617 func resolveSecretsBackend(backend string) domain.SecretsBackend {
@@ -619,11 +634,8 @@ func resolveDataDir(dataDir string) string {
619 return dataDir634 return dataDir
620 }635 }
621 636
622-func createTokenStore(authType domain.AuthType, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) (out.TokenStore, error) {637+func createTokenStore(backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) (out.TokenStore, error) {
623- if authType != domain.AuthTypeToken {638+ // Token store is always created since tokens work in both auth modes
624- return nil, nil
625- }
626-
627 store, err := tokenstore.NewStore(backend, dataDir, log)639 store, err := tokenstore.NewStore(backend, dataDir, log)
628 if err != nil {640 if err != nil {
629 return nil, log.WrapErr(err, "failed to create token store")641 return nil, log.WrapErr(err, "failed to create token store")
@@ -638,8 +650,16 @@ func buildAuthConfig(ctx context.Context, cfg Config, authType domain.AuthType,
638 Username: cfg.Auth.Username,650 Username: cfg.Auth.Username,
639 }651 }
640 652
641- switch authType {653+ // Token config is always required (tokens work in all auth modes)
642- case domain.AuthTypePassword:654+ secret, expiry, err := loadTokenConfig(ctx, cfg, backend, dataDir, log)
655+ if err != nil {
656+ return auth.Config{}, err
657+ }
658+ authConfig.TokenSecret = secret
659+ authConfig.TokenExpiry = expiry
660+
661+ // Password config only needed for password auth mode
662+ if authType == domain.AuthTypePassword {
643 hash, err := loadPasswordHash(ctx, cfg, backend, dataDir, log)663 hash, err := loadPasswordHash(ctx, cfg, backend, dataDir, log)
644 if err != nil {664 if err != nil {
645 return auth.Config{}, err665 return auth.Config{}, err
@@ -648,13 +668,6 @@ func buildAuthConfig(ctx context.Context, cfg Config, authType domain.AuthType,
648 return auth.Config{}, errAuthNotConfigured()668 return auth.Config{}, errAuthNotConfigured()
649 }669 }
650 authConfig.PasswordHash = hash670 authConfig.PasswordHash = hash
651- case domain.AuthTypeToken:
652- secret, expiry, err := loadTokenConfig(ctx, cfg, backend, dataDir, log)
653- if err != nil {
654- return auth.Config{}, err
655- }
656- authConfig.TokenSecret = secret
657- authConfig.TokenExpiry = expiry
658 }671 }
659 672
660 return authConfig, nil673 return authConfig, nil
@@ -714,7 +727,7 @@ func loadTokenSecret(ctx context.Context, cfg Config, backend domain.SecretsBack
714 727
715 // Fall back to config-specified path via secrets backend728 // Fall back to config-specified path via secrets backend
716 if cfg.Auth.TokenSecret == "" {729 if cfg.Auth.TokenSecret == "" {
717- return nil, fmt.Errorf("token_secret is required for token authentication; set %s environment variable or configure auth.token_secret", TokenSecretEnvVar)730+ return nil, fmt.Errorf("token_secret is required for JWT token generation; set %s environment variable or configure auth.token_secret", TokenSecretEnvVar)
718 }731 }
719 732
720 secret, err := loadSecret(ctx, backend, cfg.Auth.TokenSecret, dataDir, log)733 secret, err := loadSecret(ctx, backend, cfg.Auth.TokenSecret, dataDir, log)
@@ -902,19 +915,21 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha
902 915
903 registryWithMiddleware := middleware.Chain(registryMiddlewares...)(registryHandler)916 registryWithMiddleware := middleware.Chain(registryMiddlewares...)(registryHandler)
904 917
905- // Create a mux that routes /v2/token to the token handler (no auth required)918+ // Create a mux that routes:
906- // and all other /v2/* routes to the registry handler (with auth)919+ // - /auth/* to auth handler (no auth required, for password login and token exchange)
920+ // - /v2/* to registry handler (with auth)
921+ // - /admin/* to admin handler (with auth)
907 registryMux := http.NewServeMux()922 registryMux := http.NewServeMux()
908- if svc.tokenHandler != nil {923+ if svc.authHandler != nil {
909- // Token endpoint is NOT protected by auth - it's where clients get tokens924+ // Auth endpoints are NOT protected by auth - they're where clients authenticate
910- // but it still needs rate limiting to prevent brute force attacks925+ // but still need rate limiting to prevent brute force attacks
911- tokenWithMiddleware := middleware.Chain(926+ authWithMiddleware := middleware.Chain(
912 middleware.PanicRecovery(log),927 middleware.PanicRecovery(log),
913 middleware.RequestLogger(log),928 middleware.RequestLogger(log),
914 middleware.SecurityHeaders,929 middleware.SecurityHeaders,
915 rateLimitMiddleware,930 rateLimitMiddleware,
916- )(svc.tokenHandler)931+ )(svc.authHandler)
917- registryMux.Handle("/v2/token", tokenWithMiddleware)932+ registryMux.Handle("/auth/", authWithMiddleware)
918 }933 }
919 registryMux.Handle("/v2/", registryWithMiddleware)934 registryMux.Handle("/v2/", registryWithMiddleware)
920 935
@@ -927,12 +942,15 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha
927 }942 }
928 // Add admin auth middleware if auth is enabled943 // Add admin auth middleware if auth is enabled
929 if svc.authSvc != nil {944 if svc.authSvc != nil {
930- // Rate limiter for admin API uses per-IP rate from config945+ // Create rate limiters for admin API - uses same config as registry
931- var adminLimiter *rate.Limiter946+ var globalLimiter, ipLimiter out.RateLimiter
947+ var trustedNets []*net.IPNet
932 if cfg.API.RateLimit.Enabled {948 if cfg.API.RateLimit.Enabled {
933- adminLimiter = rate.NewLimiter(rate.Limit(cfg.API.RateLimit.PerIPRPS), cfg.API.RateLimit.Burst)949+ globalLimiter = ratelimit.NewMemoryStore(cfg.API.RateLimit.GlobalRPS, cfg.API.RateLimit.Burst, log)
950+ ipLimiter = ratelimit.NewMemoryStore(cfg.API.RateLimit.PerIPRPS, cfg.API.RateLimit.Burst, log)
951+ trustedNets = middleware.ParseTrustedProxies(cfg.API.RateLimit.TrustedProxies)
934 }952 }
935- adminMiddlewares = append(adminMiddlewares, admin.AuthMiddleware(svc.authSvc, adminLimiter, log))953+ adminMiddlewares = append(adminMiddlewares, admin.AuthMiddleware(svc.authSvc, globalLimiter, ipLimiter, trustedNets, log))
936 }954 }
937 adminWithMiddleware := middleware.Chain(adminMiddlewares...)(svc.adminHandler)955 adminWithMiddleware := middleware.Chain(adminMiddlewares...)(svc.adminHandler)
938 registryMux.Handle("/admin/", adminWithMiddleware)956 registryMux.Handle("/admin/", adminWithMiddleware)

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