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
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StrongParameters
|
|
4
|
+
module Core
|
|
5
|
+
# Singleton registry for storing and retrieving param class definitions.
|
|
6
|
+
#
|
|
7
|
+
# ParamsRegistry provides a central location to register and look up params
|
|
8
|
+
# classes for models. This enables automatic inference of params classes in
|
|
9
|
+
# transform_params based on the model name.
|
|
10
|
+
#
|
|
11
|
+
# @example Registering a params class
|
|
12
|
+
# StrongParameters::Core::ParamsRegistry.register(:user, UserParams)
|
|
13
|
+
#
|
|
14
|
+
# @example Looking up a params class
|
|
15
|
+
# StrongParameters::Core::ParamsRegistry.lookup(:user) # => UserParams
|
|
16
|
+
#
|
|
17
|
+
# @example Getting permitted attributes
|
|
18
|
+
# StrongParameters::Core::ParamsRegistry.permitted_attributes_for(:user, action: :create)
|
|
19
|
+
class ParamsRegistry
|
|
20
|
+
class << self
|
|
21
|
+
# Register a params class for a model.
|
|
22
|
+
#
|
|
23
|
+
# The model name is normalized (underscored and symbolized) before storage.
|
|
24
|
+
#
|
|
25
|
+
# @param model_name [String, Symbol] the model name (e.g., 'User', 'Account')
|
|
26
|
+
# @param params_class [Class] the params class (e.g., UserParams)
|
|
27
|
+
# @return [Class] the registered params class
|
|
28
|
+
# @example
|
|
29
|
+
# ParamsRegistry.register(:user, UserParams)
|
|
30
|
+
# ParamsRegistry.register('BlogPost', BlogPostParams)
|
|
31
|
+
def register(model_name, params_class)
|
|
32
|
+
registry[normalize_key(model_name)] = params_class
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Look up the params class for a model.
|
|
36
|
+
#
|
|
37
|
+
# @param model_name [String, Symbol] the model name
|
|
38
|
+
# @return [Class, nil] the params class or nil if not found
|
|
39
|
+
# @example
|
|
40
|
+
# ParamsRegistry.lookup(:user) # => UserParams
|
|
41
|
+
# ParamsRegistry.lookup(:unknown) # => nil
|
|
42
|
+
def lookup(model_name)
|
|
43
|
+
registry[normalize_key(model_name)]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Get permitted attributes for a model.
|
|
47
|
+
#
|
|
48
|
+
# @param model_name [String, Symbol] the model name
|
|
49
|
+
# @param action [Symbol, String, nil] optional action name for filtering
|
|
50
|
+
# @return [Array<Symbol, Hash>] array of permitted attributes
|
|
51
|
+
# @example
|
|
52
|
+
# ParamsRegistry.permitted_attributes_for(:user)
|
|
53
|
+
# ParamsRegistry.permitted_attributes_for(:post, action: :create)
|
|
54
|
+
def permitted_attributes_for(model_name, action: nil)
|
|
55
|
+
params_class = lookup(model_name)
|
|
56
|
+
return [] unless params_class
|
|
57
|
+
|
|
58
|
+
params_class.permitted_attributes(action: action)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Check if a model has registered params.
|
|
62
|
+
#
|
|
63
|
+
# @param model_name [String, Symbol] the model name
|
|
64
|
+
# @return [Boolean] true if registered, false otherwise
|
|
65
|
+
# @example
|
|
66
|
+
# ParamsRegistry.registered?(:user) # => true
|
|
67
|
+
# ParamsRegistry.registered?(:unknown) # => false
|
|
68
|
+
def registered?(model_name)
|
|
69
|
+
registry.key?(normalize_key(model_name))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Clear the registry.
|
|
73
|
+
#
|
|
74
|
+
# This is primarily useful for testing to ensure a clean slate between tests.
|
|
75
|
+
#
|
|
76
|
+
# @return [Hash] the empty registry
|
|
77
|
+
def clear!
|
|
78
|
+
registry.clear
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Get all registered model names.
|
|
82
|
+
#
|
|
83
|
+
# @return [Array<String>] array of model names as strings
|
|
84
|
+
# @example
|
|
85
|
+
# ParamsRegistry.registered_models # => ["user", "post", "comment"]
|
|
86
|
+
def registered_models
|
|
87
|
+
registry.keys.map(&:to_s)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
# @return [Hash<Symbol, Class>] the internal registry hash
|
|
93
|
+
def registry
|
|
94
|
+
@registry ||= {}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Normalize a key by underscoring and symbolizing it.
|
|
98
|
+
#
|
|
99
|
+
# @param key [String, Symbol] the key to normalize
|
|
100
|
+
# @return [Symbol] the normalized key
|
|
101
|
+
# @example
|
|
102
|
+
# normalize_key('BlogPost') # => :blog_post
|
|
103
|
+
# normalize_key(:user) # => :user
|
|
104
|
+
def normalize_key(key)
|
|
105
|
+
key.to_s.gsub(/([A-Z])/) { "_#{$1.downcase}" }.gsub(/^_/, '').to_sym
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Core framework-agnostic strong parameters implementation
|
|
4
|
+
#
|
|
5
|
+
# The core module provides all essential functionality without any framework dependencies:
|
|
6
|
+
# - Parameters: Hash-based parameter filtering and whitelisting
|
|
7
|
+
# - ApplicationParams: Declarative DSL for defining parameter permissions
|
|
8
|
+
# - ParamsRegistry: Centralized registry for params classes
|
|
9
|
+
# - ForbiddenAttributesProtection: Mass assignment protection mixin
|
|
10
|
+
# - Configuration: Centralized configuration management
|
|
11
|
+
require 'durable_parameters/core/configuration'
|
|
12
|
+
require 'durable_parameters/core/parameters'
|
|
13
|
+
require 'durable_parameters/core/application_params'
|
|
14
|
+
require 'durable_parameters/core/params_registry'
|
|
15
|
+
require 'durable_parameters/core/forbidden_attributes_protection'
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/log_subscriber'
|
|
4
|
+
require 'active_support/notifications'
|
|
5
|
+
|
|
6
|
+
module StrongParameters
|
|
7
|
+
# Log subscriber for unpermitted parameters notifications.
|
|
8
|
+
#
|
|
9
|
+
# This subscriber listens for unpermitted parameter events and logs them
|
|
10
|
+
# using the configured logger. This is helpful for development and testing
|
|
11
|
+
# to identify parameters that need to be explicitly permitted.
|
|
12
|
+
class LogSubscriber < ActiveSupport::LogSubscriber
|
|
13
|
+
# Handle unpermitted_parameters notification event.
|
|
14
|
+
#
|
|
15
|
+
# @param event [ActiveSupport::Notifications::Event] the notification event
|
|
16
|
+
# @return [void]
|
|
17
|
+
def unpermitted_parameters(event)
|
|
18
|
+
unpermitted_keys = event.payload[:keys]
|
|
19
|
+
debug("Unpermitted parameters: #{unpermitted_keys.join(', ')}")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Returns the logger for this subscriber.
|
|
23
|
+
#
|
|
24
|
+
# @return [Logger] the Action Controller logger
|
|
25
|
+
def logger
|
|
26
|
+
ActionController::Base.logger
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Only attach if ActionController is loaded (Rails environment)
|
|
32
|
+
if defined?(ActionController)
|
|
33
|
+
StrongParameters::LogSubscriber.attach_to :action_controller
|
|
34
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/railtie'
|
|
4
|
+
require 'durable_parameters/adapters/rails'
|
|
5
|
+
|
|
6
|
+
module StrongParameters
|
|
7
|
+
# Rails integration for Strong Parameters.
|
|
8
|
+
#
|
|
9
|
+
# This railtie configures Strong Parameters within Rails applications,
|
|
10
|
+
# setting up generators, autoload paths, and parameter logging.
|
|
11
|
+
class Railtie < ::Rails::Railtie
|
|
12
|
+
# Setup Rails adapter
|
|
13
|
+
config.before_initialize do
|
|
14
|
+
StrongParameters::Adapters::Rails.setup!
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Configure scaffold generator to use strong_parameters controller template
|
|
18
|
+
if config.respond_to?(:app_generators)
|
|
19
|
+
config.app_generators.scaffold_controller = :strong_parameters_controller
|
|
20
|
+
else
|
|
21
|
+
config.generators.scaffold_controller = :strong_parameters_controller
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Configure action on unpermitted parameters (log in dev/test, silent in production)
|
|
25
|
+
initializer 'strong_parameters.config', before: 'action_controller.set_configs' do |app|
|
|
26
|
+
StrongParameters::Adapters::Rails::Parameters.action_on_unpermitted_parameters =
|
|
27
|
+
app.config.action_controller.delete(:action_on_unpermitted_parameters) do
|
|
28
|
+
(Rails.env.test? || Rails.env.development?) ? :log : false
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Add app/params directory to autoload paths for params classes
|
|
33
|
+
initializer 'strong_parameters.autoload_params' do |app|
|
|
34
|
+
params_path = app.root.join('app', 'params')
|
|
35
|
+
|
|
36
|
+
# Add to autoload paths if directory exists
|
|
37
|
+
if params_path.directory?
|
|
38
|
+
ActiveSupport::Dependencies.autoload_paths << params_path.to_s
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Automatically load and register all params classes after Rails initialization
|
|
43
|
+
config.after_initialize do |app|
|
|
44
|
+
params_path = app.root.join('app', 'params')
|
|
45
|
+
|
|
46
|
+
next unless params_path.directory?
|
|
47
|
+
|
|
48
|
+
# Load all params class files
|
|
49
|
+
Dir[params_path.join('**', '*_params.rb')].each do |file|
|
|
50
|
+
require_dependency file
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Register all ApplicationParams subclasses with the registry
|
|
54
|
+
next unless defined?(StrongParameters::Core::ApplicationParams)
|
|
55
|
+
|
|
56
|
+
StrongParameters::Core::ApplicationParams.descendants.each do |params_class|
|
|
57
|
+
# Extract model name from class name (e.g., UserParams -> User)
|
|
58
|
+
next unless params_class.name =~ /(.+)Params$/
|
|
59
|
+
|
|
60
|
+
model_name = ::Regexp.last_match(1)
|
|
61
|
+
StrongParameters::Core::ParamsRegistry.register(model_name, params_class)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Strong Parameters provides a whitelist-based approach to mass assignment protection.
|
|
4
|
+
#
|
|
5
|
+
# This gem provides a framework-agnostic approach to parameter filtering and mass
|
|
6
|
+
# assignment protection, with adapters for various Ruby web frameworks including
|
|
7
|
+
# Rails, Sinatra, Hanami, and Rage.
|
|
8
|
+
#
|
|
9
|
+
# @see StrongParameters::Core::Parameters
|
|
10
|
+
# @see StrongParameters::Core::ApplicationParams
|
|
11
|
+
# @see StrongParameters::Core::ParamsRegistry
|
|
12
|
+
|
|
13
|
+
require 'durable_parameters/version'
|
|
14
|
+
require 'durable_parameters/core'
|
|
15
|
+
|
|
16
|
+
# Auto-detect and load framework adapter
|
|
17
|
+
if defined?(Rails)
|
|
18
|
+
# Rails is loaded - use Rails adapter
|
|
19
|
+
require 'durable_parameters/railtie'
|
|
20
|
+
require 'durable_parameters/log_subscriber'
|
|
21
|
+
elsif defined?(Sinatra)
|
|
22
|
+
# Sinatra is loaded - auto-setup Sinatra adapter
|
|
23
|
+
require 'durable_parameters/adapters/sinatra'
|
|
24
|
+
elsif defined?(Hanami)
|
|
25
|
+
# Hanami is loaded - auto-setup Hanami adapter
|
|
26
|
+
require 'durable_parameters/adapters/hanami'
|
|
27
|
+
StrongParameters::Adapters::Hanami.setup!
|
|
28
|
+
elsif defined?(Rage) || defined?(RageController)
|
|
29
|
+
# Rage is loaded - auto-setup Rage adapter
|
|
30
|
+
require 'durable_parameters/adapters/rage'
|
|
31
|
+
StrongParameters::Adapters::Rage.setup!
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Provide top-level convenience aliases
|
|
35
|
+
module StrongParameters
|
|
36
|
+
# Convenience aliases for core classes
|
|
37
|
+
Parameters = Core::Parameters unless defined?(Parameters)
|
|
38
|
+
ApplicationParams = Core::ApplicationParams unless defined?(ApplicationParams)
|
|
39
|
+
ParamsRegistry = Core::ParamsRegistry unless defined?(ParamsRegistry)
|
|
40
|
+
ForbiddenAttributesProtection = Core::ForbiddenAttributesProtection unless defined?(ForbiddenAttributesProtection)
|
|
41
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Stubs out a scaffolded controller and its views. Different from rails
|
|
3
|
+
scaffold_controller, it uses strong_parameters to whitelist permissible
|
|
4
|
+
attributes in a private method.
|
|
5
|
+
Pass the model name, either CamelCased or under_scored. The controller
|
|
6
|
+
name is retrieved as a pluralized version of the model name.
|
|
7
|
+
|
|
8
|
+
To create a controller within a module, specify the model name as a
|
|
9
|
+
path like 'parent_module/controller_name'.
|
|
10
|
+
|
|
11
|
+
This generates a controller class in app/controllers and invokes helper,
|
|
12
|
+
template engine and test framework generators.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require 'rails/version'
|
|
2
|
+
require 'rails/generators/rails/scaffold_controller/scaffold_controller_generator'
|
|
3
|
+
|
|
4
|
+
module Rails
|
|
5
|
+
module Generators
|
|
6
|
+
class StrongParametersControllerGenerator < ScaffoldControllerGenerator
|
|
7
|
+
argument :attributes, :type => :array, :default => [], :banner => "field:type field:type"
|
|
8
|
+
source_root File.expand_path("../templates", __FILE__)
|
|
9
|
+
|
|
10
|
+
if ::Rails::VERSION::STRING < '3.1'
|
|
11
|
+
def module_namespacing
|
|
12
|
+
yield if block_given?
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<% module_namespacing do -%>
|
|
2
|
+
class <%= controller_class_name %>Controller < ApplicationController
|
|
3
|
+
# GET <%= route_url %>
|
|
4
|
+
# GET <%= route_url %>.json
|
|
5
|
+
def index
|
|
6
|
+
@<%= plural_table_name %> = <%= orm_class.all(class_name) %>
|
|
7
|
+
|
|
8
|
+
respond_to do |format|
|
|
9
|
+
format.html # index.html.erb
|
|
10
|
+
format.json { render json: <%= "@#{plural_table_name}" %> }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# GET <%= route_url %>/1
|
|
15
|
+
# GET <%= route_url %>/1.json
|
|
16
|
+
def show
|
|
17
|
+
@<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
|
|
18
|
+
|
|
19
|
+
respond_to do |format|
|
|
20
|
+
format.html # show.html.erb
|
|
21
|
+
format.json { render json: <%= "@#{singular_table_name}" %> }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# GET <%= route_url %>/new
|
|
26
|
+
# GET <%= route_url %>/new.json
|
|
27
|
+
def new
|
|
28
|
+
@<%= singular_table_name %> = <%= orm_class.build(class_name) %>
|
|
29
|
+
|
|
30
|
+
respond_to do |format|
|
|
31
|
+
format.html # new.html.erb
|
|
32
|
+
format.json { render json: <%= "@#{singular_table_name}" %> }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# GET <%= route_url %>/1/edit
|
|
37
|
+
def edit
|
|
38
|
+
@<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# POST <%= route_url %>
|
|
42
|
+
# POST <%= route_url %>.json
|
|
43
|
+
def create
|
|
44
|
+
@<%= singular_table_name %> = <%= orm_class.build(class_name, "#{singular_table_name}_params") %>
|
|
45
|
+
|
|
46
|
+
respond_to do |format|
|
|
47
|
+
if @<%= orm_instance.save %>
|
|
48
|
+
format.html { redirect_to @<%= singular_table_name %>, notice: <%= "'#{human_name} was successfully created.'" %> }
|
|
49
|
+
format.json { render json: <%= "@#{singular_table_name}" %>, status: :created, location: <%= "@#{singular_table_name}" %> }
|
|
50
|
+
else
|
|
51
|
+
format.html { render action: "new" }
|
|
52
|
+
format.json { render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# PATCH/PUT <%= route_url %>/1
|
|
58
|
+
# PATCH/PUT <%= route_url %>/1.json
|
|
59
|
+
def update
|
|
60
|
+
@<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
|
|
61
|
+
|
|
62
|
+
respond_to do |format|
|
|
63
|
+
if @<%= orm_instance.update("#{singular_table_name}_params") %>
|
|
64
|
+
format.html { redirect_to @<%= singular_table_name %>, notice: <%= "'#{human_name} was successfully updated.'" %> }
|
|
65
|
+
format.json { head :no_content }
|
|
66
|
+
else
|
|
67
|
+
format.html { render action: "edit" }
|
|
68
|
+
format.json { render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# DELETE <%= route_url %>/1
|
|
74
|
+
# DELETE <%= route_url %>/1.json
|
|
75
|
+
def destroy
|
|
76
|
+
@<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
|
|
77
|
+
@<%= orm_instance.destroy %>
|
|
78
|
+
|
|
79
|
+
respond_to do |format|
|
|
80
|
+
format.html { redirect_to <%= index_helper %>_url }
|
|
81
|
+
format.json { head :no_content }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
# Use this method to whitelist the permissible parameters. Example:
|
|
88
|
+
# params.require(:person).permit(:name, :age)
|
|
89
|
+
# Also, you can specialize this method with per-user checking of permissible attributes.
|
|
90
|
+
def <%= "#{singular_table_name}_params" %>
|
|
91
|
+
params.require(<%= ":#{singular_table_name}" %>).permit(<%= attributes.map {|a| ":#{a.name}" }.sort.join(', ') %>)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
<% end -%>
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
5
|
+
module ActionController
|
|
6
|
+
# Base class for declarative parameter permission definitions.
|
|
7
|
+
#
|
|
8
|
+
# ApplicationParams provides a DSL for defining which attributes are allowed
|
|
9
|
+
# or denied for mass assignment in controllers. This enables a centralized,
|
|
10
|
+
# declarative approach to parameter filtering that's more maintainable than
|
|
11
|
+
# inline permit() calls.
|
|
12
|
+
#
|
|
13
|
+
# @example Basic usage
|
|
14
|
+
# class UserParams < ApplicationParams
|
|
15
|
+
# allow :name
|
|
16
|
+
# allow :email
|
|
17
|
+
# deny :is_admin
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# @example With action-specific permissions
|
|
21
|
+
# class PostParams < ApplicationParams
|
|
22
|
+
# allow :title
|
|
23
|
+
# allow :body
|
|
24
|
+
# allow :published, only: :create
|
|
25
|
+
# allow :view_count, except: :create
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# @example With metadata declaration
|
|
29
|
+
# class AccountParams < ApplicationParams
|
|
30
|
+
# allow :name
|
|
31
|
+
# metadata :ip_address, :role
|
|
32
|
+
# end
|
|
33
|
+
class ApplicationParams
|
|
34
|
+
class << self
|
|
35
|
+
# Returns the list of allowed attributes.
|
|
36
|
+
#
|
|
37
|
+
# @return [Array<Symbol>] array of allowed attribute names
|
|
38
|
+
def allowed_attributes
|
|
39
|
+
@allowed_attributes ||= []
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Returns the list of denied attributes.
|
|
43
|
+
#
|
|
44
|
+
# @return [Array<Symbol>] array of denied attribute names
|
|
45
|
+
def denied_attributes
|
|
46
|
+
@denied_attributes ||= []
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Returns the flags hash.
|
|
50
|
+
#
|
|
51
|
+
# @return [Hash<Symbol, Object>] hash of flag names to values
|
|
52
|
+
def flags
|
|
53
|
+
@flags ||= {}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Returns the set of allowed metadata keys.
|
|
57
|
+
#
|
|
58
|
+
# @return [Set<Symbol>] set of allowed metadata key names
|
|
59
|
+
# @note :current_user is always implicitly allowed
|
|
60
|
+
def allowed_metadata
|
|
61
|
+
@allowed_metadata ||= Set.new
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# DSL method to allow an attribute
|
|
65
|
+
# @param attribute [Symbol, String, Hash] the attribute name to allow, or a hash for arrays
|
|
66
|
+
# @param options [Hash] additional options
|
|
67
|
+
# - :only - only allow this attribute for these actions
|
|
68
|
+
# - :except - allow this attribute except for these actions
|
|
69
|
+
# - :array - if true, permit an array of scalar values
|
|
70
|
+
# Examples:
|
|
71
|
+
# allow :name # permits scalar name
|
|
72
|
+
# allow :tags, array: true # permits array of scalars
|
|
73
|
+
# allow :tags, only: :create # only for create action
|
|
74
|
+
def allow(attribute, options = {})
|
|
75
|
+
attribute = attribute.to_sym
|
|
76
|
+
allowed_attributes << attribute unless allowed_attributes.include?(attribute)
|
|
77
|
+
|
|
78
|
+
# Store any additional options for this attribute
|
|
79
|
+
if options.any?
|
|
80
|
+
@attribute_options ||= {}
|
|
81
|
+
# Normalize :only and :except to arrays for consistency
|
|
82
|
+
normalized_options = options.dup
|
|
83
|
+
[:only, :except].each do |key|
|
|
84
|
+
if normalized_options[key] && !normalized_options[key].is_a?(Array)
|
|
85
|
+
normalized_options[key] = [normalized_options[key]]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
@attribute_options[attribute] = normalized_options
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# DSL method to deny an attribute
|
|
93
|
+
# @param attribute [Symbol, String] the attribute name to deny
|
|
94
|
+
def deny(attribute)
|
|
95
|
+
attribute = attribute.to_sym
|
|
96
|
+
denied_attributes << attribute unless denied_attributes.include?(attribute)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# DSL method to set a flag
|
|
100
|
+
# @param name [Symbol, String] the flag name
|
|
101
|
+
# @param value [Boolean, Object] the flag value
|
|
102
|
+
def flag(name, value = true)
|
|
103
|
+
flags[name.to_sym] = value
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# DSL method to declare allowed metadata keys
|
|
107
|
+
# @param key [Symbol, String] the metadata key to allow
|
|
108
|
+
# Note: :current_user is always allowed and doesn't need to be declared
|
|
109
|
+
def metadata(*keys)
|
|
110
|
+
keys.each do |key|
|
|
111
|
+
allowed_metadata << key.to_sym
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Check if an attribute is allowed.
|
|
116
|
+
#
|
|
117
|
+
# An attribute is allowed if it's in the allowed list and not in the denied list.
|
|
118
|
+
# Uses memoization for better performance on repeated checks.
|
|
119
|
+
#
|
|
120
|
+
# @param attribute [Symbol, String] the attribute name
|
|
121
|
+
# @return [Boolean] true if allowed, false otherwise
|
|
122
|
+
def allowed?(attribute)
|
|
123
|
+
attribute = attribute.to_sym
|
|
124
|
+
return false if denied_attributes.include?(attribute)
|
|
125
|
+
|
|
126
|
+
allowed_attributes.include?(attribute)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Check if an attribute is denied.
|
|
130
|
+
#
|
|
131
|
+
# @param attribute [Symbol, String] the attribute name
|
|
132
|
+
# @return [Boolean] true if denied, false otherwise
|
|
133
|
+
def denied?(attribute)
|
|
134
|
+
denied_attributes.include?(attribute.to_sym)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Check if a flag is set
|
|
138
|
+
# @param name [Symbol, String] the flag name
|
|
139
|
+
# @return [Boolean, Object] the flag value
|
|
140
|
+
def flag?(name)
|
|
141
|
+
flags[name.to_sym]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Check if a metadata key is allowed
|
|
145
|
+
# @param key [Symbol, String] the metadata key
|
|
146
|
+
# @return [Boolean] true if allowed, false otherwise
|
|
147
|
+
# Note: :current_user is always allowed
|
|
148
|
+
def metadata_allowed?(key)
|
|
149
|
+
key = key.to_sym
|
|
150
|
+
key == :current_user || allowed_metadata.include?(key)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Get options for an attribute
|
|
154
|
+
# @param attribute [Symbol, String] the attribute name
|
|
155
|
+
# @return [Hash] the options hash
|
|
156
|
+
def attribute_options(attribute)
|
|
157
|
+
@attribute_options ||= {}
|
|
158
|
+
@attribute_options[attribute.to_sym] || {}
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Generate a permit array suitable for strong_parameters.
|
|
162
|
+
#
|
|
163
|
+
# Returns an array of permitted attributes, optionally filtered by action.
|
|
164
|
+
# Results are cached per action for better performance.
|
|
165
|
+
#
|
|
166
|
+
# @param action [Symbol, String, nil] optional action name to filter by
|
|
167
|
+
# @return [Array<Symbol, Hash>] array of permitted attributes
|
|
168
|
+
# Returns symbols for scalar attributes, and {attr: []} for array attributes
|
|
169
|
+
# @example
|
|
170
|
+
# permitted_attributes # => [:name, :email]
|
|
171
|
+
# permitted_attributes(action: :create) # => [:name, :email, :published]
|
|
172
|
+
def permitted_attributes(action: nil)
|
|
173
|
+
# Use cache for performance on repeated calls
|
|
174
|
+
@permitted_cache ||= {}
|
|
175
|
+
cache_key = action || :__no_action__
|
|
176
|
+
|
|
177
|
+
return @permitted_cache[cache_key] if @permitted_cache.key?(cache_key)
|
|
178
|
+
|
|
179
|
+
attrs = allowed_attributes.dup
|
|
180
|
+
|
|
181
|
+
# Remove denied attributes
|
|
182
|
+
attrs.reject! { |attr| denied_attributes.include?(attr) }
|
|
183
|
+
|
|
184
|
+
# Filter by action-specific flags if provided
|
|
185
|
+
if action
|
|
186
|
+
action = action.to_sym
|
|
187
|
+
attrs.select! do |attr|
|
|
188
|
+
opts = attribute_options(attr)
|
|
189
|
+
if opts[:only]
|
|
190
|
+
Array(opts[:only]).include?(action)
|
|
191
|
+
elsif opts[:except]
|
|
192
|
+
!Array(opts[:except]).include?(action)
|
|
193
|
+
else
|
|
194
|
+
true
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Convert to proper permit format
|
|
200
|
+
# For array attributes, return {attr: []}, otherwise just the symbol
|
|
201
|
+
result = attrs.map do |attr|
|
|
202
|
+
opts = attribute_options(attr)
|
|
203
|
+
if opts[:array]
|
|
204
|
+
{ attr => [] }
|
|
205
|
+
else
|
|
206
|
+
attr
|
|
207
|
+
end
|
|
208
|
+
end.freeze
|
|
209
|
+
|
|
210
|
+
@permitted_cache[cache_key] = result
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Inherit attributes from parent class.
|
|
214
|
+
#
|
|
215
|
+
# When a subclass is created, it inherits all configuration from the parent
|
|
216
|
+
# including allowed/denied attributes, flags, metadata, and options. This
|
|
217
|
+
# enables building specialized params classes on top of base ones.
|
|
218
|
+
#
|
|
219
|
+
# @param subclass [Class] the inheriting subclass
|
|
220
|
+
# @return [void]
|
|
221
|
+
def inherited(subclass)
|
|
222
|
+
super
|
|
223
|
+
# Copy parent's configuration to subclass
|
|
224
|
+
subclass.instance_variable_set(:@allowed_attributes, allowed_attributes.dup)
|
|
225
|
+
subclass.instance_variable_set(:@denied_attributes, denied_attributes.dup)
|
|
226
|
+
subclass.instance_variable_set(:@flags, flags.dup)
|
|
227
|
+
subclass.instance_variable_set(:@allowed_metadata, allowed_metadata.dup)
|
|
228
|
+
if instance_variable_defined?(:@attribute_options)
|
|
229
|
+
subclass.instance_variable_set(:@attribute_options, @attribute_options.dup)
|
|
230
|
+
end
|
|
231
|
+
# Don't copy the cache - let subclass build its own
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|