130 lines
4.3 KiB
Go
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
|
|
}
|