durable_parameters 0.2.3
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +853 -0
- data/Rakefile +29 -0
- data/app/params/account_params.rb.example +38 -0
- data/app/params/application_params.rb +16 -0
- data/lib/durable_parameters/adapters/hanami.rb +138 -0
- data/lib/durable_parameters/adapters/rage.rb +124 -0
- data/lib/durable_parameters/adapters/rails.rb +280 -0
- data/lib/durable_parameters/adapters/sinatra.rb +91 -0
- data/lib/durable_parameters/core/application_params.rb +334 -0
- data/lib/durable_parameters/core/configuration.rb +83 -0
- data/lib/durable_parameters/core/forbidden_attributes_protection.rb +48 -0
- data/lib/durable_parameters/core/parameters.rb +643 -0
- data/lib/durable_parameters/core/params_registry.rb +110 -0
- data/lib/durable_parameters/core.rb +15 -0
- data/lib/durable_parameters/log_subscriber.rb +34 -0
- data/lib/durable_parameters/railtie.rb +65 -0
- data/lib/durable_parameters/version.rb +7 -0
- data/lib/durable_parameters.rb +41 -0
- data/lib/generators/rails/USAGE +12 -0
- data/lib/generators/rails/durable_parameters_controller_generator.rb +17 -0
- data/lib/generators/rails/templates/controller.rb +94 -0
- data/lib/legacy/action_controller/application_params.rb +235 -0
- data/lib/legacy/action_controller/parameters.rb +524 -0
- data/lib/legacy/action_controller/params_registry.rb +108 -0
- data/lib/legacy/active_model/forbidden_attributes_protection.rb +40 -0
- data/test/action_controller_required_params_test.rb +36 -0
- data/test/action_controller_tainted_params_test.rb +29 -0
- data/test/active_model_mass_assignment_taint_protection_test.rb +25 -0
- data/test/application_params_array_test.rb +245 -0
- data/test/application_params_edge_cases_test.rb +361 -0
- data/test/application_params_test.rb +893 -0
- data/test/controller_generator_test.rb +31 -0
- data/test/core_parameters_test.rb +2376 -0
- data/test/durable_parameters_test.rb +115 -0
- data/test/enhanced_error_messages_test.rb +120 -0
- data/test/gemfiles/Gemfile.rails-3.0.x +14 -0
- data/test/gemfiles/Gemfile.rails-3.1.x +14 -0
- data/test/gemfiles/Gemfile.rails-3.2.x +14 -0
- data/test/log_on_unpermitted_params_test.rb +49 -0
- data/test/metadata_validation_test.rb +294 -0
- data/test/multi_parameter_attributes_test.rb +38 -0
- data/test/parameters_core_methods_test.rb +503 -0
- data/test/parameters_integration_test.rb +553 -0
- data/test/parameters_permit_test.rb +491 -0
- data/test/parameters_require_test.rb +9 -0
- data/test/parameters_taint_test.rb +98 -0
- data/test/params_registry_concurrency_test.rb +422 -0
- data/test/params_registry_test.rb +112 -0
- data/test/permit_by_model_test.rb +227 -0
- data/test/raise_on_unpermitted_params_test.rb +32 -0
- data/test/test_helper.rb +38 -0
- data/test/transform_params_edge_cases_test.rb +526 -0
- data/test/transformation_test.rb +360 -0
- metadata +223 -0
data/README.md
ADDED
|
@@ -0,0 +1,853 @@
|
|
|
1
|
+
](https://travis-ci.org/durableprogramming/durable_parameters)
|
|
2
|
+
[](http://badge.fury.io/rb/durable_parameters)
|
|
3
|
+
|
|
4
|
+
# Durable Parameters
|
|
5
|
+
|
|
6
|
+
**A customized and opinionated fork of [strong_parameters](https://github.com/rails/strong_parameters)**
|
|
7
|
+
|
|
8
|
+
Durable Parameters provides a robust, flexible approach to parameter filtering for Ruby web applications. It prevents mass-assignment vulnerabilities by requiring explicit whitelisting of attributes.
|
|
9
|
+
|
|
10
|
+
This fork extends the original strong_parameters gem with additional features including declarative params classes, parameter transformations, action-specific permissions, and enhanced framework support.
|
|
11
|
+
|
|
12
|
+
**Framework Support:**
|
|
13
|
+
- **Rails** - Full integration with ActionController and ActiveModel
|
|
14
|
+
- **Sinatra** - Lightweight integration for Sinatra applications
|
|
15
|
+
- **Hanami** - Support for both Hanami 1.x and 2.x
|
|
16
|
+
- **Rage** - Integration with the Rage framework
|
|
17
|
+
- **Standalone** - Can be used without any framework
|
|
18
|
+
|
|
19
|
+
## Key Features
|
|
20
|
+
|
|
21
|
+
- **Explicit Whitelisting**: Parameters must be explicitly permitted before mass assignment
|
|
22
|
+
- **Required Parameters**: Mark parameters as required with automatic 400 Bad Request responses
|
|
23
|
+
- **Declarative Params Classes**: Define permitted attributes in reusable, centralized classes
|
|
24
|
+
- **Action-Specific Permissions**: Configure different permissions for create, update, etc.
|
|
25
|
+
- **Metadata Support**: Pass contextual information (user, IP address, etc.) to params classes
|
|
26
|
+
- **Nested Parameters**: Full support for complex nested parameter structures
|
|
27
|
+
- **Performance Optimized**: Caching and efficient algorithms for production use
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
Add to your Gemfile:
|
|
32
|
+
|
|
33
|
+
``` ruby
|
|
34
|
+
gem 'durable_parameters'
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Then run `bundle install`.
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
### Rails
|
|
42
|
+
|
|
43
|
+
``` ruby
|
|
44
|
+
class PeopleController < ActionController::Base
|
|
45
|
+
# This will raise an ActiveModel::ForbiddenAttributes exception because it's using mass assignment
|
|
46
|
+
# without an explicit permit step.
|
|
47
|
+
def create
|
|
48
|
+
Person.create(params[:person])
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# This will pass with flying colors as long as there's a person key in the parameters, otherwise
|
|
52
|
+
# it'll raise an ActionController::ParameterMissing exception, which will get caught by
|
|
53
|
+
# ActionController::Base and turned into that 400 Bad Request reply.
|
|
54
|
+
def update
|
|
55
|
+
person = current_account.people.find(params[:id])
|
|
56
|
+
person.update_attributes!(person_params)
|
|
57
|
+
redirect_to person
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
# Using a private method to encapsulate the permissible parameters is just a good pattern
|
|
62
|
+
# since you'll be able to reuse the same permit list between create and update. Also, you
|
|
63
|
+
# can specialize this method with per-user checking of permissible attributes.
|
|
64
|
+
def person_params
|
|
65
|
+
params.require(:person).permit(:name, :age)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Sinatra
|
|
71
|
+
|
|
72
|
+
``` ruby
|
|
73
|
+
require 'sinatra/base'
|
|
74
|
+
require 'strong_parameters/adapters/sinatra'
|
|
75
|
+
|
|
76
|
+
class MyApp < Sinatra::Base
|
|
77
|
+
register StrongParameters::Adapters::Sinatra
|
|
78
|
+
|
|
79
|
+
post '/users' do
|
|
80
|
+
user_params = strong_params.require(:user).permit(:name, :email)
|
|
81
|
+
User.create(user_params.to_h)
|
|
82
|
+
redirect '/users'
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Hanami (2.x)
|
|
88
|
+
|
|
89
|
+
``` ruby
|
|
90
|
+
require 'strong_parameters/adapters/hanami'
|
|
91
|
+
|
|
92
|
+
module MyApp
|
|
93
|
+
module Actions
|
|
94
|
+
module Users
|
|
95
|
+
class Create < MyApp::Action
|
|
96
|
+
include StrongParameters::Adapters::Hanami::Action
|
|
97
|
+
|
|
98
|
+
def handle(request, response)
|
|
99
|
+
user_params = strong_params(request.params).require(:user).permit(:name, :email)
|
|
100
|
+
# ... use user_params
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Hanami (1.x)
|
|
109
|
+
|
|
110
|
+
``` ruby
|
|
111
|
+
require 'strong_parameters/adapters/hanami'
|
|
112
|
+
|
|
113
|
+
module Web
|
|
114
|
+
module Controllers
|
|
115
|
+
module Users
|
|
116
|
+
class Create
|
|
117
|
+
include Web::Action
|
|
118
|
+
include StrongParameters::Adapters::Hanami::Action
|
|
119
|
+
|
|
120
|
+
def call(params)
|
|
121
|
+
user_params = strong_params.require(:user).permit(:name, :email)
|
|
122
|
+
# ... use user_params
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Rage
|
|
131
|
+
|
|
132
|
+
``` ruby
|
|
133
|
+
require 'strong_parameters/adapters/rage'
|
|
134
|
+
|
|
135
|
+
class UsersController < RageController::API
|
|
136
|
+
def create
|
|
137
|
+
user_params = params.require(:user).permit(:name, :email)
|
|
138
|
+
User.create(user_params.to_h)
|
|
139
|
+
render json: { success: true }
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Standalone (No Framework)
|
|
145
|
+
|
|
146
|
+
``` ruby
|
|
147
|
+
require 'strong_parameters/core'
|
|
148
|
+
|
|
149
|
+
# Use the core Parameters class directly
|
|
150
|
+
raw_params = { user: { name: 'John', email: 'john@example.com', admin: true } }
|
|
151
|
+
params = StrongParameters::Core::Parameters.new(raw_params)
|
|
152
|
+
|
|
153
|
+
# Require and permit parameters
|
|
154
|
+
user_params = params.require(:user).permit(:name, :email)
|
|
155
|
+
# => {"name"=>"John", "email"=>"john@example.com"}
|
|
156
|
+
|
|
157
|
+
# The :admin parameter was filtered out
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Permitted Scalar Values
|
|
161
|
+
|
|
162
|
+
Given
|
|
163
|
+
|
|
164
|
+
``` ruby
|
|
165
|
+
params.permit(:id)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
the key `:id` will pass the whitelisting if it appears in `params` and it has a permitted scalar value associated. Otherwise the key is going to be filtered out, so arrays, hashes, or any other objects cannot be injected.
|
|
169
|
+
|
|
170
|
+
The permitted scalar types are `String`, `Symbol`, `NilClass`, `Numeric`, `TrueClass`, `FalseClass`, `Date`, `Time`, `DateTime`, `StringIO`, `IO`, `ActionDispatch::Http::UploadedFile` and `Rack::Test::UploadedFile`.
|
|
171
|
+
|
|
172
|
+
To declare that the value in `params` must be an array of permitted scalar values map the key to an empty array:
|
|
173
|
+
|
|
174
|
+
``` ruby
|
|
175
|
+
params.permit(:id => [])
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
To whitelist an entire hash of parameters, the `permit!` method can be used
|
|
179
|
+
|
|
180
|
+
``` ruby
|
|
181
|
+
params.require(:log_entry).permit!
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
This will mark the `:log_entry` parameters hash and any subhash of it permitted. Extreme care should be taken when using `permit!` as it will allow all current and future model attributes to be mass-assigned.
|
|
185
|
+
|
|
186
|
+
## Nested Parameters
|
|
187
|
+
|
|
188
|
+
You can also use permit on nested parameters, like:
|
|
189
|
+
|
|
190
|
+
``` ruby
|
|
191
|
+
params.permit(:name, {:emails => []}, :friends => [ :name, { :family => [ :name ], :hobbies => [] }])
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
This declaration whitelists the `name`, `emails` and `friends` attributes. It is expected that `emails` will be an array of permitted scalar values and that `friends` will be an array of resources with specific attributes : they should have a `name` attribute (any permitted scalar values allowed), a `hobbies` attribute as an array of permitted scalar values, and a `family` attribute which is restricted to having a `name` (any permitted scalar values allowed, too).
|
|
195
|
+
|
|
196
|
+
Thanks to Nick Kallen for the permit idea!
|
|
197
|
+
|
|
198
|
+
## Require Multiple Parameters
|
|
199
|
+
|
|
200
|
+
If you want to make sure that multiple keys are present in a params hash, you can call the method twice:
|
|
201
|
+
|
|
202
|
+
``` ruby
|
|
203
|
+
params.require(:token)
|
|
204
|
+
params.require(:post).permit(:title)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Handling of Unpermitted Keys
|
|
208
|
+
|
|
209
|
+
By default parameter keys that are not explicitly permitted will be logged in the development and test environment. In other environments these parameters will simply be filtered out and ignored.
|
|
210
|
+
|
|
211
|
+
Additionally, this behaviour can be changed by changing the `config.action_controller.action_on_unpermitted_parameters` property in your environment files. If set to `:log` the unpermitted attributes will be logged, if set to `:raise` an exception will be raised.
|
|
212
|
+
|
|
213
|
+
## Use Outside of Controllers
|
|
214
|
+
|
|
215
|
+
While Strong Parameters will enforce permitted and required values in your application controllers, keep in mind
|
|
216
|
+
that you will need to sanitize untrusted data used for mass assignment when in use outside of controllers.
|
|
217
|
+
|
|
218
|
+
For example, if you retrieve JSON data from a third party API call and pass the unchecked parsed result on to
|
|
219
|
+
`Model.create`, undesired mass assignments could take place. You can alleviate this risk by slicing the hash data,
|
|
220
|
+
or wrapping the data in a new instance of `ActionController::Parameters` and declaring permissions the same as
|
|
221
|
+
you would in a controller. For example:
|
|
222
|
+
|
|
223
|
+
``` ruby
|
|
224
|
+
raw_parameters = { :email => "john@example.com", :name => "John", :admin => true }
|
|
225
|
+
parameters = ActionController::Parameters.new(raw_parameters)
|
|
226
|
+
user = User.create(parameters.permit(:name, :email))
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Declarative Parameter Permissions with `app/params/`
|
|
230
|
+
|
|
231
|
+
This enhanced version of Strong Parameters adds a powerful declarative DSL for defining parameter permissions. Instead of inline `permit()` calls scattered throughout your controllers, you can centralize permission logic in reusable params classes.
|
|
232
|
+
|
|
233
|
+
### Why Use Declarative Params?
|
|
234
|
+
|
|
235
|
+
**Before (repetitive and error-prone):**
|
|
236
|
+
``` ruby
|
|
237
|
+
class UsersController < ApplicationController
|
|
238
|
+
def create
|
|
239
|
+
user = User.create(params.require(:user).permit(:name, :email, :bio))
|
|
240
|
+
# ...
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def update
|
|
244
|
+
user = User.find(params[:id])
|
|
245
|
+
user.update_attributes!(params.require(:user).permit(:name, :email, :bio))
|
|
246
|
+
# ...
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**After (DRY and maintainable):**
|
|
252
|
+
``` ruby
|
|
253
|
+
# app/params/user_params.rb
|
|
254
|
+
class UserParams < ApplicationParams
|
|
255
|
+
allow :name
|
|
256
|
+
allow :email
|
|
257
|
+
allow :bio
|
|
258
|
+
deny :is_admin # Explicitly document what's not allowed
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# app/controllers/users_controller.rb
|
|
262
|
+
class UsersController < ApplicationController
|
|
263
|
+
def create
|
|
264
|
+
user = User.create(params.require(:user).transform_params)
|
|
265
|
+
# ...
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def update
|
|
269
|
+
user = User.find(params[:id])
|
|
270
|
+
user.update_attributes!(params.require(:user).transform_params)
|
|
271
|
+
# ...
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Basic Usage
|
|
277
|
+
|
|
278
|
+
**Step 1:** Create a params class in `app/params/account_params.rb`:
|
|
279
|
+
|
|
280
|
+
``` ruby
|
|
281
|
+
class AccountParams < ApplicationParams
|
|
282
|
+
# Explicitly allow attributes
|
|
283
|
+
allow :first_name
|
|
284
|
+
allow :last_name
|
|
285
|
+
allow :email
|
|
286
|
+
|
|
287
|
+
# Explicitly deny sensitive attributes (optional but recommended for documentation)
|
|
288
|
+
deny :is_admin
|
|
289
|
+
deny :role
|
|
290
|
+
end
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
**Step 2:** Use `transform_params` in your controller:
|
|
294
|
+
|
|
295
|
+
``` ruby
|
|
296
|
+
class AccountsController < ApplicationController
|
|
297
|
+
def update
|
|
298
|
+
account = Account.find(params[:id])
|
|
299
|
+
# Automatically infers AccountParams from :account key
|
|
300
|
+
# Permits: :first_name, :last_name, :email
|
|
301
|
+
# Denies: :is_admin, :role, and any other attributes
|
|
302
|
+
account.update_attributes!(params.require(:account).transform_params)
|
|
303
|
+
redirect_to account
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Multiple Params Classes
|
|
309
|
+
|
|
310
|
+
You can define multiple params classes for different use cases:
|
|
311
|
+
|
|
312
|
+
``` ruby
|
|
313
|
+
# app/params/account_params.rb
|
|
314
|
+
class AccountParams < ApplicationParams
|
|
315
|
+
allow :first_name
|
|
316
|
+
allow :last_name
|
|
317
|
+
allow :email
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# app/params/admin_account_params.rb
|
|
321
|
+
class AdminAccountParams < ApplicationParams
|
|
322
|
+
allow :first_name
|
|
323
|
+
allow :last_name
|
|
324
|
+
allow :email
|
|
325
|
+
allow :is_admin # Admins can modify admin status
|
|
326
|
+
allow :role
|
|
327
|
+
end
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Then explicitly specify which to use:
|
|
331
|
+
|
|
332
|
+
``` ruby
|
|
333
|
+
class AccountsController < ApplicationController
|
|
334
|
+
def update
|
|
335
|
+
account = Account.find(params[:id])
|
|
336
|
+
|
|
337
|
+
# Regular users use AccountParams
|
|
338
|
+
if current_user.admin?
|
|
339
|
+
account.update_attributes!(params.require(:account).transform_params(AdminAccountParams))
|
|
340
|
+
else
|
|
341
|
+
account.update_attributes!(params.require(:account).transform_params)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
redirect_to account
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Passing Metadata for Transformation
|
|
350
|
+
|
|
351
|
+
`transform_params` accepts metadata that can be used by custom params classes for advanced transformation logic. This enables context-aware parameter processing.
|
|
352
|
+
|
|
353
|
+
#### Current User
|
|
354
|
+
|
|
355
|
+
`current_user` is always accepted and doesn't need to be declared:
|
|
356
|
+
|
|
357
|
+
``` ruby
|
|
358
|
+
class AccountsController < ApplicationController
|
|
359
|
+
def update
|
|
360
|
+
account = Account.find(params[:id])
|
|
361
|
+
# current_user is always allowed
|
|
362
|
+
account.update_attributes!(
|
|
363
|
+
params.require(:account).transform_params(current_user: current_user)
|
|
364
|
+
)
|
|
365
|
+
redirect_to account
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
#### Declaring Additional Metadata
|
|
371
|
+
|
|
372
|
+
To pass other metadata keys, you must explicitly declare them in your params class using the `metadata` DSL:
|
|
373
|
+
|
|
374
|
+
``` ruby
|
|
375
|
+
class AccountParams < ApplicationParams
|
|
376
|
+
allow :first_name
|
|
377
|
+
allow :last_name
|
|
378
|
+
allow :email
|
|
379
|
+
|
|
380
|
+
# Declare which metadata keys this params class accepts
|
|
381
|
+
metadata :ip_address, :role
|
|
382
|
+
end
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Now you can pass these declared metadata keys:
|
|
386
|
+
|
|
387
|
+
``` ruby
|
|
388
|
+
class AccountsController < ApplicationController
|
|
389
|
+
def update
|
|
390
|
+
account = Account.find(params[:id])
|
|
391
|
+
account.update_attributes!(
|
|
392
|
+
params.require(:account).transform_params(
|
|
393
|
+
current_user: current_user, # Always allowed
|
|
394
|
+
ip_address: request.ip, # Must be declared
|
|
395
|
+
role: current_user.role # Must be declared
|
|
396
|
+
)
|
|
397
|
+
)
|
|
398
|
+
redirect_to account
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
**Important:** If you try to pass metadata that hasn't been declared, `transform_params` will raise an `ArgumentError` with a helpful message telling you which metadata key to declare.
|
|
404
|
+
|
|
405
|
+
**Note:** Metadata is validated and can be used by transformations for dynamic parameter processing.
|
|
406
|
+
|
|
407
|
+
### Parameter Transformations
|
|
408
|
+
|
|
409
|
+
You can define transformations that modify parameter values before they are filtered. Transformations receive the current value and metadata (like `current_user`, `action`, etc.), allowing for context-aware processing:
|
|
410
|
+
|
|
411
|
+
``` ruby
|
|
412
|
+
class UserParams < ApplicationParams
|
|
413
|
+
allow :email
|
|
414
|
+
allow :role
|
|
415
|
+
allow :username
|
|
416
|
+
|
|
417
|
+
metadata :current_user # Declare metadata that transformations can access
|
|
418
|
+
|
|
419
|
+
# Normalize email to lowercase
|
|
420
|
+
transform :email do |value, metadata|
|
|
421
|
+
value&.downcase&.strip
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Enforce role based on current user's permissions
|
|
425
|
+
transform :role do |value, metadata|
|
|
426
|
+
if metadata[:current_user]&.admin?
|
|
427
|
+
value # Admins can set any role
|
|
428
|
+
else
|
|
429
|
+
'user' # Non-admins always get 'user' role
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
# Sanitize username
|
|
434
|
+
transform :username do |value, metadata|
|
|
435
|
+
value&.strip&.gsub(/\s+/, '_')
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Using transformations in your controller:
|
|
441
|
+
|
|
442
|
+
``` ruby
|
|
443
|
+
class UsersController < ApplicationController
|
|
444
|
+
def create
|
|
445
|
+
user = User.create(
|
|
446
|
+
params.require(:user).transform_params(current_user: current_user)
|
|
447
|
+
)
|
|
448
|
+
redirect_to user
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
**How it works:**
|
|
454
|
+
1. Transformations are applied first, modifying parameter values
|
|
455
|
+
2. Then filtering occurs based on allowed/denied attributes
|
|
456
|
+
3. Action-specific permissions are respected
|
|
457
|
+
4. Metadata must be declared (except `current_user` which is always allowed)
|
|
458
|
+
|
|
459
|
+
### Action-Specific Permissions
|
|
460
|
+
|
|
461
|
+
Different actions often need different permissions. For example, you might want to allow setting a `published` flag only when creating or updating, but not when doing other operations. Use `:only` and `:except` options for fine-grained control:
|
|
462
|
+
|
|
463
|
+
``` ruby
|
|
464
|
+
class PostParams < ApplicationParams
|
|
465
|
+
# Always allowed
|
|
466
|
+
allow :title
|
|
467
|
+
allow :body
|
|
468
|
+
|
|
469
|
+
# Only allowed for create and update actions
|
|
470
|
+
allow :published, only: [:create, :update]
|
|
471
|
+
|
|
472
|
+
# Allowed for all actions except create
|
|
473
|
+
allow :view_count, except: :create
|
|
474
|
+
|
|
475
|
+
# Only allowed for a single action
|
|
476
|
+
allow :featured, only: :publish
|
|
477
|
+
end
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
Specify the action in your controller:
|
|
481
|
+
|
|
482
|
+
``` ruby
|
|
483
|
+
class PostsController < ApplicationController
|
|
484
|
+
def create
|
|
485
|
+
# Permits: :title, :body, :published
|
|
486
|
+
# Denies: :view_count (except: :create)
|
|
487
|
+
post = Post.create(params.require(:post).transform_params(action: :create))
|
|
488
|
+
redirect_to post
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def update
|
|
492
|
+
post = Post.find(params[:id])
|
|
493
|
+
# Permits: :title, :body, :published, :view_count
|
|
494
|
+
post.update_attributes!(params.require(:post).transform_params(action: :update))
|
|
495
|
+
redirect_to post
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def publish
|
|
499
|
+
post = Post.find(params[:id])
|
|
500
|
+
# Permits: :title, :body, :view_count, :featured
|
|
501
|
+
# Denies: :published (not in only: :publish)
|
|
502
|
+
post.update_attributes!(params.require(:post).transform_params(action: :publish))
|
|
503
|
+
redirect_to post
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
**Benefits:**
|
|
509
|
+
- Single source of truth for all action permissions
|
|
510
|
+
- Clear documentation of what's allowed where
|
|
511
|
+
- Prevents accidental exposure of sensitive fields in specific contexts
|
|
512
|
+
|
|
513
|
+
### Flags
|
|
514
|
+
|
|
515
|
+
You can set custom flags on your params classes for application-specific logic:
|
|
516
|
+
|
|
517
|
+
``` ruby
|
|
518
|
+
class AccountParams < ApplicationParams
|
|
519
|
+
allow :name
|
|
520
|
+
allow :description
|
|
521
|
+
|
|
522
|
+
flag :require_approval, true
|
|
523
|
+
flag :audit_changes, true
|
|
524
|
+
end
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
Check flags programmatically:
|
|
528
|
+
|
|
529
|
+
``` ruby
|
|
530
|
+
AccountParams.flag?(:require_approval) # => true
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### Additional Attributes
|
|
534
|
+
|
|
535
|
+
You can permit additional attributes beyond those defined in the params class:
|
|
536
|
+
|
|
537
|
+
``` ruby
|
|
538
|
+
# In your controller
|
|
539
|
+
params.require(:user).transform_params(additional_attrs: [:temporary_token])
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### Inheritance
|
|
543
|
+
|
|
544
|
+
Params classes support inheritance, allowing you to build on existing definitions:
|
|
545
|
+
|
|
546
|
+
``` ruby
|
|
547
|
+
class ApplicationParams < ActionController::ApplicationParams
|
|
548
|
+
# Common attributes for all models
|
|
549
|
+
allow :created_at
|
|
550
|
+
allow :updated_at
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
class UserParams < ApplicationParams
|
|
554
|
+
# Inherits :created_at and :updated_at
|
|
555
|
+
allow :name
|
|
556
|
+
allow :email
|
|
557
|
+
end
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
### Checking Permissions
|
|
561
|
+
|
|
562
|
+
You can query the params classes directly to check permissions:
|
|
563
|
+
|
|
564
|
+
``` ruby
|
|
565
|
+
UserParams.allowed?(:email) # => true
|
|
566
|
+
UserParams.denied?(:is_admin) # => true
|
|
567
|
+
UserParams.permitted_attributes # => [:name, :email, :created_at, :updated_at]
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### Registry
|
|
571
|
+
|
|
572
|
+
All params classes are automatically registered and can be looked up:
|
|
573
|
+
|
|
574
|
+
``` ruby
|
|
575
|
+
ActionController::ParamsRegistry.lookup(:user) # => UserParams
|
|
576
|
+
ActionController::ParamsRegistry.registered?(:user) # => true
|
|
577
|
+
ActionController::ParamsRegistry.permitted_attributes_for(:user) # => [:name, :email, ...]
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
## Architecture
|
|
581
|
+
|
|
582
|
+
This gem is built with a modular architecture that separates core functionality from framework-specific integrations:
|
|
583
|
+
|
|
584
|
+
### Core Module (`StrongParameters::Core`)
|
|
585
|
+
|
|
586
|
+
The core module provides framework-agnostic classes:
|
|
587
|
+
- **`Parameters`** - Hash-based parameter filtering and whitelisting
|
|
588
|
+
- **`ApplicationParams`** - Declarative DSL for defining parameter permissions
|
|
589
|
+
- **`ParamsRegistry`** - Registry for looking up params classes
|
|
590
|
+
- **`ForbiddenAttributesProtection`** - Mass assignment protection
|
|
591
|
+
|
|
592
|
+
The core has zero dependencies and can be used standalone.
|
|
593
|
+
|
|
594
|
+
### Framework Adapters
|
|
595
|
+
|
|
596
|
+
Each adapter extends the core with framework-specific features:
|
|
597
|
+
|
|
598
|
+
- **Rails Adapter** (`StrongParameters::Adapters::Rails`)
|
|
599
|
+
- Integrates with ActionController and ActiveModel
|
|
600
|
+
- Provides HashWithIndifferentAccess behavior
|
|
601
|
+
- Supports uploaded files (ActionDispatch, Rack)
|
|
602
|
+
- Auto-loads params classes from `app/params/`
|
|
603
|
+
|
|
604
|
+
- **Sinatra Adapter** (`StrongParameters::Adapters::Sinatra`)
|
|
605
|
+
- Provides `strong_params` helper method
|
|
606
|
+
- Automatic error handling (400 Bad Request)
|
|
607
|
+
- Logging support in development mode
|
|
608
|
+
|
|
609
|
+
- **Hanami Adapter** (`StrongParameters::Adapters::Hanami`)
|
|
610
|
+
- Supports both Hanami 1.x and 2.x
|
|
611
|
+
- Provides `strong_params` helper for actions
|
|
612
|
+
- Integrates with Hanami's error handling
|
|
613
|
+
|
|
614
|
+
- **Rage Adapter** (`StrongParameters::Adapters::Rage`)
|
|
615
|
+
- Rails-compatible API for Rage framework
|
|
616
|
+
- Automatic controller integration
|
|
617
|
+
- Error handling via `rescue_from`
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
Note that these adapters are still WIP, so please open a bug report with any bugs you find.
|
|
621
|
+
|
|
622
|
+
### Auto-Detection
|
|
623
|
+
|
|
624
|
+
The gem automatically detects which framework is loaded and activates the appropriate adapter. You can also manually require specific adapters.
|
|
625
|
+
|
|
626
|
+
## More Examples
|
|
627
|
+
|
|
628
|
+
Head over to the [Rails guide about Action Controller](http://guides.rubyonrails.org/action_controller_overview.html#more-examples).
|
|
629
|
+
|
|
630
|
+
## Framework-Specific Setup
|
|
631
|
+
|
|
632
|
+
### Rails Setup
|
|
633
|
+
|
|
634
|
+
To activate strong parameters protection in Rails models:
|
|
635
|
+
|
|
636
|
+
``` ruby
|
|
637
|
+
class Post < ActiveRecord::Base
|
|
638
|
+
include ActiveModel::ForbiddenAttributesProtection
|
|
639
|
+
end
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
Alternatively, protect all Active Record resources globally in an initializer:
|
|
643
|
+
|
|
644
|
+
``` ruby
|
|
645
|
+
# config/initializers/durable_parameters.rb
|
|
646
|
+
ActiveRecord::Base.send(:include, ActiveModel::ForbiddenAttributesProtection)
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
For Rails 3.2, disable the default whitelisting in `config/application.rb`:
|
|
650
|
+
|
|
651
|
+
``` ruby
|
|
652
|
+
config.active_record.whitelist_attributes = false
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
This allows you to remove `attr_accessible` and use strong parameters throughout your code.
|
|
656
|
+
|
|
657
|
+
### Sinatra Setup
|
|
658
|
+
|
|
659
|
+
Automatic setup when you register the adapter:
|
|
660
|
+
|
|
661
|
+
``` ruby
|
|
662
|
+
class MyApp < Sinatra::Base
|
|
663
|
+
register StrongParameters::Adapters::Sinatra
|
|
664
|
+
end
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
Or include in Sinatra classic style:
|
|
668
|
+
|
|
669
|
+
``` ruby
|
|
670
|
+
require 'sinatra'
|
|
671
|
+
require 'strong_parameters/adapters/sinatra'
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
### Hanami Setup
|
|
675
|
+
|
|
676
|
+
Setup is automatic when you include the Action module:
|
|
677
|
+
|
|
678
|
+
``` ruby
|
|
679
|
+
# Hanami 2.x - include in your actions
|
|
680
|
+
include StrongParameters::Adapters::Hanami::Action
|
|
681
|
+
|
|
682
|
+
# Or setup globally in config/app.rb
|
|
683
|
+
StrongParameters::Adapters::Hanami.setup!(Hanami.app)
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
### Rage Setup
|
|
687
|
+
|
|
688
|
+
Setup happens automatically when the adapter is loaded:
|
|
689
|
+
|
|
690
|
+
``` ruby
|
|
691
|
+
require 'strong_parameters/adapters/rage'
|
|
692
|
+
# Controllers will automatically have strong parameters support
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
### Standalone Setup
|
|
696
|
+
|
|
697
|
+
No setup required - just use the core classes:
|
|
698
|
+
|
|
699
|
+
``` ruby
|
|
700
|
+
require 'strong_parameters/core'
|
|
701
|
+
|
|
702
|
+
# Define params classes
|
|
703
|
+
class UserParams < StrongParameters::Core::ApplicationParams
|
|
704
|
+
allow :name
|
|
705
|
+
allow :email
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
# Register params classes
|
|
709
|
+
StrongParameters::Core::ParamsRegistry.register(:user, UserParams)
|
|
710
|
+
|
|
711
|
+
# Use parameters
|
|
712
|
+
params = StrongParameters::Core::Parameters.new(raw_hash)
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
## Migration Path to Rails 4
|
|
716
|
+
|
|
717
|
+
In order to have an idiomatic Rails 4 application, Rails 3 applications may
|
|
718
|
+
use this gem to introduce strong parameters in preparation for their upgrade.
|
|
719
|
+
|
|
720
|
+
The following is a way to do that gradually:
|
|
721
|
+
|
|
722
|
+
### 1 Depend on `durable_parameters`
|
|
723
|
+
|
|
724
|
+
Add this gem to the application `Gemfile`:
|
|
725
|
+
|
|
726
|
+
``` ruby
|
|
727
|
+
gem 'durable_parameters'
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
and run `bundle install`.
|
|
731
|
+
|
|
732
|
+
After this change, the `params` object in requests is of type
|
|
733
|
+
`ActionController::Parameters`. That is a subclass of
|
|
734
|
+
`ActiveSupport::HashWithIndifferentAccess` and therefore everything should
|
|
735
|
+
work as before. The test suite should be green, and the application can be
|
|
736
|
+
deployed.
|
|
737
|
+
|
|
738
|
+
### 2 Compute a Topological Sort of Active Record Models
|
|
739
|
+
|
|
740
|
+
We are going to work model by model, and the natural order to do that
|
|
741
|
+
systematically is topological. That is, if post has many comments, first you
|
|
742
|
+
do `Post`, and later you do `Comment`.
|
|
743
|
+
|
|
744
|
+
Reason is that order plays well with nested attributes. You can mass-assign
|
|
745
|
+
`ActionController::Parameters` to `Post`, and if that includes
|
|
746
|
+
`comments_attributes` and the `Comment` model is not yet done, it will work.
|
|
747
|
+
But if `Comment` is done first, then the mass-assigning to `Post` won't permit
|
|
748
|
+
its attributes and won't work.
|
|
749
|
+
|
|
750
|
+
This script prints a topological sort of the Active Record models to standard
|
|
751
|
+
output:
|
|
752
|
+
|
|
753
|
+
```ruby
|
|
754
|
+
require 'tsort'
|
|
755
|
+
require 'set'
|
|
756
|
+
|
|
757
|
+
class Graph < Hash
|
|
758
|
+
include TSort
|
|
759
|
+
|
|
760
|
+
alias tsort_each_node each_key
|
|
761
|
+
|
|
762
|
+
def tsort_each_child(node, &block)
|
|
763
|
+
fetch(node).each(&block)
|
|
764
|
+
end
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
def children(model)
|
|
768
|
+
Set.new.tap do |children|
|
|
769
|
+
model.reflect_on_all_associations.each do |association|
|
|
770
|
+
next unless [:has_many, :has_one].include?(association.macro)
|
|
771
|
+
next if association.options[:through]
|
|
772
|
+
|
|
773
|
+
children << association.klass
|
|
774
|
+
end
|
|
775
|
+
end
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
Dir.glob('app/models/**/*.rb') do |model|
|
|
779
|
+
load model
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
graph = Graph.new
|
|
783
|
+
ActiveRecord::Base.descendants.each do |model|
|
|
784
|
+
graph[model] = children(model) unless model.abstract_class?
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
graph.tsort.reverse_each do |klass|
|
|
788
|
+
puts klass.name
|
|
789
|
+
end
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
Execute it with `rails runner`.
|
|
793
|
+
|
|
794
|
+
### 3 Protect Every Active Record Model, One at a Time
|
|
795
|
+
|
|
796
|
+
Once the dependency is in place and the topological listing computed, you can
|
|
797
|
+
work model by model. Do one model, deploy. Do another model, deploy. Etc.
|
|
798
|
+
|
|
799
|
+
For each model:
|
|
800
|
+
|
|
801
|
+
#### 3.1 Add Protection
|
|
802
|
+
|
|
803
|
+
Remove any `attr_accessible` or `attr_protected` declarations and include
|
|
804
|
+
`ActiveModel::ForbiddenAttributesProtection`:
|
|
805
|
+
|
|
806
|
+
``` ruby
|
|
807
|
+
class Post < ActiveRecord::Base
|
|
808
|
+
include ActiveModel::ForbiddenAttributesProtection
|
|
809
|
+
end
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
#### 3.2 (Optional) Check the Suite is Red
|
|
813
|
+
|
|
814
|
+
If the application performs any mass-assignment into that model, the test
|
|
815
|
+
suite should not pass. Expect the test suite to raise
|
|
816
|
+
`ActiveModel::ForbiddenAttributes` in those spots.
|
|
817
|
+
|
|
818
|
+
If the test suite is green, either it lacks coverage (fix it), or there is no
|
|
819
|
+
mass-assignment going on (ready to deploy).
|
|
820
|
+
|
|
821
|
+
#### 3.3 Whitelisting
|
|
822
|
+
|
|
823
|
+
Go to every controller whose actions trigger mass-assignment on that model via
|
|
824
|
+
`params` and sanitize the input data using `require` and `permit`, as
|
|
825
|
+
explained above.
|
|
826
|
+
|
|
827
|
+
#### 3.4 Deploy
|
|
828
|
+
|
|
829
|
+
Once everything is whitelisted and the suite is green, this particular model
|
|
830
|
+
can be pushed.
|
|
831
|
+
|
|
832
|
+
Ready to work on the next model.
|
|
833
|
+
|
|
834
|
+
### 4 Add Protection Globally
|
|
835
|
+
|
|
836
|
+
Once all models are done, remove their inclusion of the protecting module:
|
|
837
|
+
|
|
838
|
+
``` ruby
|
|
839
|
+
class Post < ActiveRecord::Base
|
|
840
|
+
# REMOVE THIS LINE IN EVERY PERSISTENT MODEL
|
|
841
|
+
include ActiveModel::ForbiddenAttributesProtection
|
|
842
|
+
end
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
and add it globally in an initializer:
|
|
846
|
+
|
|
847
|
+
``` ruby
|
|
848
|
+
# config/initializers/durable_parameters.rb
|
|
849
|
+
ActiveRecord::Base.class_eval do
|
|
850
|
+
include ActiveModel::ForbiddenAttributesProtection
|
|
851
|
+
end
|
|
852
|
+
```
|
|
853
|
+
|