package auth import ( "context" "errors" "git.sendico.io/tech/sxchnge/bff/pkg/merrors" "git.sendico.io/tech/sxchnge/bff/pkg/mlogger" "git.sendico.io/tech/sxchnge/bff/pkg/model" "git.sendico.io/tech/sxchnge/bff/pkg/mservice" "git.sendico.io/tech/sxchnge/bff/pkg/mutil/mzap" "go.mongodb.org/mongo-driver/v2/bson" "go.uber.org/zap" ) // ownerLoader is the minimal DB contract that ScopedDB needs: fetch a resource // by ID and populate the caller-supplied result. *template.DBImp[T] satisfies // this interface without any adapter code. type ownerLoader[T any] interface { Get(ctx context.Context, objectRef bson.ObjectID, result T) error } // ScopedDB[T] provides loadAndEnforce semantics for resources that implement // model.OwnerBound. It loads the resource from the database (so ownership is // always read from the authoritative source, never from request fields) and // then delegates access control to ScopedAuthorizer, which checks membership // before ownership/policy enforcement. // // Use ProtectedDB for resources that carry no ownerKind/ownerRef and rely // solely on org-level policy checks. type ScopedDB[T model.OwnerBound] struct { // DB is the raw repository accessor. *template.DBImp[T] satisfies this. DB ownerLoader[T] // Logger receives Debug/Warn messages from the enforcement path. Logger mlogger.Logger // Authorizer performs the full membership-aware access check. Authorizer *ScopedAuthorizer // OrgPolicyRef is the built-in policy checked when ownerKind==organization. OrgPolicyRef bson.ObjectID // Collection identifies the resource type in access-denied errors and logs. Collection mservice.Type // New allocates a zero-value T ready to be filled by DB.Get. New func() T } // NewScopedDB constructs a ScopedDB and validates required dependencies. // Logger defaults to zap.NewNop() if nil. func NewScopedDB[T model.OwnerBound]( db ownerLoader[T], authorizer *ScopedAuthorizer, orgPolicyRef bson.ObjectID, collection mservice.Type, newFn func() T, logger mlogger.Logger, ) *ScopedDB[T] { if db == nil { panic("auth.NewScopedDB: DB must not be nil") } if authorizer == nil { panic("auth.NewScopedDB: Authorizer must not be nil") } if newFn == nil { panic("auth.NewScopedDB: New must not be nil") } if logger == nil { logger = zap.NewNop() } return &ScopedDB[T]{ DB: db, Logger: logger, Authorizer: authorizer, OrgPolicyRef: orgPolicyRef, Collection: collection, New: newFn, } } // LoadAndEnforce fetches the resource identified by objectRef, reads the // ownerKind and ownerRef fields from the database record, and enforces access // via ScopedAuthorizer (membership check → ownership/policy check). It never // trusts ownership data from the caller. // // Logging contract: // - Debug on access-check start and after ownership is loaded from DB. // - Debug when access is denied (ErrAccessDenied) — intentional, not an error. // - Warn when the Enforcer itself returns a non-nil error (unexpected failure). // - No log on success. func (s *ScopedDB[T]) LoadAndEnforce(ctx context.Context, callerRef, objectRef bson.ObjectID, action model.Action) (T, error) { var zero T s.Logger.Debug("access check started", mzap.AccRef(callerRef), mzap.ObjRef("object_ref", objectRef), zap.String("collection", s.Collection), zap.String("action", string(action))) obj := s.New() if err := s.DB.Get(ctx, objectRef, obj); err != nil { return zero, err } s.Logger.Debug("ownership loaded from db", mzap.AccRef(callerRef), mzap.ObjRef("object_ref", objectRef), zap.String("ownerKind", string(obj.GetOwnerKind()))) err := s.Authorizer.Authorize(ctx, ScopedAccessParams{ Service: s.Collection, PolicyRef: s.OrgPolicyRef, CallerRef: callerRef, OrgRef: obj.GetOrganizationRef(), ObjectRef: objectRef, OwnerKind: obj.GetOwnerKind(), OwnerRef: obj.GetOwnerRef(), Action: action, }) if err == nil { return obj, nil } if errors.Is(err, merrors.ErrAccessDenied) { s.Logger.Debug("access denied", mzap.AccRef(callerRef), mzap.ObjRef("object_ref", objectRef), zap.String("ownerKind", string(obj.GetOwnerKind())), zap.String("action", string(action))) } else { s.Logger.Warn("enforcer returned error during scoped access check", zap.Error(err), mzap.AccRef(callerRef), mzap.ObjRef("object_ref", objectRef), zap.String("action", string(action))) } return zero, err }