306 lines
9.2 KiB
Go
306 lines
9.2 KiB
Go
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
|
|
}
|