257 lines
8.0 KiB
Go
257 lines
8.0 KiB
Go
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")
|
|
}
|
|
}
|