rsolr 2.5.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/rsolr/error.rb CHANGED
@@ -2,7 +2,18 @@ require 'json'
2
2
 
3
3
  module RSolr::Error
4
4
 
5
+ module URICleanup
6
+ # Removes username and password from URI object.
7
+ def clean_uri(uri)
8
+ uri = uri.dup
9
+ uri.password = "REDACTED" if uri.password
10
+ uri.user = "REDACTED" if uri.user
11
+ uri
12
+ end
13
+ end
14
+
5
15
  module SolrContext
16
+ include URICleanup
6
17
 
7
18
  attr_accessor :request, :response
8
19
 
@@ -13,7 +24,7 @@ module RSolr::Error
13
24
  details = parse_solr_error_response response[:body]
14
25
  m << "\nError: #{details}\n" if details
15
26
  end
16
- p = "\nURI: #{request[:uri].to_s}"
27
+ p = "\nURI: #{clean_uri(request[:uri]).to_s}"
17
28
  p << "\nRequest Headers: #{request[:headers].inspect}" if request[:headers]
18
29
  p << "\nRequest Data: #{request[:data].inspect}" if request[:data]
19
30
  p << "\n"
@@ -52,11 +63,16 @@ module RSolr::Error
52
63
  nil
53
64
  end
54
65
  end
55
-
56
-
57
66
  end
58
67
 
59
68
  class ConnectionRefused < ::Errno::ECONNREFUSED
69
+ include URICleanup
70
+
71
+ def initialize(request)
72
+ request[:uri] = clean_uri(request[:uri])
73
+
74
+ super(request.inspect)
75
+ end
60
76
  end
61
77
 
62
78
  class Http < RuntimeError
data/lib/rsolr/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  module RSolr
2
- VERSION = "2.5.0"
2
+ VERSION = "3.0.0"
3
3
 
4
4
  def self.version
5
5
  VERSION
data/rsolr.gemspec CHANGED
@@ -33,7 +33,7 @@ Gem::Specification.new do |s|
33
33
  s.requirements << 'Apache Solr'
34
34
 
35
35
  s.add_dependency 'builder', '>= 2.1.2'
36
- s.add_dependency 'faraday', '>= 0.9', '!= 2.0.0', '< 3'
36
+ s.add_dependency 'faraday', '>= 1.0', '< 3'
37
37
 
38
38
  s.add_development_dependency 'activesupport'
39
39
  s.add_development_dependency 'nokogiri', '>= 1.4.0'
@@ -41,5 +41,4 @@ Gem::Specification.new do |s|
41
41
  s.add_development_dependency 'rdoc', '>= 4.0'
42
42
  s.add_development_dependency 'rspec', '~> 3.0'
43
43
  s.add_development_dependency 'simplecov'
44
- s.add_development_dependency 'solr_wrapper'
45
44
  end
@@ -1,4 +1,5 @@
1
1
  require 'spec_helper'
2
+ require 'base64'
2
3
 
3
4
  RSpec.describe RSolr::Client do
4
5
  let(:connection) { nil }
@@ -13,6 +14,106 @@ RSpec.describe RSolr::Client do
13
14
  RSolr::Client.new connection, connection_options.merge(proxy: 'http://localhost:8080')
14
15
  end
15
16
 
17
+ context "basic auth" do
18
+ let(:stubs) { Faraday::Adapter::Test::Stubs.new }
19
+
20
+ def authorization_header_for(url:, **client_opts)
21
+ seen_header = nil
22
+ stubs.get('/solr/admin/ping') do |env|
23
+ seen_header = env.request_headers['Authorization']
24
+ [200, {}, '']
25
+ end
26
+
27
+ RSolr::Client.new(nil, { url: url, adapter: [:test, stubs] }.merge(client_opts)).get('admin/ping')
28
+ stubs.verify_stubbed_calls
29
+ seen_header
30
+ end
31
+
32
+ it "sends credentials embedded in the URL as a Basic Authorization header" do
33
+ header = authorization_header_for(url: "http://someuser:somepass@localhost:9999/solr")
34
+ expect(header).to eq("Basic #{Base64.strict_encode64('someuser:somepass')}")
35
+ end
36
+
37
+ it "sends no Authorization header when the URL has no credentials" do
38
+ header = authorization_header_for(url: "http://localhost:9999/solr")
39
+ expect(header).to be_nil
40
+ end
41
+
42
+ it "sends credentials given via the basic_auth: option" do
43
+ header = authorization_header_for(
44
+ url: "http://localhost:9999/solr",
45
+ basic_auth: { user: 'optuser', password: 'optpass' }
46
+ )
47
+ expect(header).to eq("Basic #{Base64.strict_encode64('optuser:optpass')}")
48
+ end
49
+
50
+ it "prefers the basic_auth: option over credentials embedded in the URL" do
51
+ header = authorization_header_for(
52
+ url: "http://urluser:urlpass@localhost:9999/solr",
53
+ basic_auth: { user: 'optuser', password: 'optpass' }
54
+ )
55
+ expect(header).to eq("Basic #{Base64.strict_encode64('optuser:optpass')}")
56
+ end
57
+
58
+ it "accepts a basic_auth: option with string keys, as loaded from YAML" do
59
+ header = authorization_header_for(
60
+ url: "http://localhost:9999/solr",
61
+ basic_auth: { 'user' => 'optuser', 'password' => 'optpass' }
62
+ )
63
+ expect(header).to eq("Basic #{Base64.strict_encode64('optuser:optpass')}")
64
+ end
65
+
66
+ context 'when a connection error is raised' do
67
+ def error_raised_for(url:, **client_opts)
68
+ stubs.get('/solr/admin/ping') { |_env| [500, {}, 'boom'] }
69
+ RSolr::Client.new(nil, { url: url, adapter: [:test, stubs] }.merge(client_opts)).get('admin/ping')
70
+ nil
71
+ rescue RSolr::Error::Http => e
72
+ e
73
+ end
74
+
75
+ context 'with credentials in url' do
76
+ it 'already redacts the url in RSolr::Error::Http#message' do
77
+ error = error_raised_for(url: "http://someuser:somepass@localhost:9999/solr")
78
+
79
+ expect(error.message).not_to include('somepass')
80
+ expect(error.message).to include('REDACTED')
81
+ end
82
+
83
+ it 'does not leak basic auth info' do
84
+ error = error_raised_for(url: "http://someuser:somepass@localhost:9999/solr")
85
+
86
+ expect(error.cause.message).not_to include('somepass')
87
+ expect(error.cause.inspect).not_to include('somepass')
88
+ expect(error.cause.inspect).to include('REDACTED')
89
+ end
90
+ end
91
+
92
+ context 'with credentials in header' do
93
+ it 'already omits credentials from RSolr::Error::Http#message' do
94
+ error = error_raised_for(
95
+ url: "http://localhost:9999/solr",
96
+ basic_auth: { user: 'optuser', password: 'optpass' }
97
+ )
98
+ encoded_credentials = Base64.strict_encode64('optuser:optpass')
99
+
100
+ expect(error.message).not_to include(encoded_credentials)
101
+ end
102
+
103
+ it 'does not leak basic auth info' do
104
+ error = error_raised_for(
105
+ url: "http://localhost:9999/solr",
106
+ basic_auth: { user: 'optuser', password: 'optpass' }
107
+ )
108
+ encoded_credentials = Base64.strict_encode64('optuser:optpass')
109
+
110
+ expect(error.cause.inspect).not_to include(encoded_credentials)
111
+ expect(error.cause.inspect).to include('REDACTED')
112
+ end
113
+ end
114
+ end
115
+ end
116
+
16
117
  context "initialize" do
17
118
  it "should accept whatevs and set it as the @connection" do
18
119
  expect(RSolr::Client.new(:whatevs).connection).to eq(:whatevs)
@@ -77,6 +178,24 @@ RSpec.describe RSolr::Client do
77
178
 
78
179
  expect{ client.execute({}) }.to raise_error RSolr::Error::Timeout
79
180
  end
181
+
182
+ context 'when an Errno::ECONNREFUSED error is raised' do
183
+ let(:uri) { URI.parse('http://admin:secret@hostname.local:8983/solr/admin/update?wt=json&q=test') }
184
+
185
+ before do
186
+ allow(client.connection).to receive(:send).and_raise(Errno::ECONNREFUSED)
187
+ end
188
+
189
+ it "maps error to RSolr::Error:ConnectionRefused" do
190
+ expect { client.execute({ uri: uri }) }.to raise_error RSolr::Error::ConnectionRefused
191
+ end
192
+
193
+ it "removes credentials from uri" do
194
+ expect {
195
+ client.execute({ uri: uri })
196
+ }.to raise_error(RSolr::Error::ConnectionRefused, /http:\/\/(?:REDACTED:)?REDACTED@hostname\.local:8983/)
197
+ end
198
+ end
80
199
  end
81
200
 
82
201
  context "post" do
@@ -393,6 +512,18 @@ RSpec.describe RSolr::Client do
393
512
  end
394
513
  end
395
514
 
515
+ context "when post but no hash is passed in as data" do
516
+ let(:data) { nil }
517
+
518
+ it "sets the Content-Type header to application/x-www-form-urlencoded; charset=UTF-8" do
519
+ expect(subject[:query]).to be_nil
520
+ [/fq=0/, /fq=1/, /q=test/, /wt=json/].each do |pattern|
521
+ expect(subject[:data]).to match pattern
522
+ end
523
+ expect(subject[:headers]).to eq({"Content-Type" => "application/x-www-form-urlencoded; charset=UTF-8"})
524
+ end
525
+ end
526
+
396
527
  it "should properly handle proxy configuration" do
397
528
  result = client_with_proxy.build_request('select',
398
529
  :method => :post,
@@ -401,5 +532,24 @@ RSpec.describe RSolr::Client do
401
532
  )
402
533
  expect(result[:uri].to_s).to match %r{^http://localhost:9999/solr/}
403
534
  end
535
+
536
+ context "http method" do
537
+ it "uses get by default" do
538
+ expect(client.build_request('select', { params: params })[:method]).to eq :get
539
+ end
540
+ it "uses the instance default if set" do
541
+ client = RSolr::Client.new connection, connection_options.merge(default_http_method: :post)
542
+ expect(client.build_request('select', { data: params })[:method]).to eq :post
543
+ end
544
+ it "uses the class default if set" do
545
+ RSolr::Client.default_http_method = :post
546
+ expect(client.build_request('select', { data: params })[:method]).to eq :post
547
+ RSolr::Client.default_http_method = nil
548
+ end
549
+ it "uses the method passed in opts" do
550
+ expect(client.build_request('select', { params: params, method: :get })[:method]).to eq :get
551
+ expect(client.build_request('select', { data: params, method: :post })[:method]).to eq :post
552
+ end
553
+ end
404
554
  end
405
555
  end
@@ -7,7 +7,7 @@ RSpec.describe RSolr::Error do
7
7
  exception
8
8
  end
9
9
  let (:response_lines) { (1..15).to_a.map { |i| "line #{i}" } }
10
- let(:request) { double :[] => "mocked" }
10
+ let(:request) { { uri: URI.parse('http://hostname.local:8983/solr/admin/update?wt=json&q=test') } }
11
11
  let(:response_body) { response_lines.join("\n") }
12
12
  let(:response) {{
13
13
  :body => response_body,
@@ -89,7 +89,7 @@ RSpec.describe RSolr::Error do
89
89
  "code":500
90
90
  }
91
91
  }
92
- EOS
92
+ EOS
93
93
  }
94
94
  it {
95
95
  should include msg
@@ -116,7 +116,7 @@ RSpec.describe RSolr::Error do
116
116
  "facet_fields":{}
117
117
  },
118
118
  }
119
- EOS
119
+ EOS
120
120
  }
121
121
  it "shows the first eleven lines of the response" do
122
122
  expect(subject).to include(response_body.split("\n")[0..10].join("\n"))
@@ -146,4 +146,15 @@ RSpec.describe RSolr::Error do
146
146
  expect(subject).not_to include(response_body.split("\n")[11])
147
147
  end
148
148
  end
149
+
150
+ context "when request uri contains credentials" do
151
+ let(:request) { { uri: URI.parse('http://user:pass@hostname.local:8983/solr/admin/update?wt=json&q=test') } }
152
+
153
+
154
+ it 'includes redacted url' do
155
+ expect(subject).to include 'REDACTED@hostname.local:8983/solr/admin/update?wt=json&q=test'
156
+ expect(subject).not_to include 'user'
157
+ expect(subject).not_to include 'pass'
158
+ end
159
+ end
149
160
  end
@@ -68,32 +68,10 @@ RSpec.describe RSolr::JSON do
68
68
  },
69
69
  ]
70
70
 
71
- # custom JSON object class to handle Solr's non-standard JSON command format
72
- tmp = Class.new do
73
- def initialize
74
- @source ||= {}
75
- end
76
-
77
- def []=(k, v)
78
- if k == :add
79
- @source[k] ||= []
80
- @source[k] << v.to_h
81
- elsif v.class == self.class
82
- @source[k] = v.to_h
83
- else
84
- @source[k] = v
85
- end
86
- end
87
-
88
- def to_h
89
- @source
90
- end
91
- end
92
-
93
71
  request = generator.add(data, boost: 1)
94
- message = JSON.parse(request, object_class: tmp, symbolize_names: true).to_h
95
- expect(message[:add].length).to eq 2
96
- expect(message[:add].map { |x| x[:doc] }).to eq data
72
+
73
+ expect(request).to match(/"add":{"boost":1,"doc":{"id":"1","name":"matt"}}/)
74
+ expect(request).to match(/"add":{"boost":1,"doc":{"id":"2","name":"sam"}}/)
97
75
  end
98
76
  end
99
77
 
@@ -498,7 +498,7 @@
498
498
  <fieldType name="point" class="solr.PointType" dimension="2" subFieldSuffix="_d"/>
499
499
 
500
500
  <!-- A specialized field for geospatial search. If indexed, this fieldType must not be multivalued. -->
501
- <fieldType name="location" class="solr.LatLonType" subFieldSuffix="_coordinate"/>
501
+ <fieldType name="location" class="solr.LatLonPointSpatialField"/>
502
502
 
503
503
  <!-- An alternative geospatial field type new to Solr 4. It supports multiValued and polygon shapes.
504
504
  For more information about this and other Spatial fields new to Solr 4, see:
@@ -315,8 +315,8 @@
315
315
  background concurrently. An error is returned if this limit
316
316
  is exceeded.
317
317
 
318
- Recommend values of 1-2 for read-only slaves, higher for
319
- masters w/o cache warming.
318
+ Recommend values of 1-2 for read-only followers, higher for
319
+ leaders w/o cache warming.
320
320
  -->
321
321
  <maxWarmingSearchers>2</maxWarmingSearchers>
322
322
 
@@ -0,0 +1,16 @@
1
+ {
2
+ "authentication": {
3
+ "blockUnknown": true,
4
+ "class": "solr.BasicAuthPlugin",
5
+ "credentials": {
6
+ "rsolr": "yBxMT8Js4iwRuqWHqW7eQcF1iNDaSbI8ZQtQc5nZyYg= 57WDp52Vgp2LrB9BT5/iFnGY7txUMdTlyM9rVT9XwcA="
7
+ },
8
+ "realm": "rsolr test",
9
+ "forwardCredentials": false
10
+ },
11
+ "authorization": {
12
+ "class": "solr.RuleBasedAuthorizationPlugin",
13
+ "permissions": [{ "name": "security-edit", "role": "admin" }],
14
+ "user-role": { "rsolr": "admin" }
15
+ }
16
+ }
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe "Solr basic auth" do
4
+ let(:port) { ENV.fetch('SOLR_AUTH_TEST_PORT', 8984) }
5
+ let(:base_url) { "http://localhost:#{port}/solr/basic_configs/" }
6
+ let(:user) { 'rsolr' }
7
+ let(:password) { 'RSolrTest123' }
8
+
9
+ it "succeeds using the basic_auth: option" do
10
+ solr = RSolr.connect(url: base_url, basic_auth: { user: user, password: password })
11
+ expect { solr.head('admin/ping') }.not_to raise_error
12
+ end
13
+
14
+ it "succeeds using credentials embedded in the URL" do
15
+ url = "http://#{user}:#{password}@localhost:#{port}/solr/basic_configs/"
16
+ solr = RSolr.connect(url: url)
17
+ expect { solr.head('admin/ping') }.not_to raise_error
18
+ end
19
+
20
+ it "raises RSolr::Error::Http with no credentials" do
21
+ solr = RSolr.connect(url: base_url)
22
+ expect { solr.head('admin/ping') }.to raise_error(RSolr::Error::Http, /401/)
23
+ end
24
+
25
+ it "raises RSolr::Error::Http with the wrong credentials" do
26
+ solr = RSolr.connect(url: base_url, basic_auth: { user: user, password: 'wrong' })
27
+ expect { solr.head('admin/ping') }.to raise_error(RSolr::Error::Http, /401/)
28
+ end
29
+ end
@@ -1,18 +1,9 @@
1
1
  require 'spec_helper'
2
- require 'solr_wrapper'
3
2
 
4
3
  RSpec.describe "Solr basic_configs" do
5
- SOLR_INSTANCE = SolrWrapper.default_instance({})
6
- before(:all) { SOLR_INSTANCE.start }
7
- after(:all) { SOLR_INSTANCE.stop }
8
-
9
4
  context "basic configs" do
10
- subject { RSolr.connect url: "http://localhost:#{SOLR_INSTANCE.port}/solr/basic_configs/"}
11
- around(:each) do |example|
12
- SOLR_INSTANCE.with_collection(name: "basic_configs", dir: File.join(FIXTURES_DIR, "basic_configs")) do |coll|
13
- example.run
14
- end
15
- end
5
+ subject { RSolr.connect url: "http://localhost:#{ENV.fetch('SOLR_TEST_PORT', 8983)}/solr/basic_configs/" }
6
+
16
7
  describe "HEAD admin/ping" do
17
8
  it "should not raise an exception" do
18
9
  expect { subject.head('admin/ping') }.not_to raise_error
data/spec/spec_helper.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require 'simplecov'
2
2
  SimpleCov.start do
3
- add_filter "/spec/"
3
+ skip "/spec/"
4
4
  end
5
5
 
6
6
  require 'rsolr'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rsolr
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.5.0
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Antoine Latter
@@ -26,10 +26,9 @@ authors:
26
26
  - Nathan Witmer
27
27
  - Naomi Dushay
28
28
  - '"shima"'
29
- autorequire:
30
29
  bindir: bin
31
30
  cert_chain: []
32
- date: 2022-02-11 00:00:00.000000000 Z
31
+ date: 1980-01-02 00:00:00.000000000 Z
33
32
  dependencies:
34
33
  - !ruby/object:Gem::Dependency
35
34
  name: builder
@@ -51,10 +50,7 @@ dependencies:
51
50
  requirements:
52
51
  - - ">="
53
52
  - !ruby/object:Gem::Version
54
- version: '0.9'
55
- - - "!="
56
- - !ruby/object:Gem::Version
57
- version: 2.0.0
53
+ version: '1.0'
58
54
  - - "<"
59
55
  - !ruby/object:Gem::Version
60
56
  version: '3'
@@ -64,10 +60,7 @@ dependencies:
64
60
  requirements:
65
61
  - - ">="
66
62
  - !ruby/object:Gem::Version
67
- version: '0.9'
68
- - - "!="
69
- - !ruby/object:Gem::Version
70
- version: 2.0.0
63
+ version: '1.0'
71
64
  - - "<"
72
65
  - !ruby/object:Gem::Version
73
66
  version: '3'
@@ -155,20 +148,6 @@ dependencies:
155
148
  - - ">="
156
149
  - !ruby/object:Gem::Version
157
150
  version: '0'
158
- - !ruby/object:Gem::Dependency
159
- name: solr_wrapper
160
- requirement: !ruby/object:Gem::Requirement
161
- requirements:
162
- - - ">="
163
- - !ruby/object:Gem::Version
164
- version: '0'
165
- type: :development
166
- prerelease: false
167
- version_requirements: !ruby/object:Gem::Requirement
168
- requirements:
169
- - - ">="
170
- - !ruby/object:Gem::Version
171
- version: '0'
172
151
  description: RSolr aims to provide a simple and extensible library for working with
173
152
  Solr
174
153
  email:
@@ -183,8 +162,9 @@ files:
183
162
  - CHANGES.txt
184
163
  - Gemfile
185
164
  - LICENSE
186
- - README.rdoc
165
+ - README.md
187
166
  - Rakefile
167
+ - docker-compose.yaml
188
168
  - lib/rsolr.rb
189
169
  - lib/rsolr/char.rb
190
170
  - lib/rsolr/client.rb
@@ -214,6 +194,8 @@ files:
214
194
  - spec/fixtures/basic_configs/solrconfig.xml
215
195
  - spec/fixtures/basic_configs/stopwords.txt
216
196
  - spec/fixtures/basic_configs/synonyms.txt
197
+ - spec/fixtures/security/security.json
198
+ - spec/integration/basic_auth_spec.rb
217
199
  - spec/integration/solr5_spec.rb
218
200
  - spec/lib/rsolr/client_spec.rb
219
201
  - spec/spec_helper.rb
@@ -221,7 +203,6 @@ homepage: https://github.com/rsolr/rsolr
221
203
  licenses:
222
204
  - Apache-2.0
223
205
  metadata: {}
224
- post_install_message:
225
206
  rdoc_options: []
226
207
  require_paths:
227
208
  - lib
@@ -237,8 +218,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
237
218
  version: '0'
238
219
  requirements:
239
220
  - Apache Solr
240
- rubygems_version: 3.3.3
241
- signing_key:
221
+ rubygems_version: 4.0.18
242
222
  specification_version: 4
243
223
  summary: A Ruby client for Apache Solr
244
224
  test_files: []