Bootstrap copied BFF and local accounts with public Sendico contracts (#34)
This commit is contained in:
1 parent
67dc5b5b1e
commit
7062672a87
556 files changed
+42291
-777
No files matched your search
@@ -0,0 +1,129 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user