petergate 3.1.1 → 4.0.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/CHANGELOG.md +142 -0
- data/Gemfile +12 -0
- data/README.md +151 -10
- data/lib/generators/petergate/install_generator.rb +58 -9
- data/lib/generators/petergate/templates/migrations/add_roles_to_users.rb +2 -2
- data/lib/petergate/action_controller/base.rb +502 -40
- data/lib/petergate/active_record/base.rb +34 -8
- data/lib/petergate/rule.rb +34 -0
- data/lib/petergate/version.rb +1 -1
- data/lib/petergate.rb +101 -0
- data/petergate.gemspec +19 -1
- metadata +18 -5
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Petergate
|
|
2
|
+
# One `access` declaration: the scope it authorizes against, the role => actions
|
|
3
|
+
# rules, and the denial message set alongside them.
|
|
4
|
+
#
|
|
5
|
+
# `scope` is the value as written -- a Class for `access Employee`, a Symbol
|
|
6
|
+
# for `access :member`. It is deliberately not resolved to a Devise scope
|
|
7
|
+
# here: under STI several classes resolve to the same Devise scope, and rules
|
|
8
|
+
# are keyed by what was declared so they stay distinct.
|
|
9
|
+
#
|
|
10
|
+
# `defaulted` marks a rule that named no scope, so it follows whatever
|
|
11
|
+
# `petergate_scope` the controller reading it has. That is what makes
|
|
12
|
+
# `petergate_scope` in a base controller govern rules declared above it, and
|
|
13
|
+
# it is why the scope is resolved when rules are read rather than written --
|
|
14
|
+
# rewriting the key at declaration time cannot see the subclasses to come, and
|
|
15
|
+
# cannot tell a defaulted :user from one someone asked for by name.
|
|
16
|
+
Rule = Struct.new(:scope, :rules, :message, :defaulted, keyword_init: true) do
|
|
17
|
+
# The scope this rule authorizes against, for the controller reading it.
|
|
18
|
+
def scope_for(controller_class)
|
|
19
|
+
defaulted ? controller_class.petergate_scope : scope
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The hash key a defaulted rule is filed under. A sentinel rather than the
|
|
24
|
+
# current default, so it stays distinct from `access :user, ...` and cannot
|
|
25
|
+
# collide with a rule declared for a named scope.
|
|
26
|
+
DEFAULT_SCOPE = :"petergate.default_scope"
|
|
27
|
+
|
|
28
|
+
# Raised when a declared scope has no authentication helper behind it, which
|
|
29
|
+
# nearly always means a missing `devise_for` or a typo.
|
|
30
|
+
#
|
|
31
|
+
# StandardError rather than NameError: applications rescue NameError around
|
|
32
|
+
# autoloading and would swallow this.
|
|
33
|
+
class MissingScopeError < StandardError; end
|
|
34
|
+
end
|
data/lib/petergate/version.rb
CHANGED
data/lib/petergate.rb
CHANGED
|
@@ -1,8 +1,109 @@
|
|
|
1
1
|
require "petergate/version"
|
|
2
2
|
require "petergate/railtie"
|
|
3
|
+
require "petergate/rule"
|
|
3
4
|
require 'petergate/action_controller/base'
|
|
4
5
|
require 'petergate/active_record/base'
|
|
5
6
|
|
|
6
7
|
module Petergate
|
|
8
|
+
# The Devise scope a declared petergate scope resolves to.
|
|
9
|
+
#
|
|
10
|
+
# A Symbol is taken at face value: it names the Devise mapping (or, without
|
|
11
|
+
# Devise, whatever `current_<name>` the application provides). A Class is
|
|
12
|
+
# looked up in Devise's mappings -- an exact match first, so a model with its
|
|
13
|
+
# own `devise_for` wins over an ancestor's, which is what `find_scope!` alone
|
|
14
|
+
# gets wrong for STI. A class with no mapping of its own resolves to its
|
|
15
|
+
# nearest mapped ancestor: under STI that is the parent's scope, which is
|
|
16
|
+
# correct, because there is only one login.
|
|
17
|
+
#
|
|
18
|
+
# Devise.mappings is read lazily, never at boot: on Rails 8 it calls
|
|
19
|
+
# reload_routes_unless_loaded.
|
|
20
|
+
def self.devise_scope_for(declared)
|
|
21
|
+
return declared unless declared.is_a?(Class)
|
|
22
|
+
return derived_scope_for(declared) unless defined?(::Devise) && ::Devise.respond_to?(:mappings)
|
|
7
23
|
|
|
24
|
+
mappings = ::Devise.mappings
|
|
25
|
+
exact = mappings.each_value.find { |mapping| mapping.to == declared }
|
|
26
|
+
return exact.name if exact
|
|
27
|
+
|
|
28
|
+
# The ancestor walk find_scope! does internally, minus the raise -- so no
|
|
29
|
+
# rescue is needed to turn "no mapping" into the fallback.
|
|
30
|
+
#
|
|
31
|
+
# A method-level `rescue StandardError` here used to cover ::Devise.mappings
|
|
32
|
+
# above as well, and reading that loads routes on Rails 8. A NameError in
|
|
33
|
+
# routes.rb came back as a MissingScopeError naming the scope, which sends
|
|
34
|
+
# whoever reads it after the wrong bug. A routes error now surfaces as
|
|
35
|
+
# itself.
|
|
36
|
+
#
|
|
37
|
+
# Falling through means Devise is installed but covers nothing this class
|
|
38
|
+
# descends from, so authentication for it is hand-rolled -- which the README
|
|
39
|
+
# supports. Fall back to the class's own name.
|
|
40
|
+
mappings.each_value.find { |mapping| declared <= mapping.to }&.name ||
|
|
41
|
+
derived_scope_for(declared)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Roles already warned about, and the lock guarding it. Created at load time:
|
|
45
|
+
# a lazy `||=` would itself race on the authorization path.
|
|
46
|
+
@warned_roles = {}
|
|
47
|
+
@warned_roles_lock = Mutex.new
|
|
48
|
+
|
|
49
|
+
# Reports a stored role that the record's own class does not define.
|
|
50
|
+
#
|
|
51
|
+
# Once per class and role: this is read on every authorization check, and a
|
|
52
|
+
# warning per request would be noise rather than a signal. Kernel#warn rather
|
|
53
|
+
# than ActiveSupport::Deprecation -- nothing is deprecated, and the data is
|
|
54
|
+
# wrong now.
|
|
55
|
+
def self.warn_about_unavailable_roles(klass, rejected)
|
|
56
|
+
rejected.each do |role|
|
|
57
|
+
key = [klass.name, role]
|
|
58
|
+
|
|
59
|
+
# Read before locking. #roles is read once per declared rule on every
|
|
60
|
+
# request, so for an application whose data has drifted -- exactly the
|
|
61
|
+
# audience this warning is for -- taking the mutex unconditionally would
|
|
62
|
+
# serialize every thread on it forever, long after the warning had been
|
|
63
|
+
# printed. The lock guards the write only, and losing the race costs one
|
|
64
|
+
# duplicate line.
|
|
65
|
+
next if @warned_roles.key?(key)
|
|
66
|
+
|
|
67
|
+
# Reached from the authorization path of every request, so without this
|
|
68
|
+
# concurrent threads could mutate the hash while another iterates it.
|
|
69
|
+
first_time = @warned_roles_lock.synchronize do
|
|
70
|
+
next false if @warned_roles.key?(key)
|
|
71
|
+
|
|
72
|
+
@warned_roles[key] = true
|
|
73
|
+
end
|
|
74
|
+
next unless first_time
|
|
75
|
+
|
|
76
|
+
warn "petergate: #{klass.name} is ignoring the stored role #{role.inspect}. " +
|
|
77
|
+
if role.is_a?(String) && klass::ROLES.include?(role.to_sym)
|
|
78
|
+
"#{klass.name} declares #{role.to_sym.inspect}, but the column holds it as a " \
|
|
79
|
+
"string. petergate stores roles as symbols, so this row was written by something " \
|
|
80
|
+
"other than `roles=` -- raw SQL, update_column, or an import."
|
|
81
|
+
else
|
|
82
|
+
"#{klass.name} does not declare it; #{klass.name}::ROLES is #{klass::ROLES.inspect}. " \
|
|
83
|
+
"A record whose type changed, or a role removed from a petergate declaration, " \
|
|
84
|
+
"leaves roles behind in the column."
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The scope name a class implies when Devise cannot answer.
|
|
90
|
+
#
|
|
91
|
+
# Derived from the STI *base* class, so subclasses sharing one table share one
|
|
92
|
+
# scope -- `Employee < User` asks `current_user`, which is the whole point of
|
|
93
|
+
# STI -- while an independent model gets a scope of its own.
|
|
94
|
+
def self.derived_scope_for(klass)
|
|
95
|
+
base = klass.respond_to?(:base_class) ? klass.base_class : klass
|
|
96
|
+
base.model_name.singular_route_key.to_sym
|
|
97
|
+
rescue StandardError
|
|
98
|
+
:user
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# The scope name this class would have entirely on its own, ignoring any
|
|
102
|
+
# hierarchy. Used only to tell "resolved to something else's login" from
|
|
103
|
+
# "resolved to its own", which the missing-scope message reports differently.
|
|
104
|
+
def self.own_scope_name_for(klass)
|
|
105
|
+
klass.model_name.singular_route_key.to_sym
|
|
106
|
+
rescue StandardError
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
8
109
|
end
|
data/petergate.gemspec
CHANGED
|
@@ -24,7 +24,25 @@ Gem::Specification.new do |spec|
|
|
|
24
24
|
spec.files = `git ls-files -z`.split("\x0").reject { |p| p.start_with?("test/", "gemfiles/", ".github/", "assets/") }
|
|
25
25
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
26
26
|
spec.require_paths = ["lib"]
|
|
27
|
-
|
|
27
|
+
|
|
28
|
+
# The floor Rails 8.0 and 8.1 already set, so this takes nothing away that
|
|
29
|
+
# the dependencies below did not. Ruby 4.0 is covered by CI as well; nothing
|
|
30
|
+
# here is pinned to a major.
|
|
31
|
+
spec.required_ruby_version = ">= 3.2.0"
|
|
32
|
+
spec.post_install_message = <<~NOTICE
|
|
33
|
+
NOTICE: 4.0.0 has three breaking changes. All of them can only take access
|
|
34
|
+
away, never grant it.
|
|
35
|
+
|
|
36
|
+
* `roles` returns only roles the record's own class defines. A role left in
|
|
37
|
+
the column by a `type` change, or by being dropped from a petergate
|
|
38
|
+
declaration, no longer grants. Roles set through `roles=` are unaffected.
|
|
39
|
+
* A subclass declaring `access` now runs the check at its own position in
|
|
40
|
+
the callback chain, so filters declared above it run before the denial.
|
|
41
|
+
* `:all` and `except:` cover fewer methods: `all_actions` no longer counts
|
|
42
|
+
methods that were never actions.
|
|
43
|
+
|
|
44
|
+
See CHANGELOG.md.
|
|
45
|
+
NOTICE
|
|
28
46
|
|
|
29
47
|
spec.add_development_dependency "bundler", "> 1.7"
|
|
30
48
|
spec.add_development_dependency "rake", ">= 12.3"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: petergate
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 4.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Isaac Sloan
|
|
@@ -104,6 +104,7 @@ extra_rdoc_files: []
|
|
|
104
104
|
files:
|
|
105
105
|
- ".gitignore"
|
|
106
106
|
- ".ruby-version"
|
|
107
|
+
- CHANGELOG.md
|
|
107
108
|
- Gemfile
|
|
108
109
|
- LICENSE.txt
|
|
109
110
|
- README.md
|
|
@@ -114,6 +115,7 @@ files:
|
|
|
114
115
|
- lib/petergate/action_controller/base.rb
|
|
115
116
|
- lib/petergate/active_record/base.rb
|
|
116
117
|
- lib/petergate/railtie.rb
|
|
118
|
+
- lib/petergate/rule.rb
|
|
117
119
|
- lib/petergate/version.rb
|
|
118
120
|
- lib/templates/rails/scaffold_controller/controller.rb
|
|
119
121
|
- petergate.gemspec
|
|
@@ -123,8 +125,19 @@ licenses:
|
|
|
123
125
|
metadata:
|
|
124
126
|
source_code_uri: https://github.com/elorest/petergate
|
|
125
127
|
bug_tracker_uri: https://github.com/elorest/petergate/issues
|
|
126
|
-
post_install_message:
|
|
127
|
-
|
|
128
|
+
post_install_message: |
|
|
129
|
+
NOTICE: 4.0.0 has three breaking changes. All of them can only take access
|
|
130
|
+
away, never grant it.
|
|
131
|
+
|
|
132
|
+
* `roles` returns only roles the record's own class defines. A role left in
|
|
133
|
+
the column by a `type` change, or by being dropped from a petergate
|
|
134
|
+
declaration, no longer grants. Roles set through `roles=` are unaffected.
|
|
135
|
+
* A subclass declaring `access` now runs the check at its own position in
|
|
136
|
+
the callback chain, so filters declared above it run before the denial.
|
|
137
|
+
* `:all` and `except:` cover fewer methods: `all_actions` no longer counts
|
|
138
|
+
methods that were never actions.
|
|
139
|
+
|
|
140
|
+
See CHANGELOG.md.
|
|
128
141
|
rdoc_options: []
|
|
129
142
|
require_paths:
|
|
130
143
|
- lib
|
|
@@ -132,14 +145,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
132
145
|
requirements:
|
|
133
146
|
- - ">="
|
|
134
147
|
- !ruby/object:Gem::Version
|
|
135
|
-
version:
|
|
148
|
+
version: 3.2.0
|
|
136
149
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
137
150
|
requirements:
|
|
138
151
|
- - ">="
|
|
139
152
|
- !ruby/object:Gem::Version
|
|
140
153
|
version: '0'
|
|
141
154
|
requirements: []
|
|
142
|
-
rubygems_version:
|
|
155
|
+
rubygems_version: 4.0.16
|
|
143
156
|
specification_version: 4
|
|
144
157
|
summary: Authorization system allowing verbose easy read controller syntax.
|
|
145
158
|
test_files: []
|