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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -0
  3. data/Gemfile +7 -10
  4. data/Gemfile.lock +53 -5
  5. data/LICENSE.txt +125 -21
  6. data/README.md +32 -1
  7. data/Rakefile +0 -2
  8. data/USAGE.md +619 -0
  9. data/VISUALIZATION.md +58 -0
  10. data/app/controllers/super_auth/graph_controller.rb +661 -0
  11. data/app/views/super_auth/graph/index.html.erb +1408 -0
  12. data/config/routes.rb +73 -0
  13. data/db/migrate/1_users.rb +7 -4
  14. data/db/migrate/2_groups.rb +14 -3
  15. data/db/migrate/3_permissions.rb +6 -2
  16. data/db/migrate/4_roles.rb +14 -3
  17. data/db/migrate/5_resources.rb +7 -4
  18. data/db/migrate/6_edge.rb +6 -4
  19. data/db/migrate/7_authorization.rb +41 -0
  20. data/db/migrate/8_add_indexes_to_edges.rb +17 -0
  21. data/db/migrate_activerecord/20250101000001_create_super_auth_users.rb +10 -0
  22. data/db/migrate_activerecord/20250101000002_create_super_auth_groups.rb +11 -0
  23. data/db/migrate_activerecord/20250101000003_create_super_auth_permissions.rb +8 -0
  24. data/db/migrate_activerecord/20250101000004_create_super_auth_roles.rb +11 -0
  25. data/db/migrate_activerecord/20250101000005_create_super_auth_resources.rb +10 -0
  26. data/db/migrate_activerecord/20250101000006_create_super_auth_edges.rb +12 -0
  27. data/db/migrate_activerecord/20250101000007_create_super_auth_authorizations.rb +41 -0
  28. data/db/seeds/sample_data.rb +193 -0
  29. data/lib/basic_loader.rb +10 -2
  30. data/lib/generators/super_auth/install/install_generator.rb +19 -0
  31. data/lib/generators/super_auth/install/templates/README +29 -0
  32. data/lib/generators/super_auth/install/templates/super_auth.rb +7 -0
  33. data/lib/super_auth/active_record/authorization.rb +3 -0
  34. data/lib/super_auth/active_record/by_current_user.rb +39 -0
  35. data/lib/super_auth/active_record/edge.rb +48 -0
  36. data/lib/super_auth/active_record/group.rb +10 -0
  37. data/lib/super_auth/active_record/permission.rb +7 -0
  38. data/lib/super_auth/active_record/resource.rb +4 -0
  39. data/lib/super_auth/active_record/role.rb +10 -0
  40. data/lib/super_auth/active_record/user.rb +14 -0
  41. data/lib/super_auth/active_record.rb +20 -0
  42. data/lib/super_auth/authorization.rb +2 -0
  43. data/lib/super_auth/edge.rb +205 -92
  44. data/lib/super_auth/group.rb +1 -0
  45. data/lib/super_auth/nestable.rb +17 -10
  46. data/lib/super_auth/permission.rb +1 -1
  47. data/lib/super_auth/railtie.rb +30 -0
  48. data/lib/super_auth/role.rb +2 -1
  49. data/lib/super_auth/user.rb +14 -14
  50. data/lib/super_auth/version.rb +1 -3
  51. data/lib/super_auth.rb +103 -29
  52. data/lib/tasks/super_auth_tasks.rake +9 -8
  53. data/visualization.html +747 -0
  54. 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