// monorepo/cloud/backend/internal/maplefile/service/collection/get.go package collection import ( "context" "go.uber.org/zap" "github.com/gocql/gocql" "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config" "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config/constants" dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection" uc_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/user" "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror" "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/ratelimit" "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation" ) type GetCollectionService interface { Execute(ctx context.Context, collectionID gocql.UUID) (*CollectionResponseDTO, error) } type getCollectionServiceImpl struct { config *config.Configuration logger *zap.Logger repo dom_collection.CollectionRepository userGetByIDUseCase uc_user.UserGetByIDUseCase authFailureRateLimiter ratelimit.AuthFailureRateLimiter } func NewGetCollectionService( config *config.Configuration, logger *zap.Logger, repo dom_collection.CollectionRepository, userGetByIDUseCase uc_user.UserGetByIDUseCase, authFailureRateLimiter ratelimit.AuthFailureRateLimiter, ) GetCollectionService { logger = logger.Named("GetCollectionService") return &getCollectionServiceImpl{ config: config, logger: logger, repo: repo, userGetByIDUseCase: userGetByIDUseCase, authFailureRateLimiter: authFailureRateLimiter, } } func (svc *getCollectionServiceImpl) Execute(ctx context.Context, collectionID gocql.UUID) (*CollectionResponseDTO, error) { // // STEP 1: Validation // if collectionID.String() == "" { svc.logger.Warn("Empty collection ID provided") return nil, httperror.NewForBadRequestWithSingleField("collection_id", "Collection ID is required") } // // STEP 2: Get user ID from context // userID, ok := ctx.Value(constants.SessionUserID).(gocql.UUID) if !ok { svc.logger.Error("Failed getting user ID from context") return nil, httperror.NewForInternalServerErrorWithSingleField("message", "Authentication context error") } // // STEP 3: Get collection from repository // collection, err := svc.repo.Get(ctx, collectionID) if err != nil { svc.logger.Error("Failed to get collection", zap.Any("error", err), zap.Any("collection_id", collectionID)) return nil, err } if collection == nil { svc.logger.Debug("Collection not found", zap.Any("collection_id", collectionID)) return nil, httperror.NewForNotFoundWithSingleField("message", "Collection not found") } // // STEP 4: Check rate limiting for authorization failures // // Check if user has exceeded authorization failure limits before checking access // This helps prevent privilege escalation attempts if svc.authFailureRateLimiter != nil { allowed, remainingAttempts, resetTime, err := svc.authFailureRateLimiter.CheckAuthFailure( ctx, userID.String(), collectionID.String(), "collection:get") if err != nil { // Log error but continue - fail open for availability svc.logger.Error("Failed to check auth failure rate limit", zap.Error(err), zap.Any("user_id", userID), zap.Any("collection_id", collectionID)) } else if !allowed { svc.logger.Warn("User blocked due to excessive authorization failures", zap.Any("user_id", userID), zap.Any("collection_id", collectionID), zap.Int("remaining_attempts", remainingAttempts), zap.Time("reset_time", resetTime)) return nil, httperror.NewTooManyRequestsError( "Too many authorization failures. Please try again later") } } // // STEP 5: Check if the user has access to this collection // // Use CheckAccess to verify both access and permission level // For GET operations, read_only permission is sufficient hasAccess, err := svc.repo.CheckAccess(ctx, collectionID, userID, dom_collection.CollectionPermissionReadOnly) if err != nil { svc.logger.Error("Failed to check collection access", zap.Error(err), zap.Any("user_id", userID), zap.Any("collection_id", collectionID)) return nil, httperror.NewInternalServerError("Failed to check collection access") } if !hasAccess { // Record authorization failure for rate limiting if svc.authFailureRateLimiter != nil { if err := svc.authFailureRateLimiter.RecordAuthFailure( ctx, userID.String(), collectionID.String(), "collection:get", "insufficient_permission"); err != nil { svc.logger.Error("Failed to record auth failure", zap.Error(err), zap.Any("user_id", userID), zap.Any("collection_id", collectionID)) } } svc.logger.Warn("Unauthorized collection access attempt", zap.Any("user_id", userID), zap.Any("collection_id", collectionID), zap.String("required_permission", dom_collection.CollectionPermissionReadOnly)) return nil, httperror.NewForForbiddenWithSingleField("message", "You don't have access to this collection") } // Record successful authorization if svc.authFailureRateLimiter != nil { if err := svc.authFailureRateLimiter.RecordAuthSuccess( ctx, userID.String(), collectionID.String(), "collection:get"); err != nil { svc.logger.Debug("Failed to record auth success", zap.Error(err), zap.Any("user_id", userID), zap.Any("collection_id", collectionID)) } } // // STEP 5: Get owner's email // var ownerEmail string svc.logger.Info("🔍 GetCollectionService: Looking up owner email", zap.String("collection_id", collectionID.String()), zap.String("owner_id", collection.OwnerID.String())) owner, err := svc.userGetByIDUseCase.Execute(ctx, collection.OwnerID) if err != nil { svc.logger.Warn("Failed to get owner email, continuing without it", zap.Any("error", err), zap.Any("owner_id", collection.OwnerID)) // Don't fail the request, just continue without the owner email } else if owner != nil { ownerEmail = owner.Email svc.logger.Info("🔍 GetCollectionService: Found owner email", zap.String("owner_email", validation.MaskEmail(ownerEmail))) } else { svc.logger.Warn("🔍 GetCollectionService: Owner user not found", zap.String("owner_id", collection.OwnerID.String())) } // // STEP 6: Map domain model to response DTO // // Note: We pass collection.FileCount (not 0) to include the actual file count // in the response. This field is maintained by IncrementFileCount/DecrementFileCount // calls when files are added/removed from the collection. // svc.logger.Info("🔍 GetCollectionService: Mapping to DTO with owner_email", zap.String("owner_email", validation.MaskEmail(ownerEmail))) response := mapCollectionToDTO(collection, int(collection.FileCount), ownerEmail) return response, nil }