package auth import ( "context" "errors" "github.com/tech/sendico/pkg/db/policy" "github.com/tech/sendico/pkg/db/repository" "github.com/tech/sendico/pkg/db/repository/builder" "github.com/tech/sendico/pkg/db/template" "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/pkg/mutil/mzap" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" "go.uber.org/zap" ) type AccountBoundDBImp[T model.AccountBoundStorable] struct { Logger mlogger.Logger DBImp *template.DBImp[T] Enforcer Enforcer PermissionRef primitive.ObjectID Collection mservice.Type } func (db *AccountBoundDBImp[T]) enforce(ctx context.Context, action model.Action, object model.AccountBoundStorable, accountRef primitive.ObjectID) error { // FIRST: Check if the object's AccountRef equals the calling accountRef - if so, ALLOW objectAccountRef := object.GetAccountRef() if objectAccountRef != nil && *objectAccountRef == accountRef { db.Logger.Debug("Access granted - object belongs to calling account", mzap.ObjRef("object_account_ref", *objectAccountRef), mzap.ObjRef("calling_account_ref", accountRef), zap.String("action", string(action))) return nil } // SECOND: If not owned by calling account, check organization-level permissions organizationRef := object.GetOrganizationRef() res, err := db.Enforcer.Enforce(ctx, db.PermissionRef, accountRef, organizationRef, organizationRef, action) if err != nil { db.Logger.Warn("Failed to enforce permission", zap.Error(err), mzap.ObjRef("permission_ref", db.PermissionRef), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef), zap.String("action", string(action))) return err } if !res { db.Logger.Debug("Access denied", mzap.ObjRef("permission_ref", db.PermissionRef), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef), zap.String("action", string(action))) return merrors.AccessDenied(db.Collection, string(action), primitive.NilObjectID) } return nil } func (db *AccountBoundDBImp[T]) enforceInterface(ctx context.Context, action model.Action, object model.AccountBoundStorable, accountRef primitive.ObjectID) error { // FIRST: Check if the object's AccountRef equals the calling accountRef - if so, ALLOW objectAccountRef := object.GetAccountRef() if objectAccountRef != nil && *objectAccountRef == accountRef { db.Logger.Debug("Access granted - object belongs to calling account", mzap.ObjRef("object_account_ref", *objectAccountRef), mzap.ObjRef("calling_account_ref", accountRef), zap.String("action", string(action))) return nil } // SECOND: If not owned by calling account, check organization-level permissions organizationRef := object.GetOrganizationRef() res, err := db.Enforcer.Enforce(ctx, db.PermissionRef, accountRef, organizationRef, organizationRef, action) if err != nil { db.Logger.Warn("Failed to enforce permission", zap.Error(err), mzap.ObjRef("permission_ref", db.PermissionRef), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef), zap.String("action", string(action))) return err } if !res { db.Logger.Debug("Access denied", mzap.ObjRef("permission_ref", db.PermissionRef), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef), zap.String("action", string(action))) return merrors.AccessDenied(db.Collection, string(action), primitive.NilObjectID) } return nil } func (db *AccountBoundDBImp[T]) Create(ctx context.Context, accountRef primitive.ObjectID, object T) error { orgRef := object.GetOrganizationRef() db.Logger.Debug("Attempting to create object", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", orgRef), zap.String("collection", string(db.Collection))) // Check organization update permission for create operations if err := db.enforce(ctx, model.ActionUpdate, object, accountRef); err != nil { return err } if err := db.DBImp.Create(ctx, object); err != nil { db.Logger.Warn("Failed to create object", zap.Error(err), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", orgRef), zap.String("collection", string(db.Collection))) return err } db.Logger.Debug("Successfully created object", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", orgRef), zap.String("collection", string(db.Collection))) return nil } func (db *AccountBoundDBImp[T]) Get(ctx context.Context, accountRef, objectRef primitive.ObjectID, result T) error { db.Logger.Debug("Attempting to get object", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef)) // First get the object to check its organization if err := db.DBImp.Get(ctx, objectRef, result); err != nil { db.Logger.Warn("Failed to get object", zap.Error(err), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.String("collection", string(db.Collection))) return err } // Check organization read permission if err := db.enforce(ctx, model.ActionRead, result, accountRef); err != nil { return err } db.Logger.Debug("Successfully retrieved object", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", result.GetOrganizationRef()), zap.String("collection", string(db.Collection))) return nil } func (db *AccountBoundDBImp[T]) Update(ctx context.Context, accountRef primitive.ObjectID, object T) error { db.Logger.Debug("Attempting to update object", mzap.ObjRef("account_ref", accountRef), mzap.StorableRef(object)) // Check organization update permission if err := db.enforce(ctx, model.ActionUpdate, object, accountRef); err != nil { return err } if err := db.DBImp.Update(ctx, object); err != nil { db.Logger.Warn("Failed to update object", zap.Error(err), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", object.GetOrganizationRef()), mzap.StorableRef(object)) return err } db.Logger.Debug("Successfully updated object", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", object.GetOrganizationRef()), mzap.StorableRef(object)) return nil } func (db *AccountBoundDBImp[T]) Patch(ctx context.Context, accountRef, objectRef primitive.ObjectID, patch builder.Patch) error { db.Logger.Debug("Attempting to patch object", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef)) // First get the object to check its organization objs, err := db.DBImp.Repository.ListAccountBound(ctx, repository.IDFilter(objectRef)) if err != nil { db.Logger.Warn("Failed to get object for permission check when deleting", zap.Error(err), mzap.ObjRef("object_ref", objectRef)) return err } if len(objs) == 0 { db.Logger.Debug("Permission denied for deletion", mzap.ObjRef("object_ref", objectRef), mzap.ObjRef("account_ref", accountRef)) return merrors.AccessDenied(db.Collection, string(model.ActionDelete), objectRef) } // Check organization update permission if err := db.enforce(ctx, model.ActionUpdate, objs[0], accountRef); err != nil { return err } if err := db.DBImp.Patch(ctx, objectRef, patch); err != nil { db.Logger.Warn("Failed to patch object", zap.Error(err), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.String("collection", string(db.Collection))) return err } db.Logger.Debug("Successfully patched object", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef)) return nil } func (db *AccountBoundDBImp[T]) Delete(ctx context.Context, accountRef, objectRef primitive.ObjectID) error { db.Logger.Debug("Attempting to delete object", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef)) // First get the object to check its organization objs, err := db.DBImp.Repository.ListAccountBound(ctx, repository.IDFilter(objectRef)) if err != nil { db.Logger.Warn("Failed to get object for permission check when deleting", zap.Error(err), mzap.ObjRef("object_ref", objectRef)) return err } if len(objs) == 0 { db.Logger.Debug("Permission denied for deletion", mzap.ObjRef("object_ref", objectRef), mzap.ObjRef("account_ref", accountRef)) return merrors.AccessDenied(db.Collection, string(model.ActionDelete), objectRef) } // Check organization update permission for delete operations if err := db.enforce(ctx, model.ActionUpdate, objs[0], accountRef); err != nil { return err } if err := db.DBImp.Delete(ctx, objectRef); err != nil { db.Logger.Warn("Failed to delete object", zap.Error(err), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.String("collection", string(db.Collection))) return err } db.Logger.Debug("Successfully deleted object", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef)) return nil } func (db *AccountBoundDBImp[T]) DeleteMany(ctx context.Context, accountRef primitive.ObjectID, query builder.Query) error { db.Logger.Debug("Attempting to delete many objects", mzap.ObjRef("account_ref", accountRef), zap.String("collection", string(db.Collection))) // Get all candidate objects for batch permission checking allObjects, err := db.DBImp.Repository.ListPermissionBound(ctx, query) if err != nil { db.Logger.Warn("Failed to list objects for delete many", zap.Error(err), mzap.ObjRef("account_ref", accountRef)) return err } // Use batch enforcement for efficiency allowedResults, err := db.Enforcer.EnforceBatch(ctx, allObjects, accountRef, model.ActionUpdate) if err != nil { db.Logger.Warn("Failed to enforce batch permissions for delete many", zap.Error(err), mzap.ObjRef("account_ref", accountRef)) return err } // Build query for objects that passed permission check var allowedIDs []primitive.ObjectID for _, obj := range allObjects { if allowedResults[*obj.GetID()] { allowedIDs = append(allowedIDs, *obj.GetID()) } } if len(allowedIDs) == 0 { db.Logger.Debug("No objects allowed for deletion", mzap.ObjRef("account_ref", accountRef)) return nil } // Delete only the allowed objects allowedQuery := query.And(repository.Query().In(repository.IDField(), allowedIDs)) if err := db.DBImp.DeleteMany(ctx, allowedQuery); err != nil { db.Logger.Warn("Failed to delete many objects", zap.Error(err), mzap.ObjRef("account_ref", accountRef)) return err } db.Logger.Debug("Successfully deleted many objects", mzap.ObjRef("account_ref", accountRef), zap.Int("count", len(allowedIDs))) return nil } func (db *AccountBoundDBImp[T]) FindOne(ctx context.Context, accountRef primitive.ObjectID, query builder.Query, result T) error { db.Logger.Debug("Attempting to find one object", mzap.ObjRef("account_ref", accountRef), zap.String("collection", string(db.Collection))) // For FindOne, we need to check read permission after finding the object if err := db.DBImp.FindOne(ctx, query, result); err != nil { db.Logger.Warn("Failed to find one object", zap.Error(err), mzap.ObjRef("account_ref", accountRef)) return err } // Check organization read permission for the found object if err := db.enforce(ctx, model.ActionRead, result, accountRef); err != nil { return err } db.Logger.Debug("Successfully found one object", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", result.GetOrganizationRef())) return nil } func (db *AccountBoundDBImp[T]) ListIDs(ctx context.Context, accountRef primitive.ObjectID, query builder.Query) ([]primitive.ObjectID, error) { db.Logger.Debug("Attempting to list object IDs", mzap.ObjRef("account_ref", accountRef), zap.String("collection", string(db.Collection))) // Get all candidate objects for batch permission checking allObjects, err := db.DBImp.Repository.ListPermissionBound(ctx, query) if err != nil { db.Logger.Warn("Failed to list objects for ID filtering", zap.Error(err), mzap.ObjRef("account_ref", accountRef)) return nil, err } // Use batch enforcement for efficiency allowedResults, err := db.Enforcer.EnforceBatch(ctx, allObjects, accountRef, model.ActionRead) if err != nil { db.Logger.Warn("Failed to enforce batch permissions for ID listing", zap.Error(err), mzap.ObjRef("account_ref", accountRef)) return nil, err } // Filter to only allowed object IDs var allowedIDs []primitive.ObjectID for _, obj := range allObjects { if allowedResults[*obj.GetID()] { allowedIDs = append(allowedIDs, *obj.GetID()) } } db.Logger.Debug("Successfully filtered object IDs", zap.Int("total_count", len(allObjects)), zap.Int("allowed_count", len(allowedIDs)), mzap.ObjRef("account_ref", accountRef)) return allowedIDs, nil } func (db *AccountBoundDBImp[T]) ListAccountBound(ctx context.Context, accountRef, organizationRef primitive.ObjectID, query builder.Query) ([]model.AccountBoundStorable, error) { db.Logger.Debug("Attempting to list account bound objects", mzap.ObjRef("account_ref", accountRef), zap.String("collection", string(db.Collection))) // Build query to find objects where accountRef matches OR is null/absent accountQuery := repository.WithOrg(accountRef, organizationRef) // Combine with the provided query finalQuery := query.And(accountQuery) // Get all candidate objects allObjects, err := db.DBImp.Repository.ListAccountBound(ctx, finalQuery) if err != nil { db.Logger.Warn("Failed to list account bound objects", zap.Error(err), mzap.ObjRef("account_ref", accountRef)) return nil, err } // Filter objects based on read permissions (AccountBoundStorable doesn't have permission info, so we check organization level) var allowedObjects []model.AccountBoundStorable for _, obj := range allObjects { if err := db.enforceInterface(ctx, model.ActionRead, obj, accountRef); err == nil { allowedObjects = append(allowedObjects, obj) } else if !errors.Is(err, merrors.ErrAccessDenied) { // If the error is something other than AccessDenied, we want to fail db.Logger.Warn("Error while enforcing read permission", zap.Error(err), mzap.ObjRef("object_ref", *obj.GetID())) return nil, err } // If AccessDenied, we simply skip that object } db.Logger.Debug("Successfully filtered account bound objects", zap.Int("total_count", len(allObjects)), zap.Int("allowed_count", len(allowedObjects)), mzap.ObjRef("account_ref", accountRef)) return allowedObjects, nil } func (db *AccountBoundDBImp[T]) GetByAccountRef(ctx context.Context, accountRef primitive.ObjectID, result T) error { db.Logger.Debug("Attempting to get object by account ref", mzap.ObjRef("account_ref", accountRef)) // Build query to find objects where accountRef matches OR is null/absent query := repository.WithoutOrg(accountRef) if err := db.DBImp.FindOne(ctx, query, result); err != nil { db.Logger.Warn("Failed to get object by account ref", zap.Error(err), mzap.ObjRef("account_ref", accountRef)) return err } // Check organization read permission for the found object if err := db.enforce(ctx, model.ActionRead, result, accountRef); err != nil { return err } db.Logger.Debug("Successfully retrieved object by account ref", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", result.GetOrganizationRef())) return nil } func (db *AccountBoundDBImp[T]) DeleteByAccountRef(ctx context.Context, accountRef primitive.ObjectID) error { db.Logger.Debug("Attempting to delete objects by account ref", mzap.ObjRef("account_ref", accountRef)) // Build query to find objects where accountRef matches OR is null/absent query := repository.WithoutOrg(accountRef) // Get all candidate objects for individual permission checking allObjects, err := db.DBImp.Repository.ListAccountBound(ctx, query) if err != nil { db.Logger.Warn("Failed to list objects for delete by account ref", zap.Error(err), mzap.ObjRef("account_ref", accountRef)) return err } // Check permissions for each object individually (AccountBoundStorable doesn't have permission info) var allowedIDs []primitive.ObjectID for _, obj := range allObjects { if err := db.enforceInterface(ctx, model.ActionUpdate, obj, accountRef); err == nil { allowedIDs = append(allowedIDs, *obj.GetID()) } else if !errors.Is(err, merrors.ErrAccessDenied) { // If the error is something other than AccessDenied, we want to fail db.Logger.Warn("Error while enforcing update permission", zap.Error(err), mzap.ObjRef("object_ref", *obj.GetID())) return err } // If AccessDenied, we simply skip that object } if len(allowedIDs) == 0 { db.Logger.Debug("No objects allowed for deletion by account ref", mzap.ObjRef("account_ref", accountRef)) return nil } // Delete only the allowed objects allowedQuery := query.And(repository.Query().In(repository.IDField(), allowedIDs)) if err := db.DBImp.DeleteMany(ctx, allowedQuery); err != nil { db.Logger.Warn("Failed to delete objects by account ref", zap.Error(err), mzap.ObjRef("account_ref", accountRef)) return err } db.Logger.Debug("Successfully deleted objects by account ref", mzap.ObjRef("account_ref", accountRef), zap.Int("count", len(allowedIDs))) return nil } func (db *AccountBoundDBImp[T]) DeleteCascade(ctx context.Context, objectRef primitive.ObjectID) error { return db.DBImp.DeleteCascade(ctx, objectRef) } // CreateAccountBoundImp creates a concrete AccountBoundDBImp instance for internal use func CreateAccountBoundImp[T model.AccountBoundStorable]( ctx context.Context, logger mlogger.Logger, pdb policy.DB, enforcer Enforcer, collection mservice.Type, db *mongo.Database, ) (*AccountBoundDBImp[T], error) { logger = logger.Named("account_bound") var policy model.PolicyDescription if err := pdb.GetBuiltInPolicy(ctx, mservice.Organizations, &policy); err != nil { logger.Warn("Failed to fetch organization policy description", zap.Error(err)) return nil, err } res := &AccountBoundDBImp[T]{ Logger: logger, DBImp: template.Create[T](logger, collection, db), Enforcer: enforcer, PermissionRef: policy.ID, Collection: collection, } return res, nil }