super_auth 0.1.4 → 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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -0
- data/Gemfile +7 -10
- data/Gemfile.lock +53 -5
- data/LICENSE.txt +125 -21
- data/README.md +32 -1
- data/Rakefile +0 -2
- data/USAGE.md +619 -0
- data/VISUALIZATION.md +58 -0
- data/app/controllers/super_auth/graph_controller.rb +661 -0
- data/app/views/super_auth/graph/index.html.erb +1408 -0
- data/config/routes.rb +73 -0
- data/db/migrate/1_users.rb +7 -4
- data/db/migrate/2_groups.rb +14 -3
- data/db/migrate/3_permissions.rb +6 -2
- data/db/migrate/4_roles.rb +14 -3
- data/db/migrate/5_resources.rb +7 -4
- data/db/migrate/6_edge.rb +6 -4
- data/db/migrate/7_authorization.rb +41 -0
- data/db/migrate/8_add_indexes_to_edges.rb +17 -0
- data/db/migrate_activerecord/20250101000001_create_super_auth_users.rb +10 -0
- data/db/migrate_activerecord/20250101000002_create_super_auth_groups.rb +11 -0
- data/db/migrate_activerecord/20250101000003_create_super_auth_permissions.rb +8 -0
- data/db/migrate_activerecord/20250101000004_create_super_auth_roles.rb +11 -0
- data/db/migrate_activerecord/20250101000005_create_super_auth_resources.rb +10 -0
- data/db/migrate_activerecord/20250101000006_create_super_auth_edges.rb +12 -0
- data/db/migrate_activerecord/20250101000007_create_super_auth_authorizations.rb +41 -0
- data/db/seeds/sample_data.rb +193 -0
- data/lib/basic_loader.rb +10 -2
- data/lib/generators/super_auth/install/install_generator.rb +19 -0
- data/lib/generators/super_auth/install/templates/README +29 -0
- data/lib/generators/super_auth/install/templates/super_auth.rb +7 -0
- data/lib/super_auth/active_record/authorization.rb +3 -0
- data/lib/super_auth/active_record/by_current_user.rb +39 -0
- data/lib/super_auth/active_record/edge.rb +48 -0
- data/lib/super_auth/active_record/group.rb +10 -0
- data/lib/super_auth/active_record/permission.rb +7 -0
- data/lib/super_auth/active_record/resource.rb +4 -0
- data/lib/super_auth/active_record/role.rb +10 -0
- data/lib/super_auth/active_record/user.rb +14 -0
- data/lib/super_auth/active_record.rb +20 -0
- data/lib/super_auth/authorization.rb +2 -0
- data/lib/super_auth/edge.rb +205 -92
- data/lib/super_auth/group.rb +1 -0
- data/lib/super_auth/nestable.rb +17 -10
- data/lib/super_auth/permission.rb +1 -1
- data/lib/super_auth/railtie.rb +30 -0
- data/lib/super_auth/role.rb +2 -1
- data/lib/super_auth/user.rb +14 -14
- data/lib/super_auth/version.rb +1 -3
- data/lib/super_auth.rb +103 -29
- data/lib/tasks/super_auth_tasks.rake +9 -8
- data/visualization.html +747 -0
- metadata +33 -6
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
module SuperAuth
|
|
2
|
+
class GraphController < ActionController::Base
|
|
3
|
+
protect_from_forgery with: :null_session
|
|
4
|
+
|
|
5
|
+
before_action :initialize_super_auth
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
# Render the interactive graph visualization view
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def data
|
|
12
|
+
# Check if filters are applied
|
|
13
|
+
has_filters = params[:user_id].present? || params[:resource_id].present?
|
|
14
|
+
|
|
15
|
+
if has_filters
|
|
16
|
+
# Apply filters
|
|
17
|
+
edges = edge_class.all
|
|
18
|
+
edges = edges.where(user_id: params[:user_id]) if params[:user_id].present?
|
|
19
|
+
edges = edges.where(resource_id: params[:resource_id]) if params[:resource_id].present?
|
|
20
|
+
|
|
21
|
+
# Get IDs of entities connected by filtered edges
|
|
22
|
+
user_ids = edges.map(&:user_id).compact.uniq
|
|
23
|
+
group_ids = edges.map(&:group_id).compact.uniq
|
|
24
|
+
role_ids = edges.map(&:role_id).compact.uniq
|
|
25
|
+
permission_ids = edges.map(&:permission_id).compact.uniq
|
|
26
|
+
resource_ids = edges.map(&:resource_id).compact.uniq
|
|
27
|
+
|
|
28
|
+
# Include parent groups and roles for hierarchy visualization
|
|
29
|
+
groups = group_class.where(id: group_ids)
|
|
30
|
+
groups.each do |group|
|
|
31
|
+
current = group
|
|
32
|
+
while current.parent_id
|
|
33
|
+
parent = group_class.find_by(id: current.parent_id)
|
|
34
|
+
break unless parent
|
|
35
|
+
group_ids << parent.id unless group_ids.include?(parent.id)
|
|
36
|
+
current = parent
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
roles = role_class.where(id: role_ids)
|
|
41
|
+
roles.each do |role|
|
|
42
|
+
current = role
|
|
43
|
+
while current.parent_id
|
|
44
|
+
parent = role_class.find_by(id: current.parent_id)
|
|
45
|
+
break unless parent
|
|
46
|
+
role_ids << parent.id unless role_ids.include?(role.id)
|
|
47
|
+
current = parent
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
render json: {
|
|
52
|
+
all_users: user_class.all.map { |u| { id: u.id, name: u.name } },
|
|
53
|
+
all_resources: resource_class.all.map { |r| { id: r.id, name: r.name } },
|
|
54
|
+
users: user_class.where(id: user_ids).map { |u| { id: u.id, name: u.name, type: 'user' } },
|
|
55
|
+
groups: group_class.where(id: group_ids).map { |g| { id: g.id, name: g.name, parent_id: g.parent_id, type: 'group' } },
|
|
56
|
+
roles: role_class.where(id: role_ids).map { |r| { id: r.id, name: r.name, parent_id: r.parent_id, type: 'role' } },
|
|
57
|
+
permissions: permission_class.where(id: permission_ids).map { |p| { id: p.id, name: p.name, type: 'permission' } },
|
|
58
|
+
resources: resource_class.where(id: resource_ids).map { |r| { id: r.id, name: r.name, type: 'resource' } },
|
|
59
|
+
edges: edges.map { |e| {
|
|
60
|
+
id: e.id,
|
|
61
|
+
user_id: e.user_id,
|
|
62
|
+
group_id: e.group_id,
|
|
63
|
+
role_id: e.role_id,
|
|
64
|
+
permission_id: e.permission_id,
|
|
65
|
+
resource_id: e.resource_id
|
|
66
|
+
}}
|
|
67
|
+
}
|
|
68
|
+
else
|
|
69
|
+
# No filters - return all entities
|
|
70
|
+
render json: {
|
|
71
|
+
all_users: user_class.all.map { |u| { id: u.id, name: u.name } },
|
|
72
|
+
all_resources: resource_class.all.map { |r| { id: r.id, name: r.name } },
|
|
73
|
+
users: user_class.all.map { |u| { id: u.id, name: u.name, type: 'user' } },
|
|
74
|
+
groups: group_class.all.map { |g| { id: g.id, name: g.name, parent_id: g.parent_id, type: 'group' } },
|
|
75
|
+
roles: role_class.all.map { |r| { id: r.id, name: r.name, parent_id: r.parent_id, type: 'role' } },
|
|
76
|
+
permissions: permission_class.all.map { |p| { id: p.id, name: p.name, type: 'permission' } },
|
|
77
|
+
resources: resource_class.all.map { |r| { id: r.id, name: r.name, type: 'resource' } },
|
|
78
|
+
edges: edge_class.all.map { |e| {
|
|
79
|
+
id: e.id,
|
|
80
|
+
user_id: e.user_id,
|
|
81
|
+
group_id: e.group_id,
|
|
82
|
+
role_id: e.role_id,
|
|
83
|
+
permission_id: e.permission_id,
|
|
84
|
+
resource_id: e.resource_id
|
|
85
|
+
}}
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def create_user
|
|
91
|
+
user = user_class.create!(user_params)
|
|
92
|
+
render json: { success: true, user: { id: user.id, name: user.name, type: 'user' } }
|
|
93
|
+
rescue => e
|
|
94
|
+
render json: { success: false, error: e.message }, status: 422
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def delete_user
|
|
98
|
+
user_class.find(params[:id]).destroy!
|
|
99
|
+
render json: { success: true }
|
|
100
|
+
rescue => e
|
|
101
|
+
render json: { success: false, error: e.message }, status: 422
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def create_group
|
|
105
|
+
group = group_class.create!(group_params)
|
|
106
|
+
render json: { success: true, group: { id: group.id, name: group.name, parent_id: group.parent_id, type: 'group' } }
|
|
107
|
+
rescue => e
|
|
108
|
+
render json: { success: false, error: e.message }, status: 422
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def delete_group
|
|
112
|
+
group_class.find(params[:id]).destroy!
|
|
113
|
+
render json: { success: true }
|
|
114
|
+
rescue => e
|
|
115
|
+
render json: { success: false, error: e.message }, status: 422
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def create_role
|
|
119
|
+
role = role_class.create!(role_params)
|
|
120
|
+
render json: { success: true, role: { id: role.id, name: role.name, parent_id: role.parent_id, type: 'role' } }
|
|
121
|
+
rescue => e
|
|
122
|
+
render json: { success: false, error: e.message }, status: 422
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def delete_role
|
|
126
|
+
role_class.find(params[:id]).destroy!
|
|
127
|
+
render json: { success: true }
|
|
128
|
+
rescue => e
|
|
129
|
+
render json: { success: false, error: e.message }, status: 422
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def create_permission
|
|
133
|
+
permission = permission_class.create!(permission_params)
|
|
134
|
+
render json: { success: true, permission: { id: permission.id, name: permission.name, type: 'permission' } }
|
|
135
|
+
rescue => e
|
|
136
|
+
render json: { success: false, error: e.message }, status: 422
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def delete_permission
|
|
140
|
+
permission_class.find(params[:id]).destroy!
|
|
141
|
+
render json: { success: true }
|
|
142
|
+
rescue => e
|
|
143
|
+
render json: { success: false, error: e.message }, status: 422
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def create_resource
|
|
147
|
+
resource = resource_class.create!(resource_params)
|
|
148
|
+
render json: { success: true, resource: { id: resource.id, name: resource.name, type: 'resource' } }
|
|
149
|
+
rescue => e
|
|
150
|
+
render json: { success: false, error: e.message }, status: 422
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def delete_resource
|
|
154
|
+
resource_class.find(params[:id]).destroy!
|
|
155
|
+
render json: { success: true }
|
|
156
|
+
rescue => e
|
|
157
|
+
render json: { success: false, error: e.message }, status: 422
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def create_edge
|
|
161
|
+
edge = edge_class.create!(edge_params)
|
|
162
|
+
render json: {
|
|
163
|
+
success: true,
|
|
164
|
+
edge: {
|
|
165
|
+
id: edge.id,
|
|
166
|
+
user_id: edge.user_id,
|
|
167
|
+
group_id: edge.group_id,
|
|
168
|
+
role_id: edge.role_id,
|
|
169
|
+
permission_id: edge.permission_id,
|
|
170
|
+
resource_id: edge.resource_id
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
rescue => e
|
|
174
|
+
render json: { success: false, error: e.message }, status: 422
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def delete_edge
|
|
178
|
+
edge_class.find(params[:id]).destroy!
|
|
179
|
+
render json: { success: true }
|
|
180
|
+
rescue => e
|
|
181
|
+
render json: { success: false, error: e.message }, status: 422
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def compile_authorizations
|
|
185
|
+
# Compile all authorization paths and populate the authorizations table
|
|
186
|
+
begin
|
|
187
|
+
authorization_class.delete_all
|
|
188
|
+
|
|
189
|
+
# Call the authorizations method which compiles all paths
|
|
190
|
+
authorizations = edge_class.authorizations
|
|
191
|
+
authorizations.each(&:save!)
|
|
192
|
+
|
|
193
|
+
count = authorizations.count
|
|
194
|
+
|
|
195
|
+
render json: {
|
|
196
|
+
success: true,
|
|
197
|
+
message: "Compiled #{count} authorization paths",
|
|
198
|
+
count: count
|
|
199
|
+
}
|
|
200
|
+
rescue => e
|
|
201
|
+
render json: { success: false, error: e.message }, status: 422
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def orphaned
|
|
206
|
+
# Find orphaned records that aren't part of any complete authorization path
|
|
207
|
+
orphans = {
|
|
208
|
+
users: [],
|
|
209
|
+
groups: [],
|
|
210
|
+
roles: [],
|
|
211
|
+
permissions: [],
|
|
212
|
+
resources: []
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
# Get all edges
|
|
216
|
+
edges = edge_class.all
|
|
217
|
+
|
|
218
|
+
# Check each user - can they reach any resource?
|
|
219
|
+
user_class.find_each do |user|
|
|
220
|
+
can_reach_resource = can_reach_any_resource?(user.id, edges)
|
|
221
|
+
orphans[:users] << { id: user.id, name: user.name, reason: 'Cannot reach any resource' } unless can_reach_resource
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Check each resource - can any user reach it?
|
|
225
|
+
resource_class.find_each do |resource|
|
|
226
|
+
can_be_reached = can_resource_be_reached?(resource.id, edges)
|
|
227
|
+
orphans[:resources] << { id: resource.id, name: resource.name, reason: 'Cannot be reached by any user' } unless can_be_reached
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Check groups - are they part of any user-to-resource path?
|
|
231
|
+
group_class.find_each do |group|
|
|
232
|
+
has_user_connection = edges.any? { |e| e.user_id && e.group_id == group.id }
|
|
233
|
+
has_downstream = edges.any? { |e| e.group_id == group.id && (e.role_id || e.permission_id || e.resource_id) }
|
|
234
|
+
|
|
235
|
+
unless has_user_connection && has_downstream
|
|
236
|
+
reason = if !has_user_connection
|
|
237
|
+
'Not connected to any user'
|
|
238
|
+
elsif !has_downstream
|
|
239
|
+
'Not connected downstream'
|
|
240
|
+
end
|
|
241
|
+
orphans[:groups] << { id: group.id, name: group.name, reason: reason }
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Check roles
|
|
246
|
+
role_class.find_each do |role|
|
|
247
|
+
has_upstream = edges.any? { |e| (e.user_id || e.group_id) && e.role_id == role.id }
|
|
248
|
+
has_downstream = edges.any? { |e| e.role_id == role.id && (e.permission_id || e.resource_id) }
|
|
249
|
+
|
|
250
|
+
unless has_upstream && has_downstream
|
|
251
|
+
reason = if !has_upstream
|
|
252
|
+
'Not connected to any user or group'
|
|
253
|
+
elsif !has_downstream
|
|
254
|
+
'Not connected downstream'
|
|
255
|
+
end
|
|
256
|
+
orphans[:roles] << { id: role.id, name: role.name, reason: reason }
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Check permissions
|
|
261
|
+
permission_class.find_each do |permission|
|
|
262
|
+
has_upstream = edges.any? { |e| (e.user_id || e.group_id || e.role_id) && e.permission_id == permission.id }
|
|
263
|
+
has_downstream = edges.any? { |e| e.permission_id == permission.id && e.resource_id }
|
|
264
|
+
|
|
265
|
+
unless has_upstream && has_downstream
|
|
266
|
+
reason = if !has_upstream
|
|
267
|
+
'Not connected to any user/group/role'
|
|
268
|
+
elsif !has_downstream
|
|
269
|
+
'Not connected to any resource'
|
|
270
|
+
end
|
|
271
|
+
orphans[:permissions] << { id: permission.id, name: permission.name, reason: reason }
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
render json: { orphans: orphans }
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def authorize
|
|
279
|
+
user_id = params[:user_id]
|
|
280
|
+
resource_id = params[:resource_id]
|
|
281
|
+
|
|
282
|
+
if user_id.blank? || resource_id.blank?
|
|
283
|
+
render json: { error: 'user_id and resource_id are required' }, status: :bad_request
|
|
284
|
+
return
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
paths = find_authorization_paths(user_id.to_i, resource_id.to_i)
|
|
288
|
+
|
|
289
|
+
render json: {
|
|
290
|
+
authorized: paths.any?,
|
|
291
|
+
paths: paths,
|
|
292
|
+
count: paths.length
|
|
293
|
+
}
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def visualization
|
|
297
|
+
html_path = File.join(SuperAuth::Engine.root, 'visualization.html')
|
|
298
|
+
send_file html_path, type: 'text/html', disposition: 'inline'
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
private
|
|
302
|
+
|
|
303
|
+
def initialize_super_auth
|
|
304
|
+
# Ensure SuperAuth is loaded
|
|
305
|
+
SuperAuth.load unless defined?(SuperAuth::User) || defined?(SuperAuth::ActiveRecord::User)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def nodes_data
|
|
309
|
+
nodes = []
|
|
310
|
+
|
|
311
|
+
# Users
|
|
312
|
+
user_class.all.each do |user|
|
|
313
|
+
nodes << {
|
|
314
|
+
id: "u#{user.id}",
|
|
315
|
+
label: user.name,
|
|
316
|
+
type: 'user',
|
|
317
|
+
database_id: user.id
|
|
318
|
+
}
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Groups with hierarchy
|
|
322
|
+
group_class.all.each do |group|
|
|
323
|
+
nodes << {
|
|
324
|
+
id: "g#{group.id}",
|
|
325
|
+
label: group.name,
|
|
326
|
+
type: 'group',
|
|
327
|
+
parent: group.parent_id ? "g#{group.parent_id}" : nil,
|
|
328
|
+
database_id: group.id
|
|
329
|
+
}
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Roles with hierarchy
|
|
333
|
+
role_class.all.each do |role|
|
|
334
|
+
nodes << {
|
|
335
|
+
id: "r#{role.id}",
|
|
336
|
+
label: role.name,
|
|
337
|
+
type: 'role',
|
|
338
|
+
parent: role.parent_id ? "r#{role.parent_id}" : nil,
|
|
339
|
+
database_id: role.id
|
|
340
|
+
}
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# Permissions
|
|
344
|
+
permission_class.all.each do |permission|
|
|
345
|
+
nodes << {
|
|
346
|
+
id: "p#{permission.id}",
|
|
347
|
+
label: permission.name,
|
|
348
|
+
type: 'permission',
|
|
349
|
+
database_id: permission.id
|
|
350
|
+
}
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Resources
|
|
354
|
+
resource_class.all.each do |resource|
|
|
355
|
+
nodes << {
|
|
356
|
+
id: "res#{resource.id}",
|
|
357
|
+
label: resource.name,
|
|
358
|
+
type: 'resource',
|
|
359
|
+
database_id: resource.id
|
|
360
|
+
}
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
nodes
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def edges_data
|
|
367
|
+
edges = []
|
|
368
|
+
|
|
369
|
+
edge_class.all.each do |edge|
|
|
370
|
+
# Create edge connections
|
|
371
|
+
if edge.user_id && edge.group_id
|
|
372
|
+
edges << { from: "u#{edge.user_id}", to: "g#{edge.group_id}", type: 'authorization' }
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
if edge.user_id && edge.role_id
|
|
376
|
+
edges << { from: "u#{edge.user_id}", to: "r#{edge.role_id}", type: 'authorization' }
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
if edge.user_id && edge.permission_id
|
|
380
|
+
edges << { from: "u#{edge.user_id}", to: "p#{edge.permission_id}", type: 'authorization' }
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
if edge.user_id && edge.resource_id
|
|
384
|
+
edges << { from: "u#{edge.user_id}", to: "res#{edge.resource_id}", type: 'authorization' }
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
if edge.group_id && edge.role_id
|
|
388
|
+
edges << { from: "g#{edge.group_id}", to: "r#{edge.role_id}", type: 'authorization' }
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
if edge.group_id && edge.permission_id
|
|
392
|
+
edges << { from: "g#{edge.group_id}", to: "p#{edge.permission_id}", type: 'authorization' }
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
if edge.role_id && edge.permission_id
|
|
396
|
+
edges << { from: "r#{edge.role_id}", to: "p#{edge.permission_id}", type: 'authorization' }
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
if edge.permission_id && edge.resource_id
|
|
400
|
+
edges << { from: "p#{edge.permission_id}", to: "res#{edge.resource_id}", type: 'authorization' }
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# Add hierarchy edges for groups
|
|
405
|
+
group_class.where.not(parent_id: nil).each do |group|
|
|
406
|
+
edges << { from: "g#{group.id}", to: "g#{group.parent_id}", type: 'hierarchy' }
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# Add hierarchy edges for roles
|
|
410
|
+
role_class.where.not(parent_id: nil).each do |role|
|
|
411
|
+
edges << { from: "r#{role.id}", to: "r#{role.parent_id}", type: 'hierarchy' }
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
edges.uniq
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def stats_data
|
|
418
|
+
{
|
|
419
|
+
users: user_class.count,
|
|
420
|
+
groups: group_class.count,
|
|
421
|
+
roles: role_class.count,
|
|
422
|
+
permissions: permission_class.count,
|
|
423
|
+
resources: resource_class.count,
|
|
424
|
+
edges: edge_class.count
|
|
425
|
+
}
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def find_authorization_paths(user_id, resource_id)
|
|
429
|
+
user_node = "u#{user_id}"
|
|
430
|
+
resource_node = "res#{resource_id}"
|
|
431
|
+
|
|
432
|
+
# Build adjacency list
|
|
433
|
+
adjacency = build_adjacency_list
|
|
434
|
+
|
|
435
|
+
# BFS to find all paths
|
|
436
|
+
paths = []
|
|
437
|
+
queue = [[user_node]]
|
|
438
|
+
visited_paths = Set.new
|
|
439
|
+
|
|
440
|
+
while queue.any? && paths.length < 100 # Limit to prevent infinite loops
|
|
441
|
+
path = queue.shift
|
|
442
|
+
current = path.last
|
|
443
|
+
|
|
444
|
+
# Skip if we've seen this exact path before
|
|
445
|
+
path_key = path.join(',')
|
|
446
|
+
next if visited_paths.include?(path_key)
|
|
447
|
+
visited_paths.add(path_key)
|
|
448
|
+
|
|
449
|
+
# Found a complete path
|
|
450
|
+
if current == resource_node
|
|
451
|
+
paths << path_with_labels(path)
|
|
452
|
+
next
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# Prevent too-long paths
|
|
456
|
+
next if path.length > 10
|
|
457
|
+
|
|
458
|
+
# Explore neighbors
|
|
459
|
+
neighbors = adjacency[current] || []
|
|
460
|
+
neighbors.each do |neighbor|
|
|
461
|
+
next if path.include?(neighbor) # Avoid cycles
|
|
462
|
+
queue << path + [neighbor]
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
paths
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def build_adjacency_list
|
|
470
|
+
adjacency = Hash.new { |h, k| h[k] = [] }
|
|
471
|
+
|
|
472
|
+
edges_data.each do |edge|
|
|
473
|
+
# Make it bidirectional for path finding
|
|
474
|
+
adjacency[edge[:from]] << edge[:to]
|
|
475
|
+
adjacency[edge[:to]] << edge[:from]
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
adjacency
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def path_with_labels(path)
|
|
482
|
+
path.map do |node_id|
|
|
483
|
+
node = nodes_data.find { |n| n[:id] == node_id }
|
|
484
|
+
node ? node[:label] : node_id
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# Dynamic class detection for Sequel vs ActiveRecord
|
|
489
|
+
def user_class
|
|
490
|
+
if defined?(SuperAuth::ActiveRecord::User)
|
|
491
|
+
SuperAuth::ActiveRecord::User
|
|
492
|
+
else
|
|
493
|
+
SuperAuth::User
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def group_class
|
|
498
|
+
if defined?(SuperAuth::ActiveRecord::Group)
|
|
499
|
+
SuperAuth::ActiveRecord::Group
|
|
500
|
+
else
|
|
501
|
+
SuperAuth::Group
|
|
502
|
+
end
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
def role_class
|
|
506
|
+
if defined?(SuperAuth::ActiveRecord::Role)
|
|
507
|
+
SuperAuth::ActiveRecord::Role
|
|
508
|
+
else
|
|
509
|
+
SuperAuth::Role
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
def permission_class
|
|
514
|
+
if defined?(SuperAuth::ActiveRecord::Permission)
|
|
515
|
+
SuperAuth::ActiveRecord::Permission
|
|
516
|
+
else
|
|
517
|
+
SuperAuth::Permission
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def resource_class
|
|
522
|
+
if defined?(SuperAuth::ActiveRecord::Resource)
|
|
523
|
+
SuperAuth::ActiveRecord::Resource
|
|
524
|
+
else
|
|
525
|
+
SuperAuth::Resource
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def edge_class
|
|
530
|
+
if defined?(SuperAuth::ActiveRecord::Edge)
|
|
531
|
+
SuperAuth::ActiveRecord::Edge
|
|
532
|
+
else
|
|
533
|
+
SuperAuth::Edge
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
def authorization_class
|
|
538
|
+
if defined?(SuperAuth::ActiveRecord::Authorization)
|
|
539
|
+
SuperAuth::ActiveRecord::Authorization
|
|
540
|
+
else
|
|
541
|
+
SuperAuth::Authorization
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
def can_reach_any_resource?(user_id, edges)
|
|
546
|
+
# BFS to see if user can reach any resource through any path
|
|
547
|
+
visited = Set.new
|
|
548
|
+
queue = [{ type: 'user', id: user_id }]
|
|
549
|
+
|
|
550
|
+
while queue.any?
|
|
551
|
+
current = queue.shift
|
|
552
|
+
key = "#{current[:type]}-#{current[:id]}"
|
|
553
|
+
next if visited.include?(key)
|
|
554
|
+
visited.add(key)
|
|
555
|
+
|
|
556
|
+
return true if current[:type] == 'resource'
|
|
557
|
+
|
|
558
|
+
case current[:type]
|
|
559
|
+
when 'user'
|
|
560
|
+
edges.each do |e|
|
|
561
|
+
next unless e.user_id == current[:id]
|
|
562
|
+
queue << { type: 'group', id: e.group_id } if e.group_id
|
|
563
|
+
queue << { type: 'role', id: e.role_id } if e.role_id
|
|
564
|
+
queue << { type: 'permission', id: e.permission_id } if e.permission_id
|
|
565
|
+
queue << { type: 'resource', id: e.resource_id } if e.resource_id
|
|
566
|
+
end
|
|
567
|
+
when 'group'
|
|
568
|
+
edges.each do |e|
|
|
569
|
+
next unless e.group_id == current[:id]
|
|
570
|
+
queue << { type: 'role', id: e.role_id } if e.role_id
|
|
571
|
+
queue << { type: 'permission', id: e.permission_id } if e.permission_id
|
|
572
|
+
queue << { type: 'resource', id: e.resource_id } if e.resource_id
|
|
573
|
+
end
|
|
574
|
+
when 'role'
|
|
575
|
+
edges.each do |e|
|
|
576
|
+
next unless e.role_id == current[:id]
|
|
577
|
+
queue << { type: 'permission', id: e.permission_id } if e.permission_id
|
|
578
|
+
queue << { type: 'resource', id: e.resource_id } if e.resource_id
|
|
579
|
+
end
|
|
580
|
+
when 'permission'
|
|
581
|
+
edges.each do |e|
|
|
582
|
+
next unless e.permission_id == current[:id]
|
|
583
|
+
queue << { type: 'resource', id: e.resource_id } if e.resource_id
|
|
584
|
+
end
|
|
585
|
+
end
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
false
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def can_resource_be_reached?(resource_id, edges)
|
|
592
|
+
# BFS backwards to see if any user can reach this resource
|
|
593
|
+
visited = Set.new
|
|
594
|
+
queue = [{ type: 'resource', id: resource_id }]
|
|
595
|
+
|
|
596
|
+
while queue.any?
|
|
597
|
+
current = queue.shift
|
|
598
|
+
key = "#{current[:type]}-#{current[:id]}"
|
|
599
|
+
next if visited.include?(key)
|
|
600
|
+
visited.add(key)
|
|
601
|
+
|
|
602
|
+
return true if current[:type] == 'user'
|
|
603
|
+
|
|
604
|
+
case current[:type]
|
|
605
|
+
when 'resource'
|
|
606
|
+
edges.each do |e|
|
|
607
|
+
next unless e.resource_id == current[:id]
|
|
608
|
+
queue << { type: 'user', id: e.user_id } if e.user_id
|
|
609
|
+
queue << { type: 'group', id: e.group_id } if e.group_id
|
|
610
|
+
queue << { type: 'role', id: e.role_id } if e.role_id
|
|
611
|
+
queue << { type: 'permission', id: e.permission_id } if e.permission_id
|
|
612
|
+
end
|
|
613
|
+
when 'permission'
|
|
614
|
+
edges.each do |e|
|
|
615
|
+
next unless e.permission_id == current[:id]
|
|
616
|
+
queue << { type: 'user', id: e.user_id } if e.user_id
|
|
617
|
+
queue << { type: 'group', id: e.group_id } if e.group_id
|
|
618
|
+
queue << { type: 'role', id: e.role_id } if e.role_id
|
|
619
|
+
end
|
|
620
|
+
when 'role'
|
|
621
|
+
edges.each do |e|
|
|
622
|
+
next unless e.role_id == current[:id]
|
|
623
|
+
queue << { type: 'user', id: e.user_id } if e.user_id
|
|
624
|
+
queue << { type: 'group', id: e.group_id } if e.group_id
|
|
625
|
+
end
|
|
626
|
+
when 'group'
|
|
627
|
+
edges.each do |e|
|
|
628
|
+
next unless e.group_id == current[:id]
|
|
629
|
+
queue << { type: 'user', id: e.user_id } if e.user_id
|
|
630
|
+
end
|
|
631
|
+
end
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
false
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
def user_params
|
|
638
|
+
params.require(:user).permit(:name, :external_id, :external_type)
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
def group_params
|
|
642
|
+
params.require(:group).permit(:name, :parent_id)
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
def role_params
|
|
646
|
+
params.require(:role).permit(:name, :parent_id)
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
def permission_params
|
|
650
|
+
params.require(:permission).permit(:name)
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
def resource_params
|
|
654
|
+
params.require(:resource).permit(:name, :external_id, :external_type)
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
def edge_params
|
|
658
|
+
params.require(:edge).permit(:user_id, :group_id, :role_id, :permission_id, :resource_id)
|
|
659
|
+
end
|
|
660
|
+
end
|
|
661
|
+
end
|