199 lines
6.8 KiB
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
|
|
}
|