diff --git a/ee/app/assets/javascripts/ci/job_details/components/job_log_top_bar.vue b/ee/app/assets/javascripts/ci/job_details/components/job_log_top_bar.vue index fa1960a2d6bc9952fbe6fbef140d53c94bb08fff..e4f921147df068a8e784fa9c427e2213cf142227 100644 --- a/ee/app/assets/javascripts/ci/job_details/components/job_log_top_bar.vue +++ b/ee/app/assets/javascripts/ci/job_details/components/job_log_top_bar.vue @@ -4,6 +4,7 @@ import { GlButton } from '@gitlab/ui'; import { mapState } from 'vuex'; import { createAlert } from '~/alert'; import { s__ } from '~/locale'; +import glAbilitiesMixin from '~/vue_shared/mixins/gl_abilities_mixin'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import chatMutation from 'ee/ai/graphql/chat.mutation.graphql'; import { convertToGraphQLId } from '~/graphql_shared/utils'; @@ -18,7 +19,7 @@ export default { GlButton, RootCauseAnalysis, }, - mixins: [glFeatureFlagMixin()], + mixins: [glAbilitiesMixin(), glFeatureFlagMixin()], inject: ['aiRootCauseAnalysisAvailable', 'duoFeaturesEnabled', 'jobGid'], props: { size: { @@ -84,7 +85,8 @@ export default { this.glFeatures.aiBuildFailureCause && this.aiRootCauseAnalysisAvailable && this.duoFeaturesEnabled && - this.glFeatures.rootCauseAnalysisDuo + this.glFeatures.rootCauseAnalysisDuo && + this.glAbilities.troubleshootJobWithAi ); }, jobFailed() { diff --git a/ee/app/controllers/ee/projects/jobs_controller.rb b/ee/app/controllers/ee/projects/jobs_controller.rb index 22fcd51e140f683a81e18c1ce2c4b94c400d286b..5f67f8191191e10538e11ce2c7859022d095048e 100644 --- a/ee/app/controllers/ee/projects/jobs_controller.rb +++ b/ee/app/controllers/ee/projects/jobs_controller.rb @@ -7,6 +7,7 @@ module JobsController prepended do before_action only: [:show] do + push_frontend_ability(ability: :troubleshoot_job_with_ai, resource: @build, user: @current_user) push_frontend_feature_flag(:root_cause_analysis_duo, @current_user) end end diff --git a/ee/app/models/gitlab_subscriptions/features.rb b/ee/app/models/gitlab_subscriptions/features.rb index a1a169c44c95e58b5b9e849aadb104c0839f28be..1eca8f1d6e3c9dcbde093efa066d6749a0e02a4a 100644 --- a/ee/app/models/gitlab_subscriptions/features.rb +++ b/ee/app/models/gitlab_subscriptions/features.rb @@ -261,6 +261,7 @@ class Features suggested_reviewers subepics observability + troubleshoot_job unique_project_download_limit vulnerability_finding_signatures container_scanning_for_registry diff --git a/ee/app/policies/ee/ci/build_policy.rb b/ee/app/policies/ee/ci/build_policy.rb new file mode 100644 index 0000000000000000000000000000000000000000..3e79e822c0e94f89c58cbc6775caf20cf1a09379 --- /dev/null +++ b/ee/app/policies/ee/ci/build_policy.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module EE + module Ci + module BuildPolicy + extend ActiveSupport::Concern + + prepended do + # Authorize access to the troubleshoot job to Cloud Connector Service + condition(:troubleshoot_job_cloud_connector_authorized) do + next true if troubleshoot_job_connection.allowed_for?(@user) + + next false unless troubleshoot_job_connection.free_access? + + if ::Gitlab::Saas.feature_available?(:duo_chat_on_saas) # check if we are on SaaS + user.any_group_with_ai_available? + else + License.feature_available?(:ai_features) + end + end + + # Authorize access to Troubleshoot Job + condition(:troubleshoot_job_with_ai_authorized, scope: :subject) do + ::Gitlab::Llm::Chain::Utils::ChatAuthorizer.resource( + resource: subject.project, + user: @user + ).allowed? + end + + condition(:troubleshoot_job_licensed) do + next false unless ::Feature.enabled?(:root_cause_analysis_duo, @user) + + ::License.feature_available?(:troubleshoot_job) + end + + rule do + can?(:read_build_trace) & + troubleshoot_job_cloud_connector_authorized & + troubleshoot_job_with_ai_authorized & + troubleshoot_job_licensed + end.enable(:troubleshoot_job_with_ai) + + def troubleshoot_job_connection + CloudConnector::AvailableServices.find_by_name(:troubleshoot_job) + end + end + end + end +end diff --git a/ee/config/cloud_connector/access_data.yml b/ee/config/cloud_connector/access_data.yml index 49fa28766b61cf9597f3c46228c4ec0a322af55a..118292c6576529748e4aef10360d3d0bba3a5fae 100644 --- a/ee/config/cloud_connector/access_data.yml +++ b/ee/config/cloud_connector/access_data.yml @@ -77,6 +77,12 @@ services: # Cloud connector features (i.e. code_suggestions, duo_chat...) duo_enterprise: unit_primitives: - resolve_vulnerability + troubleshoot_job: + backend: 'gitlab-ai-gateway' + bundled_with: + duo_enterprise: + unit_primitives: + - troubleshoot_job self_hosted_models: backend: 'gitlab-ai-gateway' cut_off_date: 2024-10-17 00:00:00 UTC diff --git a/ee/lib/gitlab/llm/chain/tools/troubleshoot_job/executor.rb b/ee/lib/gitlab/llm/chain/tools/troubleshoot_job/executor.rb index 58e75ef5444ab8a26b959ca4db3e97def7b2da21..aeadfec7a5ba66c4f7bb334fdad1c707f56ae4fd 100644 --- a/ee/lib/gitlab/llm/chain/tools/troubleshoot_job/executor.rb +++ b/ee/lib/gitlab/llm/chain/tools/troubleshoot_job/executor.rb @@ -68,9 +68,6 @@ def self.slash_commands def perform error_message = if disabled? _('This feature is not enabled yet.') - elsif !job.is_a?(::Ci::Build) - _('This command is used for troubleshooting jobs and can only be invoked from ' \ - 'a job log page.') elsif !job.failed? _('This command is used for troubleshooting jobs and can only be invoked from ' \ 'a failed job log page.') @@ -83,6 +80,17 @@ def perform private + def unit_primitive + 'troubleshoot_job' + end + + def tracking_context + { + request_id: context.request_id, + action: unit_primitive + } + end + def disabled? Feature.disabled?(:root_cause_analysis_duo, context.current_user) end @@ -109,8 +117,7 @@ def job strong_memoize_attr :job def authorize - context.current_user.can?(:read_build_trace, job) && - Utils::ChatAuthorizer.context(context: context).allowed? + context.current_user.can?(:troubleshoot_job_with_ai, job) end def resource_name diff --git a/ee/spec/features/projects/jobs/root_cause_analysis_job_page_spec.rb b/ee/spec/features/projects/jobs/root_cause_analysis_job_page_spec.rb index e15552744369571189e8a5c2df3c9294b5806499..bc2feca01a4d349e4d93622b32c8ac56b0554e30 100644 --- a/ee/spec/features/projects/jobs/root_cause_analysis_job_page_spec.rb +++ b/ee/spec/features/projects/jobs/root_cause_analysis_job_page_spec.rb @@ -71,17 +71,24 @@ stub_feature_flags(root_cause_analysis_duo: true) end - context 'with failed jobs' do + context 'with duo enterprise license' do before do - allow(failed_job).to receive(:debug_mode?).and_return(false) + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(user, :troubleshoot_job_with_ai, failed_job).and_return(true) + end - visit(project_job_path(project, failed_job)) + context 'with failed jobs' do + before do + allow(failed_job).to receive(:debug_mode?).and_return(false) - wait_for_requests - end + visit(project_job_path(project, failed_job)) + + wait_for_requests + end - it 'does display rca with duo button' do - expect(page).to have_selector("[data-testid='rca-duo-button']") + it 'does display rca with duo button' do + expect(page).to have_selector("[data-testid='rca-duo-button']") + end end end end diff --git a/ee/spec/frontend/ci/job_details/components/sidebar/job_log_top_bar_spec.js b/ee/spec/frontend/ci/job_details/components/sidebar/job_log_top_bar_spec.js index 7d0542cc83dc16579eb8ea0f9791ca0e9b68f58f..5e7d89637c50718a5301a1c2892c5fb315745497 100644 --- a/ee/spec/frontend/ci/job_details/components/sidebar/job_log_top_bar_spec.js +++ b/ee/spec/frontend/ci/job_details/components/sidebar/job_log_top_bar_spec.js @@ -28,6 +28,9 @@ describe('EE JobLogTopBar', () => { duoFeaturesEnabled: true, rootCauseAnalysisDuo: false, jobGid: 'gid://gitlab/Ci::Build/123', + glAbilities: { + troubleshootJobWithAi: false, + }, }, }); }; diff --git a/ee/spec/lib/cloud_connector/self_signed/access_data_reader_spec.rb b/ee/spec/lib/cloud_connector/self_signed/access_data_reader_spec.rb index d82a201f231b4b2abc25232150659e8ebb23ac8f..6a86268ce718dc3013d512118abb079da005a1ef 100644 --- a/ee/spec/lib/cloud_connector/self_signed/access_data_reader_spec.rb +++ b/ee/spec/lib/cloud_connector/self_signed/access_data_reader_spec.rb @@ -57,6 +57,14 @@ } end + let_it_be(:troubleshoot_job_bundled_with) do + { + "duo_enterprise" => %i[ + troubleshoot_job + ] + } + end + let_it_be(:resolve_vulnerability_bundled_with) do { "duo_enterprise" => %i[ @@ -103,7 +111,8 @@ glab_ask_git_command: [nil, glab_ask_git_command_bundled_with, backend], explain_vulnerability: [nil, explain_vulnerability_bundled_with, backend], - summarize_comments: [nil, summarize_comments_bundled_with, backend] + summarize_comments: [nil, summarize_comments_bundled_with, backend], + troubleshoot_job: [nil, troubleshoot_job_bundled_with, backend] } end end diff --git a/ee/spec/lib/gitlab/llm/chain/tools/troubleshoot_job/executor_spec.rb b/ee/spec/lib/gitlab/llm/chain/tools/troubleshoot_job/executor_spec.rb index 373a267fd16c6b912be31eb0dac2bb8c5afbf193..d6c22878a7047a017cc56c38f34ec74546d0447a 100644 --- a/ee/spec/lib/gitlab/llm/chain/tools/troubleshoot_job/executor_spec.rb +++ b/ee/spec/lib/gitlab/llm/chain/tools/troubleshoot_job/executor_spec.rb @@ -41,6 +41,11 @@ end end + before do + allow(user).to receive(:can?).and_call_original + allow(user).to receive(:can?).with(:troubleshoot_job_with_ai, build).and_return(true) + end + describe '#name' do it 'returns the correct tool name' do expect(described_class::NAME).to eq('TroubleshootJob') @@ -68,10 +73,6 @@ include_context 'with stubbed LLM authorizer', allowed: true before do - allow(user).to receive(:can?).and_call_original - allow(user).to receive(:can?).with(:read_build, build).and_return(true) - allow(user).to receive(:can?).with(:read_build_trace, build).and_return(true) - allow(Gitlab::Llm::Chain::Utils::ChatAuthorizer).to receive_message_chain(:context, :allowed?).and_return(true) allow(tool).to receive(:provider_prompt_class).and_return(prompt_class) end @@ -80,6 +81,12 @@ expect(tool.execute.content).to eq('Troubleshooting response') end + it 'sets the correct unit primitive' do + expect(ai_request_double).to receive(:request).with(tool.prompt, unit_primitive: 'troubleshoot_job') + + tool.execute + end + context 'with repository languages' do include_context 'with repo languages' @@ -133,7 +140,7 @@ end it 'returns an error message' do - expect(tool.execute.content).to include('This command is used for troubleshooting jobs') + expect(tool.execute.content).to include("I'm sorry, I can't generate a response.") end end end @@ -145,7 +152,7 @@ allow(tool).to receive(:provider_prompt_class).and_return( ::Gitlab::Llm::Chain::Tools::TroubleshootJob::Prompts::Anthropic ) - allow(user).to receive(:can?).with(:read_build_trace, build).and_return(false) + allow(user).to receive(:can?).with(:troubleshoot_job_with_ai, build).and_return(false) end it 'returns an error message' do diff --git a/ee/spec/policies/ci/build_policy_spec.rb b/ee/spec/policies/ci/build_policy_spec.rb index ac992958620106111ee80282942e34ffd9c518e3..cc6436e86c69066d49f68168e2033c38adea4e38 100644 --- a/ee/spec/policies/ci/build_policy_spec.rb +++ b/ee/spec/policies/ci/build_policy_spec.rb @@ -4,4 +4,122 @@ RSpec.describe Ci::BuildPolicy, feature_category: :continuous_integration do it_behaves_like 'a deployable job policy in EE', :ci_build + + describe 'troubleshoot_job_with_ai' do + let(:authorized) { true } + let(:cloud_connector_free_access) { true } + let(:cloud_connector_user_access) { true } + let_it_be(:project) { create(:project, :private) } + let_it_be(:pipeline) { create(:ci_empty_pipeline, project: project) } + let_it_be(:build) { create(:ci_build, pipeline: pipeline) } + let_it_be(:user) { create(:user) } + + subject { described_class.new(user, build) } + + before_all do + project.add_maintainer(user) + end + + before do + stub_licensed_features(ai_features: true, troubleshoot_job: true) + allow(::Gitlab::Llm::Chain::Utils::ChatAuthorizer).to receive_message_chain( + :resource, :allowed?).and_return(authorized) + allow(user).to receive(:can?).with(:admin_all_resources).and_call_original + allow(::Gitlab::Llm::StageCheck).to receive(:available?).and_return(true) + allow(user).to receive(:can?).with(:access_duo_chat).and_return(true) + allow(user).to receive(:can?).with(:access_duo_features, build.project).and_return(true) + allow(::CloudConnector::AvailableServices).to receive(:find_by_name).with(:troubleshoot_job).and_return( + instance_double( + CloudConnector::BaseAvailableServiceData, + free_access?: cloud_connector_free_access, + allowed_for?: cloud_connector_user_access + ) + ) + end + + context 'when feature is chat authorized' do + subject { described_class.new(user, build) } + + let(:authorized) { true } + + it { is_expected.to be_allowed(:troubleshoot_job_with_ai) } + + context 'when user cannot read_build' do + before_all do + project.add_guest(user) + end + + before do + project.update_attribute(:public_builds, false) + end + + it { is_expected.to be_disallowed(:troubleshoot_job_with_ai) } + end + + context 'when the feature is not ai licensed' do + before do + stub_licensed_features(ai_features: false) + end + + it { is_expected.to be_disallowed(:troubleshoot_job_with_ai) } + end + + context 'when feature is not licensed' do + before do + stub_licensed_features(troubleshoot_job: false) + end + + it { is_expected.to be_disallowed(:troubleshoot_job_with_ai) } + end + end + + context 'when feature is not authorized' do + let(:authorized) { false } + + it { is_expected.to be_disallowed(:troubleshoot_job_with_ai) } + end + + # TODO: remove these tests when implementing https://gitlab.com/gitlab-org/gitlab/-/issues/473087 + describe 'cloud connector' do + using RSpec::Parameterized::TableSyntax + where(:free_access, :user_access, :allowed) do + true | true | true + true | false | true + false | true | true + false | false | false + end + + with_them do + let(:cloud_connector_free_access) { free_access } + let(:cloud_connector_user_access) { user_access } + let(:policy) { :troubleshoot_job_with_ai } + + it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) } + end + end + + context 'when on .org or .com', :saas do + using RSpec::Parameterized::TableSyntax + where(:group_with_ai_membership, :free_access, :user_access, :allowed) do + true | true | true | true + true | false | true | true + false | false | true | true + false | false | false | false + true | true | false | true + false | true | false | false + end + + with_them do + before do + allow(user).to receive(:any_group_with_ai_available?).and_return(group_with_ai_membership) + end + + let(:cloud_connector_free_access) { free_access } + let(:cloud_connector_user_access) { user_access } + let(:policy) { :troubleshoot_job_with_ai } + + it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) } + end + end + end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index bdcd35a7b0fdd076ad95a7781594a64c64cdbecd..ff2659a670fdcd0c812b71ef53decd4356867bf5 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -54190,9 +54190,6 @@ msgstr "" msgid "This command is used for troubleshooting jobs and can only be invoked from a failed job log page." msgstr "" -msgid "This command is used for troubleshooting jobs and can only be invoked from a job log page." -msgstr "" - msgid "This comment changed after you started editing it. Review the %{startTag}updated comment%{endTag} to ensure information is not lost." msgstr ""