Files
sxchnge/backend/pkg/auth/scopeddb.go
T

130 lines
4.3 KiB
Go

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
}