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 2 times, most recently 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
authorize! :update, user

user.update_with_flattened_attributes(flat_params)
user.update(user_params)
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.

Seems like the underlying method is not used anymore and can be removed?

porta/app/lib/fields/extra_fields.rb
73:  def update_with_flattened_attributes(flattened_attrs, options = {})

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.

Hmm, yes, and probably assign_unflattened_attributes too.
I still see one use of unflattened_attributes=, but maybe if it's possible to get rid of it, maybe nest_extra_fields can go too...? 🤔

But I guess I was unsure if this was actually needed for something...

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.

Done: f56d132


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

end

def permitted_user_params
filter_readonly_params(user_params, User).permit(*resource.defined_fields_names, :password, :password_confirmation)
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.

While resource.special_fields should be [] anyway, shouldn't we still add it to the permit list just to keep things consistent and for future proofing? Although most likely whoever ever adds such fields will notice that they don't get set and update this here.

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.

Actually, for User model special_fields return %i[password password_confirmation].

I decided to be provide these two fields explicitly here, to me it made more sense. special_fields is a bit too obscure, and it's not clear at all what it's supposed to mean. Having them explicitly stated in the permitted params list, on the other hand, explains the behavior of the controller better.

In fact, now looking at where special_fields is still used, I think I will try to remove this - one confusing thing less 🙃

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.

Your review made me look into this, and I:

  • removed special_fields, because it was not needed: 6d9e844
  • while working on that, I realized that moving to strong parameters caused a regression in some APIs - passing the annotations parameter stopped working. I fixed it and added tests: fd9fc80

So, while annotations was inlcuded in the list of special_fields for Account model, it was not used anywhere, apparently, so it was easy to miss 🤷

The fact that there were no test also didn't help.

Hopefully, we're good now! 🤞


def user_params
params.require(:user).permit(*@user.defined_fields.map(&:name), *@user.special_fields)
params.require(:user).permit(*@user.defined_fields_names, :password, :password_confirmation)
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.

same question here, should we remove special_fields? How did things work before having :password, :password_confirmation permitted?

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.

As explained in another comment, User#special_fields returns [:password, :password_confirmation], so this change is actually fully equivalent, but more explicit.

def special_fields
%i[password password_confirmation]
end

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)? 🤔

let(:provider) { FactoryBot.create(:provider_account, self_domain: 'example.org') }
let(:provider) do
provider_acc = FactoryBot.create(:provider_account, self_domain: 'example.org')
FieldsDefinition.create_defaults!(provider_acc.provider_account)
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.

btw, related to all places where this line was added, isn't it a little stupid that we create field definitions for master in db:seed and then the provider callback to create field definitions has unless master??

Won't it be easier to remove that line from seeds and remove that condition from the callback and have one way to do things?

The only advantage I see presently is that not all tests need these definitions while we create master for almost all tests.

But I don't ask you to modify anything. Just thinking out loud. I'm not sure that master.provider? is always true so it may lead to other small adjustments needed here and there and complicate this PR further. It's fine to keep it sweet, short and simple 👼

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.

Yeah, not sure why that unless master? is there 🤷

Maybe it would be better to add fields definitions creation to the factory (to avoid changes to the main application logic). But yeah, I guess the idea is to not create extra objects and associations that most of the tests don't need.

Comment thread .rubocop.yml
EnforcedStyle: referrer

Rails/ActionOrder:
Enabled: false
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.

why disable this?

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.

Because it was annoying me every time 😬

Apparently, nobody ever cared about following a specific order, except @jlledom who has his own preference (which does not coincide with the default), see #4241 (comment)

So, now each controller has its own order, and as I was touching controllers, I kept getting these warnings.

We may chose to stick to the same order everywhere, and to fix all controllers and enable this rule again. But in another PR.

Or we may keep ignoring it 😬

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).

account.validate_fields! if validate_fields
account.unflattened_attributes = account_attributes
vat_rate = account_attributes[:vat_rate]
account.vat_rate = vat_rate.to_f if vat_rate
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.

Here it converted vat_rate.to_f while I don't see this being done in the controllers. While I'm not impressed about seeing a money related value to use the Float data type, I just want to make sure that the change was intentional and thought through. And that what we get by the automatic ActiveRecord conversion is what we actually want.

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, yes, this conversion in the end was not needed, because ActiveRecord converts the value automatically (e.g. from string coming from user to BigDecimal which is the actual column's format (t.decimal "vat_rate", precision: 20, scale: 2).

I've added a new test in test/unit/account_test.rb to validate the behavior - and adding .to_f doesn't change anything.

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