super_auth 0.1.5 → 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/.ruby-version +1 -1
- data/Gemfile +3 -7
- data/Gemfile.lock +25 -14
- 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 +1 -0
- data/db/migrate/2_groups.rb +8 -1
- data/db/migrate/4_roles.rb +8 -1
- data/db/migrate/5_resources.rb +1 -0
- data/db/migrate/7_authorization.rb +2 -0
- data/db/migrate/8_add_indexes_to_edges.rb +17 -0
- data/db/migrate/9_add_by_current_user_index.rb +12 -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/migrate_activerecord/20250101000009_add_by_current_user_index_to_super_auth_authorizations.rb +7 -0
- data/db/seeds/sample_data.rb +193 -0
- data/lib/basic_loader.rb +0 -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/by_current_user.rb +26 -11
- data/lib/super_auth/active_record/edge.rb +45 -0
- data/lib/super_auth/active_record/group.rb +7 -0
- data/lib/super_auth/active_record/permission.rb +4 -0
- data/lib/super_auth/active_record/resource.rb +1 -0
- data/lib/super_auth/active_record/role.rb +7 -0
- data/lib/super_auth/active_record/user.rb +6 -0
- data/lib/super_auth/active_record.rb +17 -0
- data/lib/super_auth/edge.rb +190 -131
- 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 +26 -25
- data/lib/super_auth/role.rb +2 -1
- data/lib/super_auth/user.rb +8 -8
- data/lib/super_auth/version.rb +1 -3
- data/lib/super_auth.rb +72 -40
- data/super_auth.gemspec +35 -0
- data/visualization.html +747 -0
- metadata +24 -6
data/USAGE.md
ADDED
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
# SuperAuth Usage Guide
|
|
2
|
+
|
|
3
|
+
SuperAuth is a graph-based authorization engine that makes unauthorized access structurally impossible. Instead of scattering authorization checks throughout your codebase, SuperAuth centralizes all rules in a database-backed graph and computes every valid access path automatically.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Installation](#installation)
|
|
8
|
+
- [Core Concepts](#core-concepts)
|
|
9
|
+
- [Quick Start](#quick-start)
|
|
10
|
+
- [Creating Entities](#creating-entities)
|
|
11
|
+
- [Drawing Edges](#drawing-edges)
|
|
12
|
+
- [Authorization Strategies](#authorization-strategies)
|
|
13
|
+
- [Querying Authorizations](#querying-authorizations)
|
|
14
|
+
- [Revoking Access](#revoking-access)
|
|
15
|
+
- [Rails Integration](#rails-integration)
|
|
16
|
+
- [Auditing](#auditing)
|
|
17
|
+
- [Visualization](#visualization)
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
Add to your Gemfile:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
gem "super_auth"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Then run:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
bundle install
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Rails Setup
|
|
34
|
+
|
|
35
|
+
**Step 1.** Add SuperAuth and the Sequel-ActiveRecord bridge to your Gemfile:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
gem "super_auth"
|
|
39
|
+
gem "sequel-activerecord_connection"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Then run:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
bundle install
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The `sequel-activerecord_connection` gem lets SuperAuth's Sequel engine share your existing ActiveRecord database connection, so there is nothing extra to configure.
|
|
49
|
+
|
|
50
|
+
**Step 2.** Run the install generator:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
rails generate super_auth:install
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This creates an initializer at `config/initializers/super_auth.rb`. The SuperAuth Railtie automatically connects to your ActiveRecord database and loads all models on boot -- no manual configuration is needed.
|
|
57
|
+
|
|
58
|
+
**Step 3.** Copy the SuperAuth migrations into your app and run them:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
rails railties:install:migrations
|
|
62
|
+
rails db:migrate
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
This creates the `super_auth_*` tables (users, groups, roles, permissions, resources, edges, authorizations) alongside your application's tables.
|
|
66
|
+
|
|
67
|
+
**Step 4.** Set the current user in your controller:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
class ApplicationController < ActionController::Base
|
|
71
|
+
before_action :set_super_auth_user
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def set_super_auth_user
|
|
76
|
+
SuperAuth.current_user = current_user
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
SuperAuth uses `SuperAuth.current_user` (a thread-local) to know who is making the request. You can pass your own application's user object -- SuperAuth matches it via `external_id` and `external_type`.
|
|
82
|
+
|
|
83
|
+
**Step 5.** Add `super_auth` to any model you want to protect:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
class Post < ApplicationRecord
|
|
87
|
+
super_auth
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
This adds a default scope that automatically filters records. Only records the current user is authorized to access will be returned from queries:
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
Post.all # only posts the current user can access
|
|
95
|
+
Post.where(published: true) # scoped AND filtered by authorization
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Step 6 (optional).** Mount the visualization engine:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
# config/routes.rb
|
|
102
|
+
mount SuperAuth::Engine => '/super_auth'
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Then visit `http://localhost:3000/super_auth/visualization` to see your authorization graph.
|
|
106
|
+
|
|
107
|
+
### Standalone Setup (without Rails)
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
require "super_auth"
|
|
111
|
+
|
|
112
|
+
# Connect to a database
|
|
113
|
+
SuperAuth.db = Sequel.sqlite("super_auth.db")
|
|
114
|
+
# Or use an environment variable:
|
|
115
|
+
# ENV['SUPER_AUTH_DATABASE_URL'] = 'postgresql://user:pass@localhost/mydb'
|
|
116
|
+
|
|
117
|
+
SuperAuth.install_migrations
|
|
118
|
+
SuperAuth.load
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
SuperAuth uses [Sequel](https://sequel.jeremyevans.net/) for all database operations and supports SQLite, PostgreSQL, and MySQL.
|
|
122
|
+
|
|
123
|
+
## Core Concepts
|
|
124
|
+
|
|
125
|
+
SuperAuth models authorization as a graph with 5 entity types:
|
|
126
|
+
|
|
127
|
+
| Entity | Purpose | Hierarchical? |
|
|
128
|
+
|----------------|------------------------------------------------|---------------|
|
|
129
|
+
| **User** | Who is requesting access | No |
|
|
130
|
+
| **Group** | Organizational units (teams, departments, etc) | Yes (nested) |
|
|
131
|
+
| **Role** | Job titles or permission sets | Yes (nested) |
|
|
132
|
+
| **Permission** | Actions (read, write, deploy, etc) | No |
|
|
133
|
+
| **Resource** | Things being protected (files, APIs, records) | No |
|
|
134
|
+
|
|
135
|
+
**Edges** are connections drawn between any two entities. SuperAuth traverses the graph to find all valid paths from a User to a Resource. If a path exists, access is granted.
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
+-------+ +------+
|
|
139
|
+
| Group |<----->| Role |
|
|
140
|
+
+-------+\ / +------+
|
|
141
|
+
^ \ / ^
|
|
142
|
+
| \/ |
|
|
143
|
+
| /\ |
|
|
144
|
+
| / \ |
|
|
145
|
+
V / \ V
|
|
146
|
+
+--------+ +------+/ \+------------+ +----------+
|
|
147
|
+
| App |<-------->| User |<------>| Permission |<-->| Resource |
|
|
148
|
+
| Models | +------+ +------------+ +----------+
|
|
149
|
+
+--------+ ^ ^
|
|
150
|
+
| |
|
|
151
|
+
+----------------------------------+
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Quick Start
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
# Create a user, a permission, and a resource
|
|
158
|
+
alice = SuperAuth::User.create(name: "Alice")
|
|
159
|
+
read = SuperAuth::Permission.create(name: "read")
|
|
160
|
+
docs = SuperAuth::Resource.create(name: "documents")
|
|
161
|
+
|
|
162
|
+
# Draw edges to connect them
|
|
163
|
+
SuperAuth::Edge.create(user: alice, permission: read)
|
|
164
|
+
SuperAuth::Edge.create(permission: read, resource: docs)
|
|
165
|
+
|
|
166
|
+
# Query authorizations
|
|
167
|
+
auths = SuperAuth::Edge.authorizations.all
|
|
168
|
+
alice_auths = auths.select { |a| a[:user_id] == alice.id }
|
|
169
|
+
|
|
170
|
+
alice_auths.first[:permission_name] # => "read"
|
|
171
|
+
alice_auths.first[:resource_name] # => "documents"
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Creating Entities
|
|
175
|
+
|
|
176
|
+
### Users
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
# Basic user
|
|
180
|
+
peter = SuperAuth::User.create(name: "Peter")
|
|
181
|
+
|
|
182
|
+
# User linked to your app's user model
|
|
183
|
+
alice = SuperAuth::User.create(
|
|
184
|
+
name: "Alice",
|
|
185
|
+
external_id: 42,
|
|
186
|
+
external_type: "User"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# System user (bypasses all authorization checks in ActiveRecord integration)
|
|
190
|
+
system = SuperAuth::User.system
|
|
191
|
+
system.system? # => true
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Groups (hierarchical)
|
|
195
|
+
|
|
196
|
+
Groups represent organizational structure. They can be nested to any depth.
|
|
197
|
+
|
|
198
|
+
```ruby
|
|
199
|
+
company = SuperAuth::Group.create(name: "Company")
|
|
200
|
+
engineering = SuperAuth::Group.create(name: "Engineering", parent: company)
|
|
201
|
+
backend = SuperAuth::Group.create(name: "Backend", parent: engineering)
|
|
202
|
+
frontend = SuperAuth::Group.create(name: "Frontend", parent: engineering)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
This creates the hierarchy:
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
Company
|
|
209
|
+
└── Engineering
|
|
210
|
+
├── Backend
|
|
211
|
+
└── Frontend
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Navigate the hierarchy:
|
|
215
|
+
|
|
216
|
+
```ruby
|
|
217
|
+
SuperAuth::Group.roots # Groups with no parent
|
|
218
|
+
SuperAuth::Group.trees # All groups with computed paths
|
|
219
|
+
backend.ancestors_dataset.all # => [Engineering, Company]
|
|
220
|
+
company.descendants_dataset.all # => [Engineering, Backend, Frontend]
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Roles (hierarchical)
|
|
224
|
+
|
|
225
|
+
Roles work exactly like Groups -- they support the same nesting.
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
employee = SuperAuth::Role.create(name: "Employee")
|
|
229
|
+
engineer = SuperAuth::Role.create(name: "Engineer", parent: employee)
|
|
230
|
+
senior_dev = SuperAuth::Role.create(name: "Senior Developer", parent: engineer)
|
|
231
|
+
jr_dev = SuperAuth::Role.create(name: "Junior Developer", parent: engineer)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Permissions
|
|
235
|
+
|
|
236
|
+
Permissions are flat -- they represent actions.
|
|
237
|
+
|
|
238
|
+
```ruby
|
|
239
|
+
read_perm = SuperAuth::Permission.create(name: "read")
|
|
240
|
+
write_perm = SuperAuth::Permission.create(name: "write")
|
|
241
|
+
deploy_perm = SuperAuth::Permission.create(name: "deploy")
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Resources
|
|
245
|
+
|
|
246
|
+
Resources represent what you are protecting. They can link to your app's models.
|
|
247
|
+
|
|
248
|
+
```ruby
|
|
249
|
+
# Named resource
|
|
250
|
+
staging = SuperAuth::Resource.create(name: "staging")
|
|
251
|
+
|
|
252
|
+
# Linked to an ActiveRecord model
|
|
253
|
+
posts = SuperAuth::Resource.create(
|
|
254
|
+
name: "posts",
|
|
255
|
+
external_id: nil,
|
|
256
|
+
external_type: "Post"
|
|
257
|
+
)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Drawing Edges
|
|
261
|
+
|
|
262
|
+
Edges are the core of SuperAuth. Each edge connects exactly two entities.
|
|
263
|
+
|
|
264
|
+
```ruby
|
|
265
|
+
# User belongs to a group
|
|
266
|
+
SuperAuth::Edge.create(user: peter, group: backend)
|
|
267
|
+
|
|
268
|
+
# Group has a role
|
|
269
|
+
SuperAuth::Edge.create(group: backend, role: engineer)
|
|
270
|
+
|
|
271
|
+
# Role has a permission
|
|
272
|
+
SuperAuth::Edge.create(role: engineer, permission: read_perm)
|
|
273
|
+
|
|
274
|
+
# Permission applies to a resource
|
|
275
|
+
SuperAuth::Edge.create(permission: read_perm, resource: staging)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
You can also create shortcuts by skipping intermediate entities:
|
|
279
|
+
|
|
280
|
+
```ruby
|
|
281
|
+
# User directly linked to a role (no group)
|
|
282
|
+
SuperAuth::Edge.create(user: alice, role: senior_dev)
|
|
283
|
+
|
|
284
|
+
# User directly linked to a permission (no group or role)
|
|
285
|
+
SuperAuth::Edge.create(user: alice, permission: deploy_perm)
|
|
286
|
+
|
|
287
|
+
# Group directly linked to a permission (no role)
|
|
288
|
+
SuperAuth::Edge.create(group: backend, permission: write_perm)
|
|
289
|
+
|
|
290
|
+
# User directly linked to a resource (no permission check)
|
|
291
|
+
SuperAuth::Edge.create(user: alice, resource: staging)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Authorization Strategies
|
|
295
|
+
|
|
296
|
+
SuperAuth automatically evaluates 5 pathing strategies and unions the results. You don't need to choose one -- all valid paths are discovered.
|
|
297
|
+
|
|
298
|
+
| # | Path | Use Case |
|
|
299
|
+
|---|------------------------------------------------------------|-------------------------------------|
|
|
300
|
+
| 1 | User -> Group(s) -> Role(s) -> Permission -> Resource | Full organizational hierarchy |
|
|
301
|
+
| 2 | User -> Role(s) -> Permission -> Resource | Direct role assignment |
|
|
302
|
+
| 3 | User -> Group(s) -> Permission -> Resource | Group-level permissions (no roles) |
|
|
303
|
+
| 4 | User -> Permission -> Resource | Direct permission grant |
|
|
304
|
+
| 5 | User -> Resource | Direct resource access (no permissions) |
|
|
305
|
+
|
|
306
|
+
### Hierarchy propagation
|
|
307
|
+
|
|
308
|
+
When groups or roles are nested, SuperAuth considers the full tree. If you assign a user to a parent group, they can access resources through roles attached to that group *and all its descendants*.
|
|
309
|
+
|
|
310
|
+
```ruby
|
|
311
|
+
# Bethany is in Company (the root group)
|
|
312
|
+
SuperAuth::Edge.create(user: bethany, group: company)
|
|
313
|
+
SuperAuth::Edge.create(group: company, role: employee)
|
|
314
|
+
SuperAuth::Edge.create(role: employee, permission: login_perm)
|
|
315
|
+
SuperAuth::Edge.create(permission: login_perm, resource: app)
|
|
316
|
+
|
|
317
|
+
# Bethany can login to app -- the path flows through Company -> Employee -> login -> app
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## Querying Authorizations
|
|
321
|
+
|
|
322
|
+
### Get all authorizations
|
|
323
|
+
|
|
324
|
+
```ruby
|
|
325
|
+
authorizations = SuperAuth::Edge.authorizations.all
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Each row contains the full path:
|
|
329
|
+
|
|
330
|
+
```ruby
|
|
331
|
+
auth = authorizations.first
|
|
332
|
+
auth[:user_id] # Integer
|
|
333
|
+
auth[:user_name] # "Peter"
|
|
334
|
+
auth[:group_id] # Integer (0 if no group in path)
|
|
335
|
+
auth[:group_name] # "Backend" or nil
|
|
336
|
+
auth[:group_path] # "1,2,3" (comma-separated group IDs)
|
|
337
|
+
auth[:group_name_path] # "Company,Engineering,Backend"
|
|
338
|
+
auth[:role_id] # Integer (0 if no role in path)
|
|
339
|
+
auth[:role_name] # "Engineer" or nil
|
|
340
|
+
auth[:role_path] # "1,2" (comma-separated role IDs)
|
|
341
|
+
auth[:role_name_path] # "Employee,Engineer"
|
|
342
|
+
auth[:permission_id] # Integer (0 if no permission in path)
|
|
343
|
+
auth[:permission_name] # "read" or nil
|
|
344
|
+
auth[:resource_id] # Integer
|
|
345
|
+
auth[:resource_name] # "staging"
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Filter by user
|
|
349
|
+
|
|
350
|
+
```ruby
|
|
351
|
+
peter_auths = SuperAuth::Edge.authorizations.all.select { |a| a[:user_id] == peter.id }
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Check specific access
|
|
355
|
+
|
|
356
|
+
```ruby
|
|
357
|
+
auths = SuperAuth::Edge.authorizations.all
|
|
358
|
+
|
|
359
|
+
can_deploy = auths.any? { |a|
|
|
360
|
+
a[:user_id] == peter.id &&
|
|
361
|
+
a[:resource_name] == "staging" &&
|
|
362
|
+
a[:permission_name] == "deploy"
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Query individual strategies
|
|
367
|
+
|
|
368
|
+
```ruby
|
|
369
|
+
SuperAuth::Edge.users_groups_roles_permissions_resources # Strategy 1
|
|
370
|
+
SuperAuth::Edge.users_roles_permissions_resources # Strategy 2
|
|
371
|
+
SuperAuth::Edge.users_groups_permissions_resources # Strategy 3
|
|
372
|
+
SuperAuth::Edge.users_permissions_resources # Strategy 4
|
|
373
|
+
SuperAuth::Edge.users_resources # Strategy 5
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
## Revoking Access
|
|
377
|
+
|
|
378
|
+
Delete the edge to revoke access. All authorization paths that flowed through that edge are immediately removed.
|
|
379
|
+
|
|
380
|
+
```ruby
|
|
381
|
+
# Find and destroy the edge
|
|
382
|
+
edge = SuperAuth::Edge.where(user_id: guest.id, group_id: customers.id).first
|
|
383
|
+
edge.destroy
|
|
384
|
+
|
|
385
|
+
# Authorizations are recomputed -- guest no longer has access through that group
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## Rails Integration
|
|
389
|
+
|
|
390
|
+
### How it works
|
|
391
|
+
|
|
392
|
+
SuperAuth uses [Sequel](https://sequel.jeremyevans.net/) internally for its graph query engine and ships a set of ActiveRecord adapters so it integrates seamlessly with your Rails app. The `sequel-activerecord_connection` gem lets both ORMs share the same database connection, so there is nothing extra to configure.
|
|
393
|
+
|
|
394
|
+
On boot, the SuperAuth Railtie:
|
|
395
|
+
|
|
396
|
+
1. Detects your ActiveRecord database configuration
|
|
397
|
+
2. Creates a matching Sequel connection (shared via `sequel-activerecord_connection`)
|
|
398
|
+
3. Loads all SuperAuth models (both Sequel and ActiveRecord)
|
|
399
|
+
|
|
400
|
+
### ActiveRecord auto-filtering
|
|
401
|
+
|
|
402
|
+
Add `super_auth` to any model to automatically filter records based on the current user's authorizations:
|
|
403
|
+
|
|
404
|
+
```ruby
|
|
405
|
+
class Post < ApplicationRecord
|
|
406
|
+
super_auth
|
|
407
|
+
end
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
Set the current user in your controller (see [Rails Setup](#rails-setup) Step 4):
|
|
411
|
+
|
|
412
|
+
```ruby
|
|
413
|
+
class ApplicationController < ActionController::Base
|
|
414
|
+
before_action :set_super_auth_user
|
|
415
|
+
|
|
416
|
+
private
|
|
417
|
+
|
|
418
|
+
def set_super_auth_user
|
|
419
|
+
SuperAuth.current_user = current_user
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
Now queries are automatically scoped:
|
|
425
|
+
|
|
426
|
+
```ruby
|
|
427
|
+
# Only returns posts the current user is authorized to access
|
|
428
|
+
Post.all
|
|
429
|
+
Post.where(published: true)
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
The system user bypasses all filters:
|
|
433
|
+
|
|
434
|
+
```ruby
|
|
435
|
+
SuperAuth.current_user = SuperAuth::User.system
|
|
436
|
+
Post.all # Returns all posts
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### Linking to your app's models
|
|
440
|
+
|
|
441
|
+
Connect SuperAuth entities to your ActiveRecord models via `external_id` and `external_type`:
|
|
442
|
+
|
|
443
|
+
```ruby
|
|
444
|
+
# Link a SuperAuth user to your app's User model
|
|
445
|
+
sa_user = SuperAuth::User.create(
|
|
446
|
+
name: user.name,
|
|
447
|
+
external_id: user.id,
|
|
448
|
+
external_type: "User"
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
# Link a SuperAuth resource to your app's Post model
|
|
452
|
+
sa_resource = SuperAuth::Resource.create(
|
|
453
|
+
name: "posts",
|
|
454
|
+
external_type: "Post"
|
|
455
|
+
)
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
When `super_auth` is included in a model, the default scope matches the current user's `id` and class name against `external_id` / `external_type` in the authorizations table. This means your application user objects work directly -- no need to convert to SuperAuth users in the controller.
|
|
459
|
+
|
|
460
|
+
### ActiveRecord models
|
|
461
|
+
|
|
462
|
+
SuperAuth provides ActiveRecord-compatible models under the `SuperAuth::ActiveRecord` namespace:
|
|
463
|
+
|
|
464
|
+
```ruby
|
|
465
|
+
SuperAuth::ActiveRecord::User
|
|
466
|
+
SuperAuth::ActiveRecord::Group
|
|
467
|
+
SuperAuth::ActiveRecord::Role
|
|
468
|
+
SuperAuth::ActiveRecord::Permission
|
|
469
|
+
SuperAuth::ActiveRecord::Resource
|
|
470
|
+
SuperAuth::ActiveRecord::Edge
|
|
471
|
+
SuperAuth::ActiveRecord::Authorization
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
These models point to the same `super_auth_*` tables and can be used alongside the Sequel models. The ActiveRecord Edge model delegates authorization queries to the Sequel engine:
|
|
475
|
+
|
|
476
|
+
```ruby
|
|
477
|
+
# Works the same as SuperAuth::Edge.authorizations, but returns ActiveRecord objects
|
|
478
|
+
SuperAuth::ActiveRecord::Edge.authorizations
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
## Auditing
|
|
482
|
+
|
|
483
|
+
Every authorization path is stored with full context. This makes it straightforward to answer questions like:
|
|
484
|
+
|
|
485
|
+
**"Why does Peter have access to the design template?"**
|
|
486
|
+
|
|
487
|
+
```ruby
|
|
488
|
+
auths = SuperAuth::Edge.authorizations.all
|
|
489
|
+
peter_design = auths.select { |a|
|
|
490
|
+
a[:user_id] == peter.id && a[:resource_name] == "core_design_template"
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
peter_design.each do |auth|
|
|
494
|
+
puts "#{auth[:user_name]} -> #{auth[:group_name]} -> #{auth[:role_name]} -> #{auth[:permission_name]} -> #{auth[:resource_name]}"
|
|
495
|
+
end
|
|
496
|
+
# Peter -> Frontend -> Engineering -> create -> core_design_template
|
|
497
|
+
# Peter -> Frontend -> Engineering -> read -> core_design_template
|
|
498
|
+
# Peter -> Frontend -> Engineering -> update -> core_design_template
|
|
499
|
+
# Peter -> Frontend -> Engineering -> delete -> core_design_template
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
**"Who can deploy to staging?"**
|
|
503
|
+
|
|
504
|
+
```ruby
|
|
505
|
+
deployers = SuperAuth::Edge.authorizations.all.select { |a|
|
|
506
|
+
a[:resource_name] == "staging" && a[:permission_name] == "deploy"
|
|
507
|
+
}
|
|
508
|
+
deployers.map { |a| a[:user_name] }.uniq
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
## Visualization
|
|
512
|
+
|
|
513
|
+
SuperAuth includes an interactive graph visualization UI. After mounting the engine:
|
|
514
|
+
|
|
515
|
+
```ruby
|
|
516
|
+
# config/routes.rb
|
|
517
|
+
mount SuperAuth::Engine => '/super_auth'
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
Load sample data (optional):
|
|
521
|
+
|
|
522
|
+
```bash
|
|
523
|
+
rails runner "load File.join(SuperAuth::Engine.root, 'db/seeds/sample_data.rb')"
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
Visit `http://localhost:3000/super_auth/visualization` to see:
|
|
527
|
+
|
|
528
|
+
- Color-coded nodes for each entity type
|
|
529
|
+
- Interactive path finding
|
|
530
|
+
- Real-time authorization queries
|
|
531
|
+
|
|
532
|
+
## Full Example
|
|
533
|
+
|
|
534
|
+
Here's a complete example modeling a company with departments, roles, and resources:
|
|
535
|
+
|
|
536
|
+
```ruby
|
|
537
|
+
# Organization
|
|
538
|
+
company = SuperAuth::Group.create(name: "Acme Corp")
|
|
539
|
+
engineering = SuperAuth::Group.create(name: "Engineering", parent: company)
|
|
540
|
+
backend = SuperAuth::Group.create(name: "Backend", parent: engineering)
|
|
541
|
+
frontend = SuperAuth::Group.create(name: "Frontend", parent: engineering)
|
|
542
|
+
sales = SuperAuth::Group.create(name: "Sales", parent: company)
|
|
543
|
+
|
|
544
|
+
# Roles
|
|
545
|
+
developer = SuperAuth::Role.create(name: "Developer")
|
|
546
|
+
senior_dev = SuperAuth::Role.create(name: "Senior Developer", parent: developer)
|
|
547
|
+
ops = SuperAuth::Role.create(name: "Operations", parent: developer)
|
|
548
|
+
|
|
549
|
+
# Permissions
|
|
550
|
+
read = SuperAuth::Permission.create(name: "read")
|
|
551
|
+
write = SuperAuth::Permission.create(name: "write")
|
|
552
|
+
deploy = SuperAuth::Permission.create(name: "deploy")
|
|
553
|
+
|
|
554
|
+
# Resources
|
|
555
|
+
api = SuperAuth::Resource.create(name: "api", external_type: "API")
|
|
556
|
+
dashboard = SuperAuth::Resource.create(name: "dashboard", external_type: "Dashboard")
|
|
557
|
+
prod_db = SuperAuth::Resource.create(name: "production_db")
|
|
558
|
+
|
|
559
|
+
# Users
|
|
560
|
+
alice = SuperAuth::User.create(name: "Alice") # Senior backend dev
|
|
561
|
+
bob = SuperAuth::User.create(name: "Bob") # Frontend dev
|
|
562
|
+
carol = SuperAuth::User.create(name: "Carol") # Ops
|
|
563
|
+
|
|
564
|
+
# Assign users to groups
|
|
565
|
+
SuperAuth::Edge.create(user: alice, group: backend)
|
|
566
|
+
SuperAuth::Edge.create(user: bob, group: frontend)
|
|
567
|
+
SuperAuth::Edge.create(user: carol, group: backend)
|
|
568
|
+
|
|
569
|
+
# Assign roles to groups
|
|
570
|
+
SuperAuth::Edge.create(group: backend, role: senior_dev)
|
|
571
|
+
SuperAuth::Edge.create(group: frontend, role: developer)
|
|
572
|
+
|
|
573
|
+
# Give Carol ops role directly
|
|
574
|
+
SuperAuth::Edge.create(user: carol, role: ops)
|
|
575
|
+
|
|
576
|
+
# Assign permissions to roles
|
|
577
|
+
SuperAuth::Edge.create(role: developer, permission: read)
|
|
578
|
+
SuperAuth::Edge.create(role: developer, permission: write)
|
|
579
|
+
SuperAuth::Edge.create(role: ops, permission: deploy)
|
|
580
|
+
|
|
581
|
+
# Assign permissions to resources
|
|
582
|
+
SuperAuth::Edge.create(permission: read, resource: api)
|
|
583
|
+
SuperAuth::Edge.create(permission: write, resource: api)
|
|
584
|
+
SuperAuth::Edge.create(permission: read, resource: dashboard)
|
|
585
|
+
SuperAuth::Edge.create(permission: deploy, resource: api)
|
|
586
|
+
SuperAuth::Edge.create(permission: deploy, resource: prod_db)
|
|
587
|
+
|
|
588
|
+
# Now query:
|
|
589
|
+
auths = SuperAuth::Edge.authorizations.all
|
|
590
|
+
|
|
591
|
+
# Alice can read and write the API (via Backend -> Senior Developer -> read/write -> api)
|
|
592
|
+
# Bob can read and write the API (via Frontend -> Developer -> read/write -> api)
|
|
593
|
+
# Bob can read the dashboard (via Frontend -> Developer -> read -> dashboard)
|
|
594
|
+
# Carol can deploy to the API and prod_db (via direct ops role -> deploy)
|
|
595
|
+
# Carol can also read/write the API (via Backend -> Senior Developer -> read/write -> api)
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
## Configuration Reference
|
|
599
|
+
|
|
600
|
+
| Method | Description |
|
|
601
|
+
|---------------------------------|------------------------------------------------------|
|
|
602
|
+
| `SuperAuth.load` | Load all SuperAuth models |
|
|
603
|
+
| `SuperAuth.db` | Access the Sequel database connection |
|
|
604
|
+
| `SuperAuth.db = connection` | Set a custom Sequel database connection |
|
|
605
|
+
| `SuperAuth.current_user = user` | Set the current user (required for AR auto-filtering)|
|
|
606
|
+
| `SuperAuth.current_user` | Get the current user |
|
|
607
|
+
| `SuperAuth.install_migrations` | Create all `super_auth_*` tables |
|
|
608
|
+
| `SuperAuth.uninstall_migrations`| Drop all `super_auth_*` tables |
|
|
609
|
+
|
|
610
|
+
### Environment Variables
|
|
611
|
+
|
|
612
|
+
| Variable | Description |
|
|
613
|
+
|----------------------------|-------------------------------------------|
|
|
614
|
+
| `SUPER_AUTH_DATABASE_URL` | Database connection string (non-Rails) |
|
|
615
|
+
| `SUPER_AUTH_LOG_LEVEL` | Set to `"debug"` for SQL query logging |
|
|
616
|
+
|
|
617
|
+
## License
|
|
618
|
+
|
|
619
|
+
SuperAuth is available as open source under the [GPL License](https://www.gnu.org/licenses/quick-guide-gplv3.html).
|
data/VISUALIZATION.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# SuperAuth Graph Visualization
|
|
2
|
+
|
|
3
|
+
SuperAuth includes an interactive graph visualization tool that helps you understand and debug your authorization rules.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
### 1. Run the Installer
|
|
8
|
+
|
|
9
|
+
Generate the initializer and install migrations:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
rails generate super_auth:install
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This will:
|
|
16
|
+
- Create `config/initializers/super_auth.rb`
|
|
17
|
+
- Install SuperAuth database migrations
|
|
18
|
+
- Show you the next steps
|
|
19
|
+
|
|
20
|
+
### 2. Mount the Engine
|
|
21
|
+
|
|
22
|
+
Add the following to your `config/routes.rb`:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
Rails.application.routes.draw do
|
|
26
|
+
mount SuperAuth::Engine => '/super_auth'
|
|
27
|
+
|
|
28
|
+
# Your other routes...
|
|
29
|
+
end
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
### Interactive Graph
|
|
35
|
+
|
|
36
|
+
- **Nodes**: Color-coded by type (Users, Groups, Roles, Permissions, Resources)
|
|
37
|
+
- **Edges**: Solid lines for authorization relationships, dashed for hierarchy
|
|
38
|
+
- **Zoom & Pan**: Navigate large graphs easily
|
|
39
|
+
- **Click nodes**: View node details
|
|
40
|
+
|
|
41
|
+
### Authorization Query
|
|
42
|
+
|
|
43
|
+
1. Select a user from the dropdown
|
|
44
|
+
2. Select a resource from the dropdown
|
|
45
|
+
3. Click "Find Authorization Paths"
|
|
46
|
+
4. View all paths that grant access
|
|
47
|
+
5. See the first path highlighted on the graph
|
|
48
|
+
|
|
49
|
+
### Statistics Panel
|
|
50
|
+
|
|
51
|
+
Real-time counts of:
|
|
52
|
+
- Users
|
|
53
|
+
- Groups (with hierarchical relationships)
|
|
54
|
+
- Roles (with hierarchical relationships)
|
|
55
|
+
- Permissions
|
|
56
|
+
- Resources
|
|
57
|
+
- Authorization edges
|
|
58
|
+
|