From 9c7915842a9c35260cf29cfb354adf58122808a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jarka=20Ko=C5=A1anov=C3=A1?= Date: Mon, 15 May 2023 08:12:38 +0200 Subject: [PATCH 1/3] Add ability to read_vulnerability to custom roles - new FF: custom_roles_vulnerability - updating models and API - adding specs Changelog: added EE: true --- .../security_training_urls_resolver.rb | 2 +- ee/app/models/ee/user.rb | 7 + ee/app/models/members/member_role.rb | 23 ++- ...user_member_roles_in_projects_preloader.rb | 19 +- ee/app/policies/ee/project_policy.rb | 16 ++ .../custom_roles_vulnerability.yml | 8 + ee/lib/api/member_roles.rb | 4 +- ee/spec/factories/member_roles.rb | 1 + .../security_training_urls_resolver_spec.rb | 4 + ...rability_severities_count_resolver_spec.rb | 2 +- .../lib/ee/api/entities/member_role_spec.rb | 1 + ee/spec/models/ee/user_spec.rb | 63 +++++++ ee/spec/models/members/member_role_spec.rb | 26 +++ ...member_roles_in_projects_preloader_spec.rb | 174 ++++++++++-------- ee/spec/policies/project_policy_spec.rb | 157 +++++++++++++--- ee/spec/requests/api/member_roles_spec.rb | 12 +- locale/gitlab.pot | 3 + 17 files changed, 407 insertions(+), 115 deletions(-) create mode 100644 ee/config/feature_flags/development/custom_roles_vulnerability.yml diff --git a/ee/app/graphql/resolvers/security_training_urls_resolver.rb b/ee/app/graphql/resolvers/security_training_urls_resolver.rb index 30a7438fed2c26..f4141d4f312100 100644 --- a/ee/app/graphql/resolvers/security_training_urls_resolver.rb +++ b/ee/app/graphql/resolvers/security_training_urls_resolver.rb @@ -6,7 +6,7 @@ class SecurityTrainingUrlsResolver < BaseResolver type [::Types::Security::TrainingUrlType], null: true - authorize :access_security_and_compliance + authorize :read_security_resource authorizes_object! argument :identifier_external_ids, diff --git a/ee/app/models/ee/user.rb b/ee/app/models/ee/user.rb index 7fde47c6dc9fe6..bccda661fb5597 100644 --- a/ee/app/models/ee/user.rb +++ b/ee/app/models/ee/user.rb @@ -516,6 +516,13 @@ def read_code_for?(project) roles&.include?(:read_code) end + def read_vulnerability_for?(project) + return false unless ::Feature.enabled?(:custom_roles_vulnerability, project.root_ancestor) + + roles = preloaded_member_roles_for_projects([project])[project.id] + roles&.include?(:read_vulnerability) + end + override :preloaded_member_roles_for_projects def preloaded_member_roles_for_projects(projects) resource_key = "member_roles_in_projects:#{self.class}:#{self.id}" diff --git a/ee/app/models/members/member_role.rb b/ee/app/models/members/member_role.rb index 0927da80e574f1..f2fcb40310eb53 100644 --- a/ee/app/models/members/member_role.rb +++ b/ee/app/models/members/member_role.rb @@ -5,7 +5,12 @@ class MemberRole < ApplicationRecord # rubocop:disable Gitlab/NamespacedClass ignore_column :download_code, remove_with: '15.9', remove_after: '2023-01-22' MAX_COUNT_PER_GROUP_HIERARCHY = 10 - ALL_CUSTOMIZABLE_PERMISSIONS = { read_code: 'Permission to read code' }.freeze + ALL_CUSTOMIZABLE_PERMISSIONS = { + read_code: + { description: 'Permission to read code', minimal_level: Gitlab::Access::GUEST }, + read_vulnerability: + { descripition: 'Permission to read vulnerability', minimal_level: Gitlab::Access::GUEST } + }.freeze CUSTOMIZABLE_PERMISSIONS_EXEMPT_FROM_CONSUMING_SEAT = [:read_code].freeze NON_PERMISSION_COLUMNS = [:id, :namespace_id, :created_at, :updated_at, :base_access_level].freeze @@ -18,6 +23,9 @@ class MemberRole < ApplicationRecord # rubocop:disable Gitlab/NamespacedClass validate :max_count_per_group_hierarchy, on: :create validate :validate_namespace_locked, on: :update validate :attributes_locked_after_member_associated, on: :update + validate :validate_minimal_base_access_level, if: ->(member_role) do + Feature.enabled?(:custom_roles_vulnerability, member_role.namespace&.root_ancestor) + end validates_associated :members @@ -64,6 +72,19 @@ def validate_namespace_locked errors.add(:namespace, s_("MemberRole|can't be changed")) end + def validate_minimal_base_access_level + ALL_CUSTOMIZABLE_PERMISSIONS.each do |permission, params| + min_level = params[:minimal_level] + next unless self[permission] + next if base_access_level >= min_level + + errors.add(:base_access_level, + format(s_("MemberRole|minimal base access level must be %{min_access_level}."), + min_access_level: "#{Gitlab::Access.options_with_owner.key(min_level)} (#{min_level})") + ) + end + end + def attributes_locked_after_member_associated return unless members.present? diff --git a/ee/app/models/preloaders/user_member_roles_in_projects_preloader.rb b/ee/app/models/preloaders/user_member_roles_in_projects_preloader.rb index 96c81198ce0030..fd8681383cf0a3 100644 --- a/ee/app/models/preloaders/user_member_roles_in_projects_preloader.rb +++ b/ee/app/models/preloaders/user_member_roles_in_projects_preloader.rb @@ -26,29 +26,31 @@ def execute value_list = Arel::Nodes::ValuesList.new(sql_values_array) sql = <<~SQL - SELECT project_ids.project_id, custom_permissions.read_code FROM (#{value_list.to_sql}) AS project_ids (project_id, namespace_ids), + SELECT project_ids.project_id, bool_or(custom_permissions.read_code) as read_code, bool_or(custom_permissions.read_vulnerability) as read_vulnerability + FROM (#{value_list.to_sql}) AS project_ids (project_id, namespace_ids), LATERAL ( ( - #{Member.select('read_code') + #{Member.select('read_code, read_vulnerability') .left_outer_joins(:member_role) .where("members.source_type = 'Project' AND members.source_id = project_ids.project_id") .with_user(user) - .where(member_roles: { read_code: true }) + .where('member_roles.read_code = true or member_roles.read_vulnerability = true') .limit(1).to_sql} ) UNION ALL ( - #{Member.select('read_code') + #{Member.select('read_code, read_vulnerability') .left_outer_joins(:member_role) .where("members.source_type = 'Namespace' AND members.source_id IN (SELECT UNNEST(project_ids.namespace_ids) as ids)") .with_user(user) - .where(member_roles: { read_code: true }) + .where('member_roles.read_code = true or member_roles.read_vulnerability = true') .limit(1).to_sql} ) UNION ALL ( - SELECT false AS read_code + SELECT false AS read_code, false AS read_vulnerability ) LIMIT 1 ) AS custom_permissions + GROUP BY project_ids.project_id; SQL grouped_by_project = ApplicationRecord.connection.execute(sql).to_a.group_by do |h| @@ -58,6 +60,11 @@ def execute grouped_by_project.transform_values do |value| custom_attributes = [] custom_attributes << :read_code if value.find { |custom_role| custom_role["read_code"] == true } + + if value.find { |custom_role| custom_role["read_vulnerability"] == true } + custom_attributes << :read_vulnerability + end + custom_attributes end end diff --git a/ee/app/policies/ee/project_policy.rb b/ee/app/policies/ee/project_policy.rb index 4cfdc37b6c38b4..e66e624483eb80 100644 --- a/ee/app/policies/ee/project_policy.rb +++ b/ee/app/policies/ee/project_policy.rb @@ -207,6 +207,10 @@ module ProjectPolicy @subject.custom_roles_enabled? end + condition(:custom_roles_vulnerabilities_allowed) do + ::Feature.enabled?(:custom_roles_vulnerability, @subject.root_ancestor) + end + desc "Custom role on project that enables read code" condition(:role_enables_read_code) do next unless @user.is_a?(User) @@ -214,6 +218,13 @@ module ProjectPolicy @user.read_code_for?(project) end + desc "Custom role on project that enables read vulnerability" + condition(:role_enables_read_vulnerability) do + next unless @user.is_a?(User) + + @user.read_vulnerability_for?(project) + end + with_scope :subject condition(:suggested_reviewers_available) do @subject.can_suggest_reviewers? @@ -546,6 +557,11 @@ module ProjectPolicy end rule { custom_roles_allowed & role_enables_read_code }.enable :read_code + rule { custom_roles_allowed & custom_roles_vulnerabilities_allowed & role_enables_read_vulnerability }.policy do + enable :read_vulnerability + enable :read_security_resource + enable :create_vulnerability_export + end rule { can?(:create_issue) & okrs_enabled }.policy do enable :create_objective diff --git a/ee/config/feature_flags/development/custom_roles_vulnerability.yml b/ee/config/feature_flags/development/custom_roles_vulnerability.yml new file mode 100644 index 00000000000000..c6a3317a26e4f6 --- /dev/null +++ b/ee/config/feature_flags/development/custom_roles_vulnerability.yml @@ -0,0 +1,8 @@ +--- +name: custom_roles_vulnerability +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/114734 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/409387 +milestone: '16.0' +type: development +group: group::authentication and authorization +default_enabled: false diff --git a/ee/lib/api/member_roles.rb b/ee/lib/api/member_roles.rb index e5af5463ae0bf8..512dd61f9c652c 100644 --- a/ee/lib/api/member_roles.rb +++ b/ee/lib/api/member_roles.rb @@ -42,8 +42,8 @@ class MemberRoles < ::API::Base documentation: { example: 10 } ) - ::MemberRole::ALL_CUSTOMIZABLE_PERMISSIONS.each do |permission_name, permission_description| - optional permission_name.to_s, type: Boolean, desc: permission_description, default: false + ::MemberRole::ALL_CUSTOMIZABLE_PERMISSIONS.each do |permission_name, permission_params| + optional permission_name.to_s, type: Boolean, desc: permission_params[:description], default: false end end diff --git a/ee/spec/factories/member_roles.rb b/ee/spec/factories/member_roles.rb index 503438d2521193..1c6f9806ee902c 100644 --- a/ee/spec/factories/member_roles.rb +++ b/ee/spec/factories/member_roles.rb @@ -6,6 +6,7 @@ base_access_level { Gitlab::Access::DEVELOPER } trait(:developer) { base_access_level { Gitlab::Access::DEVELOPER } } + trait(:reporter) { base_access_level { Gitlab::Access::REPORTER } } trait(:guest) { base_access_level { Gitlab::Access::GUEST } } end end diff --git a/ee/spec/graphql/resolvers/security_training_urls_resolver_spec.rb b/ee/spec/graphql/resolvers/security_training_urls_resolver_spec.rb index 6e8866429f1bdd..31454b579635e2 100644 --- a/ee/spec/graphql/resolvers/security_training_urls_resolver_spec.rb +++ b/ee/spec/graphql/resolvers/security_training_urls_resolver_spec.rb @@ -6,6 +6,10 @@ include GraphqlHelpers describe '#resolve' do + before do + stub_licensed_features(security_dashboard: true) + end + subject { resolve(described_class, obj: project, ctx: { current_user: user }) } let_it_be(:user) { create(:user) } diff --git a/ee/spec/graphql/resolvers/vulnerability_severities_count_resolver_spec.rb b/ee/spec/graphql/resolvers/vulnerability_severities_count_resolver_spec.rb index 01ee1440cb6360..18cfe5f97c9552 100644 --- a/ee/spec/graphql/resolvers/vulnerability_severities_count_resolver_spec.rb +++ b/ee/spec/graphql/resolvers/vulnerability_severities_count_resolver_spec.rb @@ -8,7 +8,7 @@ describe '#resolve' do subject { resolve(described_class, obj: vulnerable, args: filters, ctx: { current_user: current_user }) } - let_it_be(:project) { create(:project) } + let_it_be(:project) { create(:project, :security_and_compliance_enabled) } let_it_be(:cluster_agent) { create(:cluster_agent, project: project) } let_it_be(:user) { create(:user, security_dashboard_projects: [project]) } diff --git a/ee/spec/lib/ee/api/entities/member_role_spec.rb b/ee/spec/lib/ee/api/entities/member_role_spec.rb index 9eba9e7ce204a5..87bae88d765950 100644 --- a/ee/spec/lib/ee/api/entities/member_role_spec.rb +++ b/ee/spec/lib/ee/api/entities/member_role_spec.rb @@ -16,6 +16,7 @@ expect(subject[:id]).to eq member_role.id expect(subject[:base_access_level]).to eq member_role.base_access_level expect(subject[:read_code]).to eq member_role.read_code + expect(subject[:read_vulnerability]).to eq member_role.read_vulnerability expect(subject[:group_id]).to eq(member_role.namespace.id) end end diff --git a/ee/spec/models/ee/user_spec.rb b/ee/spec/models/ee/user_spec.rb index 7d50825acb921f..7b857dcb81e1ba 100644 --- a/ee/spec/models/ee/user_spec.rb +++ b/ee/spec/models/ee/user_spec.rb @@ -2129,6 +2129,69 @@ end end + describe '#read_vulnerability_for?', :request_store do + let_it_be(:project) { create(:project, :in_group) } + let_it_be(:user) { create(:user) } + let_it_be(:another_user) { create(:user) } + + before do + stub_licensed_features(custom_roles: true) + end + + before_all do + project_member = create(:project_member, :reporter, user: user, source: project) + create( + :member_role, + :reporter, + read_vulnerability: true, + members: [project_member], + namespace: project.group + ) + end + + context 'with custom_roles_vulnerability FF enabled' do + before do + stub_feature_flags(custom_roles_vulnerability: [project.group]) + end + + context 'when read_vulnerability present in preloaded custom roles' do + before do + user.read_vulnerability_for?(project) + end + + it 'returns true' do + expect(user.read_vulnerability_for?(project)).to be true + end + + it 'does not perform extra queries when asked for projects have already been preloaded' do + expect { user.read_vulnerability_for?(project) }.not_to exceed_query_limit(0) + end + end + + context 'when project not present in preloaded custom roles' do + it 'loads the custom role' do + expect(user.read_vulnerability_for?(project)).to be true + end + end + + context 'when a user is not assigned to the custom role' do + it 'returns false' do + expect(another_user.read_vulnerability_for?(project)).to be false + end + end + end + + context 'with custom_roles_vulnerability FF disabled' do + before do + stub_feature_flags(custom_roles_vulnerability: false) + end + + it 'returns false' do + expect(user.read_vulnerability_for?(project)).to be false + end + end + end + describe '#can_group_owner_disable_two_factor?' do let_it_be(:group) { create(:group) } let_it_be(:owner) { create(:user) } diff --git a/ee/spec/models/members/member_role_spec.rb b/ee/spec/models/members/member_role_spec.rb index 77e0a1542ff869..a5b3a377afea15 100644 --- a/ee/spec/models/members/member_role_spec.rb +++ b/ee/spec/models/members/member_role_spec.rb @@ -100,6 +100,32 @@ expect(member_role.errors[:namespace]).to include(s_("MemberRole|can't be changed")) end end + + context 'when base_access_level is too low' do + context 'when custom_roles_vulnerability FF is enabled' do + it 'creates a validation error' do + member_role.base_access_level = 5 + member_role.read_vulnerability = true + + expect(member_role).not_to be_valid + expect(member_role.errors[:base_access_level]) + .to include(s_("MemberRole|minimal base access level must be Guest (10).")) + end + end + + context 'when custom_roles_vulnerability FF is disabled' do + before do + stub_feature_flags(custom_roles_vulnerability: false) + end + + it 'is valid' do + member_role.base_access_level = 5 + member_role.read_vulnerability = true + + expect(member_role).to be_valid + end + end + end end end diff --git a/ee/spec/models/preloaders/user_member_roles_in_projects_preloader_spec.rb b/ee/spec/models/preloaders/user_member_roles_in_projects_preloader_spec.rb index e9b5a44071ee4b..0db84fb0101a3d 100644 --- a/ee/spec/models/preloaders/user_member_roles_in_projects_preloader_spec.rb +++ b/ee/spec/models/preloaders/user_member_roles_in_projects_preloader_spec.rb @@ -11,108 +11,130 @@ subject(:result) { described_class.new(projects: project_list, user: user).execute } - context 'when custom_roles license is not enabled on project root ancestor' do - it 'skips preload' do - stub_licensed_features(custom_roles: false) - create(:member_role, :guest, read_code: true, members: [project_member], namespace: project.group) - - expect(result).to eq({}) - end - end + shared_examples 'custom roles' do |ability| + context 'when custom_roles license is not enabled on project root ancestor' do + it 'skips preload' do + stub_licensed_features(custom_roles: false) + create(:member_role, :guest, namespace: project.group).tap do |record| + record[ability] = true + record.save! + record.members << project_member + end - context 'when custom_roles license is enabled on project root ancestor' do - before do - stub_licensed_features(custom_roles: true) + expect(result).to eq({}) + end end - context 'when project has custom role' do - let_it_be(:member_role) do - create(:member_role, :guest, members: [project_member], namespace: project.group, read_code: true) + context 'when custom_roles license is enabled on project root ancestor' do + before do + stub_licensed_features(custom_roles: true) end - context 'when custom role has read_code: true' do - context 'when Array of project passed' do - it 'returns the project_id with a value array that includes :read_code' do - expect(result).to eq({ project.id => [:read_code] }) + context 'when project has custom role' do + let_it_be(:member_role) do + create(:member_role, :guest, namespace: project.group).tap do |record| + record[ability] = true + record.save! + record.members << project_member end end - context 'when ActiveRecord::Relation of projects passed' do - let(:project_list) { Project.where(id: project.id) } + context 'when custom role has ability: true' do + context 'when Array of project passed' do + it 'returns the project_id with a value array that includes the ability' do + expect(result).to eq({ project.id => [ability] }) + end + end + + context 'when ActiveRecord::Relation of projects passed' do + let(:project_list) { Project.where(id: project.id) } - it 'returns the project_id with a value array that includes :read_code' do - expect(result).to eq({ project.id => [:read_code] }) + it 'returns the project_id with a value array that includes the ability' do + expect(result).to eq({ project.id => [ability] }) + end end end end - end - context 'when project namespace has a custom role with read_code: true' do - let_it_be(:group_member) { create(:group_member, :guest, user: user, source: project.namespace) } - let_it_be(:member_role) do - create(:member_role, :guest, read_code: true, members: [group_member], namespace: project.group) - end + context 'when project namespace has a custom role with ability: true' do + let_it_be(:group_member) { create(:group_member, :guest, user: user, source: project.namespace) } + let_it_be(:member_role) do + create(:member_role, :guest, namespace: project.group).tap do |record| + record[ability] = true + record.save! + record.members << group_member + end + end - it 'returns the project_id with a value array that includes :read_code' do - expect(result).to eq({ project.id => [:read_code] }) + it 'returns the project_id with a value array that includes the ability' do + expect(result).to eq({ project.id => [ability] }) + end end - end - context 'when user is a member of the project in multiple ways' do - let_it_be(:group_member) { create(:group_member, :guest, user: user, source: project.group) } + context 'when user is a member of the project in multiple ways' do + let_it_be(:group_member) { create(:group_member, :guest, user: user, source: project.group) } - it 'project value array includes :read_code if any custom roles enable them' do - create(:member_role, :guest, read_code: false, members: [project_member], namespace: project.group) - create(:member_role, :guest, read_code: true, members: [project_member], namespace: project.group) + it 'project value array includes the ability' do + create(:member_role, :guest, namespace: project.group).tap do |record| + record[ability] = false + record.save! + record.members << project_member + end + create(:member_role, :guest, namespace: project.group).tap do |record| + record[ability] = true + record.save! + record.members << project_member + end - expect(result[project.id]).to match_array([:read_code]) + expect(result[project.id]).to match_array([ability]) + end end - end - context 'when project membership has no custom role' do - let_it_be(:project) { create(:project, :private, :in_group) } + context 'when project membership has no custom role' do + let_it_be(:project) { create(:project, :private, :in_group) } - it 'returns project id with empty value array' do - expect(result).to eq(project.id => []) + it 'returns project id with empty value array' do + expect(result).to eq(project.id => []) + end end - end - context 'when project membership has custom role that does not enable custom permission' do - let_it_be(:project) { create(:project, :private, :in_group) } - - it 'returns project id with empty value array' do - project_without_custom_permission_member = create( - :project_member, - :guest, - user: user, - source: project - ) - create( - :member_role, - :guest, - read_code: false, - members: [project_without_custom_permission_member], - namespace: project.group - ) - - expect(result).to eq(project.id => []) + context 'when project membership has custom role that does not enable custom permission' do + let_it_be(:project) { create(:project, :private, :in_group) } + + it 'returns project id with empty value array' do + project_without_custom_permission_member = create( + :project_member, + :guest, + user: user, + source: project + ) + create(:member_role, :guest, namespace: project.group).tap do |record| + record[ability] = false + record.save! + record.members << project_without_custom_permission_member + end + + expect(result).to eq(project.id => []) + end end - end - context 'when user has custom role that enables custom permission outside of project hierarchy' do - it 'ignores custom role outside of project hierarchy' do - # subgroup is within parent group of project but not above project - subgroup = create(:group, parent: project.group) - subgroup_member = create(:group_member, :guest, user: user, source: subgroup) - _custom_role_outside_hierarchy = create( - :member_role, :guest, - members: [subgroup_member], - read_code: true, - namespace: project.group - ) - - expect(result).to eq({ project.id => [] }) + context 'when user has custom role that enables custom permission outside of project hierarchy' do + it 'ignores custom role outside of project hierarchy' do + # subgroup is within parent group of project but not above project + subgroup = create(:group, parent: project.group) + subgroup_member = create(:group_member, :guest, user: user, source: subgroup) + _custom_role_outside_hierarchy = create(:member_role, :guest, namespace: project.group).tap do |record| + record[ability] = false + record.save! + record.members << subgroup_member + end + + expect(result).to eq({ project.id => [] }) + end end end end + + it_behaves_like 'custom roles', :read_code + it_behaves_like 'custom roles', :read_vulnerability end diff --git a/ee/spec/policies/project_policy_spec.rb b/ee/spec/policies/project_policy_spec.rb index 50dff6a92314e2..aa84e55c303ac1 100644 --- a/ee/spec/policies/project_policy_spec.rb +++ b/ee/spec/policies/project_policy_spec.rb @@ -2435,27 +2435,51 @@ def expect_private_project_permissions_as_if_non_member end context 'custom role' do - let_it_be(:current_user) { create(:user) } + def custom_role_allowed?(user, ability) + described_class.new(user, project).allowed?(ability) + end + + let_it_be(:guest) { create(:user) } + let_it_be(:reporter) { create(:user) } let_it_be(:project) { private_project_in_group } - let_it_be(:group_member) do + let_it_be(:group_member_guest) do create( :group_member, - user: current_user, + user: guest, source: project.group, access_level: Gitlab::Access::GUEST ) end - let_it_be(:project_member) do + let_it_be(:group_member_reporter) do + create( + :group_member, + user: reporter, + source: project.group, + access_level: Gitlab::Access::REPORTER + ) + end + + let_it_be(:project_member_guest) do create( :project_member, :guest, - user: current_user, + user: guest, project: project, access_level: Gitlab::Access::GUEST ) end + let_it_be(:project_member_reporter) do + create( + :project_member, + :guest, + user: reporter, + project: project, + access_level: Gitlab::Access::REPORTER + ) + end + let_it_be(:member_role_read_code_true) do create( :member_role, @@ -2474,62 +2498,147 @@ def expect_private_project_permissions_as_if_non_member ) end + let_it_be(:member_role_read_vulnerability_true) do + create( + :member_role, + :reporter, + namespace: project.group, + read_vulnerability: true + ) + end + + let_it_be(:member_role_read_vulnerability_false) do + create( + :member_role, + :reporter, + namespace: project.group, + read_vulnerability: false + ) + end + context 'custom_roles license enabled' do before do stub_licensed_features(custom_roles: true) end context 'custom role for parent group' do - context 'custom role allows read code' do - before do - member_role_read_code_true.members << group_member + context 'custom role allows abilities' do + it 'allows guest to read code' do + member_role_read_code_true.members << group_member_guest + + expect(custom_role_allowed?(guest, :read_code)).to be_truthy end - it { is_expected.to be_allowed(:read_code) } + it 'allows reporter to read security related resources' do + member_role_read_vulnerability_true.members << group_member_reporter + + expect(custom_role_allowed?(reporter, :read_vulnerability)).to be_truthy + expect(custom_role_allowed?(reporter, :read_security_resource)).to be_truthy + expect(custom_role_allowed?(reporter, :create_vulnerability_export)).to be_truthy + end end - context 'custom role disallows read code' do - before do - member_role_read_code_false.members << group_member + context 'custom role disallows abilities' do + it 'does not allow guest to read code' do + member_role_read_code_false.members << group_member_guest + + expect(custom_role_allowed?(guest, :read_code)).to be_falsey end - it { is_expected.to be_disallowed(:read_code) } + it 'does not allow reporter to read security related resources' do + member_role_read_vulnerability_false.members << group_member_reporter + + expect(custom_role_allowed?(reporter, :read_vulnerability)).to be_falsey + expect(custom_role_allowed?(reporter, :read_security_resource)).to be_falsey + expect(custom_role_allowed?(reporter, :create_vulnerability_export)).to be_falsey + end end end context 'custom role on project membership' do - context 'custom role allows read code' do - before do - member_role_read_code_true.members << project_member + context 'custom role allows abilities' do + it 'allows guest to read code' do + member_role_read_code_true.members << project_member_guest + + expect(custom_role_allowed?(guest, :read_code)).to be_truthy + end + + context 'when custom_roles_vulnerability FF is enabled' do + before do + stub_feature_flags(custom_roles_vulnerability: [project.group]) + end + + it 'allows reporter to read security related resources' do + member_role_read_vulnerability_true.members << project_member_reporter + + expect(custom_role_allowed?(reporter, :read_vulnerability)).to be_truthy + expect(custom_role_allowed?(reporter, :read_security_resource)).to be_truthy + expect(custom_role_allowed?(reporter, :create_vulnerability_export)).to be_truthy + end end - it { is_expected.to be_allowed(:read_code) } + context 'when custom_roles_vulnerability FF is disabled' do + it 'does not allow reporter to read security related resources' do + member_role_read_vulnerability_true.members << project_member_reporter + + expect(custom_role_allowed?(reporter, :read_vulnerability)).to be_falsey + expect(custom_role_allowed?(reporter, :read_security_resource)).to be_falsey + expect(custom_role_allowed?(reporter, :create_vulnerability_export)).to be_falsey + end + end end - context 'custom role disallows read code' do - before do - member_role_read_code_false.members << project_member + context 'custom role disallows abilities' do + it 'does not allow guest to read code' do + member_role_read_code_false.members << project_member_guest + + expect(custom_role_allowed?(guest, :read_code)).to be_falsey end - it { is_expected.to be_disallowed(:read_code) } + it 'does not allow reporter to read security related resources' do + member_role_read_vulnerability_false.members << project_member_reporter + + expect(custom_role_allowed?(reporter, :read_vulnerability)).to be_falsey + expect(custom_role_allowed?(reporter, :read_security_resource)).to be_falsey + expect(custom_role_allowed?(reporter, :create_vulnerability_export)).to be_falsey + end end end context 'multiple custom roles in hierarchy with different read_code values' do + let(:current_user) { guest } + before do - member_role_read_code_true.members << project_member - member_role_read_code_false.members << group_member + member_role_read_code_true.members << project_member_guest + member_role_read_code_false.members << group_member_guest end # allows read code if any of the custom roles allow it it { is_expected.to be_allowed(:read_code) } end + + context 'multiple custom roles in hierarchy with different read_vulnerability values' do + let(:current_user) { reporter } + + before do + member_role_read_vulnerability_true.members << project_member_reporter + member_role_read_vulnerability_false.members << group_member_reporter + end + + it 'allows reporter to read security related resources' do + expect(custom_role_allowed?(reporter, :read_vulnerability)).to be_truthy + expect(custom_role_allowed?(reporter, :read_security_resource)).to be_truthy + expect(custom_role_allowed?(reporter, :create_vulnerability_export)).to be_truthy + end + end end context 'without custom_roles license enabled' do + let(:current_user) { guest } + before do stub_licensed_features(custom_roles: false) - member_role_read_code_true.members << project_member + member_role_read_code_true.members << project_member_guest end it { is_expected.to be_disallowed(:read_code) } diff --git a/ee/spec/requests/api/member_roles_spec.rb b/ee/spec/requests/api/member_roles_spec.rb index 5cff4002c3b8b5..bb255c8a8eb640 100644 --- a/ee/spec/requests/api/member_roles_spec.rb +++ b/ee/spec/requests/api/member_roles_spec.rb @@ -22,7 +22,8 @@ :member_role, namespace: group_with_member_roles, base_access_level: ::Gitlab::Access::REPORTER, - read_code: false + read_code: false, + read_vulnerability: true ) end @@ -31,7 +32,8 @@ :member_role, namespace: group_with_member_roles, base_access_level: ::Gitlab::Access::REPORTER, - read_code: true + read_code: true, + read_vulnerability: false ) end @@ -93,12 +95,14 @@ "id" => member_role_1.id, "base_access_level" => ::Gitlab::Access::REPORTER, "read_code" => false, + "read_vulnerability" => true, "group_id" => group_id }, { "id" => member_role_2.id, "base_access_level" => ::Gitlab::Access::REPORTER, "read_code" => true, + "read_vulnerability" => false, "group_id" => group_id } ] @@ -129,7 +133,7 @@ describe "POST /groups/:id/member_roles" do let_it_be(:params) do - { base_access_level: ::Gitlab::Access::MAINTAINER, read_code: true } + { base_access_level: ::Gitlab::Access::GUEST, read_code: true } end subject { post api("/groups/#{group_id}/member_roles", current_user), params: params } @@ -171,7 +175,7 @@ aggregate_failures "testing response" do expect(response).to have_gitlab_http_status(:created) - expect(json_response['base_access_level']).to eq(::Gitlab::Access::MAINTAINER) + expect(json_response['base_access_level']).to eq(::Gitlab::Access::GUEST) expect(json_response['read_code']).to eq(true) end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 7a76d7ebb42db1..6b88c26da909b5 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -27707,6 +27707,9 @@ msgstr "" msgid "MemberRole|maximum number of Member Roles are already in use by the group hierarchy. Please delete an existing Member Role." msgstr "" +msgid "MemberRole|minimal base access level must be %{min_access_level}." +msgstr "" + msgid "MemberRole|must be top-level namespace" msgstr "" -- GitLab From 26cec8986c7ec21b1fa90cff1e1f495423716420 Mon Sep 17 00:00:00 2001 From: Kassio Borges Date: Thu, 18 May 2023 13:59:59 +0000 Subject: [PATCH 2/3] Apply code review suggestions --- .../preloaders/user_member_roles_in_projects_preloader.rb | 4 ++-- ee/app/policies/ee/project_policy.rb | 2 +- ee/spec/models/members/member_role_spec.rb | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ee/app/models/preloaders/user_member_roles_in_projects_preloader.rb b/ee/app/models/preloaders/user_member_roles_in_projects_preloader.rb index fd8681383cf0a3..8d8f212cc59df6 100644 --- a/ee/app/models/preloaders/user_member_roles_in_projects_preloader.rb +++ b/ee/app/models/preloaders/user_member_roles_in_projects_preloader.rb @@ -34,7 +34,7 @@ def execute .left_outer_joins(:member_role) .where("members.source_type = 'Project' AND members.source_id = project_ids.project_id") .with_user(user) - .where('member_roles.read_code = true or member_roles.read_vulnerability = true') + .where('member_roles.read_code = true OR member_roles.read_vulnerability = true') .limit(1).to_sql} ) UNION ALL ( @@ -42,7 +42,7 @@ def execute .left_outer_joins(:member_role) .where("members.source_type = 'Namespace' AND members.source_id IN (SELECT UNNEST(project_ids.namespace_ids) as ids)") .with_user(user) - .where('member_roles.read_code = true or member_roles.read_vulnerability = true') + .where('member_roles.read_code = true OR member_roles.read_vulnerability = true') .limit(1).to_sql} ) UNION ALL ( diff --git a/ee/app/policies/ee/project_policy.rb b/ee/app/policies/ee/project_policy.rb index e66e624483eb80..909af944b11466 100644 --- a/ee/app/policies/ee/project_policy.rb +++ b/ee/app/policies/ee/project_policy.rb @@ -557,7 +557,7 @@ module ProjectPolicy end rule { custom_roles_allowed & role_enables_read_code }.enable :read_code - rule { custom_roles_allowed & custom_roles_vulnerabilities_allowed & role_enables_read_vulnerability }.policy do + rule { custom_roles_vulnerabilities_allowed & custom_roles_allowed & role_enables_read_vulnerability }.policy do enable :read_vulnerability enable :read_security_resource enable :create_vulnerability_export diff --git a/ee/spec/models/members/member_role_spec.rb b/ee/spec/models/members/member_role_spec.rb index a5b3a377afea15..e28df6c1bfb8a7 100644 --- a/ee/spec/models/members/member_role_spec.rb +++ b/ee/spec/models/members/member_role_spec.rb @@ -104,7 +104,7 @@ context 'when base_access_level is too low' do context 'when custom_roles_vulnerability FF is enabled' do it 'creates a validation error' do - member_role.base_access_level = 5 + member_role.base_access_level = Gitlab::Access::MINIMAL_ACCESS member_role.read_vulnerability = true expect(member_role).not_to be_valid @@ -119,7 +119,7 @@ end it 'is valid' do - member_role.base_access_level = 5 + member_role.base_access_level = Gitlab::Access::MINIMAL_ACCESS member_role.read_vulnerability = true expect(member_role).to be_valid -- GitLab From 9c1629660b3ebaf955c6625d646deae7eb882057 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jarka=20Ko=C5=A1anov=C3=A1?= Date: Mon, 22 May 2023 08:40:56 +0200 Subject: [PATCH 3/3] Change order of the conditions --- ee/app/policies/ee/project_policy.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ee/app/policies/ee/project_policy.rb b/ee/app/policies/ee/project_policy.rb index 909af944b11466..e66e624483eb80 100644 --- a/ee/app/policies/ee/project_policy.rb +++ b/ee/app/policies/ee/project_policy.rb @@ -557,7 +557,7 @@ module ProjectPolicy end rule { custom_roles_allowed & role_enables_read_code }.enable :read_code - rule { custom_roles_vulnerabilities_allowed & custom_roles_allowed & role_enables_read_vulnerability }.policy do + rule { custom_roles_allowed & custom_roles_vulnerabilities_allowed & role_enables_read_vulnerability }.policy do enable :read_vulnerability enable :read_security_resource enable :create_vulnerability_export -- GitLab