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) } }