Skip to content

Commit 2c0fc1c

Browse files
committed
[Client] Adds product validation
1 parent c5d4039 commit 2c0fc1c

File tree

3 files changed

+301
-1
lines changed

3 files changed

+301
-1
lines changed

elasticsearch/elasticsearch.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ Gem::Specification.new do |s|
5656
s.add_development_dependency 'rspec'
5757
s.add_development_dependency 'ruby-prof' unless defined?(JRUBY_VERSION) || defined?(Rubinius)
5858
s.add_development_dependency 'simplecov'
59+
s.add_development_dependency 'webmock'
5960
s.add_development_dependency 'yard'
6061

6162
s.description = <<-DESC.gsub(/^ /, '')

elasticsearch/lib/elasticsearch.rb

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
require 'elasticsearch/api'
2121

2222
module Elasticsearch
23+
NOT_ELASTICSEARCH_WARNING = 'The client noticed that the server is not Elasticsearch and we do not support this unknown product.'.freeze
24+
SECURITY_PRIVILEGES_VALIDATION_WARNING = 'The client is unable to verify that the server is Elasticsearch due to security privileges on the server side. Some functionality may not be compatible if the server is running an unsupported product.'.freeze
25+
2326
# This is the stateful Elasticsearch::Client, using an instance of elastic-transport.
2427
class Client
2528
include Elasticsearch::API
@@ -38,6 +41,7 @@ class Client
3841
# This will be prepended to the id you set before each request
3942
# if you're using X-Opaque-Id
4043
def initialize(arguments = {}, &block)
44+
@verified = false
4145
@opaque_id_prefix = arguments[:opaque_id_prefix] || nil
4246
api_key(arguments) if arguments[:api_key]
4347
if arguments[:cloud_id]
@@ -62,7 +66,10 @@ def method_missing(name, *args, &block)
6266
opaque_id = @opaque_id_prefix ? "#{@opaque_id_prefix}#{opaque_id}" : opaque_id
6367
args[4] = headers.merge('X-Opaque-Id' => opaque_id)
6468
end
65-
@transport.send(name, *args, &block)
69+
if name == :perform_request
70+
verify_elasticsearch unless @verified
71+
@transport.perform_request(*args, &block)
72+
end
6673
else
6774
@transport.send(name, *args, &block)
6875
end
@@ -74,6 +81,37 @@ def respond_to_missing?(method_name, *args)
7481

7582
private
7683

84+
def verify_elasticsearch
85+
begin
86+
response = elasticsearch_validation_request
87+
rescue Elastic::Transport::Transport::Errors::Unauthorized,
88+
Elastic::Transport::Transport::Errors::Forbidden
89+
@verified = true
90+
warn(SECURITY_PRIVILEGES_VALIDATION_WARNING)
91+
return
92+
end
93+
94+
body = if response.headers['content-type'] == 'application/yaml'
95+
require 'yaml'
96+
YAML.load(response.body)
97+
else
98+
response.body
99+
end
100+
version = body.dig('version', 'number')
101+
verify_with_version_or_header(version, response.headers)
102+
end
103+
104+
def verify_with_version_or_header(version, headers)
105+
if version.nil? ||
106+
Gem::Version.new(version) < Gem::Version.new('8.0.0.pre') && version != '8.0.0-SNAPSHOT' ||
107+
headers['x-elastic-product'] != 'Elasticsearch'
108+
109+
raise Elasticsearch::UnsupportedProductError
110+
end
111+
112+
@verified = true
113+
end
114+
77115
def setup_cloud_host(cloud_id, user, password, port)
78116
name = cloud_id.split(':')[0]
79117
cloud_url, elasticsearch_instance = Base64.decode64(cloud_id.gsub("#{name}:", '')).split('$')
@@ -112,6 +150,17 @@ def api_key(arguments)
112150
def encode(api_key)
113151
Base64.strict_encode64([api_key[:id], api_key[:api_key]].join(':'))
114152
end
153+
154+
def elasticsearch_validation_request
155+
@transport.perform_request('GET', '/')
156+
end
157+
end
158+
159+
# Error class for when we detect an unsupported version of Elasticsearch
160+
class UnsupportedProductError < StandardError
161+
def initialize(message = NOT_ELASTICSEARCH_WARNING)
162+
super(message)
163+
end
115164
end
116165
end
117166

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
# Licensed to Elasticsearch B.V. under one or more contributor
2+
# license agreements. See the NOTICE file distributed with
3+
# this work for additional information regarding copyright
4+
# ownership. Elasticsearch B.V. licenses this file to you under
5+
# the Apache License, Version 2.0 (the "License"); you may
6+
# not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
require 'spec_helper'
19+
require 'webmock/rspec'
20+
21+
describe 'Elasticsearch: Validation' do
22+
let(:host) { 'http://localhost:9200' }
23+
let(:verify_request_stub) do
24+
stub_request(:get, host)
25+
.to_return(status: status, body: body, headers: headers)
26+
end
27+
let(:count_request_stub) do
28+
stub_request(:post, "#{host}/_count")
29+
.to_return(status: 200, body: nil, headers: {})
30+
end
31+
let(:status) { 200 }
32+
let(:body) { {}.to_json }
33+
let(:client) { Elasticsearch::Client.new }
34+
let(:headers) do
35+
{ 'content-type' => 'application/json' }
36+
end
37+
38+
def error_requests_and_expectations(message = Elasticsearch::NOT_ELASTICSEARCH_WARNING)
39+
expect { client.count }.to raise_error Elasticsearch::UnsupportedProductError, message
40+
assert_requested :get, host
41+
assert_not_requested :post, "#{host}/_count"
42+
expect { client.cluster.health }.to raise_error Elasticsearch::UnsupportedProductError, message
43+
expect(client.instance_variable_get('@verified')).to be false
44+
expect { client.cluster.health }.to raise_error Elasticsearch::UnsupportedProductError, message
45+
end
46+
47+
def valid_requests_and_expectations
48+
expect(client.instance_variable_get('@verified')).to be false
49+
assert_not_requested :get, host
50+
51+
client.count
52+
expect(client.instance_variable_get('@verified'))
53+
assert_requested :get, host
54+
assert_requested :post, "#{host}/_count"
55+
end
56+
57+
context 'When Elasticsearch replies with status 401' do
58+
let(:status) { 401 }
59+
let(:body) { {}.to_json }
60+
61+
it 'Verifies the request but shows a warning' do
62+
stderr = $stderr
63+
fake_stderr = StringIO.new
64+
$stderr = fake_stderr
65+
66+
verify_request_stub
67+
count_request_stub
68+
69+
valid_requests_and_expectations
70+
71+
fake_stderr.rewind
72+
expect(fake_stderr.string).to eq("#{Elasticsearch::SECURITY_PRIVILEGES_VALIDATION_WARNING}\n")
73+
ensure
74+
$stderr = stderr
75+
end
76+
end
77+
78+
context 'When Elasticsearch replies with status 403' do
79+
let(:status) { 403 }
80+
let(:body) { {}.to_json }
81+
82+
it 'Verifies the request but shows a warning' do
83+
stderr = $stderr
84+
fake_stderr = StringIO.new
85+
$stderr = fake_stderr
86+
87+
verify_request_stub
88+
count_request_stub
89+
90+
valid_requests_and_expectations
91+
92+
fake_stderr.rewind
93+
expect(fake_stderr.string).to eq("#{Elasticsearch::SECURITY_PRIVILEGES_VALIDATION_WARNING}\n")
94+
ensure
95+
$stderr = stderr
96+
end
97+
end
98+
99+
context 'When the Elasticsearch version is >= 8.0.0' do
100+
context 'With a valid Elasticsearch response' do
101+
let(:body) { { 'version' => { 'number' => '8.0.0' } }.to_json }
102+
let(:headers) do
103+
{
104+
'X-Elastic-Product' => 'Elasticsearch',
105+
'content-type' => 'json'
106+
}
107+
end
108+
109+
it 'Makes requests and passes validation' do
110+
verify_request_stub
111+
count_request_stub
112+
113+
valid_requests_and_expectations
114+
end
115+
end
116+
117+
context 'When the header is not present' do
118+
it 'Fails validation' do
119+
verify_request_stub
120+
121+
expect(client.instance_variable_get('@verified')).to be false
122+
assert_not_requested :get, host
123+
124+
error_requests_and_expectations
125+
end
126+
end
127+
end
128+
129+
context 'When the Elasticsearch version is >= 8.1.0' do
130+
context 'With a valid Elasticsearch response' do
131+
let(:body) { { 'version' => { 'number' => '8.1.0' } }.to_json }
132+
let(:headers) do
133+
{
134+
'X-Elastic-Product' => 'Elasticsearch',
135+
'content-type' => 'json'
136+
}
137+
end
138+
139+
it 'Makes requests and passes validation' do
140+
verify_request_stub
141+
count_request_stub
142+
143+
valid_requests_and_expectations
144+
end
145+
end
146+
147+
context 'When the header is not present' do
148+
it 'Fails validation' do
149+
verify_request_stub
150+
151+
expect(client.instance_variable_get('@verified')).to be false
152+
assert_not_requested :get, host
153+
154+
error_requests_and_expectations
155+
end
156+
end
157+
end
158+
159+
160+
context 'When the Elasticsearch version is 8.0.0.pre' do
161+
context 'With a valid Elasticsearch response' do
162+
let(:body) { { 'version' => { 'number' => '8.0.0.pre' } }.to_json }
163+
let(:headers) do
164+
{
165+
'X-Elastic-Product' => 'Elasticsearch',
166+
'content-type' => 'json'
167+
}
168+
end
169+
170+
it 'Makes requests and passes validation' do
171+
verify_request_stub
172+
count_request_stub
173+
174+
valid_requests_and_expectations
175+
end
176+
end
177+
178+
context 'When the header is not present' do
179+
it 'Fails validation' do
180+
verify_request_stub
181+
182+
expect(client.instance_variable_get('@verified')).to be false
183+
assert_not_requested :get, host
184+
185+
error_requests_and_expectations
186+
end
187+
end
188+
end
189+
190+
context 'When the version is 8.0.0-SNAPSHOT' do
191+
let(:body) { { 'version' => { 'number' => '8.0.0-SNAPSHOT' } }.to_json }
192+
193+
context 'When the header is not present' do
194+
it 'Fails validation' do
195+
verify_request_stub
196+
count_request_stub
197+
198+
error_requests_and_expectations
199+
end
200+
end
201+
202+
context 'With a valid Elasticsearch response' do
203+
let(:headers) do
204+
{
205+
'X-Elastic-Product' => 'Elasticsearch',
206+
'content-type' => 'json'
207+
}
208+
end
209+
210+
it 'Makes requests and passes validation' do
211+
verify_request_stub
212+
count_request_stub
213+
214+
valid_requests_and_expectations
215+
end
216+
end
217+
end
218+
219+
context 'When Elasticsearch version is < 8.0.0' do
220+
let(:body) { { 'version' => { 'number' => '7.16.0' } }.to_json }
221+
222+
it 'Raises an exception and client doesnae work' do
223+
verify_request_stub
224+
error_requests_and_expectations
225+
end
226+
end
227+
228+
context 'When there is no version data' do
229+
let(:body) { {}.to_json }
230+
it 'Raises an exception and client doesnae work' do
231+
verify_request_stub
232+
error_requests_and_expectations
233+
end
234+
end
235+
236+
context 'When doing a yaml content-type request' do
237+
let(:client) do
238+
Elasticsearch::Client.new(transport_options: {headers: { accept: 'application/yaml', content_type: 'application/yaml' }})
239+
end
240+
241+
let(:headers) { { 'content-type' => 'application/yaml', 'X-Elastic-Product' => 'Elasticsearch' } }
242+
let(:body) { "---\nversion:\n number: \"8.0.0-SNAPSHOT\"\n" }
243+
244+
it 'validates' do
245+
verify_request_stub
246+
count_request_stub
247+
valid_requests_and_expectations
248+
end
249+
end
250+
end

0 commit comments

Comments
 (0)