diff --git a/backend/app/controllers/spree/admin/roles_controller.rb b/backend/app/controllers/spree/admin/roles_controller.rb
new file mode 100644
index 00000000000..78543202391
--- /dev/null
+++ b/backend/app/controllers/spree/admin/roles_controller.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Spree
+ module Admin
+ class RolesController < ResourceController
+ end
+ end
+end
diff --git a/backend/app/views/spree/admin/roles/_form.html.erb b/backend/app/views/spree/admin/roles/_form.html.erb
new file mode 100644
index 00000000000..a807ab6cf35
--- /dev/null
+++ b/backend/app/views/spree/admin/roles/_form.html.erb
@@ -0,0 +1,45 @@
+
+
+ <%= f.field_container :name do %>
+ <%= f.label :name %>
+ <%= f.text_field :name, class: 'fullwidth' %>
+ <%= error_message_on :role, :name %>
+ <% end %>
+
+
+
+
+
<%= label_tag nil, t('spree.display_permissions') %>
+
+ <% Spree::PermissionSet.display_permissions.each do |permission| %>
+ -
+ <%= check_box_tag 'role[permission_set_ids][]', permission.id, f.object.try(:permission_sets).include?(permission) %>
+ <%= permission.name.titleize %>
+
+ <% end %>
+
+
+
+
<%= label_tag nil, t('spree.management_permissions') %>
+
+ <% Spree::PermissionSet.management_permissions.each do |permission| %>
+ -
+ <%= check_box_tag 'role[permission_set_ids][]', permission.id, f.object.try(:permission_sets).include?(permission) %>
+ <%= permission.name.titleize %>
+
+ <% end %>
+
+
+
+
<%= label_tag nil, t('spree.custom_permissions') %>
+
+ <% Spree::PermissionSet.custom_permissions.each do |permission| %>
+ -
+ <%= check_box_tag 'role[permission_set_ids][]', permission.id, f.object.try(:permission_sets).include?(permission) %>
+ <%= permission.name.titleize %>
+
+ <% end %>
+
+
+
+
diff --git a/backend/app/views/spree/admin/roles/edit.html.erb b/backend/app/views/spree/admin/roles/edit.html.erb
new file mode 100644
index 00000000000..20ad8bf1338
--- /dev/null
+++ b/backend/app/views/spree/admin/roles/edit.html.erb
@@ -0,0 +1,17 @@
+<% admin_breadcrumb(link_to plural_resource_name(Spree::LegacyUser), spree.admin_users_path) %>
+<% admin_breadcrumb(link_to t('spree.roles'), spree.admin_roles_path) %>
+<% admin_breadcrumb(@role.name) %>
+
+
+ <%= render partial: 'spree/shared/error_messages', locals: { target: @role } %>
+
+
+
+ <%= form_for [:admin, @role], url: admin_role_url(@role), method: :put do |f| %>
+ <%= render partial: 'form', locals: { f: f } %>
+
+
+ <%= render partial: 'spree/admin/shared/edit_resource_links' %>
+
+ <% end %>
+
diff --git a/backend/app/views/spree/admin/roles/index.html.erb b/backend/app/views/spree/admin/roles/index.html.erb
new file mode 100644
index 00000000000..573c6e8e37b
--- /dev/null
+++ b/backend/app/views/spree/admin/roles/index.html.erb
@@ -0,0 +1,40 @@
+<% admin_breadcrumb(plural_resource_name(Spree::LegacyUser)) %>
+<% admin_breadcrumb(t('spree.roles')) %>
+
+<% content_for :page_actions do %>
+ <% if can?(:admin, Spree::Role) && can?(:create, Spree::Role) %>
+
+ <%= link_to t('spree.new_role'), new_admin_role_url, class: 'btn btn-primary' %>
+
+ <% end %>
+<% end %>
+
+
+
+
+
+
+
+
+ | <%= Spree.user_class.human_attribute_name(:name) %> |
+ <%= Spree.user_class.human_attribute_name(:permissions) %> |
+ |
+
+
+
+ <% @roles.each do |role| %>
+
+ | <%= role.try(:name) || t(:not_available) %> |
+ <%= role.permission_sets.map(&:name).to_sentence %> |
+
+ <% if can?(:edit, role) %>
+ <%= link_to_edit role, no_text: true %>
+ <% end %>
+ <% if can?(:destroy, role) && role.can_be_deleted? %>
+ <%= link_to_delete role, no_text: true, url: spree.admin_role_path(role) %>
+ <% end %>
+ |
+
+ <% end %>
+
+
diff --git a/backend/app/views/spree/admin/roles/new.html.erb b/backend/app/views/spree/admin/roles/new.html.erb
new file mode 100644
index 00000000000..2480c871941
--- /dev/null
+++ b/backend/app/views/spree/admin/roles/new.html.erb
@@ -0,0 +1,17 @@
+<% admin_breadcrumb(link_to plural_resource_name(Spree::LegacyUser), spree.admin_users_path) %>
+<% admin_breadcrumb(link_to t('spree.roles'), spree.admin_roles_path) %>
+<% admin_breadcrumb(t('spree.new_role')) %>
+
+
+ <%= render partial: 'spree/shared/error_messages', locals: { target: @role } %>
+
+
+
+ <%= form_for [:admin, @role], url: admin_roles_url, method: :post do |f| %>
+ <%= render partial: 'form', locals: { f: f } %>
+
+
+ <%= render partial: 'spree/admin/shared/new_resource_links' %>
+
+ <% end %>
+
diff --git a/backend/app/views/spree/admin/shared/_user_sub_menu.html.erb b/backend/app/views/spree/admin/shared/_user_sub_menu.html.erb
new file mode 100644
index 00000000000..6ebb8157ae0
--- /dev/null
+++ b/backend/app/views/spree/admin/shared/_user_sub_menu.html.erb
@@ -0,0 +1,9 @@
+
+ <% if can?(:admin, Spree.user_class) %>
+ <%= tab :user, label: :users, url: spree.admin_users_path %>
+ <% end %>
+
+ <% if can?(:admin, Spree::Role) %>
+ <%= tab :roles, label: :roles, url: spree.admin_roles_path %>
+ <% end %>
+
diff --git a/backend/config/routes.rb b/backend/config/routes.rb
index f8c70640e61..9d371a0ac96 100644
--- a/backend/config/routes.rb
+++ b/backend/config/routes.rb
@@ -181,6 +181,8 @@
end
end
+ resources :roles
+
resources :style_guide, only: [:index]
end
diff --git a/backend/lib/spree/backend_configuration.rb b/backend/lib/spree/backend_configuration.rb
index b9e14fcb26b..87fa54d69c7 100644
--- a/backend/lib/spree/backend_configuration.rb
+++ b/backend/lib/spree/backend_configuration.rb
@@ -47,7 +47,7 @@ def theme_path(user_theme = nil)
:store_credit_reasons]
PROMOTION_TABS ||= [:promotions, :promotion_categories]
STOCK_TABS ||= [:stock_items]
- USER_TABS ||= [:users, :store_credits]
+ USER_TABS ||= [:users, :roles, :store_credits]
# Items can be added to the menu by using code like the following:
#
@@ -107,6 +107,7 @@ def menu_items
MenuItem.new(
USER_TABS,
'user',
+ partial: 'spree/admin/shared/user_sub_menu',
condition: -> { Spree.user_class && can?(:admin, Spree.user_class) },
url: :admin_users_path,
position: 4
diff --git a/backend/spec/features/admin/store_credits_spec.rb b/backend/spec/features/admin/store_credits_spec.rb
index aa05612a028..afdf85184d0 100644
--- a/backend/spec/features/admin/store_credits_spec.rb
+++ b/backend/spec/features/admin/store_credits_spec.rb
@@ -17,7 +17,7 @@
describe "visiting the store credits page" do
before do
visit spree.admin_path
- click_link "Users"
+ click_nav "Users"
end
it "should be on the store credits page" do
@@ -37,7 +37,7 @@
describe "creating store credit" do
before do
visit spree.admin_path
- click_link "Users"
+ click_nav "Users"
click_link store_credit.user.email
click_link "Store Credit"
allow_any_instance_of(Spree::Admin::StoreCreditsController).to receive_messages(spree_current_user: admin_user)
@@ -59,7 +59,7 @@
describe "displaying a store credit details page" do
before do
visit spree.admin_path
- click_link "Users"
+ click_nav "Users"
click_link store_credit.user.email
click_link "Store Credit"
page.find(".sc-table td.actions a.fa-edit").click
@@ -111,7 +111,7 @@
before do
visit spree.admin_path
- click_link "Users"
+ click_nav "Users"
click_link store_credit.user.email
click_link "Store Credit"
allow_any_instance_of(Spree::Admin::StoreCreditsController).to receive_messages(spree_current_user: admin_user)
diff --git a/backend/spec/features/admin/users_spec.rb b/backend/spec/features/admin/users_spec.rb
index 5fdcdbb568d..2e1ffd8ca0d 100644
--- a/backend/spec/features/admin/users_spec.rb
+++ b/backend/spec/features/admin/users_spec.rb
@@ -99,7 +99,7 @@
before do
visit spree.admin_path
- click_link 'Users'
+ click_nav 'Users'
end
context 'users index' do
@@ -357,7 +357,7 @@ def always_invalid_email
context 'if an user has placed orders' do
before do
visit spree.admin_path
- click_link 'Users'
+ click_nav 'Users'
end
it "can't be deleted" do
diff --git a/core/app/models/spree/permission_set.rb b/core/app/models/spree/permission_set.rb
new file mode 100644
index 00000000000..5aaf0bc816d
--- /dev/null
+++ b/core/app/models/spree/permission_set.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Spree
+ class PermissionSet < Spree::Base
+ has_many :role_permissions
+ has_many :roles, through: :role_permissions
+
+ validates :name, :set, presence: true
+
+ scope :display_permissions, -> { where('name LIKE ?', '%Display') }
+ scope :management_permissions, -> { where('name LIKE ?', '%Management') }
+
+ scope :custom_permissions, -> {
+ where.not(id: display_permissions)
+ .where.not(id: management_permissions)
+ .where.not(set: ['Spree::PermissionSets::SuperUser', 'Spree::PermissionSets::DefaultCustomer'])
+ }
+ end
+end
diff --git a/core/app/models/spree/role.rb b/core/app/models/spree/role.rb
index 3e2d7d1a234..9d22f4b75d6 100644
--- a/core/app/models/spree/role.rb
+++ b/core/app/models/spree/role.rb
@@ -3,12 +3,30 @@
module Spree
class Role < Spree::Base
has_many :role_users, class_name: "Spree::RoleUser", dependent: :destroy
+ has_many :role_permissions, dependent: :destroy
+ has_many :permission_sets, through: :role_permissions
has_many :users, through: :role_users
+ scope :non_base_roles, -> { where.not(name: ['admin', 'default']) }
+
validates_uniqueness_of :name, case_sensitive: true
+ validates :name, uniqueness: true
+ after_save :assign_permissions
def admin?
name == "admin"
end
+
+ def permission_sets_constantized
+ permission_sets.map(&:set).map(&:constantize)
+ end
+
+ def assign_permissions
+ ::Spree::Config.roles.assign_permissions name, permission_sets_constantized
+ end
+
+ def can_be_deleted?
+ permission_sets.find_by(set: ["Spree::PermissionSets::SuperUser", "Spree::PermissionSets::DefaultCustomer"]).nil?
+ end
end
end
diff --git a/core/app/models/spree/role_permission.rb b/core/app/models/spree/role_permission.rb
new file mode 100644
index 00000000000..a194df5b2f0
--- /dev/null
+++ b/core/app/models/spree/role_permission.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Spree
+ class RolePermission < Spree::Base
+ belongs_to :role
+ belongs_to :permission_set
+ end
+end
diff --git a/core/config/locales/en.yml b/core/config/locales/en.yml
index 64de0cbb2b1..911df76c502 100644
--- a/core/config/locales/en.yml
+++ b/core/config/locales/en.yml
@@ -961,6 +961,7 @@ en:
promotions: Promotions
properties: Property Types
rma: RMA
+ roles: Roles
settings: Settings
shipping: Shipping
stock: Stock
@@ -1450,6 +1451,7 @@ en:
currency_settings: Currency Settings
current: Current
current_promotion_usage: 'Current Usage: %{count}'
+ custom_permissions: Custom Permissions
customer: Customer
customer_details: Customer Details
customer_details_updated: Customer Details Updated
@@ -1491,6 +1493,7 @@ en:
discount_rules: Discount Rules
dismiss_banner: No. Thanks! I'm not interested, do not display this message again
display: Display
+ display_permissions: Display Permissions
download_promotion_codes_list: Download codes list
edit: Edit
edit_refund_reason: Edit Refund Reason
@@ -1763,6 +1766,7 @@ en:
manage_promotion_categories: Manage Promotion Categories
manage_stock: Store Stock
manage_variants: Manage Variants
+ management_permissions: Management Permissions
manual_intervention_required: Manual intervention required
master_price: Master Price
master_sku: Master SKU
@@ -1811,6 +1815,7 @@ en:
new_refund_reason: New Refund Reason
new_return_authorization: New RMA
new_rma_reason: New RMA Reason
+ new_role: New Role
new_shipment_at_location: New shipment at location
new_shipping_category: New Shipping Category
new_shipping_method: New Shipping Method
diff --git a/core/db/migrate/20230607100048_create_spree_permission_sets.rb b/core/db/migrate/20230607100048_create_spree_permission_sets.rb
new file mode 100644
index 00000000000..c28579bdfca
--- /dev/null
+++ b/core/db/migrate/20230607100048_create_spree_permission_sets.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class CreateSpreePermissionSets < ActiveRecord::Migration[7.0]
+ def change
+ create_table :spree_permission_sets do |t|
+ t.string :name
+ t.string :set
+ t.timestamps
+ end
+ end
+end
diff --git a/core/db/migrate/20230607100109_create_spree_roles_permissions.rb b/core/db/migrate/20230607100109_create_spree_roles_permissions.rb
new file mode 100644
index 00000000000..e2f0e08da0f
--- /dev/null
+++ b/core/db/migrate/20230607100109_create_spree_roles_permissions.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class CreateSpreeRolesPermissions < ActiveRecord::Migration[7.0]
+ def change
+ create_table :spree_role_permissions do |t|
+ t.references :role
+ t.references :permission_set
+ t.timestamps
+ end
+ end
+end
diff --git a/core/lib/spree/app_configuration.rb b/core/lib/spree/app_configuration.rb
index 49baffcc554..ee1c7a9522a 100644
--- a/core/lib/spree/app_configuration.rb
+++ b/core/lib/spree/app_configuration.rb
@@ -605,6 +605,10 @@ def roles
@roles ||= Spree::RoleConfiguration.new.tap do |roles|
roles.assign_permissions :default, ['Spree::PermissionSets::DefaultCustomer']
roles.assign_permissions :admin, ['Spree::PermissionSets::SuperUser']
+
+ Spree::Role.non_base_roles.each do |role|
+ roles.assign_permissions role.name, role.permission_sets_constantized
+ end
end
end
diff --git a/core/lib/spree/permission_sets.rb b/core/lib/spree/permission_sets.rb
index 95786aaf079..69a673e6e03 100644
--- a/core/lib/spree/permission_sets.rb
+++ b/core/lib/spree/permission_sets.rb
@@ -13,6 +13,7 @@
require 'spree/permission_sets/promotion_management'
require 'spree/permission_sets/restricted_stock_display'
require 'spree/permission_sets/restricted_stock_management'
+require 'spree/permission_sets/role_management'
require 'spree/permission_sets/stock_display'
require 'spree/permission_sets/stock_management'
require 'spree/permission_sets/super_user'
diff --git a/core/lib/spree/permission_sets/role_management.rb b/core/lib/spree/permission_sets/role_management.rb
new file mode 100644
index 00000000000..a375ccd99de
--- /dev/null
+++ b/core/lib/spree/permission_sets/role_management.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Spree
+ module PermissionSets
+ # Permissions for viewing and editing the user roles.
+ #
+ # This permission set allows full control over roles, but only allows reading users.
+ class RoleManagement < PermissionSets::Base
+ def activate!
+ can [:read, :admin, :edit, :addresses, :orders, :items], Spree.user_class
+ can :manage, Spree::Role
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/permission_sets/user_display.rb b/core/lib/spree/permission_sets/user_display.rb
index 0ff154ee2af..464b5cbd576 100644
--- a/core/lib/spree/permission_sets/user_display.rb
+++ b/core/lib/spree/permission_sets/user_display.rb
@@ -10,7 +10,7 @@ class UserDisplay < PermissionSets::Base
def activate!
can [:read, :admin, :edit, :addresses, :orders, :items], Spree.user_class
can [:read, :admin], Spree::StoreCredit
- can :read, Spree::Role
+ can [:read, :admin], Spree::Role
end
end
end