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 %> + + + + + + + + + + + + + + + <% @roles.each do |role| %> + + + + + + <% end %> + +
    <%= Spree.user_class.human_attribute_name(:name) %><%= Spree.user_class.human_attribute_name(:permissions) %>
    <%= 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 %> +
    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 @@ + 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