org
This commit is contained in:
289
kms/internal/services/application_service.go
Normal file
289
kms/internal/services/application_service.go
Normal file
@ -0,0 +1,289 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user