diff --git a/db/migrate/20230424094117_add_read_vulnerability_to_member_roles.rb b/db/migrate/20230424094117_add_read_vulnerability_to_member_roles.rb new file mode 100644 index 0000000000000000000000000000000000000000..4ae4a7f6bffb785bc33bd599c9997b6214b03ceb --- /dev/null +++ b/db/migrate/20230424094117_add_read_vulnerability_to_member_roles.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddReadVulnerabilityToMemberRoles < Gitlab::Database::Migration[2.1] + def change + add_column :member_roles, :read_vulnerability, :boolean, default: false, null: false + end +end diff --git a/db/schema_migrations/20230424094117 b/db/schema_migrations/20230424094117 new file mode 100644 index 0000000000000000000000000000000000000000..3e0002ddd6b58b588b9ef1876382c63a5183bc16 --- /dev/null +++ b/db/schema_migrations/20230424094117 @@ -0,0 +1 @@ +e69855d9b788edb799158a839917e5320461891be3a0bd1799e46827e36bdaab \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 94c36a22fd46d4824a06220e76ab874cb24fab90..833e83c61dfae1531cf1fb2b3dfdece35eb92572 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -17762,7 +17762,8 @@ CREATE TABLE member_roles ( created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, base_access_level integer NOT NULL, - read_code boolean DEFAULT false + read_code boolean DEFAULT false, + read_vulnerability boolean DEFAULT false NOT NULL ); CREATE SEQUENCE member_roles_id_seq 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 7aae0e2c7cd7f724d4ade75a2e0b6cf1ee84b656..4991d5ccbd5ad43b42aea2c10ea2ac1e13c7b8c3 100644 --- a/ee/app/models/ee/user.rb +++ b/ee/app/models/ee/user.rb @@ -508,6 +508,11 @@ def read_code_for?(project) roles&.include?(:read_code) end + def read_vulnerability_for?(project) + 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 4ac27dd3c5364ad1119b3beb521e6d5960db2ddc..cf4a99e6f4cea3e2d86b8334f7f4f4e4c05d96b7 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 @@ -63,6 +71,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..fd8681383cf0a3351d987295cd1348e043c72b72 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 6208ae4415589f614cfcaec1c377f598f27af2b4..681e0401edc16676aa038368fcfb5874d09fc868 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? @@ -543,6 +554,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/app/policies/instance_security_dashboard_policy.rb b/ee/app/policies/instance_security_dashboard_policy.rb index 89dda82a86287f837144f0aeeb07ee7093f98e0e..22e950fbafab6fa6622070ab52b9a355e51b08d9 100644 --- a/ee/app/policies/instance_security_dashboard_policy.rb +++ b/ee/app/policies/instance_security_dashboard_policy.rb @@ -14,6 +14,7 @@ class InstanceSecurityDashboardPolicy < BasePolicy rule { ~anonymous }.policy do enable :read_instance_security_dashboard enable :read_security_resource + enable :read_vulnerability end rule { security_dashboard_enabled & can?(:read_instance_security_dashboard) }.policy do 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 e0234ad599e60a6173ca1f85cef322e5636415c0..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 + ::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 ecbae299a08506d8a43ac40b6b04db30601967f2..da36e3f9ab9ddd03e43638abdca457bd717256c8 100644 --- a/ee/spec/models/ee/user_spec.rb +++ b/ee/spec/models/ee/user_spec.rb @@ -757,8 +757,12 @@ ("users"."user_type" = 0 OR "users"."user_type" IS NULL OR "users"."user_type" IN (0, 4, 5)) AND (EXISTS (SELECT 1 FROM ((SELECT "members".* FROM "members" - WHERE (members.access_level > 10))) members - WHERE "members"."user_id" = "users"."id")) + WHERE (members.access_level > 10)) + UNION + (SELECT "members".* FROM "members" INNER JOIN "member_roles" ON "member_roles"."id" = "members"."member_role_id" + WHERE "members"."access_level" = 10 + AND + (read_vulnerability = true))) members WHERE "members"."user_id" = "users"."id")) SQL expect(users.to_sql.squish).to eq(expected_sql.squish), "query was changed. Please ensure query is covered with an index and adjust this test case" @@ -2124,6 +2128,53 @@ 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 '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 + 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 07149eac6130921a570a1d53ea072555a666daf3..b8732ee895d7cfa8b3879ac30442b23ff3951b75 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 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 d20d2932177aae52e10156e30a6c75ee0336b12a..9b6d3f83496fe73442c8cad49ded38299f332248 100644 --- a/ee/spec/policies/project_policy_spec.rb +++ b/ee/spec/policies/project_policy_spec.rb @@ -2425,27 +2425,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, @@ -2464,62 +2488,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 3b426ca7f89a6b96d8054676208d56f27fa7c89d..486e124b76ba6c9bf50d109568eb443107e9c328 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -27548,6 +27548,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 ""