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 +5 -5
- data/CHANGELOG.md +183 -1
- data/README.md +203 -57
- data/SECURITY.md +19 -0
- data/lib/banken/authorization.rb +202 -0
- data/lib/banken/deprecator.rb +21 -0
- data/lib/banken/error.rb +28 -4
- data/lib/banken/loyalty_finder.rb +23 -4
- data/lib/banken/railtie.rb +9 -0
- data/lib/banken/version.rb +3 -1
- data/lib/banken.rb +11 -71
- data/lib/generators/banken/install/install_generator.rb +3 -1
- data/lib/generators/banken/loyalty/loyalty_generator.rb +3 -3
- metadata +29 -46
- data/.gitignore +0 -9
- data/.rspec +0 -2
- data/.travis.yml +0 -8
- data/CODE_OF_CONDUCT.md +0 -13
- data/Gemfile +0 -3
- data/Rakefile +0 -17
- data/banken.gemspec +0 -30
- /data/lib/generators/banken/install/templates/{application_loyalty.rb → application_loyalty.rb.tt} +0 -0
- /data/lib/generators/banken/loyalty/templates/{loyalty.rb → loyalty.rb.tt} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 76fb7526d0910217060fb72f1c1fa941bc7d8afe4a92a9f1ee08ab7ef3d8d3d1
|
|
4
|
+
data.tar.gz: 57d909cf4bc3337d62fc2f8c944abc06f5256fde7604bf55d02d261c2e9a0219
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
[](https://codeclimate.com/github/kyuden/banken)
|
|
3
|
+
[](https://github.com/kyuden/banken/actions/workflows/ci.yml)
|
|
5
4
|
[](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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
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
|
|
206
|
-
|
|
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](
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
280
|
-
controller (e.g. `
|
|
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`, `
|
|
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
|
-
|
|
360
|
+
rescue_from Banken::NotAuthorizedError, with: :user_not_authorized
|
|
291
361
|
|
|
292
|
-
|
|
362
|
+
private
|
|
293
363
|
|
|
294
|
-
|
|
295
|
-
|
|
364
|
+
def user_not_authorized(exception)
|
|
365
|
+
loyalty_name = exception.loyalty.class.to_s.underscore
|
|
296
366
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
323
|
-
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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?(
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|