cccux 0.1.0 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +66 -0
- data/README.md +124 -112
- data/Rakefile +57 -4
- data/app/assets/stylesheets/cccux/application.css +96 -72
- data/app/controllers/cccux/ability_permissions_controller.rb +138 -33
- data/app/controllers/cccux/application_controller.rb +7 -0
- data/app/controllers/cccux/cccux_controller.rb +20 -10
- data/app/controllers/cccux/dashboard_controller.rb +203 -32
- data/app/controllers/cccux/role_abilities_controller.rb +70 -0
- data/app/controllers/cccux/roles_controller.rb +70 -81
- data/app/controllers/cccux/users_controller.rb +22 -7
- data/app/controllers/concerns/cccux/application_controller_concern.rb +6 -2
- data/app/helpers/cccux/authorization_helper.rb +29 -21
- data/app/models/cccux/ability.rb +83 -32
- data/app/models/cccux/ability_permission.rb +9 -0
- data/app/models/cccux/post.rb +5 -0
- data/app/models/cccux/role.rb +19 -1
- data/app/models/cccux/role_ability.rb +3 -0
- data/app/models/concerns/cccux/user_concern.rb +5 -2
- data/app/views/cccux/ability_permissions/new.html.erb +2 -2
- data/app/views/cccux/dashboard/model_discovery.html.erb +7 -2
- data/app/views/cccux/roles/_form.html.erb +24 -71
- data/app/views/cccux/roles/edit.html.erb +5 -5
- data/app/views/cccux/roles/index.html.erb +1 -8
- data/app/views/cccux/roles/new.html.erb +1 -3
- data/app/views/cccux/users/edit.html.erb +4 -4
- data/app/views/cccux/users/new.html.erb +30 -15
- data/app/views/layouts/cccux/admin.html.erb +1 -2
- data/app/views/shared/_footer.html.erb +1 -1
- data/config/routes.rb +7 -6
- data/lib/cccux/engine.rb +7 -6
- data/lib/cccux/version.rb +1 -1
- data/lib/cccux.rb +12 -0
- data/lib/tasks/cccux.rake +271 -159
- metadata +10 -22
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
module Cccux
|
|
2
2
|
class RolesController < CccuxController
|
|
3
|
-
# Ensure only Role Managers can access role management
|
|
4
|
-
before_action :ensure_role_manager
|
|
5
3
|
# Skip authorization for model_columns since it's just a helper endpoint
|
|
6
4
|
skip_authorization_check only: [:model_columns]
|
|
7
5
|
|
|
8
|
-
|
|
6
|
+
# Add load_and_authorize_resource to automatically load roles
|
|
7
|
+
load_and_authorize_resource class: Cccux::Role
|
|
8
|
+
|
|
9
|
+
# Remove manual set_role - let load_and_authorize_resource handle it
|
|
10
|
+
# before_action :set_role, only: [:show, :edit, :update, :destroy]
|
|
9
11
|
|
|
10
12
|
def index
|
|
11
13
|
@roles = Cccux::Role.includes(:ability_permissions, :users)
|
|
@@ -25,7 +27,8 @@ module Cccux
|
|
|
25
27
|
@role = Cccux::Role.new(role_params)
|
|
26
28
|
|
|
27
29
|
respond_to do |format|
|
|
28
|
-
if @role.save
|
|
30
|
+
format.html { redirect_to cccux.role_path(@role), notice: 'Role was successfully created.' } if @role.save
|
|
31
|
+
if defined?(Turbo::StreamsChannel) && @role.save
|
|
29
32
|
format.turbo_stream do
|
|
30
33
|
render turbo_stream: [
|
|
31
34
|
turbo_stream.update("new_role_form", ""),
|
|
@@ -33,13 +36,15 @@ module Cccux
|
|
|
33
36
|
turbo_stream.update("flash", partial: "flash", locals: { notice: "Role was successfully created." })
|
|
34
37
|
]
|
|
35
38
|
end
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
format.turbo_stream do
|
|
39
|
-
render turbo_stream: turbo_stream.update("new_role_form",
|
|
40
|
-
partial: "form", locals: { role: @role })
|
|
41
|
-
end
|
|
39
|
+
end
|
|
40
|
+
unless @role.save
|
|
42
41
|
format.html { render :new, status: :unprocessable_entity }
|
|
42
|
+
if defined?(Turbo::StreamsChannel)
|
|
43
|
+
format.turbo_stream do
|
|
44
|
+
render turbo_stream: turbo_stream.update("new_role_form",
|
|
45
|
+
partial: "form", locals: { role: @role })
|
|
46
|
+
end
|
|
47
|
+
end
|
|
43
48
|
end
|
|
44
49
|
end
|
|
45
50
|
end
|
|
@@ -65,18 +70,22 @@ module Cccux
|
|
|
65
70
|
def destroy
|
|
66
71
|
respond_to do |format|
|
|
67
72
|
if @role.users.any?
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
if defined?(Turbo::StreamsChannel)
|
|
74
|
+
format.turbo_stream do
|
|
75
|
+
render turbo_stream: turbo_stream.update("flash",
|
|
76
|
+
partial: "flash", locals: { alert: "Cannot delete role that has users assigned to it." })
|
|
77
|
+
end
|
|
71
78
|
end
|
|
72
79
|
format.html { redirect_to cccux.roles_path, alert: 'Cannot delete role that has users assigned to it.' }
|
|
73
80
|
else
|
|
74
81
|
@role.destroy
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
turbo_stream
|
|
78
|
-
|
|
79
|
-
|
|
82
|
+
if defined?(Turbo::StreamsChannel)
|
|
83
|
+
format.turbo_stream do
|
|
84
|
+
render turbo_stream: [
|
|
85
|
+
turbo_stream.remove("role_#{@role.id}"),
|
|
86
|
+
turbo_stream.update("flash", partial: "flash", locals: { notice: "Role was successfully deleted." })
|
|
87
|
+
]
|
|
88
|
+
end
|
|
80
89
|
end
|
|
81
90
|
format.html { redirect_to cccux.roles_path, notice: 'Role was successfully deleted.' }
|
|
82
91
|
end
|
|
@@ -126,9 +135,10 @@ module Cccux
|
|
|
126
135
|
|
|
127
136
|
private
|
|
128
137
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
138
|
+
# Remove set_role method - load_and_authorize_resource handles this
|
|
139
|
+
# def set_role
|
|
140
|
+
# @role = Cccux::Role.find(params[:id])
|
|
141
|
+
# end
|
|
132
142
|
|
|
133
143
|
def build_permission_matrix
|
|
134
144
|
subjects = Cccux::AbilityPermission.distinct.pluck(:subject).sort
|
|
@@ -200,15 +210,7 @@ module Cccux
|
|
|
200
210
|
conditions["user_key"] = ownership_user_key[permission.id.to_s]
|
|
201
211
|
end
|
|
202
212
|
|
|
203
|
-
if conditions.any?
|
|
204
|
-
role_ability.ownership_conditions = conditions.to_json
|
|
205
|
-
else
|
|
206
|
-
role_ability.ownership_conditions = nil
|
|
207
|
-
end
|
|
208
|
-
else
|
|
209
|
-
# Clear ownership configuration for non-owned access types
|
|
210
|
-
role_ability.ownership_source = nil
|
|
211
|
-
role_ability.ownership_conditions = nil
|
|
213
|
+
role_ability.ownership_conditions = conditions.to_json if conditions.any?
|
|
212
214
|
end
|
|
213
215
|
|
|
214
216
|
role_ability.save!
|
|
@@ -218,73 +220,60 @@ module Cccux
|
|
|
218
220
|
def role_params
|
|
219
221
|
params.require(:role).permit(:name, :description, :active, :priority)
|
|
220
222
|
end
|
|
221
|
-
|
|
223
|
+
|
|
222
224
|
def discover_application_models
|
|
223
225
|
models = []
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
!skip_model_by_name?(model_name)
|
|
241
|
-
models << model_name
|
|
242
|
-
end
|
|
243
|
-
else
|
|
244
|
-
# Model constant doesn't exist yet, but table does - likely a valid model
|
|
245
|
-
unless skip_model_by_name?(model_name)
|
|
246
|
-
models << model_name
|
|
247
|
-
end
|
|
248
|
-
end
|
|
249
|
-
rescue => e
|
|
250
|
-
# Ignore
|
|
251
|
-
end
|
|
252
|
-
end
|
|
253
|
-
# Always include CCCUX engine models for management (but not User since host app owns it)
|
|
254
|
-
cccux_models = %w[Cccux::Role Cccux::AbilityPermission Cccux::UserRole Cccux::RoleAbility]
|
|
255
|
-
models += cccux_models
|
|
256
|
-
rescue => e
|
|
257
|
-
Rails.logger.warn "Error discovering models: #{e.message}"
|
|
258
|
-
Rails.logger.warn e.backtrace.join("\n")
|
|
226
|
+
|
|
227
|
+
# Eager load all models to ensure they're available
|
|
228
|
+
Rails.application.eager_load!
|
|
229
|
+
|
|
230
|
+
# Get all ActiveRecord models from the application
|
|
231
|
+
ActiveRecord::Base.descendants.each do |model|
|
|
232
|
+
model_name = model.name
|
|
233
|
+
|
|
234
|
+
# Skip if model should be excluded
|
|
235
|
+
next if skip_model_by_name?(model_name)
|
|
236
|
+
|
|
237
|
+
# Skip if table doesn't exist or should be excluded
|
|
238
|
+
table_name = model.table_name
|
|
239
|
+
next if table_name.blank? || skip_table?(table_name)
|
|
240
|
+
|
|
241
|
+
models << model_name
|
|
259
242
|
end
|
|
260
|
-
|
|
243
|
+
|
|
244
|
+
# Sort by name for consistency
|
|
245
|
+
models.sort
|
|
261
246
|
end
|
|
262
|
-
|
|
247
|
+
|
|
263
248
|
def skip_model_by_name?(model_name)
|
|
264
249
|
excluded_patterns = [
|
|
265
|
-
/^
|
|
266
|
-
/^
|
|
267
|
-
/^ActionText::/,
|
|
268
|
-
/^
|
|
269
|
-
/^
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
250
|
+
/^HABTM_/, # Has and belongs to many join tables
|
|
251
|
+
/^ActiveRecord::/, # ActiveRecord internal classes
|
|
252
|
+
/^ActionText::/, # ActionText models
|
|
253
|
+
/^ActiveStorage::/, # ActiveStorage models
|
|
254
|
+
/^ActionMailbox::/, # ActionMailbox models
|
|
255
|
+
/^Cccux::/, # CCCUX engine models (we'll handle these separately)
|
|
256
|
+
/^ApplicationRecord$/, # Base application record
|
|
257
|
+
/^ApplicationController$/, # Controllers
|
|
258
|
+
/^ApplicationHelper$/, # Helpers
|
|
259
|
+
/^ApplicationMailer$/ # Mailers
|
|
273
260
|
]
|
|
261
|
+
|
|
274
262
|
excluded_patterns.any? { |pattern| model_name.match?(pattern) }
|
|
275
263
|
end
|
|
276
|
-
|
|
264
|
+
|
|
277
265
|
def skip_table?(table_name)
|
|
278
266
|
excluded_tables = [
|
|
267
|
+
'schema_migrations',
|
|
268
|
+
'ar_internal_metadata',
|
|
279
269
|
'active_storage_blobs',
|
|
280
270
|
'active_storage_attachments',
|
|
281
|
-
'active_storage_variant_records',
|
|
282
271
|
'action_text_rich_texts',
|
|
283
272
|
'action_mailbox_inbound_emails',
|
|
284
|
-
'
|
|
273
|
+
'action_mailbox_routing_rules'
|
|
285
274
|
]
|
|
286
|
-
|
|
287
|
-
table_name.
|
|
275
|
+
|
|
276
|
+
excluded_tables.include?(table_name) || table_name.start_with?('active_storage_') || table_name.start_with?('action_text_')
|
|
288
277
|
end
|
|
289
278
|
end
|
|
290
279
|
end
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
class Cccux::UsersController < Cccux::CccuxController
|
|
2
|
-
#
|
|
3
|
-
|
|
2
|
+
# Restore load_and_authorize_resource for User
|
|
3
|
+
load_and_authorize_resource class: User
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
# Add simple authentication check - user must be signed in
|
|
6
|
+
before_action :require_authentication
|
|
7
|
+
|
|
8
|
+
# Remove manual set_user - let load_and_authorize_resource handle it
|
|
9
|
+
# before_action :set_user, only: [:show, :edit, :update, :destroy]
|
|
6
10
|
|
|
7
11
|
def index
|
|
8
|
-
@users
|
|
12
|
+
# Do not override @users, let load_and_authorize_resource scope it
|
|
9
13
|
@roles = Cccux::Role.active.order(:name)
|
|
10
14
|
end
|
|
11
15
|
|
|
@@ -102,11 +106,22 @@ class Cccux::UsersController < Cccux::CccuxController
|
|
|
102
106
|
|
|
103
107
|
private
|
|
104
108
|
|
|
105
|
-
def
|
|
106
|
-
|
|
109
|
+
def require_authentication
|
|
110
|
+
unless user_signed_in?
|
|
111
|
+
respond_to do |format|
|
|
112
|
+
format.html { render plain: "Access denied", status: :forbidden }
|
|
113
|
+
format.json { render json: { error: 'Access denied' }, status: :forbidden }
|
|
114
|
+
end
|
|
115
|
+
return
|
|
116
|
+
end
|
|
107
117
|
end
|
|
108
118
|
|
|
119
|
+
# Remove set_user method - load_and_authorize_resource handles this
|
|
120
|
+
# def set_user
|
|
121
|
+
# @user = User.find(params[:id])
|
|
122
|
+
# end
|
|
123
|
+
|
|
109
124
|
def user_params
|
|
110
|
-
params.require(:user).permit(:email, :password, :password_confirmation)
|
|
125
|
+
params.require(:user).permit(:email, :password, :password_confirmation, :first_name, :last_name)
|
|
111
126
|
end
|
|
112
127
|
end
|
|
@@ -13,12 +13,16 @@ module Cccux
|
|
|
13
13
|
|
|
14
14
|
# Handle CanCanCan authorization errors gracefully
|
|
15
15
|
rescue_from CanCan::AccessDenied do |exception|
|
|
16
|
-
|
|
16
|
+
if Rails.env.test?
|
|
17
|
+
render plain: "Access denied", status: :forbidden
|
|
18
|
+
else
|
|
19
|
+
redirect_to cccux.root_path, alert: 'Access denied.'
|
|
20
|
+
end
|
|
17
21
|
end
|
|
18
22
|
|
|
19
23
|
# Handle 404 errors gracefully
|
|
20
24
|
rescue_from ActiveRecord::RecordNotFound do |exception|
|
|
21
|
-
redirect_to root_path, alert: 'The requested resource was not found.'
|
|
25
|
+
redirect_to cccux.root_path, alert: 'The requested resource was not found.'
|
|
22
26
|
end
|
|
23
27
|
end
|
|
24
28
|
|
|
@@ -1,67 +1,79 @@
|
|
|
1
1
|
module Cccux
|
|
2
2
|
module AuthorizationHelper
|
|
3
3
|
# Link helpers for common actions
|
|
4
|
+
def link_if_can_with_wrapper(action, subject, text, path, **opts)
|
|
5
|
+
prepend = opts.delete(:prepend)
|
|
6
|
+
append = opts.delete(:append)
|
|
7
|
+
if can?(action, subject)
|
|
8
|
+
"#{prepend}#{link_to(text, path, **opts)}#{append}".html_safe
|
|
9
|
+
elsif opts.delete(:show_text)
|
|
10
|
+
text
|
|
11
|
+
else
|
|
12
|
+
""
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
4
16
|
def link_if_can_index(subject, text, path, **opts)
|
|
5
|
-
|
|
17
|
+
link_if_can_with_wrapper(:index, subject, text, path, **opts)
|
|
6
18
|
end
|
|
7
19
|
|
|
8
20
|
def link_if_can_show(subject, text, path, **opts)
|
|
9
|
-
|
|
21
|
+
link_if_can_with_wrapper(:show, subject, text, path, **opts)
|
|
10
22
|
end
|
|
11
23
|
|
|
12
24
|
def link_if_can_create(subject, text, path, **opts)
|
|
13
|
-
|
|
25
|
+
link_if_can_with_wrapper(:create, subject, text, path, **opts)
|
|
14
26
|
end
|
|
15
27
|
|
|
16
28
|
def link_if_can_edit(subject, text, path, **opts)
|
|
17
|
-
|
|
29
|
+
link_if_can_with_wrapper(:edit, subject, text, path, **opts)
|
|
18
30
|
end
|
|
19
31
|
|
|
20
32
|
def link_if_can_update(subject, text, path, **opts)
|
|
21
|
-
|
|
33
|
+
link_if_can_with_wrapper(:update, subject, text, path, **opts)
|
|
22
34
|
end
|
|
23
35
|
|
|
24
36
|
def link_if_can_destroy(subject, text, path, **opts)
|
|
25
|
-
|
|
37
|
+
link_if_can_with_wrapper(:destroy, subject, text, path, **opts)
|
|
26
38
|
end
|
|
27
39
|
|
|
28
40
|
# Button helpers for common actions
|
|
29
41
|
def button_if_can_index(subject, text, path, **opts)
|
|
30
|
-
button_to(text, path, **opts)
|
|
42
|
+
can?(:index, subject) ? button_to(text, path, **opts) : ""
|
|
31
43
|
end
|
|
32
44
|
|
|
33
45
|
def button_if_can_show(subject, text, path, **opts)
|
|
34
|
-
button_to(text, path, **opts)
|
|
46
|
+
can?(:show, subject) ? button_to(text, path, **opts) : ""
|
|
35
47
|
end
|
|
36
48
|
|
|
37
49
|
def button_if_can_create(subject, text, path, **opts)
|
|
38
|
-
button_to(text, path, **opts)
|
|
50
|
+
can?(:create, subject) ? button_to(text, path, **opts) : ""
|
|
39
51
|
end
|
|
40
52
|
|
|
41
53
|
def button_if_can_edit(subject, text, path, **opts)
|
|
42
|
-
button_to(text, path, **opts)
|
|
54
|
+
can?(:edit, subject) ? button_to(text, path, **opts) : ""
|
|
43
55
|
end
|
|
44
56
|
|
|
45
57
|
def button_if_can_update(subject, text, path, **opts)
|
|
46
|
-
button_to(text, path, **opts)
|
|
58
|
+
can?(:update, subject) ? button_to(text, path, **opts) : ""
|
|
47
59
|
end
|
|
48
60
|
|
|
49
61
|
def button_if_can_destroy(subject, text, path, **opts)
|
|
50
|
-
button_to(text, path, **opts)
|
|
62
|
+
can?(:destroy, subject) ? button_to(text, path, **opts) : ""
|
|
51
63
|
end
|
|
52
64
|
|
|
53
65
|
# Generic action helpers
|
|
54
66
|
def link_if_can(action, subject, text, path, **opts)
|
|
55
|
-
link_to(text, path, **opts)
|
|
67
|
+
can?(action, subject) ? link_to(text, path, **opts) : ""
|
|
56
68
|
end
|
|
57
69
|
|
|
58
70
|
def button_if_can(action, subject, text, path, **opts)
|
|
59
|
-
button_to(text, path, **opts)
|
|
71
|
+
can?(action, subject) ? button_to(text, path, **opts) : ""
|
|
60
72
|
end
|
|
61
73
|
|
|
62
74
|
# Content helpers for conditional rendering
|
|
63
75
|
def content_if_can(action, subject, &block)
|
|
64
|
-
|
|
76
|
+
can?(action, subject) ? capture(&block) : ""
|
|
65
77
|
end
|
|
66
78
|
|
|
67
79
|
def content_if_can_index(subject, &block)
|
|
@@ -90,15 +102,11 @@ module Cccux
|
|
|
90
102
|
|
|
91
103
|
# Icon helpers (useful for action buttons)
|
|
92
104
|
def icon_link_if_can(action, subject, icon_class, text, path, **opts)
|
|
93
|
-
link_to(path, **opts)
|
|
94
|
-
content_tag(:i, '', class: icon_class) + ' ' + text
|
|
95
|
-
end if can?(action, subject)
|
|
105
|
+
can?(action, subject) ? link_to(path, **opts) { content_tag(:i, '', class: icon_class) + ' ' + text } : ""
|
|
96
106
|
end
|
|
97
107
|
|
|
98
108
|
def icon_button_if_can(action, subject, icon_class, text, path, **opts)
|
|
99
|
-
button_to(path, **opts)
|
|
100
|
-
content_tag(:i, '', class: icon_class) + ' ' + text
|
|
101
|
-
end if can?(action, subject)
|
|
109
|
+
can?(action, subject) ? button_to(path, **opts) { content_tag(:i, '', class: icon_class) + ' ' + text } : ""
|
|
102
110
|
end
|
|
103
111
|
|
|
104
112
|
# Common action button helpers with icons
|
data/app/models/cccux/ability.rb
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module Cccux
|
|
4
4
|
class Ability
|
|
5
|
-
|
|
5
|
+
include CanCan::Ability
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
def initialize(user, context = nil)
|
|
8
8
|
user ||= User.new # guest user (not logged in)
|
|
9
9
|
@context = context || {}
|
|
10
10
|
|
|
@@ -60,24 +60,33 @@ module Cccux
|
|
|
60
60
|
def apply_access_ability(role_ability, permission, model_class, user)
|
|
61
61
|
action = permission.action.to_sym
|
|
62
62
|
|
|
63
|
+
# Handle action aliases (read includes show and index)
|
|
64
|
+
actions_to_grant = case action
|
|
65
|
+
when :read
|
|
66
|
+
[:read, :show, :index]
|
|
67
|
+
when :update
|
|
68
|
+
[:update, :edit]
|
|
69
|
+
when :create
|
|
70
|
+
[:create, :new]
|
|
71
|
+
when :destroy
|
|
72
|
+
[:destroy, :delete]
|
|
73
|
+
else
|
|
74
|
+
[action]
|
|
75
|
+
end
|
|
76
|
+
|
|
63
77
|
# For User resource, keep owned logic for now
|
|
64
78
|
if permission.subject == 'User'
|
|
65
|
-
if role_ability.context == 'owned' ||
|
|
66
|
-
apply_owned_ability(
|
|
79
|
+
if role_ability.context == 'owned' || role_ability.owned
|
|
80
|
+
actions_to_grant.each { |act| apply_owned_ability(act, model_class, user, role_ability) }
|
|
67
81
|
else
|
|
68
|
-
can
|
|
82
|
+
actions_to_grant.each { |act| can act, model_class }
|
|
69
83
|
end
|
|
70
84
|
else
|
|
71
|
-
# For all other resources, use global/owned
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
can action, model_class
|
|
75
|
-
when 'owned'
|
|
76
|
-
apply_owned_ability(action, model_class, user, role_ability)
|
|
85
|
+
# For all other resources, use global/owned logic based on context field
|
|
86
|
+
if role_ability.context == 'owned' || role_ability.owned
|
|
87
|
+
actions_to_grant.each { |act| apply_owned_ability(act, model_class, user, role_ability) }
|
|
77
88
|
else
|
|
78
|
-
|
|
79
|
-
Rails.logger.warn "CCCUX: Unknown access_type '#{role_ability.access_type}' for #{model_class.name}, denying access"
|
|
80
|
-
# Don't grant any permissions - CanCanCan denies by default
|
|
89
|
+
actions_to_grant.each { |act| can act, model_class }
|
|
81
90
|
end
|
|
82
91
|
end
|
|
83
92
|
end
|
|
@@ -89,13 +98,31 @@ module Cccux
|
|
|
89
98
|
if ownership_model && user&.persisted?
|
|
90
99
|
# Parse conditions (should be a JSON string or nil)
|
|
91
100
|
conditions = role_ability.ownership_conditions.present? ? JSON.parse(role_ability.ownership_conditions) : {}
|
|
92
|
-
foreign_key = conditions["foreign_key"]
|
|
101
|
+
foreign_key = conditions["foreign_key"]
|
|
93
102
|
user_key = conditions["user_key"] || "user_id"
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
103
|
+
|
|
104
|
+
# Require foreign_key to be explicitly specified when using ownership model
|
|
105
|
+
if foreign_key.present?
|
|
106
|
+
# Find all records owned by user via the join model
|
|
107
|
+
owned_ids = ownership_model.where(user_key => user.id).pluck(foreign_key)
|
|
108
|
+
if owned_ids.any?
|
|
109
|
+
# Check if the target model has the foreign key column
|
|
110
|
+
if model_class.column_names.include?(foreign_key)
|
|
111
|
+
# Direct ownership: model has the foreign key (e.g., Comment has post_id)
|
|
112
|
+
can action, model_class, foreign_key.to_sym => owned_ids
|
|
113
|
+
else
|
|
114
|
+
# Indirect ownership: model doesn't have the foreign key, use primary key
|
|
115
|
+
# This means the foreign key in the join table refers to the target model's primary key
|
|
116
|
+
can action, model_class, id: owned_ids
|
|
117
|
+
end
|
|
118
|
+
else
|
|
119
|
+
can action, model_class, id: []
|
|
120
|
+
end
|
|
121
|
+
else
|
|
122
|
+
# Fall back to no access if foreign_key is not specified
|
|
123
|
+
can action, model_class, id: []
|
|
124
|
+
end
|
|
97
125
|
else
|
|
98
|
-
Rails.logger.warn "CCCUX: Invalid ownership_source #{role_ability.ownership_source} for #{model_class.name}"
|
|
99
126
|
can action, model_class, id: []
|
|
100
127
|
end
|
|
101
128
|
# 2. Model custom owned_by?
|
|
@@ -112,30 +139,54 @@ module Cccux
|
|
|
112
139
|
else
|
|
113
140
|
can action, model_class, scoped_records
|
|
114
141
|
end
|
|
115
|
-
# 4.
|
|
142
|
+
# 4. Special case for User model (self-ownership)
|
|
143
|
+
elsif model_class == User
|
|
144
|
+
can action, model_class, id: user.id
|
|
145
|
+
# 5. Standard user_id
|
|
116
146
|
elsif model_class.column_names.include?('user_id')
|
|
117
147
|
can action, model_class, user_id: user.id
|
|
118
|
-
#
|
|
148
|
+
# 6. Standard creator_id
|
|
119
149
|
elsif model_class.column_names.include?('creator_id')
|
|
120
150
|
can action, model_class, creator_id: user.id
|
|
151
|
+
# 7. Dynamic ownership check for individual records
|
|
121
152
|
else
|
|
122
|
-
#
|
|
123
|
-
|
|
124
|
-
|
|
153
|
+
# For cases where we need to check individual record attributes
|
|
154
|
+
can action, model_class do |record|
|
|
155
|
+
if record.respond_to?(:creator_id)
|
|
156
|
+
record.creator_id == user.id
|
|
157
|
+
elsif record.respond_to?(:user_id)
|
|
158
|
+
record.user_id == user.id
|
|
159
|
+
else
|
|
160
|
+
false
|
|
161
|
+
end
|
|
162
|
+
end
|
|
125
163
|
end
|
|
126
164
|
end
|
|
127
165
|
|
|
128
166
|
def resolve_model_class(subject)
|
|
129
|
-
#
|
|
130
|
-
|
|
131
|
-
|
|
167
|
+
# Try to resolve the model class in a robust way
|
|
168
|
+
candidates = []
|
|
169
|
+
if subject.include?("::")
|
|
170
|
+
candidates << subject
|
|
171
|
+
candidates << subject.split("::").last
|
|
132
172
|
else
|
|
133
|
-
|
|
134
|
-
|
|
173
|
+
candidates << subject
|
|
174
|
+
candidates << "Cccux::#{subject}"
|
|
175
|
+
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Add more candidates for common patterns
|
|
179
|
+
candidates << subject.split("::").last if subject.include?("::")
|
|
180
|
+
candidates << subject.gsub("Cccux::", "") if subject.start_with?("Cccux::")
|
|
181
|
+
|
|
182
|
+
candidates.each do |candidate|
|
|
183
|
+
begin
|
|
184
|
+
klass = candidate.constantize
|
|
185
|
+
return klass
|
|
186
|
+
rescue NameError => e
|
|
187
|
+
next
|
|
188
|
+
end
|
|
135
189
|
end
|
|
136
|
-
rescue NameError
|
|
137
|
-
# If the model doesn't exist, we can't define permissions for it
|
|
138
|
-
Rails.logger.warn "CCCUX: Model '#{subject}' not found, skipping permission"
|
|
139
190
|
nil
|
|
140
191
|
end
|
|
141
192
|
end
|
|
@@ -12,6 +12,9 @@ module Cccux
|
|
|
12
12
|
scope :for_subject, ->(subject) { where(subject: subject) }
|
|
13
13
|
scope :for_action, ->(action) { where(action: action) }
|
|
14
14
|
|
|
15
|
+
# Ensure all permissions are created as active by default
|
|
16
|
+
before_create :ensure_active
|
|
17
|
+
|
|
15
18
|
def display_name
|
|
16
19
|
"#{action.humanize} #{subject}"
|
|
17
20
|
end
|
|
@@ -57,5 +60,11 @@ module Cccux
|
|
|
57
60
|
klass.column_names.include?('user_id') ||
|
|
58
61
|
klass.column_names.include?('creator_id')
|
|
59
62
|
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def ensure_active
|
|
67
|
+
self.active = true if active.nil?
|
|
68
|
+
end
|
|
60
69
|
end
|
|
61
70
|
end
|
data/app/models/cccux/role.rb
CHANGED
|
@@ -9,10 +9,11 @@ module Cccux
|
|
|
9
9
|
has_many :role_abilities, dependent: :destroy, class_name: 'Cccux::RoleAbility'
|
|
10
10
|
has_many :ability_permissions, through: :role_abilities, class_name: 'Cccux::AbilityPermission'
|
|
11
11
|
|
|
12
|
-
validates :name, presence: true
|
|
12
|
+
validates :name, presence: true
|
|
13
13
|
validates :priority, presence: true, numericality: { only_integer: true, greater_than: 0 }
|
|
14
14
|
|
|
15
15
|
after_initialize :set_default_priority, if: :new_record?
|
|
16
|
+
validate :name_uniqueness_case_insensitive
|
|
16
17
|
|
|
17
18
|
scope :active, -> { where(active: true) }
|
|
18
19
|
scope :ordered, -> { order(:priority, :name) }
|
|
@@ -81,6 +82,23 @@ module Cccux
|
|
|
81
82
|
end
|
|
82
83
|
end
|
|
83
84
|
|
|
85
|
+
# Generate slug from name
|
|
86
|
+
def slug
|
|
87
|
+
name.parameterize.underscore if name.present?
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Case insensitive name validation
|
|
91
|
+
def name_uniqueness_case_insensitive
|
|
92
|
+
return unless name.present?
|
|
93
|
+
|
|
94
|
+
existing_role = self.class.where('LOWER(name) = ?', name.downcase)
|
|
95
|
+
existing_role = existing_role.where.not(id: id) if persisted?
|
|
96
|
+
|
|
97
|
+
if existing_role.exists?
|
|
98
|
+
errors.add(:name, 'has already been taken')
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
84
102
|
private
|
|
85
103
|
|
|
86
104
|
def set_default_priority
|
|
@@ -5,6 +5,9 @@ module Cccux
|
|
|
5
5
|
belongs_to :role, class_name: 'Cccux::Role'
|
|
6
6
|
belongs_to :ability_permission, class_name: 'Cccux::AbilityPermission'
|
|
7
7
|
|
|
8
|
+
# Delegate methods to ability_permission
|
|
9
|
+
delegate :action, :subject, :active?, to: :ability_permission, allow_nil: true
|
|
10
|
+
|
|
8
11
|
# Simplified access types: global, owned
|
|
9
12
|
ACCESS_TYPES = %w[global owned].freeze
|
|
10
13
|
|
|
@@ -9,13 +9,16 @@ module Cccux
|
|
|
9
9
|
has_many :cccux_user_roles, class_name: 'Cccux::UserRole', dependent: :destroy
|
|
10
10
|
has_many :cccux_roles, through: :cccux_user_roles, source: :role, class_name: 'Cccux::Role'
|
|
11
11
|
|
|
12
|
+
# Alias for easier access
|
|
13
|
+
alias_method :roles, :cccux_roles
|
|
14
|
+
|
|
12
15
|
# Automatically assign Basic User role to new users
|
|
13
16
|
after_create :assign_default_role
|
|
14
17
|
end
|
|
15
18
|
|
|
16
19
|
# Instance methods for user authorization
|
|
17
20
|
def has_role?(role_name)
|
|
18
|
-
|
|
21
|
+
cccux_user_roles.active.joins(:role).where(cccux_roles: { name: role_name }).exists?
|
|
19
22
|
end
|
|
20
23
|
|
|
21
24
|
def has_any_role?(*role_names)
|
|
@@ -59,7 +62,7 @@ module Cccux
|
|
|
59
62
|
end
|
|
60
63
|
|
|
61
64
|
def role_names
|
|
62
|
-
|
|
65
|
+
cccux_user_roles.active.joins(:role).pluck('cccux_roles.name')
|
|
63
66
|
end
|
|
64
67
|
|
|
65
68
|
def highest_priority_role
|