Files
sxchnge/backend/pkg/auth/dbimpscopedpatch.go
T

130 lines
4.8 KiB
Go

package auth
import (
"context"
"errors"
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository"
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository/builder"
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
"git.sendico.io/tech/sxchnge/bff/pkg/model"
"git.sendico.io/tech/sxchnge/bff/pkg/mutil/mzap"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
)
// Patch applies a partial update after verifying ActionUpdate.
func (db *ScopedDBImp[T]) Patch(ctx context.Context, accountRef, objectRef bson.ObjectID, patch builder.Patch) error {
db.DBImp.Logger.Debug("attempting to patch scoped object",
mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))
if err := denyImmutablePatch(patch); err != nil {
return err
}
if _, err := db.loadAndEnforce(ctx, accountRef, objectRef, model.ActionUpdate); err != nil {
return err
}
if err := db.DBImp.Patch(ctx, objectRef, patch); err != nil {
db.DBImp.Logger.Warn("failed to patch scoped object",
zap.Error(err), mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))
return err
}
return nil
}
// SetArchived patches the isArchived field after verifying ActionUpdate.
//
// organizationRef is accepted for interface compatibility but not used for
// enforcement — ownership is read from the DB via LoadAndEnforce.
// cascade is accepted but not applied; resources that require cascading archived
// status must implement cascade logic in a dedicated wrapper.
func (db *ScopedDBImp[T]) SetArchived(ctx context.Context, accountRef, _ bson.ObjectID, objectRef bson.ObjectID, isArchived, _ bool) error {
db.DBImp.Logger.Debug("attempting to change archive status of scoped object",
mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef),
zap.Bool("archived", isArchived))
if _, err := db.loadAndEnforce(ctx, accountRef, objectRef, model.ActionUpdate); err != nil {
return err
}
patch := repository.Patch().Set(repository.IsArchivedField(), isArchived)
if err := db.DBImp.Patch(ctx, objectRef, patch); err != nil {
db.DBImp.Logger.Warn("failed to change archive status of scoped object",
zap.Error(err), mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))
return err
}
return nil
}
// ListIDs returns the authorized subset of IDs matching query for the given
// action. Each candidate is checked via LoadAndEnforce; unauthorized IDs are
// silently skipped. Non-access-denied errors abort the call.
func (db *ScopedDBImp[T]) ListIDs(ctx context.Context, action model.Action, accountRef bson.ObjectID, query builder.Query) ([]bson.ObjectID, error) {
db.DBImp.Logger.Debug("attempting to list scoped object IDs",
mzap.AccRef(accountRef), zap.String("collection", db.Collection))
rawIDs, err := db.DBImp.ListIDs(ctx, query)
if err != nil {
db.DBImp.Logger.Warn("failed to list raw object IDs",
zap.Error(err), mzap.AccRef(accountRef), zap.String("collection", db.Collection))
return nil, err
}
allowed := make([]bson.ObjectID, 0, len(rawIDs))
for _, id := range rawIDs {
if _, authErr := db.loadAndEnforce(ctx, accountRef, id, action); authErr == nil {
allowed = append(allowed, id)
} else if !errors.Is(authErr, merrors.ErrAccessDenied) {
db.DBImp.Logger.Warn("enforcer error during ListIDs",
zap.Error(authErr), mzap.AccRef(accountRef), mzap.ObjRef("object_ref", id))
return nil, authErr
}
// ErrAccessDenied: silently skip
}
db.DBImp.Logger.Debug("completed ListIDs",
mzap.AccRef(accountRef), zap.Int("total", len(rawIDs)), zap.Int("allowed", len(allowed)))
return allowed, nil
}
// PatchMany applies patch to all resources in query that accountRef is
// authorized to update. Authorization is resolved via ListIDs; the write is a
// single bulk operation using originalQuery AND _id IN (authorizedIDs) so
// records that leave the original query between the read and write are not
// inadvertently mutated.
func (db *ScopedDBImp[T]) PatchMany(ctx context.Context, accountRef bson.ObjectID, query builder.Query, patch builder.Patch) (int, error) {
db.DBImp.Logger.Debug("attempting to patch many scoped objects",
mzap.AccRef(accountRef), zap.String("collection", db.Collection))
if err := denyImmutablePatch(patch); err != nil {
return 0, err
}
ids, err := db.ListIDs(ctx, model.ActionUpdate, accountRef, query)
if err != nil {
return 0, err
}
if len(ids) == 0 {
return 0, nil
}
values := make([]any, len(ids))
for i, id := range ids {
values[i] = id
}
idFilter := repository.Query().In(repository.IDField(), values...)
finalQuery := idFilter
if query != nil {
finalQuery = query.And(idFilter)
}
modified, err := db.DBImp.PatchMany(ctx, finalQuery, patch)
if err != nil {
db.DBImp.Logger.Warn("failed to patch many scoped objects",
zap.Error(err), mzap.AccRef(accountRef))
return 0, err
}
db.DBImp.Logger.Debug("patched many scoped objects",
mzap.AccRef(accountRef), zap.Int("count", modified))
return modified, nil
}