✨ feat(auth): JWT secret retention policy + automatic cleanup loop (ADR-0021)
Implements the cleanup half of ADR-0021 (which had only config infrastructure landed). Non-primary expired secrets are removed by a goroutine that runs at auth.jwt.secret_retention.cleanup_interval (default 1h). Primary secret is never removed regardless of expiration — invariant preserved. Changes: - pkg/user/jwt_manager.go : add sync.Mutex protection; add RemoveExpiredSecrets() int and StartCleanupLoop(ctx, interval) methods. Reset() now also cancels any running cleanup goroutine. - pkg/user/auth_service.go : delegate to manager via new AuthService methods StartJWTSecretCleanupLoop and RemoveExpiredJWTSecrets. - pkg/user/user.go : extend AuthService interface accordingly. - pkg/server/server.go Run() : start cleanup loop tied to rootCtx so it stops on graceful shutdown. - pkg/jwt/* : same treatment on the secondary (less-used) implementation for consistency. - adr/0021-jwt-secret-retention-policy.md : Status → Implemented + fix numbering (was incorrectly "10."). Tests: - 4 new unit tests in pkg/user/jwt_manager_test.go covering RemoveExpiredSecrets (expired removed, primary preserved, future kept) and StartCleanupLoop (fires + stops on context cancel). - go test -race ./pkg/user/... passes. - Full BDD suite (auth/config/greet/health/info/jwt) still green. - BDD scenarios at @todo / @skip remain so — they require an admin endpoint /api/v1/admin/jwt/secrets which is explicitly out of scope. Verifier verdict: APPROVE_WITH_NITS — StartCleanupLoop is 34 lines (just over the 30-line guideline); 2 time.Sleeps in TestStartCleanupLoop_FiresAndStops are justified by the goroutine-timing nature of the test.
This commit is contained in:
@@ -218,6 +218,18 @@ func (s *userServiceImpl) ResetJWTSecrets() {
|
||||
s.secretManager.Reset(s.jwtConfig.Secret)
|
||||
}
|
||||
|
||||
// StartJWTSecretCleanupLoop delegates to the underlying secret manager to
|
||||
// start the periodic cleanup goroutine described in ADR-0021.
|
||||
func (s *userServiceImpl) StartJWTSecretCleanupLoop(ctx context.Context, interval time.Duration) {
|
||||
s.secretManager.StartCleanupLoop(ctx, interval)
|
||||
}
|
||||
|
||||
// RemoveExpiredJWTSecrets triggers an immediate cleanup pass via the
|
||||
// underlying secret manager. Returns the count of removed expired secrets.
|
||||
func (s *userServiceImpl) RemoveExpiredJWTSecrets() int {
|
||||
return s.secretManager.RemoveExpiredSecrets()
|
||||
}
|
||||
|
||||
// UserExists checks if a user exists by username
|
||||
func (s *userServiceImpl) UserExists(ctx context.Context, username string) (bool, error) {
|
||||
return s.repo.UserExists(ctx, username)
|
||||
|
||||
Reference in New Issue
Block a user