Files
openfoodnetwork/app/models/spree/ability.rb
Greg Austic c72976b1e2 Fix guest order cancellation redirecting to home page
When a guest places an order and tries to cancel it from the order
confirmation page, the cancellation silently failed and redirected
to the home page. The guest was left unsure whether the order was
cancelled, and the hub received no cancellation notification.

Root cause: two missing pieces for guest (token-based) authorization:

1. The `:cancel` ability in Ability#add_shopping_abilities only checked
   `order.user == user`, ignoring the guest token. The `:read` and
   `:update` abilities already support `order.token && token == order.token`
   as a fallback — `:cancel` now does the same.

2. The `cancel` action called `authorize! :cancel, @order` without
   passing `session[:access_token]`, so even with the corrected ability
   the token was never evaluated.

Fixes #13817

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 09:05:47 -04:00

485 lines
18 KiB
Ruby

# frozen_string_literal: true
require 'cancan'
module Spree
class Ability
include CanCan::Ability
REPORTS_SEARCH_ACTIONS = [
:search_enterprise_fees, :search_enterprise_fee_owners, :search_distributors,
:search_suppliers, :search_order_cycles, :search_order_customers
].freeze
def initialize(user)
clear_aliased_actions
# override cancan default aliasing (we don't want to differentiate between read and index)
alias_action :delete, to: :destroy
alias_action :edit, to: :update
alias_action :new, to: :create
alias_action :new_action, to: :create
alias_action :show, to: :read
user ||= Spree::User.new
if user.try(:admin?)
can :manage, :all
# this action was needed for restrictions for distributors and suppliers
# however, admins don't need to be restricted, so, bypassing it for admins
cannot :edit_as_producer_only, Spree::Order
else
can [:index, :read], Country
can :create, Order
can :read, Order do |order, token|
order.user == user || (order.token && token == order.token)
end
can :update, Order do |order, token|
order.user == user || (order.token && token == order.token)
end
can [:index, :read], ProductProperty
can [:index, :read], Property
can :create, Spree::User
can [:read, :update, :destroy], Spree::User, id: user.id
can [:index, :read], State
can [:index, :read], StockItem
can [:index, :read], Taxon
can [:index, :read], Variant
can [:index, :read], Zone
end
add_shopping_abilities user
add_base_abilities user if is_new_user? user
add_enterprise_management_abilities user if can_manage_enterprises? user
add_group_management_abilities user if can_manage_groups? user
add_product_management_abilities user if can_manage_products? user
add_order_cycle_management_abilities user if can_manage_order_cycles? user
if can_manage_orders? user
add_order_management_abilities user
elsif can_manage_line_items_in_orders? user
add_manage_line_items_abilities user
end
add_relationship_management_abilities user if can_manage_relationships? user
add_customer_account_transaction_abilities user if can_manage_enterprises? user
end
# New users have no enterprises.
def is_new_user?(user)
user.enterprises.blank?
end
# Users can manage an enterprise if they have one.
def can_manage_enterprises?(user)
user.enterprises.present?
end
# Users can manage a group if they have one.
def can_manage_groups?(user)
user.owned_groups.present?
end
# Users can manage products if they have an enterprise that is not a profile.
def can_manage_products?(user)
can_manage_enterprises?(user) &&
user.enterprises.any? { |e| e.category != :hub_profile && e.producer_profile_only != true }
end
# Users can manage order cycles if they manage a sells own/any enterprise
# OR if they manage a producer which is included in any order cycles
def can_manage_order_cycles?(user)
can_manage_orders?(user) ||
OrderCycle.visible_by(user).any?
end
# Users can manage orders if they have a sells own/any enterprise.
def can_manage_orders?(user)
user.can_manage_orders?
end
# Users can manage line items in orders if they have producer enterprise and
# any of order distributors allow them to edit their orders.
def can_manage_line_items_in_orders?(user)
user.can_manage_line_items_in_orders?
end
def can_manage_relationships?(user)
can_manage_enterprises? user
end
def add_shopping_abilities(user)
can [:destroy], Spree::LineItem do |item|
user == item.order.user &&
item.order.changes_allowed?
end
can :cancel, Spree::Order do |order, token|
order.user == user || (order.token && token == order.token)
end
can :bulk_cancel, Spree::Order do |order|
order.user == user
end
can [:update, :destroy], Spree::CreditCard do |credit_card|
credit_card.user == user
end
can [:update], Customer do |customer|
customer.user == user
end
end
# New users can create an enterprise, and gain other permissions from doing this.
def add_base_abilities(_user)
can [:create], Enterprise
end
def add_group_management_abilities(user)
can [:admin, :index], :overview
can [:admin, :index], EnterpriseGroup
can [:read, :edit, :update], EnterpriseGroup do |group|
user.owned_groups.include? group
end
end
def add_enterprise_management_abilities(user)
# We perform authorize! on (:create, nil) when creating a new order from admin,
# and also (:search, nil) when searching for variants to add to the order
can [:create, :search], nil
can [:admin, :index], :overview
can [:admin, :index, :read, :create, :edit, :update_positions, :destroy], ProducerProperty
can :new, TagRule
can [:admin, :map_by_tag, :destroy, :variant_tag_rules], TagRule do |tag_rule|
user.enterprises.include? tag_rule.enterprise
end
can [:admin, :index, :create], Enterprise
can [:read, :edit, :update,
:remove_logo, :remove_promo_image, :remove_terms_and_conditions,
:bulk_update, :resend_confirmation, :new_tag_rule_group], Enterprise do |enterprise|
OpenFoodNetwork::Permissions.new(user).editable_enterprises.include? enterprise
end
can [:welcome, :register], Enterprise do |enterprise|
enterprise.owner == user
end
can [:manage_payment_methods,
:manage_shipping_methods,
:manage_enterprise_fees,
:manage_connected_apps], Enterprise do |enterprise|
user.enterprises.include? enterprise
end
# All enterprises can have fees, though possibly suppliers don't need them?
can [:index, :create], EnterpriseFee
can [:admin, :read, :edit, :bulk_update, :destroy], EnterpriseFee do |enterprise_fee|
user.enterprises.include? enterprise_fee.enterprise
end
can [:admin, :known_users, :customers], :search
can [:admin, :show], :account
# For printing own account invoice orders
can [:print], Spree::Order do |order|
order.user == user
end
can [:admin, :bulk_update], ColumnPreference do |column_preference|
column_preference.user == user
end
can [:admin, :connect, :status, :destroy], StripeAccount do |stripe_account|
user.enterprises.include? stripe_account.enterprise
end
can [:admin, :create], UserInvitation
can [:admin, :index, :destroy], :oidc_setting
can [:admin, :create], Voucher
can [:admin, :destroy], EnterpriseRole do |enterprise_role|
enterprise_role.enterprise.owner_id == user.id
end
end
def add_product_management_abilities(user)
# Enterprise User can only access products that they are a supplier for
can [:create], Spree::Product
# An enterprise user can change a product if they are supplier of at least
# one of the product's associated variants
can [:admin, :read, :index, :update,
:seo, :group_buy_options,
:bulk_update, :clone, :delete,
:destroy], Spree::Product do |product|
variant_suppliers = product.variants.map(&:supplier)
OpenFoodNetwork::Permissions.new(user).managed_product_enterprises.intersect?(
variant_suppliers
)
end
# An enterprise user can clone if they have been granted permission to the source variant.
# Technically I'd call this permission clone_linked_variant, but it would be less confusing to
# use the same name as everywhere else.
can [:create_linked_variant], Spree::Variant do |variant|
OpenFoodNetwork::Permissions.new(user).
enterprises_granting_linked_variants.include? variant.supplier
end
can [:admin, :index, :bulk_update, :destroy, :destroy_variant, :clone,
:create_linked_variant], :products_v3
can [:create], Spree::Variant
can [:admin, :index, :read, :edit,
:update, :search, :delete, :destroy], Spree::Variant do |variant|
OpenFoodNetwork::Permissions.new(user).
managed_product_enterprises.include? variant.supplier
end
if OpenFoodNetwork::FeatureToggle.enabled?(:inventory, *user.enterprises)
can [:admin, :index, :read, :update, :bulk_update, :bulk_reset], VariantOverride do |vo|
next false unless vo.hub.present? && vo.variant&.supplier.present?
hub_auth = OpenFoodNetwork::Permissions.new(user).
variant_override_hubs.
include? vo.hub
producer_auth = OpenFoodNetwork::Permissions.new(user).
variant_override_producers.
include? vo.variant.supplier
hub_auth && producer_auth
end
end
can [:admin, :create, :update], InventoryItem do |ii|
next false unless ii.enterprise.present? &&
ii.variant&.supplier.present?
hub_auth = OpenFoodNetwork::Permissions.new(user).
variant_override_hubs.
include? ii.enterprise
producer_auth = OpenFoodNetwork::Permissions.new(user).
variant_override_producers.
include? ii.variant.supplier
hub_auth && producer_auth
end
can [:admin, :index, :read, :create,
:edit, :update_positions, :destroy], Spree::ProductProperty
can [:admin, :index, :read, :create, :edit, :update, :destroy], Spree::Image
can [:admin, :index, :read, :search], Spree::Taxon
can [:admin, :index, :guide, :import, :save, :save_data,
:validate_data, :reset_absent_products], ProductImport::ProductImporter
can [:admin, :index, :import], ::Admin::DfcProductImportsController
# Reports page
can [:admin, :index, :show, :create, *REPORTS_SEARCH_ACTIONS],
::Admin::ReportsController
can [:admin, :show, :create, :customers, :orders_and_distributors, :group_buys, :payments,
:orders_and_fulfillment, :products_and_inventory, :order_cycle_management,
:packing, :enterprise_fee_summary, :bulk_coop, :suppliers], :report
end
def add_order_cycle_management_abilities(user)
can [:admin, :index], OrderCycle do |order_cycle|
OrderCycle.visible_by(user).include?(order_cycle) ||
order_cycle.orders.editable_by_producers(user.enterprises).exists?
end
can [
:read, :edit, :update, :incoming, :outgoing, :checkout_options
], OrderCycle do |order_cycle|
OrderCycle.visible_by(user).include? order_cycle
end
can [:admin, :index, :create], Schedule
can [:admin, :update, :destroy], Schedule do |schedule|
OpenFoodNetwork::Permissions.new(user).editable_schedules.include? schedule
end
can [:bulk_update, :clone, :destroy, :notify_producers], OrderCycle do |order_cycle|
user.enterprises.include? order_cycle.coordinator
end
can [:for_order_cycle], Enterprise
can [:for_order_cycle], EnterpriseFee
end
def add_order_management_abilities(user)
can [:manage_order_sections], Spree::Order do |order|
user.admin? ||
order.distributor.nil? ||
user.enterprises.include?(order.distributor) ||
order.order_cycle&.coordinated_by?(user)
end
can [:edit_as_producer_only], Spree::Order do |order|
cannot?(:manage_order_sections, order) && can_edit_as_producer(order, user)
end
can [:index], Spree::Order do
user.admin? ||
user.enterprises.any?(&:is_distributor) ||
user.enterprises.distributors.where(enable_producers_to_edit_orders: true).exist?
end
can [:create], Spree::Order
# Spree::Admin::PaymentController need to load the order to credit_customer
can [:read, :update, :credit_customer], Spree::Order do |order|
# We allow editing orders with a nil distributor as this state occurs
# during the order creation process from the admin backend
order.distributor.nil? ||
# Enterprise User can access orders that they are a distributor for
user.enterprises.include?(order.distributor) ||
# Enterprise User can access orders that are placed inside a OC they coordinate
order.order_cycle&.coordinated_by?(user) ||
can_edit_as_producer(order, user)
end
can [:fire, :resend, :invoice, :print], Spree::Order do |order|
# We allow editing orders with a nil distributor as this state occurs
# during the order creation process from the admin backend
order.distributor.nil? ||
# Enterprise User can access orders that they are a distributor for
user.enterprises.include?(order.distributor) ||
# Enterprise User can access orders that are placed inside a OC they coordinate
order.order_cycle&.coordinated_by?(user)
end
can [:admin, :bulk_management], Spree::Order do |order|
user.admin? ||
user.enterprises.any?(&:is_distributor) ||
can_edit_as_producer(order, user)
end
can [:managed, :distribution], Spree::Order do
user.admin? || user.enterprises.any?(&:is_distributor)
end
can [:admin, :index, :create, :show, :poll, :generate], :invoice
can [:admin, :visible], Enterprise
can [:admin, :index, :create, :update, :destroy], :line_item
can [:admin, :index, :create], Spree::LineItem do |item|
user.admin? ||
user.enterprises.any?(&:is_distributor) ||
can_edit_as_producer(item.order, user)
end
can [:destroy, :update], Spree::LineItem do |item|
order = item.order
user.admin? ||
user.enterprises.include?(order.distributor) ||
order.order_cycle&.coordinated_by?(user) ||
can_edit_as_producer(order, user)
end
can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::Shipment do |shipment|
user.admin? ||
user.enterprises.any?(&:is_distributor) ||
can_edit_as_producer(shipment.order, user)
end
can [:admin, :index, :read, :create, :edit, :update, :fire, :credit_customer], Spree::Payment
can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::Adjustment
can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::ReturnAuthorization
can [:destroy], Spree::Adjustment do |adjustment|
if user.admin?
true
else
order = adjustment.order
user.enterprises.include?(order.distributor) ||
order.order_cycle&.coordinated_by?(user)
end
end
can [:create], OrderCycle
can [:admin, :index, :read, :create, :edit, :update], ExchangeVariant
can [:admin, :index, :read, :create, :edit, :update], Exchange
can [:admin, :index, :read, :create, :edit, :update], ExchangeFee
# Enterprise user can only access payment and shipping methods for their distributors
can [:index, :create], Spree::PaymentMethod
can [:admin, :read, :update, :fire, :resend,
:destroy, :show_provider_preferences], Spree::PaymentMethod do |payment_method|
(user.enterprises & payment_method.distributors).any?
end
can [:index, :create], Spree::ShippingMethod
can [:admin, :read, :update, :destroy], Spree::ShippingMethod do |shipping_method|
(user.enterprises & shipping_method.distributors).any?
end
# Reports page
can [:admin, :index, :show, :create, *REPORTS_SEARCH_ACTIONS], ::Admin::ReportsController
can [:admin, :customers, :group_buys, :sales_tax, :payments,
:orders_and_distributors, :orders_and_fulfillment, :products_and_inventory,
:order_cycle_management, :xero_invoices, :enterprise_fee_summary, :bulk_coop], :report
can [:create], Customer
can [:admin, :index, :update,
:destroy, :show], Customer, enterprise_id: Enterprise.managed_by(user).pluck(:id)
can [:admin, :new, :index], Subscription
can [:create, :edit, :update, :cancel, :pause, :unpause], Subscription do |subscription|
user.enterprises.include?(subscription.shop)
end
can [:admin, :build], SubscriptionLineItem
can [:destroy], SubscriptionLineItem do |subscription_line_item|
user.enterprises.include?(subscription_line_item.subscription.shop)
end
can [:admin, :edit, :cancel, :resume], ProxyOrder do |proxy_order|
user.enterprises.include?(proxy_order.subscription.shop)
end
can [:visible], Enterprise
end
def can_edit_as_producer(order, user)
return unless order.distributor&.enable_producers_to_edit_orders
order.variants.any? { |variant| user.enterprises.ids.include?(variant.supplier_id) }
end
def add_manage_line_items_abilities(user)
can [
:admin,
:read,
:index,
:edit,
:update,
:bulk_management,
:edit_as_producer_only
], Spree::Order do |order|
can_edit_as_producer(order, user)
end
can [:admin, :index, :create, :destroy, :update], Spree::LineItem do |item|
can_edit_as_producer(item.order, user)
end
can [:index, :create, :add, :read, :edit, :update], Spree::Shipment do |shipment|
can_edit_as_producer(shipment.order, user)
end
can [:admin, :index], OrderCycle do |order_cycle|
can_edit_as_producer(order_cycle.order, user)
end
can [:visible], Enterprise
end
def add_relationship_management_abilities(user)
can [:admin, :index, :create], EnterpriseRelationship
can [:destroy], EnterpriseRelationship do |enterprise_relationship|
user.enterprises.include?(enterprise_relationship.parent) ||
user.enterprises.include?(enterprise_relationship.child)
end
end
def add_customer_account_transaction_abilities(_user)
can [:admin, :create, :index], CustomerAccountTransaction
end
end
end