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,142 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository/builder"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/template"
|
||||
"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"
|
||||
)
|
||||
|
||||
// ownerKindField and ownerRefField are the canonical BSON paths produced by
|
||||
// model.Owner embedded with bson:"owner". Resources managed by ScopedDBImp
|
||||
// must use these same tags for BuildScopedQuery to produce correct filters.
|
||||
const (
|
||||
ownerKindField = "owner.kind"
|
||||
ownerRefField = "owner.ref"
|
||||
)
|
||||
|
||||
// ScopedDBImp provides generic CRUD, patch, and bulk operations for resources
|
||||
// that follow the personal-vs-org dual-ownership model (ownerKind + ownerRef).
|
||||
//
|
||||
// Unlike ProtectedDBImp, it never uses permissionRef as a routing sentinel.
|
||||
// Every read/mutate operation calls LoadAndEnforce, which reads ownerKind and
|
||||
// ownerRef from the authoritative database record before making the access
|
||||
// decision:
|
||||
// - personal (ownerKind=account): caller must be the owner (no policy consulted)
|
||||
// - org (ownerKind=organization): caller must pass the org policy check
|
||||
//
|
||||
// This eliminates the permissionRef==NilObjectID pattern that would otherwise
|
||||
// be required for personal-owned resources under ProtectedDBImp.
|
||||
type ScopedDBImp[T model.ScopedStorable] struct {
|
||||
DBImp *template.DBImp[T]
|
||||
scoped *ScopedDB[T]
|
||||
Enforcer Enforcer
|
||||
OrgPolicyRef bson.ObjectID
|
||||
Collection mservice.Type
|
||||
}
|
||||
|
||||
// NewScopedDBImp wires a ScopedDBImp. Panics on nil required dependencies to
|
||||
// surface misconfiguration at startup, matching the contract of NewScopedDB
|
||||
// and CreateDBImp.
|
||||
func NewScopedDBImp[T model.ScopedStorable](
|
||||
dbImp *template.DBImp[T],
|
||||
enforcer Enforcer,
|
||||
membership MembershipChecker,
|
||||
orgPolicyRef bson.ObjectID,
|
||||
collection mservice.Type,
|
||||
newFn func() T,
|
||||
logger mlogger.Logger,
|
||||
) *ScopedDBImp[T] {
|
||||
if dbImp == nil {
|
||||
panic("auth.NewScopedDBImp: DBImp must not be nil")
|
||||
}
|
||||
if enforcer == nil {
|
||||
panic("auth.NewScopedDBImp: Enforcer must not be nil")
|
||||
}
|
||||
if membership == nil {
|
||||
panic("auth.NewScopedDBImp: MembershipChecker must not be nil")
|
||||
}
|
||||
if newFn == nil {
|
||||
panic("auth.NewScopedDBImp: New must not be nil")
|
||||
}
|
||||
if logger == nil {
|
||||
logger = zap.NewNop()
|
||||
}
|
||||
return &ScopedDBImp[T]{
|
||||
DBImp: dbImp,
|
||||
Enforcer: enforcer,
|
||||
OrgPolicyRef: orgPolicyRef,
|
||||
Collection: collection,
|
||||
scoped: NewScopedDB(
|
||||
dbImp,
|
||||
NewScopedAuthorizer(membership, enforcer, logger.Named("scoped")),
|
||||
orgPolicyRef,
|
||||
collection,
|
||||
newFn,
|
||||
logger.Named("scoped"),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (db *ScopedDBImp[T]) loadAndEnforce(ctx context.Context, accountRef, objectRef bson.ObjectID, action model.Action) (T, error) {
|
||||
return db.scoped.LoadAndEnforce(ctx, accountRef, objectRef, action)
|
||||
}
|
||||
|
||||
// Unprotected returns the raw DBImp for operations that bypass scoped auth.
|
||||
// Callers are responsible for their own authorization.
|
||||
func (db *ScopedDBImp[T]) Unprotected() template.DB[T] {
|
||||
return db.DBImp
|
||||
}
|
||||
|
||||
// BuildScopedQuery returns the ownership-aware OR-query for use in List
|
||||
// implementations. It mirrors the access model used by all other ScopedDBImp
|
||||
// operations: membership is checked first, then org policy read access.
|
||||
//
|
||||
// The returned query always includes personal resources owned by accountRef,
|
||||
// regardless of the organization they were created in — personal resources
|
||||
// follow their owner across every org the account is a member of. Org-owned
|
||||
// resources are bounded to organizationRef and added only when the caller is
|
||||
// a member of the org AND the org policy grants read access.
|
||||
//
|
||||
// Callers must compose the returned query with resource-specific filters
|
||||
// (additional field conditions, cursor pagination, etc.).
|
||||
//
|
||||
// Resources must embed model.Owner with bson:"owner" so the generated field
|
||||
// paths (owner.kind, owner.ref) match the stored documents.
|
||||
func (db *ScopedDBImp[T]) BuildScopedQuery(ctx context.Context, accountRef, organizationRef bson.ObjectID) (builder.Query, error) {
|
||||
personalQ := repository.Query().
|
||||
Filter(repository.Field(ownerKindField), model.OwnerKindAccount).
|
||||
Filter(repository.Field(ownerRefField), accountRef)
|
||||
|
||||
conditions := []builder.Query{personalQ}
|
||||
|
||||
isMember, err := db.scoped.Authorizer.Membership.IsMember(ctx, organizationRef, accountRef)
|
||||
if err != nil {
|
||||
// Membership backend failure degrades org access but must not prevent
|
||||
// the caller from seeing their own personal resources.
|
||||
db.scoped.Logger.Warn("membership check failed, returning personal-only results",
|
||||
zap.Error(err), mzap.AccRef(accountRef), mzap.OrgRef(organizationRef))
|
||||
} else if isMember {
|
||||
hasOrgAccess, enforceErr := db.Enforcer.Enforce(
|
||||
ctx, db.OrgPolicyRef, accountRef, organizationRef, bson.NilObjectID, model.ActionRead,
|
||||
)
|
||||
if enforceErr != nil {
|
||||
return nil, enforceErr
|
||||
}
|
||||
if hasOrgAccess {
|
||||
conditions = append(conditions, repository.Query().And(
|
||||
repository.OrgFilter(organizationRef),
|
||||
repository.Filter(ownerKindField, model.OwnerKindOrganization),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
return repository.Query().Or(conditions...), nil
|
||||
}
|
||||
Reference in new issue
Block a user