diff --git a/ee/app/graphql/resolvers/security_training_urls_resolver.rb b/ee/app/graphql/resolvers/security_training_urls_resolver.rb index 30a7438fed2c26c0d3f649a83a2edb28906d86c9..f4141d4f312100112efd9b0e6a442451ec89f5d8 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 7fde47c6dc9fe69f2a418124cde42779417b8678..bccda661fb559796cfea64ac4691fb983b146712 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 0927da80e574f1adb24c4d2551bced0720bb3fb7..f2fcb40310eb537e9547c3214dc24399a43f7b5e 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 96c81198ce003044796335f50ef028f65560b5c9..8d8f212cc59df65d52535278dad02c9169444e1a 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 4cfdc37b6c38b45de4c93525042bd93dc215b774..e66e624483eb800b44ebc2feed4660c5499dc9fb 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 0000000000000000000000000000000000000000..c6a3317a26e4f6b44805cb7c02e046e6afa8801f --- /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 e5af5463ae0bf88b34546d61fbc9bee7306672d7..512dd61f9c652c937948d2897b64e2f76be5479f 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 503438d25211939d229ed03c1bb59d827591c40a..1c6f9806ee902cb0d23a3c3ba84c8e8898237e4a 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 6e8866429f1bdd3201ab924063aaa5bbfb8f0953..31454b579635e244799747d58b306ee7c6799c52 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 01ee1440cb63609aa274574f2185a33a4b2f839f..18cfe5f97c95527ff3472542012e6035355d60ec 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 9eba9e7ce204a53c5d464603300c41d5849ac4c5..87bae88d76595054443c53b6d3a91e2d6fb9aabc 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 7d50825acb921fe5048fb4dc3e503cfaeb900197..7b857dcb81e1ba658e37e72aebc43951eafb35f6 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 77e0a1542ff8694f35d7581d15452442166b5722..e28df6c1bfb8a73ef0d4a2f694e3a3feecaa0024 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 = Gitlab::Access::MINIMAL_ACCESS + 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 = Gitlab::Access::MINIMAL_ACCESS + 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 e9b5a44071ee4bb8e30d8d34f78f067613774a0b..0db84fb0101a3d49846c47c4b22649a81a7e8d4f 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 50dff6a92314e279b5b654d0d9a8a4adea09a206..aa84e55c303ac1885d733ea3754b523e4424d891 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 5cff4002c3b8b599730a2eb3ee0432cf009cda79..bb255c8a8eb640331deed8181f4b04f5d284f938 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 d4fd08e6267b5bd16db996fff035e635982c9117..e445147d38f821b9d76e2357168ffec336819d24 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -27716,6 +27716,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 ""