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 +5 -5
- data/CHANGELOG.md +185 -0
- data/README.md +211 -55
- 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 +27 -9
- data/lib/banken/railtie.rb +9 -0
- data/lib/banken/version.rb +3 -1
- data/lib/banken.rb +11 -65
- data/lib/generators/banken/install/install_generator.rb +3 -1
- data/lib/generators/banken/loyalty/loyalty_generator.rb +3 -3
- data/lib/generators/banken/loyalty/templates/{loyalty.rb → loyalty.rb.tt} +0 -0
- metadata +30 -47
- 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
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
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
|
-
[](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,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
|
|
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
|
|
196
|
-
|
|
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](
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
270
|
-
controller (e.g. `
|
|
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`, `
|
|
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
|
-
|
|
360
|
+
rescue_from Banken::NotAuthorizedError, with: :user_not_authorized
|
|
281
361
|
|
|
282
|
-
|
|
362
|
+
private
|
|
283
363
|
|
|
284
|
-
|
|
285
|
-
|
|
364
|
+
def user_not_authorized(exception)
|
|
365
|
+
loyalty_name = exception.loyalty.class.to_s.underscore
|
|
286
366
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
313
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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?(
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|