Commit b32d9f16 authored by Terri Chu's avatar Terri Chu

Merge branch 'jm-fix-special-character-passwords' into 'master'

Pass hash instead of URI to Elasticsearch client

See merge request gitlab-org/gitlab!85122
parents 946c9385 a1b716a5
...@@ -317,12 +317,8 @@ module EE ...@@ -317,12 +317,8 @@ module EE
end end
def elasticsearch_url_with_credentials def elasticsearch_url_with_credentials
return elasticsearch_url if elasticsearch_username.blank?
elasticsearch_url.map do |uri| elasticsearch_url.map do |uri|
uri.user = URI.encode_www_form_component(elasticsearch_username) ::Gitlab::Elastic::Helper.connection_settings(uri: uri, user: elasticsearch_username, password: elasticsearch_password)
uri.password = URI.encode_www_form_component(elasticsearch_password)
uri
end end
end end
......
...@@ -25,7 +25,7 @@ module GemExtensions ...@@ -25,7 +25,7 @@ module GemExtensions
adapter = ::Gitlab::Elastic::Client.adapter adapter = ::Gitlab::Elastic::Client.adapter
if store.cached_client.nil? || config != store.cached_config || adapter != store.cached_adapter if store.cached_client.nil? || config != store.cached_config || adapter != store.cached_adapter
store.cached_client = ::Gitlab::Elastic::Client.build(config) store.cached_client = ::Gitlab::Elastic::Client.build(config.deep_dup)
store.cached_config = config store.cached_config = config
store.cached_adapter = adapter store.cached_adapter = adapter
end end
......
...@@ -42,6 +42,28 @@ module Gitlab ...@@ -42,6 +42,28 @@ module Gitlab
def default def default
self.new self.new
end end
def connection_settings(uri:, user: nil, password: nil)
# Returns a hash that is compatible with elasticsearch-transport client settings.
#
# See:
# https://github.com/elastic/elasticsearch-ruby/blob/v7.3.0/elasticsearch-transport/lib/elasticsearch/transport/client.rb#L196
uri = Addressable::URI.parse(uri) if uri.is_a? String
{
scheme: uri.scheme,
user: user.presence || Addressable::URI.unencode(uri.user),
password: password.presence || Addressable::URI.unencode(uri.password),
host: uri.host,
path: uri.path,
port: uri.port
}.compact
end
def url_string(url_settings)
# Converts the hash from connection_settings into a percent encoded URL string.
Addressable::URI.new(url_settings).normalize.to_s
end
end end
def default_settings def default_settings
......
...@@ -183,9 +183,18 @@ module Gitlab ...@@ -183,9 +183,18 @@ module Gitlab
end end
def elasticsearch_config(target) def elasticsearch_config(target)
Gitlab::CurrentSettings.elasticsearch_config.merge( config = Gitlab::CurrentSettings.elasticsearch_config.merge(
index_name: target.index_name index_name: target.index_name
).to_json )
# We need to pass a percent encoded URL string instead of a hash
# to the go indexer because it passes authentication credentials
# embedded in the url.
#
# See:
# https://gitlab.com/gitlab-org/gitlab-elasticsearch-indexer/blob/main/elastic/elastic.go#L22
config[:url] = config[:url].map { |u| ::Gitlab::Elastic::Helper.url_string(u) }
config.to_json
end end
def gitaly_config def gitaly_config
......
...@@ -45,6 +45,71 @@ RSpec.describe Gitlab::Elastic::Helper, :request_store do ...@@ -45,6 +45,71 @@ RSpec.describe Gitlab::Elastic::Helper, :request_store do
end end
end end
describe '.connection_settings' do
it 'returns a hash compatible with elasticsearcht-transport client settings' do
settings = described_class.connection_settings(uri: "http://localhost:9200")
expect(settings).to eq({
scheme: "http",
host: "localhost",
path: "",
port: 9200
})
end
it 'works when given a URI' do
settings = described_class.connection_settings(uri: Addressable::URI.parse("http://localhost:9200"))
expect(settings).to eq({
scheme: "http",
host: "localhost",
path: "",
port: 9200
})
end
it 'parses credentials out of the uri' do
settings = described_class.connection_settings(uri: "http://elastic:myp%40ssword@localhost:9200")
expect(settings).to eq({
scheme: "http",
host: "localhost",
user: "elastic",
password: "myp@ssword",
path: "",
port: 9200
})
end
it 'prioritizes creds in arguments over those in url' do
settings = described_class.connection_settings(uri: "http://elastic:password@localhost:9200", user: "myuser", password: "p@ssword")
expect(settings).to eq({
scheme: "http",
host: "localhost",
user: "myuser",
password: "p@ssword",
path: "",
port: 9200
})
end
end
describe '.`url_string`' do
it 'returns a percent encoded url string' do
settings = {
scheme: "http",
host: "localhost",
user: "myuser",
password: "p@ssword",
path: "/foo",
port: 9200
}
expect(described_class.url_string(settings)).to eq("http://myuser:p%40ssword@localhost:9200/foo")
end
end
describe '#default_mappings' do describe '#default_mappings' do
it 'has only one type' do it 'has only one type' do
expect(helper.default_mappings.keys).to match_array %i(doc) expect(helper.default_mappings.keys).to match_array %i(doc)
......
...@@ -456,7 +456,9 @@ RSpec.describe Gitlab::Elastic::Indexer do ...@@ -456,7 +456,9 @@ RSpec.describe Gitlab::Elastic::Indexer do
def elasticsearch_config def elasticsearch_config
Gitlab::CurrentSettings.elasticsearch_config.merge( Gitlab::CurrentSettings.elasticsearch_config.merge(
index_name: 'gitlab-test' index_name: 'gitlab-test'
) ).tap do |config|
config[:url] = config[:url].map { |u| ::Gitlab::Elastic::Helper.url_string(u) }
end
end end
def envvars def envvars
......
...@@ -334,54 +334,78 @@ RSpec.describe ApplicationSetting do ...@@ -334,54 +334,78 @@ RSpec.describe ApplicationSetting do
end end
describe '#elasticsearch_url_with_credentials' do describe '#elasticsearch_url_with_credentials' do
it 'embeds credentials in the result' do let(:elasticsearch_url) { "#{host1},#{host2}" }
setting.elasticsearch_url = 'http://example.com,https://example.org:9200' let(:host1) { 'http://example.com' }
setting.elasticsearch_username = 'foo' let(:host2) { 'https://example.org:9200' }
setting.elasticsearch_password = 'bar' let(:elasticsearch_username) { 'elastic' }
let(:elasticsearch_password) { 'password' }
expect(setting.elasticsearch_url_with_credentials).to match_array([URI.parse('http://foo:bar@example.com'), URI.parse('https://foo:bar@example.org:9200')]) before do
setting.elasticsearch_url = elasticsearch_url
setting.elasticsearch_username = elasticsearch_username
setting.elasticsearch_password = elasticsearch_password
end end
it 'embeds username only' do context 'when credentials are embedded in url' do
setting.elasticsearch_url = 'http://example.com,https://example.org:9200' let(:elasticsearch_url) { 'http://username:password@example.com,https://test:test@example.org:9200' }
setting.elasticsearch_username = 'foo'
setting.elasticsearch_password = ''
expect(setting.elasticsearch_url_with_credentials).to match_array([URI.parse('http://foo:@example.com'), URI.parse('https://foo:@example.org:9200')]) it 'ignores them and uses elasticsearch_username and elasticsearch_password settings' do
expect(setting.elasticsearch_url_with_credentials).to match_array([
{ scheme: 'http', user: elasticsearch_username, password: elasticsearch_password, host: 'example.com', path: '', port: 80 },
{ scheme: 'https', user: elasticsearch_username, password: elasticsearch_password, host: 'example.org', path: '', port: 9200 }
])
end
end end
it 'overrides existing embedded credentials' do context 'when credential settings are blank' do
setting.elasticsearch_url = 'http://username:password@example.com,https://test:test@example.org:9200' let(:elasticsearch_username) { nil }
setting.elasticsearch_username = 'foo' let(:elasticsearch_password) { nil }
setting.elasticsearch_password = 'bar'
expect(setting.elasticsearch_url_with_credentials).to match_array([URI.parse('http://foo:bar@example.com'), URI.parse('https://foo:bar@example.org:9200')]) it 'does not return credential info' do
end expect(setting.elasticsearch_url_with_credentials).to match_array([
{ scheme: 'http', host: 'example.com', path: '', port: 80 },
{ scheme: 'https', host: 'example.org', path: '', port: 9200 }
])
end
context 'and url contains credentials' do
let(:elasticsearch_url) { 'http://username:password@example.com,https://test:test@example.org:9200' }
it 'returns credentials from url' do
expect(setting.elasticsearch_url_with_credentials).to match_array([
{ scheme: 'http', user: 'username', password: 'password', host: 'example.com', path: '', port: 80 },
{ scheme: 'https', user: 'test', password: 'test', host: 'example.org', path: '', port: 9200 }
])
end
end
it 'returns original url if credentials blank' do context 'and url contains credentials with special characters' do
setting.elasticsearch_url = 'http://username:password@example.com,https://test:test@example.org:9200' let(:elasticsearch_url) { 'http://admin:p%40ssword@localhost:9200/' }
setting.elasticsearch_username = ''
setting.elasticsearch_password = ''
expect(setting.elasticsearch_url_with_credentials).to match_array([URI.parse('http://username:password@example.com'), URI.parse('https://test:test@example.org:9200')]) it 'returns decoded credentials from url' do
expect(setting.elasticsearch_url_with_credentials).to match_array([
{ scheme: 'http', user: 'admin', password: 'p@ssword', host: 'localhost', path: '', port: 9200 }
])
end
end
end end
it 'encodes the credentials' do context 'when credentials settings have special characters' do
setting.elasticsearch_url = 'http://username:password@example.com,https://test:test@example.org:9200' let(:elasticsearch_username) { 'foo/admin' }
setting.elasticsearch_username = 'foo/admin' let(:elasticsearch_password) { 'b@r+baz!$' }
setting.elasticsearch_password = 'b@r'
expect(setting.elasticsearch_url_with_credentials).to match_array([ it 'returns the correct values' do
URI.parse('http://foo%2Fadmin:b%40r@example.com'), expect(setting.elasticsearch_url_with_credentials).to match_array([
URI.parse('https://foo%2Fadmin:b%40r@example.org:9200') { scheme: 'http', user: elasticsearch_username, password: elasticsearch_password, host: 'example.com', path: '', port: 80 },
]) { scheme: 'https', user: elasticsearch_username, password: elasticsearch_password, host: 'example.org', path: '', port: 9200 }
])
end
end end
end end
describe '#elasticsearch_password' do describe '#elasticsearch_password' do
it 'does not modify password if it is unchanged in the form' do it 'does not modify password if it is unchanged in the form' do
setting.elasticsearch_password = 'foo' setting.elasticsearch_password = 'foo'
setting.elasticsearch_password = ApplicationSetting::MASK_PASSWORD setting.elasticsearch_password = ApplicationSetting::MASK_PASSWORD
expect(setting.elasticsearch_password).to eq('foo') expect(setting.elasticsearch_password).to eq('foo')
...@@ -404,7 +428,7 @@ RSpec.describe ApplicationSetting do ...@@ -404,7 +428,7 @@ RSpec.describe ApplicationSetting do
) )
expect(setting.elasticsearch_config).to eq( expect(setting.elasticsearch_config).to eq(
url: [URI.parse('http://foo:bar@example.com:9200')], url: [Gitlab::Elastic::Helper.connection_settings(uri: URI.parse('http://foo:bar@example.com:9200'))],
aws: false, aws: false,
aws_region: 'test-region', aws_region: 'test-region',
aws_access_key: 'test-access-key', aws_access_key: 'test-access-key',
......
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