Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ system-builder-ruby33: &system-builder-ruby33
ORACLE_SYSTEM_PASSWORD: threescalepass
NLS_LANG: AMERICAN_AMERICA.UTF8
TZ: UTC
MASTER_PASSWORD: p
USER_PASSWORD: p
MASTER_PASSWORD: superSecret1234#
USER_PASSWORD: superSecret1234#
LC_ALL: C.utf8
RAILS_ENV: test

Expand Down
8 changes: 8 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[allowlist]
description = "Global Allowlist"

# Ignore based on any subset of the file path
paths = [
'''test/unit/authentication/by_password_test.rb''',
'''test/integration/admin/api/buyers_users_controller_test.rb'''
]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Had to add this so our password leak system allows me to commit.

6 changes: 3 additions & 3 deletions app/controllers/admin/api/settings_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ class Admin::Api::SettingsController < Admin::Api::BaseController
before_action :validate_enforcing_sso_allowed, only: [:update]

ALLOWED_PARAMS = %i[
useraccountarea_enabled hide_service signups_enabled account_approval_required strong_passwords_enabled
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_visible change_service_plan_permission
enforce_sso
].freeze

# Settings Read
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/partners/providers_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def account_params
def user_params
{
signup_type: partner.signup_type,
password: permitted_params[:password].presence || SecureRandom.hex,
password: permitted_params[:password].presence,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

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.

email: permitted_params[:email],
first_name: permitted_params[:first_name],
last_name: permitted_params[:last_name],
Expand Down
12 changes: 6 additions & 6 deletions app/controllers/partners/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,19 @@ def destroy
def create
@user = @account.users.build
@user.email = params[:email]
@user.password = SecureRandom.hex
@user.password = params[:password].presence
Copy link
Contributor Author

@jlledom jlledom Feb 6, 2026

Choose a reason for hiding this comment

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

Same, no password required.

@user.first_name = params[:first_name].presence
@user.last_name = params[:last_name].presence
@user.open_id = params[:open_id].presence
@user.username = params[:username]
@user.signup_type = :partner
@user.role = :admin
@user.activate!
if @user.save
render json: {id: @user.id, success: true}
else
render json: {errors: @user.errors, success: false}
end
@user.save!

render json: {id: @user.id, success: true}
rescue StandardError
Copy link
Contributor

Choose a reason for hiding this comment

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

why rescue instead of rely on @user.save return value?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because the previous call to @user.activate! can raise an exception, which was uncaught.

render json: {errors: @user.errors, success: false}, status: :unprocessable_entity
end

private
Expand Down
11 changes: 10 additions & 1 deletion app/javascript/src/Login/utils/validations.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import validate from 'validate.js'

// 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.
// The error message is also defined in config/locales/en.yml at:
// 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.`
Copy link
Contributor

Choose a reason for hiding this comment

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

should we just check the size and not a RE?


Comment on lines 3 to 11
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Enforce the same NIST policy in frontend that we enforce in backend

const loginConstraints = {
username: { presence: { allowEmpty: false, message: 'Email or username is mandatory' } },
password: { presence: { allowEmpty: false, message: 'Password is mandatory' } }
Expand All @@ -12,7 +21,7 @@ function validateLogin (fields: Record<keyof typeof loginConstraints, string>):
const changePasswordConstraints = {
password: {
presence: { allowEmpty: false, message: 'Password is mandatory' },
length: { minimum: 6 }
format: { pattern: RE_STRONG_PASSWORD, message: STRONG_PASSWORD_FAIL_MSG }
},
passwordConfirmation: {
presence: { allowEmpty: false, message: 'Password confirmation is mandatory' },
Expand Down
43 changes: 21 additions & 22 deletions app/lib/authentication/by_password.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,26 @@ module ByPassword
extend ActiveSupport::Concern

# strong passwords
SPECIAL_CHARACTERS = '-+=><_$#.:;!?@&*()~][}{|'
RE_STRONG_PASSWORD = /
\A
(?=.*\d) # number
(?=.*[a-z]) # lowercase
(?=.*[A-Z]) # uppercase
(?=.*[#{Regexp.escape(SPECIAL_CHARACTERS)}]) # special char
(?!.*\s) # does not end with space
.{8,} # at least 8 characters
\z
/x
STRONG_PASSWORD_FAIL_MSG = "Password must be at least 8 characters long, and contain both upper and lowercase letters, a digit and one special character of #{SPECIAL_CHARACTERS}.".freeze
STRONG_PASSWORD_MIN_SIZE = 15
# 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},}\z/
STRONG_PASSWORD_FAIL_MSG = I18n.t('activerecord.errors.models.user.attributes.password.weak', min_size: STRONG_PASSWORD_MIN_SIZE)

included do
# We only need length validations as they are already set in Authentication::ByPassword
has_secure_password validations: false

validates_presence_of :password, if: :password_required?
validates_presence_of :password, if: :validate_password?

validates_confirmation_of :password, allow_blank: true

validates :password, format: { :with => RE_STRONG_PASSWORD, :message => STRONG_PASSWORD_FAIL_MSG,
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? } }
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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: true in settings.yml. Which is intended to be used only by us in development.

Copy link
Contributor

Choose a reason for hiding this comment

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

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Min pass size to 15: b986bea


validates :lost_password_token, :password_digest, length: { maximum: 255 }

attr_accessible :password, :password_confirmation
attr_accessible :password, :password_confirmation, as: %i[default member admin]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

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?


scope :with_valid_password_token, -> { where { lost_password_token_generated_at >= 24.hours.ago } }

Expand All @@ -45,8 +36,15 @@ def find_with_valid_password_token(token)
end
end

def password_required?
signup.by_user? && (password_digest.blank? || password_digest_changed?)
def validate_password?
will_save_change_to_password_digest? || (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
Comment on lines 39 to 48
Copy link
Contributor Author

Choose a reason for hiding this comment

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

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.


def just_changed_password?
Expand All @@ -59,9 +57,10 @@ def expire_password_token

def generate_lost_password_token
token = SecureRandom.hex(32)
return unless update_columns(lost_password_token: token, lost_password_token_generated_at: Time.current)
self.lost_password_token = token
self.lost_password_token_generated_at = Time.current

token
token if save(validate: false)
end

def generate_lost_password_token!
Expand All @@ -82,7 +81,7 @@ def update_password(new_password, new_password_confirmation)
end

def using_password?
password_digest.present?
password_digest_in_database.present?
end

def can_set_password?
Expand Down
2 changes: 1 addition & 1 deletion app/lib/logic/provider_signup.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def ensure_users(count)
def signup_user
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}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

In order to exclude "John Doe" from strong password requirement, I added a new signup type :sample_data to identify it.

signup_params = ::Signup::SignupParams.new(plans: [], user_attributes: user_attributes, account_attributes: { org_name: 'Developer' })
::Signup::DeveloperAccountManager.new(@provider).create(signup_params)
end
Expand Down
2 changes: 1 addition & 1 deletion app/lib/signup/developer_account_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def persist!(result, plans, defaults)

# TODO: Temporary here. A new object should have the responsability to activate and approve when needed
# As part of THREESCALE-1317
result.user_activate! if result.user_activate_on_minimal_signup?
result.user_activate! if result.user_activate_on_minimal_or_sample_data?
end
end
end
2 changes: 1 addition & 1 deletion app/lib/signup/result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def local_initialize; end
attr_reader :user, :account

delegate :approve, :approve!, :approved?, :approval_required?, :make_pending!, to: :account, prefix: true
delegate :activate, :activate!, :active?, :activate_on_minimal_signup?, to: :user, prefix: true
delegate :activate, :activate!, :active?, :activate_on_minimal_or_sample_data?, to: :user, prefix: true
delegate :id, to: :account

def valid?
Expand Down
15 changes: 6 additions & 9 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -344,14 +344,6 @@ def provider_id_for_audits
account.try!(:provider_id_for_audits) || provider_account.try!(:provider_id_for_audits)
end

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
Comment on lines -347 to -353
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Dead code AFAIK


protected

def account_for_sphinx
Expand Down Expand Up @@ -436,6 +428,11 @@ def minimal?
signup_type == :minimal
end

def sample_data?
# This is true only for John Doe
signup_type == :sample_data
end

def api?
signup_type == :api
end
Expand All @@ -457,7 +454,7 @@ def cas?
end

def machine?
minimal? || api? || created_by_provider? || open_id? || cas? || oauth2?
minimal? || sample_data? || api? || created_by_provider? || open_id? || cas? || oauth2?
end

def by_user?
Expand Down
4 changes: 2 additions & 2 deletions app/models/user/states.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ def make_activation_code
self.activation_code = self.class.make_token
end

def activate_on_minimal_signup?
minimal_signup? && password.present? && !account.try!(:bought_account_plan).try!(:approval_required?)
def activate_on_minimal_or_sample_data?
(minimal_signup? || signup.sample_data?) && password.present? && !account.try!(:bought_account_plan).try!(:approval_required?)
Comment on lines +78 to +79
Copy link
Contributor Author

Choose a reason for hiding this comment

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

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.

end

def generate_email_verification_token
Expand Down
1 change: 0 additions & 1 deletion app/representers/settings_representer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ module SettingsRepresenter
property :hide_service
property :signups_enabled
property :account_approval_required
property :strong_passwords_enabled
property :public_search
property :account_plans_ui_visible
property :change_account_plan_permission
Expand Down
2 changes: 1 addition & 1 deletion app/views/buyers/accounts/new.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
</div>
<%= 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 %>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Password input was in clear text

<% end %>
</section>

Expand Down
4 changes: 2 additions & 2 deletions app/views/buyers/users/edit.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
<% end %>

<%= form.inputs :name => "Change Password" do %>
<%= form.input :password, required: true, input_html: { autocomplete: 'off' } %>
<%= form.input :password_confirmation, required: true, input_html: { autocomplete: 'off' } %>
<%= form.input :password, required: false, input_html: { autocomplete: 'off' } %>
<%= form.input :password_confirmation, required: false, input_html: { autocomplete: 'off' } %>
Comment on lines +16 to +17
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was wrong, there's no scenario when it's required to change a password in this form.

Copy link
Contributor

Choose a reason for hiding this comment

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

In fact, it was a "fake" requirement, the form submit was working without provided values.

<% end -%>

<% if can? :update_role, @user %>
Expand Down
4 changes: 2 additions & 2 deletions app/views/provider/admin/account/users/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
<% end %>

<%= form.inputs :name => "Change Password" do %>
<%= form.input :password, :required => true %>
<%= form.input :password_confirmation, :required => true %>
<%= form.input :password, :required => false %>
<%= form.input :password_confirmation, :required => false %>
<% end -%>

<% if can? :update_role, @user %>
Expand Down
18 changes: 5 additions & 13 deletions app/views/provider/admin/user/personal_details/edit.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,6 @@
- content_for :javascripts do
= stylesheet_packs_chunks_tag 'pf_form'

- if current_user.can_set_password? && !current_account.settings.enforce_sso?
- content_for :page_header_alert do
br
= pf_inline_alert t('.set_password_html', href: new_provider_password_path), variant: :info

- using_password = current_user.using_password?

div class="pf-c-card"
div class="pf-c-card__body"
= semantic_form_for current_user, builder: Fields::PatternflyFormBuilder,
Expand All @@ -20,13 +13,12 @@ div class="pf-c-card"

= form.inputs 'User Information' do
= form.user_defined_form
- if using_password
= form.input :password, as: :patternfly_input,
label: t('.new_password_label'),
required: current_user.password_required?,
input_html: { value: '', required: current_user.password_required? }
= 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?
Comment on lines +16 to +21
Copy link
Contributor Author

@jlledom jlledom Feb 6, 2026

Choose a reason for hiding this comment

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

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.

= form.inputs "Provide your current password and update your personal details" do
= form.input :current_password, as: :patternfly_input,
required: true,
Expand Down
4 changes: 0 additions & 4 deletions app/views/sites/usage_rules/edit.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,6 @@ div class="pf-c-card"
input_html: { disabled: settings.approval_required_disabled? },
hint: account_approval_required_hint(current_account)

= form.inputs 'Users'
= form.input :strong_passwords_enabled, as: :patternfly_checkbox,
hint: t("formtastic.hints.settings.strong_passwords_enabled", strong_password_definition: strong_password_definition)

= form.inputs 'Search'
= form.input :public_search, as: :patternfly_checkbox

Expand Down
1 change: 1 addition & 0 deletions config/examples/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ development:
force_ssl: false
report_traffic: false
secure_cookie: false
strong_passwords_disabled: true

test:
<<: *default
Expand Down
6 changes: 2 additions & 4 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@ en:

invitee_signups:
create:
error: There are some errors:\n%{errors}
error: "There are some errors:\n%{errors}"
success: Thanks for signing up! You can now sign in
already_logged_in: You are already signed up. Log out if you want to sign up again

Expand Down Expand Up @@ -897,7 +897,6 @@ en:
edit:
header_title: Personal Details
submit_button_label: Update Details
set_password_html: You have no password set. If you'd like to set one use the <a href="%{href}">password reset form</a>.
new_password_label: New password
update:
success: User was successfully updated
Expand Down Expand Up @@ -1930,6 +1929,7 @@ en:
blank: "can't be blank"
password:
blank: "can't be blank"
weak: "Password must be at least %{min_size} characters long, and contain only valid characters."
current_password_incorrect: "is incorrect"
last_admin: "Can't delete last admin"

Expand Down Expand Up @@ -2216,7 +2216,6 @@ en:
approval_required_referer: "Set per account plan from %{link}."
account_approval_required: "Approval is required by you before developer accounts are activated."
useraccountarea_enabled: 'Only disable this if you provide another way to manage this information (e.g. via the User Management API).'
strong_passwords_enabled: "Require strong passwords from your users: %{strong_password_definition} Existing passwords will still work."
public_search: >
Allow anyone to search on your Developer Portal.
If you have Restricted Pages your Developer Portal we advise you not to change this setting.
Expand Down Expand Up @@ -2356,7 +2355,6 @@ en:
hide_service: 'Used a default service plan'
signups_enabled: 'Developers are allowed sign up themselves'
cas_server_url: 'CAS Server URL'
strong_passwords_enabled: 'Strong passwords'
public_search: 'Enable public search on Developer Portal'
cms_escape_draft_html: 'Use Automatic Escaping for draft content'
cms_escape_published_html: 'Use Automatic Escaping for published content'
Expand Down
4 changes: 0 additions & 4 deletions doc/active_docs/account_management_api.json
Original file line number Diff line number Diff line change
Expand Up @@ -9973,10 +9973,6 @@
"type": "boolean",
"description": "Approval is required by you before developer accounts are activated."
},
"strong_passwords_enabled": {
"type": "boolean",
"description": "Require strong passwords from your users: Password must be at least 8 characters long, and contain both upper and lowercase letters, a digit and one special character of -+=><_$#.:;!?@&*()~][}{|. Existing passwords will still work. "
},
"public_search": {
"type": "boolean",
"description": "Enables public search on Developer Portal"
Expand Down
Loading