Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
d746accf38
commit
0e6ff93eba
|
@ -15,6 +15,7 @@
|
||||||
# exclude_group_ids: array of integers
|
# exclude_group_ids: array of integers
|
||||||
# include_parent_descendants: boolean (defaults to false) - includes descendant groups when
|
# include_parent_descendants: boolean (defaults to false) - includes descendant groups when
|
||||||
# filtering by parent. The parent param must be present.
|
# filtering by parent. The parent param must be present.
|
||||||
|
# include_ancestors: boolean (defaults to true)
|
||||||
#
|
#
|
||||||
# Users with full private access can see all groups. The `owned` and `parent`
|
# Users with full private access can see all groups. The `owned` and `parent`
|
||||||
# params can be used to restrict the groups that are returned.
|
# params can be used to restrict the groups that are returned.
|
||||||
|
@ -52,15 +53,7 @@ class GroupsFinder < UnionFinder
|
||||||
return [Group.all] if current_user&.can_read_all_resources? && all_available?
|
return [Group.all] if current_user&.can_read_all_resources? && all_available?
|
||||||
|
|
||||||
groups = []
|
groups = []
|
||||||
|
groups = get_groups_for_user if current_user
|
||||||
if current_user
|
|
||||||
if Feature.enabled?(:use_traversal_ids_groups_finder, current_user)
|
|
||||||
groups << current_user.authorized_groups.self_and_ancestors
|
|
||||||
groups << current_user.groups.self_and_descendants
|
|
||||||
else
|
|
||||||
groups << Gitlab::ObjectHierarchy.new(groups_for_ancestors, groups_for_descendants).all_objects
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
groups << Group.unscoped.public_to_user(current_user) if include_public_groups?
|
groups << Group.unscoped.public_to_user(current_user) if include_public_groups?
|
||||||
groups << Group.none if groups.empty?
|
groups << Group.none if groups.empty?
|
||||||
|
@ -136,4 +129,29 @@ class GroupsFinder < UnionFinder
|
||||||
def min_access_level?
|
def min_access_level?
|
||||||
current_user && params[:min_access_level].present?
|
current_user && params[:min_access_level].present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def include_ancestors?
|
||||||
|
params.fetch(:include_ancestors, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_groups_for_user
|
||||||
|
groups = []
|
||||||
|
|
||||||
|
if Feature.enabled?(:use_traversal_ids_groups_finder, current_user)
|
||||||
|
groups << if include_ancestors?
|
||||||
|
current_user.authorized_groups.self_and_ancestors
|
||||||
|
else
|
||||||
|
current_user.authorized_groups
|
||||||
|
end
|
||||||
|
|
||||||
|
groups << current_user.groups.self_and_descendants
|
||||||
|
elsif include_ancestors?
|
||||||
|
groups << Gitlab::ObjectHierarchy.new(groups_for_ancestors, groups_for_descendants).all_objects
|
||||||
|
else
|
||||||
|
groups << current_user.authorized_groups
|
||||||
|
groups << Gitlab::ObjectHierarchy.new(groups_for_descendants).base_and_descendants
|
||||||
|
end
|
||||||
|
|
||||||
|
groups
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -261,6 +261,7 @@ class User < ApplicationRecord
|
||||||
presence: true,
|
presence: true,
|
||||||
numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
|
numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
|
||||||
validates :username, presence: true
|
validates :username, presence: true
|
||||||
|
validate :check_password_weakness, if: :encrypted_password_changed?
|
||||||
|
|
||||||
validates :namespace, presence: true
|
validates :namespace, presence: true
|
||||||
validate :namespace_move_dir_allowed, if: :username_changed?
|
validate :namespace_move_dir_allowed, if: :username_changed?
|
||||||
|
@ -2314,6 +2315,14 @@ class User < ApplicationRecord
|
||||||
errors.add(:username, _('ending with a reserved file extension is not allowed.'))
|
errors.add(:username, _('ending with a reserved file extension is not allowed.'))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def check_password_weakness
|
||||||
|
if Feature.enabled?(:block_weak_passwords) &&
|
||||||
|
password.present? &&
|
||||||
|
Security::WeakPasswords.weak_for_user?(password, self)
|
||||||
|
errors.add(:password, _('must not contain commonly used combinations of words and letters'))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def groups_with_developer_maintainer_project_access
|
def groups_with_developer_maintainer_project_access
|
||||||
project_creation_levels = [::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS]
|
project_creation_levels = [::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS]
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
name: block_weak_passwords
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86310
|
||||||
|
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/363445
|
||||||
|
milestone: '15.4'
|
||||||
|
type: development
|
||||||
|
group: group::authentication and authorization
|
||||||
|
default_enabled: false
|
|
@ -218,6 +218,7 @@ Settings.gitlab['impersonation_enabled'] ||= true if Settings.gitlab['impersonat
|
||||||
Settings.gitlab['usage_ping_enabled'] = true if Settings.gitlab['usage_ping_enabled'].nil?
|
Settings.gitlab['usage_ping_enabled'] = true if Settings.gitlab['usage_ping_enabled'].nil?
|
||||||
Settings.gitlab['max_request_duration_seconds'] ||= 57
|
Settings.gitlab['max_request_duration_seconds'] ||= 57
|
||||||
Settings.gitlab['display_initial_root_password'] = false if Settings.gitlab['display_initial_root_password'].nil?
|
Settings.gitlab['display_initial_root_password'] = false if Settings.gitlab['display_initial_root_password'].nil?
|
||||||
|
Settings.gitlab['weak_passwords_digest_set'] ||= YAML.safe_load(File.open(Rails.root.join('config', 'weak_password_digests.yml')), permitted_classes: [String]).to_set.freeze
|
||||||
|
|
||||||
Gitlab.ee do
|
Gitlab.ee do
|
||||||
Settings.gitlab['mirror_max_delay'] ||= 300
|
Settings.gitlab['mirror_max_delay'] ||= 300
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -103,8 +103,11 @@ When applications are deleted, all grants and tokens associated with the applica
|
||||||
|
|
||||||
## Authorized applications
|
## Authorized applications
|
||||||
|
|
||||||
Every application you authorize with your GitLab credentials is shown
|
To see all the application you've authorized with your GitLab credentials:
|
||||||
in the **Authorized applications** section under **Settings > Applications**.
|
|
||||||
|
1. On the top bar, in the top right corner, select your avatar.
|
||||||
|
1. Select **Edit profile** and then select **Applications**.
|
||||||
|
1. Scroll down to the **Authorized applications** section.
|
||||||
|
|
||||||
The GitLab OAuth 2 applications support scopes, which allow various actions that any given
|
The GitLab OAuth 2 applications support scopes, which allow various actions that any given
|
||||||
application can perform. Available scopes are depicted in the following table.
|
application can perform. Available scopes are depicted in the following table.
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
module Security
|
||||||
|
module WeakPasswords
|
||||||
|
# These words are predictable in GitLab's specific context, and
|
||||||
|
# therefore cannot occur anywhere within a password.
|
||||||
|
FORBIDDEN_WORDS = Set['gitlab', 'devops'].freeze
|
||||||
|
|
||||||
|
# Substrings shorter than this may appear legitimately in a truly
|
||||||
|
# random password.
|
||||||
|
MINIMUM_SUBSTRING_SIZE = 4
|
||||||
|
|
||||||
|
class << self
|
||||||
|
# Returns true when the password is on a list of weak passwords,
|
||||||
|
# or contains predictable substrings derived from user attributes.
|
||||||
|
# Case insensitive.
|
||||||
|
def weak_for_user?(password, user)
|
||||||
|
forbidden_word_appears_in_password?(password) ||
|
||||||
|
name_appears_in_password?(password, user) ||
|
||||||
|
username_appears_in_password?(password, user) ||
|
||||||
|
email_appears_in_password?(password, user) ||
|
||||||
|
password_on_weak_list?(password)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def forbidden_word_appears_in_password?(password)
|
||||||
|
contains_predicatable_substring?(password, FORBIDDEN_WORDS)
|
||||||
|
end
|
||||||
|
|
||||||
|
def name_appears_in_password?(password, user)
|
||||||
|
return false if user.name.blank?
|
||||||
|
|
||||||
|
# Check for the full name
|
||||||
|
substrings = [user.name]
|
||||||
|
# Also check parts of their name
|
||||||
|
substrings += user.name.split(/[^\p{Alnum}]/)
|
||||||
|
|
||||||
|
contains_predicatable_substring?(password, substrings)
|
||||||
|
end
|
||||||
|
|
||||||
|
def username_appears_in_password?(password, user)
|
||||||
|
return false if user.username.blank?
|
||||||
|
|
||||||
|
# Check for the full username
|
||||||
|
substrings = [user.username]
|
||||||
|
# Also check sub-strings in the username
|
||||||
|
substrings += user.username.split(/[^\p{Alnum}]/)
|
||||||
|
|
||||||
|
contains_predicatable_substring?(password, substrings)
|
||||||
|
end
|
||||||
|
|
||||||
|
def email_appears_in_password?(password, user)
|
||||||
|
return false if user.email.blank?
|
||||||
|
|
||||||
|
# Check for the full email
|
||||||
|
substrings = [user.email]
|
||||||
|
# Also check full first part and full domain name
|
||||||
|
substrings += user.email.split("@")
|
||||||
|
# And any parts of non-word characters (e.g. firstname.lastname+tag@...)
|
||||||
|
substrings += user.email.split(/[^\p{Alnum}]/)
|
||||||
|
|
||||||
|
contains_predicatable_substring?(password, substrings)
|
||||||
|
end
|
||||||
|
|
||||||
|
def password_on_weak_list?(password)
|
||||||
|
# Our weak list stores SHA2 hashes of passwords, not the weak
|
||||||
|
# passwords themselves.
|
||||||
|
digest = Digest::SHA256.base64digest(password.downcase)
|
||||||
|
Settings.gitlab.weak_passwords_digest_set.include?(digest)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Case-insensitively checks whether a password includes a dynamic
|
||||||
|
# list of substrings. Substrings which are too short are not
|
||||||
|
# predictable and may occur randomly, and therefore not checked.
|
||||||
|
def contains_predicatable_substring?(password, substrings)
|
||||||
|
substrings = substrings.filter_map do |substring|
|
||||||
|
substring.downcase if substring.length >= MINIMUM_SUBSTRING_SIZE
|
||||||
|
end
|
||||||
|
|
||||||
|
password = password.downcase
|
||||||
|
|
||||||
|
# Returns true when a predictable substring occurs anywhere
|
||||||
|
# in the password.
|
||||||
|
substrings.any? { |word| password.include?(word) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -47735,6 +47735,9 @@ msgstr ""
|
||||||
msgid "must match %{association}.project_id"
|
msgid "must match %{association}.project_id"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "must not contain commonly used combinations of words and letters"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "my-awesome-group"
|
msgid "my-awesome-group"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,7 @@ module QA
|
||||||
alias_method :ldap_username, :username
|
alias_method :ldap_username, :username
|
||||||
|
|
||||||
def password
|
def password
|
||||||
@password || 'password'
|
@password ||= SecureRandom.hex(8)
|
||||||
end
|
end
|
||||||
alias_method :ldap_password, :password
|
alias_method :ldap_password, :password
|
||||||
|
|
||||||
|
|
|
@ -23,14 +23,15 @@ RSpec.describe QA::Resource::User do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#password' do
|
describe '#password' do
|
||||||
it 'generates a default password' do
|
it 'generates a random 16 character password by default' do
|
||||||
expect(subject.password).to eq('password')
|
expect(subject.password).to match(/\w{16}/)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'is possible to set the password' do
|
it 'is possible to set the password' do
|
||||||
subject.password = 'secret'
|
new_password = "21c7a808"
|
||||||
|
subject.password = new_password
|
||||||
|
|
||||||
expect(subject.password).to eq('secret')
|
expect(subject.password).to eq(new_password)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -493,6 +493,33 @@ RSpec.describe RegistrationsController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when the password is weak' do
|
||||||
|
render_views
|
||||||
|
let_it_be(:new_user_params) { { new_user: base_user_params.merge({ password: "password" }) } }
|
||||||
|
|
||||||
|
subject { post(:create, params: new_user_params) }
|
||||||
|
|
||||||
|
context 'when block_weak_passwords is enabled (default)' do
|
||||||
|
it 'renders the form with errors' do
|
||||||
|
expect { subject }.not_to change(User, :count)
|
||||||
|
|
||||||
|
expect(controller.current_user).to be_nil
|
||||||
|
expect(response).to render_template(:new)
|
||||||
|
expect(response.body).to include(_('Password must not contain commonly used combinations of words and letters'))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when block_weak_passwords is disabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(block_weak_passwords: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'permits weak passwords' do
|
||||||
|
expect { subject }.to change(User, :count).by(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#destroy' do
|
describe '#destroy' do
|
||||||
|
|
|
@ -261,6 +261,108 @@ RSpec.describe GroupsFinder do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with include_ancestors' do
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
|
let_it_be(:parent_group) { create(:group, :public) }
|
||||||
|
let_it_be(:public_subgroup) { create(:group, :public, parent: parent_group) }
|
||||||
|
let_it_be(:public_subgroup2) { create(:group, :public, parent: parent_group) }
|
||||||
|
let_it_be(:private_subgroup1) { create(:group, :private, parent: parent_group) }
|
||||||
|
let_it_be(:internal_sub_subgroup) { create(:group, :internal, parent: public_subgroup) }
|
||||||
|
let_it_be(:public_sub_subgroup) { create(:group, :public, parent: public_subgroup) }
|
||||||
|
let_it_be(:private_subgroup2) { create(:group, :private, parent: parent_group) }
|
||||||
|
let_it_be(:private_sub_subgroup) { create(:group, :private, parent: private_subgroup2) }
|
||||||
|
let_it_be(:private_sub_sub_subgroup) { create(:group, :private, parent: private_sub_subgroup) }
|
||||||
|
|
||||||
|
context 'if include_ancestors is true' do
|
||||||
|
let(:params) { { include_ancestors: true } }
|
||||||
|
|
||||||
|
it 'returns ancestors of user groups' do
|
||||||
|
private_sub_subgroup.add_developer(user)
|
||||||
|
|
||||||
|
expect(described_class.new(user, params).execute).to contain_exactly(
|
||||||
|
parent_group,
|
||||||
|
public_subgroup,
|
||||||
|
public_subgroup2,
|
||||||
|
internal_sub_subgroup,
|
||||||
|
public_sub_subgroup,
|
||||||
|
private_subgroup2,
|
||||||
|
private_sub_subgroup,
|
||||||
|
private_sub_sub_subgroup
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns subgroup if user is member of project of subgroup' do
|
||||||
|
project = create(:project, :private, namespace: private_sub_subgroup)
|
||||||
|
project.add_developer(user)
|
||||||
|
|
||||||
|
expect(described_class.new(user, params).execute).to contain_exactly(
|
||||||
|
parent_group,
|
||||||
|
public_subgroup,
|
||||||
|
public_subgroup2,
|
||||||
|
internal_sub_subgroup,
|
||||||
|
public_sub_subgroup,
|
||||||
|
private_subgroup2,
|
||||||
|
private_sub_subgroup
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns only groups related to user groups if all_available is false' do
|
||||||
|
params[:all_available] = false
|
||||||
|
private_sub_subgroup.add_developer(user)
|
||||||
|
|
||||||
|
expect(described_class.new(user, params).execute).to contain_exactly(
|
||||||
|
parent_group,
|
||||||
|
private_subgroup2,
|
||||||
|
private_sub_subgroup,
|
||||||
|
private_sub_sub_subgroup
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'if include_ancestors is false' do
|
||||||
|
let(:params) { { include_ancestors: false } }
|
||||||
|
|
||||||
|
it 'does not return private ancestors of user groups' do
|
||||||
|
private_sub_subgroup.add_developer(user)
|
||||||
|
|
||||||
|
expect(described_class.new(user, params).execute).to contain_exactly(
|
||||||
|
parent_group,
|
||||||
|
public_subgroup,
|
||||||
|
public_subgroup2,
|
||||||
|
internal_sub_subgroup,
|
||||||
|
public_sub_subgroup,
|
||||||
|
private_sub_subgroup,
|
||||||
|
private_sub_sub_subgroup
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns project's parent group if user is member of project" do
|
||||||
|
project = create(:project, :private, namespace: private_sub_subgroup)
|
||||||
|
project.add_developer(user)
|
||||||
|
|
||||||
|
expect(described_class.new(user, params).execute).to contain_exactly(
|
||||||
|
parent_group,
|
||||||
|
public_subgroup,
|
||||||
|
public_subgroup2,
|
||||||
|
internal_sub_subgroup,
|
||||||
|
public_sub_subgroup,
|
||||||
|
private_sub_subgroup
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns only user groups and their descendants if all_available is false' do
|
||||||
|
params[:all_available] = false
|
||||||
|
private_sub_subgroup.add_developer(user)
|
||||||
|
|
||||||
|
expect(described_class.new(user, params).execute).to contain_exactly(
|
||||||
|
private_sub_subgroup,
|
||||||
|
private_sub_sub_subgroup
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#execute' do
|
describe '#execute' do
|
||||||
|
|
|
@ -58,4 +58,40 @@ RSpec.describe Settings do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "#weak_passwords_digest_set" do
|
||||||
|
subject { described_class.gitlab.weak_passwords_digest_set }
|
||||||
|
|
||||||
|
it 'is a Set' do
|
||||||
|
expect(subject).to be_kind_of(Set)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'contains 4500 password digests' do
|
||||||
|
expect(subject.length).to eq(4500)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes 8 char weak password digest' do
|
||||||
|
expect(subject).to include(digest("password"))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes 16 char weak password digest' do
|
||||||
|
expect(subject).to include(digest("progressivehouse"))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes long char weak password digest' do
|
||||||
|
expect(subject).to include(digest("01234567890123456789"))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not include 7 char weak password digest' do
|
||||||
|
expect(subject).not_to include(digest("1234567"))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not include plaintext' do
|
||||||
|
expect(subject).not_to include("password")
|
||||||
|
end
|
||||||
|
|
||||||
|
def digest(plaintext)
|
||||||
|
Digest::SHA256.base64digest(plaintext)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Security::WeakPasswords do
|
||||||
|
describe "#weak_for_user?" do
|
||||||
|
using RSpec::Parameterized::TableSyntax
|
||||||
|
|
||||||
|
let(:user) do
|
||||||
|
build_stubbed(:user, username: "56d4ab689a_win",
|
||||||
|
name: "Weakést McWeaky-Pass Jr",
|
||||||
|
email: "predictāble.ZZZ+seventeen@examplecorp.com",
|
||||||
|
public_email: "fortunate@acme.com"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
where(:password, :too_weak) do
|
||||||
|
# A random password is not too weak
|
||||||
|
"d2262d56" | false
|
||||||
|
|
||||||
|
# The case-insensitive weak password list
|
||||||
|
"password" | true
|
||||||
|
"pAssWord" | true
|
||||||
|
"princeofdarkness" | true
|
||||||
|
|
||||||
|
# Forbidden substrings
|
||||||
|
"A1B2gitlabC3" | true
|
||||||
|
"gitlab123" | true
|
||||||
|
"theonedevopsplatform" | true
|
||||||
|
"A1gitlib" | false
|
||||||
|
|
||||||
|
# Predicatable name substrings
|
||||||
|
"Aweakést" | true
|
||||||
|
"!@mCwEaKy" | true
|
||||||
|
"A1B2pass" | true
|
||||||
|
"A1B2C3jr" | false # jr is too short
|
||||||
|
|
||||||
|
# Predictable username substrings
|
||||||
|
"56d4ab689a" | true
|
||||||
|
"56d4ab689a_win" | true
|
||||||
|
"56d4ab68" | false # it's part of the username, but not a full part
|
||||||
|
"A1B2Cwin" | false # win is too short
|
||||||
|
|
||||||
|
# Predictable user.email substrings
|
||||||
|
"predictāble.ZZZ+seventeen@examplecorp.com" | true
|
||||||
|
"predictable.ZZZ+seventeen@examplecorp.com" | true
|
||||||
|
"predictāble.ZZZ+seventeen" | true
|
||||||
|
"examplecorp.com" | true
|
||||||
|
"!@exAmplecorp" | true
|
||||||
|
"predictāble123" | true
|
||||||
|
"seventeen" | true
|
||||||
|
"predictable" | false # the accent is different
|
||||||
|
"A1B2CZzZ" | false # ZZZ is too short
|
||||||
|
# Other emails are not considered
|
||||||
|
"fortunate@acme.com" | false
|
||||||
|
"A1B2acme" | false
|
||||||
|
"fortunate" | false
|
||||||
|
|
||||||
|
# A short password is not automatically too weak
|
||||||
|
# We rely on User's password length validation, not WeakPasswords.
|
||||||
|
"1" | false
|
||||||
|
"1234567" | false
|
||||||
|
# But a short password with forbidden words or user attributes
|
||||||
|
# is still weak
|
||||||
|
"gitlab" | true
|
||||||
|
"pass" | true
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
it { expect(subject.weak_for_user?(password, user)).to eq(too_weak) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a user who has short email parts' do
|
||||||
|
before do
|
||||||
|
user.email = 'sid@1.io'
|
||||||
|
end
|
||||||
|
|
||||||
|
where(:password, :too_weak) do
|
||||||
|
"11111111" | true # This is on the weak password list
|
||||||
|
"1.ioABCD" | true # 1.io is long enough to match
|
||||||
|
"sid@1.io" | true # matches the email in full
|
||||||
|
"sid@1.ioAB" | true
|
||||||
|
# sid, 1, and io on their own are too short
|
||||||
|
"sid1ioAB" | false
|
||||||
|
"sidsidsi" | false
|
||||||
|
"ioioioio" | false
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
it { expect(subject.weak_for_user?(password, user)).to eq(too_weak) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a user who is missing attributes' do
|
||||||
|
before do
|
||||||
|
user.name = nil
|
||||||
|
user.email = nil
|
||||||
|
user.username = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
where(:password, :too_weak) do
|
||||||
|
"d2262d56" | false
|
||||||
|
"password" | true
|
||||||
|
"gitlab123" | true
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
it { expect(subject.weak_for_user?(password, user)).to eq(too_weak) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -334,6 +334,58 @@ RSpec.describe User do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'check_password_weakness' do
|
||||||
|
let(:weak_password) { "qwertyuiop" }
|
||||||
|
|
||||||
|
context 'when feature flag is disabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(block_weak_passwords: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not add an error when password is weak' do
|
||||||
|
expect(Security::WeakPasswords).not_to receive(:weak_for_user?)
|
||||||
|
|
||||||
|
user.password = weak_password
|
||||||
|
expect(user).to be_valid
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when feature flag is enabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(block_weak_passwords: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'checks for password weakness when password changes' do
|
||||||
|
expect(Security::WeakPasswords).to receive(:weak_for_user?)
|
||||||
|
.with(weak_password, user).and_call_original
|
||||||
|
user.password = weak_password
|
||||||
|
expect(user).not_to be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'adds an error when password is weak' do
|
||||||
|
user.password = weak_password
|
||||||
|
expect(user).not_to be_valid
|
||||||
|
expect(user.errors).to be_of_kind(:password, 'must not contain commonly used combinations of words and letters')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is valid when password is not weak' do
|
||||||
|
user.password = ::User.random_password
|
||||||
|
expect(user).to be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is valid when weak password was already set' do
|
||||||
|
user = build(:user, password: weak_password)
|
||||||
|
user.save!(validate: false)
|
||||||
|
|
||||||
|
expect(Security::WeakPasswords).not_to receive(:weak_for_user?)
|
||||||
|
|
||||||
|
# Change an unrelated value
|
||||||
|
user.name = "Example McExampleFace"
|
||||||
|
expect(user).to be_valid
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'name' do
|
describe 'name' do
|
||||||
|
|
Loading…
Reference in New Issue