train-rest 0.4.2 → 0.5.0

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