easy-admin-rails 0.1.14 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/easy_admin.base.js +254 -18
  3. data/app/assets/builds/easy_admin.base.js.map +4 -4
  4. data/app/assets/builds/easy_admin.css +112 -18
  5. data/app/components/easy_admin/base_component.rb +1 -0
  6. data/app/components/easy_admin/form_tabs_component.rb +5 -2
  7. data/app/components/easy_admin/navbar_component.rb +5 -1
  8. data/app/components/easy_admin/permissions/user_role_assignment_component.rb +254 -0
  9. data/app/components/easy_admin/permissions/user_role_permissions_component.rb +186 -0
  10. data/app/components/easy_admin/resources/index_component.rb +1 -4
  11. data/app/components/easy_admin/sidebar_component.rb +67 -2
  12. data/app/components/easy_admin/versions/diff_modal_component.rb +5 -1
  13. data/app/controllers/easy_admin/application_controller.rb +131 -1
  14. data/app/controllers/easy_admin/batch_actions_controller.rb +27 -0
  15. data/app/controllers/easy_admin/concerns/belongs_to_editing.rb +201 -0
  16. data/app/controllers/easy_admin/concerns/inline_field_editing.rb +297 -0
  17. data/app/controllers/easy_admin/concerns/resource_authorization.rb +55 -0
  18. data/app/controllers/easy_admin/concerns/resource_filtering.rb +178 -0
  19. data/app/controllers/easy_admin/concerns/resource_loading.rb +149 -0
  20. data/app/controllers/easy_admin/concerns/resource_pagination.rb +135 -0
  21. data/app/controllers/easy_admin/dashboard_controller.rb +2 -1
  22. data/app/controllers/easy_admin/dashboards_controller.rb +6 -40
  23. data/app/controllers/easy_admin/resources_controller.rb +13 -762
  24. data/app/controllers/easy_admin/row_actions_controller.rb +25 -0
  25. data/app/helpers/easy_admin/fields_helper.rb +61 -9
  26. data/app/javascript/easy_admin/controllers/event_emitter_controller.js +2 -4
  27. data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +0 -10
  28. data/app/javascript/easy_admin/controllers/jsoneditor_controller.js +1 -4
  29. data/app/javascript/easy_admin/controllers/permission_toggle_controller.js +227 -0
  30. data/app/javascript/easy_admin/controllers/role_preview_controller.js +93 -0
  31. data/app/javascript/easy_admin/controllers/select_field_controller.js +1 -2
  32. data/app/javascript/easy_admin/controllers/settings_button_controller.js +1 -2
  33. data/app/javascript/easy_admin/controllers/settings_sidebar_controller.js +1 -4
  34. data/app/javascript/easy_admin/controllers/turbo_stream_redirect.js +0 -2
  35. data/app/javascript/easy_admin/controllers.js +5 -1
  36. data/app/models/easy_admin/admin_user.rb +6 -0
  37. data/app/policies/admin_user_policy.rb +36 -0
  38. data/app/policies/application_policy.rb +83 -0
  39. data/app/views/easy_admin/application/authorization_failure.turbo_stream.erb +8 -0
  40. data/app/views/easy_admin/dashboards/card.html.erb +5 -0
  41. data/app/views/easy_admin/dashboards/card.turbo_stream.erb +7 -0
  42. data/app/views/easy_admin/dashboards/card_error.html.erb +3 -0
  43. data/app/views/easy_admin/dashboards/card_error.turbo_stream.erb +5 -0
  44. data/app/views/easy_admin/dashboards/show.turbo_stream.erb +7 -0
  45. data/app/views/easy_admin/resources/belongs_to_edit_attached.html.erb +6 -0
  46. data/app/views/easy_admin/resources/belongs_to_edit_attached.turbo_stream.erb +8 -0
  47. data/app/views/easy_admin/resources/belongs_to_reattach.html.erb +5 -0
  48. data/app/views/easy_admin/resources/edit.html.erb +1 -1
  49. data/app/views/easy_admin/resources/edit_field.html.erb +5 -0
  50. data/app/views/easy_admin/resources/edit_field.turbo_stream.erb +7 -0
  51. data/app/views/easy_admin/resources/index.html.erb +1 -1
  52. data/app/views/easy_admin/resources/index_frame.html.erb +8 -142
  53. data/app/views/easy_admin/resources/update_belongs_to_attached.turbo_stream.erb +25 -0
  54. data/app/views/layouts/easy_admin/application.html.erb +15 -2
  55. data/config/initializers/easy_admin_permissions.rb +73 -0
  56. data/db/seeds/easy_admin_permissions.rb +121 -0
  57. data/lib/easy-admin-rails.rb +2 -0
  58. data/lib/easy_admin/permissions/component.rb +168 -0
  59. data/lib/easy_admin/permissions/configuration.rb +37 -0
  60. data/lib/easy_admin/permissions/controller.rb +164 -0
  61. data/lib/easy_admin/permissions/dsl.rb +180 -0
  62. data/lib/easy_admin/permissions/models.rb +44 -0
  63. data/lib/easy_admin/permissions/permission_denied_component.rb +121 -0
  64. data/lib/easy_admin/permissions/resource_permissions.rb +231 -0
  65. data/lib/easy_admin/permissions/role_definition.rb +45 -0
  66. data/lib/easy_admin/permissions/role_denied_component.rb +159 -0
  67. data/lib/easy_admin/permissions/role_dsl.rb +73 -0
  68. data/lib/easy_admin/permissions/user_extensions.rb +129 -0
  69. data/lib/easy_admin/permissions.rb +113 -0
  70. data/lib/easy_admin/resource/base.rb +119 -0
  71. data/lib/easy_admin/resource/configuration.rb +148 -0
  72. data/lib/easy_admin/resource/dsl.rb +117 -0
  73. data/lib/easy_admin/resource/field_registry.rb +189 -0
  74. data/lib/easy_admin/resource/form_builder.rb +123 -0
  75. data/lib/easy_admin/resource/layout_builder.rb +249 -0
  76. data/lib/easy_admin/resource/scope_manager.rb +252 -0
  77. data/lib/easy_admin/resource/show_builder.rb +359 -0
  78. data/lib/easy_admin/resource.rb +8 -835
  79. data/lib/easy_admin/resource_modules.rb +11 -0
  80. data/lib/easy_admin/version.rb +1 -1
  81. data/lib/generators/easy_admin/permissions/install_generator.rb +90 -0
  82. data/lib/generators/easy_admin/permissions/templates/initializers/permissions.rb +37 -0
  83. data/lib/generators/easy_admin/permissions/templates/migrations/create_permission_tables.rb +27 -0
  84. data/lib/generators/easy_admin/permissions/templates/migrations/update_users_for_permissions.rb +6 -0
  85. data/lib/generators/easy_admin/permissions/templates/models/permission.rb +9 -0
  86. data/lib/generators/easy_admin/permissions/templates/models/role.rb +9 -0
  87. data/lib/generators/easy_admin/permissions/templates/models/role_permission.rb +9 -0
  88. data/lib/generators/easy_admin/permissions/templates/models/user_role.rb +9 -0
  89. data/lib/generators/easy_admin/permissions/templates/policies/application_policy.rb +47 -0
  90. data/lib/generators/easy_admin/permissions/templates/policies/user_policy.rb +36 -0
  91. data/lib/generators/easy_admin/permissions/templates/seeds/permissions.rb +89 -0
  92. metadata +62 -5
  93. data/db/migrate/20250101000001_create_easy_admin_admin_users.rb +0 -45
@@ -0,0 +1,186 @@
1
+ module EasyAdmin
2
+ module Permissions
3
+ class UserRolePermissionsComponent < EasyAdmin::BaseComponent
4
+ def initialize(user:)
5
+ @user = user
6
+ # Use direct role association for role info
7
+ if user && user.respond_to?(:role)
8
+ user.reload if user.persisted?
9
+ @current_role = user.role
10
+ else
11
+ @current_role = nil
12
+ end
13
+
14
+ # Get actual permissions from permissions_cache
15
+ @user_permissions = get_user_permissions_from_cache(user)
16
+ @all_permissions = EasyAdmin::Permissions::Permission.all.order(:resource_type, :action)
17
+
18
+ Rails.logger.debug "UserRolePermissionsComponent: user=#{@user&.id}, role=#{@current_role&.name}, cached_permissions=#{@user_permissions.size}"
19
+ end
20
+
21
+ def view_template
22
+ div(class: "space-y-6") do
23
+ # Current Role Section
24
+ render_current_role_section
25
+
26
+ # Individual Permissions Section (from cache)
27
+ render_permissions_section
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def render_current_role_section
34
+ div(class: "bg-white border border-gray-200 rounded-lg p-6") do
35
+ div(class: "flex items-center justify-between mb-4") do
36
+ h3(class: "text-lg font-medium text-gray-900") { "Current Role" }
37
+
38
+ if @current_role
39
+ span(class: "inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800") do
40
+ @current_role.name
41
+ end
42
+ else
43
+ span(class: "inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-800") do
44
+ "No role assigned"
45
+ end
46
+ end
47
+ end
48
+
49
+ if @current_role
50
+ if @current_role.description.present?
51
+ p(class: "text-gray-600 mb-4") { @current_role.description }
52
+ end
53
+
54
+ div(class: "grid grid-cols-3 gap-4 text-sm") do
55
+ div do
56
+ span(class: "font-medium text-gray-500") { "Role Slug:" }
57
+ p(class: "text-gray-900 font-mono") { @current_role.slug }
58
+ end
59
+
60
+ div do
61
+ span(class: "font-medium text-gray-500") { "Permissions Count:" }
62
+ p(class: "text-gray-900") { @current_role.permissions.count.to_s }
63
+ end
64
+
65
+ div do
66
+ span(class: "font-medium text-gray-500") { "Status:" }
67
+ p(class: "text-green-600 font-medium") { @current_role.active? ? "Active" : "Inactive" }
68
+ end
69
+ end
70
+ else
71
+ p(class: "text-gray-500 italic") do
72
+ "This user has not been assigned any role yet."
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ def render_permissions_section
79
+ div(class: "bg-white border border-gray-200 rounded-lg p-6") do
80
+ div(class: "mb-4") do
81
+ h3(class: "text-lg font-medium text-gray-900 mb-2") { "Individual Permissions" }
82
+ p(class: "text-sm text-gray-600") do
83
+ "The following permissions have been specifically assigned to this user:"
84
+ end
85
+ end
86
+
87
+ # Get permissions that are granted (true) from cache
88
+ granted_permission_names = @user_permissions.select { |name, granted| granted == "true" }.keys
89
+ granted_permissions = @all_permissions.select { |p| granted_permission_names.include?(p.name) }
90
+
91
+ if granted_permissions.any?
92
+ # Group permissions by resource type
93
+ grouped_permissions = granted_permissions.group_by(&:resource_type)
94
+
95
+ div(class: "space-y-6") do
96
+ grouped_permissions.each do |resource_type, resource_permissions|
97
+ render_resource_permissions_group(resource_type, resource_permissions, granted: true)
98
+ end
99
+ end
100
+ else
101
+ render_no_permissions
102
+ end
103
+ end
104
+ end
105
+
106
+ def render_resource_permissions_group(resource_type, permissions, granted: true)
107
+ div(class: "border border-gray-200 rounded-lg p-4") do
108
+ # Resource header
109
+ div(class: "flex items-center mb-3") do
110
+ h4(class: "text-md font-medium text-gray-900 capitalize") { resource_type.humanize }
111
+ span(class: "ml-2 inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-700") do
112
+ "#{permissions.count} permissions"
113
+ end
114
+ end
115
+
116
+ # Permissions grid
117
+ div(class: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3") do
118
+ permissions.each do |permission|
119
+ render_permission_card(permission)
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ def render_permission_card(permission)
126
+ div(class: "flex items-start p-3 bg-green-50 border border-green-200 rounded-lg") do
127
+ # Permission icon
128
+ div(class: "flex-shrink-0 mr-3") do
129
+ unsafe_raw '<svg class="w-5 h-5 text-green-500 mt-0.5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path></svg>'
130
+ end
131
+
132
+ # Permission details
133
+ div(class: "flex-1 min-w-0") do
134
+ div(class: "flex items-center mb-1") do
135
+ span(class: "text-sm font-medium text-gray-900 capitalize") { permission.action.humanize }
136
+ span(class: "ml-2 inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-green-100 text-green-800") do
137
+ permission.action
138
+ end
139
+ end
140
+
141
+ if permission.description.present?
142
+ p(class: "text-xs text-gray-600 leading-relaxed") { permission.description }
143
+ end
144
+
145
+ # Permission name (technical)
146
+ p(class: "text-xs text-gray-500 font-mono mt-1") { permission.name }
147
+ end
148
+ end
149
+ end
150
+
151
+ def render_no_permissions
152
+ div(class: "text-center py-8") do
153
+ unsafe_raw '<svg class="w-12 h-12 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path></svg>'
154
+ h3(class: "text-lg font-medium text-gray-900 mb-2") { "No Permissions" }
155
+ p(class: "text-gray-600") { "This role has no permissions assigned." }
156
+ end
157
+ end
158
+
159
+ def render_no_role_assigned
160
+ div(class: "text-center py-12 bg-gray-50 border border-gray-200 rounded-lg") do
161
+ unsafe_raw '<svg class="w-16 h-16 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg>'
162
+ h3(class: "text-lg font-medium text-gray-900 mb-2") { "No Role Assigned" }
163
+ p(class: "text-gray-600 mb-4") { "This user needs to be assigned a role to view their permissions." }
164
+
165
+ div(class: "text-sm text-gray-500") do
166
+ p { "Available roles: super_admin, admin, editor, moderator, viewer" }
167
+ end
168
+ end
169
+ end
170
+
171
+ # Helper method to extract permissions from permissions_cache JSON field
172
+ def get_user_permissions_from_cache(user)
173
+ return {} unless user&.permissions_cache.present?
174
+
175
+ case user.permissions_cache
176
+ when Hash
177
+ user.permissions_cache
178
+ when String
179
+ JSON.parse(user.permissions_cache) rescue {}
180
+ else
181
+ {}
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
@@ -28,10 +28,7 @@ module EasyAdmin
28
28
  div(class: "mb-6") do
29
29
  div(class: "sm:flex sm:items-center") do
30
30
  div(class: "sm:flex-auto") do
31
- h1(class: "text-2xl font-bold leading-6 text-gray-900") { @resource_class.title }
32
- p(class: "mt-2 text-sm text-gray-700") do
33
- "A list of all #{@resource_class.title.downcase} in your account."
34
- end
31
+ # Title now handled by navbar via content_for in ERB template
35
32
  end
36
33
  div(class: "mt-4 sm:ml-16 sm:mt-0 sm:flex-none") do
37
34
  render_new_resource_link
@@ -1,8 +1,9 @@
1
1
  module EasyAdmin
2
2
  class SidebarComponent < Phlex::HTML
3
- def initialize(items: nil, current_path: nil)
3
+ def initialize(items: nil, current_path: nil, current_user: nil)
4
4
  @items = items || EasyAdmin.configuration.sidebar_items
5
5
  @current_path = current_path
6
+ @current_user = current_user
6
7
  end
7
8
 
8
9
  def view_template
@@ -51,7 +52,7 @@ module EasyAdmin
51
52
  # Navigation menu
52
53
  nav(class: "flex-1 px-4 py-6 overflow-y-auto") do
53
54
  ul(class: "space-y-2") do
54
- @items.each do |item|
55
+ filtered_items.each do |item|
55
56
  render_sidebar_item(item)
56
57
  end
57
58
  end
@@ -170,5 +171,69 @@ module EasyAdmin
170
171
  end
171
172
  end
172
173
  end
174
+
175
+ # Filter items based on permissions
176
+ def filtered_items
177
+ return @items unless @current_user
178
+
179
+ @items.filter_map do |item|
180
+ if item[:children]&.any?
181
+ # This is a group/parent item
182
+ filtered_children = filter_children(item[:children])
183
+
184
+ # Hide group if no accessible children
185
+ next if filtered_children.empty?
186
+
187
+ # Return group with filtered children
188
+ item.merge(children: filtered_children)
189
+ else
190
+ # This is a simple item - check if user has access
191
+ can_access_item?(item) ? item : nil
192
+ end
193
+ end
194
+ end
195
+
196
+ def filter_children(children)
197
+ return [] unless children
198
+
199
+ children.filter_map do |child|
200
+ if child[:children]&.any?
201
+ # Nested group
202
+ filtered_nested = filter_children(child[:children])
203
+ next if filtered_nested.empty?
204
+ child.merge(children: filtered_nested)
205
+ else
206
+ # Simple child item
207
+ can_access_item?(child) ? child : nil
208
+ end
209
+ end
210
+ end
211
+
212
+ def can_access_item?(item)
213
+ # Allow non-resource items (like Dashboard, Settings, etc.)
214
+ return true unless item[:resource]
215
+
216
+ # For resource items, check permission
217
+ resource_name = item[:resource]
218
+ # Convert complex resource names to permission format
219
+ permission_resource_name = convert_resource_to_permission_name(resource_name)
220
+ permission_name = "#{permission_resource_name}:read"
221
+
222
+ EasyAdmin::Permissions.authorized?(@current_user, permission_name)
223
+ end
224
+
225
+ def convert_resource_to_permission_name(resource_name)
226
+ # Handle complex resource names like "Catalog::PaymentMethod" -> "payment_methods"
227
+ if resource_name.include?('::')
228
+ # Get the last part and convert to snake_case plural
229
+ resource_name.split('::').last.underscore.pluralize
230
+ elsif resource_name.start_with?('EasyAdmin::')
231
+ # Handle EasyAdmin resources like "EasyAdmin::AdminUser" -> "admin_users"
232
+ resource_name.sub('EasyAdmin::', '').underscore.pluralize
233
+ else
234
+ # Simple resource names, just pluralize
235
+ resource_name.underscore.pluralize
236
+ end
237
+ end
173
238
  end
174
239
  end
@@ -283,9 +283,13 @@ module EasyAdmin
283
283
  changes = {}
284
284
  current_attrs = current_item.attributes
285
285
 
286
- (old_attrs.keys + current_attrs.keys).uniq.each do |key|
286
+ # Only check fields that exist in the old_attrs (were tracked by Paper Trail)
287
+ old_attrs.keys.each do |key|
287
288
  next if %w[id updated_at].include?(key.to_s)
288
289
 
290
+ # Skip if field wasn't tracked in this version
291
+ next unless old_attrs.has_key?(key)
292
+
289
293
  old_val = normalize_value_for_comparison(old_attrs[key])
290
294
  new_val = normalize_value_for_comparison(current_attrs[key])
291
295
 
@@ -1,6 +1,7 @@
1
1
  module EasyAdmin
2
2
  class ApplicationController < ActionController::Base
3
3
  include Pagy::Backend
4
+ include ActionPolicy::Controller
4
5
 
5
6
  helper EasyAdmin::FieldsHelper
6
7
  helper EasyAdmin::PagyHelper
@@ -10,6 +11,18 @@ module EasyAdmin
10
11
  before_action :set_feature_toggles
11
12
  before_action :set_paper_trail_whodunnit
12
13
 
14
+ # ActionPolicy authorization context
15
+ authorize :user, through: :current_admin_user
16
+
17
+ # Handle ActionPolicy authorization failures
18
+ rescue_from ActionPolicy::Unauthorized, with: :handle_authorization_failure
19
+
20
+ # Configure ActionPolicy to use ApplicationPolicy as default for all models
21
+ def policy_for(record:, **opts)
22
+ # Always use ApplicationPolicy for all models (EasyAdmin and regular models)
23
+ ApplicationPolicy.new(record, **authorization_context, **opts)
24
+ end
25
+
13
26
  protected
14
27
 
15
28
  def authenticate_easy_admin_admin_user!
@@ -69,6 +82,123 @@ module EasyAdmin
69
82
  end
70
83
  end
71
84
 
72
- helper_method :current_admin_user, :admin_user_signed_in?
85
+ helper_method :current_admin_user, :admin_user_signed_in?, :flash_type_to_notification_type
86
+
87
+ private
88
+
89
+ def handle_authorization_failure(exception)
90
+ Rails.logger.warn "🚫 Authorization failed: #{exception.message}"
91
+ Rails.logger.warn "🚫 User: #{current_admin_user&.email} (Role: #{current_admin_user&.role&.name || 'None'})"
92
+ Rails.logger.warn "🚫 Action: #{action_name} on #{params[:resource_name]}"
93
+
94
+ # Extract meaningful error details
95
+ action_name = extract_action_from_exception(exception)
96
+ resource_name = extract_resource_from_exception(exception)
97
+
98
+ error_message = build_authorization_error_message(action_name, resource_name)
99
+
100
+ # Store data for templates
101
+ @error_message = error_message
102
+ @action_name = action_name
103
+ @resource_name = resource_name
104
+
105
+ respond_to do |format|
106
+ format.html do
107
+ flash[:alert] = error_message
108
+
109
+ # Try to redirect back, fallback to dashboard
110
+ redirect_path = request.referer&.start_with?(request.base_url) ? request.referer : easy_admin.root_path
111
+ redirect_to redirect_path
112
+ end
113
+
114
+ format.turbo_stream do
115
+ render template: 'easy_admin/application/authorization_failure'
116
+ end
117
+
118
+ format.json do
119
+ render json: {
120
+ error: "Access Denied",
121
+ message: error_message,
122
+ user_role: current_admin_user&.role&.name
123
+ }, status: :forbidden
124
+ end
125
+ end
126
+ end
127
+
128
+ def extract_action_from_exception(exception)
129
+ # Try to extract action from exception message
130
+ case exception.message
131
+ when /index\?/
132
+ "view"
133
+ when /show\?/
134
+ "view details of"
135
+ when /create\?/
136
+ "create"
137
+ when /update\?/
138
+ "update"
139
+ when /destroy\?/
140
+ "delete"
141
+ when /batch_action\?/
142
+ "perform batch actions on"
143
+ when /row_action\?/
144
+ "perform actions on"
145
+ when /manage_versions\?/
146
+ "manage versions of"
147
+ else
148
+ "access"
149
+ end
150
+ end
151
+
152
+ def extract_resource_from_exception(exception)
153
+ # Try to extract resource name from exception message or current context
154
+ if defined?(@resource_class) && @resource_class
155
+ # Get the model class name and clean it up for display
156
+ model_name = @resource_class.model_class.name
157
+
158
+ # Handle namespaced models (e.g., "Catalog::PaymentMethod" -> "Payment Methods")
159
+ if model_name.include?('::')
160
+ model_name.split('::').last.underscore.humanize.pluralize.downcase
161
+ else
162
+ model_name.underscore.humanize.pluralize.downcase
163
+ end
164
+ elsif params[:resource_name]
165
+ # Clean up resource name from params (e.g., "catalog/payment_methods" -> "payment methods")
166
+ if params[:resource_name].include?('/')
167
+ params[:resource_name].split('/').last.humanize.downcase
168
+ else
169
+ params[:resource_name].humanize.downcase
170
+ end
171
+ else
172
+ "this resource"
173
+ end
174
+ end
175
+
176
+ def build_authorization_error_message(action_name, resource_name)
177
+ case action_name
178
+ when "view"
179
+ "Access denied: You cannot view #{resource_name}."
180
+ when "create"
181
+ "Access denied: You cannot create new #{resource_name}."
182
+ when "update"
183
+ "Access denied: You cannot edit #{resource_name}."
184
+ when "delete"
185
+ "Access denied: You cannot delete #{resource_name}."
186
+ else
187
+ "Access denied: You cannot #{action_name} #{resource_name}."
188
+ end
189
+ end
190
+
191
+ def flash_type_to_notification_type(flash_type)
192
+ case flash_type.to_s
193
+ when 'alert', 'error'
194
+ :error
195
+ when 'notice', 'success'
196
+ :success
197
+ when 'warning'
198
+ :warning
199
+ else
200
+ :info
201
+ end
202
+ end
73
203
  end
74
204
  end
@@ -2,6 +2,7 @@ module EasyAdmin
2
2
  class BatchActionsController < ApplicationController
3
3
  before_action :set_resource_class
4
4
  before_action :set_selected_records, only: [:execute, :form]
5
+ before_action :authorize_batch_action!, only: [:execute, :form]
5
6
 
6
7
  def execute
7
8
  action_class_name = params[:action_class]
@@ -162,5 +163,31 @@ module EasyAdmin
162
163
  submit_url: submit_url
163
164
  )
164
165
  end
166
+
167
+ def authorize_batch_action!
168
+ action_class_name = params[:action_class]
169
+
170
+ # Basic permission: user must be able to update records for this resource
171
+ @selected_records.each do |record|
172
+ authorize! record, to: :update?
173
+ end
174
+
175
+ # Additional permission: user must have batch action permission for this resource
176
+ authorize! @resource_class.model_class, to: :batch_action?
177
+
178
+ # Specific batch action authorization if the action defines it
179
+ if action_class_name.present?
180
+ begin
181
+ action_class = action_class_name.constantize
182
+ action_permission = "#{@resource_class.resource_name}:#{action_class.name.underscore.split('/').last}"
183
+
184
+ unless current_admin_user && EasyAdmin::Permissions.authorized?(current_admin_user, action_permission)
185
+ raise ActionPolicy::Unauthorized, "Not authorized to perform #{action_class.name} batch action"
186
+ end
187
+ rescue NameError
188
+ # Action class doesn't exist, will be handled in the main action
189
+ end
190
+ end
191
+ end
165
192
  end
166
193
  end