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,262 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mutil/mzap"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Create inserts a new resource. If ownerKind is absent or invalid it defaults
|
||||
// to account.
|
||||
//
|
||||
// Personal path (ownerKind=account): requires the caller to be a member of
|
||||
// organizationRef (so a non-member cannot plant personal objects in a foreign
|
||||
// org namespace), then sets ownerRef=accountRef and inserts. The org policy is
|
||||
// not consulted — after creation, access is governed by ownership alone.
|
||||
//
|
||||
// Org path (ownerKind=organization): checks org membership and the org policy
|
||||
// (via authorizeOrgMutation) first, then stamps ownerRef/permissionRef and
|
||||
// inserts. Ownership fields are only mutated after the access check so a denied
|
||||
// caller's object is unchanged.
|
||||
func (db *ScopedDBImp[T]) Create(ctx context.Context, accountRef, organizationRef bson.ObjectID, object T) error {
|
||||
db.DBImp.Logger.Debug("attempting to create scoped object",
|
||||
mzap.AccRef(accountRef), mzap.OrgRef(organizationRef),
|
||||
zap.String("collection", db.Collection))
|
||||
|
||||
if !object.GetOwnerKind().IsValid() {
|
||||
object.SetOwnerKind(model.OwnerKindAccount)
|
||||
}
|
||||
|
||||
switch object.GetOwnerKind() {
|
||||
case model.OwnerKindAccount:
|
||||
if err := db.authorizeOrgMembership(ctx, accountRef, organizationRef, model.ActionCreate); err != nil {
|
||||
return err
|
||||
}
|
||||
object.SetOwnerRef(accountRef)
|
||||
object.SetOrganizationRef(organizationRef)
|
||||
if err := db.DBImp.Create(ctx, object); err != nil {
|
||||
db.DBImp.Logger.Warn("failed to create personal scoped object",
|
||||
zap.Error(err), mzap.AccRef(accountRef), mzap.OrgRef(organizationRef))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
case model.OwnerKindOrganization:
|
||||
if err := db.authorizeOrgMutation(ctx, accountRef, organizationRef, model.ActionCreate); err != nil {
|
||||
return err
|
||||
}
|
||||
object.SetOwnerRef(organizationRef)
|
||||
object.SetOrganizationRef(organizationRef)
|
||||
object.SetPermissionRef(db.OrgPolicyRef)
|
||||
if err := db.DBImp.Create(ctx, object); err != nil {
|
||||
db.DBImp.Logger.Warn("failed to create org scoped object",
|
||||
zap.Error(err), mzap.AccRef(accountRef), mzap.OrgRef(organizationRef))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
default:
|
||||
db.DBImp.Logger.Warn("unsupported ownerKind on create",
|
||||
zap.String("ownerKind", string(object.GetOwnerKind())),
|
||||
mzap.AccRef(accountRef), mzap.OrgRef(organizationRef))
|
||||
return merrors.InvalidArgument("unsupported ownerKind", string(object.GetOwnerKind()))
|
||||
}
|
||||
}
|
||||
|
||||
// InsertMany bulk-inserts resources. Ownership fields are normalized per
|
||||
// object (defaulting to account when ownerKind is absent). Org membership, and
|
||||
// any required org policy check, run before any object is mutated.
|
||||
func (db *ScopedDBImp[T]) InsertMany(ctx context.Context, accountRef, organizationRef bson.ObjectID, objects []T) error {
|
||||
if len(objects) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
db.DBImp.Logger.Debug("attempting to insert many scoped objects",
|
||||
mzap.AccRef(accountRef), mzap.OrgRef(organizationRef),
|
||||
zap.Int("count", len(objects)), zap.String("collection", db.Collection))
|
||||
|
||||
if err := db.authorizeOrgMembership(ctx, accountRef, organizationRef, model.ActionCreate); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ownerKinds := make([]model.OwnerKind, len(objects))
|
||||
var hasOrgObjects bool
|
||||
for i, obj := range objects {
|
||||
ownerKind := obj.GetOwnerKind()
|
||||
if !ownerKind.IsValid() {
|
||||
ownerKind = model.OwnerKindAccount
|
||||
}
|
||||
switch ownerKind {
|
||||
case model.OwnerKindAccount:
|
||||
case model.OwnerKindOrganization:
|
||||
hasOrgObjects = true
|
||||
default:
|
||||
return merrors.InvalidArgument("unsupported ownerKind in InsertMany", string(ownerKind))
|
||||
}
|
||||
ownerKinds[i] = ownerKind
|
||||
}
|
||||
|
||||
if hasOrgObjects {
|
||||
if err := db.authorizeOrgPolicy(ctx, accountRef, organizationRef, model.ActionCreate); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for i, obj := range objects {
|
||||
switch ownerKinds[i] {
|
||||
case model.OwnerKindAccount:
|
||||
obj.SetOwnerKind(model.OwnerKindAccount)
|
||||
obj.SetOwnerRef(accountRef)
|
||||
obj.SetOrganizationRef(organizationRef)
|
||||
case model.OwnerKindOrganization:
|
||||
obj.SetOwnerKind(model.OwnerKindOrganization)
|
||||
obj.SetOwnerRef(organizationRef)
|
||||
obj.SetOrganizationRef(organizationRef)
|
||||
obj.SetPermissionRef(db.OrgPolicyRef)
|
||||
default:
|
||||
return merrors.InvalidArgument("unsupported ownerKind in InsertMany", string(ownerKinds[i]))
|
||||
}
|
||||
}
|
||||
|
||||
if err := db.DBImp.InsertMany(ctx, objects); err != nil {
|
||||
db.DBImp.Logger.Warn("failed to insert many scoped objects",
|
||||
zap.Error(err), mzap.AccRef(accountRef), mzap.OrgRef(organizationRef),
|
||||
zap.Int("count", len(objects)))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// authorizeOrgMembership requires the caller to be a member of the
|
||||
// organization. It is the create-time gate shared by the personal and org
|
||||
// paths: since no record exists yet to read ownership from, membership in the
|
||||
// target org is the minimum proof that the caller may write into its
|
||||
// namespace. It fails closed — a membership backend error denies the mutation.
|
||||
func (db *ScopedDBImp[T]) authorizeOrgMembership(ctx context.Context, accountRef, organizationRef bson.ObjectID, action model.Action) error {
|
||||
isMember, err := db.scoped.Authorizer.Membership.IsMember(ctx, organizationRef, accountRef)
|
||||
if err != nil {
|
||||
db.DBImp.Logger.Warn("membership check failed during create-time mutation",
|
||||
zap.Error(err), mzap.AccRef(accountRef), mzap.OrgRef(organizationRef),
|
||||
zap.String("collection", db.Collection))
|
||||
return err
|
||||
}
|
||||
if !isMember {
|
||||
db.DBImp.Logger.Debug("access denied: caller is not a member of the organization",
|
||||
mzap.AccRef(accountRef), mzap.OrgRef(organizationRef),
|
||||
zap.String("collection", db.Collection), zap.String("action", string(action)))
|
||||
return merrors.AccessDenied(db.Collection, string(action), bson.NilObjectID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// authorizeOrgPolicy checks that the org policy allows the caller to perform
|
||||
// the action. It assumes membership has already been verified — callers must
|
||||
// go through authorizeOrgMembership (or authorizeOrgMutation) first.
|
||||
func (db *ScopedDBImp[T]) authorizeOrgPolicy(ctx context.Context, accountRef, organizationRef bson.ObjectID, action model.Action) error {
|
||||
ok, err := db.Enforcer.Enforce(ctx, db.OrgPolicyRef, accountRef, organizationRef, bson.NilObjectID, action)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
db.DBImp.Logger.Debug("access denied: org mutation policy",
|
||||
mzap.AccRef(accountRef), mzap.OrgRef(organizationRef),
|
||||
zap.String("collection", db.Collection), zap.String("action", string(action)))
|
||||
return merrors.AccessDenied(db.Collection, string(action), bson.NilObjectID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// authorizeOrgMutation gates a create-time org mutation: the caller must be a
|
||||
// member of the organization (authorizeOrgMembership) AND the org policy must
|
||||
// allow the action (authorizeOrgPolicy). Because no record exists yet to read
|
||||
// ownerKind from, it cannot go through loadAndEnforce; this mirrors the
|
||||
// membership-first check ScopedAuthorizer applies to read/update/delete of
|
||||
// org-owned resources.
|
||||
func (db *ScopedDBImp[T]) authorizeOrgMutation(ctx context.Context, accountRef, organizationRef bson.ObjectID, action model.Action) error {
|
||||
isMemberErr := db.authorizeOrgMembership(ctx, accountRef, organizationRef, action)
|
||||
if isMemberErr != nil {
|
||||
return isMemberErr
|
||||
}
|
||||
if isPolicyErr := db.authorizeOrgPolicy(ctx, accountRef, organizationRef, action); isPolicyErr != nil {
|
||||
return isPolicyErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get fetches a resource after verifying ActionRead via LoadAndEnforce.
|
||||
func (db *ScopedDBImp[T]) Get(ctx context.Context, accountRef, objectRef bson.ObjectID, result T) error {
|
||||
db.DBImp.Logger.Debug("attempting to get scoped object",
|
||||
mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))
|
||||
|
||||
if _, err := db.loadAndEnforce(ctx, accountRef, objectRef, model.ActionRead); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.DBImp.Get(ctx, objectRef, result); err != nil {
|
||||
db.DBImp.Logger.Warn("failed to get scoped object after auth",
|
||||
zap.Error(err), mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update replaces a resource after verifying ActionUpdate. Immutable ownership
|
||||
// fields (ownerKind, ownerRef, orgRef, permissionRef) are always restored from
|
||||
// the database record so callers cannot change resource ownership via the payload.
|
||||
func (db *ScopedDBImp[T]) Update(ctx context.Context, accountRef bson.ObjectID, object T) error {
|
||||
db.DBImp.Logger.Debug("attempting to update scoped object",
|
||||
mzap.AccRef(accountRef), mzap.StorableRef(object))
|
||||
|
||||
existing, err := db.loadAndEnforce(ctx, accountRef, *object.GetID(), model.ActionUpdate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
object.SetOwnerKind(existing.GetOwnerKind())
|
||||
object.SetOwnerRef(existing.GetOwnerRef())
|
||||
object.SetOrganizationRef(existing.GetOrganizationRef())
|
||||
object.SetPermissionRef(existing.GetPermissionRef())
|
||||
|
||||
if err := db.DBImp.Update(ctx, object); err != nil {
|
||||
db.DBImp.Logger.Warn("failed to update scoped object",
|
||||
zap.Error(err), mzap.AccRef(accountRef), mzap.StorableRef(object))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete removes a resource after verifying ActionDelete.
|
||||
func (db *ScopedDBImp[T]) Delete(ctx context.Context, accountRef, objectRef bson.ObjectID) error {
|
||||
db.DBImp.Logger.Debug("attempting to delete scoped object",
|
||||
mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))
|
||||
|
||||
if _, err := db.loadAndEnforce(ctx, accountRef, objectRef, model.ActionDelete); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.DBImp.Delete(ctx, objectRef); err != nil {
|
||||
db.DBImp.Logger.Warn("failed to delete scoped object",
|
||||
zap.Error(err), mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteCascadeAuth removes a resource (with cascade via the registered deleter)
|
||||
// after verifying ActionDelete.
|
||||
func (db *ScopedDBImp[T]) DeleteCascadeAuth(ctx context.Context, accountRef, objectRef bson.ObjectID) error {
|
||||
db.DBImp.Logger.Debug("attempting cascade-delete scoped object",
|
||||
mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))
|
||||
|
||||
if _, err := db.loadAndEnforce(ctx, accountRef, objectRef, model.ActionDelete); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.DBImp.DeleteCascade(ctx, objectRef); err != nil {
|
||||
db.DBImp.Logger.Warn("failed to cascade-delete scoped object",
|
||||
zap.Error(err), mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user