Commit 95917eb2 authored by Markus Koller's avatar Markus Koller

Use type to detect password fields in integrations instead of name

When updating integration settings, password fields only need to be
filled in if the user wants to change them, otherwise the previous
values would be preserved.

This relied on clearing blank params with the name `password`, but some
integrations use a different name, such as `api_key` in the Datadog
integration. So instead we check for fields with `type: 'password'`,
which is also what the frontend does to decide how to render these
fields in the `DynamicField` component.

Changelog: fixed
parent 46c10594
...@@ -77,18 +77,15 @@ module Integrations ...@@ -77,18 +77,15 @@ module Integrations
:webhook :webhook
].freeze ].freeze
# Parameters to ignore if no value is specified
FILTER_BLANK_PARAMS = [:password].freeze
def integration_params def integration_params
dynamic_params = @integration.event_channel_names + @integration.event_names # rubocop:disable Gitlab/ModuleWithInstanceVariables dynamic_params = integration.event_channel_names + integration.event_names
allowed = allowed_integration_params + dynamic_params allowed = allowed_integration_params + dynamic_params
return_value = params.permit(:id, integration: allowed, service: allowed) return_value = params.permit(:id, integration: allowed, service: allowed)
return_value[:integration] ||= return_value.delete(:service) return_value[:integration] ||= return_value.delete(:service)
param_values = return_value[:integration] param_values = return_value[:integration]
if param_values.is_a?(ActionController::Parameters) if param_values.is_a?(ActionController::Parameters)
FILTER_BLANK_PARAMS.each do |param| integration.password_fields.each do |param|
param_values.delete(param) if param_values[param].blank? param_values.delete(param) if param_values[param].blank?
end end
end end
......
...@@ -26,15 +26,15 @@ class Projects::ServicesController < Projects::ApplicationController ...@@ -26,15 +26,15 @@ class Projects::ServicesController < Projects::ApplicationController
attributes = integration_params[:integration] attributes = integration_params[:integration]
if use_inherited_settings?(attributes) if use_inherited_settings?(attributes)
@integration.inherit_from_id = default_integration.id integration.inherit_from_id = default_integration.id
if saved = @integration.save(context: :manual_change) if saved = integration.save(context: :manual_change)
BulkUpdateIntegrationService.new(default_integration, [@integration]).execute BulkUpdateIntegrationService.new(default_integration, [integration]).execute
end end
else else
attributes[:inherit_from_id] = nil attributes[:inherit_from_id] = nil
@integration.attributes = attributes integration.attributes = attributes
saved = @integration.save(context: :manual_change) saved = integration.save(context: :manual_change)
end end
respond_to do |format| respond_to do |format|
...@@ -65,15 +65,15 @@ class Projects::ServicesController < Projects::ApplicationController ...@@ -65,15 +65,15 @@ class Projects::ServicesController < Projects::ApplicationController
private private
def redirect_path def redirect_path
safe_redirect_path(params[:redirect_to]).presence || edit_project_service_path(@project, @integration) safe_redirect_path(params[:redirect_to]).presence || edit_project_service_path(project, integration)
end end
def service_test_response def service_test_response
unless @integration.update(integration_params[:integration]) unless integration.update(integration_params[:integration])
return { error: true, message: _('Validations failed.'), service_response: @integration.errors.full_messages.join(','), test_failed: false } return { error: true, message: _('Validations failed.'), service_response: integration.errors.full_messages.join(','), test_failed: false }
end end
result = ::Integrations::Test::ProjectService.new(@integration, current_user, params[:event]).execute result = ::Integrations::Test::ProjectService.new(integration, current_user, params[:event]).execute
unless result[:success] unless result[:success]
return { error: true, message: s_('Integrations|Connection failed. Please check your settings.'), service_response: result[:message].to_s, test_failed: true } return { error: true, message: s_('Integrations|Connection failed. Please check your settings.'), service_response: result[:message].to_s, test_failed: true }
...@@ -93,7 +93,7 @@ class Projects::ServicesController < Projects::ApplicationController ...@@ -93,7 +93,7 @@ class Projects::ServicesController < Projects::ApplicationController
end end
def integration def integration
@integration ||= @project.find_or_initialize_integration(params[:id]) @integration ||= project.find_or_initialize_integration(params[:id])
end end
alias_method :service, :integration alias_method :service, :integration
......
...@@ -357,6 +357,10 @@ class Integration < ApplicationRecord ...@@ -357,6 +357,10 @@ class Integration < ApplicationRecord
[] []
end end
def password_fields
fields.select { |f| f[:type] == 'password' }.pluck(:name)
end
# Expose a list of fields in the JSON endpoint. # Expose a list of fields in the JSON endpoint.
# #
# This list is used in `Integration#as_json(only: json_fields)`. # This list is used in `Integration#as_json(only: json_fields)`.
......
...@@ -9,6 +9,14 @@ RSpec.describe Admin::IntegrationsController do ...@@ -9,6 +9,14 @@ RSpec.describe Admin::IntegrationsController do
sign_in(admin) sign_in(admin)
end end
it_behaves_like IntegrationsActions do
let(:integration_attributes) { { instance: true, project: nil } }
let(:routing_params) do
{ id: integration.to_param }
end
end
describe '#edit' do describe '#edit' do
Integration.available_integration_names.each do |integration_name| Integration.available_integration_names.each do |integration_name|
context "#{integration_name}" do context "#{integration_name}" do
......
...@@ -10,6 +10,21 @@ RSpec.describe Groups::Settings::IntegrationsController do ...@@ -10,6 +10,21 @@ RSpec.describe Groups::Settings::IntegrationsController do
sign_in(user) sign_in(user)
end end
it_behaves_like IntegrationsActions do
let(:integration_attributes) { { group: group, project: nil } }
let(:routing_params) do
{
group_id: group,
id: integration.to_param
}
end
before do
group.add_owner(user)
end
end
describe '#index' do describe '#index' do
context 'when user is not owner' do context 'when user is not owner' do
it 'renders not_found' do it 'renders not_found' do
......
...@@ -18,6 +18,18 @@ RSpec.describe Projects::ServicesController do ...@@ -18,6 +18,18 @@ RSpec.describe Projects::ServicesController do
project.add_maintainer(user) project.add_maintainer(user)
end end
it_behaves_like IntegrationsActions do
let(:integration_attributes) { { project: project } }
let(:routing_params) do
{
namespace_id: project.namespace,
project_id: project,
id: integration.to_param
}
end
end
describe '#test' do describe '#test' do
context 'when the integration is not testable' do context 'when the integration is not testable' do
it 'renders 404' do it 'renders 404' do
......
...@@ -12,6 +12,12 @@ FactoryBot.define do ...@@ -12,6 +12,12 @@ FactoryBot.define do
issue_tracker issue_tracker
end end
factory :datadog_integration, class: 'Integrations::Datadog' do
project
active { true }
api_key { 'secret' }
end
factory :emails_on_push_integration, class: 'Integrations::EmailsOnPush' do factory :emails_on_push_integration, class: 'Integrations::EmailsOnPush' do
project project
type { 'EmailsOnPushService' } type { 'EmailsOnPushService' }
......
...@@ -825,4 +825,20 @@ RSpec.describe Integration do ...@@ -825,4 +825,20 @@ RSpec.describe Integration do
.to include(*described_class::PROJECT_SPECIFIC_INTEGRATION_NAMES) .to include(*described_class::PROJECT_SPECIFIC_INTEGRATION_NAMES)
end end
end end
describe '#password_fields' do
it 'returns all fields with type `password`' do
allow(subject).to receive(:fields).and_return([
{ name: 'password', type: 'password' },
{ name: 'secret', type: 'password' },
{ name: 'public', type: 'text' }
])
expect(subject.password_fields).to match_array(%w[password secret])
end
it 'returns an empty array if no password fields exist' do
expect(subject.password_fields).to eq([])
end
end
end end
# frozen_string_literal: true
RSpec.shared_examples IntegrationsActions do
let(:integration) do
create(:datadog_integration,
integration_attributes.merge(
api_url: 'http://example.com',
api_key: 'secret'
)
)
end
describe 'GET #edit' do
before do
get :edit, params: routing_params
end
it 'assigns the integration' do
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:integration)).to eq(integration)
end
end
describe 'PUT #update' do
let(:params) do
{
datadog_env: 'env',
datadog_service: 'service'
}
end
before do
put :update, params: routing_params.merge(integration: params)
end
it 'updates the integration with the provided params and redirects to the form' do
expect(response).to redirect_to(routing_params.merge(action: :edit))
expect(integration.reload).to have_attributes(params)
end
context 'when sending a password field' do
let(:params) { super().merge(api_key: 'new') }
it 'updates the integration with the password and other params' do
expect(response).to be_redirect
expect(integration.reload).to have_attributes(params)
end
end
context 'when sending a blank password field' do
let(:params) { super().merge(api_key: '') }
it 'ignores the password field and saves the other params' do
expect(response).to be_redirect
expect(integration.reload).to have_attributes(params.merge(api_key: 'secret'))
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment