From afe2063a63f716c8e0affb6a55ebb564782c4e90 Mon Sep 17 00:00:00 2001 From: Julie Huang Date: Tue, 21 Oct 2025 15:27:13 +1100 Subject: [PATCH 1/2] Allow AvailableModelsResolver to authorize by project --- doc/api/graphql/reference/_index.md | 1 + .../components/duo_agentic_chat.vue | 1 + ...get_ai_chat_available_models.query.graphql | 4 +- .../ai/chat/available_models_resolver.rb | 14 +++++- .../ai/chat/available_models_resolver_spec.rb | 44 +++++++++++++++---- 5 files changed, 51 insertions(+), 13 deletions(-) diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 4abf36ac582f61..e3d5de80f68f8e 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -334,6 +334,7 @@ Returns [`AvailableModels`](#availablemodels). | Name | Type | Description | | ---- | ---- | ----------- | +| `projectId` | [`ProjectID`](#projectid) | Global ID of the project the user is acting on. | | `rootNamespaceId` | [`GroupID!`](#groupid) | Global ID of the namespace the user is acting on. | ### `Query.aiChatContextPresets` diff --git a/ee/app/assets/javascripts/ai/duo_agentic_chat/components/duo_agentic_chat.vue b/ee/app/assets/javascripts/ai/duo_agentic_chat/components/duo_agentic_chat.vue index 4b468abd92a5d9..8af978c34aae5c 100644 --- a/ee/app/assets/javascripts/ai/duo_agentic_chat/components/duo_agentic_chat.vue +++ b/ee/app/assets/javascripts/ai/duo_agentic_chat/components/duo_agentic_chat.vue @@ -159,6 +159,7 @@ export default { variables() { return { rootNamespaceId: this.rootNamespaceId, + projectId: this.projectId, }; }, update(data) { diff --git a/ee/app/assets/javascripts/ai/graphql/get_ai_chat_available_models.query.graphql b/ee/app/assets/javascripts/ai/graphql/get_ai_chat_available_models.query.graphql index def23d46b46020..289af54d2bed74 100644 --- a/ee/app/assets/javascripts/ai/graphql/get_ai_chat_available_models.query.graphql +++ b/ee/app/assets/javascripts/ai/graphql/get_ai_chat_available_models.query.graphql @@ -1,5 +1,5 @@ -query getAiChatAvailableModels($rootNamespaceId: GroupID!) { - aiChatAvailableModels(rootNamespaceId: $rootNamespaceId) { +query getAiChatAvailableModels($rootNamespaceId: GroupID!, $projectId: ProjectID) { + aiChatAvailableModels(rootNamespaceId: $rootNamespaceId, projectId: $projectId) { defaultModel { name ref diff --git a/ee/app/graphql/resolvers/ai/chat/available_models_resolver.rb b/ee/app/graphql/resolvers/ai/chat/available_models_resolver.rb index 2650f8c4ba80ee..681ce4e7e51fc2 100644 --- a/ee/app/graphql/resolvers/ai/chat/available_models_resolver.rb +++ b/ee/app/graphql/resolvers/ai/chat/available_models_resolver.rb @@ -15,8 +15,18 @@ class AvailableModelsResolver < BaseResolver required: true, description: 'Global ID of the namespace the user is acting on.' - def resolve(root_namespace_id:) - namespace = authorized_find!(id: root_namespace_id) + argument :project_id, + ::Types::GlobalIDType[::Project], + required: false, + description: 'Global ID of the project the user is acting on.' + + def resolve(root_namespace_id:, project_id: nil) + if project_id + project = authorized_find!(id: project_id) + namespace = project.root_namespace + else + namespace = authorized_find!(id: root_namespace_id) + end result = ::Ai::ModelSelection::FetchModelDefinitionsService .new(current_user, model_selection_scope: namespace) diff --git a/ee/spec/graphql/resolvers/ai/chat/available_models_resolver_spec.rb b/ee/spec/graphql/resolvers/ai/chat/available_models_resolver_spec.rb index fbe71301fe6a8c..3eeb71647057d3 100644 --- a/ee/spec/graphql/resolvers/ai/chat/available_models_resolver_spec.rb +++ b/ee/spec/graphql/resolvers/ai/chat/available_models_resolver_spec.rb @@ -21,6 +21,12 @@ .and_return(true) end + shared_examples 'returns model selection data' do + it "returns the correct structure with default and selectable models" do + expect(resolver).to eq(expected_result) + end + end + context "when service returns successful result" do let(:service_result) do ServiceResponse.success(payload: { @@ -41,21 +47,41 @@ }) end - before do - allow_next_instance_of(::Ai::ModelSelection::FetchModelDefinitionsService) do |service| - allow(service).to receive(:execute).and_return(service_result) - end - end - - it "returns the correct structure with default and selectable models" do - expect(resolver).to eq({ + let(:expected_result) do + { default_model: { name: "Claude Sonnet 4.0 - Anthropic", ref: "claude_sonnet_4_20250514" }, selectable_models: [ { name: "Claude Sonnet 4.0 - Anthropic", ref: "claude_sonnet_4_20250514" }, { name: "Claude Sonnet 3.7 - Anthropic", ref: "claude_sonnet_3_7_20250219" } ], pinned_model: nil - }) + } + end + + before do + allow_next_instance_of(::Ai::ModelSelection::FetchModelDefinitionsService) do |service| + allow(service).to receive(:execute).and_return(service_result) + end + end + + context "when a root namespace can access duo agentic chat" do + include_examples 'returns model selection data' + end + + context "when a project can access duo agentic chat" do + let_it_be(:project) { create(:project) } + + let(:args) do + { root_namespace_id: GitlabSchema.id_from_object(group), project_id: GitlabSchema.id_from_object(project) } + end + + before do + allow(Ability).to receive(:allowed?) + .with(current_user, :access_duo_agentic_chat, project) + .and_return(true) + end + + include_examples 'returns model selection data' end context "when there is a pinned model" do -- GitLab From 52df636e40a9fa6df307ade403b2e3ed12b0734b Mon Sep 17 00:00:00 2001 From: Julie Huang Date: Wed, 22 Oct 2025 22:38:51 +1100 Subject: [PATCH 2/2] Fix test mocks and add auth tests --- .../ai/chat/available_models_resolver_spec.rb | 90 ++++++++++++++++--- 1 file changed, 76 insertions(+), 14 deletions(-) diff --git a/ee/spec/graphql/resolvers/ai/chat/available_models_resolver_spec.rb b/ee/spec/graphql/resolvers/ai/chat/available_models_resolver_spec.rb index 3eeb71647057d3..8894acdf5e0693 100644 --- a/ee/spec/graphql/resolvers/ai/chat/available_models_resolver_spec.rb +++ b/ee/spec/graphql/resolvers/ai/chat/available_models_resolver_spec.rb @@ -21,12 +21,61 @@ .and_return(true) end - shared_examples 'returns model selection data' do + shared_examples "returns model selection data" do it "returns the correct structure with default and selectable models" do expect(resolver).to eq(expected_result) end end + shared_examples "returns a ResourceNotAvailable error" do + it "generates an error" do + expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) do + resolver + end + end + end + + describe "the resource is not authorized" do + context "when the resource is a root namespace" do + context "when the namespace does not have duo agentic chat enabled" do + before do + allow(Ability).to receive(:allowed?) + .with(current_user, :access_duo_agentic_chat, group) + .and_return(false) + end + + include_examples "returns a ResourceNotAvailable error" + end + end + + context "when the resource is a project" do + context "when the namespace has duo agentic chat enabled" do + before do + allow(Ability).to receive(:allowed?) + .with(current_user, :access_duo_agentic_chat, group) + .and_return(true) + end + + context "when the child project does not have duo agentic chat enabled" do + let_it_be(:project) { create(:project, group: group) } + + let(:args) do + { root_namespace_id: GitlabSchema.id_from_object(group), + project_id: GitlabSchema.id_from_object(project) } + end + + before do + allow(Ability).to receive(:allowed?) + .with(current_user, :access_duo_agentic_chat, project) + .and_return(false) + end + + include_examples "returns a ResourceNotAvailable error" + end + end + end + end + context "when service returns successful result" do let(:service_result) do ServiceResponse.success(payload: { @@ -64,24 +113,37 @@ end end - context "when a root namespace can access duo agentic chat" do - include_examples 'returns model selection data' + context "when the resource is a root namespace" do + context "when the namespace has duo agentic chat enabled" do + include_examples "returns model selection data" + end end - context "when a project can access duo agentic chat" do - let_it_be(:project) { create(:project) } + context "when the resource is a project" do + context "when the namespace does not have duo agentic chat enabled" do + before do + allow(Ability).to receive(:allowed?) + .with(current_user, :access_duo_agentic_chat, group) + .and_return(false) + end - let(:args) do - { root_namespace_id: GitlabSchema.id_from_object(group), project_id: GitlabSchema.id_from_object(project) } - end + context "when the child project has duo agentic chat enabled" do + let_it_be(:project) { create(:project, group: group) } - before do - allow(Ability).to receive(:allowed?) - .with(current_user, :access_duo_agentic_chat, project) - .and_return(true) - end + let(:args) do + { root_namespace_id: GitlabSchema.id_from_object(group), + project_id: GitlabSchema.id_from_object(project) } + end - include_examples 'returns model selection data' + before do + allow(Ability).to receive(:allowed?) + .with(current_user, :access_duo_agentic_chat, project) + .and_return(true) + end + + include_examples "returns model selection data" + end + end end context "when there is a pinned model" do -- GitLab