package services import ( "context" "fmt" "strings" "time" "go.uber.org/zap" "github.com/kms/api-key-service/internal/auth" "github.com/kms/api-key-service/internal/config" "github.com/kms/api-key-service/internal/domain" "github.com/kms/api-key-service/internal/errors" "github.com/kms/api-key-service/internal/repository" ) // authenticationService implements the AuthenticationService interface type authenticationService struct { config config.ConfigProvider logger *zap.Logger jwtManager *auth.JWTManager permissionRepo repository.PermissionRepository } // NewAuthenticationService creates a new authentication service func NewAuthenticationService(config config.ConfigProvider, logger *zap.Logger, permissionRepo repository.PermissionRepository) AuthenticationService { jwtManager := auth.NewJWTManager(config, logger) return &authenticationService{ config: config, logger: logger, jwtManager: jwtManager, permissionRepo: permissionRepo, } } // GetUserID extracts user ID from context func (s *authenticationService) GetUserID(ctx context.Context) (string, error) { // For now, this is a simple implementation // In a real implementation, this would extract from JWT tokens, session, etc. if userID, ok := ctx.Value("user_id").(string); ok { return userID, nil } return "", fmt.Errorf("user ID not found in context") } // ValidatePermissions checks if user has required permissions func (s *authenticationService) ValidatePermissions(ctx context.Context, userID string, appID string, requiredPermissions []string) error { s.logger.Debug("Validating permissions", zap.String("user_id", userID), zap.String("app_id", appID), zap.Strings("required_permissions", requiredPermissions)) // Implement role-based permission validation userRoles := s.getUserRoles(userID) // Check each required permission for _, requiredPerm := range requiredPermissions { hasPermission := false // Check if user has the permission directly through role mapping for _, role := range userRoles { if s.roleHasPermission(role, requiredPerm) { hasPermission = true break } } // If not found through roles, check direct permission grants if !hasPermission { hasPermission = s.hasDirectPermission(ctx, userID, requiredPerm) } if !hasPermission { s.logger.Warn("User lacks required permission", zap.String("user_id", userID), zap.String("required_permission", requiredPerm), zap.Strings("user_roles", userRoles)) return fmt.Errorf("insufficient permissions: missing '%s'", requiredPerm) } } s.logger.Debug("Permission validation successful", zap.String("user_id", userID), zap.Strings("required_permissions", requiredPermissions), zap.Strings("user_roles", userRoles)) return nil } // GetUserClaims retrieves user claims func (s *authenticationService) GetUserClaims(ctx context.Context, userID string) (map[string]string, error) { s.logger.Debug("Getting user claims", zap.String("user_id", userID)) // Implement actual claims retrieval claims := make(map[string]string) // Set basic user claims claims["user_id"] = userID claims["subject"] = userID // Extract name from email if userID is an email if strings.Contains(userID, "@") { claims["email"] = userID namePart := strings.Split(userID, "@")[0] claims["preferred_username"] = namePart // Convert underscores/dots to spaces for display name displayName := strings.ReplaceAll(strings.ReplaceAll(namePart, "_", " "), ".", " ") claims["name"] = displayName } else { claims["preferred_username"] = userID claims["name"] = userID } // Add role-based claims userRoles := s.getUserRoles(userID) if len(userRoles) > 0 { claims["roles"] = strings.Join(userRoles, ",") claims["primary_role"] = userRoles[0] } // Add environment-specific claims claims["provider"] = "internal" claims["auth_method"] = "header" claims["issued_at"] = fmt.Sprintf("%d", time.Now().Unix()) return claims, nil } // getUserRoles retrieves roles for a user based on patterns and rules func (s *authenticationService) getUserRoles(userID string) []string { var roles []string // Role assignment based on email patterns and business rules userLower := strings.ToLower(userID) // Super admin roles if strings.Contains(userLower, "admin@") || strings.Contains(userLower, "superadmin") { roles = append(roles, "super_admin") return roles // Super admins get all permissions } // Admin roles if strings.Contains(userLower, "admin") { roles = append(roles, "admin") } // Developer roles if strings.Contains(userLower, "dev") || strings.Contains(userLower, "engineer") || strings.Contains(userLower, "tech") { roles = append(roles, "developer") } // Manager roles if strings.Contains(userLower, "manager") || strings.Contains(userLower, "lead") { roles = append(roles, "manager") } // Default role for all users if len(roles) == 0 { roles = append(roles, "viewer") } return roles } // roleHasPermission checks if a role has a specific permission func (s *authenticationService) roleHasPermission(role, permission string) bool { // Define role-based permission matrix rolePermissions := map[string][]string{ "super_admin": { "internal.*", "app.*", "token.*", "repo.*", "permission.*", "admin.*", }, "admin": { "app.*", "token.*", "permission.read", "permission.list", "repo.read", "repo.write", }, "developer": { "app.read", "app.list", "token.create", "token.read", "token.list", "repo.*", }, "manager": { "app.read", "app.list", "token.read", "token.list", "repo.read", "permission.read", }, "viewer": { "app.read", "repo.read", "token.read", }, } permissions, exists := rolePermissions[role] if !exists { return false } // Check for exact match or wildcard match for _, perm := range permissions { if perm == permission { return true } // Check wildcard permissions (e.g., "app.*" matches "app.read") if strings.HasSuffix(perm, "*") { prefix := strings.TrimSuffix(perm, "*") if strings.HasPrefix(permission, prefix) { return true } } // Check hierarchical permissions (e.g., "repo" includes "repo.read") if !strings.Contains(perm, ".") && strings.HasPrefix(permission, perm+".") { return true } } return false } // hasDirectPermission checks if user has direct permission grant func (s *authenticationService) hasDirectPermission(ctx context.Context, userID, permission string) bool { // This would typically query the database for direct user permissions // For now, implement basic logic // Check for system-level permissions that might be granted to specific users if permission == "internal.system" && strings.Contains(userID, "system") { return true } // In a real system, this would query the granted_permissions table // or a user_permissions table for direct grants return false } // ValidateJWTToken validates a JWT token and returns claims func (s *authenticationService) ValidateJWTToken(ctx context.Context, tokenString string) (*domain.AuthContext, error) { s.logger.Debug("Validating JWT token") // Validate the token using JWT manager claims, err := s.jwtManager.ValidateToken(tokenString) if err != nil { s.logger.Warn("JWT token validation failed", zap.Error(err)) return nil, err } // Check if token is revoked revoked, err := s.jwtManager.IsTokenRevoked(tokenString) if err != nil { s.logger.Error("Failed to check token revocation status", zap.Error(err)) return nil, errors.NewInternalError("Failed to validate token").WithInternal(err) } if revoked { s.logger.Warn("JWT token is revoked", zap.String("user_id", claims.UserID)) return nil, errors.NewAuthenticationError("Token has been revoked") } // Convert JWT claims to AuthContext authContext := &domain.AuthContext{ UserID: claims.UserID, TokenType: claims.TokenType, Permissions: claims.Permissions, Claims: claims.Claims, AppID: claims.AppID, } s.logger.Debug("JWT token validated successfully", zap.String("user_id", claims.UserID), zap.String("app_id", claims.AppID)) return authContext, nil } // GenerateJWTToken generates a new JWT token for a user func (s *authenticationService) GenerateJWTToken(ctx context.Context, userToken *domain.UserToken) (string, error) { s.logger.Debug("Generating JWT token", zap.String("user_id", userToken.UserID), zap.String("app_id", userToken.AppID)) // Generate the token using JWT manager tokenString, err := s.jwtManager.GenerateToken(userToken) if err != nil { s.logger.Error("Failed to generate JWT token", zap.Error(err)) return "", err } s.logger.Debug("JWT token generated successfully", zap.String("user_id", userToken.UserID), zap.String("app_id", userToken.AppID)) return tokenString, nil } // RefreshJWTToken refreshes an existing JWT token func (s *authenticationService) RefreshJWTToken(ctx context.Context, tokenString string, newExpiration time.Time) (string, error) { s.logger.Debug("Refreshing JWT token") // Refresh the token using JWT manager newTokenString, err := s.jwtManager.RefreshToken(tokenString, newExpiration) if err != nil { s.logger.Error("Failed to refresh JWT token", zap.Error(err)) return "", err } s.logger.Debug("JWT token refreshed successfully") return newTokenString, nil }