Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-09-16 03:13:19 +00:00
parent d746accf38
commit 0e6ff93eba
15 changed files with 5026 additions and 16 deletions

View File

@ -15,6 +15,7 @@
# exclude_group_ids: array of integers
# include_parent_descendants: boolean (defaults to false) - includes descendant groups when
# 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`
# 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?
groups = []
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 = get_groups_for_user if current_user
groups << Group.unscoped.public_to_user(current_user) if include_public_groups?
groups << Group.none if groups.empty?
@ -136,4 +129,29 @@ class GroupsFinder < UnionFinder
def min_access_level?
current_user && params[:min_access_level].present?
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

View File

@ -261,6 +261,7 @@ class User < ApplicationRecord
presence: true,
numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
validates :username, presence: true
validate :check_password_weakness, if: :encrypted_password_changed?
validates :namespace, presence: true
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.'))
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
project_creation_levels = [::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS]

View File

@ -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

View File

@ -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['max_request_duration_seconds'] ||= 57
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
Settings.gitlab['mirror_max_delay'] ||= 300

File diff suppressed because it is too large Load Diff

View File

@ -103,8 +103,11 @@ When applications are deleted, all grants and tokens associated with the applica
## Authorized applications
Every application you authorize with your GitLab credentials is shown
in the **Authorized applications** section under **Settings > Applications**.
To see all the application you've authorized with your GitLab credentials:
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
application can perform. Available scopes are depicted in the following table.

View File

@ -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

View File

@ -47735,6 +47735,9 @@ msgstr ""
msgid "must match %{association}.project_id"
msgstr ""
msgid "must not contain commonly used combinations of words and letters"
msgstr ""
msgid "my-awesome-group"
msgstr ""

View File

@ -44,7 +44,7 @@ module QA
alias_method :ldap_username, :username
def password
@password || 'password'
@password ||= SecureRandom.hex(8)
end
alias_method :ldap_password, :password

View File

@ -23,14 +23,15 @@ RSpec.describe QA::Resource::User do
end
describe '#password' do
it 'generates a default password' do
expect(subject.password).to eq('password')
it 'generates a random 16 character password by default' do
expect(subject.password).to match(/\w{16}/)
end
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

View File

@ -493,6 +493,33 @@ RSpec.describe RegistrationsController do
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
describe '#destroy' do

View File

@ -261,6 +261,108 @@ RSpec.describe GroupsFinder do
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
describe '#execute' do

View File

@ -58,4 +58,40 @@ RSpec.describe Settings do
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

View File

@ -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

View File

@ -334,6 +334,58 @@ RSpec.describe User do
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
describe 'name' do