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.
@@ -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
@@ -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
@@ -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.
@@ -0,0 +1,160 @@
1
+ # Opendistro
2
+
3
+ [![Build Status](https://img.shields.io/github/workflow/status/psyreactor/opendistro/CI/master)](https://github.com/psyreactor/opendistro/actions?query=workflow%3ARuby)
4
+ [![Inline docs](https://inch-ci.org/github/psyreactor/opendistro.svg?branch=master)](https://inch-ci.org/github/psyreactor/opendistro)
5
+ [![Coverage Status](https://coveralls.io/repos/github/psyreactor/opendistro/badge.svg)](https://coveralls.io/github/psyreactor/opendistro)
6
+ [![Gem version](https://img.shields.io/gem/v/opendistro.svg)](https://rubygems.org/gems/opendistro)
7
+ [![License](https://img.shields.io/badge/license-BSD-red.svg)](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.
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opendistro
4
+ VERSION = '1.0.0'
5
+ 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: []