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/mutil/mzap" "go.mongodb.org/mongo-driver/v2/bson" "go.uber.org/zap" ) type MembershipChecker interface { IsMember(ctx context.Context, orgRef, callerRef bson.ObjectID) (bool, error) } type ScopedAuthorizer struct { // Logger receives Debug/Warn messages from the enforcement path. Logger mlogger.Logger Membership MembershipChecker Enforcer Enforcer } // NewScopedAuthorizer constructs a ScopedAuthorizer and validates required dependencies. // Logger defaults to zap.NewNop() if nil. func NewScopedAuthorizer(membership MembershipChecker, enforcer Enforcer, logger mlogger.Logger) *ScopedAuthorizer { if membership == nil { panic("auth.NewScopedAuthorizer: MembershipChecker must not be nil") } if enforcer == nil { panic("auth.NewScopedAuthorizer: Enforcer must not be nil") } if logger == nil { logger = zap.NewNop() } return &ScopedAuthorizer{ Logger: logger, Membership: membership, Enforcer: enforcer, } } func (a *ScopedAuthorizer) Authorize(ctx context.Context, p ScopedAccessParams) error { a.Logger.Debug("scoped access check started", mzap.AccRef(p.CallerRef), mzap.ObjRef("object_ref", p.ObjectRef), mzap.OrgRef(p.OrgRef), zap.String("action", string(p.Action))) // Personal resources are guarded by ownership alone — membership is not consulted. // Checking membership first would lock the owner out after org membership is revoked. if p.OwnerKind == model.OwnerKindAccount { err := enforceOwnershipAccess(ctx, a.Enforcer, p) if err != nil && errors.Is(err, merrors.ErrAccessDenied) { a.Logger.Debug("access denied: not the owner", mzap.AccRef(p.CallerRef), mzap.ObjRef("object_ref", p.ObjectRef), zap.String("action", string(p.Action))) } return err } ok, err := a.Membership.IsMember(ctx, p.OrgRef, p.CallerRef) if err != nil { a.Logger.Warn("membership check failed", zap.Error(err), mzap.AccRef(p.CallerRef), mzap.OrgRef(p.OrgRef)) return err } if !ok { a.Logger.Debug("access denied: caller is not a member of the organization", mzap.AccRef(p.CallerRef), mzap.OrgRef(p.OrgRef), zap.String("action", string(p.Action))) return merrors.AccessDenied(p.Service, string(p.Action), p.ObjectRef) } err = enforceOwnershipAccess(ctx, a.Enforcer, p) if err != nil { if errors.Is(err, merrors.ErrAccessDenied) { a.Logger.Debug("access denied", mzap.AccRef(p.CallerRef), mzap.ObjRef("object_ref", p.ObjectRef), mzap.OrgRef(p.OrgRef), zap.String("action", string(p.Action))) } else { a.Logger.Warn("enforcer returned error during scoped access check", zap.Error(err), mzap.AccRef(p.CallerRef), mzap.ObjRef("object_ref", p.ObjectRef), zap.String("action", string(p.Action))) } } return err }