opendistro 1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +11 -0
- data/LICENSE.txt +24 -0
- data/README.md +160 -0
- data/lib/opendistro.rb +47 -0
- data/lib/opendistro/api.rb +19 -0
- data/lib/opendistro/client.rb +32 -0
- data/lib/opendistro/client/users.rb +71 -0
- data/lib/opendistro/configuration.rb +56 -0
- data/lib/opendistro/error.rb +154 -0
- data/lib/opendistro/file_response.rb +48 -0
- data/lib/opendistro/objectified_hash.rb +51 -0
- data/lib/opendistro/request.rb +95 -0
- data/lib/opendistro/version.rb +5 -0
- metadata +138 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 464adecef8a65b39b82d4c0e566b81fe6106120109772a7a6ec64be44931472e
|
4
|
+
data.tar.gz: bf5f38e38ff4f02320ec641b55dff8c5e6d82fd0d7a0963dc9581f71795cfce9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 915168ca8d9f9eca5719bc96063e1172b536d0277ff759c9b50b5b85d9887da449ea3e2466929a179cc88b535fc2c1712fcb661affcbe14376a567346f1ef70e
|
7
|
+
data.tar.gz: 9a51b7daff70aa7028d3ac79a60b20dc568b45350c9b5d34e09a7d15c82f9d00d7e061e488c0931581a17200072b2ba9696ce9b8145cc94a68de1a3b33440811
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
## CHANGELOG
|
2
|
+
|
3
|
+
### Newer releases
|
4
|
+
|
5
|
+
Please see: https://github.com/psyreactor/opendistro/releases
|
6
|
+
|
7
|
+
### 1.0.0 (20/08/2020)
|
8
|
+
- Add user endpoints (users, user, create_user, edit_user, delete_user)
|
9
|
+
- Add Documentation site
|
10
|
+
- Add Github actions
|
11
|
+
- Add Coverall support
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
Copyright (c) 2020 Lucas Mariani <marianilucas@gmail.com>
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
6
|
+
|
7
|
+
1. Redistributions of source code must retain the above copyright notice,
|
8
|
+
this list of conditions and the following disclaimer.
|
9
|
+
|
10
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
11
|
+
this list of conditions and the following disclaimer in the documentation
|
12
|
+
and/or other materials provided with the distribution.
|
13
|
+
|
14
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
15
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
16
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
17
|
+
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
18
|
+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
19
|
+
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
20
|
+
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
21
|
+
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
22
|
+
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
23
|
+
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
24
|
+
POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
# Opendistro
|
2
|
+
|
3
|
+
[](https://github.com/psyreactor/opendistro/actions?query=workflow%3ARuby)
|
4
|
+
[](https://inch-ci.org/github/psyreactor/opendistro)
|
5
|
+
[](https://coveralls.io/github/psyreactor/opendistro)
|
6
|
+
[](https://rubygems.org/gems/opendistro)
|
7
|
+
[](https://github.com/psyreactor/opendistro/blob/master/LICENSE.txt)
|
8
|
+
|
9
|
+
[website](https://psyreactor.github.io/opendistro/) |
|
10
|
+
[documentation](https://www.rubydoc.info/gems/opendistro/frames)
|
11
|
+
|
12
|
+
A Ruby wrapper around the Opendistro API.
|
13
|
+
|
14
|
+
This library will strive to achieve reliable coverage of the Opendistro API. Please submit an issue if you find a bug and feel free to submit a pull request to contribute fixes or new features.
|
15
|
+
|
16
|
+
Initial versions of this gem to not achieve 100% coverage of the Opendistro API. Each release will include more and more endpoint support until there is 100% coverage.
|
17
|
+
|
18
|
+
The layout and the code in this library is inspired by https://github.com/NARKOZ/gitlab. NARKOZ does a fantastic job of keeping code simple and achieving API feature parity.
|
19
|
+
|
20
|
+
## Installation
|
21
|
+
|
22
|
+
Install it from rubygems:
|
23
|
+
|
24
|
+
```sh
|
25
|
+
gem install opendistro
|
26
|
+
```
|
27
|
+
|
28
|
+
Or add to a Gemfile:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
gem 'opendistro'
|
32
|
+
# gem 'opendistro', github: 'psyreactor/opendistro'
|
33
|
+
```
|
34
|
+
|
35
|
+
Mac OS users can install using Homebrew (may not be the latest version):
|
36
|
+
|
37
|
+
```sh
|
38
|
+
brew install opendistro-gem
|
39
|
+
```
|
40
|
+
|
41
|
+
## Usage
|
42
|
+
|
43
|
+
Configuration example:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
Opendistro.configure do |config|
|
47
|
+
config.endpoint = 'https://example.net:9200'
|
48
|
+
config.username = 'useradmin'
|
49
|
+
config.password = 'secretpassword'
|
50
|
+
# Optional
|
51
|
+
# config.user_agent = 'Custom User Agent'
|
52
|
+
# config.verify_ssl = false
|
53
|
+
# config.ca_cert = '/etc/pki/ca_cert/ca.crt'
|
54
|
+
end
|
55
|
+
```
|
56
|
+
|
57
|
+
Usage examples:
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
# set an API endpoint
|
61
|
+
Opendistro.endpoint = 'https://example.net:9200'
|
62
|
+
# => "https://example.net:9200"
|
63
|
+
|
64
|
+
# set a user user
|
65
|
+
Opendistro.username = 'useradmin'
|
66
|
+
# => "useradmin"
|
67
|
+
|
68
|
+
# set a user password
|
69
|
+
Opendistro.password = 'secretpassword'
|
70
|
+
# => "secretpassword"
|
71
|
+
|
72
|
+
# configure a proxy server
|
73
|
+
Opendistro.http_proxy('proxyhost', 8888)
|
74
|
+
# proxy server with basic auth
|
75
|
+
Opendistro.http_proxy('proxyhost', 8888, 'proxyuser', 'strongpasswordhere')
|
76
|
+
# set timeout for responses
|
77
|
+
ENV['OPENDISTRO_API_HTTPARTY_OPTIONS'] = '{read_timeout: 60}'
|
78
|
+
|
79
|
+
# list users
|
80
|
+
Opendistro.users()
|
81
|
+
# #<Opendistro::ObjectifiedHash:46080 {hash: {"logstash"=>{"hash"=>"", "reserved"=>false, "hidden"=>false, "backend_roles"=>["logstash"], "attributes"=>{}, "description"=>"Demo logstash user", "opendistro_security_roles"=>[], "static"=>false}, "snapshotrestore"=>{"hash"=>"", "reserved"=>false, "hidden"=>false, "backend_roles"=>["snapshotrestore"], "attributes"=>{}, "description"=>"Demo snapshotrestore user", "opendistro_security_roles"=>[], "static"=>false}, "admin"=>{"hash"=>"", "reserved"=>true, "hidden"=>false, "backend_roles"=>["admin"], "attributes"=>{}, "description"=>"Demo admin user", "opendistro_security_roles"=>[], "static"=>false}, "kibanaserver"=>{"hash"=>"", "reserved"=>true, "hidden"=>false, "backend_roles"=>[], "attributes"=>{}, "description"=>"Demo kibanaserver user", "opendistro_security_roles"=>[], "static"=>false}, "kibanaro"=>{"hash"=>"", "reserved"=>false, "hidden"=>false, "backend_roles"=>["kibanauser", "readall"], "attributes"=>{"attribute1"=>"value1", "attribute2"=>"value2", "attribute3"=>"value3"}, "description"=>"Demo kibanaro user", "opendistro_security_roles"=>[], "static"=>false}, "readall"=>{"hash"=>"", "reserved"=>false, "hidden"=>false, "backend_roles"=>["readall"], "attributes"=>{}, "description"=>"Demo readall user", "opendistro_security_roles"=>[], "static"=>false}}}
|
82
|
+
|
83
|
+
# initialize a new client with custom headers
|
84
|
+
od = Opendistro.client(
|
85
|
+
endpoint: 'https://example.com:9200',
|
86
|
+
username: 'useradmin',
|
87
|
+
password: 'secretpassword',
|
88
|
+
httparty: {
|
89
|
+
headers: { 'Cookie' => 'opendistro_canary=true' }
|
90
|
+
}
|
91
|
+
)
|
92
|
+
|
93
|
+
# get a users
|
94
|
+
user = od.users
|
95
|
+
#<Opendistro::ObjectifiedHash:46080 {hash: {"logstash"=>{"hash"=>"", "reserved"=>false, "hidden"=>false, "backend_roles"=>["logstash"], "attributes"=>{}, "description"=>"Demo logstash user", "opendistro_security_roles"=>[], "static"=>false}, "snapshotrestore"=>{"hash"=>"", "reserved"=>false, "hidden"=>false, "backend_roles"=>["snapshotrestore"], "attributes"=>{}, "description"=>"Demo snapshotrestore user", "opendistro_security_roles"=>[], "static"=>false}, "admin"=>{"hash"=>"", "reserved"=>true, "hidden"=>false, "backend_roles"=>["admin"], "attributes"=>{}, "description"=>"Demo admin user", "opendistro_security_roles"=>[], "static"=>false}, "kibanaserver"=>{"hash"=>"", "reserved"=>true, "hidden"=>false, "backend_roles"=>[], "attributes"=>{}, "description"=>"Demo kibanaserver user", "opendistro_security_roles"=>[], "static"=>false}, "kibanaro"=>{"hash"=>"", "reserved"=>false, "hidden"=>false, "backend_roles"=>["kibanauser", "readall"], "attributes"=>{"attribute1"=>"value1", "attribute2"=>"value2", "attribute3"=>"value3"}, "description"=>"Demo kibanaro user", "opendistro_security_roles"=>[], "static"=>false}, "readall"=>{"hash"=>"", "reserved"=>false, "hidden"=>false, "backend_roles"=>["readall"], "attributes"=>{}, "description"=>"Demo readall user", "opendistro_security_roles"=>[], "static"=>false}}}
|
96
|
+
|
97
|
+
# get a user's
|
98
|
+
user = od.user('logstash')
|
99
|
+
|
100
|
+
# get user description
|
101
|
+
user.description
|
102
|
+
# => 'logstash demo user'
|
103
|
+
|
104
|
+
```
|
105
|
+
|
106
|
+
|
107
|
+
## Development
|
108
|
+
|
109
|
+
### With a dockerized Opendistro instance
|
110
|
+
|
111
|
+
```shell
|
112
|
+
docker-compose up -d opendistro # Will start the Opendistro instance in the background (approx. 3 minutes)
|
113
|
+
```
|
114
|
+
|
115
|
+
After a while, your Opendistro instance will be accessible on http://localhost:9200.
|
116
|
+
|
117
|
+
Once you have set your new password admin, you can login with the admin user.
|
118
|
+
|
119
|
+
Once you have your token, set the variables to the correct values in the `docker.env` file.
|
120
|
+
|
121
|
+
Then, launch the tool:
|
122
|
+
|
123
|
+
```shell
|
124
|
+
docker-compose run app
|
125
|
+
```
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
Opendistro.users
|
129
|
+
=> #<Opendistro::ObjectifiedHash:46080 {hash: {"logstash"=>{"hash"=>"", "reserved"=>false, "hidden"=>false, "backend_roles"=>["logstash"], "attributes"=>{}, "description"=>"Demo logstash user", "opendistro_security_roles"=>[], "static"=>false}, "snapshotrestore"=>{"hash"=>"", "reserved"=>false, "hidden"=>false, "backend_roles"=>["snapshotrestore"], "attributes"=>{}, "description"=>"Demo snapshotrestore user", "opendistro_security_roles"=>[], "static"=>false}, "admin"=>{"hash"=>"", "reserved"=>true, "hidden"=>false, "backend_roles"=>["admin"], "attributes"=>{}, "description"=>"Demo admin user", "opendistro_security_roles"=>[], "static"=>false}, "kibanaserver"=>{"hash"=>"", "reserved"=>true, "hidden"=>false, "backend_roles"=>[], "attributes"=>{}, "description"=>"Demo kibanaserver user", "opendistro_security_roles"=>[], "static"=>false}, "kibanaro"=>{"hash"=>"", "reserved"=>false, "hidden"=>false, "backend_roles"=>["kibanauser", "readall"], "attributes"=>{"attribute1"=>"value1", "attribute2"=>"value2", "attribute3"=>"value3"}, "description"=>"Demo kibanaro user", "opendistro_security_roles"=>[], "static"=>false}, "readall"=>{"hash"=>"", "reserved"=>false, "hidden"=>false, "backend_roles"=>["readall"], "attributes"=>{}, "description"=>"Demo readall user", "opendistro_security_roles"=>[], "static"=>false}}}
|
130
|
+
|
131
|
+
```
|
132
|
+
To launch the specs:
|
133
|
+
|
134
|
+
```shell
|
135
|
+
docker-compose run app rake spec
|
136
|
+
```
|
137
|
+
|
138
|
+
### With an external Opendistro instance
|
139
|
+
|
140
|
+
First, set the variables to the correct values in the `docker.env` file.
|
141
|
+
|
142
|
+
Then, launch the tool:
|
143
|
+
|
144
|
+
```shell
|
145
|
+
docker-compose run app
|
146
|
+
```
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
Opendistro.users
|
150
|
+
```
|
151
|
+
|
152
|
+
To launch the specs,
|
153
|
+
|
154
|
+
```shell
|
155
|
+
docker-compose run app rake spec
|
156
|
+
```
|
157
|
+
|
158
|
+
## License
|
159
|
+
|
160
|
+
Released under the BSD 2-clause license. See LICENSE.txt for details.
|
data/lib/opendistro.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'opendistro/version'
|
4
|
+
require 'opendistro/objectified_hash'
|
5
|
+
require 'opendistro/configuration'
|
6
|
+
require 'opendistro/error'
|
7
|
+
require 'opendistro/file_response'
|
8
|
+
require 'opendistro/request'
|
9
|
+
require 'opendistro/api'
|
10
|
+
require 'opendistro/client'
|
11
|
+
|
12
|
+
module Opendistro
|
13
|
+
extend Configuration
|
14
|
+
|
15
|
+
# Alias for Opendistro::Client.new
|
16
|
+
#
|
17
|
+
# @return [Opendistro::Client]
|
18
|
+
def self.client(options = {})
|
19
|
+
Opendistro::Client.new(options)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Delegate to Opendistro::Client
|
23
|
+
def self.method_missing(method, *args, &block)
|
24
|
+
return super unless client.respond_to?(method)
|
25
|
+
|
26
|
+
client.send(method, *args, &block)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Delegate to Opendistro::Client
|
30
|
+
def self.respond_to_missing?(method_name, include_private = false)
|
31
|
+
client.respond_to?(method_name) || super
|
32
|
+
end
|
33
|
+
|
34
|
+
# Delegate to HTTParty.http_proxy
|
35
|
+
def self.http_proxy(address = nil, port = nil, username = nil, password = nil)
|
36
|
+
Opendistro::Request.http_proxy(address, port, username, password)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns an unsorted array of available client methods.
|
40
|
+
#
|
41
|
+
# @return [Array<Symbol>]
|
42
|
+
def self.actions
|
43
|
+
hidden =
|
44
|
+
/endpoint|username|password|user_agent|ca_cert|verify_ssl|get|post|put|\Adelete\z|validate\z|request_defaults|httparty/
|
45
|
+
(Opendistro::Client.instance_methods - Object.methods).reject { |e| e[hidden] }
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Opendistro
|
4
|
+
# @private
|
5
|
+
class API < Request
|
6
|
+
# @private
|
7
|
+
attr_accessor(*Configuration::VALID_OPTIONS_KEYS)
|
8
|
+
|
9
|
+
# Creates a new API.
|
10
|
+
# @raise [Error:MissingCredentials]
|
11
|
+
def initialize(options = {})
|
12
|
+
options = Opendistro.options.merge(options)
|
13
|
+
Configuration::VALID_OPTIONS_KEYS.each do |key|
|
14
|
+
send("#{key}=", options[key]) if options[key]
|
15
|
+
end
|
16
|
+
self.class.headers 'User-Agent' => user_agent
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Opendistro
|
4
|
+
# Wrapper for the Opendistro REST API.
|
5
|
+
class Client < API
|
6
|
+
Dir[File.expand_path('client/*.rb', __dir__)].each { |f| require f }
|
7
|
+
|
8
|
+
# Please keep in alphabetical order
|
9
|
+
include Users
|
10
|
+
|
11
|
+
# Text representation of the client, masking private token.
|
12
|
+
#
|
13
|
+
# @return [String]
|
14
|
+
def inspect
|
15
|
+
inspected = super
|
16
|
+
inspected.sub! @password, only_show_last_four_chars(@password) if @password
|
17
|
+
inspected
|
18
|
+
end
|
19
|
+
|
20
|
+
# Utility method for URL encoding of a string.
|
21
|
+
# Copied from https://ruby-doc.org/stdlib-2.7.0/libdoc/erb/rdoc/ERB/Util.html
|
22
|
+
#
|
23
|
+
# @return [String]
|
24
|
+
def url_encode(url)
|
25
|
+
url.to_s.b.gsub(/[^a-zA-Z0-9_\-.~]/n) { |m| sprintf('%%%02X', m.unpack1('C')) } # rubocop:disable Style/FormatString, Style/FormatStringToken
|
26
|
+
end
|
27
|
+
|
28
|
+
def only_show_last_four_chars(password)
|
29
|
+
"#{'*' * (password.size - 4)}#{password[-4..-1]}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Opendistro::Client
|
4
|
+
# Defines methods related to users.
|
5
|
+
# @see https://opendistro.github.io/for-elasticsearch-docs/docs/security/access-control/api/#users
|
6
|
+
module Users
|
7
|
+
# Gets a list of users.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# Opendistro.users
|
11
|
+
#
|
12
|
+
# @return [Opendistro::ObjectifiedHash]
|
13
|
+
def users
|
14
|
+
get('/_opendistro/_security/api/internalusers/')
|
15
|
+
end
|
16
|
+
|
17
|
+
# Gets information about a user.
|
18
|
+
# Will return information about an authorized user if no user passed.
|
19
|
+
#
|
20
|
+
# @example
|
21
|
+
# Opendistro.user
|
22
|
+
# Opendistro.user('kibanero')
|
23
|
+
#
|
24
|
+
# @param [String] name The name of a user.
|
25
|
+
# @return [Opendistro::ObjectifiedHash]
|
26
|
+
def user(username = nil)
|
27
|
+
username.nil? ? get('/_opendistro/_security/api/account') : get("/_opendistro/_security/api/internalusers/#{username}")
|
28
|
+
end
|
29
|
+
|
30
|
+
# Creates a new user.
|
31
|
+
# Requires authentication from an admin account.
|
32
|
+
#
|
33
|
+
# @example
|
34
|
+
# Opendistro.create_user('joe','secret', { attribute1: ...})
|
35
|
+
# or
|
36
|
+
# Opendistro.create_user('joe','secret', { description: 'user for test' })
|
37
|
+
#
|
38
|
+
# @param [String] username(required) The username of a user.
|
39
|
+
# @param [String] password(required) The password of a user.
|
40
|
+
# @param [Hash] options A customizable set of options.
|
41
|
+
# @return [Opendistro::ObjectifiedHash] Information about created user.
|
42
|
+
def create_user(username, password, options = {})
|
43
|
+
raise ArgumentError, 'Missing required parameters' unless username || password
|
44
|
+
|
45
|
+
put("/_opendistro/_security/api/internalusers/#{username}", body: { password: password }.merge!(options))
|
46
|
+
end
|
47
|
+
|
48
|
+
# Updates a user.
|
49
|
+
#
|
50
|
+
# @example
|
51
|
+
# Opendistro.edit_user('admin', [{ 'op' => 'replace', 'path': '/description', 'value': 'new description' }])
|
52
|
+
#
|
53
|
+
# @param [Integer] id The ID of a user.
|
54
|
+
# @param [Hash] options A customizable set of options.
|
55
|
+
# @return [Opendistro::ObjectifiedHash] Information about created user.
|
56
|
+
def edit_user(username, options = {})
|
57
|
+
patch("/_opendistro/_security/api/internalusers/#{username}", body: options.to_json)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Deletes a user.
|
61
|
+
#
|
62
|
+
# @example
|
63
|
+
# Opendistro.delete_user(1)
|
64
|
+
#
|
65
|
+
# @param [String] username The username of a user.
|
66
|
+
# @return [Opendistro::ObjectifiedHash] Information about deleted user.
|
67
|
+
def delete_user(username)
|
68
|
+
delete("/_opendistro/_security/api/internalusers/#{username}")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Opendistro
|
4
|
+
# Defines constants and methods related to configuration.
|
5
|
+
module Configuration
|
6
|
+
# An array of valid keys in the options hash when configuring a Opendistro::API.
|
7
|
+
VALID_OPTIONS_KEYS = %i[endpoint username password ca_cert httparty user_agent verify_ssl].freeze
|
8
|
+
|
9
|
+
# The user agent that will be sent to the API endpoint if none is set.
|
10
|
+
DEFAULT_USER_AGENT = "Opendistro Ruby Gem #{Opendistro::VERSION}"
|
11
|
+
DEFAULT_VERIFY_SSL = true
|
12
|
+
# @private
|
13
|
+
attr_accessor(*VALID_OPTIONS_KEYS)
|
14
|
+
|
15
|
+
# Sets all configuration options to their default values
|
16
|
+
# when this module is extended.
|
17
|
+
def self.extended(base)
|
18
|
+
base.reset
|
19
|
+
end
|
20
|
+
|
21
|
+
# Convenience method to allow configuration options to be set in a block.
|
22
|
+
def configure
|
23
|
+
yield self
|
24
|
+
end
|
25
|
+
|
26
|
+
# Creates a hash of options and their values.
|
27
|
+
def options
|
28
|
+
VALID_OPTIONS_KEYS.inject({}) do |option, key|
|
29
|
+
option.merge!(key => send(key))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Resets all configuration options to the defaults.
|
34
|
+
def reset
|
35
|
+
self.endpoint = ENV['OPENDISTRO_API_ENDPOINT']
|
36
|
+
self.username = ENV['OPENDISTRO_API_USER']
|
37
|
+
self.password = ENV['OPENDISTRO_API_PASSWORD']
|
38
|
+
self.ca_cert = ENV['OPENDISTRO_API_CA_CERT_PATH']
|
39
|
+
self.httparty = get_httparty_config(ENV['OPENDISTRO_API_HTTPARTY_OPTIONS'])
|
40
|
+
self.user_agent = DEFAULT_USER_AGENT
|
41
|
+
self.verify_ssl = ENV['OPENDISTRO_API_VERIFY_SSL'] || DEFAULT_VERIFY_SSL
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# Allows HTTParty config to be specified in ENV using YAML hash.
|
47
|
+
def get_httparty_config(options)
|
48
|
+
return if options.nil?
|
49
|
+
|
50
|
+
httparty = Opendistro::CLI::Helpers.yaml_load(options)
|
51
|
+
raise ArgumentError, 'HTTParty config should be a Hash.' unless httparty.is_a? Hash
|
52
|
+
|
53
|
+
Opendistro::CLI::Helpers.symbolize_keys httparty
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Opendistro
|
4
|
+
module Error
|
5
|
+
# Custom error class for rescuing from all Opendistro errors.
|
6
|
+
class Error < StandardError; end
|
7
|
+
|
8
|
+
# Raised when API endpoint credentials not configured.
|
9
|
+
class MissingCredentials < Error; end
|
10
|
+
|
11
|
+
# Raised when impossible to parse response body.
|
12
|
+
class Parsing < Error; end
|
13
|
+
|
14
|
+
# Custom error class for rescuing from HTTP response errors.
|
15
|
+
class ResponseError < Error
|
16
|
+
POSSIBLE_MESSAGE_KEYS = %i[message error_description error].freeze
|
17
|
+
|
18
|
+
def initialize(response)
|
19
|
+
@response = response
|
20
|
+
super(build_error_message)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Status code returned in the HTTP response.
|
24
|
+
#
|
25
|
+
# @return [Integer]
|
26
|
+
def response_status
|
27
|
+
@response.code
|
28
|
+
end
|
29
|
+
|
30
|
+
# Body content returned in the HTTP response
|
31
|
+
#
|
32
|
+
# @return [String]
|
33
|
+
def response_message
|
34
|
+
@response.parsed_response.message
|
35
|
+
end
|
36
|
+
|
37
|
+
# Additional error context returned by some API endpoints
|
38
|
+
#
|
39
|
+
# @return [String]
|
40
|
+
def error_code
|
41
|
+
if @response.respond_to?(:error_code)
|
42
|
+
@response.error_code
|
43
|
+
else
|
44
|
+
''
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
# Human friendly message.
|
51
|
+
#
|
52
|
+
# @return [String]
|
53
|
+
def build_error_message
|
54
|
+
parsed_response = classified_response
|
55
|
+
message = check_error_keys(parsed_response)
|
56
|
+
"Server responded with code #{@response.code}, message: " \
|
57
|
+
"#{handle_message(message)}. " \
|
58
|
+
"Request URI: #{@response.request.base_uri}#{@response.request.path}"
|
59
|
+
end
|
60
|
+
|
61
|
+
# Error keys vary across the API, find the first key that the parsed_response
|
62
|
+
# object responds to and return that, otherwise return the original.
|
63
|
+
def check_error_keys(resp)
|
64
|
+
key = POSSIBLE_MESSAGE_KEYS.find { |k| resp.respond_to?(k) }
|
65
|
+
key ? resp.send(key) : resp
|
66
|
+
end
|
67
|
+
|
68
|
+
# Parse the body based on the classification of the body content type
|
69
|
+
#
|
70
|
+
# @return parsed response
|
71
|
+
def classified_response
|
72
|
+
if @response.respond_to?('headers')
|
73
|
+
@response.headers['content-type'] == 'text/plain' ? { message: @response.to_s } : @response.parsed_response
|
74
|
+
else
|
75
|
+
@response.parsed_response
|
76
|
+
end
|
77
|
+
rescue Opendistro::Error::Parsing
|
78
|
+
# Return stringified response when receiving a
|
79
|
+
# parsing error to avoid obfuscation of the
|
80
|
+
# api error.
|
81
|
+
#
|
82
|
+
# note: The Opendistro API does not always return valid
|
83
|
+
# JSON when there are errors.
|
84
|
+
@response.to_s
|
85
|
+
end
|
86
|
+
|
87
|
+
# Handle error response message in case of nested hashes
|
88
|
+
def handle_message(message)
|
89
|
+
case message
|
90
|
+
when Opendistro::ObjectifiedHash
|
91
|
+
message.to_h.sort.map do |key, val|
|
92
|
+
"'#{key}' #{(val.is_a?(Hash) ? val.sort.map { |k, v| "(#{k}: #{v.join(' ')})" } : [val].flatten).join(' ')}"
|
93
|
+
end.join(', ')
|
94
|
+
when Array
|
95
|
+
message.join(' ')
|
96
|
+
else
|
97
|
+
message
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Raised when API endpoint returns the HTTP status code 400.
|
103
|
+
class BadRequest < ResponseError; end
|
104
|
+
|
105
|
+
# Raised when API endpoint returns the HTTP status code 401.
|
106
|
+
class Unauthorized < ResponseError; end
|
107
|
+
|
108
|
+
# Raised when API endpoint returns the HTTP status code 403.
|
109
|
+
class Forbidden < ResponseError; end
|
110
|
+
|
111
|
+
# Raised when API endpoint returns the HTTP status code 404.
|
112
|
+
class NotFound < ResponseError; end
|
113
|
+
|
114
|
+
# Raised when API endpoint returns the HTTP status code 405.
|
115
|
+
class MethodNotAllowed < ResponseError; end
|
116
|
+
|
117
|
+
# Raised when API endpoint returns the HTTP status code 406.
|
118
|
+
class NotAcceptable < ResponseError; end
|
119
|
+
|
120
|
+
# Raised when API endpoint returns the HTTP status code 409.
|
121
|
+
class Conflict < ResponseError; end
|
122
|
+
|
123
|
+
# Raised when API endpoint returns the HTTP status code 422.
|
124
|
+
class Unprocessable < ResponseError; end
|
125
|
+
|
126
|
+
# Raised when API endpoint returns the HTTP status code 429.
|
127
|
+
class TooManyRequests < ResponseError; end
|
128
|
+
|
129
|
+
# Raised when API endpoint returns the HTTP status code 500.
|
130
|
+
class InternalServerError < ResponseError; end
|
131
|
+
|
132
|
+
# Raised when API endpoint returns the HTTP status code 502.
|
133
|
+
class BadGateway < ResponseError; end
|
134
|
+
|
135
|
+
# Raised when API endpoint returns the HTTP status code 503.
|
136
|
+
class ServiceUnavailable < ResponseError; end
|
137
|
+
|
138
|
+
# HTTP status codes mapped to error classes.
|
139
|
+
STATUS_MAPPINGS = {
|
140
|
+
400 => BadRequest,
|
141
|
+
401 => Unauthorized,
|
142
|
+
403 => Forbidden,
|
143
|
+
404 => NotFound,
|
144
|
+
405 => MethodNotAllowed,
|
145
|
+
406 => NotAcceptable,
|
146
|
+
409 => Conflict,
|
147
|
+
422 => Unprocessable,
|
148
|
+
429 => TooManyRequests,
|
149
|
+
500 => InternalServerError,
|
150
|
+
502 => BadGateway,
|
151
|
+
503 => ServiceUnavailable
|
152
|
+
}.freeze
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Opendistro
|
4
|
+
# Wrapper class of file response.
|
5
|
+
class FileResponse
|
6
|
+
HEADER_CONTENT_DISPOSITION = 'Content-Disposition'
|
7
|
+
|
8
|
+
attr_reader :filename
|
9
|
+
|
10
|
+
def initialize(file)
|
11
|
+
@file = file
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [bool] Always false
|
15
|
+
def empty?
|
16
|
+
false
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [Hash] A hash consisting of filename and io object
|
20
|
+
def to_hash
|
21
|
+
{ filename: @filename, data: @file }
|
22
|
+
end
|
23
|
+
alias to_h to_hash
|
24
|
+
|
25
|
+
# @return [String] Formatted string with the class name, object id and filename.
|
26
|
+
def inspect
|
27
|
+
"#<#{self.class}:#{object_id} {filename: #{filename.inspect}}>"
|
28
|
+
end
|
29
|
+
|
30
|
+
def method_missing(name, *args, &block)
|
31
|
+
if @file.respond_to?(name)
|
32
|
+
@file.send(name, *args, &block)
|
33
|
+
else
|
34
|
+
super
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def respond_to_missing?(method_name, include_private = false)
|
39
|
+
super || @file.respond_to?(method_name, include_private)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Parse filename from the 'Content Disposition' header.
|
43
|
+
def parse_headers!(headers)
|
44
|
+
@filename = headers[HEADER_CONTENT_DISPOSITION].split('filename=')[1]
|
45
|
+
@filename = @filename[1...-1] if @filename[0] == '"' # Unquote filenames
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Opendistro
|
4
|
+
# Converts hashes to the objects.
|
5
|
+
class ObjectifiedHash
|
6
|
+
# Creates a new ObjectifiedHash object.
|
7
|
+
def initialize(hash)
|
8
|
+
@hash = hash
|
9
|
+
@data = hash.each_with_object({}) do |(key, value), data|
|
10
|
+
value = self.class.new(value) if value.is_a? Hash
|
11
|
+
value = value.map { |v| v.is_a?(Hash) ? self.class.new(v) : v } if value.is_a? Array
|
12
|
+
data[key.to_s] = value
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return [Hash] The original hash.
|
17
|
+
def to_hash
|
18
|
+
hash
|
19
|
+
end
|
20
|
+
alias to_h to_hash
|
21
|
+
|
22
|
+
# @return [String] Formatted string with the class name, object id and original hash.
|
23
|
+
def inspect
|
24
|
+
"#<#{self.class}:#{object_id} {hash: #{hash.inspect}}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def [](key)
|
28
|
+
data[key]
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :hash, :data
|
34
|
+
|
35
|
+
# Respond to messages for which `self.data` has a key
|
36
|
+
def method_missing(method_name, *args, &block)
|
37
|
+
if data.key?(method_name.to_s)
|
38
|
+
data[method_name.to_s]
|
39
|
+
elsif data.respond_to?(method_name)
|
40
|
+
warn 'WARNING: Please convert ObjectifiedHash object to hash before calling Hash methods on it.'
|
41
|
+
data.send(method_name, *args, &block)
|
42
|
+
else
|
43
|
+
super
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def respond_to_missing?(method_name, include_private = false)
|
48
|
+
hash.keys.map(&:to_sym).include?(method_name.to_sym) || super
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'httparty'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module Opendistro
|
7
|
+
# @private
|
8
|
+
class Request
|
9
|
+
include HTTParty
|
10
|
+
format :json
|
11
|
+
headers 'Accept' => 'application/json', 'Content-Type' => 'application/json'
|
12
|
+
parser(proc { |body, _| parse(body) })
|
13
|
+
|
14
|
+
attr_accessor :username, :password, :verify_ssl, :ca_cert, :endpoint
|
15
|
+
|
16
|
+
# Converts the response body to an ObjectifiedHash.
|
17
|
+
def self.parse(body)
|
18
|
+
body = decode(body)
|
19
|
+
|
20
|
+
if body.is_a? Hash
|
21
|
+
ObjectifiedHash.new body
|
22
|
+
elsif body.is_a? Array
|
23
|
+
PaginatedResponse.new(body.collect! { |e| ObjectifiedHash.new(e) })
|
24
|
+
elsif body
|
25
|
+
true
|
26
|
+
elsif !body
|
27
|
+
false
|
28
|
+
elsif body.nil?
|
29
|
+
false
|
30
|
+
else
|
31
|
+
raise Error::Parsing, "Couldn't parse a response body"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Decodes a JSON response into Ruby object.
|
36
|
+
def self.decode(response)
|
37
|
+
response ? JSON.load(response) : {}
|
38
|
+
rescue JSON::ParserError
|
39
|
+
raise Error::Parsing, 'The response is not a valid JSON'
|
40
|
+
end
|
41
|
+
|
42
|
+
%w[get post put delete patch].each do |method|
|
43
|
+
define_method method do |path, options = {}|
|
44
|
+
params = options.dup
|
45
|
+
|
46
|
+
httparty_config(params)
|
47
|
+
|
48
|
+
params[:headers] ||= {}
|
49
|
+
params[:headers].merge!(authorization_header)
|
50
|
+
|
51
|
+
params[:ssl_ca_file] = @ca_cert unless @ca_cert.nil?
|
52
|
+
params[:verify] = @verify_ssl unless @verify_ssl
|
53
|
+
params[:body] = params[:body].to_json if params[:body].is_a? Hash
|
54
|
+
|
55
|
+
validate self.class.send(method, @endpoint + path, params)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Checks the response code for common errors.
|
60
|
+
# Returns parsed response for successful requests.
|
61
|
+
def validate(response)
|
62
|
+
error_klass = Error::STATUS_MAPPINGS[response.code]
|
63
|
+
raise error_klass, response if error_klass
|
64
|
+
|
65
|
+
parsed = response.parsed_response
|
66
|
+
parsed.client = self if parsed.respond_to?(:client=)
|
67
|
+
parsed.parse_headers!(response.headers) if parsed.respond_to?(:parse_headers!)
|
68
|
+
parsed
|
69
|
+
end
|
70
|
+
|
71
|
+
# Sets a base_uri and default_params for requests.
|
72
|
+
# @raise [Error::MissingCredentials] if endpoint not set.
|
73
|
+
def request_defaults
|
74
|
+
raise Error::MissingCredentials, 'Please set an endpoint to API' unless @endpoint
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
# Returns an Authorization header hash
|
80
|
+
#
|
81
|
+
# @raise [Error::MissingCredentials] if private_token and auth_token are not set.
|
82
|
+
def authorization_header
|
83
|
+
raise Error::MissingCredentials, 'Please provide a private_token or auth_token for user' if @username.nil? || @password.nil?
|
84
|
+
|
85
|
+
auth = Base64.encode64("#{@username}:#{@password}")
|
86
|
+
{ 'Authorization' => "Basic #{auth}" }
|
87
|
+
end
|
88
|
+
|
89
|
+
# Set HTTParty configuration
|
90
|
+
# @see https://github.com/jnunemaker/httparty
|
91
|
+
def httparty_config(options)
|
92
|
+
options.merge!(httparty) if httparty
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
metadata
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: opendistro
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Lucas Mariani
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-08-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: httparty
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.14'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 0.14.0
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0.14'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.14.0
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: terminal-table
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '1.5'
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: 1.5.1
|
43
|
+
type: :runtime
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - "~>"
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '1.5'
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: 1.5.1
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: rake
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
type: :development
|
61
|
+
prerelease: false
|
62
|
+
version_requirements: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '0'
|
67
|
+
- !ruby/object:Gem::Dependency
|
68
|
+
name: rspec
|
69
|
+
requirement: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0'
|
74
|
+
type: :development
|
75
|
+
prerelease: false
|
76
|
+
version_requirements: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: webmock
|
83
|
+
requirement: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
type: :development
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
description: Ruby client and CLI for Opendistro API
|
96
|
+
email:
|
97
|
+
- marianilucas@gmail.com
|
98
|
+
executables: []
|
99
|
+
extensions: []
|
100
|
+
extra_rdoc_files: []
|
101
|
+
files:
|
102
|
+
- CHANGELOG.md
|
103
|
+
- LICENSE.txt
|
104
|
+
- README.md
|
105
|
+
- lib/opendistro.rb
|
106
|
+
- lib/opendistro/api.rb
|
107
|
+
- lib/opendistro/client.rb
|
108
|
+
- lib/opendistro/client/users.rb
|
109
|
+
- lib/opendistro/configuration.rb
|
110
|
+
- lib/opendistro/error.rb
|
111
|
+
- lib/opendistro/file_response.rb
|
112
|
+
- lib/opendistro/objectified_hash.rb
|
113
|
+
- lib/opendistro/request.rb
|
114
|
+
- lib/opendistro/version.rb
|
115
|
+
homepage: https://github.com/psyreactor/opendistro
|
116
|
+
licenses:
|
117
|
+
- BSD-2-Clause
|
118
|
+
metadata: {}
|
119
|
+
post_install_message:
|
120
|
+
rdoc_options: []
|
121
|
+
require_paths:
|
122
|
+
- lib
|
123
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
124
|
+
requirements:
|
125
|
+
- - ">="
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: '2.5'
|
128
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0'
|
133
|
+
requirements: []
|
134
|
+
rubygems_version: 3.1.2
|
135
|
+
signing_key:
|
136
|
+
specification_version: 4
|
137
|
+
summary: A Ruby wrapper and CLI for the Opendistro API
|
138
|
+
test_files: []
|