train-rest 0.4.2 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bdaed9296e5756a522d6fbaf9fd8d5aca6ed8f2e74250387a97f65fbf58c51b2
4
- data.tar.gz: 0bd7dd4791ea737ff122343445bb58a974c0e01e03431a31ab57616e548805aa
3
+ metadata.gz: 73a28c27d6e8385d3d8356c363847ddae1380ff0d0e75d6f919f64cefb315637
4
+ data.tar.gz: 1b0140ce7079d48ac070b05607acc1b142c987cd598a7f60fdc28ff3025dc3f6
5
5
  SHA512:
6
- metadata.gz: dc8d624d5b44cf12e3fb1810cc0f3e84acd53230e503a4a46dee3d04335aef445d21178959b35a712e3b9d2e98eb6328eb45c6e8dc55684cecd1322f3a8be128
7
- data.tar.gz: cd8d93fc0e5b0926ce86c6d1ab826f0a84c1996b0c15586e46cd7d78096e851127a4f82fac49998a34e57e3b04d626ef274b5870c771eb824450cf8179b7d2ce
6
+ metadata.gz: 293e8e03b838b888108eb25ac164080f76aa0291420bcd04feb8503245c3149ff3d03257ad01df3ea3d2fae018094a0ec85c1e4a3b052b1a07c3cc0db69b0244
7
+ data.tar.gz: a816054a1e6db4bd24134249adaedd933b16b9796c3cfde7afa90f4e66ea70d9cdf744bea124ee2e93b383d6ad4b666e4bdd2a4ae363fa09056bad3eb85ada6f
@@ -0,0 +1,119 @@
1
+ require 'aws-sigv4'
2
+ require 'json'
3
+
4
+ require_relative "../auth_handler"
5
+
6
+ module TrainPlugins
7
+ module Rest
8
+ class AWSV4 < AuthHandler
9
+ VALID_CREDENTIALS = %w[
10
+ access_keys
11
+ ].freeze
12
+
13
+ SIGNED_HEADERS = %w[
14
+ content-type host x-amz-date x-amz-target
15
+ ].freeze
16
+
17
+ def check_options
18
+ options[:credentials] ||= "access_keys"
19
+
20
+ unless VALID_CREDENTIALS.include? credentials
21
+ raise ArgumentError.new("Invalid type of credentials: #{credentials}")
22
+ end
23
+
24
+ if access_keys?
25
+ raise ArgumentError.new('Missing `access_key` credential') unless access_key
26
+ raise ArgumentError.new('Missing `secret_access_key` credential') unless secret_access_key
27
+ end
28
+ end
29
+
30
+ def signature_based?
31
+ true
32
+ end
33
+
34
+ def process(payload: "", headers: {}, url: "", method: nil)
35
+ headers.merge! ({
36
+ 'Accept-Encoding' => 'identity',
37
+ 'User-Agent' => "train-rest/#{TrainPlugins::Rest::VERSION}",
38
+ 'Content-Type' => 'application/x-amz-json-1.0'
39
+ })
40
+
41
+ signed_headers = headers.select do |name, _value|
42
+ SIGNED_HEADERS.include? name.downcase
43
+ end
44
+
45
+ @url = url
46
+
47
+ signature = signer(url).sign_request(
48
+ http_method: method.to_s.upcase,
49
+ url: url,
50
+ headers: signed_headers,
51
+ body: payload.to_json
52
+ )
53
+
54
+ {
55
+ headers: headers.merge(signature.headers)
56
+ }
57
+ end
58
+
59
+ def process_error(error)
60
+ raise AuthenticationError.new("Authentication failed: #{error.response.to_s.chop}") if error.response.code == 401
61
+ raise BadRequest.new("Bad request: #{error.response.to_s.chop}") if error.response.code == 400
62
+
63
+ message = JSON.parse(error.response.to_s)
64
+
65
+ raise AuthenticationError.new(message["message"] || message["__type"])
66
+ rescue JSON::ParserError => e
67
+ raise AuthenticationError.new(error.response.to_s)
68
+ end
69
+
70
+ def access_key
71
+ options[:access_key] || ENV['AWS_ACCESS_KEY_ID']
72
+ end
73
+
74
+ def region(url = default_url)
75
+ url.delete_prefix('https://').split('.').at(1)
76
+ end
77
+
78
+ private
79
+
80
+ def credentials
81
+ options[:credentials]
82
+ end
83
+
84
+ def default_url
85
+ options[:endpoint]
86
+ end
87
+
88
+ def access_keys?
89
+ credentials == 'access_keys'
90
+ end
91
+
92
+ def secret_access_key
93
+ options[:secret_access_key] || ENV['AWS_SECRET_ACCESS_KEY']
94
+ end
95
+
96
+ def service(url)
97
+ url.delete_prefix('https://').split('.').at(0)
98
+ end
99
+
100
+ def signer(url)
101
+ Aws::Sigv4::Signer.new(
102
+ service: service(url),
103
+ region: region(url),
104
+
105
+ **signer_credentials
106
+ )
107
+ end
108
+
109
+ def signer_credentials
110
+ if access_keys?
111
+ {
112
+ access_key_id: access_key,
113
+ secret_access_key: secret_access_key
114
+ }
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,39 @@
1
+ require_relative "../auth_handler"
2
+
3
+ module TrainPlugins
4
+ module Rest
5
+ # Authentication via HMAC Signature.
6
+ class HmacSignature < AuthHandler
7
+ def check_options
8
+ raise ArgumentError.new("Need :hmac_secret for HMAC signatures") unless options[:hmac_secret]
9
+
10
+ options[:header] ||= "X-Signature"
11
+ options[:digest] ||= "SHA256"
12
+ end
13
+
14
+ def hmac_secret
15
+ options[:hmac_secret]
16
+ end
17
+
18
+ def digest
19
+ options[:digest]
20
+ end
21
+
22
+ def header
23
+ options[:header]
24
+ end
25
+
26
+ def signature_based?
27
+ true
28
+ end
29
+
30
+ def process(payload: "", headers: {}, url: "", method: nil)
31
+ {
32
+ headers: {
33
+ header => OpenSSL::HMAC.hexdigest(digest, hmac_secret, payload)
34
+ }
35
+ }
36
+ end
37
+ end
38
+ end
39
+ end
@@ -67,6 +67,30 @@ module TrainPlugins
67
67
  { headers: auth_headers }
68
68
  end
69
69
 
70
+ # This Auth Handler will need payload, URI and headers, e.g. for signatures.
71
+ #
72
+ # @return [Boolean]
73
+ def signature_based?
74
+ false
75
+ end
76
+
77
+ # Return headers based on payload signing.
78
+ #
79
+ # @param [Hash] data different types of data for processing
80
+ # @option data [String] :payload contents of the message body
81
+ # @option data [Hash] :headers existing headers to the request
82
+ # @option data [String] :url URL which will be requested
83
+ # @option data [Symbol] :method Method to execute
84
+ # @returns [Hash]
85
+ def process(payload: "", headers: {}, url: "", method: nil)
86
+ {}
87
+ end
88
+
89
+ # Allow processing errors related to authentication.
90
+ #
91
+ # @param [RestClient::Exception] error raw error data
92
+ def process_error(_error); end
93
+
70
94
  class << self
71
95
  private
72
96
 
@@ -77,7 +101,8 @@ module TrainPlugins
77
101
  # @see https://github.com/chef/chef/blob/main/lib/chef/mixin/convert_to_class_name.rb
78
102
  def convert_to_snake_case(str)
79
103
  str = str.dup
80
- str.gsub!(/[A-Z]/) { |s| "_" + s }
104
+ str.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
105
+ str.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
81
106
  str.downcase!
82
107
  str.sub!(/^\_/, "")
83
108
  str
@@ -114,6 +114,21 @@ module TrainPlugins
114
114
  # Merge override headers + request specific headers
115
115
  parameters[:headers].merge!(override_headers || {})
116
116
  parameters[:headers].merge!(headers)
117
+
118
+ # Merge payload based headers (e.g. signature-based auth)
119
+ if auth_handler.signature_based?
120
+ auth_signature = auth_handler.process(
121
+ payload: data,
122
+ headers: parameters[:headers],
123
+ url: parameters[:url],
124
+ method: method
125
+ )
126
+
127
+ parameters[:headers].merge! auth_signature[:headers]
128
+ else
129
+ parameters[:headers].merge! auth_parameters[:headers]
130
+ end
131
+
117
132
  parameters.compact!
118
133
 
119
134
  logger.info format("[REST] => %s", parameters.to_s) if options[:debug_rest]
@@ -121,6 +136,9 @@ module TrainPlugins
121
136
 
122
137
  logger.info format("[REST] <= %s", response.to_s) if options[:debug_rest]
123
138
  transform_response(response, json_processing)
139
+
140
+ rescue RestClient::Exception => error
141
+ auth_handler.process_error(error)
124
142
  end
125
143
 
126
144
  # Allow switching generic handlers for an API-specific one.
@@ -144,12 +162,21 @@ module TrainPlugins
144
162
  options[:auth_type]
145
163
  end
146
164
 
165
+ attr_writer :auth_handler
166
+
147
167
  # Auth Handlers-faced API
148
168
 
149
169
  def auth_parameters
150
170
  auth_handler.auth_parameters
151
171
  end
152
172
 
173
+ def auth_handler
174
+ desired_handler = auth_handler_classes.detect { |handler| handler.name == auth_type.to_s }
175
+ raise NameError.new(format("Authentication handler %s not found", auth_type.to_s)) unless desired_handler
176
+
177
+ @auth_handler ||= desired_handler.new(self)
178
+ end
179
+
153
180
  private
154
181
 
155
182
  def global_parameters
@@ -160,8 +187,6 @@ module TrainPlugins
160
187
  headers: options[:headers],
161
188
  }
162
189
 
163
- params.merge!(auth_parameters)
164
-
165
190
  params
166
191
  end
167
192
 
@@ -184,23 +209,14 @@ module TrainPlugins
184
209
  :basic if options[:username] && options[:password]
185
210
  end
186
211
 
187
- attr_writer :auth_handler
188
-
189
212
  def auth_handler_classes
190
- AuthHandler.descendants
213
+ ::TrainPlugins::Rest::AuthHandler.descendants
191
214
  end
192
215
 
193
216
  def auth_handlers
194
217
  auth_handler_classes.map { |handler| handler.name.to_sym }
195
218
  end
196
219
 
197
- def auth_handler
198
- desired_handler = auth_handler_classes.detect { |handler| handler.name == auth_type.to_s }
199
- raise NameError.new(format("Authentication handler %s not found", auth_type.to_s)) unless desired_handler
200
-
201
- @auth_handler ||= desired_handler.new(self)
202
- end
203
-
204
220
  def login
205
221
  logger.info format("REST Login via %s authentication handler", auth_type.to_s) unless %i{anonymous basic}.include? auth_type
206
222
 
@@ -0,0 +1,6 @@
1
+ module TrainPlugins
2
+ module Rest
3
+ class AuthenticationError < RuntimeError; end
4
+ class BadRequest < RuntimeError; end
5
+ end
6
+ end
@@ -1,5 +1,5 @@
1
1
  module TrainPlugins
2
2
  module Rest
3
- VERSION = "0.4.2".freeze
3
+ VERSION = "0.5.0".freeze
4
4
  end
5
5
  end
data/lib/train-rest.rb CHANGED
@@ -1,14 +1,18 @@
1
1
  libdir = File.dirname(__FILE__)
2
2
  $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
3
3
 
4
+ require "train-rest/errors"
4
5
  require "train-rest/version"
5
6
 
6
7
  require "train-rest/transport"
7
8
  require "train-rest/connection"
8
9
 
10
+ require "train-rest/auth_handler"
11
+ require "train-rest/auth_handler/awsv4"
9
12
  require "train-rest/auth_handler/anonymous"
10
13
  require "train-rest/auth_handler/authtype-apikey"
11
- require "train-rest/auth_handler/header"
12
14
  require "train-rest/auth_handler/basic"
13
15
  require "train-rest/auth_handler/bearer"
16
+ require "train-rest/auth_handler/header"
17
+ require "train-rest/auth_handler/hmac-signature"
14
18
  require "train-rest/auth_handler/redfish"
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: train-rest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Heinen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-03-01 00:00:00.000000000 Z
11
+ date: 2022-09-06 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sigv4
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.5'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.5'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: train-core
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -120,11 +134,14 @@ files:
120
134
  - lib/train-rest/auth_handler.rb
121
135
  - lib/train-rest/auth_handler/anonymous.rb
122
136
  - lib/train-rest/auth_handler/authtype-apikey.rb
137
+ - lib/train-rest/auth_handler/awsv4.rb
123
138
  - lib/train-rest/auth_handler/basic.rb
124
139
  - lib/train-rest/auth_handler/bearer.rb
125
140
  - lib/train-rest/auth_handler/header.rb
141
+ - lib/train-rest/auth_handler/hmac-signature.rb
126
142
  - lib/train-rest/auth_handler/redfish.rb
127
143
  - lib/train-rest/connection.rb
144
+ - lib/train-rest/errors.rb
128
145
  - lib/train-rest/transport.rb
129
146
  - lib/train-rest/version.rb
130
147
  homepage: https://github.com/tecracer-chef/train-rest