super_auth 0.7.0 → 0.9.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +100 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +440 -42
  5. data/USAGE.md +127 -21
  6. data/db/migrate/11_add_parent_id_to_resources.rb +32 -0
  7. data/db/migrate/12_add_resource_indexes.rb +116 -0
  8. data/db/migrate/13_add_resource_tree_guard.rb +28 -0
  9. data/db/migrate_activerecord/20250101000001_create_super_auth_users.rb +7 -1
  10. data/db/migrate_activerecord/20250101000002_create_super_auth_groups.rb +7 -1
  11. data/db/migrate_activerecord/20250101000003_create_super_auth_permissions.rb +7 -1
  12. data/db/migrate_activerecord/20250101000004_create_super_auth_roles.rb +7 -1
  13. data/db/migrate_activerecord/20250101000005_create_super_auth_resources.rb +7 -1
  14. data/db/migrate_activerecord/20250101000006_create_super_auth_edges.rb +7 -1
  15. data/db/migrate_activerecord/20250101000007_create_super_auth_authorizations.rb +7 -1
  16. data/db/migrate_activerecord/20250101000011_add_parent_id_to_super_auth_resources.rb +9 -0
  17. data/db/migrate_activerecord/20250101000012_add_super_auth_resource_indexes.rb +89 -0
  18. data/db/migrate_activerecord/20250101000013_add_resource_tree_guard_to_super_auth_resources.rb +15 -0
  19. data/db/seeds/sample_data.rb +1 -0
  20. data/lib/generators/super_auth/install/templates/README +6 -2
  21. data/lib/generators/super_auth/install/templates/super_auth.rb +6 -3
  22. data/lib/generators/super_auth/rls/templates/migration.rb.erb +2 -0
  23. data/lib/super_auth/active_record/authorization.rb +14 -3
  24. data/lib/super_auth/active_record/by_current_user.rb +136 -29
  25. data/lib/super_auth/active_record/group.rb +3 -0
  26. data/lib/super_auth/active_record/nested.rb +43 -0
  27. data/lib/super_auth/active_record/resource.rb +28 -4
  28. data/lib/super_auth/active_record/role.rb +3 -0
  29. data/lib/super_auth/active_record.rb +30 -2
  30. data/lib/super_auth/authorization.rb +63 -10
  31. data/lib/super_auth/edge.rb +73 -18
  32. data/lib/super_auth/editor/index.html +16 -10
  33. data/lib/super_auth/editor/seed.rb +11 -5
  34. data/lib/super_auth/editor.rb +35 -11
  35. data/lib/super_auth/nestable.rb +105 -4
  36. data/lib/super_auth/reach.rb +88 -0
  37. data/lib/super_auth/resource.rb +71 -0
  38. data/lib/super_auth/rls.rb +576 -44
  39. data/lib/super_auth/tree_guard.rb +115 -0
  40. data/lib/super_auth/version.rb +1 -1
  41. data/lib/super_auth.rb +55 -2
  42. metadata +10 -1
@@ -1,2 +1,73 @@
1
1
  class SuperAuth::Resource < Sequel::Model(:super_auth_resources)
2
+ # Resources nest like groups and roles: a grant on a node reaches the node
3
+ # and every node under it (SuperAuth::Edge.join_resource_subtree). No
4
+ # unrestrict_primary_key, unlike those two: it exists for the ActiveRecord
5
+ # twins' descendants_dataset, which builds a Sequel model with an id, and
6
+ # the ActiveRecord Resource deliberately has none — nothing at runtime
7
+ # walks a resource tree.
8
+ include SuperAuth::Nestable
9
+
10
+ class << self
11
+ # A node with an external_type and no external_id is a type-level
12
+ # (wildcard) node: at runtime it means every record of that type, present
13
+ # and future (the ByCurrentUser type_level branch, the policy's
14
+ # `resource_external_id IS NULL OR` clause). A node with neither is a
15
+ # container. The type-level grant is a supported, permanent primitive —
16
+ # "this principal may act on every record of a type" has no cheaper
17
+ # spelling — and it is flat: what it may not do is join the tree.
18
+ def wildcards
19
+ exclude(external_type: nil).where(external_id: nil)
20
+ end
21
+
22
+ # The node registered for one record: the pair every host helper looks
23
+ # up before it grants, revokes or labels. It refuses a nil id rather than
24
+ # answering, because where(external_type: type, external_id: nil) is not
25
+ # "no node" — it IS the type-level node for that type, and a helper called
26
+ # with an unset foreign key would otherwise act on the grant that covers
27
+ # every record of the type.
28
+ def record(type, id)
29
+ if id.nil?
30
+ raise SuperAuth::Error, "SuperAuth::Resource.record(#{type.to_s.inspect}, nil): the id is nil. " \
31
+ "A node with an external_type and no external_id is the type-level node for every #{type} record, " \
32
+ "not the node for one of them; pass the record's id, or use wildcards for the type-level node."
33
+ end
34
+
35
+ first(external_type: type.to_s, external_id: id)
36
+ end
37
+
38
+ # The recursive step of descendant_pairs stops at a type-level node. A
39
+ # per-record node nested under one would otherwise receive the
40
+ # type-level node's grants: one accidental parent_id, and a grant on
41
+ # "every Claim" also compiled a row for every claim node beneath it, on
42
+ # every path that reads the walk — including a host that reads
43
+ # Edge.authorizations directly and never calls compile!, where
44
+ # assert_compilable! does not run. The walk still anchors on the node a
45
+ # grant names, so a granted type-level node yields its own (type, NULL)
46
+ # row and nothing else; join_resource_subtree drops the other direction,
47
+ # a type-level node reached as a descendant.
48
+ def descend_from(parent)
49
+ Sequel.|({ Sequel[parent][:external_type] => nil }, Sequel.~(Sequel[parent][:external_id] => nil))
50
+ end
51
+
52
+ # "Type-level nodes are flat." compile! calls this before touching the
53
+ # compiled table, so a refused compile leaves the previous rows in place.
54
+ # The walk and the join above make the shape harmless; this makes it
55
+ # loud, because nobody means it. A type-level node with a parent looks
56
+ # like a container grant that reaches every record of a type, and one
57
+ # with children looks like a container whose children can only be
58
+ # reached through a grant that already covers them. One query: the
59
+ # type-level nodes that have a parent, or that some node names as its
60
+ # parent.
61
+ def assert_compilable!
62
+ parents = dataset.exclude(parent_id: nil).select(:parent_id)
63
+ nested = wildcards.where(Sequel.|(Sequel.~(parent_id: nil), { id: parents })).select_order_map(:id)
64
+ return if nested.empty?
65
+
66
+ raise SuperAuth::Error, "Wildcard resource nodes must be flat, but wildcard node(s) #{nested.join(', ')} " \
67
+ "have a parent or children. A resource node with an external_type and no external_id is a wildcard " \
68
+ "for every record of that type, not a container: nested in the tree it would compile to a row that " \
69
+ "reaches every record of its type through the tree. Move each to the root with no children, or give " \
70
+ "it an external_id."
71
+ end
72
+ end
2
73
  end