package auth import ( "context" "errors" "testing" "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" ) // testOwned is a minimal model.OwnerBound used exclusively by ScopedDB tests. type testOwned struct { id bson.ObjectID orgRef bson.ObjectID kind model.OwnerKind ownerRef bson.ObjectID } func (o *testOwned) GetID() *bson.ObjectID { return &o.id } func (o *testOwned) SetID(id bson.ObjectID) { o.id = id } func (o *testOwned) Update() {} func (o *testOwned) Collection() string { return "test_owned" } func (o *testOwned) GetOrganizationRef() bson.ObjectID { return o.orgRef } func (o *testOwned) SetOrganizationRef(ref bson.ObjectID) { o.orgRef = ref } func (o *testOwned) GetOwnerKind() model.OwnerKind { return o.kind } func (o *testOwned) GetOwnerRef() bson.ObjectID { return o.ownerRef } var _ model.OwnerBound = (*testOwned)(nil) // fakeOwnerLoader returns the pre-built object or the configured error. type fakeOwnerLoader struct { obj *testOwned err error } func (f *fakeOwnerLoader) Get(_ context.Context, _ bson.ObjectID, result *testOwned) error { if f.err != nil { return f.err } *result = *f.obj return nil } // fakeMembership controls whether IsMember returns true/false/error. type fakeMembership struct { member bool err error called bool } func (f *fakeMembership) IsMember(_ context.Context, _, _ bson.ObjectID) (bool, error) { f.called = true return f.member, f.err } func newTestScopedDB(loader *fakeOwnerLoader, membership *fakeMembership, enf Enforcer) *ScopedDB[*testOwned] { authorizer := &ScopedAuthorizer{ Logger: zap.NewNop(), Membership: membership, Enforcer: enf, } return &ScopedDB[*testOwned]{ DB: loader, Logger: zap.NewNop(), Authorizer: authorizer, OrgPolicyRef: bson.NewObjectID(), Collection: mservice.Type("test_owned"), New: func() *testOwned { return &testOwned{} }, } } func TestScopedDB_PersonalOwner_Allowed(t *testing.T) { caller := bson.NewObjectID() objectRef := bson.NewObjectID() obj := &testOwned{ id: objectRef, orgRef: bson.NewObjectID(), kind: model.OwnerKindAccount, ownerRef: caller, // owner == caller } enf := &fakeEnforcer{} db := newTestScopedDB(&fakeOwnerLoader{obj: obj}, &fakeMembership{member: true}, enf) result, err := db.LoadAndEnforce(context.Background(), caller, objectRef, model.ActionRead) if err != nil { t.Fatalf("expected allow, got %v", err) } if result == nil { t.Fatal("expected non-nil result on success") } if enf.called { t.Fatal("personal access must not consult the enforcer") } } func TestScopedDB_PersonalOwner_Denied(t *testing.T) { caller := bson.NewObjectID() other := bson.NewObjectID() objectRef := bson.NewObjectID() obj := &testOwned{ id: objectRef, orgRef: bson.NewObjectID(), kind: model.OwnerKindAccount, ownerRef: other, // owner != caller } enf := &fakeEnforcer{} db := newTestScopedDB(&fakeOwnerLoader{obj: obj}, &fakeMembership{member: true}, enf) _, err := db.LoadAndEnforce(context.Background(), caller, objectRef, model.ActionRead) if !errors.Is(err, merrors.ErrAccessDenied) { t.Fatalf("expected access denied, got %v", err) } if enf.called { t.Fatal("personal access must not consult the enforcer") } } func TestScopedDB_OrgOwner_Allowed(t *testing.T) { caller := bson.NewObjectID() orgRef := bson.NewObjectID() objectRef := bson.NewObjectID() obj := &testOwned{ id: objectRef, orgRef: orgRef, kind: model.OwnerKindOrganization, ownerRef: orgRef, } enf := &fakeEnforcer{allow: true} db := newTestScopedDB(&fakeOwnerLoader{obj: obj}, &fakeMembership{member: true}, enf) result, err := db.LoadAndEnforce(context.Background(), caller, objectRef, model.ActionRead) if err != nil { t.Fatalf("expected allow, got %v", err) } if result == nil { t.Fatal("expected non-nil result on success") } if !enf.called { t.Fatal("org access must consult the enforcer") } } func TestScopedDB_OrgOwner_Denied(t *testing.T) { caller := bson.NewObjectID() orgRef := bson.NewObjectID() objectRef := bson.NewObjectID() obj := &testOwned{ id: objectRef, orgRef: orgRef, kind: model.OwnerKindOrganization, ownerRef: orgRef, } enf := &fakeEnforcer{allow: false} db := newTestScopedDB(&fakeOwnerLoader{obj: obj}, &fakeMembership{member: true}, enf) _, err := db.LoadAndEnforce(context.Background(), caller, objectRef, model.ActionRead) if !errors.Is(err, merrors.ErrAccessDenied) { t.Fatalf("expected access denied, got %v", err) } if !enf.called { t.Fatal("org access must consult the enforcer") } } func TestScopedDB_OrgOwner_EnforcerError_Propagated(t *testing.T) { boom := merrors.Define("enforcer backend down") caller := bson.NewObjectID() orgRef := bson.NewObjectID() objectRef := bson.NewObjectID() obj := &testOwned{ id: objectRef, orgRef: orgRef, kind: model.OwnerKindOrganization, ownerRef: orgRef, } enf := &fakeEnforcer{err: boom} db := newTestScopedDB(&fakeOwnerLoader{obj: obj}, &fakeMembership{member: true}, enf) _, err := db.LoadAndEnforce(context.Background(), caller, objectRef, model.ActionRead) if !errors.Is(err, boom) { t.Fatalf("expected underlying error propagated, got %v", err) } if errors.Is(err, merrors.ErrAccessDenied) { t.Fatal("enforcer internal error must not be masked as access denied") } } func TestScopedDB_DBError_Propagated(t *testing.T) { dbErr := merrors.Define("mongo unavailable") db := newTestScopedDB(&fakeOwnerLoader{err: dbErr}, &fakeMembership{member: true}, &fakeEnforcer{}) _, err := db.LoadAndEnforce(context.Background(), bson.NewObjectID(), bson.NewObjectID(), model.ActionRead) if !errors.Is(err, dbErr) { t.Fatalf("expected DB error propagated, got %v", err) } } // TestScopedDB_PersonalOwner_NonMember_Denied is the main regression case: // even when ownerRef == callerRef, a non-member of the organization must be // denied before ownership is checked, so the tenant boundary is enforced. // TestScopedDB_PersonalOwner_NonMember_Allowed: personal resource, caller is the // owner but is NOT a member of the org. Membership is not consulted for // account-owned resources — ownership alone is sufficient. func TestScopedDB_PersonalOwner_NonMember_Allowed(t *testing.T) { caller := bson.NewObjectID() objectRef := bson.NewObjectID() obj := &testOwned{ id: objectRef, orgRef: bson.NewObjectID(), kind: model.OwnerKindAccount, ownerRef: caller, // owner == caller, membership is irrelevant } membership := &fakeMembership{member: false} enf := &fakeEnforcer{} db := newTestScopedDB(&fakeOwnerLoader{obj: obj}, membership, enf) result, err := db.LoadAndEnforce(context.Background(), caller, objectRef, model.ActionRead) if err != nil { t.Fatalf("expected allow for personal resource owner regardless of membership, got %v", err) } if result == nil { t.Fatal("expected non-nil result on success") } if enf.called { t.Fatal("enforcer must not be called for account-owned resource") } } // TestScopedDB_OrgOwner_NonMember_Denied: org-owned object, caller is not a // member — must be denied before the policy enforcer is consulted. func TestScopedDB_OrgOwner_NonMember_Denied(t *testing.T) { caller := bson.NewObjectID() orgRef := bson.NewObjectID() objectRef := bson.NewObjectID() obj := &testOwned{ id: objectRef, orgRef: orgRef, kind: model.OwnerKindOrganization, ownerRef: orgRef, } membership := &fakeMembership{member: false} enf := &fakeEnforcer{allow: true} // would allow if reached db := newTestScopedDB(&fakeOwnerLoader{obj: obj}, membership, enf) _, err := db.LoadAndEnforce(context.Background(), caller, objectRef, model.ActionRead) if !errors.Is(err, merrors.ErrAccessDenied) { t.Fatalf("expected access denied for non-member org-owned object, got %v", err) } if enf.called { t.Fatal("enforcer must not be called when membership check fails") } }