Few days ago I was working on adding new roles with their permissions into a Solidus application, it wasn’t that hard, but if you are new into Solidus, but you are already familiar with Rails and Cancan like me, you should read this post since it will give you some “aha!” moments that will help you to understand how the roles and permissions get done in Solidus.
I had to add 2 new roles: warehouse_admin and technical_support. The first one is to create the new roles, which is pretty easy. We can use the simple Spree::Role.create(name: ‘warehouse_admin’) method or just add them to a rake task to be able to create them in staging and production without the rails console, and even call them in the seeds when necessary.
db/seeds.rb
## you awesome seeds here
Rake::Task['test_app:settings:roles:default'].invoke # the call to the new rake task
lib/tasks/roles.rake
namespace :test_app do
namespace :settings do
namespace :roles do
desc 'Run Default Warehouse Settings'
task warehouse_admin: :environment do
Spree::Role.find_or_create_by(name: 'warehouse_admin')
end
task technical_support: :environment do
Spree::Role.find_or_create_by (name: 'technical_support')
end
task default: [:warehouse_admin, :technical_support]
end
end
end
This code brought me joy. I like to add the roles in this particular way because it is pretty easy to call the right task when I just need one of them, or call the default one to add all of them and also avoid creating the same role twice.
The next thing we need to do is to add the permissions to the new roles. Solidus handles permissions in a slightly different way than plain Rails and Cancan do because Solidus has some permissions sets already predefined, so it would be good if you first gave them a look before creating a new one (Solidus default permissions).
In my case, the warehouse_admin role should be able to manage everything, but just in its own stock_location (warehouse), that is because of the business’ logic, and I achieve it by doing something like the permissions above.
test_app/app/models/spree/permission_sets/warehouse_admin.rb
require 'cancan'
module Spree
module PermissionSets
class WarehouseAdmin < PermissionSets::Base
def activate!
can :manage, Spree::StockItem, stock_location_id: location_ids
can :display, Spree::StockLocation, id: location_ids
can :manage, Spree::Order, shipments: { stock_location_id: location_ids }
can :manage, Spree::Shipment, stock_location_id: location_ids
can :manage, Spree::Product
can :manage, Spree::OrderCancellations
can :display, Spree::ReturnAuthorization
can :display, Spree::CustomerReturn
end
def location_ids
@location_ids ||= user.stock_locations.pluck(:id)
end
end
end
end
And the technical support should be able to see everything but not to change something.
app/models/spree/permission_sets/technical_support.rb
require 'cancan'
module Spree
module PermissionSets
class TechnicalSupport < PermissionSets::Base
def activate!
can [:display, :index, :read, :admin], :all
cannot :admin, Spree::Store
end
end
end
end
Now we have the roles and permissions, but we still have to do something else in order to see the changes in our app, so it registers the roles and its permissions in an initializer to let Solidus know what permissions has each new role. You can create a new initializer for the roles or use the spree initializer.
config/initializers/spree.rb
Spree::Config.configure do |config|
config.roles.assign_permissions :warehouse_admin, ['Spree::PermissionSets::WarehouseAdmin']
config.roles.assign_permissions :technical_support, ['Spree::PermissionSets::TechnicalSupport']
end
Now we are able to see the new roles with their own permissions, but you should see an error where the stock admin can manage the inventory of all the stock, and that is because our permissions set is being mixed with the default_customerpermission set, which allows us to see all the stocks inventories, so I fix that by overriding that permission set.
spree/permission_sets/default_customer_decorator.rb
Spree::PermissionSets::DefaultCustomer.class_eval do
def activate!
can :display, Spree::Country
can :display, Spree::OptionType
can :display, Spree::OptionValue
can :create, Spree::Order
can [:read, :update], Spree::Order do |order, token|
order.user == user || (order.guest_token.present? && token == order.guest_token)
end
can :create, Spree::ReturnAuthorization do |return_authorization|
return_authorization.order.user == user
end
can [:display, :update], Spree::CreditCard, user_id: user.id
can :display, Spree::Product
can :display, Spree::ProductProperty
can :display, Spree::Property
can :create, Spree.user_class
can [:read, :update, :update_email], Spree.user_class, id: user.id
can :display, Spree::State
can :display, Spree::StockItem, stock_location: { active: true }
can :display, Spree::Taxon
can :display, Spree::Taxonomy
can [:save_in_address_book, :remove_from_address_book], Spree.user_class, id: user.id
can [:display, :view_out_of_stock], Spree::Variant
can :display, Spree::Zone
end
end
Now we are able to see only the warehouses assigned to our user.
But our technical support role still has an issue. Some of the actions in the admin allowed navbar to redirect some edit actions, and as long as that role can see the index and show actions of each class, we have to create some new items for that specific role. We can do that in the spree initializer just like the code above.
config/initializers/spree.rb
TECH_SUPPORT_MENU_ITEMS = [
Spree::BackendConfiguration::MenuItem.new(
Spree::BackendConfiguration::CONFIGURATION_TABS,
'wrench',
condition: -> { can?(:read, Spree::Store) && spree_current_user.has_role?('technical_support') },
label: :settings,
partial: 'spree/admin/shared/settings_sub_menu',
url: :admin_payment_methods_path
)
]
Spree.config.menu_items = (config.menu_items | TECH_SUPPORT_MENU_ITEMS)
Now we add a new method to our user model to know if that user has a specific role.
app/models/spree/user_decorator.rb
Spree::User.class_eval do
def has_role?(target_role)
@roles ||= spree_roles
@roles.any? { |role| role.name == target_role }
end
end
Now our technical support role has the permissions and the menu options designed for it. This how the admin should look for the warehouse’s admin.
And this is what the admin for technical support role looks like.
If you have some feedback or a better way to achieve this, please feel free to let me know in the comments.