Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
199
cloud/maplefile-backend/internal/service/collection/get.go
Normal file
199
cloud/maplefile-backend/internal/service/collection/get.go
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
// 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue