service backend
All checks were successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful

This commit is contained in:
Stephan D
2025-11-07 18:35:26 +01:00
parent 20e8f9acc4
commit 62a6631b9a
537 changed files with 48453 additions and 0 deletions

View File

@@ -0,0 +1,37 @@
package mutil
import (
"context"
"github.com/tech/sendico/pkg/db/repository"
"github.com/tech/sendico/pkg/db/storable"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mutil/mzap"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
)
func SetArchived[T storable.Storable](ctx context.Context, logger mlogger.Logger, newArchived bool, objectRef primitive.ObjectID, repo repository.Repository) error {
objs, err := GetObjects[T](ctx, logger, repository.IDFilter(objectRef), nil, repo)
if err != nil {
logger.Warn("Failed to fetch object", zap.Error(err), mzap.ObjRef("object_ref", objectRef))
return err
}
if len(objs) == 0 {
logger.Warn("No objects found to archive", mzap.ObjRef("object_ref", objectRef))
return nil
}
// Archive the first object found
obj := objs[0]
if archivable, ok := any(obj).(interface{ SetArchived(bool) }); ok {
archivable.SetArchived(newArchived)
if err := repo.Update(ctx, obj); err != nil {
logger.Warn("Failed to update object archived status", zap.Error(err), mzap.ObjRef("object_ref", objectRef))
return err
}
}
return nil
}

26
api/pkg/mutil/db/array.go Normal file
View File

@@ -0,0 +1,26 @@
package mutil
import (
"context"
"github.com/tech/sendico/pkg/db/repository"
"github.com/tech/sendico/pkg/db/repository/builder"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
"go.mongodb.org/mongo-driver/mongo"
"go.uber.org/zap"
)
func GetObjects[T any](ctx context.Context, logger mlogger.Logger, filter builder.Query, cursor *model.ViewCursor, repo repository.Repository) ([]T, error) {
entities := make([]T, 0)
decoder := func(cur *mongo.Cursor) error {
var next T
if e := cur.Decode(&next); e != nil {
logger.Warn("Failed to decode entity", zap.Error(e))
return e
}
entities = append(entities, next)
return nil
}
return entities, repo.FindManyByFilter(ctx, repository.ApplyCursor(filter, cursor), decoder)
}

View File

@@ -0,0 +1,89 @@
// Package mutil provides utility functions for working with account-bound objects
// with permission enforcement.
//
// Example usage:
//
// // Using the low-level repository approach
// objects, err := mutil.GetAccountBoundObjects[model.ProjectFilter](
// ctx, logger, accountRef, orgRef, model.ActionRead,
// repository.Query(), &model.ViewCursor{Limit: &limit, Offset: &offset, IsArchived: &isArchived},
// enforcer, repo)
//
// // Using the AccountBoundDB interface approach
// objects, err := mutil.GetAccountBoundObjectsFromDB[model.ProjectFilter](
// ctx, logger, accountRef, orgRef,
// repository.Query(), &model.ViewCursor{Limit: &limit, Offset: &offset, IsArchived: &isArchived},
// accountBoundDB)
package mutil
import (
"context"
"errors"
"github.com/tech/sendico/pkg/auth"
"github.com/tech/sendico/pkg/db/repository"
"github.com/tech/sendico/pkg/db/repository/builder"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
mutil "github.com/tech/sendico/pkg/mutil/db"
"github.com/tech/sendico/pkg/mutil/mzap"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
)
// GetAccountBoundObjects retrieves account-bound objects with permission enforcement.
// This function handles the complex logic of:
// 1. Finding objects where accountRef matches OR is null/absent
// 2. Checking organization-level permissions for each object
// 3. Filtering to only objects the account has permission to read
func GetAccountBoundObjects[T any](
ctx context.Context,
logger mlogger.Logger,
accountRef, organizationRef primitive.ObjectID,
filter builder.Query,
cursor *model.ViewCursor,
enforcer auth.Enforcer,
repo repository.Repository,
) ([]T, error) {
// Build query to find objects where accountRef matches OR is null/absent
accountQuery := repository.WithOrg(accountRef, organizationRef)
// Get all account-bound objects that match the criteria
allObjects, err := repo.ListAccountBound(ctx, repository.ApplyCursor(accountQuery, cursor))
if err != nil {
if !errors.Is(err, merrors.ErrNoData) {
logger.Warn("Failed to fetch account bound objects", zap.Error(err),
mzap.ObjRef("account_ref", accountRef),
mzap.ObjRef("organization_ref", organizationRef),
)
} else {
logger.Debug("No matching account bound objects found", zap.Error(err),
mzap.ObjRef("account_ref", accountRef),
mzap.ObjRef("organization_ref", organizationRef),
)
}
return nil, err
}
if len(allObjects) == 0 {
return nil, merrors.NoData("no_account_bound_objects_found")
}
allowed := make([]primitive.ObjectID, 0, len(allObjects))
for _, ref := range allObjects {
allowed = append(allowed, *ref.GetID())
}
if len(allowed) == 0 {
return nil, merrors.NoData("no_data_found_or_allowed")
}
logger.Debug("Successfully retrieved account bound objects",
zap.Int("total_count", len(allObjects)),
mzap.ObjRef("account_ref", accountRef),
mzap.ObjRef("organization_ref", organizationRef),
zap.Any("objs", allObjects),
)
return mutil.GetObjects[T](ctx, logger, repository.Query().In(repository.IDField(), allowed), cursor, repo)
}

View File

@@ -0,0 +1,58 @@
package mutil
import (
"context"
"errors"
"github.com/tech/sendico/pkg/auth"
"github.com/tech/sendico/pkg/db/repository"
"github.com/tech/sendico/pkg/db/repository/builder"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
mutil "github.com/tech/sendico/pkg/mutil/db"
"github.com/tech/sendico/pkg/mutil/mzap"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
)
func GetProtectedObjects[T any](
ctx context.Context,
logger mlogger.Logger,
accountRef, organizationRef primitive.ObjectID,
action model.Action,
filter builder.Query,
cursor *model.ViewCursor,
enforcer auth.Enforcer,
repo repository.Repository,
) ([]T, error) {
refs, err := repo.ListPermissionBound(ctx, repository.ApplyCursor(filter, cursor))
if err != nil {
if !errors.Is(err, merrors.ErrNoData) {
logger.Warn("Failed to fetch object IDs", zap.Error(err), mzap.ObjRef("account_ref", accountRef),
mzap.ObjRef("organization_ref", organizationRef), zap.String("action", string(action)))
} else {
logger.Debug("No matching IDs found", zap.Error(err), mzap.ObjRef("account_ref", accountRef),
mzap.ObjRef("organization_ref", organizationRef), zap.String("action", string(action)))
}
return nil, err
}
res, err := enforcer.EnforceBatch(ctx, refs, accountRef, action)
if err != nil {
logger.Warn("Failed to enforce object IDs", zap.Error(err), mzap.ObjRef("account_ref", accountRef),
mzap.ObjRef("organization_ref", organizationRef), zap.String("action", string(action)))
return nil, err
}
allowed := make([]primitive.ObjectID, 0, len(res))
for _, ref := range refs {
if ok := res[*ref.GetID()]; ok {
allowed = append(allowed, *ref.GetID())
}
}
if len(allowed) == 0 {
return nil, merrors.NoData("no_data_found_or_allowed")
}
return mutil.GetObjects[T](ctx, logger, repository.Query().In(repository.IDField(), allowed), cursor, repo)
}

20
api/pkg/mutil/db/db.go Normal file
View File

@@ -0,0 +1,20 @@
package mutil
import (
"context"
"github.com/tech/sendico/pkg/db/repository"
"github.com/tech/sendico/pkg/db/storable"
"github.com/tech/sendico/pkg/mlogger"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
)
func GetObjectByID(ctx context.Context, logger mlogger.Logger, id string, val storable.Storable, repo repository.Repository) error {
p, err := primitive.ObjectIDFromHex(id)
if err != nil {
logger.Warn("Failed to decode object reference", zap.String("reference", id), zap.String("collection", val.Collection()))
return err
}
return repo.Get(ctx, p, val)
}