banken 1.0.2 → 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: c0165126f4ba4d74766ee5eb58d91dc7ff2da97c
4
- data.tar.gz: 3033cd6c9f9d63a93413f0bc89f0e748b708d124
2
+ SHA256:
3
+ metadata.gz: 76fb7526d0910217060fb72f1c1fa941bc7d8afe4a92a9f1ee08ab7ef3d8d3d1
4
+ data.tar.gz: 57d909cf4bc3337d62fc2f8c944abc06f5256fde7604bf55d02d261c2e9a0219
5
5
  SHA512:
6
- metadata.gz: ddba855ceb965c1c720a8b8a0d308285284510cf98d383ab4e7737fe79bc97235e9fd133400628887233a70a64d68b7b42a98157ebb3b95c1f8ea55cd87362bd
7
- data.tar.gz: 6db1afc5300330a1ddb09a186a7bc1beea908cf6491b123c61a1593173ebfe449a10c6fe06abea41d711144fce51264ab8a396339da4f021bf342f997be38ffe
6
+ metadata.gz: e8840887cc92393a79b1ba6b6dcac8e31fddd330bc54bb3c6d8be0b28666db791244ada3e391e429f8a064a28536d5f19bb3fd860bbb736248d0eacdc036f509
7
+ data.tar.gz: dfdab031e56c3fd23213c6f2e72ae9dccd8f19b379a82d292869b40e82fb044ea37cda788bb02b0bc3cfc9d9f8cf8c8ec22d64ee2f4d017e4a79615cc3f292dc
data/CHANGELOG.md ADDED
@@ -0,0 +1,185 @@
1
+ ## 1.1.0 (2026-10-26)
2
+
3
+ Requires Ruby >= 3.3 and Rails >= 7.1. No public methods or constants were removed. CI covers
4
+ Ruby 3.3 / 3.4 / 4.0 x Rails 7.1 / 7.2 / 8.0 / 8.1.
5
+
6
+ ### Upgrading from 1.0.x
7
+
8
+ No breaking changes to the public API: no method, constant or argument was removed or changed
9
+ its signature, `include Banken` keeps working unchanged (`include Banken::Authorization` is the
10
+ recommended form from now on), and an application that upgrades without touching its code keeps
11
+ authorizing the same requests. What follows are behaviour changes, most of them closing security
12
+ gaps or replacing confusing errors with clear ones, that can affect code or tests relying on the
13
+ old behaviour. Details in the sections below.
14
+
15
+ * The default `Banken::NotAuthorizedError` message changed from
16
+ `not allowed to update? of posts by #<PostsLoyalty ...>` to
17
+ `not allowed to PostsLoyalty#update? on posts`. Code or tests matching on the message text
18
+ must change; the `controller`, `query` and `loyalty` attributes are unchanged.
19
+ * A query the loyalty does not define raises `Banken::QueryNotDefinedError` (a subclass of
20
+ `NotDefinedError`) instead of `NoMethodError`; code rescuing or asserting `NoMethodError` there
21
+ must change, and a `rescue_from Banken::NotDefinedError` now also catches it. Queries inherited
22
+ from `Object` (an action named `present` or `duplicable`) and private queries raise it too.
23
+ * Anonymous controllers (`controller_path` nil) raise `NotDefinedError` from `authorize!` and
24
+ `loyalty`; pass the controller name explicitly (`loyalty(record, :posts)`). On a Rails controller
25
+ `params[:controller]` / `params[:action]` are never consulted any more.
26
+ * Banken's helpers (`authorize!`, `loyalty`, `skip_authorization`, `verify_authorized`,
27
+ `banken_user`, `permitted_attributes`, `banken_authorization_performed?`,
28
+ `banken_loyalty_authorized?`) are no longer routable actions, and neither is an application
29
+ action that carries one of those names.
30
+ * The `Banken::AuthorizationNotPerformedError` message changed from the bare class name to
31
+ `PostsController#update did not call authorize! or skip_authorization`.
32
+ * `NotAuthorizedError#controller` (and the messages) carry `controller_path`, which differs from
33
+ the old `params[:controller]` when the controller overrides `self.controller_path` or when the
34
+ route name does not round-trip through camelize/underscore (`users_2fa` -> `Users2faController`
35
+ -> `users2fa`). The loyalty resolved is the same.
36
+ * `permitted_attributes`: a scalar or array params value raises `ActionController::ParameterMissing`
37
+ (400) instead of `NoMethodError` (500); `nil` and anonymous classes raise `ArgumentError`; the
38
+ params key is derived from the class's `name` (no longer `to_s`), which matters only for classes
39
+ overriding one of them.
40
+ * Errors raised while a loyalty file loads now propagate instead of becoming
41
+ `Banken::NotDefinedError`; a `rescue_from Banken::NotDefinedError` no longer catches them.
42
+ * The controller `loyalty` helper no longer calls `Banken.loyalty!`; stubs on `Banken.loyalty!`
43
+ stop intercepting controller lookups (stub `Banken::LoyaltyFinder` instead). The memoized loyalty
44
+ class is kept in `@_banken_loyalty_classes`, which shows up in `view_assigns` / `assigns` next to
45
+ the existing `@_banken_authorization_performed`.
46
+ * `config.action_dispatch.rescue_responses` matches exact class names (Rails behaviour): a mapping
47
+ for `Banken::NotDefinedError` does not cover `Banken::QueryNotDefinedError`.
48
+ * `rails g banken:loyalty` no longer invokes a `test_framework` generator; a custom
49
+ `test_unit:loyalty` / `rspec:loyalty` generator must be invoked directly.
50
+ * Ruby < 3.3 and Rails < 7.1 are no longer supported; such environments keep resolving 1.0.3.
51
+
52
+ ### Requirements
53
+
54
+ * Ruby >= 3.3 and Rails (Active Support) >= 7.1. Supported matrix: Ruby 3.3 / 3.4 / 4.0 x Rails
55
+ 7.1 / 7.2 / 8.0 / 8.1. Environments below these floors keep resolving 1.0.3.
56
+
57
+ ### Security
58
+
59
+ * The default `Banken::NotAuthorizedError` message no longer embeds `loyalty.inspect`, which
60
+ included the inspected user and record (every instance variable of plain Ruby objects; for Active
61
+ Record models every attribute not covered by `filter_attributes`) and reached logs, error trackers
62
+ and any `flash` built from the message. The message is now `not allowed to PostsLoyalty#update? on
63
+ posts`. The `controller`, `query` and `loyalty` attributes are unchanged.
64
+ * `authorize!` only consults queries defined by the loyalty itself (or a module or superclass below
65
+ `Object`). ActiveSupport defines `present?` and `duplicable?` on every object and both return
66
+ `true`, so an action named `present` or `duplicable` passed `authorize!` with a loyalty that
67
+ defined nothing. Such queries, undefined queries and private queries now raise
68
+ `Banken::QueryNotDefinedError`; queries answered through `method_missing` keep working.
69
+ * For routes that target an endpoint directly (`get "/x" => PostsController.action(:destroy)`,
70
+ `mount`), the path parameters carry no controller or action, so the previous inference read them
71
+ from the query string and a request could pick which loyalty and query were checked. The loyalty
72
+ and query now come from the controller class and the dispatched action.
73
+ * On a Rails controller, `params[:controller]` / `params[:action]` are never consulted any more.
74
+ Anonymous controllers (`controller_path` nil) used to fall back to `params[:controller]`, which
75
+ comes from the query string there, letting a request choose the loyalty. They now raise
76
+ `NotDefinedError`; pass the controller name to `loyalty` explicitly.
77
+ * Banken's helpers are excluded from the controller's `action_methods`, so routes with a dynamic
78
+ `:action` segment can no longer dispatch to `skip_authorization`, `verify_authorized` and the
79
+ others. Same as Pundit #269 / #357.
80
+
81
+ ### Added
82
+
83
+ * `Banken::Authorization`, the controller mixin. Prefer `include Banken::Authorization` over
84
+ `include Banken`: including the top-level module also makes every constant under `Banken` (`Error`,
85
+ `VERSION`, `LoyaltyFinder`, `Railtie`, ...) resolvable inside the controller, shadowing application
86
+ constants of the same name. `include Banken` keeps working (it includes `Banken::Authorization`)
87
+ and will be deprecated in 2.0. Same motivation as Pundit 2.2.0's `Pundit::Authorization`.
88
+ * `Banken::QueryNotDefinedError < Banken::NotDefinedError`, raised when the loyalty exists but the
89
+ query is undefined, private or inherited from `Object`.
90
+ * `Banken.deprecator`, an `ActiveSupport::Deprecation` instance, and `Banken::Railtie`, which
91
+ registers it with `Rails.application.deprecators` so `config.active_support.deprecation` applies
92
+ to Banken (its `:notify` events are published as `deprecation.banken`). If banken is required
93
+ after Rails has initialized (for example `require: false` plus a later `require`), register it
94
+ yourself with `Rails.application.deprecators[:banken] = Banken.deprecator`.
95
+ * Two private hooks overridable in the controller: `banken_param_key(record)` (defaults to the
96
+ demodulized, underscored class name) and `banken_params_for(record)` (defaults to
97
+ `params.require(banken_param_key(record))`).
98
+ * `permitted_attributes` accepts a class or a Symbol as the record (`permitted_attributes(Post)` and
99
+ `permitted_attributes(:post)` both read `params[:post]`).
100
+
101
+ ### Fixed
102
+
103
+ * `banken_loyalty_authorized?` raised `NoMethodError` on Rails >= 7.2 because it called the
104
+ removed `ActiveSupport::Deprecation` singleton. It now warns through `Banken.deprecator`.
105
+ * Errors raised while a loyalty file loads (for example a `NoMethodError` from a typo in the class
106
+ body, or a `NameError` for an unrelated constant) now propagate instead of being reported as
107
+ `Banken::NotDefinedError`. `LoyaltyFinder` uses `safe_constantize`, so only `NameError`s that name
108
+ the loyalty constant or one of its namespaces (including Zeitwerk's "expected file to define
109
+ constant" error for a file that defines the wrong constant name) still surface as `NotDefinedError`.
110
+ The `NotDefinedError` message now also names the file expected to define the loyalty.
111
+ * `permitted_attributes(Post)` reads `params[:post]` (it used to read `params[:class]`); a scalar or
112
+ array value raises `ActionController::ParameterMissing` (400) instead of `NoMethodError` (500);
113
+ `nil` raises `ArgumentError` instead of reading `params[:nil_class]`; an anonymous class raises
114
+ `ArgumentError`.
115
+ * `verify_authorized` says which controller and action forgot to authorize
116
+ (`PostsController#update did not call authorize! or skip_authorization`).
117
+ * An empty controller name no longer resolves a top-level `Loyalty` constant; `Banken.loyalty!(nil, ...)`
118
+ raises `NotDefinedError` ("no controller name was given").
119
+ * `Banken::NotAuthorizedError.new` accepts any non-Hash argument as the message (e.g. `nil` or a
120
+ Symbol) instead of raising `NoMethodError`.
121
+ * `rails g banken:loyalty` no longer prints `error test_unit [not found]` (or `rspec [not found]`):
122
+ the generator asked the configured test framework for a `loyalty` generator that Banken never
123
+ shipped. It now only creates the loyalty file; a `--test-framework` option is ignored if passed.
124
+
125
+ ### Changed
126
+
127
+ * The loyalty and the query are inferred from `controller_path` and `action_name` instead of
128
+ `params[:controller]` / `params[:action]`; params are read only on hosts that provide neither
129
+ (plain Ruby objects). Routed requests resolve the same loyalty as before; `NotAuthorizedError#controller`
130
+ is now `controller_path`, which differs from `params[:controller]` when the controller overrides
131
+ `self.controller_path` (the loyalty is then looked up from the overridden value) or when the route
132
+ name does not round-trip through camelize/underscore (`users_2fa` -> `users2fa`). `authorize!` and
133
+ `loyalty` now also work when a controller is dispatched directly
134
+ (`PostsController.action(:index).call(env)`), and `loyalty` resolves inside
135
+ `ActionController::Renderer` (there is no action there, so `authorize!` raises a `Banken::Error`
136
+ explaining that instead of a `NoMethodError` for `?`).
137
+ * `loyalty` resolves the loyalty class once per controller instance (per request) instead of on
138
+ every call; loyalty instances are still created fresh for each record and the current
139
+ `banken_user`. The controller helper no longer goes through `Banken.loyalty!` (which is unchanged).
140
+ * `permitted_attributes` derives the params key from the class's `name` instead of `record.class.to_s`.
141
+
142
+ ### Deprecated (removal planned for 2.0)
143
+
144
+ * `include Banken` in controllers (use `include Banken::Authorization`); no warning is emitted yet.
145
+ * `banken_loyalty_authorized?` (use `banken_authorization_performed?`); it now warns through
146
+ `Banken.deprecator`.
147
+ * `Banken::LoyaltyScopingNotPerformedError`, which nothing raises.
148
+
149
+ ### Packaging and documentation
150
+
151
+ * The gem ships only `lib/`, README, CHANGELOG, LICENSE and SECURITY.md (no more Gemfile, Rakefile,
152
+ gemfiles/ or CI config); `rubygems_mfa_required` is set, so publishing needs MFA; banken no longer
153
+ requires `active_support/core_ext/module/introspection`, which it never used. Generator templates
154
+ are shipped as `.rb.tt` files, the Rails convention.
155
+ * README rewritten for Rails 7.1+ (`update` instead of `update_attributes`, no Rails 4 /
156
+ strong_parameters notes), the `rescue_from` examples use
157
+ `redirect_back_or_to(root_path, allow_other_host: false)` instead of redirecting to the `Referer`
158
+ header (an open redirect or an `UnsafeRedirectError`, depending on the app's open-redirect
159
+ configuration), `rescue_responses` documented as an alternative, inference from `controller_path` /
160
+ `action_name` explained, naming caveat for an action called `loyalty`, `Banken.loyalty!` for use
161
+ outside controllers, `after_action :verify_authorized` without `except: :index` (Banken has no
162
+ scopes), the `UserContext` example returns nil for anonymous requests so a "must be logged in"
163
+ guard still works, queries must return booleans, `permitted_attributes` uses the current
164
+ controller's loyalty, never pass request data as the controller name to `loyalty`. Added SECURITY.md.
165
+
166
+ ## 1.0.3 (2019-01-14)
167
+
168
+ * Renamed `banken_loyalty_authorized?` to `banken_authorization_performed?`; the old name emits a
169
+ deprecation warning (okuramasafumi)
170
+ * README: how to call a namespaced loyalty from a view (`loyalty(@post, 'admin/posts')`)
171
+
172
+ ## 1.0.2 (2016-01-30)
173
+
174
+ * Removed the loyalty cache and the public `loyalties` method; `loyalty` builds a new instance on every
175
+ call (the 1.0.1 cache ignored the controller name)
176
+
177
+ ## 1.0.1 (2016-01-30)
178
+
179
+ * Fixed `loyalty` returning the same instance for different records: the cache, keyed per controller
180
+ since 1.0.0, is now keyed by record (#1, tkawa)
181
+
182
+ ## 1.0.0 (2015-12-05)
183
+
184
+ * First stable release: `authorize!`, `loyalty`, `permitted_attributes`, `verify_authorized`,
185
+ `skip_authorization`, `banken_user`, `banken:install` and `banken:loyalty` generators
data/README.md CHANGED
@@ -1,39 +1,51 @@
1
- <img width="350"src="https://raw.github.com/wiki/kyuden/banken/images/banken.png">
1
+ <img width="350" src="https://raw.github.com/wiki/kyuden/banken/images/banken.png">
2
2
 
3
- [![Build Status](https://img.shields.io/travis/kyuden/banken/master.svg)](https://travis-ci.org/kyuden/banken)
4
- [![Code Climate](https://codeclimate.com/github/kyuden/banken/badges/gpa.svg)](https://codeclimate.com/github/kyuden/banken)
3
+ [![CI](https://github.com/kyuden/banken/actions/workflows/ci.yml/badge.svg)](https://github.com/kyuden/banken/actions/workflows/ci.yml)
5
4
  [![Gem Version](https://badge.fury.io/rb/banken.svg)](https://badge.fury.io/rb/banken)
6
5
 
7
6
  Simple and lightweight authorization library for Rails inspired by Pundit.
8
7
  Banken provides a set of helpers which restricts what resources
9
8
  a given user is allowed to access.
10
9
 
11
- In first, Look this tutorial:
10
+ Start with the tutorial:
12
11
  - [Tutorial](https://github.com/kyuden/banken/wiki/Tutorial)
13
12
  - [Tutorial (Japanese)](https://github.com/kyuden/banken/wiki/Tutorial-(japanese))
14
13
 
15
- ========
16
14
  ## What's the difference between Banken and Pundit?
17
15
  - [The difference between Banken and Pundit](https://github.com/kyuden/banken/wiki/The-difference-between-Banken-and-Pundit)
18
16
  - [The difference between Banken and Pundit (Japanese)](https://github.com/kyuden/banken/wiki/The-difference-between-Banken-and-Pundit-(Japanese))
19
17
 
20
-
21
18
  ## Installation
22
19
 
23
20
  ``` ruby
24
21
  gem "banken"
25
22
  ```
26
23
 
27
- Include Banken in your application controller:
24
+ Supported versions (tested in CI):
25
+
26
+ | Ruby | Rails |
27
+ |---|---|
28
+ | 3.3, 3.4, 4.0 | 7.1, 7.2, 8.0, 8.1 |
29
+
30
+ Ruby versions follow the official end-of-life schedule; Rails versions that
31
+ reached end-of-life are dropped gradually as applications move on. Environments
32
+ below these floors stay on banken 1.0.3, which works on Rails 7.1 and older.
33
+
34
+ Include `Banken::Authorization` in your application controller
35
+ (`ActionController::API` works too):
28
36
 
29
37
  ``` ruby
30
38
  # app/controllers/application_controller.rb
31
39
  class ApplicationController < ActionController::Base
32
- include Banken
33
- protect_from_forgery
40
+ include Banken::Authorization
34
41
  end
35
42
  ```
36
43
 
44
+ `include Banken` (the pre-1.1 form) still works and will be deprecated in 2.0.
45
+ Avoid it: it also brings every constant under the `Banken` namespace (`Error`,
46
+ `VERSION`, `LoyaltyFinder`, ...) into the controller's constant lookup, where it
47
+ shadows application constants of the same name.
48
+
37
49
  Optionally, you can run the generator, which will set up an application loyalty
38
50
  with some useful defaults for you:
39
51
 
@@ -107,7 +119,7 @@ class PostsController < ApplicationController
107
119
  if @post.update(post_params)
108
120
  redirect_to @post
109
121
  else
110
- render :edit
122
+ render :edit, status: 422
111
123
  end
112
124
  end
113
125
  end
@@ -123,8 +135,8 @@ and the given optional object. It then infers from the action name, that it shou
123
135
  raise "not authorized" unless PostsLoyalty.new(current_user, @post).update?
124
136
  ```
125
137
 
126
- If you don't have an optional object for the first argument to `authorize!`, then you can pass
127
- the class. For example:
138
+ If there is no record to check, call `authorize!` without an argument (the
139
+ loyalty then receives `nil` as its record), or pass the class. For example:
128
140
 
129
141
  Loyalty:
130
142
  ```ruby
@@ -157,6 +169,51 @@ conditionally showing links or buttons in the view:
157
169
  <% end %>
158
170
  ```
159
171
 
172
+ Banken infers the loyalty from the controller path (`controller_path`) and the
173
+ query from the action name (`action_name`). A namespaced controller such as
174
+ `Admin::PostsController` (controller path `admin/posts`) therefore uses
175
+ `Admin::PostsLoyalty`, which lives in `app/loyalties/admin/posts_loyalty.rb`.
176
+ The same applies to controllers inside Rails engines: `Blorgh::PostsController`
177
+ uses `Blorgh::PostsLoyalty`. There is no fallback to a top-level loyalty.
178
+
179
+ To use another controller's loyalty, pass its controller path as the second
180
+ argument. Below calls `Admin::PostsLoyalty`:
181
+
182
+ ``` erb
183
+ <% if loyalty(@post, 'admin/posts').update? %>
184
+ <%= link_to "Edit post", edit_post_path(@post) %>
185
+ <% end %>
186
+ ```
187
+
188
+ Banken also works for controllers dispatched without the router
189
+ (`PostsController.action(:index).call(env)`). In templates rendered through
190
+ `ActionController::Renderer` (`PostsController.render ...`), `loyalty` infers
191
+ the loyalty from the controller the renderer was called on; `authorize!` cannot
192
+ be used there because no action is running.
193
+
194
+ A query is only consulted if the loyalty (or a module or superclass of it)
195
+ defines it (`method_missing` counts); methods every Ruby object has, such as
196
+ `present?`, do not, and an undefined or private query raises
197
+ `Banken::QueryNotDefinedError`, a subclass of `NotDefinedError`. Return `true` or `false`
198
+ from queries: any truthy value authorizes.
199
+
200
+ The second argument of `loyalty` must come from your code, never from the
201
+ request: it selects which loyalty class is instantiated.
202
+
203
+ **Naming caveat:** `loyalty` is a public method on your controllers. Do not
204
+ define a controller action named `loyalty`, or it will shadow the helper. The
205
+ helper is scheduled to be renamed to `banken_loyalty` (with `loyalty` kept as an
206
+ alias) in 2.0.
207
+
208
+ ## Using loyalties outside controllers
209
+
210
+ `Banken.loyalty!` builds a loyalty from a controller path, a user and a record,
211
+ for jobs, GraphQL resolvers or anywhere there is no controller:
212
+
213
+ ``` ruby
214
+ Banken.loyalty!('admin/posts', user, post).update?
215
+ ```
216
+
160
217
  ## Ensuring loyalties are used
161
218
 
162
219
  Banken adds a method called `verify_authorized` to your controllers. This
@@ -167,10 +224,15 @@ forgotten to authorize the action. For example:
167
224
  ``` ruby
168
225
  # app/controllers/application_controller.rb
169
226
  class ApplicationController < ActionController::Base
170
- after_action :verify_authorized, except: :index
227
+ after_action :verify_authorized
171
228
  end
172
229
  ```
173
230
 
231
+ `verify_authorized` runs after the action, so it is a development-time safety
232
+ net that turns a forgotten `authorize!` into an error; it does not stop the
233
+ action from running. Do not exempt `index`: Banken has no scope feature, so an
234
+ index action needs `authorize!` like any other.
235
+
174
236
 
175
237
  If you're using `verify_authorized` in your controllers but need to
176
238
  conditionally bypass verification, you can use `skip_authorization`.
@@ -192,8 +254,8 @@ end
192
254
  ```
193
255
 
194
256
  If you need to perform some more sophisticated logic or you want to raise a custom
195
- exception you can use the two lower level method `banken_loyalty_authorized?` which
196
- return `true` or `false` depending on whether `authorize!` have been called, respectively.
257
+ exception you can use the lower-level method `banken_authorization_performed?`, which
258
+ returns `true` or `false` depending on whether `authorize!` (or `skip_authorization`) has been called.
197
259
 
198
260
  ## Just plain old Ruby
199
261
 
@@ -243,15 +305,14 @@ end
243
305
  ## Rescuing a denied Authorization in Rails
244
306
 
245
307
  Banken raises a `Banken::NotAuthorizedError` you can
246
- [rescue_from](http://guides.rubyonrails.org/action_controller_overview.html#rescue-from)
308
+ [rescue_from](https://guides.rubyonrails.org/action_controller_overview.html#rescue-from)
247
309
  in your `ApplicationController`. You can customize the `user_not_authorized`
248
310
  method in every controller.
249
311
 
250
312
  ```ruby
251
313
  # app/controllers/application_controller.rb
252
314
  class ApplicationController < ActionController::Base
253
- protect_from_forgery
254
- include Banken
315
+ include Banken::Authorization
255
316
 
256
317
  rescue_from Banken::NotAuthorizedError, with: :user_not_authorized
257
318
 
@@ -259,44 +320,63 @@ class ApplicationController < ActionController::Base
259
320
 
260
321
  def user_not_authorized
261
322
  flash[:alert] = "You are not authorized to perform this action."
262
- redirect_to(request.referrer || root_path)
323
+ redirect_back_or_to(root_path, allow_other_host: false)
263
324
  end
264
325
  end
265
326
  ```
266
327
 
328
+ Do not redirect to `request.referrer`: the `Referer` header is controlled by
329
+ the client. Depending on your open-redirect configuration that either sends the
330
+ user to another site or raises `ActionController::Redirecting::UnsafeRedirectError`.
331
+ `redirect_back_or_to` falls back to `root_path` when there is no referrer, and
332
+ `allow_other_host: false` makes it fall back for off-site referrers too,
333
+ whatever `raise_on_open_redirects` / `action_on_open_redirect` are set to.
334
+
335
+ If you only need the status code, map the error to 403 and let Rails render it:
336
+
337
+ ```ruby
338
+ # config/application.rb
339
+ config.action_dispatch.rescue_responses["Banken::NotAuthorizedError"] = :forbidden
340
+ ```
341
+
342
+ Without either, an unrescued `NotAuthorizedError` is a 500. `rescue_responses`
343
+ matches exact class names, so a mapping for `Banken::NotDefinedError` does not
344
+ cover `Banken::QueryNotDefinedError`; `rescue_from Banken::NotDefinedError` does.
345
+
267
346
  ## Creating custom error messages
268
347
 
269
- `NotAuthorizedError`s provide information on what query (e.g. `:create?`), what
270
- controller (e.g. `PostsController`), and what loyalty (e.g. an instance of
271
- `PostsLoyalty`) caused the error to be raised.
348
+ `NotAuthorizedError`s provide information on what query (e.g. `"create?"`), what
349
+ controller (the controller path, e.g. `"posts"` or `"admin/posts"`), and what
350
+ loyalty (e.g. an instance of `PostsLoyalty`) caused the error to be raised. The
351
+ default message names only these, never the user or the record.
272
352
 
273
- One way to use these `query`, `record`, and `loyalty` properties is to connect
353
+ One way to use these `query`, `controller`, and `loyalty` properties is to connect
274
354
  them with `I18n` to generate error messages. Here's how you might go about doing
275
355
  that.
276
356
 
277
357
  ```ruby
278
358
  # app/controllers/application_controller.rb
279
359
  class ApplicationController < ActionController::Base
280
- rescue_from Banken::NotAuthorizedError, with: :user_not_authorized
360
+ rescue_from Banken::NotAuthorizedError, with: :user_not_authorized
281
361
 
282
- private
362
+ private
283
363
 
284
- def user_not_authorized(exception)
285
- loyalty_name = exception.loyalty.class.to_s.underscore
364
+ def user_not_authorized(exception)
365
+ loyalty_name = exception.loyalty.class.to_s.underscore
286
366
 
287
- flash[:error] = t "#{loyalty_name}.#{exception.query}", scope: "banken", default: :default
288
- redirect_to(request.referrer || root_path)
289
- end
367
+ flash[:error] = t "#{loyalty_name}.#{exception.query}", scope: "banken", default: :default
368
+ redirect_back_or_to(root_path, allow_other_host: false)
369
+ end
290
370
  end
291
371
  ```
292
372
 
293
373
  ```yaml
294
374
  en:
295
- banken:
296
- default: 'You cannot perform this action.'
297
- posts_loyalty:
298
- update?: 'You cannot edit this post!'
299
- create?: 'You cannot create posts!'
375
+ banken:
376
+ default: 'You cannot perform this action.'
377
+ posts_loyalty:
378
+ update?: 'You cannot edit this post!'
379
+ create?: 'You cannot create posts!'
300
380
  ```
301
381
 
302
382
  Of course, this is just an example. Banken is agnostic as to how you implement
@@ -306,14 +386,46 @@ your error messaging.
306
386
 
307
387
  In some cases your controller might not have access to `current_user`, or your
308
388
  `current_user` is not the method that should be invoked by Banken. Simply
309
- define a method in your controller called `banken_user`.
389
+ define a method in your controller called `banken_user`. The Rails 8
390
+ authentication generator, for example, exposes the user as `Current.user` and
391
+ defines no `current_user`:
310
392
 
311
393
  ```ruby
312
- def banken_user
313
- User.find_by_other_means
394
+ class ApplicationController < ActionController::Base
395
+ include Banken::Authorization
396
+
397
+ private
398
+ def banken_user
399
+ Current.user
400
+ end
314
401
  end
315
402
  ```
316
403
 
404
+ Define `banken_user` in the controller class itself (or in a module included
405
+ *after* `Banken::Authorization`): a module included before it is shadowed by
406
+ Banken's default, which silently keeps calling `current_user`. The loyalty is
407
+ built fresh with the current `banken_user` on every call, so changing the user
408
+ mid-request needs no reset.
409
+
410
+ Banken keeps its own helpers (`authorize!`, `loyalty`, `banken_user`,
411
+ `skip_authorization`, `verify_authorized`, `permitted_attributes`, ...) out of
412
+ the controller's `action_methods`, so a wildcard route cannot dispatch to them
413
+ even though they are public; a `banken_user` override is covered by that too.
414
+
415
+ ## Deprecation warnings
416
+
417
+ Banken reports its own deprecations through `Banken.deprecator`, an
418
+ `ActiveSupport::Deprecation` instance. When banken is loaded by Bundler in the
419
+ usual way (after `require "rails/all"`), `Banken::Railtie` registers it with
420
+ `Rails.application.deprecators`, so `config.active_support.deprecation` and
421
+ `config.active_support.report_deprecations` apply to Banken as well (with the
422
+ `:notify` behaviour the events are published as `deprecation.banken`). If you
423
+ require banken only after Rails has initialized, register it yourself:
424
+
425
+ ```ruby
426
+ Rails.application.deprecators[:banken] = Banken.deprecator
427
+ ```
428
+
317
429
  ## Additional context
318
430
 
319
431
  Banken strongly encourages you to model your application in such a way that the
@@ -342,28 +454,31 @@ class UserContext
342
454
  end
343
455
 
344
456
  # app/controllers/application_controller.rb
345
- class ApplicationController
346
- include Banken
457
+ class ApplicationController < ActionController::Base
458
+ include Banken::Authorization
347
459
 
348
- def banken_user
349
- UserContext.new(current_user, request.ip)
350
- end
460
+ private
461
+ # Return nil when nobody is signed in, so a "must be logged in" guard in
462
+ # ApplicationLoyalty#initialize keeps working; inside loyalties check
463
+ # `user.user`, not `user`, which is the wrapper.
464
+ def banken_user
465
+ UserContext.new(current_user, request.remote_ip) if current_user
466
+ end
351
467
  end
352
468
  ```
353
469
 
354
470
  ## Strong parameters
355
471
 
356
- In Rails 4 (or Rails 3.2 with the
357
- [strong_parameters](https://github.com/rails/strong_parameters) gem),
358
- mass-assignment protection is handled in the controller. With Banken you can
359
- control which attributes a user has access to update via your loyalties. You can
360
- set up a `permitted_attributes` method in your loyalty like this:
472
+ Mass-assignment protection is handled in the controller with strong
473
+ parameters. With Banken you can control which attributes a user has access to
474
+ update via your loyalties. You can set up a `permitted_attributes` method in
475
+ your loyalty like this:
361
476
 
362
477
  ```ruby
363
478
  # app/loyalties/posts_loyalty.rb
364
479
  class PostsLoyalty < ApplicationLoyalty
365
480
  def permitted_attributes
366
- if user.admin? || user.owner_of?(post)
481
+ if user.admin? || user.owner_of?(record)
367
482
  [:title, :body, :tag_list]
368
483
  else
369
484
  [:tag_list]
@@ -379,10 +494,10 @@ You can now retrieve these attributes from the loyalty:
379
494
  class PostsController < ApplicationController
380
495
  def update
381
496
  @post = Post.find(params[:id])
382
- if @post.update_attributes(post_params)
497
+ if @post.update(post_params)
383
498
  redirect_to @post
384
499
  else
385
- render :edit
500
+ render :edit, status: 422
386
501
  end
387
502
  end
388
503
 
@@ -394,22 +509,63 @@ class PostsController < ApplicationController
394
509
  end
395
510
  ```
396
511
 
397
- However, this is a bit cumbersome, so Banken provides a convenient helper method:
512
+ However, this is a bit cumbersome, so Banken provides a convenient helper method.
513
+ The record may be an instance or a class (`permitted_attributes(Post)` on create):
398
514
 
399
515
  ```ruby
400
516
  # app/controllers/posts_controller.rb
401
517
  class PostsController < ApplicationController
402
518
  def update
403
519
  @post = Post.find(params[:id])
404
- if @post.update_attributes(permitted_attributes(@post))
520
+ if @post.update(permitted_attributes(@post))
405
521
  redirect_to @post
406
522
  else
407
- render :edit
523
+ render :edit, status: 422
408
524
  end
409
525
  end
410
526
  end
411
527
  ```
412
528
 
413
- # License
529
+ Attributes that the loyalty does not list are dropped like any other
530
+ unpermitted parameter. With `config.action_controller.action_on_unpermitted_parameters = :raise`
531
+ they raise `ActionController::UnpermittedParameters`, which Rails reports as a 500
532
+ unless you map it in `rescue_responses`.
533
+
534
+ `permitted_attributes(record)` always uses the current controller's loyalty
535
+ (`PostsLoyalty` inside `PostsController`, even for `permitted_attributes(@comment)`)
536
+ and expects `params[<key>]` to be a hash of attributes; a scalar or array value is
537
+ rejected with `ActionController::ParameterMissing` (400).
538
+
539
+ The list may contain hash filters for arrays and nested attributes, exactly as
540
+ `permit` accepts them: `[:title, { tags: [] }, { meta: [:key] }]`.
541
+
542
+ `permitted_attributes` reads `params[<key>]`, where the key is the demodulized,
543
+ underscored class name (`Post` and `Admin::Post` both give `post`). Rails forms
544
+ use `model_name.param_key` instead, so a namespaced model (`Admin::Post` is
545
+ posted as `params[:admin_post]`) or a model overriding `model_name` needs the
546
+ `banken_param_key` override below, or every submit ends in
547
+ `ActionController::ParameterMissing`. To change the key, override one of these
548
+ hooks in your controller. **Define the override under `private` (or
549
+ `protected`)**: a public method on a controller is a routable action.
550
+
551
+ ```ruby
552
+ class ApplicationController < ActionController::Base
553
+ include Banken::Authorization
554
+
555
+ private
556
+ # Use the key Rails forms generate (Admin::Post -> "admin_post");
557
+ # model_name works on ActiveModel classes and instances alike
558
+ def banken_param_key(record)
559
+ record.respond_to?(:model_name) ? record.model_name.param_key : super
560
+ end
561
+
562
+ # Or take the attributes from somewhere else entirely
563
+ def banken_params_for(record)
564
+ params.require(:payload)
565
+ end
566
+ end
567
+ ```
568
+
569
+ ## License
414
570
 
415
571
  Licensed under the MIT license, see the separate LICENSE.txt file.
data/SECURITY.md ADDED
@@ -0,0 +1,19 @@
1
+ # Security Policy
2
+
3
+ ## Supported versions
4
+
5
+ Only the most recent minor release receives security fixes. The supported Ruby
6
+ and Rails versions are listed in the README.
7
+
8
+ ## Reporting a vulnerability
9
+
10
+ Please do not open a public issue or pull request for a security problem.
11
+
12
+ Report it privately through
13
+ [GitHub Security Advisories](https://github.com/kyuden/banken/security/advisories/new).
14
+ If that page is unavailable, email the maintainer listed in the gemspec
15
+ (msmsms.um@gmail.com). You will get an acknowledgement within seven days, and
16
+ a fix will be released together with a CHANGELOG entry marked "Security" and an
17
+ advisory once it is available.
18
+
19
+ Thank you for helping keep Banken's users safe.