From 4ea1635b98003b21011649617990738110bbb8df Mon Sep 17 00:00:00 2001 From: Andrew Fontaine <afontaine@gitlab.com> Date: Wed, 6 Dec 2023 14:29:18 +0000 Subject: [PATCH] Add endpoints to paginate deploy keys by type This will give the frontend the ability to fetch a specific set of deploy keys at at time, reducing page load times and DB query time. --- .../projects/deploy_keys_controller.rb | 41 +++++++ app/finders/deploy_keys/deploy_keys_finder.rb | 50 +++++++++ app/models/deploy_key.rb | 1 + .../deploy_keys/deploy_key_serializer.rb | 1 + config/routes/project.rb | 6 + .../projects/deploy_keys_controller_spec.rb | 104 ++++++++++++------ .../deploy_keys/deploy_keys_finder_spec.rb | 78 +++++++++++++ 7 files changed, 246 insertions(+), 35 deletions(-) create mode 100644 app/finders/deploy_keys/deploy_keys_finder.rb create mode 100644 spec/finders/deploy_keys/deploy_keys_finder_spec.rb diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index 9cdbd2a30f64..65aa836c5621 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -22,6 +22,34 @@ def index end end + def enabled_keys + respond_to do |format| + format.json do + enabled_keys = find_keys(filter: :enabled_keys) + render json: serialize(enabled_keys) + end + end + end + + def available_project_keys + respond_to do |format| + format.json do + available_project_keys = find_keys(filter: :available_project_keys) + render json: serialize(available_project_keys) + end + end + end + + def available_public_keys + respond_to do |format| + format.json do + available_public_keys = find_keys(filter: :available_public_keys) + + render json: serialize(available_public_keys) + end + end + end + def new redirect_to_repository end @@ -108,4 +136,17 @@ def authorize_update_deploy_key! def redirect_to_repository redirect_to_repository_settings(@project, anchor: 'js-deploy-keys-settings') end + + def find_keys(params) + DeployKeys::DeployKeysFinder.new(@project, current_user, params) + .execute + end + + def serialize(keys) + opts = { user: current_user, project: project } + + DeployKeys::DeployKeySerializer.new + .with_pagination(request, response) + .represent(keys, opts) + end end diff --git a/app/finders/deploy_keys/deploy_keys_finder.rb b/app/finders/deploy_keys/deploy_keys_finder.rb new file mode 100644 index 000000000000..5924a6568016 --- /dev/null +++ b/app/finders/deploy_keys/deploy_keys_finder.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module DeployKeys + class DeployKeysFinder + attr_reader :project, :current_user, :params + + def initialize(project, current_user, params = {}) + @project = project + @current_user = current_user + @params = params + end + + def execute + return empty unless can_admin_project? + + case params[:filter] + when :enabled_keys + enabled_keys + when :available_project_keys + available_project_keys + when :available_public_keys + available_public_keys + else + empty + end + end + + private + + def enabled_keys + project.deploy_keys.with_projects + end + + def available_project_keys + current_user.project_deploy_keys.with_projects.not_in(enabled_keys) + end + + def available_public_keys + DeployKey.are_public.with_projects.not_in(enabled_keys) + end + + def empty + DeployKey.none + end + + def can_admin_project? + current_user.can?(:admin_project, project) + end + end +end diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb index 9ede494cfccd..5923497a1a3c 100644 --- a/app/models/deploy_key.rb +++ b/app/models/deploy_key.rb @@ -25,6 +25,7 @@ class DeployKey < Key scope :with_projects, -> { includes(deploy_keys_projects: { project: [:route, namespace: :route] }) } scope :including_projects_with_write_access, -> { includes(:projects_with_write_access) } scope :including_projects_with_readonly_access, -> { includes(:projects_with_readonly_access) } + scope :not_in, ->(keys) { where.not(id: keys.select(:id)) } accepts_nested_attributes_for :deploy_keys_projects, reject_if: :reject_deploy_keys_projects? diff --git a/app/serializers/deploy_keys/deploy_key_serializer.rb b/app/serializers/deploy_keys/deploy_key_serializer.rb index b00ef65696f9..2e6291a95f20 100644 --- a/app/serializers/deploy_keys/deploy_key_serializer.rb +++ b/app/serializers/deploy_keys/deploy_key_serializer.rb @@ -3,5 +3,6 @@ module DeployKeys class DeployKeySerializer < BaseSerializer entity DeployKeyEntity + include WithPagination end end diff --git a/config/routes/project.rb b/config/routes/project.rb index 9016a683404f..7064e1e19dbb 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -186,6 +186,12 @@ end resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create, :edit, :update] do + collection do + get :enabled_keys + get :available_project_keys + get :available_public_keys + end + member do put :enable put :disable diff --git a/spec/controllers/projects/deploy_keys_controller_spec.rb b/spec/controllers/projects/deploy_keys_controller_spec.rb index 96addb4b6c5d..43f089cede90 100644 --- a/spec/controllers/projects/deploy_keys_controller_spec.rb +++ b/spec/controllers/projects/deploy_keys_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Projects::DeployKeysController do +RSpec.describe Projects::DeployKeysController, feature_category: :continuous_delivery do let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:admin) { create(:admin) } @@ -13,60 +13,94 @@ sign_in(user) end - describe 'GET index' do - let(:params) do - { namespace_id: project.namespace, project_id: project } - end + describe 'GET actions' do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user) } - context 'when html requested' do - it 'redirects to project settings with the correct anchor' do - get :index, params: params + let_it_be(:accessible_project) { create(:project, :internal).tap { |p| p.add_developer(user) } } + let_it_be(:inaccessible_project) { create(:project, :internal) } + let_it_be(:project_private) { create(:project, :private) } - expect(response).to redirect_to(project_settings_repository_path(project, anchor: 'js-deploy-keys-settings')) - end + let_it_be(:deploy_key_for_target_project) do + create(:deploy_keys_project, project: project, deploy_key: create(:deploy_key)) end - context 'when json requested' do - let(:project2) { create(:project, :internal) } - let(:project_private) { create(:project, :private) } + let_it_be(:deploy_key_for_accessible_project) do + create(:deploy_keys_project, project: accessible_project, deploy_key: create(:deploy_key)) + end - let(:deploy_key_internal) { create(:deploy_key) } - let(:deploy_key_actual) { create(:deploy_key) } - let!(:deploy_key_public) { create(:deploy_key, public: true) } + let_it_be(:deploy_key_for_inaccessible_project) do + create(:deploy_keys_project, project: inaccessible_project, deploy_key: create(:deploy_key)) + end - let!(:deploy_keys_project_internal) do - create(:deploy_keys_project, project: project2, deploy_key: deploy_key_internal) - end + let_it_be(:deploy_keys_project_private) do + create(:deploy_keys_project, project: project_private, deploy_key: create(:another_deploy_key)) + end - let!(:deploy_keys_project_actual) do - create(:deploy_keys_project, project: project, deploy_key: deploy_key_actual) - end + let_it_be(:deploy_key_public) { create(:deploy_key, public: true) } - let!(:deploy_keys_project_private) do - create(:deploy_keys_project, project: project_private, deploy_key: create(:another_deploy_key)) + describe 'GET index' do + let(:params) do + { namespace_id: project.namespace, project_id: project } end - context 'when user has access to all projects where deploy keys are used' do - before do - project2.add_developer(user) + context 'when html requested' do + it 'redirects to project settings with the correct anchor' do + get :index, params: params + + expect(response).to redirect_to(project_settings_repository_path(project, anchor: 'js-deploy-keys-settings')) end + end + context 'when json requested' do it 'returns json in a correct format' do get :index, params: params.merge(format: :json) expect(json_response.keys).to match_array(%w[enabled_keys available_project_keys public_keys]) - expect(json_response['enabled_keys'].count).to eq(1) - expect(json_response['available_project_keys'].count).to eq(1) - expect(json_response['public_keys'].count).to eq(1) + expect(json_response['enabled_keys'].pluck('id')).to match_array( + [deploy_key_for_target_project.deploy_key_id] + ) + expect(json_response['available_project_keys'].pluck('id')).to match_array( + [deploy_key_for_accessible_project.deploy_key_id] + ) + expect(json_response['public_keys'].pluck('id')).to match_array([deploy_key_public.id]) end end + end - context 'when user has no access to all projects where deploy keys are used' do - it 'returns json in a correct format' do - get :index, params: params.merge(format: :json) + describe 'GET enabled_keys' do + let(:params) do + { namespace_id: project.namespace, project_id: project } + end - expect(json_response['available_project_keys'].count).to eq(0) - end + it 'returns only enabled keys' do + get :enabled_keys, params: params.merge(format: :json) + + expect(json_response.pluck("id")).to match_array([deploy_key_for_target_project.deploy_key_id]) + end + end + + describe 'GET available_project_keys' do + let(:params) do + { namespace_id: project.namespace, project_id: project } + end + + it 'returns available project keys' do + get :available_project_keys, params: params.merge(format: :json) + + expect(json_response.pluck("id")).to match_array([deploy_key_for_accessible_project.deploy_key_id]) + end + end + + describe 'GET available_public_keys' do + let(:params) do + { namespace_id: project.namespace, project_id: project } + end + + it 'returns available public keys' do + get :available_public_keys, params: params.merge(format: :json) + + expect(json_response.pluck("id")).to match_array([deploy_key_public.id]) end end end diff --git a/spec/finders/deploy_keys/deploy_keys_finder_spec.rb b/spec/finders/deploy_keys/deploy_keys_finder_spec.rb new file mode 100644 index 000000000000..f0d3935cc957 --- /dev/null +++ b/spec/finders/deploy_keys/deploy_keys_finder_spec.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe DeployKeys::DeployKeysFinder, feature_category: :continuous_delivery do + describe '#execute' do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :repository) } + + let_it_be(:accessible_project) { create(:project, :internal).tap { |p| p.add_developer(user) } } + let_it_be(:inaccessible_project) { create(:project, :internal) } + let_it_be(:project_private) { create(:project, :private) } + + let_it_be(:deploy_key_for_target_project) do + create(:deploy_keys_project, project: project, deploy_key: create(:deploy_key)) + end + + let_it_be(:deploy_key_for_accessible_project) do + create(:deploy_keys_project, project: accessible_project, deploy_key: create(:deploy_key)) + end + + let_it_be(:deploy_key_for_inaccessible_project) do + create(:deploy_keys_project, project: inaccessible_project, deploy_key: create(:deploy_key)) + end + + let_it_be(:deploy_keys_project_private) do + create(:deploy_keys_project, project: project_private, deploy_key: create(:another_deploy_key)) + end + + let_it_be(:deploy_key_public) { create(:deploy_key, public: true) } + + let(:params) { {} } + + subject(:result) { described_class.new(project, user, params).execute } + + context 'with access' do + before_all do + project.add_maintainer(user) + end + + context 'when filtering for enabled_keys' do + let(:params) { { filter: :enabled_keys } } + + it 'returns the correct result' do + expect(result.map(&:id)).to match_array([deploy_key_for_target_project.deploy_key_id]) + end + end + + context 'when filtering for available project keys' do + let(:params) { { filter: :available_project_keys } } + + it 'returns the correct result' do + expect(result.map(&:id)).to match_array([deploy_key_for_accessible_project.deploy_key_id]) + end + end + + context 'when filtering for available public keys' do + let(:params) { { filter: :available_public_keys } } + + it 'returns the correct result' do + expect(result.map(&:id)).to match_array([deploy_key_public.id]) + end + end + + context 'when there are no set filters' do + it 'returns an empty collection' do + expect(result).to eq DeployKey.none + end + end + end + + context 'without access' do + it 'returns an empty collection' do + expect(result).to eq DeployKey.none + end + end + end +end -- GitLab