Roles and permissions in Solidus

Reading Time: 4 minutes

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.

0 Shares:
You May Also Like