diff --git a/doc/user/compliance/audit_event_types.md b/doc/user/compliance/audit_event_types.md index 5da74211bb8b6e0370bde139087b10cee228375d..edeb5a7cd9afc5ceb35bdf557bfeffa4cde66da6 100644 --- a/doc/user/compliance/audit_event_types.md +++ b/doc/user/compliance/audit_event_types.md @@ -539,12 +539,15 @@ Audit event types belong to the following product categories. | [`security_policy_access_token_push_bypass`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/196249) | Branch push that is blocked by a security policy is bypassed for configured access token | {{< icon name="check-circle" >}} Yes | GitLab [18.2](https://gitlab.com/gitlab-org/gitlab/-/issues/549644) | Project | | [`security_policy_create`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/192797) | A security policy is created | {{< icon name="check-circle" >}} Yes | GitLab [18.1](https://gitlab.com/gitlab-org/gitlab/-/issues/539230) | Project | | [`security_policy_delete`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/192797) | A security policy is deleted | {{< icon name="check-circle" >}} Yes | GitLab [18.1](https://gitlab.com/gitlab-org/gitlab/-/issues/539230) | Project | +| [`security_policy_group_push_bypass`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/199546) | Branch push that is blocked by a security policy is bypassed for configured groups | {{< icon name="check-circle" >}} Yes | GitLab [18.3](https://gitlab.com/gitlab-org/gitlab/-/issues/549797) | Project | | [`security_policy_limit_exceeded`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/196005) | Enabled policies count exceeded the maximum allowed limit for policy type | {{< icon name="check-circle" >}} Yes | GitLab [18.2](https://gitlab.com/gitlab-org/gitlab/-/work_items/550891) | Project | | [`security_policy_merge_request_merged_with_policy_violations`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/195775) | A merge request merged with security policy violations | {{< icon name="check-circle" >}} Yes | GitLab [18.2](https://gitlab.com/gitlab-org/gitlab/-/work_items/549813) | Project | | [`security_policy_pipeline_failed`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/196628) | A pipeline with security policy jobs failed | {{< icon name="dotted-circle" >}} No | GitLab [18.3](https://gitlab.com/gitlab-org/gitlab/-/issues/539232) | Project | | [`security_policy_pipeline_skipped`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/195325) | A security policy pipeline is skipped | {{< icon name="dotted-circle" >}} No | GitLab [18.2](https://gitlab.com/gitlab-org/gitlab/-/issues/539232) | Project | +| [`security_policy_role_push_bypass`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/199546) | Branch push that is blocked by a security policy is bypassed for configured roles | {{< icon name="check-circle" >}} Yes | GitLab [18.3](https://gitlab.com/gitlab-org/gitlab/-/issues/549797) | Project | | [`security_policy_service_account_push_bypass`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/196249) | Branch push that is blocked by a security policy is bypassed for configured service account | {{< icon name="check-circle" >}} Yes | GitLab [18.2](https://gitlab.com/gitlab-org/gitlab/-/issues/549644) | Project | | [`security_policy_update`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/192797) | A security policy is updated | {{< icon name="check-circle" >}} Yes | GitLab [18.1](https://gitlab.com/gitlab-org/gitlab/-/issues/539230) | Project | +| [`security_policy_user_push_bypass`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/199546) | Branch push that is blocked by a security policy is bypassed for configured users | {{< icon name="check-circle" >}} Yes | GitLab [18.3](https://gitlab.com/gitlab-org/gitlab/-/issues/549797) | Project | | [`security_policy_violations_detected`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/193482) | Security policy violation is detected in the merge request | {{< icon name="dotted-circle" >}} No | GitLab [18.2](https://gitlab.com/gitlab-org/gitlab/-/work_items/549811) | Project | | [`security_policy_violations_resolved`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/193482) | Security policy violations are resolved in the merge request | {{< icon name="dotted-circle" >}} No | GitLab [18.2](https://gitlab.com/gitlab-org/gitlab/-/issues/549812) | Project | | [`security_policy_yaml_invalidated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/196721) | The policy YAML is invalidated in security policy project | {{< icon name="check-circle" >}} Yes | GitLab [18.2](https://gitlab.com/gitlab-org/gitlab/-/work_items/550892) | Project | diff --git a/ee/app/models/ee/group_member.rb b/ee/app/models/ee/group_member.rb index c2b8b73058aa111712613f44234ddb123290a7c3..310270ac70058c41b3a62161272f6efa6239a533 100644 --- a/ee/app/models/ee/group_member.rb +++ b/ee/app/models/ee/group_member.rb @@ -47,6 +47,13 @@ def member_of_group?(group, user) exists?(group: group, user: user) end + def direct_member_of_groups?(group_ids, user) + active_without_invites_and_requests + .non_minimal_access + .where(source_id: group_ids) + .exists?(id: user.id) + end + def filter_by_enterprise_users(value) subquery = ::UserDetail.where( diff --git a/ee/app/models/ee/project_team.rb b/ee/app/models/ee/project_team.rb index af61084968264e5f08c2797a6feb9c11ed79ea71..57f8f75a820a6bc9ab9996179369f54a8af3f8a4 100644 --- a/ee/app/models/ee/project_team.rb +++ b/ee/app/models/ee/project_team.rb @@ -24,6 +24,13 @@ def members_with_access_level_or_custom_roles(levels: [], member_role_ids: []) users end + def user_exists_with_access_level_or_custom_roles?(user, levels: [], member_role_ids: []) + return false unless levels.any? || member_role_ids.any? + return false unless user + + members_with_access_level_or_custom_roles(levels: levels, member_role_ids: member_role_ids).exists?(id: user.id) + end + override :add_members def add_members( users, diff --git a/ee/config/audit_events/types/security_policy_group_push_bypass.yml b/ee/config/audit_events/types/security_policy_group_push_bypass.yml new file mode 100644 index 0000000000000000000000000000000000000000..1e8b8d6905dd17d38c5eb8871c83bc14a9001c0a --- /dev/null +++ b/ee/config/audit_events/types/security_policy_group_push_bypass.yml @@ -0,0 +1,11 @@ +--- +name: security_policy_group_push_bypass +description: Branch push that is blocked by a security policy is bypassed for configured + groups +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/549797 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/199546 +feature_category: security_policy_management +milestone: '18.3' +saved_to_database: true +streamed: true +scope: [Project] diff --git a/ee/config/audit_events/types/security_policy_role_push_bypass.yml b/ee/config/audit_events/types/security_policy_role_push_bypass.yml new file mode 100644 index 0000000000000000000000000000000000000000..6738e61c48b2ca4fa9ee525ccbb9491306c7a148 --- /dev/null +++ b/ee/config/audit_events/types/security_policy_role_push_bypass.yml @@ -0,0 +1,11 @@ +--- +name: security_policy_role_push_bypass +description: Branch push that is blocked by a security policy is bypassed for configured + roles +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/549797 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/199546 +feature_category: security_policy_management +milestone: '18.3' +saved_to_database: true +streamed: true +scope: [Project] diff --git a/ee/config/audit_events/types/security_policy_user_push_bypass.yml b/ee/config/audit_events/types/security_policy_user_push_bypass.yml new file mode 100644 index 0000000000000000000000000000000000000000..f8264f4cdfe4b1336177e2ddebaa8497b71d6ecd --- /dev/null +++ b/ee/config/audit_events/types/security_policy_user_push_bypass.yml @@ -0,0 +1,11 @@ +--- +name: security_policy_user_push_bypass +description: Branch push that is blocked by a security policy is bypassed for configured + users +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/549797 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/199546 +feature_category: security_policy_management +milestone: '18.3' +saved_to_database: true +streamed: true +scope: [Project] diff --git a/ee/lib/ee/gitlab/checks/security/policy_check.rb b/ee/lib/ee/gitlab/checks/security/policy_check.rb index 903fddd8e35995d38cbeb8fb997b33bc7a10a881..698ec77812ce48b0fd89bdbf9617194decb8eb0a 100644 --- a/ee/lib/ee/gitlab/checks/security/policy_check.rb +++ b/ee/lib/ee/gitlab/checks/security/policy_check.rb @@ -7,6 +7,7 @@ module Security module PolicyCheck PUSH_ERROR_MESSAGE = "Push is blocked by settings overridden by a security policy" FORCE_PUSH_ERROR_MESSAGE = "Force push is blocked by settings overridden by a security policy" + BYPASS_REASON_ERROR_MESSAGE = "Bypass reason is required when bypassing security policy restrictions" LOG_MESSAGE = "Checking if scan result policies apply to branch..." def validate! @@ -37,8 +38,13 @@ def policies_bypass_applied? return false if ::Feature.disabled?(:security_policies_bypass_options_tokens_accounts, project) ::Security::ScanResultPolicies::PushBypassChecker.new( - project: project, user_access: user_access, branch_name: branch_name + project: project, + user_access: user_access, + branch_name: branch_name, + push_options: change_access.push_options ).check_bypass! + rescue ::Security::ScanResultPolicies::PolicyBypassChecker::BypassReasonRequiredError + raise ::Gitlab::GitAccess::ForbiddenError, BYPASS_REASON_ERROR_MESSAGE end def force_push? diff --git a/ee/lib/security/scan_result_policies/policy_bypass_checker.rb b/ee/lib/security/scan_result_policies/policy_bypass_checker.rb index 7f2a4bd0ba7b814770a72f857e1aaf2872f3d7ab..3782a06e980e6c11cae3733a806c1584f1b2ac48 100644 --- a/ee/lib/security/scan_result_policies/policy_bypass_checker.rb +++ b/ee/lib/security/scan_result_policies/policy_bypass_checker.rb @@ -5,19 +5,26 @@ module ScanResultPolicies class PolicyBypassChecker include Gitlab::Utils::StrongMemoize - def initialize(security_policy:, project:, user_access:, branch_name:) + BypassReasonRequiredError = Class.new(StandardError) + + def initialize(security_policy:, project:, user_access:, branch_name:, push_options:) @security_policy = security_policy @project = project @user = user_access.user @branch_name = branch_name + @push_options = push_options end def bypass_allowed? return false unless user - bypass_with_access_token? || bypass_with_service_account? + bypass_with_access_token? || bypass_with_service_account? || bypass_with_user? end + private + + attr_reader :security_policy, :project, :user, :branch_name, :push_options + def bypass_with_access_token? policy_token_ids = security_policy.bypass_settings.access_token_ids return false if policy_token_ids.blank? @@ -27,7 +34,14 @@ def bypass_with_access_token? user_token_ids = user.personal_access_tokens.active.id_in(policy_token_ids).pluck_primary_key return false if user_token_ids.blank? - log_bypass_audit!(:access_token, user_token_ids) + log_bypass_audit!( + :access_token, + bypass_audit_message(:access_token, user_token_ids), + additional_details: { + bypass_type: :access_token, + access_token_ids: user_token_ids + } + ) true end @@ -38,20 +52,99 @@ def bypass_with_service_account? return false unless user.service_account? return false unless policy_service_account_ids.include?(user.id) - log_bypass_audit!(:service_account, user.id) + log_bypass_audit!( + :service_account, + bypass_audit_message(:service_account, user.id), + additional_details: { + bypass_type: :service_account, + service_account_id: user.id + } + ) true end - private + def bypass_with_user? + return false if Feature.disabled?(:security_policies_bypass_options_group_roles, project) - attr_reader :security_policy, :project, :user, :branch_name + users_can_bypass? || groups_can_bypass? || roles_can_bypass? + end - def log_bypass_audit!(type, id) - message = <<~MSG.squish - Branch push restriction on '#{branch_name}' for project '#{project.full_path}' - has been bypassed by #{type} with ID: #{id} - MSG + def users_can_bypass? + return false if user.project_bot? || user.service_account? + return false if security_policy.bypass_settings.user_ids.blank? + return false unless security_policy.bypass_settings.user_ids.include?(user.id) + + reason = reason_from_push_options + raise BypassReasonRequiredError, "Bypass reason is required for user bypass" if reason.blank? + + log_bypass_audit!( + :user, + bypass_audit_message(:user, user.id, reason), + additional_details: { + bypass_type: :user, + user_id: user.id, + reason: reason + } + ) + true + end + + def groups_can_bypass? + group_ids = security_policy.bypass_settings.group_ids + + return false if group_ids.blank? + return false unless GroupMember.direct_member_of_groups?(group_ids, user) + + reason = reason_from_push_options + raise BypassReasonRequiredError, "Bypass reason is required for group bypass" if reason.blank? + + log_bypass_audit!( + :group, + bypass_audit_message(:user, user.id, reason), + additional_details: { + bypass_type: :group, + group_ids: group_ids, + user_id: user.id, + reason: reason + } + ) + true + end + + def roles_can_bypass? + default_roles = security_policy.bypass_settings.default_roles + custom_role_ids = security_policy.bypass_settings.custom_role_ids + + return false if default_roles.blank? && custom_role_ids.blank? + return false unless project.team.user_exists_with_access_level_or_custom_roles?( + user, levels: default_roles, member_role_ids: custom_role_ids + ) + + reason = reason_from_push_options + raise BypassReasonRequiredError, "Bypass reason is required for role bypass" if reason.blank? + + log_bypass_audit!( + :role, + bypass_audit_message(:user, user.id, reason), + additional_details: { + bypass_type: :role, + default_roles: default_roles, + custom_role_ids: custom_role_ids, + reason: reason + } + ) + true + end + + def reason_from_push_options + return if push_options.nil? + + push_options.get(:security_policy)&.dig(:bypass_reason) + end + strong_memoize_attr :reason_from_push_options + + def log_bypass_audit!(type, message, additional_details = {}) Gitlab::Audit::Auditor.audit( name: "security_policy_#{type}_push_bypass", author: user, @@ -63,9 +156,19 @@ def log_bypass_audit!(type, id) security_policy_name: security_policy.name, security_policy_id: security_policy.id, branch_name: branch_name - } + }.merge(additional_details) ) end + + def bypass_audit_message(type, id, reason = nil) + message = <<~MSG.squish + Branch push restriction on '#{branch_name}' for project '#{project.full_path}' + has been bypassed by #{type} with ID: #{id} + MSG + + message += " with reason: #{reason}" if reason.present? + message + end end end end diff --git a/ee/lib/security/scan_result_policies/push_bypass_checker.rb b/ee/lib/security/scan_result_policies/push_bypass_checker.rb index 92d6dc600869d03824cd85558567a8df5cf1ed39..a9b53035f8fcc71b6184d48f8f402467e302ce43 100644 --- a/ee/lib/security/scan_result_policies/push_bypass_checker.rb +++ b/ee/lib/security/scan_result_policies/push_bypass_checker.rb @@ -5,10 +5,11 @@ module ScanResultPolicies class PushBypassChecker include Gitlab::Utils::StrongMemoize - def initialize(project:, user_access:, branch_name:) + def initialize(project:, user_access:, branch_name:, push_options:) @project = project @user_access = user_access @branch_name = branch_name + @push_options = push_options end def check_bypass! @@ -22,15 +23,18 @@ def check_bypass! private - attr_reader :project, :user_access, :branch_name + attr_reader :project, :user_access, :branch_name, :push_options def bypass_allowed?(policy) Security::ScanResultPolicies::PolicyBypassChecker.new( security_policy: policy, project: project, user_access: user_access, - branch_name: branch_name + branch_name: branch_name, + push_options: push_options ).bypass_allowed? + rescue Security::ScanResultPolicies::PolicyBypassChecker::BypassReasonRequiredError + raise end end end diff --git a/ee/spec/lib/gitlab/checks/security/policy_check_spec.rb b/ee/spec/lib/gitlab/checks/security/policy_check_spec.rb index c242699ded0f27e184b53001a321aae63d74f6f8..13f8fd5c7c97bba4b56ff10802b4027adefa78b6 100644 --- a/ee/spec/lib/gitlab/checks/security/policy_check_spec.rb +++ b/ee/spec/lib/gitlab/checks/security/policy_check_spec.rb @@ -129,6 +129,26 @@ ) end end + + context 'when bypass is allowed but no bypass_reason is provided' do + let_it_be(:regular_user) { create(:user) } + let_it_be(:user_access) { Gitlab::UserAccess.new(regular_user, container: project) } + + before do + # Create a policy that allows user bypass but requires a reason + create(:security_policy, linked_projects: [project], content: { + bypass_settings: { + users: [{ id: regular_user.id }] + } + }) + end + + it 'raises bypass reason error' do + expect { policy_check! }.to raise_error( + Gitlab::GitAccess::ForbiddenError, described_class::BYPASS_REASON_ERROR_MESSAGE + ) + end + end end context 'when the security_policies_bypass_options_tokens_accounts feature flag is disabled' do diff --git a/ee/spec/lib/security/scan_result_policies/policy_bypass_checker_spec.rb b/ee/spec/lib/security/scan_result_policies/policy_bypass_checker_spec.rb index a96bfa1482322e19a5931da35955e8f449d27de5..2a5f962d279d8e580044388ba79ad6791a7694a8 100644 --- a/ee/spec/lib/security/scan_result_policies/policy_bypass_checker_spec.rb +++ b/ee/spec/lib/security/scan_result_policies/policy_bypass_checker_spec.rb @@ -18,7 +18,8 @@ describe '#bypass_allowed?' do subject(:bypass_allowed?) do described_class.new( - security_policy: security_policy, project: project, user_access: user_access, branch_name: branch_name + security_policy: security_policy, project: project, user_access: user_access, branch_name: branch_name, + push_options: {} ).bypass_allowed? end diff --git a/ee/spec/lib/security/scan_result_policies/push_bypass_checker_spec.rb b/ee/spec/lib/security/scan_result_policies/push_bypass_checker_spec.rb index 302a507c2bf88f527d06a0d7d5edec5f1d257573..7a1802d41e2e372a68eec7d70b13ce84696ed048 100644 --- a/ee/spec/lib/security/scan_result_policies/push_bypass_checker_spec.rb +++ b/ee/spec/lib/security/scan_result_policies/push_bypass_checker_spec.rb @@ -8,7 +8,14 @@ let_it_be(:branch_name) { 'main' } let_it_be(:user) { create(:user, :project_bot) } let_it_be(:user_access) { Gitlab::UserAccess.new(user, container: project) } - let_it_be(:checker) { described_class.new(project: project, user_access: user_access, branch_name: branch_name) } + let_it_be(:checker) do + described_class.new( + project: project, + user_access: user_access, + branch_name: branch_name, + push_options: {} + ) + end describe '#check_bypass!' do context 'when the feature is not available' do diff --git a/ee/spec/models/ee/group_member_spec.rb b/ee/spec/models/ee/group_member_spec.rb index ffd3eb760e3e6e26b95df9257b5e26a86427aa81..879134623b9eb1f7e3c353192ce382b715ea8eb7 100644 --- a/ee/spec/models/ee/group_member_spec.rb +++ b/ee/spec/models/ee/group_member_spec.rb @@ -167,6 +167,101 @@ end end + describe '.direct_member_of_groups?' do + let_it_be(:group1) { create(:group) } + let_it_be(:group2) { create(:group) } + let_it_be(:group3) { create(:group) } + let_it_be(:user) { create(:user) } + let_it_be(:other_user) { create(:user) } + + context 'when user is a direct member of the specified groups' do + before do + group1.add_developer(user) + group2.add_maintainer(user) + end + + it 'returns true for single group' do + expect(described_class.direct_member_of_groups?([group1.id], user)).to be true + end + + it 'returns true for multiple groups where user is member of all' do + expect(described_class.direct_member_of_groups?([group1.id, group2.id], user)).to be true + end + + it 'returns true for multiple groups where user is member of some' do + expect(described_class.direct_member_of_groups?([group1.id, group3.id], user)).to be true + end + + it 'returns false for groups where user is not a member' do + expect(described_class.direct_member_of_groups?([group3.id], user)).to be false + end + + it 'returns false for other user' do + expect(described_class.direct_member_of_groups?([group1.id, group2.id], other_user)).to be false + end + end + + context 'when user has minimal access level' do + before do + stub_licensed_features(minimal_access_role: true) + group1.add_member(user, Gitlab::Access::MINIMAL_ACCESS) + end + + it 'returns false as minimal access is excluded' do + expect(described_class.direct_member_of_groups?([group1.id], user)).to be false + end + end + + context 'when user has pending invite' do + before do + create(:group_member, :invited, group: group1, user: nil, invite_email: user.email) + end + + it 'returns false as invites are excluded' do + expect(described_class.direct_member_of_groups?([group1.id], user)).to be false + end + end + + context 'when user has pending request' do + before do + create(:group_member, :awaiting, group: group1, user: user) + end + + it 'returns false as requests are excluded' do + expect(described_class.direct_member_of_groups?([group1.id], user)).to be false + end + end + + context 'when user is blocked' do + before do + user.update!(state: :blocked) + group1.add_developer(user) + end + + it 'returns false as blocked users are excluded' do + expect(described_class.direct_member_of_groups?([group1.id], user)).to be false + end + end + + context 'with empty group_ids' do + it 'returns false' do + expect(described_class.direct_member_of_groups?([], user)).to be false + end + end + + context 'with nil group_ids' do + it 'returns false' do + expect(described_class.direct_member_of_groups?(nil, user)).to be false + end + end + + context 'with nil user' do + it 'returns false' do + expect(described_class.direct_member_of_groups?([group1.id], nil)).to be false + end + end + end + describe '.filter_by_enterprise_users' do let_it_be(:group) { create(:group) } let_it_be(:enterprise_user_member_1_of_group) { group.add_developer(create(:user, enterprise_group_id: group.id)) } diff --git a/ee/spec/models/ee/project_team_spec.rb b/ee/spec/models/ee/project_team_spec.rb index c76c39740ea0366b770932ad75924f7f0737f6f1..8e9fb579a855c15b3c80a41cd80e66b02170dc62 100644 --- a/ee/spec/models/ee/project_team_spec.rb +++ b/ee/spec/models/ee/project_team_spec.rb @@ -122,4 +122,205 @@ it { is_expected.to be_empty } end end + + describe '#user_exists_with_access_level_or_custom_roles?' do + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:custom_role) { create(:member_role) } + let_it_be(:another_custom_role) { create(:member_role) } + + let_it_be(:developer) { create(:user) } + let_it_be(:maintainer) { create(:user) } + let_it_be(:reporter) { create(:user) } + let_it_be(:guest) { create(:user) } + let_it_be(:non_member) { create(:user) } + + before do + create(:project_member, :developer, project: project, user: developer, member_role: custom_role) + create(:project_member, :maintainer, project: project, user: maintainer) + create(:project_member, :reporter, project: project, user: reporter) + create(:project_member, :guest, project: project, user: guest) + end + + subject(:user_exists) do + project.team.user_exists_with_access_level_or_custom_roles?(user, levels: levels, + member_role_ids: member_role_ids) + end + + context 'when no parameters are provided' do + let(:levels) { [] } + let(:member_role_ids) { [] } + let(:user) { developer } + + it 'returns false' do + expect(user_exists).to be false + end + end + + context 'when filtering by access level' do + let(:levels) { [Gitlab::Access::MAINTAINER] } + let(:member_role_ids) { [] } + + context 'when user has the specified access level' do + let(:user) { maintainer } + + it 'returns true' do + expect(user_exists).to be true + end + end + + context 'when user does not have the specified access level' do + let(:user) { developer } + + it 'returns false' do + expect(user_exists).to be false + end + end + + context 'when user is not a member of the project' do + let(:user) { non_member } + + it 'returns false' do + expect(user_exists).to be false + end + end + + context 'when filtering by multiple access levels' do + let(:levels) { [Gitlab::Access::MAINTAINER, Gitlab::Access::REPORTER] } + let(:member_role_ids) { [] } + + context 'when user has one of the specified access levels' do + let(:user) { maintainer } + + it 'returns true' do + expect(user_exists).to be true + end + end + + context 'when user has another of the specified access levels' do + let(:user) { reporter } + + it 'returns true' do + expect(user_exists).to be true + end + end + + context 'when user does not have any of the specified access levels' do + let(:user) { guest } + + it 'returns false' do + expect(user_exists).to be false + end + end + end + end + + context 'when filtering by custom roles' do + let(:levels) { [] } + let(:member_role_ids) { [custom_role.id] } + + context 'when user has the specified custom role' do + let(:user) { developer } + + it 'returns true' do + expect(user_exists).to be true + end + end + + context 'when user does not have the specified custom role' do + let(:user) { maintainer } + + it 'returns false' do + expect(user_exists).to be false + end + end + + context 'when user is not a member of the project' do + let(:user) { non_member } + + it 'returns false' do + expect(user_exists).to be false + end + end + + context 'when filtering by multiple custom roles' do + let(:member_role_ids) { [custom_role.id, another_custom_role.id] } + + context 'when user has one of the specified custom roles' do + let(:user) { developer } + + it 'returns true' do + expect(user_exists).to be true + end + end + + context 'when user does not have any of the specified custom roles' do + let(:user) { maintainer } + + it 'returns false' do + expect(user_exists).to be false + end + end + end + + context 'when filtering with non-existent custom role' do + let(:member_role_ids) { [non_existing_record_id] } + + context 'when user is a member' do + let(:user) { developer } + + it 'returns false' do + expect(user_exists).to be false + end + end + end + end + + context 'when filtering by both access level and custom roles' do + let(:levels) { [Gitlab::Access::MAINTAINER] } + let(:member_role_ids) { [custom_role.id] } + + context 'when user has the specified access level' do + let(:user) { maintainer } + + it 'returns true' do + expect(user_exists).to be true + end + end + + context 'when user has the specified custom role' do + let(:user) { developer } + + it 'returns true' do + expect(user_exists).to be true + end + end + + context 'when user has neither the access level nor the custom role' do + let(:user) { guest } + + it 'returns false' do + expect(user_exists).to be false + end + end + + context 'when user is not a member of the project' do + let(:user) { non_member } + + it 'returns false' do + expect(user_exists).to be false + end + end + end + + context 'when user is nil' do + let(:levels) { [Gitlab::Access::MAINTAINER] } + let(:member_role_ids) { [] } + let(:user) { nil } + + it 'returns false' do + expect(user_exists).to be false + end + end + end end diff --git a/lib/gitlab/checks/changes_access.rb b/lib/gitlab/checks/changes_access.rb index 4b0ba12de511981ea01d39faaab98d3fe845f8a8..0e5eaa7761c42b50cd6104172c282713743a5cc9 100644 --- a/lib/gitlab/checks/changes_access.rb +++ b/lib/gitlab/checks/changes_access.rb @@ -104,7 +104,8 @@ def single_change_accesses protocol: protocol, logger: logger, commits: commits, - gitaly_context: gitaly_context + gitaly_context: gitaly_context, + push_options: push_options ) end end diff --git a/lib/gitlab/checks/single_change_access.rb b/lib/gitlab/checks/single_change_access.rb index 0640af9e6253e996b5f114924fcfb7601af9a747..6d40d3a7a772448d03dbc9ba035be83576b1cef1 100644 --- a/lib/gitlab/checks/single_change_access.rb +++ b/lib/gitlab/checks/single_change_access.rb @@ -5,13 +5,13 @@ module Checks class SingleChangeAccess ATTRIBUTES = %i[user_access project skip_authorization protocol oldrev newrev ref - branch_name tag_name logger commits gitaly_context].freeze + branch_name tag_name logger commits gitaly_context push_options].freeze attr_reader(*ATTRIBUTES) def initialize( change, user_access:, project:, - protocol:, logger:, commits: nil, gitaly_context: nil + protocol:, logger:, commits: nil, gitaly_context: nil, push_options: nil ) @oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref) @branch_ref = Gitlab::Git.branch_ref?(@ref) @@ -23,6 +23,7 @@ def initialize( @protocol = protocol @commits = commits @gitaly_context = gitaly_context + @push_options = push_options @logger = logger @logger.append_message("Running checks for ref: #{@branch_name || @tag_name}") diff --git a/lib/gitlab/push_options.rb b/lib/gitlab/push_options.rb index d218b9c1177f8a56ac6be8a2a0875d8e6364c7e3..d7656e01e6cfa9385dfc177c4c256eb9c4cde5d4 100644 --- a/lib/gitlab/push_options.rb +++ b/lib/gitlab/push_options.rb @@ -33,6 +33,9 @@ class PushOptions }, secret_push_protection: { keys: [:skip_all] + }, + security_policy: { + keys: [:bypass_reason] } }).freeze