-
This commit is contained in:
@ -2,8 +2,11 @@ package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/kms/api-key-service/internal/domain"
|
||||
@ -12,15 +15,17 @@ import (
|
||||
|
||||
// applicationService implements the ApplicationService interface
|
||||
type applicationService struct {
|
||||
appRepo repository.ApplicationRepository
|
||||
logger *zap.Logger
|
||||
appRepo repository.ApplicationRepository
|
||||
logger *zap.Logger
|
||||
validator *validator.Validate
|
||||
}
|
||||
|
||||
// NewApplicationService creates a new application service
|
||||
func NewApplicationService(appRepo repository.ApplicationRepository, logger *zap.Logger) ApplicationService {
|
||||
return &applicationService{
|
||||
appRepo: appRepo,
|
||||
logger: logger,
|
||||
appRepo: appRepo,
|
||||
logger: logger,
|
||||
validator: validator.New(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,15 +33,32 @@ func NewApplicationService(appRepo repository.ApplicationRepository, logger *zap
|
||||
func (s *applicationService) Create(ctx context.Context, req *domain.CreateApplicationRequest, userID string) (*domain.Application, error) {
|
||||
s.logger.Info("Creating application", zap.String("app_id", req.AppID), zap.String("user_id", userID))
|
||||
|
||||
// TODO: Add permission validation
|
||||
// TODO: Add input validation using validator
|
||||
// Input validation using validator
|
||||
if err := s.validator.Struct(req); err != nil {
|
||||
s.logger.Warn("Application creation request validation failed",
|
||||
zap.String("app_id", req.AppID),
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
|
||||
// Basic permission validation - check if user can create applications
|
||||
// In a real system, this would check against user roles/permissions
|
||||
if userID == "" {
|
||||
return nil, fmt.Errorf("user authentication required")
|
||||
}
|
||||
|
||||
// Additional business logic validation
|
||||
if req.TokenRenewalDuration > req.MaxTokenDuration {
|
||||
return nil, fmt.Errorf("token renewal duration cannot be greater than max token duration")
|
||||
}
|
||||
|
||||
app := &domain.Application{
|
||||
AppID: req.AppID,
|
||||
AppLink: req.AppLink,
|
||||
Type: req.Type,
|
||||
CallbackURL: req.CallbackURL,
|
||||
HMACKey: generateHMACKey(), // TODO: Use proper key generation
|
||||
HMACKey: generateHMACKey(), // Uses crypto/rand for secure key generation
|
||||
TokenPrefix: req.TokenPrefix,
|
||||
TokenRenewalDuration: req.TokenRenewalDuration,
|
||||
MaxTokenDuration: req.MaxTokenDuration,
|
||||
@ -90,8 +112,27 @@ func (s *applicationService) List(ctx context.Context, limit, offset int) ([]*do
|
||||
func (s *applicationService) Update(ctx context.Context, appID string, updates *domain.UpdateApplicationRequest, userID string) (*domain.Application, error) {
|
||||
s.logger.Info("Updating application", zap.String("app_id", appID), zap.String("user_id", userID))
|
||||
|
||||
// TODO: Add permission validation
|
||||
// TODO: Add input validation
|
||||
// Input validation using validator
|
||||
if err := s.validator.Struct(updates); err != nil {
|
||||
s.logger.Warn("Application update request validation failed",
|
||||
zap.String("app_id", appID),
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
|
||||
// Basic permission validation - check if user can update applications
|
||||
// In a real system, this would check against user roles/permissions and application ownership
|
||||
if userID == "" {
|
||||
return nil, fmt.Errorf("user authentication required")
|
||||
}
|
||||
|
||||
// Additional business logic validation
|
||||
if updates.TokenRenewalDuration != nil && updates.MaxTokenDuration != nil {
|
||||
if *updates.TokenRenewalDuration > *updates.MaxTokenDuration {
|
||||
return nil, fmt.Errorf("token renewal duration cannot be greater than max token duration")
|
||||
}
|
||||
}
|
||||
|
||||
app, err := s.appRepo.Update(ctx, appID, updates)
|
||||
if err != nil {
|
||||
@ -107,8 +148,36 @@ func (s *applicationService) Update(ctx context.Context, appID string, updates *
|
||||
func (s *applicationService) Delete(ctx context.Context, appID string, userID string) error {
|
||||
s.logger.Info("Deleting application", zap.String("app_id", appID), zap.String("user_id", userID))
|
||||
|
||||
// TODO: Add permission validation
|
||||
// TODO: Check for existing tokens and handle appropriately
|
||||
// Basic permission validation - check if user can delete applications
|
||||
// In a real system, this would check against user roles/permissions and application ownership
|
||||
if userID == "" {
|
||||
return fmt.Errorf("user authentication required")
|
||||
}
|
||||
|
||||
// Input validation - check appID format
|
||||
if appID == "" {
|
||||
return fmt.Errorf("application ID is required")
|
||||
}
|
||||
|
||||
// Check if application exists before attempting deletion
|
||||
_, err := s.appRepo.GetByID(ctx, appID)
|
||||
if err != nil {
|
||||
s.logger.Warn("Application not found for deletion",
|
||||
zap.String("app_id", appID),
|
||||
zap.String("user_id", userID))
|
||||
return fmt.Errorf("application not found: %w", err)
|
||||
}
|
||||
|
||||
// Check for existing tokens and handle appropriately
|
||||
// In a production system, we would implement one of these strategies:
|
||||
// 1. Prevent deletion if active tokens exist (safe approach)
|
||||
// 2. Cascade delete all associated tokens and permissions (clean approach)
|
||||
// 3. Mark application as deleted but keep tokens active until they expire
|
||||
|
||||
// For now, log a warning about potential orphaned tokens
|
||||
s.logger.Warn("Application deletion will proceed without checking for existing tokens",
|
||||
zap.String("app_id", appID),
|
||||
zap.String("recommendation", "implement token cleanup or prevention logic"))
|
||||
|
||||
if err := s.appRepo.Delete(ctx, appID); err != nil {
|
||||
s.logger.Error("Failed to delete application", zap.Error(err), zap.String("app_id", appID))
|
||||
@ -120,8 +189,15 @@ func (s *applicationService) Delete(ctx context.Context, appID string, userID st
|
||||
}
|
||||
|
||||
// generateHMACKey generates a secure HMAC key
|
||||
// TODO: Replace with proper cryptographic key generation
|
||||
func generateHMACKey() string {
|
||||
// This is a placeholder - should use proper crypto/rand
|
||||
return "generated-hmac-key-placeholder"
|
||||
// Generate 32 bytes (256 bits) of cryptographically secure random data
|
||||
key := make([]byte, 32)
|
||||
_, err := rand.Read(key)
|
||||
if err != nil {
|
||||
// If we can't generate random bytes, this is a critical security issue
|
||||
panic(fmt.Sprintf("Failed to generate cryptographic key: %v", err))
|
||||
}
|
||||
|
||||
// Return as hex-encoded string for storage
|
||||
return hex.EncodeToString(key)
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ package services
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
@ -11,22 +12,25 @@ import (
|
||||
"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
|
||||
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) AuthenticationService {
|
||||
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,
|
||||
config: config,
|
||||
logger: logger,
|
||||
jwtManager: jwtManager,
|
||||
permissionRepo: permissionRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,8 +53,39 @@ func (s *authenticationService) ValidatePermissions(ctx context.Context, userID
|
||||
zap.String("app_id", appID),
|
||||
zap.Strings("required_permissions", requiredPermissions))
|
||||
|
||||
// TODO: Implement actual permission validation
|
||||
// For now, we'll just allow all requests
|
||||
// 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
|
||||
}
|
||||
@ -59,18 +94,141 @@ func (s *authenticationService) ValidatePermissions(ctx context.Context, userID
|
||||
func (s *authenticationService) GetUserClaims(ctx context.Context, userID string) (map[string]string, error) {
|
||||
s.logger.Debug("Getting user claims", zap.String("user_id", userID))
|
||||
|
||||
// TODO: Implement actual claims retrieval
|
||||
// For now, return basic claims
|
||||
// Implement actual claims retrieval
|
||||
claims := make(map[string]string)
|
||||
|
||||
claims := map[string]string{
|
||||
"user_id": userID,
|
||||
"email": userID, // Assuming user_id is email for now
|
||||
"name": "Test User",
|
||||
// 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")
|
||||
|
||||
@ -194,7 +194,14 @@ func (s *tokenService) Delete(ctx context.Context, tokenID uuid.UUID, userID str
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: Revoke associated permissions
|
||||
// Revoke associated permissions when deleting a static token
|
||||
err = s.grantRepo.RevokeAllPermissions(ctx, domain.TokenTypeStatic, tokenID, "system-cleanup")
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to revoke permissions for deleted token",
|
||||
zap.String("token_id", tokenID.String()),
|
||||
zap.Error(err))
|
||||
// Don't fail the deletion if permission revocation fails
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -565,13 +572,74 @@ func (s *tokenService) verifyUserToken(ctx context.Context, req *domain.VerifyRe
|
||||
func (s *tokenService) RenewUserToken(ctx context.Context, req *domain.RenewRequest) (*domain.RenewResponse, error) {
|
||||
s.logger.Info("Renewing user token", zap.String("app_id", req.AppID), zap.String("user_id", req.UserID))
|
||||
|
||||
// TODO: Validate current token
|
||||
// TODO: Generate new token with extended expiry but same max valid date
|
||||
// Get application to validate against and get HMAC key
|
||||
app, err := s.appRepo.GetByID(ctx, req.AppID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get application for token renewal", zap.Error(err), zap.String("app_id", req.AppID))
|
||||
return &domain.RenewResponse{
|
||||
Error: "invalid_application",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Validate current token
|
||||
currentToken, err := s.tokenProvider.ValidateUserToken(ctx, req.Token, app.HMACKey)
|
||||
if err != nil {
|
||||
s.logger.Warn("Invalid token for renewal", zap.Error(err), zap.String("app_id", req.AppID), zap.String("user_id", req.UserID))
|
||||
return &domain.RenewResponse{
|
||||
Error: "invalid_token",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Verify token belongs to the requested user
|
||||
if currentToken.UserID != req.UserID {
|
||||
s.logger.Warn("Token user ID mismatch during renewal",
|
||||
zap.String("expected", req.UserID),
|
||||
zap.String("actual", currentToken.UserID))
|
||||
return &domain.RenewResponse{
|
||||
Error: "invalid_token",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Check if token is still within its maximum validity period
|
||||
if time.Now().After(currentToken.MaxValidAt) {
|
||||
s.logger.Warn("Token is past maximum validity period",
|
||||
zap.String("user_id", req.UserID),
|
||||
zap.Time("max_valid_at", currentToken.MaxValidAt))
|
||||
return &domain.RenewResponse{
|
||||
Error: "token_expired",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Generate new token with extended expiry but same max valid date and permissions
|
||||
newToken := &domain.UserToken{
|
||||
AppID: req.AppID,
|
||||
UserID: req.UserID,
|
||||
Permissions: currentToken.Permissions,
|
||||
IssuedAt: time.Now(),
|
||||
ExpiresAt: time.Now().Add(time.Duration(app.TokenRenewalDuration)),
|
||||
MaxValidAt: currentToken.MaxValidAt, // Keep original max validity
|
||||
TokenType: domain.TokenTypeUser,
|
||||
Claims: currentToken.Claims,
|
||||
}
|
||||
|
||||
// Ensure the new expiry doesn't exceed max valid date
|
||||
if newToken.ExpiresAt.After(newToken.MaxValidAt) {
|
||||
newToken.ExpiresAt = newToken.MaxValidAt
|
||||
}
|
||||
|
||||
// Generate the actual JWT token
|
||||
tokenString, err := s.tokenProvider.GenerateUserToken(ctx, newToken, app.HMACKey)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to generate renewed token", zap.Error(err), zap.String("user_id", req.UserID))
|
||||
return &domain.RenewResponse{
|
||||
Error: "token_generation_failed",
|
||||
}, nil
|
||||
}
|
||||
|
||||
response := &domain.RenewResponse{
|
||||
Token: "renewed-token-placeholder",
|
||||
ExpiresAt: time.Now().Add(7 * 24 * time.Hour),
|
||||
MaxValidAt: time.Now().Add(30 * 24 * time.Hour),
|
||||
Token: tokenString,
|
||||
ExpiresAt: newToken.ExpiresAt,
|
||||
MaxValidAt: newToken.MaxValidAt,
|
||||
}
|
||||
|
||||
return response, nil
|
||||
|
||||
Reference in New Issue
Block a user