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,168 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Permissions
|
3
|
+
module Component
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
# Check if current user has permission
|
7
|
+
def current_user_can?(permission_name, context: nil)
|
8
|
+
current_user = helpers.current_user if helpers.respond_to?(:current_user)
|
9
|
+
EasyAdmin::Permissions.authorized?(current_user, permission_name, context: context)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Check if current user has role
|
13
|
+
def current_user_has_role?(role_name, context: nil)
|
14
|
+
current_user = helpers.current_user if helpers.respond_to?(:current_user)
|
15
|
+
EasyAdmin::Permissions.has_role?(current_user, role_name, context: context)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Render content only if user has permission
|
19
|
+
def if_can(permission_name, context: nil, &block)
|
20
|
+
if current_user_can?(permission_name, context: context)
|
21
|
+
block.call if block_given?
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Render content only if user has role
|
26
|
+
def if_has_role(role_name, context: nil, &block)
|
27
|
+
if current_user_has_role?(role_name, context: context)
|
28
|
+
block.call if block_given?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Render content if user DOESN'T have permission
|
33
|
+
def unless_can(permission_name, context: nil, &block)
|
34
|
+
unless current_user_can?(permission_name, context: context)
|
35
|
+
block.call if block_given?
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Render content if user DOESN'T have role
|
40
|
+
def unless_has_role(role_name, context: nil, &block)
|
41
|
+
unless current_user_has_role?(role_name, context: context)
|
42
|
+
block.call if block_given?
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Conditional CSS classes based on permissions
|
47
|
+
def permission_classes(permission_name, enabled_classes: "", disabled_classes: "opacity-50 cursor-not-allowed", context: nil)
|
48
|
+
if current_user_can?(permission_name, context: context)
|
49
|
+
enabled_classes
|
50
|
+
else
|
51
|
+
disabled_classes
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Conditional attributes based on permissions
|
56
|
+
def permission_attrs(permission_name, enabled_attrs: {}, disabled_attrs: {}, context: nil)
|
57
|
+
if current_user_can?(permission_name, context: context)
|
58
|
+
enabled_attrs
|
59
|
+
else
|
60
|
+
disabled_attrs
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Generate link with permission check
|
65
|
+
def permission_link(text, href, permission_name, context: nil, **attrs, &block)
|
66
|
+
if current_user_can?(permission_name, context: context)
|
67
|
+
a(href: href, **attrs) do
|
68
|
+
if block_given?
|
69
|
+
block.call
|
70
|
+
else
|
71
|
+
text
|
72
|
+
end
|
73
|
+
end
|
74
|
+
else
|
75
|
+
span(class: "text-gray-400 cursor-not-allowed", **attrs.except(:href, :data)) do
|
76
|
+
if block_given?
|
77
|
+
block.call
|
78
|
+
else
|
79
|
+
text
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Generate button with permission check
|
86
|
+
def permission_button(text = nil, permission_name:, context: nil, disabled_class: "opacity-50 cursor-not-allowed", **attrs, &block)
|
87
|
+
can_access = current_user_can?(permission_name, context: context)
|
88
|
+
|
89
|
+
button_attrs = attrs.dup
|
90
|
+
button_attrs[:disabled] = true unless can_access
|
91
|
+
button_attrs[:class] = [button_attrs[:class], disabled_class].compact.join(" ") unless can_access
|
92
|
+
|
93
|
+
button(**button_attrs) do
|
94
|
+
if block_given?
|
95
|
+
block.call
|
96
|
+
elsif text
|
97
|
+
text
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Render form field only if user can edit
|
103
|
+
def permission_field(permission_name, context: nil, readonly_class: "bg-gray-100", &block)
|
104
|
+
can_edit = current_user_can?(permission_name, context: context)
|
105
|
+
|
106
|
+
if can_edit
|
107
|
+
block.call if block_given?
|
108
|
+
else
|
109
|
+
# Render read-only version
|
110
|
+
div(class: readonly_class) do
|
111
|
+
block.call if block_given?
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Show different content based on multiple permission checks
|
117
|
+
def permission_case(context: nil, &block)
|
118
|
+
permission_case_builder = PermissionCaseBuilder.new(self, context)
|
119
|
+
permission_case_builder.instance_eval(&block) if block_given?
|
120
|
+
permission_case_builder.render
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
# Helper class for building conditional permission rendering
|
126
|
+
class PermissionCaseBuilder
|
127
|
+
def initialize(component, context)
|
128
|
+
@component = component
|
129
|
+
@context = context
|
130
|
+
@cases = []
|
131
|
+
@else_block = nil
|
132
|
+
end
|
133
|
+
|
134
|
+
def when_can(permission_name, &block)
|
135
|
+
@cases << { type: :permission, name: permission_name, block: block }
|
136
|
+
self
|
137
|
+
end
|
138
|
+
|
139
|
+
def when_has_role(role_name, &block)
|
140
|
+
@cases << { type: :role, name: role_name, block: block }
|
141
|
+
self
|
142
|
+
end
|
143
|
+
|
144
|
+
def otherwise(&block)
|
145
|
+
@else_block = block
|
146
|
+
self
|
147
|
+
end
|
148
|
+
|
149
|
+
def render
|
150
|
+
@cases.each do |case_item|
|
151
|
+
case case_item[:type]
|
152
|
+
when :permission
|
153
|
+
if @component.current_user_can?(case_item[:name], context: @context)
|
154
|
+
return case_item[:block].call if case_item[:block]
|
155
|
+
end
|
156
|
+
when :role
|
157
|
+
if @component.current_user_has_role?(case_item[:name], context: @context)
|
158
|
+
return case_item[:block].call if case_item[:block]
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
@else_block&.call
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Permissions
|
3
|
+
class Configuration
|
4
|
+
attr_accessor :enabled,
|
5
|
+
:cache_duration,
|
6
|
+
:admin_bypass,
|
7
|
+
:user_model,
|
8
|
+
:context_types
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@enabled = false
|
12
|
+
@cache_duration = 1.hour
|
13
|
+
@admin_bypass = true
|
14
|
+
@user_model = 'User'
|
15
|
+
@context_types = [] # e.g., ['Organization', 'Project']
|
16
|
+
end
|
17
|
+
|
18
|
+
# Define available permission contexts
|
19
|
+
def contexts(*types)
|
20
|
+
@context_types = types.map(&:to_s)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Set the user model class
|
24
|
+
def user_class(klass = nil)
|
25
|
+
if klass
|
26
|
+
@user_model = klass.to_s
|
27
|
+
else
|
28
|
+
@user_model.constantize
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def user_class_name
|
33
|
+
@user_model
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Permissions
|
3
|
+
module Controller
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
# Include Action Policy authorization
|
8
|
+
include ActionPolicy::Controller if defined?(ActionPolicy)
|
9
|
+
|
10
|
+
# Set up authorization context for Action Policy
|
11
|
+
if respond_to?(:authorize)
|
12
|
+
authorize :user, through: :current_user
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Check if current user has permission
|
17
|
+
def current_user_can?(permission_name, context: nil)
|
18
|
+
EasyAdmin::Permissions.authorized?(current_user, permission_name, context: context)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Check if current user has role
|
22
|
+
def current_user_has_role?(role_name, context: nil)
|
23
|
+
EasyAdmin::Permissions.has_role?(current_user, role_name, context: context)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Require permission or show 403 error
|
27
|
+
def require_permission!(permission_name, context: nil)
|
28
|
+
unless current_user_can?(permission_name, context: context)
|
29
|
+
handle_permission_denied(permission_name)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Require role or show 403 error
|
34
|
+
def require_role!(role_name, context: nil)
|
35
|
+
unless current_user_has_role?(role_name, context: context)
|
36
|
+
handle_role_denied(role_name)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Before action to check permissions for CRUD operations
|
41
|
+
def check_permissions_for_action
|
42
|
+
action = action_name.to_s
|
43
|
+
resource_name = controller_name
|
44
|
+
|
45
|
+
permission_map = {
|
46
|
+
'index' => "#{resource_name}:read",
|
47
|
+
'show' => "#{resource_name}:read",
|
48
|
+
'new' => "#{resource_name}:create",
|
49
|
+
'create' => "#{resource_name}:create",
|
50
|
+
'edit' => "#{resource_name}:update",
|
51
|
+
'update' => "#{resource_name}:update",
|
52
|
+
'destroy' => "#{resource_name}:delete"
|
53
|
+
}
|
54
|
+
|
55
|
+
if permission_name = permission_map[action]
|
56
|
+
require_permission!(permission_name)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Get current user's permissions for view helpers
|
61
|
+
def current_user_permissions(context: nil)
|
62
|
+
return [] unless current_user
|
63
|
+
EasyAdmin::Permissions.user_permissions(current_user, context: context)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Check permission in views (helper method)
|
67
|
+
def can?(permission_name, context: nil)
|
68
|
+
current_user_can?(permission_name, context: context)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Check role in views (helper method)
|
72
|
+
def has_role?(role_name, context: nil)
|
73
|
+
current_user_has_role?(role_name, context: context)
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def handle_permission_denied(permission_name)
|
79
|
+
respond_to do |format|
|
80
|
+
format.html do
|
81
|
+
if request.xhr? || request.headers['Content-Type'] == 'text/html'
|
82
|
+
render plain: permission_denied_component(permission_name).call, status: :forbidden
|
83
|
+
else
|
84
|
+
redirect_to root_path, alert: "Permission denied: #{permission_name}"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
format.json { render json: { error: "Permission denied: #{permission_name}" }, status: :forbidden }
|
88
|
+
format.turbo_stream { render turbo_stream: turbo_stream.replace("main", permission_denied_component(permission_name).call) }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def handle_role_denied(role_name)
|
93
|
+
respond_to do |format|
|
94
|
+
format.html do
|
95
|
+
if request.xhr? || request.headers['Content-Type'] == 'text/html'
|
96
|
+
render plain: role_denied_component(role_name).call, status: :forbidden
|
97
|
+
else
|
98
|
+
redirect_to root_path, alert: "Role required: #{role_name}"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
format.json { render json: { error: "Role required: #{role_name}" }, status: :forbidden }
|
102
|
+
format.turbo_stream { render turbo_stream: turbo_stream.replace("main", role_denied_component(role_name).call) }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def permission_denied_component(permission_name)
|
107
|
+
if defined?(EasyAdmin::Permissions::PermissionDeniedComponent)
|
108
|
+
EasyAdmin::Permissions::PermissionDeniedComponent.new(permission: permission_name, user: current_user)
|
109
|
+
else
|
110
|
+
# Fallback component
|
111
|
+
BasicPermissionDeniedComponent.new(permission: permission_name)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def role_denied_component(role_name)
|
116
|
+
if defined?(EasyAdmin::Permissions::RoleDeniedComponent)
|
117
|
+
EasyAdmin::Permissions::RoleDeniedComponent.new(role: role_name, user: current_user)
|
118
|
+
else
|
119
|
+
# Fallback component
|
120
|
+
BasicRoleDeniedComponent.new(role: role_name)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Basic fallback components for when full components aren't defined
|
125
|
+
class BasicPermissionDeniedComponent
|
126
|
+
def initialize(permission:)
|
127
|
+
@permission = permission
|
128
|
+
end
|
129
|
+
|
130
|
+
def call
|
131
|
+
<<~HTML
|
132
|
+
<div class="flex items-center justify-center min-h-96">
|
133
|
+
<div class="text-center">
|
134
|
+
<div class="text-6xl text-gray-400 mb-4">🔒</div>
|
135
|
+
<h2 class="text-2xl font-bold text-gray-900 mb-2">Permission Denied</h2>
|
136
|
+
<p class="text-gray-600">You need the '#{@permission}' permission to access this resource.</p>
|
137
|
+
<a href="javascript:history.back()" class="inline-block mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">Go Back</a>
|
138
|
+
</div>
|
139
|
+
</div>
|
140
|
+
HTML
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
class BasicRoleDeniedComponent
|
145
|
+
def initialize(role:)
|
146
|
+
@role = role
|
147
|
+
end
|
148
|
+
|
149
|
+
def call
|
150
|
+
<<~HTML
|
151
|
+
<div class="flex items-center justify-center min-h-96">
|
152
|
+
<div class="text-center">
|
153
|
+
<div class="text-6xl text-gray-400 mb-4">👑</div>
|
154
|
+
<h2 class="text-2xl font-bold text-gray-900 mb-2">Role Required</h2>
|
155
|
+
<p class="text-gray-600">You need the '#{@role}' role to access this resource.</p>
|
156
|
+
<a href="javascript:history.back()" class="inline-block mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">Go Back</a>
|
157
|
+
</div>
|
158
|
+
</div>
|
159
|
+
HTML
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Permissions
|
3
|
+
class DSL
|
4
|
+
def initialize
|
5
|
+
@permissions = []
|
6
|
+
@roles = []
|
7
|
+
end
|
8
|
+
|
9
|
+
# Define a permission
|
10
|
+
def permission(resource_type, action, description: nil, conditions: {})
|
11
|
+
name = "#{resource_type}:#{action}"
|
12
|
+
|
13
|
+
@permissions << {
|
14
|
+
name: name,
|
15
|
+
resource_type: resource_type.to_s,
|
16
|
+
action: action.to_s,
|
17
|
+
description: description || "#{action.to_s.humanize} #{resource_type.to_s.humanize}",
|
18
|
+
conditions: conditions
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
# Define a role with permissions
|
23
|
+
def role(name, slug: nil, description: nil, permissions: [], active: true, metadata: {})
|
24
|
+
@roles << {
|
25
|
+
name: name.to_s.humanize,
|
26
|
+
slug: slug&.to_s || name.to_s.parameterize,
|
27
|
+
description: description,
|
28
|
+
permissions: permissions.map(&:to_s),
|
29
|
+
active: active,
|
30
|
+
metadata: metadata
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
# Shortcut methods for common permission patterns
|
35
|
+
def crud_permissions(resource_type, description_prefix: nil)
|
36
|
+
prefix = description_prefix || resource_type.to_s.humanize
|
37
|
+
|
38
|
+
permission resource_type, :read, description: "View #{prefix.downcase}"
|
39
|
+
permission resource_type, :create, description: "Create new #{prefix.downcase.singularize}"
|
40
|
+
permission resource_type, :update, description: "Edit existing #{prefix.downcase}"
|
41
|
+
permission resource_type, :delete, description: "Delete #{prefix.downcase}"
|
42
|
+
end
|
43
|
+
|
44
|
+
def admin_permissions(resource_type, description_prefix: nil)
|
45
|
+
crud_permissions(resource_type, description_prefix: description_prefix)
|
46
|
+
permission resource_type, :manage, description: "Full management of #{(description_prefix || resource_type.to_s.humanize).downcase}"
|
47
|
+
end
|
48
|
+
|
49
|
+
# Resource shortcuts
|
50
|
+
def resource(resource_type, actions: [:read, :create, :update, :delete], description_prefix: nil)
|
51
|
+
prefix = description_prefix || resource_type.to_s.humanize
|
52
|
+
|
53
|
+
actions.each do |action|
|
54
|
+
case action
|
55
|
+
when :read
|
56
|
+
permission resource_type, :read, description: "View #{prefix.downcase}"
|
57
|
+
when :create
|
58
|
+
permission resource_type, :create, description: "Create new #{prefix.downcase.singularize}"
|
59
|
+
when :update
|
60
|
+
permission resource_type, :update, description: "Edit existing #{prefix.downcase}"
|
61
|
+
when :delete
|
62
|
+
permission resource_type, :delete, description: "Delete #{prefix.downcase}"
|
63
|
+
else
|
64
|
+
permission resource_type, action, description: "#{action.to_s.humanize} #{prefix.downcase}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# EasyAdmin resource shortcuts using actual resource discovery
|
70
|
+
def easy_admin_resource(resource_name, actions: nil)
|
71
|
+
available_actions = EasyAdmin::Permissions::ResourcePermissions.actions_for_resource(resource_name)
|
72
|
+
actions_to_create = actions || available_actions
|
73
|
+
|
74
|
+
actions_to_create.each do |action|
|
75
|
+
next unless available_actions.include?(action.to_s)
|
76
|
+
|
77
|
+
permission_data = EasyAdmin::Permissions::ResourcePermissions.discover_all_permissions
|
78
|
+
.find { |p| p[:resource_type] == resource_name.to_s && p[:action] == action.to_s }
|
79
|
+
|
80
|
+
if permission_data
|
81
|
+
permission resource_name, action, description: permission_data[:description]
|
82
|
+
else
|
83
|
+
permission resource_name, action
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Auto-discover and create permissions for all EasyAdmin resources
|
89
|
+
def auto_discover_resources(actions: nil)
|
90
|
+
EasyAdmin::Permissions::ResourcePermissions.available_resources.each do |resource_name|
|
91
|
+
easy_admin_resource(resource_name, actions: actions)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Enhanced role definition with EasyAdmin resource support
|
96
|
+
def easy_admin_role(name, slug: nil, description: nil, resources: {}, permissions: [], active: true, metadata: {})
|
97
|
+
# Convert resources hash to permission names
|
98
|
+
resource_permissions = []
|
99
|
+
|
100
|
+
resources.each do |resource_name, resource_actions|
|
101
|
+
Array(resource_actions).each do |action|
|
102
|
+
resource_permissions << "#{resource_name}:#{action}"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Combine with manual permissions
|
107
|
+
all_permissions = resource_permissions + permissions.map(&:to_s)
|
108
|
+
|
109
|
+
role(name, slug: slug, description: description, permissions: all_permissions, active: active, metadata: metadata)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Execute the DSL block and return the collected data
|
113
|
+
def self.evaluate(&block)
|
114
|
+
dsl = new
|
115
|
+
dsl.instance_eval(&block) if block_given?
|
116
|
+
{
|
117
|
+
permissions: dsl.instance_variable_get(:@permissions),
|
118
|
+
roles: dsl.instance_variable_get(:@roles)
|
119
|
+
}
|
120
|
+
end
|
121
|
+
|
122
|
+
# Create roles in the database (simplified - no permissions table)
|
123
|
+
def self.seed_database(data)
|
124
|
+
# Skip if tables don't exist yet (during migrations or first setup)
|
125
|
+
return unless ActiveRecord::Base.connection.table_exists?(:easy_admin_roles)
|
126
|
+
|
127
|
+
# Defer execution until Rails is fully initialized
|
128
|
+
if defined?(Rails) && Rails.application && !Rails.application.initialized?
|
129
|
+
Rails.application.config.after_initialize do
|
130
|
+
# Check if models are available (avoid loading during initialization)
|
131
|
+
begin
|
132
|
+
role_class = "EasyAdmin::Permissions::Role".constantize
|
133
|
+
rescue NameError => e
|
134
|
+
Rails.logger.debug "EasyAdmin::Permissions models not yet loaded: #{e.message}"
|
135
|
+
return
|
136
|
+
end
|
137
|
+
|
138
|
+
ActiveRecord::Base.transaction do
|
139
|
+
# Create roles only (permissions are managed via JSON field)
|
140
|
+
data[:roles].each do |role_data|
|
141
|
+
role = role_class.find_or_initialize_by(slug: role_data[:slug])
|
142
|
+
role.assign_attributes(
|
143
|
+
name: role_data[:name],
|
144
|
+
description: role_data[:description],
|
145
|
+
)
|
146
|
+
role.save! if role.changed?
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Main method to define permissions and roles
|
155
|
+
def self.define(&block)
|
156
|
+
data = DSL.evaluate(&block)
|
157
|
+
DSL.seed_database(data)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Permissions
|
3
|
+
module Models
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
# Include this in your Role model
|
7
|
+
module Role
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
has_many :user_roles, class_name: 'EasyAdmin::Permissions::UserRole', dependent: :destroy
|
12
|
+
has_many :users, through: :user_roles, class_name: EasyAdmin::Permissions.configuration.user_class_name
|
13
|
+
|
14
|
+
validates :name, presence: true, uniqueness: true
|
15
|
+
validates :slug, presence: true, uniqueness: true
|
16
|
+
|
17
|
+
before_validation :generate_slug, if: -> { slug.blank? }
|
18
|
+
|
19
|
+
scope :active, -> { where(active: true) }
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def generate_slug
|
25
|
+
self.slug = name&.parameterize
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Include this in your UserRole model (join table)
|
30
|
+
module UserRole
|
31
|
+
extend ActiveSupport::Concern
|
32
|
+
|
33
|
+
included do
|
34
|
+
belongs_to :user, class_name: EasyAdmin::Permissions.configuration.user_class_name, foreign_key: 'user_id'
|
35
|
+
belongs_to :role, class_name: 'EasyAdmin::Permissions::Role'
|
36
|
+
|
37
|
+
validates :user_id, uniqueness: { scope: :role_id }
|
38
|
+
|
39
|
+
scope :active, -> { where(active: true) }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Permissions
|
3
|
+
class PermissionDeniedComponent < EasyAdmin::BaseComponent
|
4
|
+
def initialize(permission:, user: nil, context: nil)
|
5
|
+
@permission = permission
|
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-gray-400", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24") do
|
16
|
+
path(stroke_linecap: "round", stroke_linejoin: "round", stroke_width: "2",
|
17
|
+
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")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Title
|
22
|
+
h2(class: "text-3xl font-bold text-gray-900 mb-4") { "Access Denied" }
|
23
|
+
|
24
|
+
# Description
|
25
|
+
div(class: "text-gray-600 mb-6 space-y-2") do
|
26
|
+
p { "You don't have permission to access this resource." }
|
27
|
+
if @permission
|
28
|
+
div(class: "text-sm bg-gray-100 px-3 py-2 rounded-lg font-mono") do
|
29
|
+
span(class: "text-gray-500") { "Required permission: " }
|
30
|
+
span(class: "text-red-600 font-semibold") { @permission }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
if @context
|
34
|
+
div(class: "text-sm text-gray-500") do
|
35
|
+
span { "Context: " }
|
36
|
+
span(class: "font-medium") { @context.to_s }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# User info (if available)
|
42
|
+
if @user
|
43
|
+
div(class: "text-sm text-gray-500 mb-6 p-3 bg-gray-50 rounded-lg") do
|
44
|
+
p do
|
45
|
+
span { "Signed in as: " }
|
46
|
+
span(class: "font-medium text-gray-700") { @user.email || @user.name || "User ##{@user.id}" }
|
47
|
+
end
|
48
|
+
if @user.respond_to?(:roles) && @user.roles.any?
|
49
|
+
p(class: "mt-2") do
|
50
|
+
span { "Your roles: " }
|
51
|
+
@user.roles.active.each_with_index do |role, index|
|
52
|
+
span(class: "inline-flex items-center px-2 py-1 rounded-full text-xs bg-blue-100 text-blue-800 mr-1") do
|
53
|
+
role.name
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Actions
|
62
|
+
div(class: "space-y-3") do
|
63
|
+
# Go back button
|
64
|
+
button(
|
65
|
+
onclick: "history.back()",
|
66
|
+
class: "w-full px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium"
|
67
|
+
) do
|
68
|
+
"← Go Back"
|
69
|
+
end
|
70
|
+
|
71
|
+
# Contact admin link (if configured)
|
72
|
+
if support_contact_available?
|
73
|
+
a(
|
74
|
+
href: support_contact_url,
|
75
|
+
class: "inline-block text-sm text-blue-600 hover:text-blue-800 transition-colors"
|
76
|
+
) do
|
77
|
+
"Contact administrator for access"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Additional info
|
83
|
+
div(class: "mt-8 pt-6 border-t border-gray-200 text-xs text-gray-400") do
|
84
|
+
p { "If you believe this is an error, please contact your administrator." }
|
85
|
+
if Rails.env.development?
|
86
|
+
div(class: "mt-2 p-2 bg-yellow-50 rounded text-yellow-700 text-left font-mono text-xs") do
|
87
|
+
p { "Dev info:" }
|
88
|
+
p { "Permission: #{@permission}" }
|
89
|
+
p { "Context: #{@context}" } if @context
|
90
|
+
p { "User ID: #{@user&.id}" }
|
91
|
+
p { "Time: #{Time.current}" }
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def support_contact_available?
|
102
|
+
# Check if there's a support contact configured
|
103
|
+
respond_to?(:admin_email) ||
|
104
|
+
defined?(Rails.application.config.admin_email) ||
|
105
|
+
defined?(EasyAdmin.configuration&.support_email)
|
106
|
+
end
|
107
|
+
|
108
|
+
def support_contact_url
|
109
|
+
if respond_to?(:admin_email)
|
110
|
+
"mailto:#{admin_email}?subject=Access%20Request&body=I%20need%20access%20to%20#{@permission}"
|
111
|
+
elsif defined?(Rails.application.config.admin_email)
|
112
|
+
"mailto:#{Rails.application.config.admin_email}?subject=Access%20Request"
|
113
|
+
elsif defined?(EasyAdmin.configuration&.support_email)
|
114
|
+
"mailto:#{EasyAdmin.configuration.support_email}?subject=Access%20Request"
|
115
|
+
else
|
116
|
+
"#"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|