THREESCALE-8916: Make strong passwords mandatory#4195
THREESCALE-8916: Make strong passwords mandatory#4195jlledom wants to merge 19 commits into3scale:masterfrom
Conversation
da18ea7 to
5b00c7f
Compare
| const RE_STRONG_PASSWORD = new RegExp( | ||
| '^' + | ||
| '(?=.*\\d)' + // at least one digit | ||
| '(?=.*[a-z])' + // at least one lowercase | ||
| '(?=.*[A-Z])' + // at least one uppercase | ||
| `(?=.*[${STRONG_PASSWORD_SPECIAL_CHARS.replace(/[[\]\\]/g, '\\$&')}])` + // at least one special char | ||
| '(?!.*\\s)' + // no whitespace | ||
| `.{${STRONG_PASSWORD_MIN_SIZE},}` + // minimum length | ||
| '$' | ||
| ) |
There was a problem hiding this comment.
According to NIST guidelines, passwords have to be long, not contain funny characters. I think we should have a default password setting of maybe 12 characters and allow customers to set their own REGEX. This will also allow us to keep short passwords in dev mode so we don't have to type funny stuff to login local server when developing 😎
We better read about current best practices though.
There was a problem hiding this comment.
So currently NIST seems to care about password length and expiration is not recommended. I think standard was 14 chars at the moment without character enforcement. Maybe we should have a setting to allow customers setting minimal password length. idk if we should just keep what we had before and have standard 14 chars, and when strong password chosen, then go for 16 chars. Adding a new field for a custom length is probably an overkill. Lets discuss further.
There was a problem hiding this comment.
We should follow NIST SP 800-63B-4 Date Published: July 2025
https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-63B-4.pdf
See section 3.1.1.2. Password Verifiers
Maybe we don't even need the strong password setting.
| self.password = unencrypted_password | ||
| ThreeScale::Analytics.user_tracking(self).track('Migrated to BCrypt') | ||
| update_columns(password_digest: password_digest, salt: nil, crypted_password: nil) | ||
| authenticate(unencrypted_password) |
There was a problem hiding this comment.
I feel uneasy about this whole file. What is it doing? Why is it wrapping? What was super?
app/models/user.rb
Outdated
|
|
||
| def password_required? | ||
| signup.by_user? && super | ||
| (signup.by_user? || using_password?) && super |
There was a problem hiding this comment.
can you explain why do we need this change?
There was a problem hiding this comment.
Can we rename this method? What is super? I wonder if we should separate usage of password on signup and for validation.
There was a problem hiding this comment.
I asked AI whether the method can be split for checking whether password fields should be shown and whether password field has to be validated. Idk if the analysis makes sense but here it is:
● You're absolutely right - this is a confusing design! The same password_required? method is being used for two different purposes:
1. Model validation - "Should we validate password presence when saving?"
2. UI rendering - "Should we show password fields in forms?"
And worse, it's got that triple-level super chain that's hard to follow.
Better Design
This could be much clearer with separate methods:
For Validation:
# In User model
def validate_password_presence?
signup.by_user? && password_digest_requires_validation? && legacy_password_requires_validation?
end
validates_presence_of :password, if: :validate_password_presence?
For UI:
# In User model
def needs_password_field?
signup.by_user? # Or even more specific: !using_external_auth?
end
# In templates
{% if user.needs_password_field? %}
<input name="account[user][password]" type="password">
{% endif %}
Why the current design exists
Looking at the code, this seems to be a result of:
1. Legacy migration - Moving from crypted_password to password_digest (bcrypt)
2. Monkey-patching Rails - Overriding has_secure_password's password_required?
3. No refactoring - Just kept adding super chains instead of cleaning it up
The method does too many things and the name doesn't clearly indicate which concern it addresses.
| public_search account_plans_ui_visible change_account_plan_permission service_plans_ui_visible | ||
| change_service_plan_permission enforce_sso | ||
| useraccountarea_enabled hide_service signups_enabled account_approval_required public_search | ||
| account_plans_ui_visible change_account_plan_permission service_plans_ui_visiblechange_service_plan_permission |
There was a problem hiding this comment.
| account_plans_ui_visible change_account_plan_permission service_plans_ui_visiblechange_service_plan_permission | |
| account_plans_ui_visible change_account_plan_permission service_plans_ui_visible change_service_plan_permission |
33fe198 to
25f4a95
Compare
101f7d4 to
7c97a77
Compare
- Validate strong passwords in React forms - Some forms treat the password field as clear text. - Make password fields required only when they are actually required. - `Current password` field visible and required only when it should. - Password can be set for all users, no matter how did they signup. - SSO Users: don't see the toast to change password. It can be changed from the same form This fixes https://issues.redhat.com/browse/THREESCALE-11548 - When enforce SSO is enabled, passwords can still be set or changed.
f864657 to
891e8cb
Compare
This is to exclude John Doe from strong passwords validation Co-Authored-By: Claude <noreply@anthropic.com>
891e8cb to
35acc2b
Compare
| authorize! :update, user | ||
|
|
||
| user.update_with_flattened_attributes(flat_params) | ||
| user.update_with_flattened_attributes(flat_params, as: current_user.try(:role)) |
There was a problem hiding this comment.
PUT /admin/api/users/{id}.xml is adding this to it's call. This endpoint is the equivalent but for buyer users: PUT /admin/api/accounts/{id}/users/{id}.xml.
This is to unify behavior.
There was a problem hiding this comment.
I am not sure this is relevant here. From what I can see this as: is used to prevent member permissions from being updated by member users - it's only allowed to admin users:
Lines 112 to 116 in 986e1a3
However, these member permissions thing is only applicable to admin users, not to buyer users, so I don't think adding as: changes anything.
It is true that on the code level there is no distinction between buyer users and admin users...
But I wouldn't add this 🤷
There was a problem hiding this comment.
But I wouldn't add this 🤷
By "this" you mean the as: in buyers_users_controller.rb:40? or the as: in user.rb:116?
Do you want me to remove this change?
There was a problem hiding this comment.
I meant as: current_user.try(:role) in this controller. Unless this breaks something, of course 🤔
Well, I don't know if we need to remove it, I just find it a bit confusing to have it here, as it is not supposed to have any function for this controller, from what I understand.
| { | ||
| signup_type: partner.signup_type, | ||
| password: permitted_params[:password].presence || SecureRandom.hex, | ||
| password: permitted_params[:password].presence, |
There was a problem hiding this comment.
The user is valid without a password when it's an SSO user. So there's no need to enforce a random password. Also, this random password is not shown to the caller anywhere, so it couldn't be used anyway.
After this change, The SSO users for partners don't have a password, the same as any other SSO user in the project.
| @user = @account.users.build | ||
| @user.email = params[:email] | ||
| @user.password = SecureRandom.hex | ||
| @user.password = params[:password].presence |
There was a problem hiding this comment.
Same, no password required.
| // IMPORTANT: These STRONG_PASSWORD_* constants are duplicated from the backend. | ||
| // The source of truth is app/lib/authentication/by_password.rb. If those constants change in Ruby, | ||
| // you must update them here as well. Do not modify these without updating the backend first. | ||
| const STRONG_PASSWORD_MIN_SIZE = 16 | ||
| const RE_STRONG_PASSWORD = new RegExp(`^[ -~]{${STRONG_PASSWORD_MIN_SIZE},64}$`) | ||
| const STRONG_PASSWORD_FAIL_MSG = `Password must be at least ${STRONG_PASSWORD_MIN_SIZE} characters long, and contain only valid characters.` | ||
|
|
There was a problem hiding this comment.
Enforce the same NIST policy in frontend that we enforce in backend
| validates :lost_password_token, :password_digest, length: { maximum: 255 } | ||
|
|
||
| attr_accessible :password, :password_confirmation | ||
| attr_accessible :password, :password_confirmation, as: %i[default member admin] |
There was a problem hiding this comment.
This is to fix a mass-assignment error from PUT /admin/api/users/{id}.xml. Since the endpoint calls the update with role: :admin. The password was excluded from mass-assignment, because it only allowed the default role.
There was a problem hiding this comment.
I'm not sure I understand... if there is no as: argument here, doesn't it mean that the role set in :as when assigning attributes doesn't matter?
| def validate_password? | ||
| password_digest_changed? || (signup.by_user? && password_digest.blank?) | ||
| end | ||
|
|
||
| def validate_strong_password? | ||
| return false if Rails.configuration.three_scale.strong_passwords_disabled | ||
| return false if signup.sample_data? | ||
|
|
||
| validate_password? | ||
| end |
There was a problem hiding this comment.
This replaces password_required? Because it was pretty confusing:
- Being called from views to decide whether making password inputs required. IMO that's wrong, password inputs are required or not according to their purpose, not to some computed value.
- Being called also to decide whether validate the password or not. Which was wrong as well, since it didn't match all scenarios.
The new methods are tested and return proper values for all known scenarios.
| email_part = email.split('@') | ||
| user_attributes = { email: "#{email_part[0]}+test@#{email_part[1]}", username: 'john', first_name: 'John', | ||
| last_name: 'Doe', password: '123456', password_confirmation: '123456', signup_type: :minimal} | ||
| last_name: 'Doe', password: '123456', password_confirmation: '123456', signup_type: :sample_data} |
There was a problem hiding this comment.
In order to exclude "John Doe" from strong password requirement, I added a new signup type :sample_data to identify it.
| def activate_on_minimal_or_sample_data? | ||
| (minimal_signup? || signup.sample_data?) && password.present? && !account.try!(:bought_account_plan).try!(:approval_required?) |
There was a problem hiding this comment.
This method determines whether the new user must be automatically activated or not. I added the :sample_data case to it. Sample user must be activated by default.
| def provider_requires_strong_passwords? | ||
| # use fields definitons source (instance variable) as backup when creating new record | ||
| # and there is no provider account (its still new record and not set through association.build) | ||
| if source = fields_definitions_source_root | ||
| source.settings.strong_passwords_enabled? | ||
| end | ||
| end |
| <%= form.fields_for [:user, @user ] do |user| %> | ||
| <%= user.user_defined_form %> | ||
| <%= user.input :password, as: :patternfly_input, required: true %> | ||
| <%= user.input :password, as: :patternfly_input, input_html: { type: 'password' }, required: true %> |
There was a problem hiding this comment.
Password input was in clear text
| <%= form.input :password, required: false, input_html: { autocomplete: 'off' } %> | ||
| <%= form.input :password_confirmation, required: false, input_html: { autocomplete: 'off' } %> |
There was a problem hiding this comment.
This was wrong, there's no scenario when it's required to change a password in this form.
There was a problem hiding this comment.
In fact, it was a "fake" requirement, the form submit was working without provided values.
| = form.input :password, as: :patternfly_input, | ||
| label: t('.new_password_label'), | ||
| required: false, | ||
| input_html: { value: '', type: 'password', required: false } | ||
|
|
||
| - if using_password | ||
| - if current_user.using_password? |
There was a problem hiding this comment.
Password change inputs are now always visible. However, the current_password input is only visible if the user already has a password in the DB.
| paths = [ | ||
| '''test/unit/authentication/by_password_test.rb''', | ||
| '''test/integration/admin/api/buyers_users_controller_test.rb''' | ||
| ] |
There was a problem hiding this comment.
Had to add this so our password leak system allows me to commit.
| STRONG_PASSWORD_MIN_SIZE = 16 | ||
| # All printable characters in ASCII, from 'space' (32) to ~ (126) | ||
| # at least STRONG_PASSWORD_MIN_SIZE characters | ||
| RE_STRONG_PASSWORD = /\A[ -~]{#{STRONG_PASSWORD_MIN_SIZE},64}\z/ |
There was a problem hiding this comment.
Why do we limit the character ranges for passwords, I don't recall reading such nist requirement.
There was a problem hiding this comment.
I think I misunderstood this sentence from NIST:
Verifiers and CSPs SHOULD permit a maximum password length of at least 64
characters.
| # All printable characters in ASCII, from 'space' (32) to ~ (126) | ||
| # at least STRONG_PASSWORD_MIN_SIZE characters | ||
| RE_STRONG_PASSWORD = /\A[ -~]{#{STRONG_PASSWORD_MIN_SIZE},64}\z/ | ||
| STRONG_PASSWORD_FAIL_MSG = "Password must be at least #{STRONG_PASSWORD_MIN_SIZE} characters long, and contain only valid characters.".freeze |
There was a problem hiding this comment.
Done: 485d40f
(For backend, I think we don't use I18N for frontend)
| if: -> { password_required? && provider_requires_strong_passwords? } } | ||
| validates :password, length: { minimum: 6, allow_blank: true, | ||
| if: -> { password_required? && !provider_requires_strong_passwords? } } | ||
| if: -> { validate_strong_password? } } |
There was a problem hiding this comment.
I don't get it, I thought we will validate passwords according to nist requirements unconditionally which IIRC was 15 characters min. But STRONG_PASSWORD_MIN_SIZE = 16 and it is only validated when configured by the provider. I'm not sure we are on the same page.
There was a problem hiding this comment.
I thought we will validate passwords according to nist requirements unconditionally which IIRC was 15 characters min. But STRONG_PASSWORD_MIN_SIZE = 16
I think I took 16 before your comment about NIST. I can make it 15, NP.
and it is only validated when configured by the provider
I don't get this. What do you mean by configured by the provider? this PR is about making strong password mandatory, not when configured by the provider, we already have that in production.
There only exception to strong passwords validation now are:
- When sample data (John Doe)
- When we set
strong_passwords_disabled: trueinsettings.yml. Which is intended to be used only by us in development.
There was a problem hiding this comment.
Ok, maybe I misinterpret if: -> { validate_strong_password? } }
But then why do we need both - signup.sample_data? and Rails.configuration.three_scale.strong_passwords_disabled?
If we want, we can make strong passwords disabled by default in development mode. And not care about signup.sample_data?
Or are there other situations sample_data? should kick in?
There was a problem hiding this comment.
If we want, we can make strong passwords disabled by default in development mode
Yeah we could, but we would still need a way to enable it in development when we want, for instance, I need it enabled to actually develop the feature. Adding a exception in the code is worse than a setting IMO, basically because I would need to make changes in the code every time I need to switch it on an off, and have it lingering in the git cache, maybe even committing those changes by accident.
And not care about signup.sample_data? Or are there other situations sample_data? should kick in?
Sample data is basically for John Doe, and John Doe is created automatically in buyer signup in all environments, also production.
| def password_required? | ||
| signup.by_user? && (password_digest.blank? || password_digest_changed?) | ||
| def validate_password? | ||
| password_digest_changed? || (signup.by_user? && password_digest.blank?) |
There was a problem hiding this comment.
I suggest ot have this. In this case the issue described #4194 (comment) is avoided and we don't need to use update_columns
| password_digest_changed? || (signup.by_user? && password_digest.blank?) | |
| return unless will_save_change_to_password_digest? || new_record? | |
| password_digest_changed? || (signup.by_user? && password_digest.blank?) |
There was a problem hiding this comment.
There's no need to apply this because that comment you linked was about providing a way for non-migrated users to create a new password. This is already working in this branch, so no changes required.
There was a problem hiding this comment.
The issue is that in the linked PR, updating fields was switched to update_columns which IMO should be used when actually necessary as it skips all validations and callbacks. I don't see any specific other validations and callbacks that should run at that point. But the whole idea is to use update_columns as an exception, because you never know about possible side effects.
There was a problem hiding this comment.
But this method has nothing to do with that. you mean generate_lost_password_token. I can use save(validate: false) there.
There was a problem hiding this comment.
The problem is not just recovery token. One will be unable even to suspend a user with old-style password.
[3] pry(main)> u.suspend!
TRANSACTION (0.4ms) BEGIN
TRANSACTION (0.2ms) ROLLBACK
StateMachines::InvalidTransition: Cannot transition state via :suspend from :active (Reason(s): Password can't be blank, Password Password must be at least 15 characters long, and contain only valid characters.)
from /home/akostadi/.asdf/installs/ruby/3.1.7/lib/ruby/gems/3.1.0/gems/state_machines-0.5.0/lib/state_machines/event.rb:224:in `block in add_actions'I think I found the issue. Originally we had this:
def validate_password?
password_required? && password_changed?
endNow we have
password_digest_changed? || (signup.by_user? && password_digest.blank?)It is not even clear to me why do we have this logic. A comment on top of the method explaining the intention can be very useful.
I'm not sure what the idea behind (signup.by_user? && password_digest.blank?) is.
Basically in the past we didn't perform validation when password digest did not change. will_save_change_to_password_digest? is the more modern and unambiguous version of password_digest_changed?.
I think that we need to make sure password state does not prevent other operations over a user.
There was a problem hiding this comment.
What is the use case for (signup.by_user? && password_digest.blank?) ?
There was a problem hiding this comment.
About (signup.by_user? && password_digest.blank?), the scenario is the signup form, when you send it without a password, it should validate. This form is from the developer portal so clients can send anything from there.
Also, after my commit in this PR: fbd2a20, the API actually allows creating users without a password. If you also provide an open id, then signup.by_user? will be false and allow the user to not have a pass, but if you don't send neither open id or pass, this clause will prevent the user creation.
I tried with will_save_change_to_password_digest? all alone and it doesn't protect from those scenarios, so the additional clause is needed.
If you don't like it, I can do this:
- Remove the clause
- Use strong params to make
passwordrequired in the signup form - Use strong params to make either
passwordoropen_idrequired in the partner endpoints - Add tests for those controllers.
IMO, it's more secure to rely on the model validation than in controller parameters.
About unmigrated users not being editable, I don't know, they are supposed to be migrated since 9 years ago. We don't have any unmigrated user in SaaS and it's very unlikely to have any onpremises. If such user would exist, we could tell the client to follow the reset password flow to make the user valid. I'm not sure it's worth to complicate the logic even more to fit this use case.
There was a problem hiding this comment.
I agree it is better to validate within the model.
The basic idea is that when password is not changed or this is not a new object, we would not validate the password.
Then in case password changed or this is a new object, we check whether password login is allowed and validate when allowed.
From what I read, we need to handle the signup case, where you say that initially the user is created without a password. In that case your check (signup.by_user? && password_digest.blank?) might be reasonable. Will it match only when user is first created and not later in the user object lifecycle?
btw, if by signup we create a nil password user object, maybe we can change this to create the user with some long random string instead.
Do we have any other edge cases to handle?
|
|
||
| Given "{provider} is requiring strong passwords" do |provider| | ||
| provider.settings.update_attribute :strong_passwords_enabled, true | ||
| # When RAILS_ENV=test, strong passwords are disabled by default |
There was a problem hiding this comment.
Maybe it makes sense to not disable strong passwords in tests, so by default all tests assume that strong tests are required (so, it tests production behavior more closely).
And only disable strong passwords for specific scenarios.
There was a problem hiding this comment.
I did that first. I enabled strong passwords by default in tests and made changes across all the test suite to fix the tens of tests currently using weak passwords. That affected tens of files. The PR counted more that 80+ changed files. At the end I reverted those commits because I thought this PR was already complicated enough with 50+ files to be reviewed.
I can do it again if you think it's better.
There was a problem hiding this comment.
Thank you for being considerate! would be nice to do, ideally in a follow-up PR :)
There was a problem hiding this comment.
I personally think that doing it in another PR would be confusing 🙃
The test changes are already huge enough so that some people (me, for example) will not have enough courage to look through them 🫠
There was a problem hiding this comment.
OK, I'll change it again. I'll set superSecret1234# as the password for all tests now
There was a problem hiding this comment.
By the way, I undrestand that only the password length is enforced now? So, it can also be all letters or all numbers? Not saying that's what you need to use for tests, just curious.
There was a problem hiding this comment.
Yes, we are following this: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-63B-4.pdf section 3.1.1.2.
Point 5:
Verifiers and CSPs SHALL NOT impose other composition rules (e.g., requiring
mixtures of different character types) for passwords.
I can guess the reason: password length is what matters, the longer, the harder to brute-force it. If we required the password to have say uppercase,lowercase, numbers and symbols, we would make it easier to brute-force since we would be providing info about the password.
There was a problem hiding this comment.
Because studies have shown special characters are hard to remember so better rely on length. e.g. easier to remember one more word than where exactly you have replaced i with 1. And where was the capital letter. While one additional word would provide more randomness. Also when passwords are easier to remember, people are less likely to put them on a sticky note on their monitor. Similar logic behind non-expiring passwords.
But either way, I don't think we should restrict to characters. IMO just check for length, not which characters were used.
| @user.save! | ||
|
|
||
| render json: {id: @user.id, success: true} | ||
| rescue StandardError |
There was a problem hiding this comment.
why rescue instead of rely on @user.save return value?
There was a problem hiding this comment.
Because the previous call to @user.activate! can raise an exception, which was uncaught.
|
This needs to be coordinated with 3scale Operator. Currently, the operator generates some random passwords, which are currently 8 characters long: https://github.com/3scale/3scale-operator/blob/b99f57856197150a3eca1b6ae8f610c8ff9a7107/pkg/3scale/amp/component/system_options.go#L165-L175 |
Let tests fail to get the complete list of outdated tests
| // activerecord.errors.models.user.attributes.password.weak | ||
| const STRONG_PASSWORD_MIN_SIZE = 15 | ||
| const RE_STRONG_PASSWORD = new RegExp(`^[ -~]{${STRONG_PASSWORD_MIN_SIZE},}$`) | ||
| const STRONG_PASSWORD_FAIL_MSG = `Password must be at least ${STRONG_PASSWORD_MIN_SIZE} characters long, and contain only valid characters.` |
There was a problem hiding this comment.
should we just check the size and not a RE?

Note
This PR includes all changes from #4194. To make it easier to review, jlledom#3 includes only the actual diff to make strong passwords mandatory.
What this PR does / why we need it:
Currently, a provider can enforce strong password for the developer portal, and weak passwords are accepted by default. About admin portal, there's no option to enable strong passwords, weak passwords are always accepted.
I think both situations make no sense. I don't think it's acceptable to allow users decide whether they enforce strong passwords or not, as long as strong passwords are possible, that should be the default. And same thing about admin portal.
After a discussion via slack, we agreed on this terms:
settings.ymlto allow weak passwords for usstrong_passwords_disabled: truetosettings.yml. If not set at all, strong passwords are enabled, so clients don't have to change anything on their side.db:seedshould accept weak passwordsstrong_passwords_disabledtotruebefore running the task:smaple_data. Sample data accepts weak passwords, no matter the setting.This affects multiple screens, but also API endpoints, this is the complete list:
Additionally, I also added some behavioral changes to the UI. To solve bugs or behaviors I think are incorrect. This is the summarty Claude generated:
Screens/Forms Modified
app/javascript/src/Login/utils/validations.ts)length: { minimum: 6 }to regex pattern matching backend rulesSTRONG_PASSWORD_*constants mirroring backend.app/views/buyers/accounts/new.html.erb)type: 'password'to password field. Previously the field was showing password as clear text.app/views/buyers/users/edit.html.erb)required: trueto `required:app/views/provider/admin/account/users/_form.html.erb)required: truetorequired: false. Password is now optional when editing a provider user.app/views/provider/admin/user/personal_details/edit.html.slim)1. Removed the toast/alert prompting SSO users to set password via reset form
2. Password field now always visible (not conditional on
using_password)3. Password field changed to
required: false4. Added
type: 'password'to hide password input5. "Current password" section only shown when
using_password?is true (users with existing password must confirm it)config/locales/en.yml)set_password_htmltranslation (toast was removed).Summary of behavioral changes
Strong password validation in React - Frontend now enforces the same 16+ character ASCII-printable password rules as the backend.
Password fields show as masked - Fixed forms that were accidentally displaying passwords as plain text.
Password fields are optional on edit - When editing existing users (buyer or provider), password is no longer required. You can update other fields without changing the password.
Password field always visible - On personal details page, the password field is now always shown regardless of how the user signed up (SSO, password, etc.). Any user can set/change their password.
Current password only required when applicable - The "Current password" confirmation field only appears when the user already has a password set (
using_password?). New SSO users setting a password for the first timedon't need to provide a current password.
Removed SSO password reset toast - SSO users no longer see the banner telling them to use the password reset form. They can now set their password directly from the personal details form.
Works with enforce SSO - Passwords can still be set/changed even when enforce SSO is enabled.
I know this is a though one. I recommend to review the commits one by one. Also, I'll add some comments to provider further explanations.
Which issue(s) this PR fixes
https://issues.redhat.com/browse/THREESCALE-8916
https://issues.redhat.com/browse/THREESCALE-11548
Verification steps
You can go through any (ideally all) screens above an try to set a weak password. Also tests should pass.
List of added tests
This is the complete list of tests added in this PR. To cover all the new behaviour explained above:
test/unit/authentication/by_password_test.rbvalidate_password?,validate_strong_password?, andusing_password?methods. Covers scenarios for different signup types (new_signup, minimal, sample_data, machine), password change detection, and strong password validation bypass for sample_data users.test/unit/liquid/drops/user_drop_test.rbusing_password?andpassword_required?Liquid drop methods, verifying correct behavior for SSO users, by_user signups, and sample_data signups.test/unit/user/signup_type_test.rbsample_data?,machine?, andby_user?methods to verify sample_data signup type is correctly identified and classified as a machine signup.test/unit/user/states_test.rbActivateOnMinimalOrSampleDataTestclass with tests for auto-activation logic, covering minimal and sample_data signups with/without password and approval requirements.test/unit/logic/provider_signup_test.rbsample_datasignup type, bypasses strong password validation, and is auto-activated.test/integration/developer_portal/signup_test.rbStrongPasswordsTestclass testing developer portal signup with strong password validation enabled/disabled.test/integration/provider/signups_controller_integration_test.rbSignupsControllerStrongPasswordsTestclass testing admin portal provider signup with strong password validation enabled/disabled.test/integration/provider/admin/account/users_controller_test.rbtest/functional/buyers/users_controller_test.rbEditPagePasswordFieldsTestclass verifying password fields are always visible on buyer user edit page, including for SSO users.test/functional/provider/admin/user/personal_details_controller_test.rbSSOUserWithoutPasswordTest,UserWithPasswordTest, andEnforceSSOEnabledTestclasses testing personal details page behavior for SSO users, current password requirements, and enforce SSO scenarios.test/functional/partners/providers_controller_test.rbtest/functional/partners/users_controller_test.rbtest/integration/admin/api/buyers_users_controller_test.rbtest/integration/user-management-api/users_test.rbPUT /admin/api/users/{id}endpoint.