banken 1.0.3 → 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: 4faea250e6de5c7022b73be747b18978e4569a1e
4
- data.tar.gz: fe8b0f64b93b370f9beaa04ea581180a4d2b3760
2
+ SHA256:
3
+ metadata.gz: 76fb7526d0910217060fb72f1c1fa941bc7d8afe4a92a9f1ee08ab7ef3d8d3d1
4
+ data.tar.gz: 57d909cf4bc3337d62fc2f8c944abc06f5256fde7604bf55d02d261c2e9a0219
5
5
  SHA512:
6
- metadata.gz: 49a1116ac331febca898385de35fcd0c518840cb3eab76cca7e266c99832f19572063573d8f6d3b9f18c97271885091d636594429d1ffbcbb22d44c82cfb1ae7
7
- data.tar.gz: 711723dd4d3c2118624d7dca4bcaaeddef0bf203c30c063d18833df70644495634984da76196bddd6c68b685c5f7e3a232e55c808c0e5f9c672e9e6414fa6c64
6
+ metadata.gz: e8840887cc92393a79b1ba6b6dcac8e31fddd330bc54bb3c6d8be0b28666db791244ada3e391e429f8a064a28536d5f19bb3fd860bbb736248d0eacdc036f509
7
+ data.tar.gz: dfdab031e56c3fd23213c6f2e72ae9dccd8f19b379a82d292869b40e82fb044ea37cda788bb02b0bc3cfc9d9f8cf8c8ec22d64ee2f4d017e4a79615cc3f292dc
data/CHANGELOG.md CHANGED
@@ -1,3 +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
+
1
166
  ## 1.0.3 (2019-01-14)
2
167
 
3
- * Renamed `banken_loyalty_authorized?` to `banken_authorization_performed?` (okuramasafumi)
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
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,9 +169,15 @@ conditionally showing links or buttons in the view:
157
169
  <% end %>
158
170
  ```
159
171
 
160
- If you are using namespace in your controller and policy,
161
- you can access the policy passing string like 'admin/posts' as a second argument.
162
- Below calls Admin::PostsLoyalty.
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`:
163
181
 
164
182
  ``` erb
165
183
  <% if loyalty(@post, 'admin/posts').update? %>
@@ -167,6 +185,35 @@ Below calls Admin::PostsLoyalty.
167
185
  <% end %>
168
186
  ```
169
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
+
170
217
  ## Ensuring loyalties are used
171
218
 
172
219
  Banken adds a method called `verify_authorized` to your controllers. This
@@ -177,10 +224,15 @@ forgotten to authorize the action. For example:
177
224
  ``` ruby
178
225
  # app/controllers/application_controller.rb
179
226
  class ApplicationController < ActionController::Base
180
- after_action :verify_authorized, except: :index
227
+ after_action :verify_authorized
181
228
  end
182
229
  ```
183
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
+
184
236
 
185
237
  If you're using `verify_authorized` in your controllers but need to
186
238
  conditionally bypass verification, you can use `skip_authorization`.
@@ -202,8 +254,8 @@ end
202
254
  ```
203
255
 
204
256
  If you need to perform some more sophisticated logic or you want to raise a custom
205
- exception you can use the two lower level method `banken_authorization_performed?` which
206
- 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.
207
259
 
208
260
  ## Just plain old Ruby
209
261
 
@@ -253,15 +305,14 @@ end
253
305
  ## Rescuing a denied Authorization in Rails
254
306
 
255
307
  Banken raises a `Banken::NotAuthorizedError` you can
256
- [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)
257
309
  in your `ApplicationController`. You can customize the `user_not_authorized`
258
310
  method in every controller.
259
311
 
260
312
  ```ruby
261
313
  # app/controllers/application_controller.rb
262
314
  class ApplicationController < ActionController::Base
263
- protect_from_forgery
264
- include Banken
315
+ include Banken::Authorization
265
316
 
266
317
  rescue_from Banken::NotAuthorizedError, with: :user_not_authorized
267
318
 
@@ -269,44 +320,63 @@ class ApplicationController < ActionController::Base
269
320
 
270
321
  def user_not_authorized
271
322
  flash[:alert] = "You are not authorized to perform this action."
272
- redirect_to(request.referrer || root_path)
323
+ redirect_back_or_to(root_path, allow_other_host: false)
273
324
  end
274
325
  end
275
326
  ```
276
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
+
277
346
  ## Creating custom error messages
278
347
 
279
- `NotAuthorizedError`s provide information on what query (e.g. `:create?`), what
280
- controller (e.g. `PostsController`), and what loyalty (e.g. an instance of
281
- `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.
282
352
 
283
- 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
284
354
  them with `I18n` to generate error messages. Here's how you might go about doing
285
355
  that.
286
356
 
287
357
  ```ruby
288
358
  # app/controllers/application_controller.rb
289
359
  class ApplicationController < ActionController::Base
290
- rescue_from Banken::NotAuthorizedError, with: :user_not_authorized
360
+ rescue_from Banken::NotAuthorizedError, with: :user_not_authorized
291
361
 
292
- private
362
+ private
293
363
 
294
- def user_not_authorized(exception)
295
- loyalty_name = exception.loyalty.class.to_s.underscore
364
+ def user_not_authorized(exception)
365
+ loyalty_name = exception.loyalty.class.to_s.underscore
296
366
 
297
- flash[:error] = t "#{loyalty_name}.#{exception.query}", scope: "banken", default: :default
298
- redirect_to(request.referrer || root_path)
299
- 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
300
370
  end
301
371
  ```
302
372
 
303
373
  ```yaml
304
374
  en:
305
- banken:
306
- default: 'You cannot perform this action.'
307
- posts_loyalty:
308
- update?: 'You cannot edit this post!'
309
- 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!'
310
380
  ```
311
381
 
312
382
  Of course, this is just an example. Banken is agnostic as to how you implement
@@ -316,14 +386,46 @@ your error messaging.
316
386
 
317
387
  In some cases your controller might not have access to `current_user`, or your
318
388
  `current_user` is not the method that should be invoked by Banken. Simply
319
- 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`:
320
392
 
321
393
  ```ruby
322
- def banken_user
323
- 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
324
401
  end
325
402
  ```
326
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
+
327
429
  ## Additional context
328
430
 
329
431
  Banken strongly encourages you to model your application in such a way that the
@@ -352,28 +454,31 @@ class UserContext
352
454
  end
353
455
 
354
456
  # app/controllers/application_controller.rb
355
- class ApplicationController
356
- include Banken
457
+ class ApplicationController < ActionController::Base
458
+ include Banken::Authorization
357
459
 
358
- def banken_user
359
- UserContext.new(current_user, request.ip)
360
- 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
361
467
  end
362
468
  ```
363
469
 
364
470
  ## Strong parameters
365
471
 
366
- In Rails 4 (or Rails 3.2 with the
367
- [strong_parameters](https://github.com/rails/strong_parameters) gem),
368
- mass-assignment protection is handled in the controller. With Banken you can
369
- control which attributes a user has access to update via your loyalties. You can
370
- 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:
371
476
 
372
477
  ```ruby
373
478
  # app/loyalties/posts_loyalty.rb
374
479
  class PostsLoyalty < ApplicationLoyalty
375
480
  def permitted_attributes
376
- if user.admin? || user.owner_of?(post)
481
+ if user.admin? || user.owner_of?(record)
377
482
  [:title, :body, :tag_list]
378
483
  else
379
484
  [:tag_list]
@@ -389,10 +494,10 @@ You can now retrieve these attributes from the loyalty:
389
494
  class PostsController < ApplicationController
390
495
  def update
391
496
  @post = Post.find(params[:id])
392
- if @post.update_attributes(post_params)
497
+ if @post.update(post_params)
393
498
  redirect_to @post
394
499
  else
395
- render :edit
500
+ render :edit, status: 422
396
501
  end
397
502
  end
398
503
 
@@ -404,22 +509,63 @@ class PostsController < ApplicationController
404
509
  end
405
510
  ```
406
511
 
407
- 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):
408
514
 
409
515
  ```ruby
410
516
  # app/controllers/posts_controller.rb
411
517
  class PostsController < ApplicationController
412
518
  def update
413
519
  @post = Post.find(params[:id])
414
- if @post.update_attributes(permitted_attributes(@post))
520
+ if @post.update(permitted_attributes(@post))
415
521
  redirect_to @post
416
522
  else
417
- render :edit
523
+ render :edit, status: 422
418
524
  end
419
525
  end
420
526
  end
421
527
  ```
422
528
 
423
- # 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
424
570
 
425
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.