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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1e7b356d1fe3ebefd2d991eb56b317cb6eca56f6840107598fd3a5fa6eac33d4
|
|
4
|
+
data.tar.gz: 2d63adb685f6dd63476e21a7f51fa52f10687e87be3155b010c629503bb48619
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b23d89a01df2e68c8257b2e3380703617afb3d13fff7703e3f32de2ac2d34c0c148103af0c12185cc5041d10f90717aa5fa9193ad58352af76e5b9ad663c8dc5
|
|
7
|
+
data.tar.gz: 5e16dde928a7581e4b5123c23639fde74a773473ff976559317732552adad7c99bcbe22e6ebc0e397e91673b2a41c62da7e6f8958e5752a5bf569e1668c30f90
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
4.0.6
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 4.0.0
|
|
4
|
+
|
|
5
|
+
### Breaking
|
|
6
|
+
|
|
7
|
+
Three things behave differently for code that already exists, which is what
|
|
8
|
+
makes this 4.0.0 rather than a point release.
|
|
9
|
+
|
|
10
|
+
All three can only take access away, never grant it, and each fails visibly
|
|
11
|
+
when it bites -- a denial, not a silently widened door. An upgrade cannot
|
|
12
|
+
quietly let someone in who was previously refused, which is the direction an
|
|
13
|
+
authorization library should fail in. That is worth knowing before upgrading,
|
|
14
|
+
but it describes the shape of the risk rather than making the release a
|
|
15
|
+
smaller one than it is.
|
|
16
|
+
|
|
17
|
+
- **An application whose stored roles have drifted from its declared ones will
|
|
18
|
+
see those roles stop granting access.** That means a role left behind by an
|
|
19
|
+
STI `type` change, or one removed from a `petergate` declaration while rows
|
|
20
|
+
still carry it. See `roles` under Changed.
|
|
21
|
+
|
|
22
|
+
Roles written through `roles=` or `role=` are unaffected. Reaching this needs
|
|
23
|
+
a value the setter never wrote -- from `update_column`, raw SQL, a fixture or
|
|
24
|
+
an import.
|
|
25
|
+
|
|
26
|
+
- **A subclass that declares `access` now runs petergate's check at its own
|
|
27
|
+
position in the callback chain**, not the parent's. Work in a `before_action`
|
|
28
|
+
declared above it now runs before the denial rather than after. See
|
|
29
|
+
`petergate_check_access!` under Changed.
|
|
30
|
+
|
|
31
|
+
- **`:all` and `except:` cover fewer methods**, because `all_actions` no longer
|
|
32
|
+
counts things that were never actions. An application whose rules were
|
|
33
|
+
granting one of those was granting a method nobody can route to. See
|
|
34
|
+
`all_actions` under Changed.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- `access` can authorize against a model other than `User`. A controller names
|
|
39
|
+
the model with `petergate_scope`, inherited by its subclasses, or a single
|
|
40
|
+
rule set names one as `access`'s first argument:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
class Staff::BaseController < ApplicationController
|
|
44
|
+
petergate_scope Employee
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class InvoicesController < ApplicationController
|
|
48
|
+
access Vendor, supplier: :all
|
|
49
|
+
access Employee, admin: [:index]
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
One declaration covers both ways an application has several kinds of user:
|
|
54
|
+
a subclass sharing one login through single table inheritance, and a
|
|
55
|
+
separately authenticated model with a login of its own. Which one is in play
|
|
56
|
+
is resolved from Devise's mappings at request time rather than configured.
|
|
57
|
+
|
|
58
|
+
Several `access` calls in one class body are OR'd -- whichever scope is
|
|
59
|
+
satisfied grants the action. A subclass declaring `access` replaces everything
|
|
60
|
+
it inherited, exactly as before, so narrowing a parent's rules in a subclass
|
|
61
|
+
still narrows them rather than adding an alternative way in.
|
|
62
|
+
|
|
63
|
+
Type matching is exact: `access Employee` does not admit a `Manager < Employee`.
|
|
64
|
+
|
|
65
|
+
- The denial message can be given as a string before the rules --
|
|
66
|
+
`access "Staff only", admin: :all` -- so it too is out of the rules hash.
|
|
67
|
+
|
|
68
|
+
- `logged_in?` and `user_logged_in?` take a `scope:` keyword. Both default to
|
|
69
|
+
the controller's own scope.
|
|
70
|
+
|
|
71
|
+
- The install generator takes a model name: `rails g petergate:install Employee`,
|
|
72
|
+
with an optional `--table-name`. With no argument its output is unchanged.
|
|
73
|
+
|
|
74
|
+
- `Petergate::MissingScopeError`, raised when a declared scope has no
|
|
75
|
+
authentication helper behind it, rather than failing quietly.
|
|
76
|
+
|
|
77
|
+
### Changed
|
|
78
|
+
|
|
79
|
+
- **`roles` now returns only roles the record's own class defines.** `roles=`
|
|
80
|
+
has always filtered against `available_roles`, but the reader did not, so a
|
|
81
|
+
role left in the column by an STI `type` change -- or by a role being dropped
|
|
82
|
+
from a `petergate` declaration -- kept authorizing. This closes that without a
|
|
83
|
+
migration. Each ignored role is warned about once, naming the class and the
|
|
84
|
+
role.
|
|
85
|
+
|
|
86
|
+
Roles set through `roles=` or `role=` are unaffected, in either storage mode.
|
|
87
|
+
A multi-role column holding *strings* rather than symbols is rejected rather
|
|
88
|
+
than normalized, so this cannot start granting a role that previously matched
|
|
89
|
+
nothing -- see Breaking.
|
|
90
|
+
|
|
91
|
+
- `all_actions` returns only actions. Rails' `action_methods` includes public
|
|
92
|
+
methods inherited from a concrete superclass, so overriding `current_user` in
|
|
93
|
+
`ApplicationController` -- or using `devise_group` -- put those names into
|
|
94
|
+
`:all` and `except:` rules as though they were actions. It is now memoized
|
|
95
|
+
against Rails' own `action_methods`, making it faster than before the fix.
|
|
96
|
+
|
|
97
|
+
- **petergate's check runs at the position of the `access` call that declared
|
|
98
|
+
the rules**, for every class that declares them, and there is now exactly one
|
|
99
|
+
callback however many times `access` is called.
|
|
100
|
+
|
|
101
|
+
On 3.1.1 a parent and a subclass each declaring `access` registered two, and
|
|
102
|
+
the parent's ran first -- so a request was refused before any `before_action`
|
|
103
|
+
the subclass declared had run. That made `access` unusable in a subclass that
|
|
104
|
+
has to set its own authentication up first:
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
class Api::BaseController < ApplicationController
|
|
108
|
+
before_action :authenticate_from_token
|
|
109
|
+
access admin: :all # now runs after the token lookup
|
|
110
|
+
end
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
An application relying on the old ordering -- an early filter written knowing
|
|
114
|
+
petergate had already refused anonymous requests -- should move that filter
|
|
115
|
+
below the `access` call. The callback is a named method rather than a block,
|
|
116
|
+
so `skip_before_action :petergate_check_access!` can also reach it, which it
|
|
117
|
+
could not before.
|
|
118
|
+
|
|
119
|
+
### Deprecated
|
|
120
|
+
|
|
121
|
+
- `controller_rules` and `controller_message` as a way to reach the rules.
|
|
122
|
+
petergate's own callback no longer calls either, and neither can describe a
|
|
123
|
+
controller with more than one scope: `controller_rules` returns a single
|
|
124
|
+
rule's hash, so an application still calling
|
|
125
|
+
`permissions(self.class.controller_rules)` from its own filter checks one
|
|
126
|
+
scope of several without saying so. Declare the rules with `access` and let
|
|
127
|
+
petergate evaluate them.
|
|
128
|
+
|
|
129
|
+
- `message:` inside the `access` rules hash. Pass the message as a string before
|
|
130
|
+
the rules instead. The key still works, though a positional string wins when
|
|
131
|
+
both are given, but it warns, naming the file and line to change.
|
|
132
|
+
|
|
133
|
+
### Fixed
|
|
134
|
+
|
|
135
|
+
- `user_logged_in?` is registered as a view helper. It has always been
|
|
136
|
+
documented as one, but calling it from a view raised `NoMethodError`.
|
|
137
|
+
|
|
138
|
+
- The install generator no longer sleeps for a second per run.
|
|
139
|
+
|
|
140
|
+
## 3.1.1 and earlier
|
|
141
|
+
|
|
142
|
+
Not recorded here; see the commit history.
|
data/Gemfile
CHANGED
|
@@ -10,6 +10,18 @@ gem "rails", "~> 8.1"
|
|
|
10
10
|
gem "sqlite3", "~> 2.0"
|
|
11
11
|
gem "rake", ">= 13.0"
|
|
12
12
|
|
|
13
|
+
# json 3.0 (released 2026-09-07) made JSON.parse's options keyword-only, while
|
|
14
|
+
# ActiveSupport 8.1's ActiveSupport::JSON.decode still passes them positionally
|
|
15
|
+
# -- which breaks encrypted cookies with metadata, and so any test that reads a
|
|
16
|
+
# flash message. Only Rails 8.1 reaches that path.
|
|
17
|
+
#
|
|
18
|
+
# Remove once activesupport ships the fix. What to check is one line --
|
|
19
|
+
# activesupport/lib/active_support/json/decoding.rb: 8.1.3.1 has
|
|
20
|
+
# `::JSON.parse(json, options)`, and the fix reads `::JSON.parse(json, **options)`,
|
|
21
|
+
# which is already what rails main carries:
|
|
22
|
+
# https://github.com/rails/rails/blob/main/activesupport/lib/active_support/json/decoding.rb
|
|
23
|
+
gem "json", "< 3"
|
|
24
|
+
|
|
13
25
|
# Devise is not a dependency of the gem -- petergate only needs the three
|
|
14
26
|
# authentication methods the README documents. It is here so one test can prove
|
|
15
27
|
# a real Devise app satisfies that contract.
|
data/README.md
CHANGED
|
@@ -11,9 +11,13 @@
|
|
|
11
11
|
|
|
12
12
|
Requirements
|
|
13
13
|
------
|
|
14
|
-
Rails 7.1 through 8.1 on Ruby 3.2 through
|
|
14
|
+
Rails 7.1 through 8.1 on Ruby 3.2 through 4.0 are covered by CI. Older Rails
|
|
15
15
|
versions are permitted by the gemspec but are not verified.
|
|
16
16
|
|
|
17
|
+
The gemspec requires Ruby 3.2, which is the floor Rails 8.0 sets. `.ruby-version`
|
|
18
|
+
names the newest covered Ruby rather than the oldest, so development happens on
|
|
19
|
+
the version most likely to surface a deprecation first.
|
|
20
|
+
|
|
17
21
|
Installation
|
|
18
22
|
------
|
|
19
23
|
##### Get the gem
|
|
@@ -31,9 +35,6 @@ Or install it yourself as:
|
|
|
31
35
|
|
|
32
36
|
##### Prerequisites: Setup Authentication (Devise)
|
|
33
37
|
|
|
34
|
-
The generator writes into `app/models/user.rb`, so a model named `User` is the
|
|
35
|
-
supported setup.
|
|
36
|
-
|
|
37
38
|
If you're using [devise](https://github.com/heartcombo/devise) you're in luck,
|
|
38
39
|
otherwise you'll have to add the following methods to your project:
|
|
39
40
|
|
|
@@ -51,6 +52,12 @@ there.
|
|
|
51
52
|
|
|
52
53
|
This will add a migration and insert petergate into your User model.
|
|
53
54
|
|
|
55
|
+
The model defaults to `User`; pass another to configure it instead. The model
|
|
56
|
+
has to exist already.
|
|
57
|
+
|
|
58
|
+
rails g petergate:install Employee # app/models/employee.rb
|
|
59
|
+
rails g petergate:install Employee --table-name=staff
|
|
60
|
+
|
|
54
61
|
Usage
|
|
55
62
|
------
|
|
56
63
|
#### User Model
|
|
@@ -114,8 +121,38 @@ visitors who aren't signed in. The value is one of:
|
|
|
114
121
|
| `:all` | every action on the controller |
|
|
115
122
|
| `{except: [:destroy]}` | every action except those |
|
|
116
123
|
|
|
117
|
-
`:root_admin`
|
|
118
|
-
|
|
124
|
+
##### The `:root_admin` role
|
|
125
|
+
|
|
126
|
+
`:root_admin` is the one role name petergate treats specially. It is checked
|
|
127
|
+
before any rule, so a user holding it reaches every action on every controller
|
|
128
|
+
that uses `access`, and no rule ever names it:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
class ArticlesController < ApplicationController
|
|
132
|
+
access all: [:index, :show], editor: :all
|
|
133
|
+
end
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
An `:editor` gets what the rule says. A `:root_admin` gets all of it too,
|
|
137
|
+
without appearing in the rule at all.
|
|
138
|
+
|
|
139
|
+
It is not automatic, though. Like any other role it has to be declared before
|
|
140
|
+
anyone can hold it, because `roles=` drops anything the model does not define:
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
petergate(roles: [:root_admin, :editor], multiple: true)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
An application that never declares it has no such bypass, which is a reasonable
|
|
147
|
+
choice -- it exists for the account that must never be locked out of its own
|
|
148
|
+
admin area, not as a convenience for ordinary administrators. Prefer a normal
|
|
149
|
+
role you grant explicitly; reach for `:root_admin` when you specifically want an
|
|
150
|
+
account no `access` rule can shut out.
|
|
151
|
+
|
|
152
|
+
Two limits. It only applies where `access` is used: a controller with no rules
|
|
153
|
+
has nothing to bypass. And with several authentication scopes on one controller
|
|
154
|
+
it counts only within the scopes that controller declares, so a `:root_admin` in
|
|
155
|
+
one scope cannot walk into a controller whose rules are all about another.
|
|
119
156
|
|
|
120
157
|
Rules declared on a parent controller are inherited by its subclasses, so a
|
|
121
158
|
single `access` line on `ApplicationController` can cover a whole app.
|
|
@@ -124,6 +161,74 @@ single `access` line on `ApplicationController` can cover a whole app.
|
|
|
124
161
|
refused request answers with a bare `403`, and an unauthenticated one with
|
|
125
162
|
`401`, instead of redirecting.
|
|
126
163
|
|
|
164
|
+
#### Multiple authentication models
|
|
165
|
+
|
|
166
|
+
By default every rule is about `current_user`. An application with more than one
|
|
167
|
+
kind of signed-in person can say which one a rule means: `petergate_scope` sets
|
|
168
|
+
it for a whole controller tree, and `access` takes it as a first argument for a
|
|
169
|
+
single rule set.
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
class Staff::BaseController < ApplicationController
|
|
173
|
+
petergate_scope Employee # every controller below this authorizes employees
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
class Staff::PayrollController < Staff::BaseController
|
|
177
|
+
access admin: :all, support: [:index, :show]
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
class InvoicesController < ApplicationController
|
|
181
|
+
access Vendor, supplier: :all
|
|
182
|
+
access Employee, admin: [:index]
|
|
183
|
+
end
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The same declaration covers both ways of having several kinds of user, because
|
|
187
|
+
petergate reads which one you have off Devise's mappings:
|
|
188
|
+
|
|
189
|
+
| Declared | Resolves to | Reads |
|
|
190
|
+
| --- | --- | --- |
|
|
191
|
+
| an STI subclass, e.g. `Employee < User` | the parent's scope | `current_user`, required to be exactly an `Employee` |
|
|
192
|
+
| a separately mapped model, e.g. `Vendor` | its own scope | `current_vendor` |
|
|
193
|
+
|
|
194
|
+
Matching is exact: `access Employee` does not admit a `Manager < Employee`, so
|
|
195
|
+
each kind names itself. A Symbol scope -- `access :member, ...` -- names a Devise
|
|
196
|
+
mapping directly and does no type check, which is what you want for
|
|
197
|
+
`devise_for :users, singular: :member`, where no class carries the name.
|
|
198
|
+
|
|
199
|
+
Several `access` calls in one class body are **OR**'d: whichever scope is
|
|
200
|
+
satisfied grants the action. A subclass declaring `access` replaces everything it
|
|
201
|
+
inherited, so narrowing a parent's rules in a subclass still narrows them rather
|
|
202
|
+
than adding another way in.
|
|
203
|
+
|
|
204
|
+
The denial message is positional too -- `access Employee, "Staff only",
|
|
205
|
+
support: :all` -- so the rules hash holds nothing but roles, and no name is
|
|
206
|
+
reserved.
|
|
207
|
+
|
|
208
|
+
##### Which login a refused person sees
|
|
209
|
+
|
|
210
|
+
petergate works this out rather than asking you to configure it:
|
|
211
|
+
|
|
212
|
+
| | |
|
|
213
|
+
| --- | --- |
|
|
214
|
+
| the scope holds nobody | `unauthorized!` -- that scope's `authenticate_*!` |
|
|
215
|
+
| the scope holds someone of the wrong kind | `forbidden!` |
|
|
216
|
+
|
|
217
|
+
So a customer who reaches an employee-only page under STI is refused outright:
|
|
218
|
+
there is one login and they are already through it. A customer who reaches a
|
|
219
|
+
`Vendor` page is sent to the vendor login instead, because that scope really is
|
|
220
|
+
empty -- and with Devise they can sign in there without losing the session they
|
|
221
|
+
already have, since Warden keys sessions per scope.
|
|
222
|
+
|
|
223
|
+
With several scopes declared and nobody signed in to any of them, the login
|
|
224
|
+
comes from `petergate_scope`, or from the first scope declared if the controller
|
|
225
|
+
has no `petergate_scope` of its own.
|
|
226
|
+
|
|
227
|
+
If a scope has no `current_*` behind it at all, petergate raises
|
|
228
|
+
`Petergate::MissingScopeError` rather than failing quietly. The message says
|
|
229
|
+
whether the class needs a `devise_for` of its own, shares a login with a parent
|
|
230
|
+
class, or is not an authenticatable model at all.
|
|
231
|
+
|
|
127
232
|
Inside your views you can use logged_in?(:admin, :customer, :etc) to show or hide content.
|
|
128
233
|
|
|
129
234
|
```erb
|
|
@@ -133,6 +238,15 @@ Inside your views you can use logged_in?(:admin, :customer, :etc) to show or hid
|
|
|
133
238
|
`logged_in?` tests roles. To ask only whether anyone is signed in, without
|
|
134
239
|
caring which role they hold, use `user_logged_in?`.
|
|
135
240
|
|
|
241
|
+
Both resolve against the controller's own scope, and both take a `scope:` to ask
|
|
242
|
+
about another one. Both are type-exact, so under `petergate_scope Employee` a
|
|
243
|
+
signed-in `Manager < Employee` answers `false` -- it asks "is an Employee signed
|
|
244
|
+
in", not "is anybody":
|
|
245
|
+
|
|
246
|
+
```erb
|
|
247
|
+
<%= link_to "Payroll", payroll_path if logged_in?(:admin, scope: Employee) %>
|
|
248
|
+
```
|
|
249
|
+
|
|
136
250
|
If you need to access available roles within your project you can by calling:
|
|
137
251
|
|
|
138
252
|
```ruby
|
|
@@ -141,7 +255,27 @@ User.first.available_roles # the same list, from an instance
|
|
|
141
255
|
```
|
|
142
256
|
|
|
143
257
|
`ROLES` is a constant on the model, so it is also reachable from your own
|
|
144
|
-
instance methods. A subclass shares its parent's roles
|
|
258
|
+
instance methods. A subclass shares its parent's roles unless it calls
|
|
259
|
+
`petergate` itself, which gives it a vocabulary of its own -- useful with single
|
|
260
|
+
table inheritance, where each kind of user needs different roles:
|
|
261
|
+
|
|
262
|
+
```ruby
|
|
263
|
+
class User < ApplicationRecord
|
|
264
|
+
petergate(roles: [:customer], multiple: true)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
class Employee < User
|
|
268
|
+
petergate(roles: [:admin, :support], multiple: true)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
Employee::ROLES # => [:admin, :support, :user]
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
`roles` only ever returns roles the record's own class defines. A role in the
|
|
275
|
+
column that the class does not define is ignored, and warned about once. This
|
|
276
|
+
matters when a record's `type` changes, or when a role is dropped from a
|
|
277
|
+
`petergate` declaration: the column outlives whatever wrote it, and a leftover
|
|
278
|
+
role must not keep granting access.
|
|
145
279
|
|
|
146
280
|
#### Denying access yourself
|
|
147
281
|
|
|
@@ -169,21 +303,28 @@ which has no format negotiation to offer.
|
|
|
169
303
|
##### The denial message
|
|
170
304
|
|
|
171
305
|
`forbidden!` takes one for a single call, and `access` sets a default for the
|
|
172
|
-
whole controller:
|
|
306
|
+
whole controller -- as a string before the rules:
|
|
173
307
|
|
|
174
308
|
```ruby
|
|
175
309
|
forbidden! "Your account is suspended"
|
|
176
310
|
|
|
177
|
-
access user: [:show, :index]
|
|
311
|
+
access "You shall not pass", user: [:show, :index]
|
|
312
|
+
access Employee, "Staff only", support: :all
|
|
178
313
|
```
|
|
179
314
|
|
|
315
|
+
Like the scope, it sits outside the rules hash so it cannot be confused with a
|
|
316
|
+
role.
|
|
317
|
+
|
|
318
|
+
The `message:` key is deprecated. It still works and a positional string wins if
|
|
319
|
+
both are given, but it warns, naming the file and line to change.
|
|
320
|
+
|
|
180
321
|
The message is resolved in this order, first match winning:
|
|
181
322
|
|
|
182
323
|
| | Source |
|
|
183
324
|
| --- | --- |
|
|
184
325
|
| 1 | the argument passed to `forbidden!` |
|
|
185
326
|
| 2 | an `msg` request header |
|
|
186
|
-
| 3 | the
|
|
327
|
+
| 3 | the message given to `access` (or the deprecated `message:` key) |
|
|
187
328
|
| 4 | `"Permission Denied"` |
|
|
188
329
|
|
|
189
330
|
Note the second entry: the `msg` header is read off the request, so a caller
|
|
@@ -7,19 +7,34 @@ module Petergate
|
|
|
7
7
|
source_root File.expand_path("../templates", __FILE__)
|
|
8
8
|
|
|
9
9
|
desc "Sets up rails project for Petergate Authorizations"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
|
|
11
|
+
argument :model_name, type: :string, default: "User", banner: "ModelName"
|
|
12
|
+
class_option :table_name, type: :string,
|
|
13
|
+
desc: "Table to add the roles column to (defaults to the model's table)"
|
|
14
|
+
|
|
15
|
+
# Rails' own idiom: a bare timestamp collides when the generator runs
|
|
16
|
+
# twice inside one second, which the old code papered over with a
|
|
17
|
+
# `sleep 1` on every run.
|
|
18
|
+
def self.next_migration_number(dirname)
|
|
19
|
+
# ::ActiveRecord -- inside module Petergate the bare name finds
|
|
20
|
+
# Petergate::ActiveRecord, the mixin namespace.
|
|
21
|
+
::ActiveRecord::Migration.next_migration_number(current_migration_number(dirname) + 1)
|
|
13
22
|
end
|
|
14
23
|
|
|
15
24
|
def insert_into_user_model
|
|
16
|
-
|
|
25
|
+
unless File.exist?(File.join(destination_root, model_path))
|
|
26
|
+
raise Thor::Error, "#{model_path} does not exist. Generate the model first, " \
|
|
27
|
+
"or pass the name of one that does."
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
inject_into_file model_path, after: model_declaration_pattern do
|
|
17
31
|
<<-'RUBY'
|
|
18
32
|
|
|
19
33
|
############################################################################################
|
|
20
34
|
## PeterGate Roles ##
|
|
21
35
|
## The :user role is added by default and shouldn't be included in this list. ##
|
|
22
|
-
##
|
|
36
|
+
## Add :root_admin to this list for a role that reaches every action whatever ##
|
|
37
|
+
## the access rules say. Nobody can hold it until it is declared here. ##
|
|
23
38
|
## The multiple option can be set to true if you need users to have multiple roles. ##
|
|
24
39
|
petergate(roles: [:admin, :editor], multiple: false) ##
|
|
25
40
|
############################################################################################
|
|
@@ -38,12 +53,46 @@ module Petergate
|
|
|
38
53
|
# end
|
|
39
54
|
# end
|
|
40
55
|
|
|
56
|
+
# The destination filename is the only thing that sets a migration's class
|
|
57
|
+
# name -- Rails derives @migration_class_name from it -- so it has to
|
|
58
|
+
# carry the table name rather than being copied from the template's own.
|
|
41
59
|
def create_migrations
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
migration_template "migrations/#{name}", "db/migrate/#{name}"
|
|
45
|
-
end
|
|
60
|
+
migration_template "migrations/add_roles_to_users.rb",
|
|
61
|
+
"db/migrate/add_roles_to_#{roles_table_name}.rb"
|
|
46
62
|
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
def model_class_name
|
|
66
|
+
model_name.camelize
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# `class Admin::User < ...` declares the demodulized name.
|
|
70
|
+
def bare_class_name
|
|
71
|
+
model_class_name.split("::").last
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def model_path
|
|
75
|
+
File.join("app/models", "#{model_class_name.underscore}.rb")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Rails writes a namespaced model either compactly --
|
|
79
|
+
# `class Admin::User < ApplicationRecord` -- or nested inside
|
|
80
|
+
# `module Admin`, where the line reads `class User < ...`. Match both.
|
|
81
|
+
def model_declaration_pattern
|
|
82
|
+
names = [model_class_name, bare_class_name].uniq.map { |n| Regexp.escape(n) }
|
|
83
|
+
/^\s*class\s+(?:#{names.join("|")})\s+<\s+\S+.*$/
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Ask the model where it actually lives. `Admin::User` is `users` unless
|
|
87
|
+
# the namespace defines a table_name_prefix, and only the class knows
|
|
88
|
+
# which -- `tableize` would guess `admin_users` either way.
|
|
89
|
+
def roles_table_name
|
|
90
|
+
return options[:table_name] if options[:table_name]
|
|
91
|
+
|
|
92
|
+
model_class_name.constantize.table_name
|
|
93
|
+
rescue StandardError
|
|
94
|
+
model_class_name.demodulize.tableize
|
|
95
|
+
end
|
|
47
96
|
end
|
|
48
97
|
end
|
|
49
98
|
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
class
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration<%= Rails::VERSION::MAJOR >= 5 ? "[#{Rails.version.to_f}]" : "" %>
|
|
2
2
|
def change
|
|
3
|
-
add_column
|
|
3
|
+
add_column :<%= roles_table_name %>, :roles, :string
|
|
4
4
|
end
|
|
5
5
|
end
|