opendistro 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []