diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index d9416cb10c4289f63e7e7909276360cc5e228dcc..71e4195c50fca06f5355d356e1aaa22d75da9760 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -232,6 +232,7 @@ module ApplicationSettingsHelper :metrics_port, :metrics_sample_interval, :metrics_timeout, + :minimum_password_length, :mirror_available, :pages_domain_verification_enabled, :password_authentication_enabled_for_web, diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index e764b6c56b05368c5c4b783c69208e0f345a374e..456b643008851bdb978a7ceaa1926e41e60a70a6 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -46,6 +46,12 @@ class ApplicationSetting < ApplicationRecord presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validates :minimum_password_length, + presence: true, + numericality: { only_integer: true, + greater_than_or_equal_to: DEFAULT_MINIMUM_PASSWORD_LENGTH, + less_than_or_equal_to: Devise.password_length.max } + validates :home_page_url, allow_blank: true, addressable_url: true, diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index e1eb8d429bb94f8a723ac515a91e234bcf1308bb..98d8bb43b93f63615967bbc4e32446b3c23e532a 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -30,6 +30,8 @@ module ApplicationSettingImplementation '/admin/session' ].freeze + DEFAULT_MINIMUM_PASSWORD_LENGTH = 8 + class_methods do def defaults { @@ -106,6 +108,7 @@ module ApplicationSettingImplementation sourcegraph_enabled: false, sourcegraph_url: nil, sourcegraph_public_only: true, + minimum_password_length: DEFAULT_MINIMUM_PASSWORD_LENGTH, terminal_max_session_time: 0, throttle_authenticated_api_enabled: false, throttle_authenticated_api_period_in_seconds: 3600, diff --git a/app/models/user.rb b/app/models/user.rb index 698848c5b16a5f9e4cc4bd4013dc60ce388ce555..7bb376e6e068f246357c4f8d3645f24612390940 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -377,6 +377,11 @@ class User < ApplicationRecord # Class methods # class << self + # Devise method overridden to allow support for dynamic password lengths + def password_length + Gitlab::CurrentSettings.minimum_password_length..Devise.password_length.max + end + # Devise method overridden to allow sign in with email or username def find_for_database_authentication(warden_conditions) conditions = warden_conditions.dup diff --git a/app/services/users/build_service.rb b/app/services/users/build_service.rb index 8c85ad9ffd8b9c6e0ca3ab7439470721aae5449a..ea4d11e728e09ef95dac67cbca45290b0ac3c88d 100644 --- a/app/services/users/build_service.rb +++ b/app/services/users/build_service.rb @@ -23,7 +23,7 @@ module Users @reset_token = user.generate_reset_token if params[:reset_password] if user_params[:force_random_password] - random_password = Devise.friendly_token.first(Devise.password_length.min) + random_password = Devise.friendly_token.first(User.password_length.min) user.password = user.password_confirmation = random_password end end diff --git a/app/views/admin/application_settings/_signup.html.haml b/app/views/admin/application_settings/_signup.html.haml index 7c1df78f30c97cb827b41ad3db0253856341c249..b9d9d86ca30a8f7fc1a4720da5e4a6ab2283dd9c 100644 --- a/app/views/admin/application_settings/_signup.html.haml +++ b/app/views/admin/application_settings/_signup.html.haml @@ -12,6 +12,12 @@ = f.check_box :send_user_confirmation_email, class: 'form-check-input' = f.label :send_user_confirmation_email, class: 'form-check-label' do Send confirmation email on sign-up + .form-group + = f.label :minimum_password_length, _('Minimum password length (number of characters)'), class: 'label-bold' + = f.number_field :minimum_password_length, class: 'form-control', rows: 4, min: ApplicationSetting::DEFAULT_MINIMUM_PASSWORD_LENGTH, max: Devise.password_length.max + - password_policy_guidelines_link = link_to _('Password Policy Guidelines'), 'https://about.gitlab.com/handbook/security/#gitlab-password-policy-guidelines', target: '_blank', rel: 'noopener noreferrer nofollow' + .form-text.text-muted + = _("See GitLab's %{password_policy_guidelines}").html_safe % { password_policy_guidelines: password_policy_guidelines_link } .form-group = f.label :domain_whitelist, 'Whitelisted domains for sign-ups', class: 'label-bold' = f.text_area :domain_whitelist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8 diff --git a/changelogs/unreleased/36776-entropy-requirements-for-new-user-passwords-mvc.yml b/changelogs/unreleased/36776-entropy-requirements-for-new-user-passwords-mvc.yml new file mode 100644 index 0000000000000000000000000000000000000000..6b2e159c273e7862b649481dd2a9778b9e1709b4 --- /dev/null +++ b/changelogs/unreleased/36776-entropy-requirements-for-new-user-passwords-mvc.yml @@ -0,0 +1,5 @@ +--- +title: Allow administrators to set a minimum password length +merge_request: 20661 +author: +type: added diff --git a/config/initializers/devise_dynamic_password_length_validation.rb b/config/initializers/devise_dynamic_password_length_validation.rb new file mode 100644 index 0000000000000000000000000000000000000000..e71b28bc495f2861ce5b3f353b8ce88d0c8ca0a4 --- /dev/null +++ b/config/initializers/devise_dynamic_password_length_validation.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +# Discard the default Devise length validation from the `User` model. + +# This needs to be discarded because the length validation provided by Devise does not +# support dynamically checking for min and max lengths. + +# A new length validation has been added to the User model instead, to keep supporting +# dynamic password length validations, like: + +# validates :password, length: { maximum: proc { password_length.max }, minimum: proc { password_length.min } }, allow_blank: true + +def length_validator_supports_dynamic_length_checks?(validator) + validator.options[:minimum].is_a?(Proc) && + validator.options[:maximum].is_a?(Proc) +end + +# Get the in-built Devise validator on password length. +password_length_validator = User.validators_on(:password).find do |validator| + validator.kind == :length +end + +# This initializer can be removed as soon as https://github.com/plataformatec/devise/pull/5166 +# is merged into Devise. +if length_validator_supports_dynamic_length_checks?(password_length_validator) + raise "Devise now supports dynamic length checks, please remove the monkey patch in #{__FILE__}" +else + # discard the in-built length validator by always returning true + def password_length_validator.validate(*_) + true + end + + # add a custom password length validator with support for dynamic length validation. + User.class_eval do + validates :password, length: { maximum: proc { password_length.max }, minimum: proc { password_length.min } }, allow_blank: true + end +end diff --git a/db/migrate/20191123062354_add_minimum_password_length_to_application_settings.rb b/db/migrate/20191123062354_add_minimum_password_length_to_application_settings.rb new file mode 100644 index 0000000000000000000000000000000000000000..0a7ad9d81a9ac752914ae629829e399c99b3fe77 --- /dev/null +++ b/db/migrate/20191123062354_add_minimum_password_length_to_application_settings.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddMinimumPasswordLengthToApplicationSettings < ActiveRecord::Migration[5.2] + DOWNTIME = false + + DEFAULT_MINIMUM_PASSWORD_LENGTH = 8 + + def change + add_column(:application_settings, :minimum_password_length, :integer, default: DEFAULT_MINIMUM_PASSWORD_LENGTH, null: false) + end +end diff --git a/db/post_migrate/20191205084057_update_minimum_password_length.rb b/db/post_migrate/20191205084057_update_minimum_password_length.rb new file mode 100644 index 0000000000000000000000000000000000000000..d9324347075fc6d8ee357bf40d37b42414fe2a4c --- /dev/null +++ b/db/post_migrate/20191205084057_update_minimum_password_length.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class UpdateMinimumPasswordLength < ActiveRecord::Migration[5.2] + DOWNTIME = false + + def up + value_to_be_updated_to = [ + Devise.password_length.min, + ApplicationSetting::DEFAULT_MINIMUM_PASSWORD_LENGTH + ].max + + execute "UPDATE application_settings SET minimum_password_length = #{value_to_be_updated_to}" + + ApplicationSetting.expire + end + + def down + value_to_be_updated_to = ApplicationSetting::DEFAULT_MINIMUM_PASSWORD_LENGTH + + execute "UPDATE application_settings SET minimum_password_length = #{value_to_be_updated_to}" + + ApplicationSetting.expire + end +end diff --git a/db/schema.rb b/db/schema.rb index ac2372a63d098cacf87906d39999c6bece078cae..9bc3554e86a19a5f647079d6d1645b711fc9c868 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -351,6 +351,7 @@ ActiveRecord::Schema.define(version: 2019_12_08_071112) do t.string "sourcegraph_url", limit: 255 t.boolean "sourcegraph_public_only", default: true, null: false t.bigint "snippet_size_limit", default: 52428800, null: false + t.integer "minimum_password_length", default: 8, null: false t.text "encrypted_akismet_api_key" t.string "encrypted_akismet_api_key_iv", limit: 255 t.text "encrypted_elasticsearch_aws_secret_access_key" diff --git a/doc/security/password_length_limits.md b/doc/security/password_length_limits.md index 9909ef4a8e4c492bd272f081b4a6887e2153b990..235730eb8256aec55ebca5f512e160b0e4857c42 100644 --- a/doc/security/password_length_limits.md +++ b/doc/security/password_length_limits.md @@ -4,7 +4,19 @@ type: reference, howto # Custom password length limits -The user password length is set to a minimum of 8 characters by default. +By default, GitLab supports passwords with: + +- A minimum length of 8. +- A maximum length of 128. + +GitLab administrators can modify password lengths: + +- Using configuration file. +- [From](https://gitlab.com/gitlab-org/gitlab/merge_requests/20661) GitLab 12.6, using the GitLab UI. + +## Modify maximum password length using configuration file + +The user password length is set to a maximum of 128 characters by default. To change that for installations from source: 1. Edit `devise_password_length.rb`: @@ -18,15 +30,35 @@ To change that for installations from source: 1. Change the new password length limits: ```ruby - config.password_length = 12..128 + config.password_length = 12..135 ``` In this example, the minimum length is 12 characters, and the maximum length - is 128 characters. + is 135 characters. 1. [Restart GitLab](../administration/restart_gitlab.md#installations-from-source) for the changes to take effect. +NOTE: **Note:** +From GitLab 12.6, the minimum password length set in this configuration file will be ignored. Minimum password lengths will now have to be modified via the [GitLab UI](#modify-minimum-password-length-using-gitlab-ui) instead. + +## Modify minimum password length using GitLab UI + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/20661) in GitLab 12.6 + +The user password length is set to a minimum of 8 characters by default. +To change that using GitLab UI: + +In the Admin area under **Settings** (`/admin/application_settings`), go to section **Sign-up Restrictions**. + +[Minimum password length settings](../user/admin_area/img/minimum_password_length_settings_v12_6.png) + +Set the **Minimum password length** to a value greater than or equal to 8 and hit **Save changes** to save the changes. + +CAUTION: **Caution:** +Changing minimum or maximum limit does not affect existing user passwords in any manner. Existing users will not be asked to reset their password to adhere to the new limits. +The new limit restriction will only apply during new user sign-ups and when an existing user performs a password reset. +