super_auth 0.4.0 → 0.8.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 +90 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +8 -1
- data/README.md +132 -49
- data/USAGE.md +98 -31
- data/config/routes.rb +9 -71
- data/db/migrate/10_add_super_auth_label_to_resources.rb +13 -0
- data/db/migrate/11_add_parent_id_to_resources.rb +32 -0
- data/db/migrate_activerecord/20250101000010_add_super_auth_label_to_super_auth_resources.rb +5 -0
- data/db/migrate_activerecord/20250101000011_add_parent_id_to_super_auth_resources.rb +9 -0
- data/db/seeds/sample_data.rb +1 -0
- data/exe/super_auth-editor +9 -0
- data/lib/generators/super_auth/install/templates/README +18 -11
- data/lib/generators/super_auth/rls/templates/migration.rb.erb +2 -0
- data/lib/super_auth/active_record/authorization.rb +7 -0
- data/lib/super_auth/active_record/by_current_user.rb +1 -1
- data/lib/super_auth/active_record/resource.rb +45 -0
- data/lib/super_auth/active_record/user.rb +3 -1
- data/lib/super_auth/authorization.rb +24 -0
- data/lib/super_auth/edge.rb +54 -18
- data/lib/super_auth/editor/cli.rb +91 -0
- data/lib/super_auth/editor/index.html +430 -0
- data/lib/super_auth/editor/seed.rb +176 -0
- data/lib/super_auth/editor.rb +288 -0
- data/lib/super_auth/nestable.rb +16 -3
- data/lib/super_auth/railtie.rb +9 -2
- data/lib/super_auth/resource.rb +57 -0
- data/lib/super_auth/rls.rb +164 -34
- data/lib/super_auth/user.rb +3 -1
- data/lib/super_auth/version.rb +1 -1
- data/lib/super_auth.rb +73 -5
- data/lib/tasks/super_auth_tasks.rake +28 -0
- metadata +13 -8
- data/VISUALIZATION.md +0 -58
- data/app/controllers/super_auth/graph_controller.rb +0 -654
- data/app/views/super_auth/graph/index.html.erb +0 -1408
- data/super_auth.gemspec +0 -35
- data/visualization.html +0 -747
data/USAGE.md
CHANGED
|
@@ -62,7 +62,7 @@ rails railties:install:migrations
|
|
|
62
62
|
rails db:migrate
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
This creates the `super_auth_*` tables (users, groups, roles, permissions, resources, edges, authorizations) alongside your application's tables.
|
|
65
|
+
This creates the `super_auth_*` tables (users, groups, roles, permissions, resources, edges, authorizations) alongside your application's tables. The engine does not run its migrations by itself, so repeat both commands after upgrading to a version that ships a new one (0.8.0 adds migration 11, `parent_id` on resources).
|
|
66
66
|
|
|
67
67
|
**Step 4.** Set the current user in your controller:
|
|
68
68
|
|
|
@@ -95,14 +95,18 @@ Post.all # only posts the current user can access
|
|
|
95
95
|
Post.where(published: true) # scoped AND filtered by authorization
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
-
**Step 6 (optional).** Mount the
|
|
98
|
+
**Step 6 (optional).** Mount the engine, which serves the graph editor. It has no
|
|
99
|
+
authentication of its own, so mount it inside yours:
|
|
99
100
|
|
|
100
101
|
```ruby
|
|
101
102
|
# config/routes.rb
|
|
102
|
-
|
|
103
|
+
authenticate :admin do # Devise; or a constraints block
|
|
104
|
+
mount SuperAuth::Engine => '/super_auth'
|
|
105
|
+
end
|
|
103
106
|
```
|
|
104
107
|
|
|
105
|
-
Then
|
|
108
|
+
Then open `http://localhost:3000/super_auth` to edit the graph. Edits take effect at
|
|
109
|
+
runtime after **Recompile** (or `SuperAuth::ActiveRecord::Authorization.compile!`).
|
|
106
110
|
|
|
107
111
|
### Standalone Setup (without Rails)
|
|
108
112
|
|
|
@@ -130,7 +134,7 @@ SuperAuth models authorization as a graph with 5 entity types:
|
|
|
130
134
|
| **Group** | Organizational units (teams, departments, etc) | Yes (nested) |
|
|
131
135
|
| **Role** | Job titles or permission sets | Yes (nested) |
|
|
132
136
|
| **Permission** | Actions (read, write, deploy, etc) | No |
|
|
133
|
-
| **Resource** | Things being protected (files, APIs, records) |
|
|
137
|
+
| **Resource** | Things being protected (files, APIs, records) | Yes (nested) |
|
|
134
138
|
|
|
135
139
|
**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
140
|
|
|
@@ -241,22 +245,77 @@ write_perm = SuperAuth::Permission.create(name: "write")
|
|
|
241
245
|
deploy_perm = SuperAuth::Permission.create(name: "deploy")
|
|
242
246
|
```
|
|
243
247
|
|
|
244
|
-
### Resources
|
|
248
|
+
### Resources (hierarchical)
|
|
245
249
|
|
|
246
|
-
Resources represent what you are protecting.
|
|
250
|
+
Resources represent what you are protecting. A node with neither `external_type` nor `external_id` is a container; a node with both points at one record of your application. Resources nest like groups and roles, and a grant on a node reaches the node and every node under it, so the usual shape is a container per folder, project, tenant or whatever your application nests records under, with the records registered beneath it.
|
|
247
251
|
|
|
248
252
|
```ruby
|
|
249
|
-
#
|
|
253
|
+
# A named resource with nothing behind it
|
|
250
254
|
staging = SuperAuth::Resource.create(name: "staging")
|
|
251
255
|
|
|
252
|
-
#
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
external_type: "Post"
|
|
256
|
+
# A container, and a record of your app registered under it
|
|
257
|
+
reports = SuperAuth::Resource.create(name: "reports")
|
|
258
|
+
q3 = SuperAuth::Resource.create(
|
|
259
|
+
name: "Q3 report",
|
|
260
|
+
external_type: "Post",
|
|
261
|
+
external_id: post.id,
|
|
262
|
+
parent: reports
|
|
257
263
|
)
|
|
264
|
+
|
|
265
|
+
# A grant on the container reaches q3, and every node registered under
|
|
266
|
+
# reports later, as of the next compile!
|
|
267
|
+
SuperAuth::Edge.create(permission: read_perm, resource: reports)
|
|
258
268
|
```
|
|
259
269
|
|
|
270
|
+
Navigate the tree the same way as groups:
|
|
271
|
+
|
|
272
|
+
```ruby
|
|
273
|
+
SuperAuth::Resource.roots # nodes with no parent
|
|
274
|
+
reports.children_dataset.all # => [q3]
|
|
275
|
+
q3.parent # => reports
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
The compiled row for `q3` carries `q3`'s own `external_type` and `external_id` whether the edge was drawn to `q3` or to `reports`: runtime reads the record a grant reaches and nothing about how it got there. There are no resource path columns in the compiled table; "granted through which container" is a question for the graph (`parent`, `children_dataset`) and the editor.
|
|
279
|
+
|
|
280
|
+
#### Deprecated: type-level (wildcard) nodes
|
|
281
|
+
|
|
282
|
+
A node with an `external_type` and no `external_id` is a type-level, or wildcard, node: at runtime it means every record of that type, present and future. `ByCurrentUser` skips per-record filtering when one matches, and the row-level security policy's `resource_external_id IS NULL OR` clause does the same in the database.
|
|
283
|
+
|
|
284
|
+
```ruby
|
|
285
|
+
# Deprecated: every Post, present and future
|
|
286
|
+
posts = SuperAuth::Resource.create(name: "posts", external_type: "Post")
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Wildcard nodes are deprecated as of 0.8.0 and still work: `compile!` warns once per compile naming the ones that exist (silence it with `SuperAuth.deprecator.silenced = true`, or in Rails through `config.active_support.deprecation`), and no removal version is promised. What `compile!` refuses, with a `SuperAuth::Error` naming the node ids, is a wildcard with a parent or children: nested in the tree it would reach every record of its type through its ancestors' grants. A wildcard stays at the root with no children, or gets an `external_id`.
|
|
290
|
+
|
|
291
|
+
Under Postgres row-level security a wildcard remains the only way to authorize INSERT this release. The policy is `FOR ALL` with `USING` reused as `WITH CHECK`, and a per-record row can only match an id that has already been registered and compiled. A container is not a replacement for it there: it loses INSERT, it needs a node saved and a full recompile for every new record, and saving that node needs a role that can write the gem's tables, which `enable` grants `SELECT` on only. The successor is a grant on a parent record (`SuperAuth::RLS.enable(:documents, resource_type: "Document", parent: { column: :folder_id, resource_type: "Folder" })`, with a `ByCurrentUser` mirror), planned for the next release.
|
|
292
|
+
|
|
293
|
+
##### Migrating a wildcard to a container
|
|
294
|
+
|
|
295
|
+
Where the type is not under row-level security, or INSERT is not needed, a wildcard becomes a container plus one node per record. Move the edges off the wildcard before destroying it, so the grants survive, then compile. `Post` is a `super_auth` model, so its default scope hides every row from a process with no current user — the loop reads through `unscoped`; where RLS is installed the database enforces the same rule, so the work runs as the system user:
|
|
296
|
+
|
|
297
|
+
```ruby
|
|
298
|
+
wildcard = SuperAuth::Resource.where(external_type: "Post", external_id: nil).first
|
|
299
|
+
|
|
300
|
+
migrate = proc do
|
|
301
|
+
container = SuperAuth::Resource.create(name: "posts") # untyped: a container
|
|
302
|
+
Post.unscoped.find_each do |post| # ByCurrentUser hides every row without a current user
|
|
303
|
+
SuperAuth::Resource.create(name: post.title, external_type: "Post", external_id: post.id, parent: container)
|
|
304
|
+
end
|
|
305
|
+
SuperAuth::Edge.where(resource_id: wildcard.id).update(resource_id: container.id) # update_all on the ActiveRecord twin
|
|
306
|
+
wildcard.destroy
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
if SuperAuth::RLS.installed?
|
|
310
|
+
SuperAuth.as(SuperAuth::User.system, &migrate)
|
|
311
|
+
else
|
|
312
|
+
SuperAuth.db.transaction(&migrate)
|
|
313
|
+
end
|
|
314
|
+
SuperAuth::Authorization.compile! # SuperAuth::ActiveRecord::Authorization.compile! in Rails
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Under row-level security this loses INSERT on `posts` until the parent-record grant exists, and a `Post` created afterwards needs its own node and a recompile before anyone sees it.
|
|
318
|
+
|
|
260
319
|
## Drawing Edges
|
|
261
320
|
|
|
262
321
|
Edges are the core of SuperAuth. Each edge connects exactly two entities.
|
|
@@ -307,6 +366,8 @@ SuperAuth automatically evaluates 5 pathing strategies and unions the results. Y
|
|
|
307
366
|
|
|
308
367
|
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
368
|
|
|
369
|
+
Resources nest too, at the other end of the path: a grant on a container reaches every node registered under it (see [Resources](#resources-hierarchical)).
|
|
370
|
+
|
|
310
371
|
```ruby
|
|
311
372
|
# Bethany is in Company (the root group)
|
|
312
373
|
SuperAuth::Edge.create(user: bethany, group: company)
|
|
@@ -345,6 +406,8 @@ auth[:resource_id] # Integer
|
|
|
345
406
|
auth[:resource_name] # "staging"
|
|
346
407
|
```
|
|
347
408
|
|
|
409
|
+
`resource_id` and `resource_name` are the node the grant reaches: a row compiled through a container names the descendant, not the container, and there is no resource path column (see [Resources](#resources-hierarchical)).
|
|
410
|
+
|
|
348
411
|
### Filter by user
|
|
349
412
|
|
|
350
413
|
```ruby
|
|
@@ -457,6 +520,8 @@ Post::PostPublishPermission.find(id) # needs a "Post::PostPublishPermission" gra
|
|
|
457
520
|
|
|
458
521
|
Approve the subclass like any other resource — register a `SuperAuth::Resource` with `external_type: "Post::PostPublishPermission"` and draw edges to it, then recompile with `SuperAuth::ActiveRecord::Authorization.compile!`.
|
|
459
522
|
|
|
523
|
+
The resource tree is containment, not inheritance. A row compiled through a container copies the descendant node's own `external_type`, which is why the rule above survives nesting — but a `"Post::PostPublishPermission"` node registered *under* the `"Post"` node is a descendant of it and receives every grant on `"Post"`. Register capability nodes as siblings of their base-class nodes, or in a container beside them, never as their children.
|
|
524
|
+
|
|
460
525
|
For database-side enforcement of the same rules see "Postgres Row-Level Security" in the README (`SuperAuth::RLS.enable` — visibility is derived automatically from `SuperAuth.current_user`).
|
|
461
526
|
|
|
462
527
|
### Linking to your app's models
|
|
@@ -471,14 +536,15 @@ sa_user = SuperAuth::User.create(
|
|
|
471
536
|
external_type: "User"
|
|
472
537
|
)
|
|
473
538
|
|
|
474
|
-
# Link a SuperAuth resource to
|
|
539
|
+
# Link a SuperAuth resource to one Post
|
|
475
540
|
sa_resource = SuperAuth::Resource.create(
|
|
476
|
-
name:
|
|
477
|
-
external_type: "Post"
|
|
541
|
+
name: post.title,
|
|
542
|
+
external_type: "Post",
|
|
543
|
+
external_id: post.id
|
|
478
544
|
)
|
|
479
545
|
```
|
|
480
546
|
|
|
481
|
-
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.
|
|
547
|
+
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. On the resource side it matches the record's class name and `id` against `resource_external_type` / `resource_external_id`; a node with the type and no id matches every record of the type, the deprecated wildcard shape described under [Resources](#resources-hierarchical).
|
|
482
548
|
|
|
483
549
|
### ActiveRecord models
|
|
484
550
|
|
|
@@ -533,25 +599,25 @@ deployers.map { |a| a[:user_name] }.uniq
|
|
|
533
599
|
|
|
534
600
|
## Visualization
|
|
535
601
|
|
|
536
|
-
|
|
602
|
+
The graph editor shows the whole graph as five boxes (groups, roles, users,
|
|
603
|
+
permissions, resources), drawing groups, roles and resources as trees; click any record
|
|
604
|
+
to trace what it can reach and what reaches it, connect records to draw edges, create
|
|
605
|
+
records (including a resource container under a chosen parent), delete records and
|
|
606
|
+
edges, and recompile. The editor makes containers; your application registers records
|
|
607
|
+
under them. See the README's "Graph editor" section for the full description.
|
|
537
608
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
609
|
+
- Rails: mount the engine inside your own authentication (Step 6 above) and open
|
|
610
|
+
`http://localhost:3000/super_auth`.
|
|
611
|
+
- Anywhere else: `super_auth-editor` serves it on loopback against
|
|
612
|
+
`SUPER_AUTH_DATABASE_URL`, or `require "super_auth/editor"` and mount
|
|
613
|
+
`SuperAuth::Editor` in any Rack app, inside your authentication.
|
|
542
614
|
|
|
543
|
-
Load sample data (optional):
|
|
615
|
+
Load sample data (optional, Rails):
|
|
544
616
|
|
|
545
617
|
```bash
|
|
546
618
|
rails runner "load File.join(SuperAuth::Engine.root, 'db/seeds/sample_data.rb')"
|
|
547
619
|
```
|
|
548
620
|
|
|
549
|
-
Visit `http://localhost:3000/super_auth/visualization` to see:
|
|
550
|
-
|
|
551
|
-
- Color-coded nodes for each entity type
|
|
552
|
-
- Interactive path finding
|
|
553
|
-
- Real-time authorization queries
|
|
554
|
-
|
|
555
621
|
## Full Example
|
|
556
622
|
|
|
557
623
|
Here's a complete example modeling a company with departments, roles, and resources:
|
|
@@ -575,8 +641,8 @@ write = SuperAuth::Permission.create(name: "write")
|
|
|
575
641
|
deploy = SuperAuth::Permission.create(name: "deploy")
|
|
576
642
|
|
|
577
643
|
# Resources
|
|
578
|
-
api = SuperAuth::Resource.create(name: "api"
|
|
579
|
-
dashboard = SuperAuth::Resource.create(name: "dashboard"
|
|
644
|
+
api = SuperAuth::Resource.create(name: "api")
|
|
645
|
+
dashboard = SuperAuth::Resource.create(name: "dashboard")
|
|
580
646
|
prod_db = SuperAuth::Resource.create(name: "production_db")
|
|
581
647
|
|
|
582
648
|
# Users
|
|
@@ -629,6 +695,7 @@ auths = SuperAuth::Edge.authorizations.all
|
|
|
629
695
|
| `SuperAuth.current_user` | Get the current user |
|
|
630
696
|
| `SuperAuth.install_migrations` | Create all `super_auth_*` tables |
|
|
631
697
|
| `SuperAuth.uninstall_migrations`| Drop all `super_auth_*` tables |
|
|
698
|
+
| `SuperAuth.deprecator` | Where deprecation warnings go; `silenced = true` quiets them |
|
|
632
699
|
|
|
633
700
|
### Environment Variables
|
|
634
701
|
|
data/config/routes.rb
CHANGED
|
@@ -1,73 +1,11 @@
|
|
|
1
|
+
require "super_auth/editor"
|
|
2
|
+
|
|
3
|
+
# The engine serves the graph editor at its mount point. Mount it inside your
|
|
4
|
+
# own authentication: it has none of its own.
|
|
5
|
+
#
|
|
6
|
+
# authenticate :admin do
|
|
7
|
+
# mount SuperAuth::Engine => "/super_auth"
|
|
8
|
+
# end
|
|
1
9
|
SuperAuth::Engine.routes.draw do
|
|
2
|
-
|
|
3
|
-
get '/', to: 'graph#index', as: :root
|
|
4
|
-
get '/graph', to: 'graph#index'
|
|
5
|
-
|
|
6
|
-
# Graph data API
|
|
7
|
-
get '/graph/data', to: 'graph#data'
|
|
8
|
-
get '/graph/orphaned', to: 'graph#orphaned'
|
|
9
|
-
post '/graph/compile_authorizations', to: 'graph#compile_authorizations'
|
|
10
|
-
|
|
11
|
-
# Authorization check
|
|
12
|
-
get '/graph/authorize', to: 'graph#authorize'
|
|
13
|
-
|
|
14
|
-
# Legacy visualization endpoint
|
|
15
|
-
get '/visualization', to: 'graph#visualization'
|
|
16
|
-
|
|
17
|
-
# CRUD operations for graph entities
|
|
18
|
-
scope :graph do
|
|
19
|
-
resources :users, only: [:create, :destroy], controller: 'graph' do
|
|
20
|
-
collection do
|
|
21
|
-
post '/', action: :create_user
|
|
22
|
-
end
|
|
23
|
-
member do
|
|
24
|
-
delete '/', action: :delete_user
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
resources :groups, only: [:create, :destroy], controller: 'graph' do
|
|
29
|
-
collection do
|
|
30
|
-
post '/', action: :create_group
|
|
31
|
-
end
|
|
32
|
-
member do
|
|
33
|
-
delete '/', action: :delete_group
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
resources :roles, only: [:create, :destroy], controller: 'graph' do
|
|
38
|
-
collection do
|
|
39
|
-
post '/', action: :create_role
|
|
40
|
-
end
|
|
41
|
-
member do
|
|
42
|
-
delete '/', action: :delete_role
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
resources :permissions, only: [:create, :destroy], controller: 'graph' do
|
|
47
|
-
collection do
|
|
48
|
-
post '/', action: :create_permission
|
|
49
|
-
end
|
|
50
|
-
member do
|
|
51
|
-
delete '/', action: :delete_permission
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
resources :graph_resources, only: [:create, :destroy], controller: 'graph', path: 'resources' do
|
|
56
|
-
collection do
|
|
57
|
-
post '/', action: :create_resource
|
|
58
|
-
end
|
|
59
|
-
member do
|
|
60
|
-
delete '/', action: :delete_resource
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
resources :edges, only: [:create, :destroy], controller: 'graph' do
|
|
65
|
-
collection do
|
|
66
|
-
post '/', action: :create_edge
|
|
67
|
-
end
|
|
68
|
-
member do
|
|
69
|
-
delete '/', action: :delete_edge
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
10
|
+
mount SuperAuth::Editor, at: "/"
|
|
73
11
|
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Sequel.migration do
|
|
2
|
+
# Resources nest like groups and roles (2_groups.rb): same integer type as
|
|
3
|
+
# the pk, deferrable only where supported. The index is skipped on MySQL
|
|
4
|
+
# for the reason in 8_add_indexes_to_edges.rb — InnoDB indexes the foreign
|
|
5
|
+
# key column itself and will not drop that index while the constraint
|
|
6
|
+
# stands. Both branches compute database_type before alter_table: inside
|
|
7
|
+
# the block self is the generator.
|
|
8
|
+
up do
|
|
9
|
+
is_postgres = database_type == :postgres
|
|
10
|
+
is_mysql = [:mysql, :mysql2].include?(database_type)
|
|
11
|
+
|
|
12
|
+
alter_table(:super_auth_resources) do
|
|
13
|
+
if is_postgres
|
|
14
|
+
add_foreign_key :parent_id, :super_auth_resources, deferrable: true, type: :integer
|
|
15
|
+
else
|
|
16
|
+
add_foreign_key :parent_id, :super_auth_resources, type: :integer
|
|
17
|
+
end
|
|
18
|
+
add_index :parent_id unless is_mysql
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# drop_foreign_key drops the constraint and then the column; MySQL refuses
|
|
23
|
+
# to drop a column a constraint still depends on.
|
|
24
|
+
down do
|
|
25
|
+
is_mysql = [:mysql, :mysql2].include?(database_type)
|
|
26
|
+
|
|
27
|
+
alter_table(:super_auth_resources) do
|
|
28
|
+
drop_index :parent_id unless is_mysql
|
|
29
|
+
drop_foreign_key :parent_id
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
class AddParentIdToSuperAuthResources < ActiveRecord::Migration[7.0]
|
|
2
|
+
# Mirrors db/migrate/11_add_parent_id_to_resources.rb. No index on MySQL:
|
|
3
|
+
# InnoDB indexes the foreign key column itself.
|
|
4
|
+
def change
|
|
5
|
+
add_column :super_auth_resources, :parent_id, :bigint
|
|
6
|
+
add_foreign_key :super_auth_resources, :super_auth_resources, column: :parent_id
|
|
7
|
+
add_index :super_auth_resources, :parent_id unless connection.adapter_name.match?(/mysql|trilogy/i)
|
|
8
|
+
end
|
|
9
|
+
end
|
data/db/seeds/sample_data.rb
CHANGED
|
@@ -4,26 +4,33 @@ SuperAuth initializer created at config/initializers/super_auth.rb
|
|
|
4
4
|
|
|
5
5
|
Next steps:
|
|
6
6
|
|
|
7
|
-
1.
|
|
7
|
+
1. Copy the engine's migrations into your app and run them:
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
rails railties:install:migrations
|
|
10
|
+
rails db:migrate
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
(the engine does not run its migrations by itself; repeat both after an
|
|
13
|
+
upgrade that ships a new one)
|
|
12
14
|
|
|
13
|
-
2. Mount the engine in config/routes.rb
|
|
15
|
+
2. Mount the engine in config/routes.rb, inside your own authentication.
|
|
16
|
+
It serves the graph editor, which has no authentication of its own:
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
authenticate :admin do
|
|
19
|
+
mount SuperAuth::Engine => '/super_auth'
|
|
20
|
+
end
|
|
16
21
|
|
|
17
|
-
3. (Optional) Load sample data to
|
|
22
|
+
3. (Optional) Load sample data to have a graph to look at:
|
|
18
23
|
|
|
19
24
|
rails runner "load File.join(SuperAuth::Engine.root, 'db/seeds/sample_data.rb')"
|
|
20
25
|
|
|
21
|
-
4. Start your Rails server and
|
|
26
|
+
4. Start your Rails server and open the editor:
|
|
22
27
|
|
|
23
|
-
http://localhost:3000/super_auth
|
|
28
|
+
http://localhost:3000/super_auth
|
|
24
29
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
Edits change the graph; runtime access comes from the compiled table,
|
|
31
|
+
so press Recompile (or run SuperAuth::ActiveRecord::Authorization.compile!)
|
|
32
|
+
after editing.
|
|
33
|
+
|
|
34
|
+
For more information, see README.md ("Graph editor") and USAGE.md.
|
|
28
35
|
|
|
29
36
|
===============================================================================
|
|
@@ -3,6 +3,8 @@ class EnableSuperAuthRls < ActiveRecord::Migration[<%= ActiveRecord::Migration.c
|
|
|
3
3
|
<% model_names.each do |model| -%>
|
|
4
4
|
SuperAuth::RLS.enable(:<%= model.tableize.tr('/', '_') %>, resource_type: "<%= model.camelize %>")
|
|
5
5
|
<% end -%>
|
|
6
|
+
# Roles allowed to bypass the policies (migrations, seeds, admin jobs):
|
|
7
|
+
# SuperAuth::RLS.grant_system(:app_admin)
|
|
6
8
|
end
|
|
7
9
|
|
|
8
10
|
def down
|
|
@@ -9,11 +9,18 @@ class SuperAuth::ActiveRecord::Authorization < ActiveRecord::Base
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
# Clears and repopulates the authorizations table from the current graph.
|
|
12
|
+
# The wildcard guard runs before the delete, so a refused compile leaves
|
|
13
|
+
# the previous rows in place; the deprecation notice follows the commit.
|
|
12
14
|
def compile!
|
|
13
15
|
transaction do
|
|
16
|
+
# Sequel runs on this transaction's connection (sequel-activerecord_connection),
|
|
17
|
+
# so the JIT switch lands in it; see SuperAuth::Authorization.compile!.
|
|
18
|
+
SuperAuth.db.run "SET LOCAL jit = off" if SuperAuth.db.database_type == :postgres
|
|
19
|
+
SuperAuth::Resource.assert_compilable!
|
|
14
20
|
delete_all
|
|
15
21
|
from_graph.each { |auth| create!(auth.attributes.except("id")) }
|
|
16
22
|
end
|
|
23
|
+
SuperAuth::Resource.warn_deprecated_wildcards
|
|
17
24
|
count
|
|
18
25
|
end
|
|
19
26
|
end
|
|
@@ -30,7 +30,7 @@ module SuperAuth::ActiveRecord::ByCurrentUser
|
|
|
30
30
|
self
|
|
31
31
|
else
|
|
32
32
|
user_where =
|
|
33
|
-
if SuperAuth.
|
|
33
|
+
if SuperAuth.internal_user?(SuperAuth.current_user)
|
|
34
34
|
{ user_id: SuperAuth.current_user.id }
|
|
35
35
|
else
|
|
36
36
|
{ user_external_id: SuperAuth.current_user.id, user_external_type: SuperAuth.current_user.class.name }
|
|
@@ -1,4 +1,49 @@
|
|
|
1
1
|
class SuperAuth::ActiveRecord::Resource < ActiveRecord::Base
|
|
2
2
|
self.table_name = 'super_auth_resources'
|
|
3
3
|
belongs_to :external, polymorphic: true, optional: true
|
|
4
|
+
# optional: is load-bearing: Rails hosts set belongs_to_required_by_default,
|
|
5
|
+
# the gem's own suite does not, so a missing one passes CI and fails the
|
|
6
|
+
# host on every root node.
|
|
7
|
+
belongs_to :parent, class_name: 'SuperAuth::ActiveRecord::Resource', optional: true
|
|
8
|
+
|
|
9
|
+
# `super_auth_label` is a stored snapshot of the application record's human
|
|
10
|
+
# name, so the editor can render "Gulf War presumptive" instead of
|
|
11
|
+
# Claim#3a00b6fa and `super_auth-editor` can do it against a bare
|
|
12
|
+
# SUPER_AUTH_DATABASE_URL with no application loaded. It carries the prefix
|
|
13
|
+
# for the same reason the opt-in method on the host's model does: `label` is
|
|
14
|
+
# a name applications want for themselves.
|
|
15
|
+
#
|
|
16
|
+
# Deriving it here means a host that already syncs its resources gets labels
|
|
17
|
+
# with no extra wiring; renames still need refresh_label!, since they do not
|
|
18
|
+
# write this row.
|
|
19
|
+
before_save :set_label, if: :external_id?
|
|
20
|
+
|
|
21
|
+
# Re-derive the label after the application record is renamed. Hosts call it
|
|
22
|
+
# from whatever already syncs the node; super_auth:labels:backfill calls it
|
|
23
|
+
# for every row.
|
|
24
|
+
def refresh_label!
|
|
25
|
+
derived = derived_label
|
|
26
|
+
update_column(:super_auth_label, derived) unless derived.nil?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def set_label
|
|
32
|
+
derived = derived_label
|
|
33
|
+
self.super_auth_label = derived unless derived.nil?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# ponytail: a nil derivation never overwrites a stored label, in either
|
|
37
|
+
# path, and never fails the save. Three things derive nil and none of them
|
|
38
|
+
# means "this record has no name": RLS makes the application record
|
|
39
|
+
# unreadable without an asserted identity, external_type is a plain string
|
|
40
|
+
# that can name a class this process has not loaded, and id-less rows — a
|
|
41
|
+
# container, or a deprecated wildcard — have no record to name at all.
|
|
42
|
+
# Writing nil for any of them would turn "this label is stale" into data,
|
|
43
|
+
# which is the failure this column exists to avoid.
|
|
44
|
+
def derived_label
|
|
45
|
+
SuperAuth.label_for(external)
|
|
46
|
+
rescue NameError
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
4
49
|
end
|
|
@@ -5,7 +5,9 @@ class SuperAuth::ActiveRecord::User < ActiveRecord::Base
|
|
|
5
5
|
|
|
6
6
|
def model_name = ActiveModel::Name.new(:user)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
# A read: runtime roles only get SELECT on this table. `.system` creates
|
|
9
|
+
# the row when missing and belongs to migrations, seeds and consoles.
|
|
10
|
+
def system? = self.class.find_by(name: "system") == self
|
|
9
11
|
def self.system = find_or_create_by(name: "system")
|
|
10
12
|
|
|
11
13
|
has_many :edges, class_name: 'SuperAuth::ActiveRecord::Edge'
|
|
@@ -1,2 +1,26 @@
|
|
|
1
1
|
class SuperAuth::Authorization < Sequel::Model(:super_auth_authorizations)
|
|
2
|
+
# Clears and repopulates the compiled table from the current graph, inside
|
|
3
|
+
# one transaction, and returns the row count. Row by row, like
|
|
4
|
+
# SuperAuth::ActiveRecord::Authorization.compile!; a single INSERT ... SELECT
|
|
5
|
+
# is a separate change. Runtime enforcement (ByCurrentUser, the RLS policies)
|
|
6
|
+
# reads only this table, so every edit to the graph is inert until this runs.
|
|
7
|
+
# The wildcard guard runs first, before the delete, so a refused compile
|
|
8
|
+
# leaves the previous rows in place rather than an empty table; the
|
|
9
|
+
# deprecation notice comes after the commit, for a compile that happened.
|
|
10
|
+
#
|
|
11
|
+
# Postgres JIT-compiles the union's expressions on every run: 539 LLVM
|
|
12
|
+
# functions, 1.6-2.2s of optimisation and emission for a query that then
|
|
13
|
+
# executes in milliseconds. SET LOCAL scopes the switch to this transaction,
|
|
14
|
+
# so nothing leaks to the pooled connection.
|
|
15
|
+
def self.compile!
|
|
16
|
+
count = db.transaction do
|
|
17
|
+
db.run "SET LOCAL jit = off" if db.database_type == :postgres
|
|
18
|
+
SuperAuth::Resource.assert_compilable!
|
|
19
|
+
dataset.delete
|
|
20
|
+
SuperAuth::Edge.authorizations.each { |row| dataset.insert(row) }
|
|
21
|
+
dataset.count
|
|
22
|
+
end
|
|
23
|
+
SuperAuth::Resource.warn_deprecated_wildcards
|
|
24
|
+
count
|
|
25
|
+
end
|
|
2
26
|
end
|