package auth import ( "context" "github.com/tech/sendico/pkg/db/repository" "github.com/tech/sendico/pkg/db/repository/builder" "github.com/tech/sendico/pkg/db/storable" "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/mutil/mzap" "go.mongodb.org/mongo-driver/bson/primitive" "go.uber.org/zap" ) // IndexableDB implements reordering with permission checking type indexableDBImp[T storable.Storable] struct { repo repository.Repository logger mlogger.Logger enforcer Enforcer createEmpty func() T getIndexable func(T) *model.Indexable } // NewIndexableDB creates a new auth.IndexableDB instance func newIndexableDBImp[T storable.Storable]( repo repository.Repository, logger mlogger.Logger, enforcer Enforcer, createEmpty func() T, getIndexable func(T) *model.Indexable, ) IndexableDB[T] { return &indexableDBImp[T]{ repo: repo, logger: logger.Named("indexable"), enforcer: enforcer, createEmpty: createEmpty, getIndexable: getIndexable, } } // Reorder implements reordering with permission checking using EnforceBatch func (db *indexableDBImp[T]) Reorder(ctx context.Context, accountRef, objectRef primitive.ObjectID, newIndex int, filter builder.Query) error { // Get current object to find its index obj := db.createEmpty() if err := db.repo.Get(ctx, objectRef, obj); err != nil { db.logger.Warn("Failed to get object for reordering", zap.Error(err), zap.Int("new_index", newIndex), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef)) return err } // Extract index from the object indexable := db.getIndexable(obj) currentIndex := indexable.Index if currentIndex == newIndex { db.logger.Debug("No reordering needed - same index", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.Int("current_index", currentIndex), zap.Int("new_index", newIndex)) return nil // No change needed } // Determine which objects will be affected by the reordering var affectedObjects []model.PermissionBoundStorable if currentIndex < newIndex { // Moving down: items between currentIndex+1 and newIndex will be shifted up by -1 reorderFilter := filter. And(repository.IndexOpFilter(currentIndex+1, builder.Gte)). And(repository.IndexOpFilter(newIndex, builder.Lte)) // Get all affected objects using ListPermissionBound objects, err := db.repo.ListPermissionBound(ctx, reorderFilter) if err != nil { db.logger.Warn("Failed to get affected objects for reordering (moving down)", zap.Error(err), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.Int("current_index", currentIndex), zap.Int("new_index", newIndex)) return err } affectedObjects = append(affectedObjects, objects...) db.logger.Debug("Found affected objects for moving down", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.Int("affected_count", len(objects))) } else { // Moving up: items between newIndex and currentIndex-1 will be shifted down by +1 reorderFilter := filter. And(repository.IndexOpFilter(newIndex, builder.Gte)). And(repository.IndexOpFilter(currentIndex-1, builder.Lte)) // Get all affected objects using ListPermissionBound objects, err := db.repo.ListPermissionBound(ctx, reorderFilter) if err != nil { db.logger.Warn("Failed to get affected objects for reordering (moving up)", zap.Error(err), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.Int("current_index", currentIndex), zap.Int("new_index", newIndex)) return err } affectedObjects = append(affectedObjects, objects...) db.logger.Debug("Found affected objects for moving up", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.Int("affected_count", len(objects))) } // Add the target object to the list of objects that need permission checking targetObjects, err := db.repo.ListPermissionBound(ctx, repository.IDFilter(objectRef)) if err != nil { db.logger.Warn("Failed to get target object for permission checking", zap.Error(err), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef)) return err } if len(targetObjects) > 0 { affectedObjects = append(affectedObjects, targetObjects[0]) } // Check permissions for all affected objects using EnforceBatch db.logger.Debug("Checking permissions for reordering", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.Int("affected_count", len(affectedObjects)), zap.Int("current_index", currentIndex), zap.Int("new_index", newIndex)) permissions, err := db.enforcer.EnforceBatch(ctx, affectedObjects, accountRef, model.ActionUpdate) if err != nil { db.logger.Warn("Failed to check permissions for reordering", zap.Error(err), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.Int("affected_count", len(affectedObjects))) return merrors.Internal("failed to check permissions for reordering") } // Verify all objects have update permission for resObjectRef, hasPermission := range permissions { if !hasPermission { db.logger.Info("Permission denied for object during reordering", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.String("action", string(model.ActionUpdate))) return merrors.AccessDenied(db.repo.Collection(), string(model.ActionUpdate), resObjectRef) } } db.logger.Debug("All permissions granted, proceeding with reordering", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.Int("permission_count", len(permissions))) // All permissions checked, proceed with reordering if currentIndex < newIndex { // Moving down: shift items between currentIndex+1 and newIndex up by -1 patch := repository.Patch().Inc(repository.IndexField(), -1) reorderFilter := filter. And(repository.IndexOpFilter(currentIndex+1, builder.Gte)). And(repository.IndexOpFilter(newIndex, builder.Lte)) updatedCount, err := db.repo.PatchMany(ctx, reorderFilter, patch) if err != nil { db.logger.Warn("Failed to shift objects during reordering (moving down)", zap.Error(err), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.Int("current_index", currentIndex), zap.Int("new_index", newIndex), zap.Int("updated_count", updatedCount)) return err } db.logger.Debug("Successfully shifted objects (moving down)", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.Int("updated_count", updatedCount)) } else { // Moving up: shift items between newIndex and currentIndex-1 down by +1 patch := repository.Patch().Inc(repository.IndexField(), 1) reorderFilter := filter. And(repository.IndexOpFilter(newIndex, builder.Gte)). And(repository.IndexOpFilter(currentIndex-1, builder.Lte)) updatedCount, err := db.repo.PatchMany(ctx, reorderFilter, patch) if err != nil { db.logger.Warn("Failed to shift objects during reordering (moving up)", zap.Error(err), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.Int("current_index", currentIndex), zap.Int("new_index", newIndex), zap.Int("updated_count", updatedCount)) return err } db.logger.Debug("Successfully shifted objects (moving up)", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.Int("updated_count", updatedCount)) } // Update the target object to new index if err := db.repo.Patch(ctx, objectRef, repository.Patch().Set(repository.IndexField(), newIndex)); err != nil { db.logger.Warn("Failed to update target object index", zap.Error(err), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.Int("current_index", currentIndex), zap.Int("new_index", newIndex)) return err } db.logger.Debug("Successfully reordered object with permission checking", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.Int("old_index", currentIndex), zap.Int("new_index", newIndex), zap.Int("affected_count", len(affectedObjects))) return nil }