Bootstrap copied BFF and local accounts with public Sendico contracts (#34)

This commit is contained in:
Stephan D committed 2026-09-12 13:37:48 +02:00
1 parent 67dc5b5b1e
commit 7062672a87
556 files changed
+42291 -777

No files matched your search

+848
View File
@@ -0,0 +1,848 @@
package auth
import (
"context"
"errors"
"fmt"
"testing"
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository/builder"
rd "git.sendico.io/tech/sxchnge/bff/pkg/db/repository/decoder"
ri "git.sendico.io/tech/sxchnge/bff/pkg/db/repository/index"
"git.sendico.io/tech/sxchnge/bff/pkg/db/storable"
"git.sendico.io/tech/sxchnge/bff/pkg/db/template"
"git.sendico.io/tech/sxchnge/bff/pkg/db/transaction"
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
"git.sendico.io/tech/sxchnge/bff/pkg/model"
"git.sendico.io/tech/sxchnge/bff/pkg/mservice"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
)
// testScopedObj is a minimal model.ScopedStorable used exclusively by
// ScopedDBImp tests.
type testScopedObj struct {
id bson.ObjectID
orgRef bson.ObjectID
ownerKind model.OwnerKind
ownerRef bson.ObjectID
permissionRef bson.ObjectID
}
func (o *testScopedObj) GetID() *bson.ObjectID { return &o.id }
func (o *testScopedObj) SetID(id bson.ObjectID) { o.id = id }
func (o *testScopedObj) Update() {}
func (o *testScopedObj) Collection() string { return "test_scoped" }
func (o *testScopedObj) GetOrganizationRef() bson.ObjectID { return o.orgRef }
func (o *testScopedObj) SetOrganizationRef(ref bson.ObjectID) { o.orgRef = ref }
func (o *testScopedObj) GetOwnerKind() model.OwnerKind { return o.ownerKind }
func (o *testScopedObj) GetOwnerRef() bson.ObjectID { return o.ownerRef }
func (o *testScopedObj) SetOwnerKind(k model.OwnerKind) { o.ownerKind = k }
func (o *testScopedObj) SetOwnerRef(ref bson.ObjectID) { o.ownerRef = ref }
func (o *testScopedObj) GetPermissionRef() bson.ObjectID { return o.permissionRef }
func (o *testScopedObj) SetPermissionRef(ref bson.ObjectID) { o.permissionRef = ref }
var _ model.ScopedStorable = (*testScopedObj)(nil)
// fakeScopedRepository is a minimal in-memory repository.Repository that
// supports Insert, InsertMany, Get, Update, Patch, Delete, and ListIDs.
// All other methods return merrors.NotImplemented.
type fakeScopedRepository struct {
objects map[bson.ObjectID]*testScopedObj
insertErr error
getErr error
updateErr error
patchErr error
deleteErr error
listIDsErr error
patchedIDs []bson.ObjectID
// patchManyIDs is the set of IDs that PatchMany will record as affected.
// Tests using PatchMany must set this to the expected authorized IDs so the
// fake can simulate per-ID tracking without parsing the bulk filter.
patchManyIDs []bson.ObjectID
}
func newFakeScopedRepository() *fakeScopedRepository {
return &fakeScopedRepository{objects: make(map[bson.ObjectID]*testScopedObj)}
}
func (f *fakeScopedRepository) Insert(_ context.Context, obj storable.Storable, _ builder.Query) error {
if f.insertErr != nil {
return f.insertErr
}
o := obj.(*testScopedObj)
if o.id == bson.NilObjectID {
o.id = bson.NewObjectID()
}
clone := *o
f.objects[o.id] = &clone
return nil
}
func (f *fakeScopedRepository) InsertMany(_ context.Context, objects []storable.Storable) error {
if f.insertErr != nil {
return f.insertErr
}
for _, obj := range objects {
o := obj.(*testScopedObj)
if o.id == bson.NilObjectID {
o.id = bson.NewObjectID()
}
clone := *o
f.objects[o.id] = &clone
}
return nil
}
func (f *fakeScopedRepository) Get(_ context.Context, id bson.ObjectID, result storable.Storable) error {
if f.getErr != nil {
return f.getErr
}
obj, ok := f.objects[id]
if !ok {
return merrors.NoData(fmt.Sprintf("test_scoped %s not found", id.Hex()))
}
*result.(*testScopedObj) = *obj
return nil
}
func (f *fakeScopedRepository) Update(_ context.Context, obj storable.Storable) error {
if f.updateErr != nil {
return f.updateErr
}
o := obj.(*testScopedObj)
clone := *o
f.objects[*o.GetID()] = &clone
return nil
}
func (f *fakeScopedRepository) Patch(_ context.Context, id bson.ObjectID, _ builder.Patch) error {
if f.patchErr != nil {
return f.patchErr
}
f.patchedIDs = append(f.patchedIDs, id)
return nil
}
func (f *fakeScopedRepository) Delete(_ context.Context, id bson.ObjectID) error {
if f.deleteErr != nil {
return f.deleteErr
}
delete(f.objects, id)
return nil
}
func (f *fakeScopedRepository) ListIDs(_ context.Context, _ builder.Query) ([]bson.ObjectID, error) {
if f.listIDsErr != nil {
return nil, f.listIDsErr
}
ids := make([]bson.ObjectID, 0, len(f.objects))
for id := range f.objects {
ids = append(ids, id)
}
return ids, nil
}
// Unimplemented repository.Repository stubs.
func (f *fakeScopedRepository) Aggregate(context.Context, builder.Pipeline, rd.DecodingFunc) error {
return merrors.NotImplemented("aggregate")
}
func (f *fakeScopedRepository) FindOneByFilter(context.Context, builder.Query, storable.Storable) error {
return merrors.NotImplemented("FindOneByFilter")
}
func (f *fakeScopedRepository) FindFirstByFilter(context.Context, builder.Query, storable.Storable) error {
return merrors.NotImplemented("FindFirstByFilter")
}
func (f *fakeScopedRepository) FindManyByFilter(context.Context, builder.Query, rd.DecodingFunc) error {
return merrors.NotImplemented("FindManyByFilter")
}
func (f *fakeScopedRepository) FindAllByFilter(context.Context, builder.Query, any) error {
return merrors.NotImplemented("FindAllByFilter")
}
func (f *fakeScopedRepository) CountByFilter(context.Context, builder.Query) (int64, error) {
return 0, merrors.NotImplemented("CountByFilter")
}
func (f *fakeScopedRepository) ReplaceOneByFilter(context.Context, builder.Query, storable.Storable) (bool, error) {
return false, merrors.NotImplemented("ReplaceOneByFilter")
}
func (f *fakeScopedRepository) Upsert(context.Context, storable.Storable) error {
return merrors.NotImplemented("Upsert")
}
func (f *fakeScopedRepository) PatchOneByFilter(context.Context, builder.Query, builder.Patch, storable.Storable) (bool, error) {
return false, merrors.NotImplemented("PatchOneByFilter")
}
func (f *fakeScopedRepository) PatchMany(_ context.Context, _ builder.Query, _ builder.Patch) (int, error) {
f.patchedIDs = append(f.patchedIDs, f.patchManyIDs...)
return len(f.patchManyIDs), nil
}
func (f *fakeScopedRepository) DeleteMany(context.Context, builder.Query) error {
return merrors.NotImplemented("DeleteMany")
}
func (f *fakeScopedRepository) CreateIndex(*ri.Definition) error { return nil }
func (f *fakeScopedRepository) DropIndex(context.Context, string) error {
return merrors.NotImplemented("DropIndex")
}
func (f *fakeScopedRepository) ListIndexDefinitions(context.Context) ([]*ri.Definition, error) {
return nil, merrors.NotImplemented("ListIndexDefinitions")
}
func (f *fakeScopedRepository) ListPermissionBound(context.Context, builder.Query) ([]model.PermissionBoundStorable, error) {
return nil, merrors.NotImplemented("ListPermissionBound")
}
func (f *fakeScopedRepository) ListAccountBound(context.Context, builder.Query) ([]model.AccountBoundStorable, error) {
return nil, merrors.NotImplemented("ListAccountBound")
}
func (f *fakeScopedRepository) WithTransaction(_ context.Context, cb transaction.Callback) (any, error) {
return cb(context.Background())
}
func (f *fakeScopedRepository) Collection() string { return "test_scoped" }
// newTestScopedDBImp wires a ScopedDBImp backed by the given in-memory repo,
// membership, and enforcer. The orgPolicyRef is returned so callers can verify
// it was applied correctly.
func newTestScopedDBImp(repo *fakeScopedRepository, membership *fakeMembership, enforcer Enforcer) (*ScopedDBImp[*testScopedObj], bson.ObjectID) {
orgPolicyRef := bson.NewObjectID()
dbImp := &template.DBImp[*testScopedObj]{
Logger: zap.NewNop(),
Repository: repo,
}
dbImp.SetDeleter(func(_ context.Context, id bson.ObjectID) error {
return repo.Delete(context.Background(), id)
})
imp := NewScopedDBImp[*testScopedObj](
dbImp,
enforcer,
membership,
orgPolicyRef,
mservice.Type("test_scoped"),
func() *testScopedObj { return &testScopedObj{} },
zap.NewNop(),
)
return imp, orgPolicyRef
}
// ── Create ────────────────────────────────────────────────────────────────────
func TestScopedDBImp_Create_Personal_SkipsPolicy(t *testing.T) {
caller := bson.NewObjectID()
orgRef := bson.NewObjectID()
repo := newFakeScopedRepository()
enf := &fakeEnforcer{}
db, _ := newTestScopedDBImp(repo, &fakeMembership{member: true}, enf)
obj := &testScopedObj{ownerKind: model.OwnerKindAccount}
if err := db.Create(context.Background(), caller, orgRef, obj); err != nil {
t.Fatalf("expected success, got %v", err)
}
if enf.called {
t.Fatal("personal create must not consult the enforcer")
}
if obj.GetOwnerRef() != caller {
t.Fatalf("expected ownerRef=%s, got %s", caller.Hex(), obj.GetOwnerRef().Hex())
}
if obj.GetOrganizationRef() != orgRef {
t.Fatal("organizationRef must be set on create")
}
}
func TestScopedDBImp_Create_Personal_DefaultsToAccountWhenKindMissing(t *testing.T) {
caller := bson.NewObjectID()
orgRef := bson.NewObjectID()
repo := newFakeScopedRepository()
enf := &fakeEnforcer{}
db, _ := newTestScopedDBImp(repo, &fakeMembership{member: true}, enf)
obj := &testScopedObj{} // ownerKind is zero value (invalid)
if err := db.Create(context.Background(), caller, orgRef, obj); err != nil {
t.Fatalf("expected success with defaulted ownerKind, got %v", err)
}
if obj.GetOwnerKind() != model.OwnerKindAccount {
t.Fatalf("expected ownerKind=account, got %s", obj.GetOwnerKind())
}
if enf.called {
t.Fatal("must not consult enforcer when defaulting to personal")
}
}
func TestScopedDBImp_Create_Personal_DeniedWhenCallerIsNotMember(t *testing.T) {
caller := bson.NewObjectID()
orgRef := bson.NewObjectID()
repo := newFakeScopedRepository()
membership := &fakeMembership{member: false}
enf := &fakeEnforcer{allow: true}
db, _ := newTestScopedDBImp(repo, membership, enf)
obj := &testScopedObj{ownerKind: model.OwnerKindAccount}
err := db.Create(context.Background(), caller, orgRef, obj)
if !errors.Is(err, merrors.ErrAccessDenied) {
t.Fatalf("expected access denied for non-member personal create, got %v", err)
}
if !membership.called {
t.Fatal("personal create must check organization membership before insert")
}
if enf.called {
t.Fatal("personal create must not consult policy when caller is not an organization member")
}
if len(repo.objects) != 0 {
t.Fatal("object must not be inserted when membership denies")
}
}
func TestScopedDBImp_Create_Org_CallsPolicyAndSetsFields(t *testing.T) {
caller := bson.NewObjectID()
orgRef := bson.NewObjectID()
repo := newFakeScopedRepository()
enf := &fakeEnforcer{allow: true}
db, orgPolicyRef := newTestScopedDBImp(repo, &fakeMembership{member: true}, enf)
obj := &testScopedObj{ownerKind: model.OwnerKindOrganization}
if err := db.Create(context.Background(), caller, orgRef, obj); err != nil {
t.Fatalf("expected success, got %v", err)
}
if !enf.called {
t.Fatal("org create must consult the enforcer")
}
if enf.gotAction != model.ActionCreate {
t.Fatalf("expected ActionCreate, got %s", enf.gotAction)
}
if obj.GetOwnerRef() != orgRef {
t.Fatalf("expected ownerRef=orgRef for org object, got %s", obj.GetOwnerRef().Hex())
}
if obj.GetPermissionRef() != orgPolicyRef {
t.Fatal("permissionRef must be set to OrgPolicyRef for org objects")
}
}
func TestScopedDBImp_Create_Org_DeniedByPolicy(t *testing.T) {
caller := bson.NewObjectID()
orgRef := bson.NewObjectID()
repo := newFakeScopedRepository()
enf := &fakeEnforcer{allow: false}
db, _ := newTestScopedDBImp(repo, &fakeMembership{member: true}, enf)
obj := &testScopedObj{ownerKind: model.OwnerKindOrganization}
err := db.Create(context.Background(), caller, orgRef, obj)
if !errors.Is(err, merrors.ErrAccessDenied) {
t.Fatalf("expected access denied, got %v", err)
}
if len(repo.objects) != 0 {
t.Fatal("object must not be inserted when policy denies")
}
}
func TestScopedDBImp_Create_Org_DeniedWhenCallerIsNotMember(t *testing.T) {
caller := bson.NewObjectID()
orgRef := bson.NewObjectID()
repo := newFakeScopedRepository()
membership := &fakeMembership{member: false}
enf := &fakeEnforcer{allow: true}
db, _ := newTestScopedDBImp(repo, membership, enf)
obj := &testScopedObj{ownerKind: model.OwnerKindOrganization}
err := db.Create(context.Background(), caller, orgRef, obj)
if !errors.Is(err, merrors.ErrAccessDenied) {
t.Fatalf("expected access denied for non-member org create, got %v", err)
}
if !membership.called {
t.Fatal("org create must check organization membership before policy")
}
if enf.called {
t.Fatal("org create must not consult policy when caller is not an organization member")
}
if len(repo.objects) != 0 {
t.Fatal("object must not be inserted when membership denies")
}
}
func TestScopedDBImp_Create_Org_MembershipErrorFailsClosed(t *testing.T) {
boom := merrors.Define("membership backend down")
repo := newFakeScopedRepository()
membership := &fakeMembership{err: boom}
enf := &fakeEnforcer{allow: true}
db, _ := newTestScopedDBImp(repo, membership, enf)
obj := &testScopedObj{ownerKind: model.OwnerKindOrganization}
err := db.Create(context.Background(), bson.NewObjectID(), bson.NewObjectID(), obj)
if !errors.Is(err, boom) {
t.Fatalf("expected membership backend error propagated, got %v", err)
}
if enf.called {
t.Fatal("org create must fail closed and not consult policy on membership error")
}
if len(repo.objects) != 0 {
t.Fatal("object must not be inserted when the membership check errors")
}
}
// ── InsertMany ────────────────────────────────────────────────────────────────
func TestScopedDBImp_InsertMany_Empty_IsNoOp(t *testing.T) {
repo := newFakeScopedRepository()
enf := &fakeEnforcer{}
db, _ := newTestScopedDBImp(repo, &fakeMembership{member: true}, enf)
if err := db.InsertMany(context.Background(), bson.NewObjectID(), bson.NewObjectID(), nil); err != nil {
t.Fatalf("empty InsertMany must succeed, got %v", err)
}
if enf.called {
t.Fatal("empty InsertMany must not call enforcer")
}
}
func TestScopedDBImp_InsertMany_AllPersonal_SkipsPolicy(t *testing.T) {
caller := bson.NewObjectID()
orgRef := bson.NewObjectID()
repo := newFakeScopedRepository()
enf := &fakeEnforcer{}
db, _ := newTestScopedDBImp(repo, &fakeMembership{member: true}, enf)
objects := []*testScopedObj{
{ownerKind: model.OwnerKindAccount},
{ownerKind: model.OwnerKindAccount},
}
if err := db.InsertMany(context.Background(), caller, orgRef, objects); err != nil {
t.Fatalf("expected success, got %v", err)
}
if enf.called {
t.Fatal("all-personal InsertMany must not consult the enforcer")
}
if len(repo.objects) != 2 {
t.Fatalf("expected 2 inserted objects, got %d", len(repo.objects))
}
}
func TestScopedDBImp_InsertMany_AllPersonalDeniedWhenCallerIsNotMember(t *testing.T) {
caller := bson.NewObjectID()
orgRef := bson.NewObjectID()
repo := newFakeScopedRepository()
membership := &fakeMembership{member: false}
enf := &fakeEnforcer{allow: true}
db, _ := newTestScopedDBImp(repo, membership, enf)
objects := []*testScopedObj{
{ownerKind: model.OwnerKindAccount},
{ownerKind: model.OwnerKindAccount},
}
err := db.InsertMany(context.Background(), caller, orgRef, objects)
if !errors.Is(err, merrors.ErrAccessDenied) {
t.Fatalf("expected access denied for non-member personal InsertMany, got %v", err)
}
if !membership.called {
t.Fatal("personal InsertMany must check organization membership before insert")
}
if enf.called {
t.Fatal("personal InsertMany must not consult policy when caller is not an organization member")
}
if len(repo.objects) != 0 {
t.Fatal("objects must not be inserted when membership denies")
}
}
func TestScopedDBImp_InsertMany_MixedBatch_OrgPolicyCheckedOnce(t *testing.T) {
caller := bson.NewObjectID()
orgRef := bson.NewObjectID()
repo := newFakeScopedRepository()
enf := &fakeEnforcer{allow: true}
db, _ := newTestScopedDBImp(repo, &fakeMembership{member: true}, enf)
objects := []*testScopedObj{
{ownerKind: model.OwnerKindAccount},
{ownerKind: model.OwnerKindOrganization},
{ownerKind: model.OwnerKindOrganization},
}
if err := db.InsertMany(context.Background(), caller, orgRef, objects); err != nil {
t.Fatalf("expected success, got %v", err)
}
if !enf.called {
t.Fatal("enforcer must be called for mixed batch with org objects")
}
// Enforcer is called only once regardless of how many org objects are in the batch.
callCount := 0
if enf.called {
callCount = 1
}
if callCount != 1 {
t.Fatal("org policy must be checked exactly once for the entire batch")
}
}
func TestScopedDBImp_InsertMany_OrgDenied_NoObjectsInsertedOrMutated(t *testing.T) {
caller := bson.NewObjectID()
orgRef := bson.NewObjectID()
repo := newFakeScopedRepository()
enf := &fakeEnforcer{allow: false}
db, _ := newTestScopedDBImp(repo, &fakeMembership{member: true}, enf)
personal := &testScopedObj{}
org := &testScopedObj{
orgRef: bson.NewObjectID(),
ownerKind: model.OwnerKindOrganization,
ownerRef: bson.NewObjectID(),
permissionRef: bson.NewObjectID(),
}
originalPersonal := *personal
originalOrg := *org
objects := []*testScopedObj{personal, org}
err := db.InsertMany(context.Background(), caller, orgRef, objects)
if !errors.Is(err, merrors.ErrAccessDenied) {
t.Fatalf("expected access denied, got %v", err)
}
if len(repo.objects) != 0 {
t.Fatal("objects must not be inserted when policy denies")
}
if *personal != originalPersonal {
t.Fatalf("personal object was mutated on denied InsertMany: got %+v want %+v", *personal, originalPersonal)
}
if *org != originalOrg {
t.Fatalf("org object was mutated on denied InsertMany: got %+v want %+v", *org, originalOrg)
}
}
func TestScopedDBImp_InsertMany_OrgDeniedWhenCallerIsNotMember(t *testing.T) {
caller := bson.NewObjectID()
orgRef := bson.NewObjectID()
repo := newFakeScopedRepository()
membership := &fakeMembership{member: false}
enf := &fakeEnforcer{allow: true}
db, _ := newTestScopedDBImp(repo, membership, enf)
objects := []*testScopedObj{{ownerKind: model.OwnerKindOrganization}}
err := db.InsertMany(context.Background(), caller, orgRef, objects)
if !errors.Is(err, merrors.ErrAccessDenied) {
t.Fatalf("expected access denied for non-member org InsertMany, got %v", err)
}
if !membership.called {
t.Fatal("org InsertMany must check organization membership before policy")
}
if enf.called {
t.Fatal("org InsertMany must not consult policy when caller is not an organization member")
}
if len(repo.objects) != 0 {
t.Fatal("objects must not be inserted when membership denies")
}
}
func TestScopedDBImp_InsertMany_OrgMembershipErrorFailsClosed(t *testing.T) {
boom := merrors.Define("membership backend down")
repo := newFakeScopedRepository()
membership := &fakeMembership{err: boom}
enf := &fakeEnforcer{allow: true}
db, _ := newTestScopedDBImp(repo, membership, enf)
objects := []*testScopedObj{{ownerKind: model.OwnerKindOrganization}}
err := db.InsertMany(context.Background(), bson.NewObjectID(), bson.NewObjectID(), objects)
if !errors.Is(err, boom) {
t.Fatalf("expected membership backend error propagated, got %v", err)
}
if enf.called {
t.Fatal("org InsertMany must fail closed and not consult policy on membership error")
}
if len(repo.objects) != 0 {
t.Fatal("objects must not be inserted when the membership check errors")
}
}
// ── Update ────────────────────────────────────────────────────────────────────
func TestScopedDBImp_Update_RestoresOwnershipFields(t *testing.T) {
caller := bson.NewObjectID()
orgRef := bson.NewObjectID()
objectRef := bson.NewObjectID()
orgPolicyRef := bson.NewObjectID()
// Seed the repo with a personal object
repo := newFakeScopedRepository()
repo.objects[objectRef] = &testScopedObj{
id: objectRef,
orgRef: orgRef,
ownerKind: model.OwnerKindAccount,
ownerRef: caller,
permissionRef: orgPolicyRef,
}
db, _ := newTestScopedDBImp(repo, &fakeMembership{member: true}, &fakeEnforcer{})
// Submit an update that tries to change ownership
updated := &testScopedObj{
id: objectRef,
ownerKind: model.OwnerKindOrganization, // attempt to escalate
ownerRef: bson.NewObjectID(), // attempt to hijack owner
}
if err := db.Update(context.Background(), caller, updated); err != nil {
t.Fatalf("expected success, got %v", err)
}
// After update the ownership fields must match the DB, not the submitted payload
if updated.GetOwnerKind() != model.OwnerKindAccount {
t.Fatalf("ownerKind must not change: got %s", updated.GetOwnerKind())
}
if updated.GetOwnerRef() != caller {
t.Fatalf("ownerRef must not change: got %s", updated.GetOwnerRef().Hex())
}
if updated.GetOrganizationRef() != orgRef {
t.Fatal("organizationRef must not change")
}
if updated.GetPermissionRef() != orgPolicyRef {
t.Fatal("permissionRef must not change")
}
}
func TestScopedDBImp_Update_DeniedForNonOwner(t *testing.T) {
owner := bson.NewObjectID()
attacker := bson.NewObjectID()
objectRef := bson.NewObjectID()
orgRef := bson.NewObjectID()
repo := newFakeScopedRepository()
repo.objects[objectRef] = &testScopedObj{
id: objectRef,
orgRef: orgRef,
ownerKind: model.OwnerKindAccount,
ownerRef: owner,
}
db, _ := newTestScopedDBImp(repo, &fakeMembership{member: true}, &fakeEnforcer{})
err := db.Update(context.Background(), attacker, &testScopedObj{id: objectRef})
if !errors.Is(err, merrors.ErrAccessDenied) {
t.Fatalf("expected access denied for non-owner, got %v", err)
}
}
// ── ListIDs ───────────────────────────────────────────────────────────────────
func TestScopedDBImp_ListIDs_FiltersUnauthorized(t *testing.T) {
caller := bson.NewObjectID()
other := bson.NewObjectID()
orgRef := bson.NewObjectID()
id1 := bson.NewObjectID()
id2 := bson.NewObjectID()
id3 := bson.NewObjectID()
repo := newFakeScopedRepository()
repo.objects = map[bson.ObjectID]*testScopedObj{
id1: {id: id1, orgRef: orgRef, ownerKind: model.OwnerKindAccount, ownerRef: caller},
id2: {id: id2, orgRef: orgRef, ownerKind: model.OwnerKindAccount, ownerRef: other}, // unauthorized
id3: {id: id3, orgRef: orgRef, ownerKind: model.OwnerKindAccount, ownerRef: caller},
}
db, _ := newTestScopedDBImp(repo, &fakeMembership{member: true}, &fakeEnforcer{})
ids, err := db.ListIDs(context.Background(), model.ActionRead, caller, nil)
if err != nil {
t.Fatalf("expected success, got %v", err)
}
if len(ids) != 2 {
t.Fatalf("expected 2 authorized IDs, got %d", len(ids))
}
for _, id := range ids {
if id == id2 {
t.Fatal("unauthorized ID must not be returned")
}
}
}
func TestScopedDBImp_ListIDs_PropagatesNonAuthError(t *testing.T) {
boom := merrors.Define("enforcer backend down")
objectRef := bson.NewObjectID()
orgRef := bson.NewObjectID()
repo := newFakeScopedRepository()
repo.objects[objectRef] = &testScopedObj{
id: objectRef,
orgRef: orgRef,
ownerKind: model.OwnerKindOrganization,
ownerRef: orgRef,
}
enf := &fakeEnforcer{err: boom}
db, _ := newTestScopedDBImp(repo, &fakeMembership{member: true}, enf)
_, err := db.ListIDs(context.Background(), model.ActionRead, bson.NewObjectID(), nil)
if !errors.Is(err, boom) {
t.Fatalf("expected underlying error propagated, got %v", err)
}
}
func TestScopedDBImp_ListIDs_RawRepoErrorPropagated(t *testing.T) {
boom := merrors.Define("mongo down")
repo := newFakeScopedRepository()
repo.listIDsErr = boom
db, _ := newTestScopedDBImp(repo, &fakeMembership{member: true}, &fakeEnforcer{})
_, err := db.ListIDs(context.Background(), model.ActionRead, bson.NewObjectID(), nil)
if !errors.Is(err, boom) {
t.Fatalf("expected raw list error propagated, got %v", err)
}
}
// ── PatchMany ─────────────────────────────────────────────────────────────────
func TestScopedDBImp_PatchMany_PatchesOnlyAuthorizedIDs(t *testing.T) {
caller := bson.NewObjectID()
other := bson.NewObjectID()
orgRef := bson.NewObjectID()
id1 := bson.NewObjectID()
id2 := bson.NewObjectID()
repo := newFakeScopedRepository()
repo.objects = map[bson.ObjectID]*testScopedObj{
id1: {id: id1, orgRef: orgRef, ownerKind: model.OwnerKindAccount, ownerRef: caller},
id2: {id: id2, orgRef: orgRef, ownerKind: model.OwnerKindAccount, ownerRef: other},
}
// patchManyIDs declares which IDs the bulk DB call will report as affected.
// Only id1 is authorized (ownerRef==caller), so the IN filter contains only id1.
repo.patchManyIDs = []bson.ObjectID{id1}
db, _ := newTestScopedDBImp(repo, &fakeMembership{member: true}, &fakeEnforcer{})
count, err := db.PatchMany(context.Background(), caller, nil, nil)
if err != nil {
t.Fatalf("expected success, got %v", err)
}
if count != 1 {
t.Fatalf("expected 1 patched object, got %d", count)
}
if len(repo.patchedIDs) != 1 || repo.patchedIDs[0] != id1 {
t.Fatal("only the authorized ID must be patched")
}
}
// ── BuildScopedQuery ──────────────────────────────────────────────────────────
// bsonHasPair reports whether the key/value pair occurs anywhere within the
// bson.D/bson.A tree produced by builder.Query.BuildQuery().
func bsonHasPair(node any, key string, value any) bool {
switch n := node.(type) {
case bson.D:
for _, e := range n {
if e.Key == key && e.Value == value {
return true
}
if bsonHasPair(e.Value, key, value) {
return true
}
}
case bson.A:
for _, item := range n {
if bsonHasPair(item, key, value) {
return true
}
}
}
return false
}
// bsonHasKey reports whether the key occurs anywhere within the tree.
func bsonHasKey(node any, key string) bool {
switch n := node.(type) {
case bson.D:
for _, e := range n {
if e.Key == key {
return true
}
if bsonHasKey(e.Value, key) {
return true
}
}
case bson.A:
for _, item := range n {
if bsonHasKey(item, key) {
return true
}
}
}
return false
}
func TestScopedDBImp_BuildScopedQuery_WithOrgAccess_PersonalBranchIsCrossOrg(t *testing.T) {
caller := bson.NewObjectID()
orgRef := bson.NewObjectID()
enf := &fakeEnforcer{allow: true}
db, _ := newTestScopedDBImp(newFakeScopedRepository(), &fakeMembership{member: true}, enf)
q, err := db.BuildScopedQuery(context.Background(), caller, orgRef)
if err != nil {
t.Fatalf("expected success, got %v", err)
}
if !enf.called {
t.Fatal("enforcer must be called to check org access")
}
built := q.BuildQuery()
if len(built) != 1 || built[0].Key != "$or" {
t.Fatalf("expected a single top-level $or, got %v", built)
}
branches, ok := built[0].Value.(bson.A)
if !ok || len(branches) != 2 {
t.Fatalf("expected 2 $or branches, got %v", built[0].Value)
}
personal := branches[0]
if !bsonHasPair(personal, ownerKindField, model.OwnerKindAccount) ||
!bsonHasPair(personal, ownerRefField, caller) {
t.Fatalf("personal branch must match owner kind/ref, got %v", personal)
}
if bsonHasKey(personal, storable.OrganizationRefField) {
t.Fatalf("personal branch must not be org-bounded, got %v", personal)
}
org := branches[1]
if !bsonHasPair(org, storable.OrganizationRefField, orgRef) ||
!bsonHasPair(org, ownerKindField, model.OwnerKindOrganization) {
t.Fatalf("org branch must be org-bounded and org-owned, got %v", org)
}
}
func TestScopedDBImp_BuildScopedQuery_WithoutOrgAccess_PersonalOnly(t *testing.T) {
caller := bson.NewObjectID()
orgRef := bson.NewObjectID()
enf := &fakeEnforcer{allow: false}
db, _ := newTestScopedDBImp(newFakeScopedRepository(), &fakeMembership{member: true}, enf)
q, err := db.BuildScopedQuery(context.Background(), caller, orgRef)
if err != nil {
t.Fatalf("expected success, got %v", err)
}
built := q.BuildQuery()
if bsonHasKey(built, storable.OrganizationRefField) {
t.Fatalf("query must be personal-only without org access, got %v", built)
}
if !bsonHasPair(built, ownerKindField, model.OwnerKindAccount) ||
!bsonHasPair(built, ownerRefField, caller) {
t.Fatalf("query must match personal owner kind/ref, got %v", built)
}
}
func TestScopedDBImp_BuildScopedQuery_MembershipError_PersonalOnly(t *testing.T) {
caller := bson.NewObjectID()
orgRef := bson.NewObjectID()
enf := &fakeEnforcer{allow: true}
db, _ := newTestScopedDBImp(newFakeScopedRepository(), &fakeMembership{err: merrors.Define("membership down")}, enf)
q, err := db.BuildScopedQuery(context.Background(), caller, orgRef)
if err != nil {
t.Fatalf("membership failure must degrade to personal-only, got %v", err)
}
if enf.called {
t.Fatal("enforcer must not be consulted when the membership check fails")
}
built := q.BuildQuery()
if bsonHasKey(built, storable.OrganizationRefField) {
t.Fatalf("query must be personal-only on membership failure, got %v", built)
}
if !bsonHasPair(built, ownerKindField, model.OwnerKindAccount) ||
!bsonHasPair(built, ownerRefField, caller) {
t.Fatalf("query must match personal owner kind/ref, got %v", built)
}
}
func TestScopedDBImp_BuildScopedQuery_EnforcerError_Propagated(t *testing.T) {
boom := merrors.Define("enforcer down")
repo := newFakeScopedRepository()
enf := &fakeEnforcer{err: boom}
db, _ := newTestScopedDBImp(repo, &fakeMembership{member: true}, enf)
_, err := db.BuildScopedQuery(context.Background(), bson.NewObjectID(), bson.NewObjectID())
if !errors.Is(err, boom) {
t.Fatalf("expected enforcer error propagated, got %v", err)
}
}