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.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +853 -0
  4. data/Rakefile +29 -0
  5. data/app/params/account_params.rb.example +38 -0
  6. data/app/params/application_params.rb +16 -0
  7. data/lib/durable_parameters/adapters/hanami.rb +138 -0
  8. data/lib/durable_parameters/adapters/rage.rb +124 -0
  9. data/lib/durable_parameters/adapters/rails.rb +280 -0
  10. data/lib/durable_parameters/adapters/sinatra.rb +91 -0
  11. data/lib/durable_parameters/core/application_params.rb +334 -0
  12. data/lib/durable_parameters/core/configuration.rb +83 -0
  13. data/lib/durable_parameters/core/forbidden_attributes_protection.rb +48 -0
  14. data/lib/durable_parameters/core/parameters.rb +643 -0
  15. data/lib/durable_parameters/core/params_registry.rb +110 -0
  16. data/lib/durable_parameters/core.rb +15 -0
  17. data/lib/durable_parameters/log_subscriber.rb +34 -0
  18. data/lib/durable_parameters/railtie.rb +65 -0
  19. data/lib/durable_parameters/version.rb +7 -0
  20. data/lib/durable_parameters.rb +41 -0
  21. data/lib/generators/rails/USAGE +12 -0
  22. data/lib/generators/rails/durable_parameters_controller_generator.rb +17 -0
  23. data/lib/generators/rails/templates/controller.rb +94 -0
  24. data/lib/legacy/action_controller/application_params.rb +235 -0
  25. data/lib/legacy/action_controller/parameters.rb +524 -0
  26. data/lib/legacy/action_controller/params_registry.rb +108 -0
  27. data/lib/legacy/active_model/forbidden_attributes_protection.rb +40 -0
  28. data/test/action_controller_required_params_test.rb +36 -0
  29. data/test/action_controller_tainted_params_test.rb +29 -0
  30. data/test/active_model_mass_assignment_taint_protection_test.rb +25 -0
  31. data/test/application_params_array_test.rb +245 -0
  32. data/test/application_params_edge_cases_test.rb +361 -0
  33. data/test/application_params_test.rb +893 -0
  34. data/test/controller_generator_test.rb +31 -0
  35. data/test/core_parameters_test.rb +2376 -0
  36. data/test/durable_parameters_test.rb +115 -0
  37. data/test/enhanced_error_messages_test.rb +120 -0
  38. data/test/gemfiles/Gemfile.rails-3.0.x +14 -0
  39. data/test/gemfiles/Gemfile.rails-3.1.x +14 -0
  40. data/test/gemfiles/Gemfile.rails-3.2.x +14 -0
  41. data/test/log_on_unpermitted_params_test.rb +49 -0
  42. data/test/metadata_validation_test.rb +294 -0
  43. data/test/multi_parameter_attributes_test.rb +38 -0
  44. data/test/parameters_core_methods_test.rb +503 -0
  45. data/test/parameters_integration_test.rb +553 -0
  46. data/test/parameters_permit_test.rb +491 -0
  47. data/test/parameters_require_test.rb +9 -0
  48. data/test/parameters_taint_test.rb +98 -0
  49. data/test/params_registry_concurrency_test.rb +422 -0
  50. data/test/params_registry_test.rb +112 -0
  51. data/test/permit_by_model_test.rb +227 -0
  52. data/test/raise_on_unpermitted_params_test.rb +32 -0
  53. data/test/test_helper.rb +38 -0
  54. data/test/transform_params_edge_cases_test.rb +526 -0
  55. data/test/transformation_test.rb +360 -0
  56. metadata +223 -0
data/README.md ADDED
@@ -0,0 +1,853 @@
1
+ ![Build Status](https://travis-ci.org/durableprogramming/durable_parameters.svg?branch=master)](https://travis-ci.org/durableprogramming/durable_parameters)
2
+ [![Gem Version](https://badge.fury.io/rb/durable_parameters.svg)](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
+