package services import ( "context" "crypto/rand" "encoding/hex" "fmt" "time" "github.com/go-playground/validator/v10" "github.com/google/uuid" "go.uber.org/zap" "github.com/kms/api-key-service/internal/audit" "github.com/kms/api-key-service/internal/domain" "github.com/kms/api-key-service/internal/repository" ) // applicationService implements the ApplicationService interface type applicationService struct { appRepo repository.ApplicationRepository auditRepo repository.AuditRepository auditLogger audit.AuditLogger logger *zap.Logger validator *validator.Validate } // NewApplicationService creates a new application service func NewApplicationService(appRepo repository.ApplicationRepository, auditRepo repository.AuditRepository, logger *zap.Logger) ApplicationService { // Create audit logger with audit package's repository interface auditRepoImpl := &auditRepositoryAdapter{repo: auditRepo} auditLogger := audit.NewAuditLogger(nil, logger, auditRepoImpl) // config can be nil for now return &applicationService{ appRepo: appRepo, auditRepo: auditRepo, auditLogger: auditLogger, logger: logger, validator: validator.New(), } } // auditRepositoryAdapter adapts repository.AuditRepository to audit.AuditRepository type auditRepositoryAdapter struct { repo repository.AuditRepository } func (a *auditRepositoryAdapter) Create(ctx context.Context, event *audit.AuditEvent) error { return a.repo.Create(ctx, event) } func (a *auditRepositoryAdapter) Query(ctx context.Context, filter *audit.AuditFilter) ([]*audit.AuditEvent, error) { return a.repo.Query(ctx, filter) } func (a *auditRepositoryAdapter) GetStats(ctx context.Context, filter *audit.AuditStatsFilter) (*audit.AuditStats, error) { return a.repo.GetStats(ctx, filter) } func (a *auditRepositoryAdapter) DeleteOldEvents(ctx context.Context, olderThan time.Time) (int, error) { return a.repo.DeleteOldEvents(ctx, olderThan) } func (a *auditRepositoryAdapter) GetByID(ctx context.Context, eventID uuid.UUID) (*audit.AuditEvent, error) { return a.repo.GetByID(ctx, eventID) } // Create creates a new application 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)) // 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) } // Manual validation for Duration fields if req.TokenRenewalDuration.Duration <= 0 { return nil, fmt.Errorf("token_renewal_duration must be greater than 0") } if req.MaxTokenDuration.Duration <= 0 { return nil, fmt.Errorf("max_token_duration must be greater than 0") } // 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.Duration > req.MaxTokenDuration.Duration { 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(), // Uses crypto/rand for secure key generation TokenPrefix: req.TokenPrefix, TokenRenewalDuration: req.TokenRenewalDuration, MaxTokenDuration: req.MaxTokenDuration, Owner: req.Owner, } if err := s.appRepo.Create(ctx, app); err != nil { s.logger.Error("Failed to create application", zap.Error(err), zap.String("app_id", req.AppID)) // Log audit event for failed creation s.auditLogger.LogEvent(ctx, audit.NewAuditEventBuilder(audit.EventTypeAppCreated). WithSeverity(audit.SeverityError). WithStatus(audit.StatusFailure). WithActor(userID, "user", ""). WithResource(req.AppID, "application"). WithAction("create"). WithDescription(fmt.Sprintf("Failed to create application %s", req.AppID)). WithDetails(map[string]interface{}{ "error": err.Error(), "app_id": req.AppID, "user_id": userID, }). Build()) return nil, fmt.Errorf("failed to create application: %w", err) } // Log successful creation s.auditLogger.LogEvent(ctx, audit.NewAuditEventBuilder(audit.EventTypeAppCreated). WithSeverity(audit.SeverityInfo). WithStatus(audit.StatusSuccess). WithActor(userID, "user", ""). WithResource(app.AppID, "application"). WithAction("create"). WithDescription(fmt.Sprintf("Created application %s", app.AppID)). WithDetails(map[string]interface{}{ "app_id": app.AppID, "app_link": app.AppLink, "type": app.Type, "user_id": userID, "owner_name": app.Owner.Name, "owner_type": app.Owner.Type, }). Build()) s.logger.Info("Application created successfully", zap.String("app_id", app.AppID)) return app, nil } // GetByID retrieves an application by its ID func (s *applicationService) GetByID(ctx context.Context, appID string) (*domain.Application, error) { s.logger.Debug("Getting application by ID", zap.String("app_id", appID)) app, err := s.appRepo.GetByID(ctx, appID) if err != nil { s.logger.Error("Failed to get application", zap.Error(err), zap.String("app_id", appID)) return nil, fmt.Errorf("failed to get application: %w", err) } return app, nil } // List retrieves applications with pagination func (s *applicationService) List(ctx context.Context, limit, offset int) ([]*domain.Application, error) { s.logger.Debug("Listing applications", zap.Int("limit", limit), zap.Int("offset", offset)) if limit <= 0 { limit = 50 // Default limit } if limit > 100 { limit = 100 // Max limit } apps, err := s.appRepo.List(ctx, limit, offset) if err != nil { s.logger.Error("Failed to list applications", zap.Error(err)) return nil, fmt.Errorf("failed to list applications: %w", err) } s.logger.Debug("Listed applications", zap.Int("count", len(apps))) return apps, nil } // Update updates an existing application 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)) // 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") } // Manual validation for Duration fields if updates.TokenRenewalDuration != nil && updates.TokenRenewalDuration.Duration <= 0 { return nil, fmt.Errorf("token_renewal_duration must be greater than 0") } if updates.MaxTokenDuration != nil && updates.MaxTokenDuration.Duration <= 0 { return nil, fmt.Errorf("max_token_duration must be greater than 0") } // Additional business logic validation if updates.TokenRenewalDuration != nil && updates.MaxTokenDuration != nil { if updates.TokenRenewalDuration.Duration > updates.MaxTokenDuration.Duration { 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 { s.logger.Error("Failed to update application", zap.Error(err), zap.String("app_id", appID)) return nil, fmt.Errorf("failed to update application: %w", err) } s.logger.Info("Application updated successfully", zap.String("app_id", appID)) return app, nil } // Delete deletes an application 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)) // 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)) return fmt.Errorf("failed to delete application: %w", err) } s.logger.Info("Application deleted successfully", zap.String("app_id", appID)) return nil } // generateHMACKey generates a secure HMAC key func generateHMACKey() string { // 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) }