diff --git a/app/finders/autocomplete/group_users_finder.rb b/app/finders/autocomplete/group_users_finder.rb index b24f3f7f032a2465db11773ed77199cf162de0bc..599861704793291f2ce65991a32444c378f6fbf2 100644 --- a/app/finders/autocomplete/group_users_finder.rb +++ b/app/finders/autocomplete/group_users_finder.rb @@ -12,8 +12,11 @@ module Autocomplete class GroupUsersFinder include Gitlab::Utils::StrongMemoize - def initialize(group:) + attr_reader :group, :current_user + + def initialize(group:, current_user:) @group = group + @current_user = current_user end def execute @@ -67,7 +70,7 @@ def members_from_descendant_project_shares end def group_hierarchy_cte - Gitlab::SQL::CTE.new(:group_hierarchy, @group.self_and_hierarchy.select(:id)) + Gitlab::SQL::CTE.new(:group_hierarchy, group.self_and_hierarchy.select(:id)) end strong_memoize_attr :group_hierarchy_cte @@ -76,7 +79,7 @@ def group_hierarchy_ids end def descendant_projects_cte - Gitlab::SQL::CTE.new(:descendant_projects, @group.all_projects.select(:id)) + Gitlab::SQL::CTE.new(:descendant_projects, group.all_projects.select(:id)) end strong_memoize_attr :descendant_projects_cte @@ -85,3 +88,5 @@ def descendant_project_ids end end end + +Autocomplete::GroupUsersFinder.prepend_mod_with('Autocomplete::GroupUsersFinder') diff --git a/app/finders/autocomplete/users_finder.rb b/app/finders/autocomplete/users_finder.rb index ff65cd1ed14366d375410ec91d4663ee229454b7..4c03ce48a154110432fdb2ae6460b8db145e6c97 100644 --- a/app/finders/autocomplete/users_finder.rb +++ b/app/finders/autocomplete/users_finder.rb @@ -91,7 +91,7 @@ def find_users if project project_users elsif group - ::Autocomplete::GroupUsersFinder.new(group: group).execute # rubocop: disable CodeReuse/Finder + ::Autocomplete::GroupUsersFinder.new(group: group, current_user: current_user).execute # rubocop: disable CodeReuse/Finder elsif current_user User.all else diff --git a/app/finders/users_finder.rb b/app/finders/users_finder.rb index 47a7c7211482a785d2cb702f2fcd11b8549bf55f..d0ed3515bfd6684fabb6714cb38eae2d3b7e8839 100644 --- a/app/finders/users_finder.rb +++ b/app/finders/users_finder.rb @@ -67,7 +67,7 @@ def base_scope if group raise Gitlab::Access::AccessDeniedError unless user_can_read_group?(group) - scope = ::Autocomplete::GroupUsersFinder.new(group: group).execute # rubocop: disable CodeReuse/Finder -- For SQL optimization sake we need to scope out group members first see: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/137647#note_1664081899 + scope = ::Autocomplete::GroupUsersFinder.new(group: group, current_user: current_user).execute # rubocop: disable CodeReuse/Finder -- For SQL optimization sake we need to scope out group members first see: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/137647#note_1664081899 else scope = current_user&.can_admin_all_resources? ? User.all : User.without_forbidden_states end diff --git a/app/services/groups/participants_service.rb b/app/services/groups/participants_service.rb index 47a5bcd07d14e1e42bfcf8485fab6a0b4a92e46b..bd5cbec7ca624bbc8fa45007f95993187461888c 100644 --- a/app/services/groups/participants_service.rb +++ b/app/services/groups/participants_service.rb @@ -30,7 +30,7 @@ def all_members def group_hierarchy_users return [] unless group - relation = Autocomplete::GroupUsersFinder.new(group: group).execute + relation = Autocomplete::GroupUsersFinder.new(group: group, current_user: current_user).execute filter_and_sort_users(relation) end diff --git a/ee/app/finders/ee/autocomplete/group_users_finder.rb b/ee/app/finders/ee/autocomplete/group_users_finder.rb new file mode 100644 index 0000000000000000000000000000000000000000..d5a4e71879e9cb07a58b8ddfaebd6281e392a8e9 --- /dev/null +++ b/ee/app/finders/ee/autocomplete/group_users_finder.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# This finder will return all the members as the FOSS version as well as +# the Duo Code Review bot if the current user has access to the Duo Code Review +# features +module EE + module Autocomplete # rubocop:disable Gitlab/BoundedContexts -- FOSS finder is not bounded to a context + module GroupUsersFinder + extend ::Gitlab::Utils::Override + + override :execute + def execute + return super unless group.ai_review_merge_request_allowed?(current_user) + + super.union_with_user(::Users::Internal.duo_code_review_bot) + end + end + end +end diff --git a/ee/app/models/ee/group.rb b/ee/app/models/ee/group.rb index 01ba546a8fc47d17b56a9d8f47274cac64d7a33e..ee60426399780f8166f0cc8e9f7b8f1d86351ecc 100644 --- a/ee/app/models/ee/group.rb +++ b/ee/app/models/ee/group.rb @@ -329,6 +329,16 @@ def scim_oauth_access_token instance_scim_oauth_access_token end + def ai_review_merge_request_allowed?(user) + ::Feature.enabled?(:ai_review_merge_request, user) && + Ability.allowed?(user, :access_ai_review_mr, self) && + ::Gitlab::Llm::FeatureAuthorizer.new( + container: self, + feature_name: :review_merge_request, + user: user + ).allowed? + end + class_methods do def groups_user_can(groups, user, action, same_root: false) # If :use_traversal_ids is enabled we can use filter optmization diff --git a/ee/app/policies/ee/group_policy.rb b/ee/app/policies/ee/group_policy.rb index c7bebb8e8bcbffbab85ad10492e80ab7f2256484..c6ae201b72e5ceeac06162e1da6f2cfd65eecdb1 100644 --- a/ee/app/policies/ee/group_policy.rb +++ b/ee/app/policies/ee/group_policy.rb @@ -389,6 +389,19 @@ module GroupPolicy rule { (admin | reporter | auditor) & dora4_analytics_available } .enable :read_dora4_analytics + condition(:ai_review_mr_enabled) do + @subject.duo_features_enabled + end + + condition(:user_allowed_to_use_ai_review_mr) do + @user&.allowed_to_use?(:review_merge_request, licensed_feature: :ai_review_mr) + end + + rule do + ai_review_mr_enabled & + user_allowed_to_use_ai_review_mr + end.enable :access_ai_review_mr + condition(:assigned_to_duo_enterprise) do @user.assigned_to_duo_enterprise?(@subject) end diff --git a/ee/spec/finders/ee/autocomplete/group_users_finder_spec.rb b/ee/spec/finders/ee/autocomplete/group_users_finder_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..1746091939188e9042a92ebef63cf083c495db88 --- /dev/null +++ b/ee/spec/finders/ee/autocomplete/group_users_finder_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Autocomplete::GroupUsersFinder, feature_category: :code_review_workflow do + describe '#execute' do + let_it_be(:current_user) { create(:user) } + let(:params) { {} } + + let_it_be(:group) { create(:group) } + + subject do + described_class.new(current_user: current_user, group: group).execute.to_a + end + + context 'when group does not have access to Duo Code review for given user' do + before do + allow(group).to receive(:ai_review_merge_request_allowed?).with(current_user).and_return(false) + end + + it { is_expected.not_to include(::Users::Internal.duo_code_review_bot) } + end + + context 'when group has access to Duo Code review' do + before do + allow(group).to receive(:ai_review_merge_request_allowed?).with(current_user).and_return(true) + end + + it { is_expected.to include(::Users::Internal.duo_code_review_bot) } + end + end +end diff --git a/ee/spec/models/ee/group_spec.rb b/ee/spec/models/ee/group_spec.rb index 404dd67f395ff8bdd63c66d45b52dd7d4ff54f23..e457a68f72dc2c1a9879f7f8ee495d6131f2b3d8 100644 --- a/ee/spec/models/ee/group_spec.rb +++ b/ee/spec/models/ee/group_spec.rb @@ -4517,4 +4517,48 @@ def webhook_headers let(:entity) { group } end end + + describe '#ai_review_merge_request_allowed?' do + let_it_be(:group) { create(:group) } + let_it_be(:current_user) { create(:user, developer_of: group) } + + let(:authorizer) { instance_double(::Gitlab::Llm::FeatureAuthorizer) } + + subject(:ai_review_merge_request_allowed?) { group.ai_review_merge_request_allowed?(current_user) } + + before do + # Set up the "happy path" - all conditions return true by default + stub_licensed_features(ai_review_mr: true) + allow(::Gitlab::Llm::FeatureAuthorizer).to receive(:new).and_return(authorizer) + allow(authorizer).to receive(:allowed?).and_return(true) + allow(Ability).to receive(:allowed?).with(current_user, :access_ai_review_mr, group).and_return(true) + end + + # When all conditions are true, the method should return true + it { is_expected.to be(true) } + + context 'when feature is not authorized' do + before do + allow(authorizer).to receive(:allowed?).and_return(false) + end + + it { is_expected.to be(false) } + end + + context 'when user lacks permission' do + before do + allow(Ability).to receive(:allowed?).with(current_user, :access_ai_review_mr, group).and_return(false) + end + + it { is_expected.to be(false) } + end + + context 'when ai_review_merge_request feature flag is disabled' do + before do + stub_feature_flags(ai_review_merge_request: false) + end + + it { is_expected.to be(false) } + end + end end diff --git a/ee/spec/policies/group_policy_spec.rb b/ee/spec/policies/group_policy_spec.rb index 711430e27b336604fbf741e777defedb24bf8d80..a516539561e38c6302d62ad7f3442b5fb7418b3e 100644 --- a/ee/spec/policies/group_policy_spec.rb +++ b/ee/spec/policies/group_policy_spec.rb @@ -4484,4 +4484,26 @@ def create_member_role(member, abilities = member_role_abilities) it { is_expected.to be_disallowed(:generate_description) } end end + + describe 'access_ai_review_mr' do + let(:current_user) { owner } + + where(:duo_features_enabled, :allowed_to_use, :enabled_for_user) do + false | false | be_disallowed(:access_ai_review_mr) + true | false | be_disallowed(:access_ai_review_mr) + false | true | be_disallowed(:access_ai_review_mr) + true | true | be_allowed(:access_ai_review_mr) + end + + with_them do + before do + allow(group).to receive(:duo_features_enabled).and_return(duo_features_enabled) + + allow(current_user).to receive(:allowed_to_use?) + .with(:review_merge_request, licensed_feature: :ai_review_mr).and_return(allowed_to_use) + end + + it { is_expected.to enabled_for_user } + end + end end diff --git a/spec/finders/autocomplete/group_users_finder_spec.rb b/spec/finders/autocomplete/group_users_finder_spec.rb index d0a7ebf3206218aa1f6b341dd6c51eae42f9662b..b2f59a53c2308f9683ee33c462dae2249609c3ae 100644 --- a/spec/finders/autocomplete/group_users_finder_spec.rb +++ b/spec/finders/autocomplete/group_users_finder_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' RSpec.describe Autocomplete::GroupUsersFinder, feature_category: :text_editors do + let_it_be(:current_user) { create(:user) } let_it_be(:parent_group) { create(:group) } let_it_be(:group) { create(:group, parent: parent_group) } let_it_be(:subgroup) { create(:group, parent: group) } @@ -11,7 +12,7 @@ let_it_be(:group_project) { create(:project, namespace: group) } let_it_be(:subgroup_project) { create(:project, namespace: subgroup) } - let(:finder) { described_class.new(group: group) } + let(:finder) { described_class.new(group: group, current_user: current_user) } describe '#execute' do context 'with group members' do