From ae182bc96423fc45420ccdd5cef662fc895610a9 Mon Sep 17 00:00:00 2001 From: Jannik Lehmann Date: Tue, 7 Oct 2025 21:17:39 +0200 Subject: [PATCH 1/3] Fix agent drawer showing empty for soft-deleted catalog items When an agent is deleted at the explore level but still exists at the project level (via ItemConsumer), the agent details drawer would remain empty because the ItemResolver explicitly filtered out soft-deleted items. This change removes the deleted item check from the resolver, allowing soft-deleted agents that are still configured for use to be viewed. Authorization is still properly enforced through the policy layer, which already handles access control for catalog items appropriately. Changelog: fixed EE: true --- ee/app/graphql/resolvers/ai/catalog/item_resolver.rb | 2 -- ee/spec/requests/api/graphql/ai/catalog/item_spec.rb | 12 +++++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/ee/app/graphql/resolvers/ai/catalog/item_resolver.rb b/ee/app/graphql/resolvers/ai/catalog/item_resolver.rb index 31c2b1824fc348..9e136f7a1d8b19 100644 --- a/ee/app/graphql/resolvers/ai/catalog/item_resolver.rb +++ b/ee/app/graphql/resolvers/ai/catalog/item_resolver.rb @@ -15,8 +15,6 @@ class ItemResolver < BaseResolver def resolve(id:) Gitlab::Graphql::Lazy.with_value(find_object(id: id)) do |item| - next if item&.deleted? - item end end diff --git a/ee/spec/requests/api/graphql/ai/catalog/item_spec.rb b/ee/spec/requests/api/graphql/ai/catalog/item_spec.rb index 95f0ecf0bad36e..49096b645ee693 100644 --- a/ee/spec/requests/api/graphql/ai/catalog/item_spec.rb +++ b/ee/spec/requests/api/graphql/ai/catalog/item_spec.rb @@ -118,7 +118,17 @@ create(:user).tap { |user| project.add_owner(user) } end - it_behaves_like 'an unsuccessful query' + it 'returns the deleted item' do + post_graphql(query, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(data).to match( + hash_including( + 'id' => catalog_item.to_global_id.to_s, + 'name' => catalog_item.name + ) + ) + end end end end -- GitLab From 10918842b09e385e2ac8776bff9c94d1afbefeca Mon Sep 17 00:00:00 2001 From: Jannik Lehmann Date: Tue, 7 Oct 2025 21:32:16 +0200 Subject: [PATCH 2/3] Add consumer check for deleted item access Ensure soft-deleted catalog items can only be viewed if they have active consumers (are still configured in projects/groups), preventing unauthorized access to arbitrarily deleted items. --- .../resolvers/ai/catalog/item_resolver.rb | 9 ++ .../api/graphql/ai/catalog/item_spec.rb | 97 ++++++++++++++++--- 2 files changed, 95 insertions(+), 11 deletions(-) diff --git a/ee/app/graphql/resolvers/ai/catalog/item_resolver.rb b/ee/app/graphql/resolvers/ai/catalog/item_resolver.rb index 9e136f7a1d8b19..ec5c3aba8bfe92 100644 --- a/ee/app/graphql/resolvers/ai/catalog/item_resolver.rb +++ b/ee/app/graphql/resolvers/ai/catalog/item_resolver.rb @@ -15,6 +15,8 @@ class ItemResolver < BaseResolver def resolve(id:) Gitlab::Graphql::Lazy.with_value(find_object(id: id)) do |item| + next if item&.deleted? && !user_has_item_configured?(item) + item end end @@ -24,6 +26,13 @@ def resolve(id:) def find_object(id:) GitlabSchema.find_by_gid(id) end + + def user_has_item_configured?(item) + return false unless current_user + + # Allow viewing deleted items if the user has them configured in their projects/groups + item.consumers.exists? + end end end end diff --git a/ee/spec/requests/api/graphql/ai/catalog/item_spec.rb b/ee/spec/requests/api/graphql/ai/catalog/item_spec.rb index 49096b645ee693..c5cab986f2fea3 100644 --- a/ee/spec/requests/api/graphql/ai/catalog/item_spec.rb +++ b/ee/spec/requests/api/graphql/ai/catalog/item_spec.rb @@ -113,21 +113,96 @@ context 'with a deleted catalog item' do let_it_be(:catalog_item) { create(:ai_catalog_item, project: project, deleted_at: 1.day.ago) } - context 'when owner' do - let(:current_user) do - create(:user).tap { |user| project.add_owner(user) } + context 'when the item has no consumers' do + context 'when owner' do + let(:current_user) do + create(:user).tap { |user| project.add_owner(user) } + end + + it_behaves_like 'an unsuccessful query' + end + + context 'when developer' do + let(:current_user) do + create(:user).tap { |user| project.add_developer(user) } + end + + it_behaves_like 'an unsuccessful query' + end + + context 'when reporter' do + let(:current_user) do + create(:user).tap { |user| project.add_reporter(user) } + end + + it_behaves_like 'an unsuccessful query' + end + + context 'when anonymous user' do + let(:current_user) { nil } + + it_behaves_like 'an unsuccessful query' end + end + + context 'when the item has consumers (configured in a project)' do + let_it_be(:consumer_project) { create(:project) } + let_it_be(:item_consumer) { create(:ai_catalog_item_consumer, item: catalog_item, project: consumer_project) } - it 'returns the deleted item' do - post_graphql(query, current_user: current_user) + context 'when developer with access to consumer project' do + let(:current_user) do + create(:user).tap { |user| consumer_project.add_developer(user) } + end - expect(response).to have_gitlab_http_status(:success) - expect(data).to match( - hash_including( - 'id' => catalog_item.to_global_id.to_s, - 'name' => catalog_item.name + it 'returns the deleted item' do + post_graphql(query, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(data).to match( + hash_including( + 'id' => catalog_item.to_global_id.to_s, + 'name' => catalog_item.name + ) ) - ) + end + end + + context 'when maintainer with access to consumer project' do + let(:current_user) do + create(:user).tap { |user| consumer_project.add_maintainer(user) } + end + + it 'returns the deleted item' do + post_graphql(query, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(data).to match( + hash_including( + 'id' => catalog_item.to_global_id.to_s, + 'name' => catalog_item.name + ) + ) + end + end + + context 'when reporter with access to consumer project' do + let(:current_user) do + create(:user).tap { |user| consumer_project.add_reporter(user) } + end + + it_behaves_like 'an unsuccessful query' + end + + context 'when user without access to consumer project' do + let(:current_user) { create(:user) } + + it_behaves_like 'an unsuccessful query' + end + + context 'when anonymous user' do + let(:current_user) { nil } + + it_behaves_like 'an unsuccessful query' end end end -- GitLab From 016191540a703218cfd532ec0e9096d268185d6d Mon Sep 17 00:00:00 2001 From: Jannik Lehmann Date: Tue, 7 Oct 2025 21:56:00 +0200 Subject: [PATCH 3/3] Fix authorization check for deleted catalog items Verify user has access to consumer projects/groups, not just that consumers exist. This prevents unauthorized users from viewing deleted items they don't have permission to access. Also update tests to correctly reflect that reporters with access to consumer projects can view public deleted items. --- .../graphql/resolvers/ai/catalog/item_resolver.rb | 15 +++++++++++++-- .../requests/api/graphql/ai/catalog/item_spec.rb | 14 ++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/ee/app/graphql/resolvers/ai/catalog/item_resolver.rb b/ee/app/graphql/resolvers/ai/catalog/item_resolver.rb index ec5c3aba8bfe92..0f8c93463f1465 100644 --- a/ee/app/graphql/resolvers/ai/catalog/item_resolver.rb +++ b/ee/app/graphql/resolvers/ai/catalog/item_resolver.rb @@ -30,8 +30,19 @@ def find_object(id:) def user_has_item_configured?(item) return false unless current_user - # Allow viewing deleted items if the user has them configured in their projects/groups - item.consumers.exists? + # Allow viewing deleted items only if the user has access to at least one + # project/group where the item is configured + item.consumers.any? do |consumer| + if consumer.project + Ability.allowed?(current_user, :read_ai_catalog_item, item) && + Ability.allowed?(current_user, :read_project, consumer.project) + elsif consumer.group + Ability.allowed?(current_user, :read_ai_catalog_item, item) && + Ability.allowed?(current_user, :read_group, consumer.group) + else + false + end + end end end end diff --git a/ee/spec/requests/api/graphql/ai/catalog/item_spec.rb b/ee/spec/requests/api/graphql/ai/catalog/item_spec.rb index c5cab986f2fea3..e82bc64c2902e5 100644 --- a/ee/spec/requests/api/graphql/ai/catalog/item_spec.rb +++ b/ee/spec/requests/api/graphql/ai/catalog/item_spec.rb @@ -111,7 +111,7 @@ end context 'with a deleted catalog item' do - let_it_be(:catalog_item) { create(:ai_catalog_item, project: project, deleted_at: 1.day.ago) } + let_it_be(:catalog_item) { create(:ai_catalog_item, project: project, public: true, deleted_at: 1.day.ago) } context 'when the item has no consumers' do context 'when owner' do @@ -190,7 +190,17 @@ create(:user).tap { |user| consumer_project.add_reporter(user) } end - it_behaves_like 'an unsuccessful query' + it 'returns the deleted item' do + post_graphql(query, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(data).to match( + hash_including( + 'id' => catalog_item.to_global_id.to_s, + 'name' => catalog_item.name + ) + ) + end end context 'when user without access to consumer project' do -- GitLab