detached 803a99b

chore(lint): reduce cyclomatic complexity

commit
803a99b40ea1e8a6239afa81d93ff10002d1be78
author
bnema · · 2026-01-13 20:52 UTC
parent
9424b2e
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +237 -122
M internal/adapters/in/cli/auth.go +1 -1
@@ -272,7 +272,7 @@ func runPasswordHash() error {
272272 fmt.Print("Enter password: ")
273273
274274 // Read password without echo
275- passwordBytes, err := term.ReadPassword(int(syscall.Stdin))
275+ passwordBytes, err := term.ReadPassword(syscall.Stdin)
276276 if err != nil {
277277 // Fallback for non-terminal input
278278 reader := bufio.NewReader(os.Stdin)
@@ -272,7 +272,7 @@ func runPasswordHash() error {
272 fmt.Print("Enter password: ")272 fmt.Print("Enter password: ")
273 273
274 // Read password without echo274 // Read password without echo
275- passwordBytes, err := term.ReadPassword(int(syscall.Stdin))275+ passwordBytes, err := term.ReadPassword(syscall.Stdin)
276 if err != nil {276 if err != nil {
277 // Fallback for non-terminal input277 // Fallback for non-terminal input
278 reader := bufio.NewReader(os.Stdin)278 reader := bufio.NewReader(os.Stdin)
M internal/adapters/out/tokenstore/pass.go +2 -0
@@ -228,12 +228,14 @@ func (s *PassStore) DeleteToken(ctx context.Context, subject string) error {
228228 metaPath := fmt.Sprintf("%s/%s.meta", passTokenPath, subject)
229229
230230 // Remove JWT
231+ // #nosec G204 -- command and args are fixed; subject is treated as data only.
231232 cmd := exec.CommandContext(ctx, "pass", "rm", "-f", jwtPath)
232233 if err := cmd.Run(); err != nil {
233234 return fmt.Errorf("failed to delete token: %w", err)
234235 }
235236
236237 // Remove metadata
238+ // #nosec G204 -- command and args are fixed; subject is treated as data only.
237239 cmd = exec.CommandContext(ctx, "pass", "rm", "-f", metaPath)
238240 if err := cmd.Run(); err != nil {
239241 s.log.Warn().Err(err).Msg("failed to delete token metadata")
@@ -228,12 +228,14 @@ func (s *PassStore) DeleteToken(ctx context.Context, subject string) error {
228 metaPath := fmt.Sprintf("%s/%s.meta", passTokenPath, subject)228 metaPath := fmt.Sprintf("%s/%s.meta", passTokenPath, subject)
229 229
230 // Remove JWT230 // Remove JWT
231+ // #nosec G204 -- command and args are fixed; subject is treated as data only.
231 cmd := exec.CommandContext(ctx, "pass", "rm", "-f", jwtPath)232 cmd := exec.CommandContext(ctx, "pass", "rm", "-f", jwtPath)
232 if err := cmd.Run(); err != nil {233 if err := cmd.Run(); err != nil {
233 return fmt.Errorf("failed to delete token: %w", err)234 return fmt.Errorf("failed to delete token: %w", err)
234 }235 }
235 236
236 // Remove metadata237 // Remove metadata
238+ // #nosec G204 -- command and args are fixed; subject is treated as data only.
237 cmd = exec.CommandContext(ctx, "pass", "rm", "-f", metaPath)239 cmd = exec.CommandContext(ctx, "pass", "rm", "-f", metaPath)
238 if err := cmd.Run(); err != nil {240 if err := cmd.Run(); err != nil {
239 s.log.Warn().Err(err).Msg("failed to delete token metadata")241 s.log.Warn().Err(err).Msg("failed to delete token metadata")
M internal/app/run.go +118 -60
@@ -395,92 +395,150 @@ func createAuthService(ctx context.Context, cfg Config, log zerowrap.Logger) (ou
395395 return nil, nil, nil
396396 }
397397
398- // Determine auth type
399- authType := domain.AuthTypePassword
400- if cfg.RegistryAuth.Type == "token" {
401- authType = domain.AuthTypeToken
398+ authType := resolveAuthType(cfg.RegistryAuth.Type)
399+ backend := resolveSecretsBackend(cfg.Secrets.Backend)
400+ dataDir := resolveDataDir(cfg.Server.DataDir)
401+
402+ store, err := createTokenStore(authType, backend, dataDir, log)
403+ if err != nil {
404+ return nil, nil, err
405+ }
406+
407+ authConfig, err := buildAuthConfig(ctx, cfg, authType, backend, dataDir, log)
408+ if err != nil {
409+ return nil, nil, err
410+ }
411+
412+ authSvc := auth.NewService(authConfig, store, log)
413+
414+ log.Info().
415+ Str("type", string(authType)).
416+ Str("backend", string(backend)).
417+ Msg("registry authentication enabled")
418+
419+ return store, authSvc, nil
420+}
421+
422+func resolveAuthType(authType string) domain.AuthType {
423+ if authType == "token" {
424+ return domain.AuthTypeToken
402425 }
426+ return domain.AuthTypePassword
427+}
403428
404- // Determine secrets backend
405- backend := domain.SecretsBackendUnsafe // default
406- switch cfg.Secrets.Backend {
429+func resolveSecretsBackend(backend string) domain.SecretsBackend {
430+ switch backend {
407431 case "pass":
408- backend = domain.SecretsBackendPass
432+ return domain.SecretsBackendPass
409433 case "sops":
410- backend = domain.SecretsBackendSops
434+ return domain.SecretsBackendSops
411435 case "unsafe", "":
412- backend = domain.SecretsBackendUnsafe
436+ return domain.SecretsBackendUnsafe
437+ default:
438+ return domain.SecretsBackendUnsafe
413439 }
440+}
414441
415- // Get data directory
416- dataDir := cfg.Server.DataDir
442+func resolveDataDir(dataDir string) string {
417443 if dataDir == "" {
418- dataDir = DefaultDataDir()
444+ return DefaultDataDir()
419445 }
446+ return dataDir
447+}
420448
421- // Create token store (needed for token auth or to store revocations)
422- var store out.TokenStore
423- var err error
424- if authType == domain.AuthTypeToken {
425- store, err = tokenstore.NewStore(backend, dataDir, log)
426- if err != nil {
427- return nil, nil, log.WrapErr(err, "failed to create token store")
428- }
449+func createTokenStore(authType domain.AuthType, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) (out.TokenStore, error) {
450+ if authType != domain.AuthTypeToken {
451+ return nil, nil
452+ }
453+
454+ store, err := tokenstore.NewStore(backend, dataDir, log)
455+ if err != nil {
456+ return nil, log.WrapErr(err, "failed to create token store")
429457 }
458+ return store, nil
459+}
430460
431- // Build auth config
461+func buildAuthConfig(ctx context.Context, cfg Config, authType domain.AuthType, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) (auth.Config, error) {
432462 authConfig := auth.Config{
433463 Enabled: cfg.RegistryAuth.Enabled,
434464 AuthType: authType,
435465 Username: cfg.RegistryAuth.Username,
436466 }
437467
438- // Load secrets based on auth type
439- if authType == domain.AuthTypePassword {
440- // For password auth, we need the bcrypt hash
441- if cfg.RegistryAuth.PasswordHash != "" {
442- // Load from secrets backend
443- hash, err := loadSecret(ctx, backend, cfg.RegistryAuth.PasswordHash, dataDir, log)
444- if err != nil {
445- return nil, nil, log.WrapErr(err, "failed to load password hash")
446- }
447- authConfig.PasswordHash = hash
448- } else if cfg.RegistryAuth.Password != "" {
449- // Backward compatibility: use plain password (will be hashed on validation)
450- // This is less secure but maintains backward compatibility
451- log.Warn().Msg("using plain password in config is deprecated, use password_hash with a secrets backend")
452- authConfig.PasswordHash = cfg.RegistryAuth.Password
468+ switch authType {
469+ case domain.AuthTypePassword:
470+ hash, err := loadPasswordHash(ctx, cfg, backend, dataDir, log)
471+ if err != nil {
472+ return auth.Config{}, err
453473 }
454- } else {
455- // For token auth, we need the signing secret
456- if cfg.RegistryAuth.TokenSecret != "" {
457- secret, err := loadSecret(ctx, backend, cfg.RegistryAuth.TokenSecret, dataDir, log)
458- if err != nil {
459- return nil, nil, log.WrapErr(err, "failed to load token secret")
460- }
461- authConfig.TokenSecret = []byte(secret)
462- } else {
463- return nil, nil, fmt.Errorf("token_secret is required for token authentication")
474+ authConfig.PasswordHash = hash
475+ case domain.AuthTypeToken:
476+ secret, expiry, err := loadTokenConfig(ctx, cfg, backend, dataDir, log)
477+ if err != nil {
478+ return auth.Config{}, err
464479 }
480+ authConfig.TokenSecret = secret
481+ authConfig.TokenExpiry = expiry
482+ }
465483
466- // Parse token expiry
467- if cfg.RegistryAuth.TokenExpiry != "" {
468- expiry, err := time.ParseDuration(cfg.RegistryAuth.TokenExpiry)
469- if err != nil {
470- return nil, nil, fmt.Errorf("invalid token_expiry: %w", err)
471- }
472- authConfig.TokenExpiry = expiry
484+ return authConfig, nil
485+}
486+
487+func loadPasswordHash(ctx context.Context, cfg Config, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) (string, error) {
488+ if cfg.RegistryAuth.PasswordHash != "" {
489+ hash, err := loadSecret(ctx, backend, cfg.RegistryAuth.PasswordHash, dataDir, log)
490+ if err != nil {
491+ return "", log.WrapErr(err, "failed to load password hash")
473492 }
493+ return hash, nil
474494 }
475495
476- authSvc := auth.NewService(authConfig, store, log)
496+ if cfg.RegistryAuth.Password != "" {
497+ log.Warn().Msg("using plain password in config is deprecated, use password_hash with a secrets backend")
498+ return cfg.RegistryAuth.Password, nil
499+ }
477500
478- log.Info().
479- Str("type", string(authType)).
480- Str("backend", string(backend)).
481- Msg("registry authentication enabled")
501+ return "", nil
502+}
482503
483- return store, authSvc, nil
504+func loadTokenConfig(ctx context.Context, cfg Config, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) ([]byte, time.Duration, error) {
505+ secret, err := loadTokenSecret(ctx, cfg, backend, dataDir, log)
506+ if err != nil {
507+ return nil, 0, err
508+ }
509+
510+ expiry, err := parseTokenExpiry(cfg.RegistryAuth.TokenExpiry)
511+ if err != nil {
512+ return nil, 0, err
513+ }
514+
515+ return secret, expiry, nil
516+}
517+
518+func loadTokenSecret(ctx context.Context, cfg Config, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) ([]byte, error) {
519+ if cfg.RegistryAuth.TokenSecret == "" {
520+ return nil, fmt.Errorf("token_secret is required for token authentication")
521+ }
522+
523+ secret, err := loadSecret(ctx, backend, cfg.RegistryAuth.TokenSecret, dataDir, log)
524+ if err != nil {
525+ return nil, log.WrapErr(err, "failed to load token secret")
526+ }
527+
528+ return []byte(secret), nil
529+}
530+
531+func parseTokenExpiry(expiry string) (time.Duration, error) {
532+ if expiry == "" {
533+ return 0, nil
534+ }
535+
536+ parsed, err := time.ParseDuration(expiry)
537+ if err != nil {
538+ return 0, fmt.Errorf("invalid token_expiry: %w", err)
539+ }
540+
541+ return parsed, nil
484542 }
485543
486544 // loadSecret loads a secret from the configured backend.
@@ -395,92 +395,150 @@ func createAuthService(ctx context.Context, cfg Config, log zerowrap.Logger) (ou
395 return nil, nil, nil395 return nil, nil, nil
396 }396 }
397 397
398- // Determine auth type398+ authType := resolveAuthType(cfg.RegistryAuth.Type)
399- authType := domain.AuthTypePassword399+ backend := resolveSecretsBackend(cfg.Secrets.Backend)
400- if cfg.RegistryAuth.Type == "token" {400+ dataDir := resolveDataDir(cfg.Server.DataDir)
401- authType = domain.AuthTypeToken401+
402+ store, err := createTokenStore(authType, backend, dataDir, log)
403+ if err != nil {
404+ return nil, nil, err
405+ }
406+
407+ authConfig, err := buildAuthConfig(ctx, cfg, authType, backend, dataDir, log)
408+ if err != nil {
409+ return nil, nil, err
410+ }
411+
412+ authSvc := auth.NewService(authConfig, store, log)
413+
414+ log.Info().
415+ Str("type", string(authType)).
416+ Str("backend", string(backend)).
417+ Msg("registry authentication enabled")
418+
419+ return store, authSvc, nil
420+}
421+
422+func resolveAuthType(authType string) domain.AuthType {
423+ if authType == "token" {
424+ return domain.AuthTypeToken
402 }425 }
426+ return domain.AuthTypePassword
427+}
403 428
404- // Determine secrets backend429+func resolveSecretsBackend(backend string) domain.SecretsBackend {
405- backend := domain.SecretsBackendUnsafe // default430+ switch backend {
406- switch cfg.Secrets.Backend {
407 case "pass":431 case "pass":
408- backend = domain.SecretsBackendPass432+ return domain.SecretsBackendPass
409 case "sops":433 case "sops":
410- backend = domain.SecretsBackendSops434+ return domain.SecretsBackendSops
411 case "unsafe", "":435 case "unsafe", "":
412- backend = domain.SecretsBackendUnsafe436+ return domain.SecretsBackendUnsafe
437+ default:
438+ return domain.SecretsBackendUnsafe
413 }439 }
440+}
414 441
415- // Get data directory442+func resolveDataDir(dataDir string) string {
416- dataDir := cfg.Server.DataDir
417 if dataDir == "" {443 if dataDir == "" {
418- dataDir = DefaultDataDir()444+ return DefaultDataDir()
419 }445 }
446+ return dataDir
447+}
420 448
421- // Create token store (needed for token auth or to store revocations)449+func createTokenStore(authType domain.AuthType, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) (out.TokenStore, error) {
422- var store out.TokenStore450+ if authType != domain.AuthTypeToken {
423- var err error451+ return nil, nil
424- if authType == domain.AuthTypeToken {452+ }
425- store, err = tokenstore.NewStore(backend, dataDir, log)453+
426- if err != nil {454+ store, err := tokenstore.NewStore(backend, dataDir, log)
427- return nil, nil, log.WrapErr(err, "failed to create token store")455+ if err != nil {
428- }456+ return nil, log.WrapErr(err, "failed to create token store")
429 }457 }
458+ return store, nil
459+}
430 460
431- // Build auth config461+func buildAuthConfig(ctx context.Context, cfg Config, authType domain.AuthType, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) (auth.Config, error) {
432 authConfig := auth.Config{462 authConfig := auth.Config{
433 Enabled: cfg.RegistryAuth.Enabled,463 Enabled: cfg.RegistryAuth.Enabled,
434 AuthType: authType,464 AuthType: authType,
435 Username: cfg.RegistryAuth.Username,465 Username: cfg.RegistryAuth.Username,
436 }466 }
437 467
438- // Load secrets based on auth type468+ switch authType {
439- if authType == domain.AuthTypePassword {469+ case domain.AuthTypePassword:
440- // For password auth, we need the bcrypt hash470+ hash, err := loadPasswordHash(ctx, cfg, backend, dataDir, log)
441- if cfg.RegistryAuth.PasswordHash != "" {471+ if err != nil {
442- // Load from secrets backend472+ return auth.Config{}, err
443- hash, err := loadSecret(ctx, backend, cfg.RegistryAuth.PasswordHash, dataDir, log)
444- if err != nil {
445- return nil, nil, log.WrapErr(err, "failed to load password hash")
446- }
447- authConfig.PasswordHash = hash
448- } else if cfg.RegistryAuth.Password != "" {
449- // Backward compatibility: use plain password (will be hashed on validation)
450- // This is less secure but maintains backward compatibility
451- log.Warn().Msg("using plain password in config is deprecated, use password_hash with a secrets backend")
452- authConfig.PasswordHash = cfg.RegistryAuth.Password
453 }473 }
454- } else {474+ authConfig.PasswordHash = hash
455- // For token auth, we need the signing secret475+ case domain.AuthTypeToken:
456- if cfg.RegistryAuth.TokenSecret != "" {476+ secret, expiry, err := loadTokenConfig(ctx, cfg, backend, dataDir, log)
457- secret, err := loadSecret(ctx, backend, cfg.RegistryAuth.TokenSecret, dataDir, log)477+ if err != nil {
458- if err != nil {478+ return auth.Config{}, err
459- return nil, nil, log.WrapErr(err, "failed to load token secret")
460- }
461- authConfig.TokenSecret = []byte(secret)
462- } else {
463- return nil, nil, fmt.Errorf("token_secret is required for token authentication")
464 }479 }
480+ authConfig.TokenSecret = secret
481+ authConfig.TokenExpiry = expiry
482+ }
465 483
466- // Parse token expiry484+ return authConfig, nil
467- if cfg.RegistryAuth.TokenExpiry != "" {485+}
468- expiry, err := time.ParseDuration(cfg.RegistryAuth.TokenExpiry)486+
469- if err != nil {487+func loadPasswordHash(ctx context.Context, cfg Config, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) (string, error) {
470- return nil, nil, fmt.Errorf("invalid token_expiry: %w", err)488+ if cfg.RegistryAuth.PasswordHash != "" {
471- }489+ hash, err := loadSecret(ctx, backend, cfg.RegistryAuth.PasswordHash, dataDir, log)
472- authConfig.TokenExpiry = expiry490+ if err != nil {
491+ return "", log.WrapErr(err, "failed to load password hash")
473 }492 }
493+ return hash, nil
474 }494 }
475 495
476- authSvc := auth.NewService(authConfig, store, log)496+ if cfg.RegistryAuth.Password != "" {
497+ log.Warn().Msg("using plain password in config is deprecated, use password_hash with a secrets backend")
498+ return cfg.RegistryAuth.Password, nil
499+ }
477 500
478- log.Info().501+ return "", nil
479- Str("type", string(authType)).502+}
480- Str("backend", string(backend)).
481- Msg("registry authentication enabled")
482 503
483- return store, authSvc, nil504+func loadTokenConfig(ctx context.Context, cfg Config, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) ([]byte, time.Duration, error) {
505+ secret, err := loadTokenSecret(ctx, cfg, backend, dataDir, log)
506+ if err != nil {
507+ return nil, 0, err
508+ }
509+
510+ expiry, err := parseTokenExpiry(cfg.RegistryAuth.TokenExpiry)
511+ if err != nil {
512+ return nil, 0, err
513+ }
514+
515+ return secret, expiry, nil
516+}
517+
518+func loadTokenSecret(ctx context.Context, cfg Config, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) ([]byte, error) {
519+ if cfg.RegistryAuth.TokenSecret == "" {
520+ return nil, fmt.Errorf("token_secret is required for token authentication")
521+ }
522+
523+ secret, err := loadSecret(ctx, backend, cfg.RegistryAuth.TokenSecret, dataDir, log)
524+ if err != nil {
525+ return nil, log.WrapErr(err, "failed to load token secret")
526+ }
527+
528+ return []byte(secret), nil
529+}
530+
531+func parseTokenExpiry(expiry string) (time.Duration, error) {
532+ if expiry == "" {
533+ return 0, nil
534+ }
535+
536+ parsed, err := time.ParseDuration(expiry)
537+ if err != nil {
538+ return 0, fmt.Errorf("invalid token_expiry: %w", err)
539+ }
540+
541+ return parsed, nil
484 }542 }
485 543
486 // loadSecret loads a secret from the configured backend.544 // loadSecret loads a secret from the configured backend.
M internal/usecase/auth/service.go +43 -19
@@ -91,37 +91,54 @@ func (s *Service) ValidateToken(ctx context.Context, tokenString string) (*domai
9191 })
9292 log := zerowrap.FromCtx(ctx)
9393
94- // Parse the token
94+ claims, err := s.parseTokenClaims(tokenString)
95+ if err != nil {
96+ log.Debug().Err(err).Msg("failed to parse token")
97+ return nil, err
98+ }
99+
100+ tokenClaims := buildTokenClaims(claims)
101+
102+ if err := s.ensureTokenNotRevoked(ctx, tokenClaims, log); err != nil {
103+ return nil, err
104+ }
105+
106+ if err := ensureTokenNotExpired(tokenClaims, log); err != nil {
107+ return nil, err
108+ }
109+
110+ log.Debug().Str("subject", tokenClaims.Subject).Msg("token validation successful")
111+ return tokenClaims, nil
112+}
113+
114+func (s *Service) parseTokenClaims(tokenString string) (jwt.MapClaims, error) {
95115 token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {
96- // Validate signing method
97116 if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
98117 return nil, domain.ErrInvalidToken
99118 }
100119 return s.config.TokenSecret, nil
101120 })
102121 if err != nil {
103- log.Debug().Err(err).Msg("failed to parse token")
104122 return nil, domain.ErrInvalidToken
105123 }
106124
107125 if !token.Valid {
108- log.Debug().Msg("token is invalid")
109126 return nil, domain.ErrInvalidToken
110127 }
111128
112- // Extract claims
113129 claims, ok := token.Claims.(jwt.MapClaims)
114130 if !ok {
115- log.Debug().Msg("failed to extract claims")
116131 return nil, domain.ErrInvalidToken
117132 }
118133
119- // Build TokenClaims from JWT claims
134+ return claims, nil
135+}
136+
137+func buildTokenClaims(claims jwt.MapClaims) *domain.TokenClaims {
120138 tokenClaims := &domain.TokenClaims{
121139 Issuer: getStringClaim(claims, "iss"),
122140 }
123141
124- // Get standard claims
125142 if jti, ok := claims["jti"].(string); ok {
126143 tokenClaims.ID = jti
127144 }
@@ -142,27 +159,34 @@ func (s *Service) ValidateToken(ctx context.Context, tokenString string) (*domai
142159 }
143160 }
144161
145- // Check if token is revoked
162+ return tokenClaims
163+}
164+
165+func (s *Service) ensureTokenNotRevoked(ctx context.Context, tokenClaims *domain.TokenClaims, log zerowrap.Logger) error {
146166 revoked, err := s.tokenStore.IsRevoked(ctx, tokenClaims.ID)
147167 if err != nil {
148168 log.Warn().Err(err).Msg("failed to check token revocation")
149- // Continue anyway - revocation check is best effort
169+ return nil
150170 }
151171 if revoked {
152172 log.Debug().Str("token_id", tokenClaims.ID).Msg("token has been revoked")
153- return nil, domain.ErrRevokedToken
173+ return domain.ErrRevokedToken
154174 }
155175
156- // Check expiry (if set)
157- if tokenClaims.ExpiresAt > 0 {
158- if time.Now().Unix() > tokenClaims.ExpiresAt {
159- log.Debug().Str("token_id", tokenClaims.ID).Msg("token has expired")
160- return nil, domain.ErrExpiredToken
161- }
176+ return nil
177+}
178+
179+func ensureTokenNotExpired(tokenClaims *domain.TokenClaims, log zerowrap.Logger) error {
180+ if tokenClaims.ExpiresAt <= 0 {
181+ return nil
162182 }
163183
164- log.Debug().Str("subject", tokenClaims.Subject).Msg("token validation successful")
165- return tokenClaims, nil
184+ if time.Now().Unix() > tokenClaims.ExpiresAt {
185+ log.Debug().Str("token_id", tokenClaims.ID).Msg("token has expired")
186+ return domain.ErrExpiredToken
187+ }
188+
189+ return nil
166190 }
167191
168192 // GenerateToken creates a new JWT token for the given subject.
@@ -91,37 +91,54 @@ func (s *Service) ValidateToken(ctx context.Context, tokenString string) (*domai
91 })91 })
92 log := zerowrap.FromCtx(ctx)92 log := zerowrap.FromCtx(ctx)
93 93
94- // Parse the token94+ claims, err := s.parseTokenClaims(tokenString)
95+ if err != nil {
96+ log.Debug().Err(err).Msg("failed to parse token")
97+ return nil, err
98+ }
99+
100+ tokenClaims := buildTokenClaims(claims)
101+
102+ if err := s.ensureTokenNotRevoked(ctx, tokenClaims, log); err != nil {
103+ return nil, err
104+ }
105+
106+ if err := ensureTokenNotExpired(tokenClaims, log); err != nil {
107+ return nil, err
108+ }
109+
110+ log.Debug().Str("subject", tokenClaims.Subject).Msg("token validation successful")
111+ return tokenClaims, nil
112+}
113+
114+func (s *Service) parseTokenClaims(tokenString string) (jwt.MapClaims, error) {
95 token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {115 token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {
96- // Validate signing method
97 if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {116 if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
98 return nil, domain.ErrInvalidToken117 return nil, domain.ErrInvalidToken
99 }118 }
100 return s.config.TokenSecret, nil119 return s.config.TokenSecret, nil
101 })120 })
102 if err != nil {121 if err != nil {
103- log.Debug().Err(err).Msg("failed to parse token")
104 return nil, domain.ErrInvalidToken122 return nil, domain.ErrInvalidToken
105 }123 }
106 124
107 if !token.Valid {125 if !token.Valid {
108- log.Debug().Msg("token is invalid")
109 return nil, domain.ErrInvalidToken126 return nil, domain.ErrInvalidToken
110 }127 }
111 128
112- // Extract claims
113 claims, ok := token.Claims.(jwt.MapClaims)129 claims, ok := token.Claims.(jwt.MapClaims)
114 if !ok {130 if !ok {
115- log.Debug().Msg("failed to extract claims")
116 return nil, domain.ErrInvalidToken131 return nil, domain.ErrInvalidToken
117 }132 }
118 133
119- // Build TokenClaims from JWT claims134+ return claims, nil
135+}
136+
137+func buildTokenClaims(claims jwt.MapClaims) *domain.TokenClaims {
120 tokenClaims := &domain.TokenClaims{138 tokenClaims := &domain.TokenClaims{
121 Issuer: getStringClaim(claims, "iss"),139 Issuer: getStringClaim(claims, "iss"),
122 }140 }
123 141
124- // Get standard claims
125 if jti, ok := claims["jti"].(string); ok {142 if jti, ok := claims["jti"].(string); ok {
126 tokenClaims.ID = jti143 tokenClaims.ID = jti
127 }144 }
@@ -142,27 +159,34 @@ func (s *Service) ValidateToken(ctx context.Context, tokenString string) (*domai
142 }159 }
143 }160 }
144 161
145- // Check if token is revoked162+ return tokenClaims
163+}
164+
165+func (s *Service) ensureTokenNotRevoked(ctx context.Context, tokenClaims *domain.TokenClaims, log zerowrap.Logger) error {
146 revoked, err := s.tokenStore.IsRevoked(ctx, tokenClaims.ID)166 revoked, err := s.tokenStore.IsRevoked(ctx, tokenClaims.ID)
147 if err != nil {167 if err != nil {
148 log.Warn().Err(err).Msg("failed to check token revocation")168 log.Warn().Err(err).Msg("failed to check token revocation")
149- // Continue anyway - revocation check is best effort169+ return nil
150 }170 }
151 if revoked {171 if revoked {
152 log.Debug().Str("token_id", tokenClaims.ID).Msg("token has been revoked")172 log.Debug().Str("token_id", tokenClaims.ID).Msg("token has been revoked")
153- return nil, domain.ErrRevokedToken173+ return domain.ErrRevokedToken
154 }174 }
155 175
156- // Check expiry (if set)176+ return nil
157- if tokenClaims.ExpiresAt > 0 {177+}
158- if time.Now().Unix() > tokenClaims.ExpiresAt {178+
159- log.Debug().Str("token_id", tokenClaims.ID).Msg("token has expired")179+func ensureTokenNotExpired(tokenClaims *domain.TokenClaims, log zerowrap.Logger) error {
160- return nil, domain.ErrExpiredToken180+ if tokenClaims.ExpiresAt <= 0 {
161- }181+ return nil
162 }182 }
163 183
164- log.Debug().Str("subject", tokenClaims.Subject).Msg("token validation successful")184+ if time.Now().Unix() > tokenClaims.ExpiresAt {
165- return tokenClaims, nil185+ log.Debug().Str("token_id", tokenClaims.ID).Msg("token has expired")
186+ return domain.ErrExpiredToken
187+ }
188+
189+ return nil
166 }190 }
167 191
168 // GenerateToken creates a new JWT token for the given subject.192 // GenerateToken creates a new JWT token for the given subject.
M internal/usecase/container/service.go +73 -42
@@ -468,6 +468,13 @@ func (s *Service) buildImageRef(image string) string {
468468 return fmt.Sprintf("%s/%s", s.config.RegistryDomain, image)
469469 }
470470
471+func (s *Service) pullRefForDeploy(ctx context.Context, imageRef string) (string, bool) {
472+ if !domain.IsInternalDeploy(ctx) {
473+ return imageRef, false
474+ }
475+ return rewriteToLocalRegistry(imageRef, s.config.RegistryDomain, s.config.RegistryPort), true
476+}
477+
471478 // ensureImage ensures the image is available locally, pulling if needed.
472479 // Returns the canonical image reference to use for container operations.
473480 func (s *Service) ensureImage(ctx context.Context, imageRef string) (string, error) {
@@ -475,11 +482,7 @@ func (s *Service) ensureImage(ctx context.Context, imageRef string) (string, err
475482 log := zerowrap.FromCtx(ctx)
476483
477484 // Determine if this is an internal deploy and what reference to use for pulls.
478- pullRef := imageRef
479- isInternal := domain.IsInternalDeploy(ctx)
480- if isInternal {
481- pullRef = rewriteToLocalRegistry(imageRef, s.config.RegistryDomain, s.config.RegistryPort)
482- }
485+ pullRef, isInternal := s.pullRefForDeploy(ctx, imageRef)
483486 if pullRef != imageRef {
484487 log.Info().
485488 Str("original_ref", imageRef).
@@ -487,64 +490,96 @@ func (s *Service) ensureImage(ctx context.Context, imageRef string) (string, err
487490 Msg("internal deploy: using localhost registry for pull")
488491 }
489492
490- // Check local images using the canonical reference or pull reference.
493+ found, err := s.ensureLocalImage(ctx, imageRef, pullRef)
494+ if err != nil {
495+ return "", err
496+ }
497+ if found {
498+ return imageRef, nil
499+ }
500+
501+ // Pull image
502+ log.Info().Msg("pulling image from registry")
503+
504+ if err := s.pullImage(ctx, pullRef, isInternal); err != nil {
505+ return "", err
506+ }
507+
508+ if err := s.tagImageIfNeeded(ctx, pullRef, imageRef); err != nil {
509+ return "", err
510+ }
511+
512+ log.Info().Msg("image pulled successfully")
513+ return imageRef, nil
514+}
515+
516+func (s *Service) ensureLocalImage(ctx context.Context, imageRef, pullRef string) (bool, error) {
517+ log := zerowrap.FromCtx(ctx)
518+
491519 localImages, err := s.runtime.ListImages(ctx)
492520 if err != nil {
493521 log.WrapErr(err, "failed to list local images, will attempt pull")
494- } else {
495- normalizedRef := normalizeImageRef(imageRef)
496- normalizedPullRef := normalizeImageRef(pullRef)
497- for _, img := range localImages {
498- normalizedImage := normalizeImageRef(img)
499- if normalizedImage == normalizedRef {
500- log.Info().Msg("image found locally, skipping pull")
501- return imageRef, nil
502- }
503- if normalizedImage == normalizedPullRef {
504- if pullRef != imageRef {
505- if err := s.runtime.TagImage(ctx, pullRef, imageRef); err != nil {
506- return "", log.WrapErr(err, "failed to tag image from pull reference")
507- }
508- }
509- log.Info().Msg("image found locally, skipping pull")
510- return imageRef, nil
522+ return false, nil
523+ }
524+
525+ normalizedRef := normalizeImageRef(imageRef)
526+ normalizedPullRef := normalizeImageRef(pullRef)
527+ for _, img := range localImages {
528+ normalizedImage := normalizeImageRef(img)
529+ if normalizedImage == normalizedRef {
530+ log.Info().Msg("image found locally, skipping pull")
531+ return true, nil
532+ }
533+ if normalizedImage == normalizedPullRef {
534+ if err := s.tagImageIfNeeded(ctx, pullRef, imageRef); err != nil {
535+ return false, err
511536 }
537+ log.Info().Msg("image found locally, skipping pull")
538+ return true, nil
512539 }
513540 }
514541
515- // Pull image
516- log.Info().Msg("pulling image from registry")
542+ return false, nil
543+}
544+
545+func (s *Service) pullImage(ctx context.Context, pullRef string, isInternal bool) error {
546+ log := zerowrap.FromCtx(ctx)
517547
518548 switch {
519549 case isInternal && s.config.RegistryAuthEnabled:
520550 if s.config.InternalRegistryUsername == "" || s.config.InternalRegistryPassword == "" {
521- return "", log.WrapErr(fmt.Errorf("internal registry auth not configured"), "failed to pull image for internal deploy")
551+ return log.WrapErr(fmt.Errorf("internal registry auth not configured"), "failed to pull image for internal deploy")
522552 }
523553 if err := s.runtime.PullImageWithAuth(ctx, pullRef, s.config.InternalRegistryUsername, s.config.InternalRegistryPassword); err != nil {
524- return "", log.WrapErr(err, "failed to pull image with internal auth")
554+ return log.WrapErr(err, "failed to pull image with internal auth")
525555 }
526556 case isInternal:
527557 if err := s.runtime.PullImage(ctx, pullRef); err != nil {
528- return "", log.WrapErr(err, "failed to pull image")
558+ return log.WrapErr(err, "failed to pull image")
529559 }
530560 case s.config.RegistryAuthEnabled:
531561 if err := s.runtime.PullImageWithAuth(ctx, pullRef, s.config.RegistryUsername, s.config.RegistryPassword); err != nil {
532- return "", log.WrapErr(err, "failed to pull image with auth")
562+ return log.WrapErr(err, "failed to pull image with auth")
533563 }
534564 default:
535565 if err := s.runtime.PullImage(ctx, pullRef); err != nil {
536- return "", log.WrapErr(err, "failed to pull image")
566+ return log.WrapErr(err, "failed to pull image")
537567 }
538568 }
539569
540- if pullRef != imageRef {
541- if err := s.runtime.TagImage(ctx, pullRef, imageRef); err != nil {
542- return "", log.WrapErr(err, "failed to tag image from pull reference")
543- }
570+ return nil
571+}
572+
573+func (s *Service) tagImageIfNeeded(ctx context.Context, sourceRef, targetRef string) error {
574+ if sourceRef == targetRef {
575+ return nil
544576 }
545577
546- log.Info().Msg("image pulled successfully")
547- return imageRef, nil
578+ log := zerowrap.FromCtx(ctx)
579+ if err := s.runtime.TagImage(ctx, sourceRef, targetRef); err != nil {
580+ return log.WrapErr(err, "failed to tag image from pull reference")
581+ }
582+ return nil
548583 }
549584
550585 func (s *Service) loadEnvironment(ctx context.Context, domainName, imageRef string) ([]string, error) {
@@ -1021,16 +1056,12 @@ func rewriteToLocalRegistry(imageRef, registryDomain string, registryPort int) s
10211056
10221057 localRegistry := fmt.Sprintf("localhost:%d", registryPort)
10231058 localPrefix := localRegistry + "/"
1024- if strings.HasPrefix(imageRef, localPrefix) {
1025- return imageRef
1026- }
1059+ imageRef = strings.TrimPrefix(imageRef, localPrefix)
10271060
10281061 registryDomain = strings.TrimSuffix(registryDomain, "/")
10291062 if registryDomain != "" {
10301063 prefix := registryDomain + "/"
1031- if strings.HasPrefix(imageRef, prefix) {
1032- imageRef = strings.TrimPrefix(imageRef, prefix)
1033- }
1064+ imageRef = strings.TrimPrefix(imageRef, prefix)
10341065 }
10351066
10361067 return localPrefix + imageRef
@@ -468,6 +468,13 @@ func (s *Service) buildImageRef(image string) string {
468 return fmt.Sprintf("%s/%s", s.config.RegistryDomain, image)468 return fmt.Sprintf("%s/%s", s.config.RegistryDomain, image)
469 }469 }
470 470
471+func (s *Service) pullRefForDeploy(ctx context.Context, imageRef string) (string, bool) {
472+ if !domain.IsInternalDeploy(ctx) {
473+ return imageRef, false
474+ }
475+ return rewriteToLocalRegistry(imageRef, s.config.RegistryDomain, s.config.RegistryPort), true
476+}
477+
471 // ensureImage ensures the image is available locally, pulling if needed.478 // ensureImage ensures the image is available locally, pulling if needed.
472 // Returns the canonical image reference to use for container operations.479 // Returns the canonical image reference to use for container operations.
473 func (s *Service) ensureImage(ctx context.Context, imageRef string) (string, error) {480 func (s *Service) ensureImage(ctx context.Context, imageRef string) (string, error) {
@@ -475,11 +482,7 @@ func (s *Service) ensureImage(ctx context.Context, imageRef string) (string, err
475 log := zerowrap.FromCtx(ctx)482 log := zerowrap.FromCtx(ctx)
476 483
477 // Determine if this is an internal deploy and what reference to use for pulls.484 // Determine if this is an internal deploy and what reference to use for pulls.
478- pullRef := imageRef485+ pullRef, isInternal := s.pullRefForDeploy(ctx, imageRef)
479- isInternal := domain.IsInternalDeploy(ctx)
480- if isInternal {
481- pullRef = rewriteToLocalRegistry(imageRef, s.config.RegistryDomain, s.config.RegistryPort)
482- }
483 if pullRef != imageRef {486 if pullRef != imageRef {
484 log.Info().487 log.Info().
485 Str("original_ref", imageRef).488 Str("original_ref", imageRef).
@@ -487,64 +490,96 @@ func (s *Service) ensureImage(ctx context.Context, imageRef string) (string, err
487 Msg("internal deploy: using localhost registry for pull")490 Msg("internal deploy: using localhost registry for pull")
488 }491 }
489 492
490- // Check local images using the canonical reference or pull reference.493+ found, err := s.ensureLocalImage(ctx, imageRef, pullRef)
494+ if err != nil {
495+ return "", err
496+ }
497+ if found {
498+ return imageRef, nil
499+ }
500+
501+ // Pull image
502+ log.Info().Msg("pulling image from registry")
503+
504+ if err := s.pullImage(ctx, pullRef, isInternal); err != nil {
505+ return "", err
506+ }
507+
508+ if err := s.tagImageIfNeeded(ctx, pullRef, imageRef); err != nil {
509+ return "", err
510+ }
511+
512+ log.Info().Msg("image pulled successfully")
513+ return imageRef, nil
514+}
515+
516+func (s *Service) ensureLocalImage(ctx context.Context, imageRef, pullRef string) (bool, error) {
517+ log := zerowrap.FromCtx(ctx)
518+
491 localImages, err := s.runtime.ListImages(ctx)519 localImages, err := s.runtime.ListImages(ctx)
492 if err != nil {520 if err != nil {
493 log.WrapErr(err, "failed to list local images, will attempt pull")521 log.WrapErr(err, "failed to list local images, will attempt pull")
494- } else {522+ return false, nil
495- normalizedRef := normalizeImageRef(imageRef)523+ }
496- normalizedPullRef := normalizeImageRef(pullRef)524+
497- for _, img := range localImages {525+ normalizedRef := normalizeImageRef(imageRef)
498- normalizedImage := normalizeImageRef(img)526+ normalizedPullRef := normalizeImageRef(pullRef)
499- if normalizedImage == normalizedRef {527+ for _, img := range localImages {
500- log.Info().Msg("image found locally, skipping pull")528+ normalizedImage := normalizeImageRef(img)
501- return imageRef, nil529+ if normalizedImage == normalizedRef {
502- }530+ log.Info().Msg("image found locally, skipping pull")
503- if normalizedImage == normalizedPullRef {531+ return true, nil
504- if pullRef != imageRef {532+ }
505- if err := s.runtime.TagImage(ctx, pullRef, imageRef); err != nil {533+ if normalizedImage == normalizedPullRef {
506- return "", log.WrapErr(err, "failed to tag image from pull reference")534+ if err := s.tagImageIfNeeded(ctx, pullRef, imageRef); err != nil {
507- }535+ return false, err
508- }
509- log.Info().Msg("image found locally, skipping pull")
510- return imageRef, nil
511 }536 }
537+ log.Info().Msg("image found locally, skipping pull")
538+ return true, nil
512 }539 }
513 }540 }
514 541
515- // Pull image542+ return false, nil
516- log.Info().Msg("pulling image from registry")543+}
544+
545+func (s *Service) pullImage(ctx context.Context, pullRef string, isInternal bool) error {
546+ log := zerowrap.FromCtx(ctx)
517 547
518 switch {548 switch {
519 case isInternal && s.config.RegistryAuthEnabled:549 case isInternal && s.config.RegistryAuthEnabled:
520 if s.config.InternalRegistryUsername == "" || s.config.InternalRegistryPassword == "" {550 if s.config.InternalRegistryUsername == "" || s.config.InternalRegistryPassword == "" {
521- return "", log.WrapErr(fmt.Errorf("internal registry auth not configured"), "failed to pull image for internal deploy")551+ return log.WrapErr(fmt.Errorf("internal registry auth not configured"), "failed to pull image for internal deploy")
522 }552 }
523 if err := s.runtime.PullImageWithAuth(ctx, pullRef, s.config.InternalRegistryUsername, s.config.InternalRegistryPassword); err != nil {553 if err := s.runtime.PullImageWithAuth(ctx, pullRef, s.config.InternalRegistryUsername, s.config.InternalRegistryPassword); err != nil {
524- return "", log.WrapErr(err, "failed to pull image with internal auth")554+ return log.WrapErr(err, "failed to pull image with internal auth")
525 }555 }
526 case isInternal:556 case isInternal:
527 if err := s.runtime.PullImage(ctx, pullRef); err != nil {557 if err := s.runtime.PullImage(ctx, pullRef); err != nil {
528- return "", log.WrapErr(err, "failed to pull image")558+ return log.WrapErr(err, "failed to pull image")
529 }559 }
530 case s.config.RegistryAuthEnabled:560 case s.config.RegistryAuthEnabled:
531 if err := s.runtime.PullImageWithAuth(ctx, pullRef, s.config.RegistryUsername, s.config.RegistryPassword); err != nil {561 if err := s.runtime.PullImageWithAuth(ctx, pullRef, s.config.RegistryUsername, s.config.RegistryPassword); err != nil {
532- return "", log.WrapErr(err, "failed to pull image with auth")562+ return log.WrapErr(err, "failed to pull image with auth")
533 }563 }
534 default:564 default:
535 if err := s.runtime.PullImage(ctx, pullRef); err != nil {565 if err := s.runtime.PullImage(ctx, pullRef); err != nil {
536- return "", log.WrapErr(err, "failed to pull image")566+ return log.WrapErr(err, "failed to pull image")
537 }567 }
538 }568 }
539 569
540- if pullRef != imageRef {570+ return nil
541- if err := s.runtime.TagImage(ctx, pullRef, imageRef); err != nil {571+}
542- return "", log.WrapErr(err, "failed to tag image from pull reference")572+
543- }573+func (s *Service) tagImageIfNeeded(ctx context.Context, sourceRef, targetRef string) error {
574+ if sourceRef == targetRef {
575+ return nil
544 }576 }
545 577
546- log.Info().Msg("image pulled successfully")578+ log := zerowrap.FromCtx(ctx)
547- return imageRef, nil579+ if err := s.runtime.TagImage(ctx, sourceRef, targetRef); err != nil {
580+ return log.WrapErr(err, "failed to tag image from pull reference")
581+ }
582+ return nil
548 }583 }
549 584
550 func (s *Service) loadEnvironment(ctx context.Context, domainName, imageRef string) ([]string, error) {585 func (s *Service) loadEnvironment(ctx context.Context, domainName, imageRef string) ([]string, error) {
@@ -1021,16 +1056,12 @@ func rewriteToLocalRegistry(imageRef, registryDomain string, registryPort int) s
1021 1056
1022 localRegistry := fmt.Sprintf("localhost:%d", registryPort)1057 localRegistry := fmt.Sprintf("localhost:%d", registryPort)
1023 localPrefix := localRegistry + "/"1058 localPrefix := localRegistry + "/"
1024- if strings.HasPrefix(imageRef, localPrefix) {1059+ imageRef = strings.TrimPrefix(imageRef, localPrefix)
1025- return imageRef
1026- }
1027 1060
1028 registryDomain = strings.TrimSuffix(registryDomain, "/")1061 registryDomain = strings.TrimSuffix(registryDomain, "/")
1029 if registryDomain != "" {1062 if registryDomain != "" {
1030 prefix := registryDomain + "/"1063 prefix := registryDomain + "/"
1031- if strings.HasPrefix(imageRef, prefix) {1064+ imageRef = strings.TrimPrefix(imageRef, prefix)
1032- imageRef = strings.TrimPrefix(imageRef, prefix)
1033- }
1034 }1065 }
1035 1066
1036 return localPrefix + imageRef1067 return localPrefix + imageRef

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