Skip to content

THREESCALE-12434: Migrate from protected attributes to strong parameters - Part 2#4249

Open
mayorova wants to merge 8 commits into
strong-params-part1from
strong-params-part2
Open

THREESCALE-12434: Migrate from protected attributes to strong parameters - Part 2#4249
mayorova wants to merge 8 commits into
strong-params-part1from
strong-params-part2

Conversation

@mayorova
Copy link
Copy Markdown
Contributor

@mayorova mayorova commented Mar 11, 2026

What this PR does / why we need it:

This is part 2 of the migration from protected attributes to strong parameters. See the first part in #4248

Protected attributes is an old Rails feature which was deprecated a long time ago. We were using protected_attributes_continued gem to keep it working, but now it's also discontinued and does not support Rails 7+, so it's a blocker for upgrading to Rails 7.2 for us.

This Part 2 handles the models that can have custom attributes through FieldsDefinitions - Account, User, Cinstance.

Which issue(s) this PR fixes

https://redhat.atlassian.net/browse/THREESCALE-12434

Verification steps

All tests should pass, and all features should work as before.

Special notes for your reviewer:

Copy link
Copy Markdown
Contributor

@jlledom jlledom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments come from the other PR. Also, I still see a lot of

Comment thread app/controllers/master/api/providers_controller.rb Outdated
Comment thread app/lib/signup/account_manager.rb Outdated
Comment thread app/controllers/provider/admin/account/users_controller.rb Outdated
Comment thread app/lib/authentication/strategy/oauth2.rb
@mayorova mayorova force-pushed the strong-params-part2 branch 2 times, most recently from 38e277c to db25ecf Compare March 13, 2026 00:03
@account_params ||= begin
defined_fields_names = buyer_account.defined_fields_names
allowed_attrs = defined_fields_names - %w(billing_address) + %w(name)
nested_params = { extra_fields: buyer_account.defined_extra_fields_names }
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, this is different from other API controller, which have only plain parameters, i.e. not nested under extra_fields.
I only did this because there was an existing test in test/integration/admin/api/accounts_controller_test.rb which was passing extra params in this way:

params: update_params.merge(extra_fields: { my_field: 4 })

I also added a check that plain parameters would also work.

Now I'm not sure if other API controllers need to accept both ways too 🤔

@mayorova mayorova force-pushed the strong-params-part2 branch 6 times, most recently from a676b21 to 8b5a6de Compare March 17, 2026 15:36
@mayorova mayorova changed the base branch from master to strong-params-part1 March 17, 2026 17:19
@mayorova mayorova changed the title Strong params part2 THREESCALE-12434: Migrate from protected attributes to strong parameters - Part 2 Mar 17, 2026
@mayorova mayorova force-pushed the strong-params-part1 branch from 0b8ead4 to 6c69623 Compare March 18, 2026 15:35
@mayorova mayorova force-pushed the strong-params-part2 branch 3 times, most recently from 498f50b to 706cacb Compare March 18, 2026 17:47
@mayorova mayorova force-pushed the strong-params-part1 branch 3 times, most recently from a08b299 to d3304a9 Compare March 19, 2026 12:26
@mayorova mayorova force-pushed the strong-params-part2 branch 2 times, most recently from e2771a2 to 1abbc5c Compare March 19, 2026 15:32
@mayorova mayorova marked this pull request as ready for review March 19, 2026 15:34
@qltysh
Copy link
Copy Markdown

qltysh Bot commented Mar 19, 2026

❌ 30 blocking issues (32 total)

Tool Category Rule Count
reek Lint Partners::ProvidersController#assign_account_attributes refers to 'account' more than self (maybe move it to another class?) 10
reek Lint Partners::ProvidersController#assign_account_attributes has approx 8 statements 3
reek Lint Provider::SignupsController#track_user calls 'analytics_session.traits' 2 times 2
rubocop Lint Assignment Branch Condition size for create\_account is too high. [<5, 22, 6> 23.35/20] 2
reek Lint Signup::AccountManager takes parameters ['defaults', 'plans'] to 3 methods 2
reek Lint Signup::AccountManager#assign_attributes_for_account is controlled by argument 'validate_fields' 2
rubocop Lint Prefer response\.parsed\_body. 2
brakeman Vulnerability Specify exact keys allowed for mass assignment instead of using permit\! which allows any keys. 1
rubocop Lint Avoid parameter lists longer than 5 parameters. [6/5] 1
reek Lint Signup::AccountManager#create has 6 parameters 1
reek Lint Signup::AccountManager#create has boolean parameter 'validate_fields' 1
rubocop Lint Perceived complexity for create is too high. [9/8] 1
reek Lint DeveloperPortal::SignupController has missing safe method 'signup_user!' 1
rubocop Lint Predicate method names should end with ?. 1
qlty Structure Function with many parameters (count = 6): create 2

@mayorova mayorova force-pushed the strong-params-part1 branch 2 times, most recently from 4b5523f to 5f946c2 Compare April 20, 2026 10:07
@mayorova mayorova force-pushed the strong-params-part1 branch from 24a8494 to d30e446 Compare May 15, 2026 19:51
@mayorova mayorova force-pushed the strong-params-part2 branch 2 times, most recently from 7d4be7e to 8554802 Compare May 18, 2026 12:38
@mayorova mayorova force-pushed the strong-params-part2 branch from 8554802 to 14797a9 Compare May 18, 2026 17:15
Comment thread app/controllers/admin/api/buyers_users_controller.rb

def buyer
@buyer ||= @provider.buyers.build do |account|
# We need to get all the account params to run the spam check
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was indeed added explicitly in this PR: https://github.com/3scale/porta/pull/3215/changes

However, later the implementation of the spam check was changed, and this method became dead code: a798bc0

Comment on lines +65 to +88
def test_defined_fields_names
field1 = mock('field', name: 'field1')
field2 = mock('field', name: 'field2')
model = Model.new
model.stubs(:defined_fields).returns([field1, field2])
assert_same_elements %w[field1 field2], model.defined_fields_names
end

def test_defined_extra_fields_names
extra_field = mock('field', name: 'extra')
builtin_field = mock('field')
model = Model.new
model.stubs(:defined_fields).returns([extra_field, builtin_field])
model.stubs(:defined_builtin_fields).returns([builtin_field])
assert_equal %w[extra], model.defined_extra_fields_names
end

def test_defined_builtin_fields_names
field1 = mock('field', name: 'field1')
field2 = mock('field', name: 'field2')
model = Model.new
model.stubs(:defined_builtin_fields).returns([field1, field2])
assert_same_elements %w[field1 field2], model.defined_builtin_fields_names
end
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feels like all these are redundant in the presence of test_provider_fields

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I think the tests are not redundant, because each one tests a different method.

However, now I'm thinking - why didn't I use defined_fields_names_for everywhere instead of the alternatives?..

I guess the reason is that at first it hadn't occurred to me. I was relying on the existing defined_fields_names and similar, whose implementation is actually more complex, because it:

  1. requires a properly initialized instance - which is a bit inconvenient, and made me build the new instance of objects with proper associations before being able to derive the list of defined fields
  2. includes calculating the fields_definitions_source_root etc.

It makes me think - should I refactor the whole PR to use this simpler defined_fields_names_for(class_name)? 🤔

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overlooked the actual method names, too late when I was looking at these :)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should I refactor the whole PR to use this simpler defined_fields_names_for(class_name)?

If not too intrusive, it would be great but can go in another PR as well.

Comment thread spec/support/api/context.rb
Comment thread .rubocop.yml
Comment on lines +19 to +27
test "index shows users list" do
get :index
assert_response :success
end

test "edit shows user form" do
get :edit, params: { id: @member_user.id }
assert_response :success
end
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume we have some cucumberf for this. What are we testing? nothing_raised 😅 ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty much 😉

I think it doesn't hurt, and it's good practice to test every action of the controller.
Now, could these tests be more extensive (e.g. assert something more) - of course. But I think it's better to have a status check than not have the test at all. I'll try to add a couple of more assertions (e.g. that the drop is assigned).

Comment thread app/lib/signup/signup_params.rb
@codecov
Copy link
Copy Markdown

codecov Bot commented May 26, 2026

Codecov Report

❌ Patch coverage is 98.21429% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.09%. Comparing base (8573020) to head (fd9fc80).
⚠️ Report is 3 commits behind head on strong-params-part1.

Files with missing lines Patch % Lines
...controllers/provider/invitee_signups_controller.rb 66.66% 4 Missing ⚠️
...trollers/provider/admin/applications_controller.rb 50.00% 1 Missing ⚠️
Additional details and impacted files
@@                   Coverage Diff                   @@
##           strong-params-part1    #4249      +/-   ##
=======================================================
+ Coverage                88.98%   89.09%   +0.11%     
=======================================================
  Files                     1751     1750       -1     
  Lines                    44108    44104       -4     
  Branches                   686      686              
=======================================================
+ Hits                     39249    39294      +45     
+ Misses                    4843     4794      -49     
  Partials                    16       16              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

def signup_user!
Account.transaction do
SignupService.create(**signup_service_params(account_params, user_params)) do |signup_result|
@signup_service.create(account_params:, user_params: user_params.merge(signup_type: :minimal)) do |signup_result|
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bob said that previously signup_type: :new_signup was set by service while now it is set to minimal. Is this a valid concern?

@buyer.reload
assert_equal "stuff", @buyer.extra_fields["some_extra_field"]
assert_equal 33, @buyer.vat_rate
assert_equal 33.0, @buyer.vat_rate.to_f
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert_equal 33.0, @buyer.vat_rate.to_f
assert_equal BigDecimal('33'), @buyer.vat_rate

manager_without_validation.account.expects(:validate_fields!).never
manager_without_validation.user.expects(:validate_fields!).never
manager_without_validation.create(**signup_params, validate_fields: false)
end
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a nit, this is much better 2 tests

## op.parameters.add :name => "name", :description => "Name of the application to be created.", :dataType => "string", :allowMultiple => false, :required => true, :paramType => "query"
## op.parameters.add :name => "description", :description => "Description of the application to be created.", :dataType => "string", :allowMultiple => false, :required => true, :paramType => "query"
#

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the removed comment contains useful information that params are required, types, etc. Should we remove it?

Comment thread app/controllers/admin/api/buyers_applications_controller.rb

def plan_id
@plan_id ||= params.require(:cinstance).permit(:plan_id).tap { |plan_params| plan_params.require(:plan_id) }[:plan_id]
@plan_id ||= cinstance_params.require(:plan_id)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After interrogation, Bob thinks that this change is potentially unsafe:

│ Old code (SAFE):                                                             │
│  1 @plan_id ||= params.require(:cinstance).permit(:plan_id).tap { |          │
│    plan_params| plan_params.require(:plan_id) }[:plan_id]                    │
│  - .permit(:plan_id) filters out arrays/hashes, only allowing scalar         │
│    values                                                                    │
│  - Result with plan_id: [1,2,3] → returns empty {}, then [:plan_id]          │
│    returns nil                                                               │
│                                                                              │
│ New code (POTENTIALLY UNSAFE):                                               │
│  1 @plan_id ||= cinstance_params.require(:plan_id)                           │
│  - require(:plan_id) does NOT filter arrays - it just checks the key         │
│    exists                                                                    │
│  - Result with plan_id: [1,2,3] → returns the array [1, 2, 3]      

Maybe not really, because plan_id used in #find shouldn't cause real troubles but still better to make sure we get a proper value or no value.


module Provider
def provider_builds_application_for(buyer, application_plan, application_attrs = {}, service_plan = nil)
def provider_builds_application_for(buyer, application_plan, service_plan = nil, application_attrs: {})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we stopped using application_attrs as I see in app/controllers/api/applications_controller.rb and app/controllers/buyers/applications_controller.rb, why don't we remove it altogether? Since the method signature changed so much, there shouldn't be any hidden users that we would need backward compatibility for. As well it will be harder to remove this later as one will suspect existing users.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants