easy-admin-rails 0.1.15 → 0.2.1
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.
- checksums.yaml +4 -4
- data/app/assets/builds/easy_admin.base.js +254 -18
- data/app/assets/builds/easy_admin.base.js.map +4 -4
- data/app/assets/builds/easy_admin.css +112 -18
- data/app/components/easy_admin/base_component.rb +1 -0
- data/app/components/easy_admin/form_tabs_component.rb +5 -2
- data/app/components/easy_admin/navbar_component.rb +5 -1
- data/app/components/easy_admin/permissions/user_role_assignment_component.rb +254 -0
- data/app/components/easy_admin/permissions/user_role_permissions_component.rb +186 -0
- data/app/components/easy_admin/resources/index_component.rb +1 -4
- data/app/components/easy_admin/sidebar_component.rb +67 -2
- data/app/controllers/easy_admin/application_controller.rb +131 -1
- data/app/controllers/easy_admin/batch_actions_controller.rb +27 -0
- data/app/controllers/easy_admin/concerns/belongs_to_editing.rb +201 -0
- data/app/controllers/easy_admin/concerns/inline_field_editing.rb +297 -0
- data/app/controllers/easy_admin/concerns/resource_authorization.rb +55 -0
- data/app/controllers/easy_admin/concerns/resource_filtering.rb +178 -0
- data/app/controllers/easy_admin/concerns/resource_loading.rb +149 -0
- data/app/controllers/easy_admin/concerns/resource_pagination.rb +135 -0
- data/app/controllers/easy_admin/dashboard_controller.rb +2 -1
- data/app/controllers/easy_admin/dashboards_controller.rb +6 -40
- data/app/controllers/easy_admin/resources_controller.rb +13 -762
- data/app/controllers/easy_admin/row_actions_controller.rb +25 -0
- data/app/helpers/easy_admin/fields_helper.rb +61 -9
- data/app/javascript/easy_admin/controllers/event_emitter_controller.js +2 -4
- data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +0 -10
- data/app/javascript/easy_admin/controllers/jsoneditor_controller.js +1 -4
- data/app/javascript/easy_admin/controllers/permission_toggle_controller.js +227 -0
- data/app/javascript/easy_admin/controllers/role_preview_controller.js +93 -0
- data/app/javascript/easy_admin/controllers/select_field_controller.js +1 -2
- data/app/javascript/easy_admin/controllers/settings_button_controller.js +1 -2
- data/app/javascript/easy_admin/controllers/settings_sidebar_controller.js +1 -4
- data/app/javascript/easy_admin/controllers/turbo_stream_redirect.js +0 -2
- data/app/javascript/easy_admin/controllers.js +5 -1
- data/app/models/easy_admin/admin_user.rb +6 -0
- data/app/policies/admin_user_policy.rb +36 -0
- data/app/policies/application_policy.rb +83 -0
- data/app/views/easy_admin/application/authorization_failure.turbo_stream.erb +8 -0
- data/app/views/easy_admin/dashboards/card.html.erb +5 -0
- data/app/views/easy_admin/dashboards/card.turbo_stream.erb +7 -0
- data/app/views/easy_admin/dashboards/card_error.html.erb +3 -0
- data/app/views/easy_admin/dashboards/card_error.turbo_stream.erb +5 -0
- data/app/views/easy_admin/dashboards/show.turbo_stream.erb +7 -0
- data/app/views/easy_admin/resources/belongs_to_edit_attached.html.erb +6 -0
- data/app/views/easy_admin/resources/belongs_to_edit_attached.turbo_stream.erb +8 -0
- data/app/views/easy_admin/resources/belongs_to_reattach.html.erb +5 -0
- data/app/views/easy_admin/resources/edit.html.erb +1 -1
- data/app/views/easy_admin/resources/edit_field.html.erb +5 -0
- data/app/views/easy_admin/resources/edit_field.turbo_stream.erb +7 -0
- data/app/views/easy_admin/resources/index.html.erb +1 -1
- data/app/views/easy_admin/resources/index_frame.html.erb +8 -142
- data/app/views/easy_admin/resources/update_belongs_to_attached.turbo_stream.erb +25 -0
- data/app/views/layouts/easy_admin/application.html.erb +15 -2
- data/config/initializers/easy_admin_permissions.rb +73 -0
- data/db/seeds/easy_admin_permissions.rb +121 -0
- data/lib/easy-admin-rails.rb +2 -0
- data/lib/easy_admin/permissions/component.rb +168 -0
- data/lib/easy_admin/permissions/configuration.rb +37 -0
- data/lib/easy_admin/permissions/controller.rb +164 -0
- data/lib/easy_admin/permissions/dsl.rb +160 -0
- data/lib/easy_admin/permissions/models.rb +44 -0
- data/lib/easy_admin/permissions/permission_denied_component.rb +121 -0
- data/lib/easy_admin/permissions/resource_permissions.rb +231 -0
- data/lib/easy_admin/permissions/role_definition.rb +45 -0
- data/lib/easy_admin/permissions/role_denied_component.rb +159 -0
- data/lib/easy_admin/permissions/role_dsl.rb +73 -0
- data/lib/easy_admin/permissions/user_extensions.rb +129 -0
- data/lib/easy_admin/permissions.rb +113 -0
- data/lib/easy_admin/resource/base.rb +119 -0
- data/lib/easy_admin/resource/configuration.rb +148 -0
- data/lib/easy_admin/resource/dsl.rb +117 -0
- data/lib/easy_admin/resource/field_registry.rb +189 -0
- data/lib/easy_admin/resource/form_builder.rb +123 -0
- data/lib/easy_admin/resource/layout_builder.rb +249 -0
- data/lib/easy_admin/resource/scope_manager.rb +252 -0
- data/lib/easy_admin/resource/show_builder.rb +359 -0
- data/lib/easy_admin/resource.rb +8 -835
- data/lib/easy_admin/resource_modules.rb +11 -0
- data/lib/easy_admin/version.rb +1 -1
- data/lib/generators/easy_admin/permissions/install_generator.rb +90 -0
- data/lib/generators/easy_admin/permissions/templates/initializers/permissions.rb +37 -0
- data/lib/generators/easy_admin/permissions/templates/migrations/create_permission_tables.rb +27 -0
- data/lib/generators/easy_admin/permissions/templates/migrations/update_users_for_permissions.rb +6 -0
- data/lib/generators/easy_admin/permissions/templates/models/permission.rb +9 -0
- data/lib/generators/easy_admin/permissions/templates/models/role.rb +9 -0
- data/lib/generators/easy_admin/permissions/templates/models/role_permission.rb +9 -0
- data/lib/generators/easy_admin/permissions/templates/models/user_role.rb +9 -0
- data/lib/generators/easy_admin/permissions/templates/policies/application_policy.rb +47 -0
- data/lib/generators/easy_admin/permissions/templates/policies/user_policy.rb +36 -0
- data/lib/generators/easy_admin/permissions/templates/seeds/permissions.rb +89 -0
- metadata +62 -5
- data/db/migrate/20250101000001_create_easy_admin_admin_users.rb +0 -45
@@ -0,0 +1,231 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Permissions
|
3
|
+
class ResourcePermissions
|
4
|
+
# Standard EasyAdmin actions that every resource supports
|
5
|
+
STANDARD_ACTIONS = %w[create read update delete].freeze
|
6
|
+
|
7
|
+
# Additional administrative actions
|
8
|
+
ADMIN_ACTIONS = %w[manage_versions batch_actions row_actions].freeze
|
9
|
+
|
10
|
+
# Legacy mapping for backwards compatibility
|
11
|
+
ACTION_MAPPING = {
|
12
|
+
'index' => 'read',
|
13
|
+
'show' => 'read',
|
14
|
+
'new' => 'create',
|
15
|
+
'edit' => 'update',
|
16
|
+
'destroy' => 'delete'
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
class << self
|
20
|
+
# Discover all EasyAdmin resources and their permissions
|
21
|
+
def discover_all_permissions
|
22
|
+
discovered_permissions = []
|
23
|
+
|
24
|
+
all_resource_classes.each do |resource_class|
|
25
|
+
resource_name = extract_resource_name(resource_class)
|
26
|
+
|
27
|
+
# Add standard CRUD permissions
|
28
|
+
STANDARD_ACTIONS.each do |action|
|
29
|
+
discovered_permissions << {
|
30
|
+
name: "#{resource_name}:#{action}",
|
31
|
+
resource_type: resource_name,
|
32
|
+
action: action,
|
33
|
+
description: generate_permission_description(resource_name, action)
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
# Add administrative permissions
|
38
|
+
ADMIN_ACTIONS.each do |action|
|
39
|
+
discovered_permissions << {
|
40
|
+
name: "#{resource_name}:#{action}",
|
41
|
+
resource_type: resource_name,
|
42
|
+
action: action,
|
43
|
+
description: generate_permission_description(resource_name, action)
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
# Add any custom actions defined in the resource
|
48
|
+
custom_actions = extract_custom_actions(resource_class)
|
49
|
+
custom_actions.each do |action|
|
50
|
+
discovered_permissions << {
|
51
|
+
name: "#{resource_name}:#{action}",
|
52
|
+
resource_type: resource_name,
|
53
|
+
action: action,
|
54
|
+
description: generate_permission_description(resource_name, action)
|
55
|
+
}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
discovered_permissions
|
60
|
+
end
|
61
|
+
|
62
|
+
# Get all available resource names
|
63
|
+
def available_resources
|
64
|
+
all_resource_classes.map { |resource_class| extract_resource_name(resource_class) }
|
65
|
+
end
|
66
|
+
|
67
|
+
# Get all actions for a specific resource
|
68
|
+
def actions_for_resource(resource_name)
|
69
|
+
resource_class = find_resource_class(resource_name)
|
70
|
+
return STANDARD_ACTIONS if resource_class.nil?
|
71
|
+
|
72
|
+
STANDARD_ACTIONS + extract_custom_actions(resource_class)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Seed permissions into database
|
76
|
+
def seed_permissions!
|
77
|
+
Rails.logger.info "🔐 Seeding EasyAdmin resource permissions..."
|
78
|
+
|
79
|
+
permissions_data = discover_all_permissions
|
80
|
+
created_count = 0
|
81
|
+
|
82
|
+
permissions_data.each do |permission_data|
|
83
|
+
permission = EasyAdmin::Permissions::Permission.find_or_create_by(
|
84
|
+
name: permission_data[:name]
|
85
|
+
) do |p|
|
86
|
+
p.resource_type = permission_data[:resource_type]
|
87
|
+
p.action = permission_data[:action]
|
88
|
+
p.description = permission_data[:description]
|
89
|
+
end
|
90
|
+
|
91
|
+
created_count += 1 if permission.saved_change_to_id?
|
92
|
+
end
|
93
|
+
|
94
|
+
Rails.logger.info "✅ Seeded #{created_count} new permissions (#{permissions_data.size} total)"
|
95
|
+
permissions_data.size
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
# Get all EasyAdmin resource classes
|
101
|
+
def all_resource_classes
|
102
|
+
# First try the ResourceRegistry if available
|
103
|
+
if defined?(EasyAdmin::ResourceRegistry)
|
104
|
+
registry_resources = EasyAdmin::ResourceRegistry.all_resources.values
|
105
|
+
return registry_resources if registry_resources.any?
|
106
|
+
end
|
107
|
+
|
108
|
+
# Fallback: scan for resource classes in the app
|
109
|
+
discover_resource_classes_from_filesystem
|
110
|
+
end
|
111
|
+
|
112
|
+
# Discover resource classes by scanning the filesystem
|
113
|
+
def discover_resource_classes_from_filesystem
|
114
|
+
resource_classes = []
|
115
|
+
|
116
|
+
# Check standard Rails app structure
|
117
|
+
resource_paths = [
|
118
|
+
Rails.root.join("app", "easy_admin", "resources"),
|
119
|
+
Rails.root.join("app", "admin", "resources")
|
120
|
+
]
|
121
|
+
|
122
|
+
resource_paths.each do |path|
|
123
|
+
next unless path.exist?
|
124
|
+
|
125
|
+
Dir.glob(path.join("**", "*_resource.rb")).each do |file|
|
126
|
+
# Extract class name from file path
|
127
|
+
relative_path = file.gsub("#{path}/", "").gsub(".rb", "")
|
128
|
+
class_name = relative_path.camelize
|
129
|
+
|
130
|
+
begin
|
131
|
+
resource_class = class_name.constantize
|
132
|
+
resource_classes << resource_class if resource_class < EasyAdmin::Resource
|
133
|
+
rescue NameError => e
|
134
|
+
Rails.logger.warn "Could not load resource class #{class_name}: #{e.message}"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
resource_classes
|
140
|
+
end
|
141
|
+
|
142
|
+
# Extract resource name from class (e.g., UserResource -> "user")
|
143
|
+
def extract_resource_name(resource_class)
|
144
|
+
resource_class.name.demodulize.gsub(/Resource$/, '').underscore
|
145
|
+
end
|
146
|
+
|
147
|
+
# Find resource class by name
|
148
|
+
def find_resource_class(resource_name)
|
149
|
+
class_name = "#{resource_name.camelize}Resource"
|
150
|
+
|
151
|
+
# Try direct constantize first
|
152
|
+
begin
|
153
|
+
return class_name.constantize
|
154
|
+
rescue NameError
|
155
|
+
# Not found
|
156
|
+
end
|
157
|
+
|
158
|
+
# Try with common namespaces
|
159
|
+
["", "Admin::", "EasyAdmin::"].each do |namespace|
|
160
|
+
begin
|
161
|
+
full_class_name = "#{namespace}#{class_name}"
|
162
|
+
return full_class_name.constantize
|
163
|
+
rescue NameError
|
164
|
+
next
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
nil
|
169
|
+
end
|
170
|
+
|
171
|
+
# Extract custom actions from resource class
|
172
|
+
def extract_custom_actions(resource_class)
|
173
|
+
custom_actions = []
|
174
|
+
|
175
|
+
# Look for custom controller actions by examining routes or method definitions
|
176
|
+
# This is a simplified approach - could be expanded based on EasyAdmin's routing
|
177
|
+
|
178
|
+
# Check if resource has custom action definitions
|
179
|
+
if resource_class.respond_to?(:custom_actions)
|
180
|
+
custom_actions.concat(resource_class.custom_actions)
|
181
|
+
end
|
182
|
+
|
183
|
+
# Look for batch actions
|
184
|
+
if resource_class.respond_to?(:batch_actions) && resource_class.batch_actions.any?
|
185
|
+
custom_actions << "batch_actions"
|
186
|
+
end
|
187
|
+
|
188
|
+
custom_actions.uniq
|
189
|
+
end
|
190
|
+
|
191
|
+
# Generate human-readable description for permission
|
192
|
+
def generate_permission_description(resource_name, action)
|
193
|
+
resource_humanized = resource_name.humanize.downcase
|
194
|
+
|
195
|
+
case action
|
196
|
+
when 'index'
|
197
|
+
"View list of #{resource_humanized.pluralize}"
|
198
|
+
when 'show', 'read'
|
199
|
+
"View #{resource_humanized} details"
|
200
|
+
when 'new'
|
201
|
+
"Access new #{resource_humanized} form"
|
202
|
+
when 'create'
|
203
|
+
"Create new #{resource_humanized.pluralize}"
|
204
|
+
when 'edit'
|
205
|
+
"Access #{resource_humanized} edit form"
|
206
|
+
when 'update'
|
207
|
+
"Update existing #{resource_humanized.pluralize}"
|
208
|
+
when 'destroy', 'delete'
|
209
|
+
"Delete #{resource_humanized.pluralize}"
|
210
|
+
when 'export'
|
211
|
+
"Export #{resource_humanized} data"
|
212
|
+
when 'import'
|
213
|
+
"Import #{resource_humanized} data"
|
214
|
+
when 'batch_update'
|
215
|
+
"Batch update multiple #{resource_humanized.pluralize}"
|
216
|
+
when 'batch_delete'
|
217
|
+
"Batch delete multiple #{resource_humanized.pluralize}"
|
218
|
+
when 'batch_actions'
|
219
|
+
"Perform batch actions on #{resource_humanized.pluralize}"
|
220
|
+
when 'row_actions'
|
221
|
+
"Execute row actions on #{resource_humanized.pluralize}"
|
222
|
+
when 'manage_versions'
|
223
|
+
"Manage version history for #{resource_humanized.pluralize}"
|
224
|
+
else
|
225
|
+
"#{action.humanize} #{resource_humanized.pluralize}"
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Permissions
|
3
|
+
class RoleDefinition
|
4
|
+
attr_reader :name, :slug, :description, :permissions
|
5
|
+
|
6
|
+
def initialize(name, slug: nil, description: nil)
|
7
|
+
@name = name
|
8
|
+
@slug = slug || name.parameterize
|
9
|
+
@description = description
|
10
|
+
@permissions = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
# Grant permissions for a resource
|
14
|
+
def can(actions, resource)
|
15
|
+
Array(actions).each do |action|
|
16
|
+
permission_key = "#{resource}:#{action}"
|
17
|
+
@permissions[permission_key] = true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Deny permissions for a resource (explicit)
|
22
|
+
def cannot(actions, resource)
|
23
|
+
Array(actions).each do |action|
|
24
|
+
permission_key = "#{resource}:#{action}"
|
25
|
+
@permissions[permission_key] = false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Grant all CRUD permissions for a resource
|
30
|
+
def manage(resource)
|
31
|
+
can([:read, :create, :update, :delete], resource)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Check if role has a specific permission
|
35
|
+
def has_permission?(permission_key)
|
36
|
+
@permissions[permission_key] == true
|
37
|
+
end
|
38
|
+
|
39
|
+
# Get all granted permissions
|
40
|
+
def granted_permissions
|
41
|
+
@permissions.select { |_, granted| granted == true }.keys
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Permissions
|
3
|
+
class RoleDeniedComponent < EasyAdmin::BaseComponent
|
4
|
+
def initialize(role:, user: nil, context: nil)
|
5
|
+
@role = role
|
6
|
+
@user = user
|
7
|
+
@context = context
|
8
|
+
end
|
9
|
+
|
10
|
+
def view_template
|
11
|
+
div(class: "min-h-96 flex items-center justify-center p-8") do
|
12
|
+
div(class: "text-center max-w-md mx-auto") do
|
13
|
+
# Icon
|
14
|
+
div(class: "mb-6") do
|
15
|
+
svg(class: "w-20 h-20 mx-auto text-amber-400", fill: "currentColor", viewBox: "0 0 24 24") do
|
16
|
+
path(d: "M5 16L3 14L5 12L3 10L5 8L11 14L5 16ZM19 8L21 10L19 12L21 14L19 16L13 10L19 8ZM12 2L15.09 8.26L22 9L17 14L18.18 21L12 17.77L5.82 21L7 14L2 9L8.91 8.26L12 2Z")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Title
|
21
|
+
h2(class: "text-3xl font-bold text-gray-900 mb-4") { "Role Required" }
|
22
|
+
|
23
|
+
# Description
|
24
|
+
div(class: "text-gray-600 mb-6 space-y-2") do
|
25
|
+
p { "You need a specific role to access this resource." }
|
26
|
+
if @role
|
27
|
+
div(class: "text-sm bg-amber-50 px-3 py-2 rounded-lg border border-amber-200") do
|
28
|
+
span(class: "text-gray-500") { "Required role: " }
|
29
|
+
span(class: "text-amber-700 font-semibold") { @role.to_s.humanize }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
if @context
|
33
|
+
div(class: "text-sm text-gray-500") do
|
34
|
+
span { "Context: " }
|
35
|
+
span(class: "font-medium") { @context.to_s }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# User info (if available)
|
41
|
+
if @user
|
42
|
+
div(class: "text-sm text-gray-600 mb-6 p-3 bg-gray-50 rounded-lg") do
|
43
|
+
p do
|
44
|
+
span { "Signed in as: " }
|
45
|
+
span(class: "font-medium text-gray-700") { @user.email || @user.name || "User ##{@user.id}" }
|
46
|
+
end
|
47
|
+
|
48
|
+
# Show current roles
|
49
|
+
if @user.respond_to?(:roles)
|
50
|
+
current_roles = @user.roles.active
|
51
|
+
if current_roles.any?
|
52
|
+
p(class: "mt-2") do
|
53
|
+
span { "Your current roles: " }
|
54
|
+
current_roles.each do |role|
|
55
|
+
span(class: "inline-flex items-center px-2 py-1 rounded-full text-xs bg-green-100 text-green-800 mr-1") do
|
56
|
+
role.name
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
else
|
61
|
+
p(class: "mt-2 text-gray-500 italic") { "No roles assigned" }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Show what the required role would give access to
|
66
|
+
if show_role_benefits?
|
67
|
+
div(class: "mt-3 pt-3 border-t border-gray-200") do
|
68
|
+
p(class: "text-xs text-gray-500 mb-2") { "The #{@role.to_s.humanize} role includes:" }
|
69
|
+
div(class: "text-xs text-gray-600 space-y-1") do
|
70
|
+
role_permissions.each do |permission|
|
71
|
+
div(class: "flex items-center") do
|
72
|
+
span(class: "text-green-500 mr-1") { "✓" }
|
73
|
+
span { permission.humanize }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Actions
|
83
|
+
div(class: "space-y-3") do
|
84
|
+
# Go back button
|
85
|
+
button(
|
86
|
+
onclick: "history.back()",
|
87
|
+
class: "w-full px-6 py-3 bg-amber-600 text-white rounded-lg hover:bg-amber-700 transition-colors font-medium"
|
88
|
+
) do
|
89
|
+
"← Go Back"
|
90
|
+
end
|
91
|
+
|
92
|
+
# Request role access (if configured)
|
93
|
+
if role_request_available?
|
94
|
+
a(
|
95
|
+
href: role_request_url,
|
96
|
+
class: "inline-block text-sm text-amber-600 hover:text-amber-800 transition-colors"
|
97
|
+
) do
|
98
|
+
"Request #{@role.to_s.humanize} role access"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Additional info
|
104
|
+
div(class: "mt-8 pt-6 border-t border-gray-200 text-xs text-gray-400") do
|
105
|
+
p { "Contact your administrator to request the required role." }
|
106
|
+
if Rails.env.development?
|
107
|
+
div(class: "mt-2 p-2 bg-yellow-50 rounded text-yellow-700 text-left font-mono text-xs") do
|
108
|
+
p { "Dev info:" }
|
109
|
+
p { "Required role: #{@role}" }
|
110
|
+
p { "Context: #{@context}" } if @context
|
111
|
+
p { "User ID: #{@user&.id}" }
|
112
|
+
p { "Current roles: #{@user&.roles&.pluck(:name)&.join(', ')}" } if @user&.respond_to?(:roles)
|
113
|
+
p { "Time: #{Time.current}" }
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def show_role_benefits?
|
124
|
+
role_permissions.any? && Rails.env.development?
|
125
|
+
end
|
126
|
+
|
127
|
+
def role_permissions
|
128
|
+
return [] unless defined?(EasyAdmin::Permissions::Role)
|
129
|
+
|
130
|
+
@role_permissions ||= begin
|
131
|
+
role_record = EasyAdmin::Permissions::Role.find_by(slug: @role.to_s) ||
|
132
|
+
EasyAdmin::Permissions::Role.find_by(name: @role.to_s.humanize)
|
133
|
+
role_record&.permissions&.limit(5)&.pluck(:description) || []
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def role_request_available?
|
138
|
+
respond_to?(:admin_email) ||
|
139
|
+
defined?(Rails.application.config.admin_email) ||
|
140
|
+
defined?(EasyAdmin.configuration&.support_email)
|
141
|
+
end
|
142
|
+
|
143
|
+
def role_request_url
|
144
|
+
email_subject = "Role%20Access%20Request%20-%20#{@role.to_s.humanize}"
|
145
|
+
email_body = "Hello,%0D%0A%0D%0AI%20would%20like%20to%20request%20access%20to%20the%20#{@role.to_s.humanize}%20role.%0D%0A%0D%0AThank%20you"
|
146
|
+
|
147
|
+
if respond_to?(:admin_email)
|
148
|
+
"mailto:#{admin_email}?subject=#{email_subject}&body=#{email_body}"
|
149
|
+
elsif defined?(Rails.application.config.admin_email)
|
150
|
+
"mailto:#{Rails.application.config.admin_email}?subject=#{email_subject}&body=#{email_body}"
|
151
|
+
elsif defined?(EasyAdmin.configuration&.support_email)
|
152
|
+
"mailto:#{EasyAdmin.configuration.support_email}?subject=#{email_subject}&body=#{email_body}"
|
153
|
+
else
|
154
|
+
"#"
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Permissions
|
3
|
+
class RoleDSL
|
4
|
+
attr_reader :roles
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@roles = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
# Define a role with permissions
|
11
|
+
def role(name, slug: nil, description: nil, &block)
|
12
|
+
role_def = RoleDefinition.new(name, slug: slug, description: description)
|
13
|
+
|
14
|
+
if block_given?
|
15
|
+
RolePermissionDSL.new(role_def).instance_eval(&block)
|
16
|
+
end
|
17
|
+
|
18
|
+
@roles[role_def.slug] = role_def
|
19
|
+
role_def
|
20
|
+
end
|
21
|
+
|
22
|
+
# Get role by slug
|
23
|
+
def get_role(slug)
|
24
|
+
@roles[slug.to_s]
|
25
|
+
end
|
26
|
+
|
27
|
+
# Get all role slugs
|
28
|
+
def role_slugs
|
29
|
+
@roles.keys
|
30
|
+
end
|
31
|
+
|
32
|
+
# Get all roles
|
33
|
+
def all_roles
|
34
|
+
@roles.values
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class RolePermissionDSL
|
39
|
+
def initialize(role_definition)
|
40
|
+
@role = role_definition
|
41
|
+
end
|
42
|
+
|
43
|
+
# Grant permissions
|
44
|
+
def can(actions, resource)
|
45
|
+
@role.can(actions, resource)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Deny permissions
|
49
|
+
def cannot(actions, resource)
|
50
|
+
@role.cannot(actions, resource)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Grant all CRUD permissions
|
54
|
+
def manage(resource)
|
55
|
+
@role.manage(resource)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Grant permissions for all registered EasyAdmin resources
|
59
|
+
def manage_all_resources
|
60
|
+
EasyAdmin::Permissions.available_resources.each do |resource|
|
61
|
+
manage(resource)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Grant read-only access to all resources
|
66
|
+
def read_all_resources
|
67
|
+
EasyAdmin::Permissions.available_resources.each do |resource|
|
68
|
+
can(:read, resource)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Permissions
|
3
|
+
module UserExtensions
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
has_many :user_roles, class_name: 'EasyAdmin::Permissions::UserRole', foreign_key: 'user_id', dependent: :destroy
|
8
|
+
has_many :roles, through: :user_roles, class_name: 'EasyAdmin::Permissions::Role'
|
9
|
+
has_many :permissions, through: :roles, class_name: 'EasyAdmin::Permissions::Permission'
|
10
|
+
|
11
|
+
# Cache for computed permissions (JSON columns handle serialization automatically)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Check if user has specific permission
|
15
|
+
def has_permission?(permission_name, context: nil)
|
16
|
+
return false unless EasyAdmin::Permissions.enabled?
|
17
|
+
return false unless respond_to?(:permissions_cache)
|
18
|
+
return false unless permissions_cache.present?
|
19
|
+
|
20
|
+
# For now, ignore context and just use flat permission structure
|
21
|
+
permission_value = permissions_cache[permission_name.to_s]
|
22
|
+
|
23
|
+
# Check if permission exists in cache (handle both string and boolean values)
|
24
|
+
return true if permission_value == "true" || permission_value == true
|
25
|
+
return false if permission_value == "false" || permission_value == false
|
26
|
+
|
27
|
+
# If not explicitly set, default to false
|
28
|
+
false
|
29
|
+
end
|
30
|
+
|
31
|
+
# Check if user has specific role
|
32
|
+
def has_role?(role_name, context: nil)
|
33
|
+
user_roles.active.includes(:role).any? do |user_role|
|
34
|
+
next false if context && user_role.context != context
|
35
|
+
user_role.role.slug == role_name.to_s || user_role.role.name == role_name.to_s
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Get all user permissions for a context
|
40
|
+
def calculate_permissions(context: nil)
|
41
|
+
return [] unless EasyAdmin::Permissions.enabled?
|
42
|
+
return [] unless respond_to?(:permissions_cache)
|
43
|
+
return [] unless permissions_cache.present?
|
44
|
+
|
45
|
+
# Return only permissions that are explicitly set to true
|
46
|
+
permissions_cache.select { |k, v| v == "true" || v == true }.keys
|
47
|
+
end
|
48
|
+
|
49
|
+
# Set user permissions directly in cache
|
50
|
+
def set_permission(permission_name, value = true, context: nil)
|
51
|
+
return false unless EasyAdmin::Permissions.enabled?
|
52
|
+
return false unless respond_to?(:permissions_cache=)
|
53
|
+
|
54
|
+
context_key = context&.cache_key || 'global'
|
55
|
+
current_cache = permissions_cache || {}
|
56
|
+
|
57
|
+
if context
|
58
|
+
current_cache[context_key] ||= {}
|
59
|
+
current_cache[context_key][permission_name.to_s] = value.to_s
|
60
|
+
else
|
61
|
+
current_cache[permission_name.to_s] = value.to_s
|
62
|
+
end
|
63
|
+
|
64
|
+
if persisted?
|
65
|
+
update_column(:permissions_cache, current_cache)
|
66
|
+
else
|
67
|
+
self.permissions_cache = current_cache
|
68
|
+
end
|
69
|
+
|
70
|
+
true
|
71
|
+
end
|
72
|
+
|
73
|
+
# Remove permission from cache
|
74
|
+
def remove_permission(permission_name, context: nil)
|
75
|
+
return false unless EasyAdmin::Permissions.enabled?
|
76
|
+
return false unless respond_to?(:permissions_cache=)
|
77
|
+
|
78
|
+
context_key = context&.cache_key || 'global'
|
79
|
+
current_cache = permissions_cache || {}
|
80
|
+
|
81
|
+
if context && current_cache[context_key]
|
82
|
+
current_cache[context_key].delete(permission_name.to_s)
|
83
|
+
else
|
84
|
+
current_cache.delete(permission_name.to_s)
|
85
|
+
end
|
86
|
+
|
87
|
+
if persisted?
|
88
|
+
update_column(:permissions_cache, current_cache)
|
89
|
+
else
|
90
|
+
self.permissions_cache = current_cache
|
91
|
+
end
|
92
|
+
|
93
|
+
true
|
94
|
+
end
|
95
|
+
|
96
|
+
# Get all roles for user in specific context
|
97
|
+
def roles_for_context(context = nil)
|
98
|
+
if context
|
99
|
+
user_roles.active.for_context(context).includes(:role).map(&:role)
|
100
|
+
else
|
101
|
+
user_roles.active.global.includes(:role).map(&:role)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Clear permission cache
|
106
|
+
def clear_permissions_cache!
|
107
|
+
if respond_to?(:permissions_cache=)
|
108
|
+
if persisted?
|
109
|
+
update_column(:permissions_cache, {})
|
110
|
+
else
|
111
|
+
self.permissions_cache = {}
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def find_role(role_identifier)
|
119
|
+
if role_identifier.is_a?(String) || role_identifier.is_a?(Symbol)
|
120
|
+
EasyAdmin::Permissions::Role.find_by(slug: role_identifier.to_s) ||
|
121
|
+
EasyAdmin::Permissions::Role.find_by(name: role_identifier.to_s)
|
122
|
+
else
|
123
|
+
role_identifier
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|