monorepo/cloud/maplefile-backend/internal/service/collection/get.go

199 lines
6.8 KiB
Go

// 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
}