forked from inspec/inspec-azure
-
Notifications
You must be signed in to change notification settings - Fork 0
/
helpers.rb
395 lines (363 loc) · 19.5 KB
/
helpers.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
# frozen_string_literal: true
require 'backend/azure_environment'
# TODO: This file should be updated at every release.
# Source:
# https://github.com/Azure/azure-sdk-for-ruby/blob/master/runtime/ms_rest_azure/lib/ms_rest_azure/azure_environment.rb
# Base module name should be changed to => module MicrosoftRestAzure
# Azure REST API specific errors.
#
# If the API returns an invalid api_version error,
# the suggested api_version can be acquired from the error message and used at consecutive calls.
#
# E.g.:
# rescue UnsuccessfulAPIQuery::UnexpectedHTTPResponse::InvalidApiVersionParameter => e
# api_version_suggested = e.get_suggested_api
#
class UnsuccessfulAPIQuery < StandardError
class ResourceNotFound < StandardError; end
class UnexpectedHTTPResponse < StandardError
class InvalidApiVersionParameter < StandardError
# Return a list if the wrong api is not provided.
def suggested_api_version(wrong_api_version = nil)
# Capture all the api versions within the error message.
# This will include the wrong one used in HTTP request.
# It has to be removed.
#
# Example for key in a key vault: https://docs.microsoft.com/en-us/rest/api/keyvault/getkey/getkey
#
# "The specified version (7.4) is not recognized. Consider using the latest supported version (7.1)."
semver_like_versions = message.scan(/[0-9]\.[0-9]/)
unless semver_like_versions.empty?
# The last one will be the supported version.
return semver_like_versions.last
end
#
# Example for specific resource type api (for detailed description)
# "No registered resource provider found for location 'westeurope' and API version '2022-01-01' for type
# 'virtualMachines'. The supported api-versions are '2015-05-01-preview, 2015-06-15, 2016-03-30,
# 2016-04-30-preview, 2016-08-30, 2017-03-30, 2017-12-01, 2018-04-01, 2018-06-01, 2018-10-01, 2019-03-01,
# 2019-07-01, 2019-12-01, 2020-06-01'. The supported locations are 'eastus, eastus2, westus, centralus,
# northcentralus, southcentralus, northeurope, westeurope, eastasia, southeastasia, japaneast, japanwest,
# australiaeast, australiasoutheast, australiacentral, brazilsouth, southindia, centralindia, westindia,
# canadacentral, canadaeast, westus2, westcentralus, uksouth, ukwest, koreacentral, koreasouth, francecentral,
# southafricanorth, uaenorth, switzerlandnorth, germanywestcentral, norwayeast'."
#
# Example for resource manager api (for short description)
# "The api-version '2019-10-11' is invalid. The supported versions are '2020-01-01,2019-11-01,2019-10-01,
# 2019-09-01,2019-08-01,2019-07-01,2019-06-01,2019-05-10,2019-05-01,2019-03-01,2018-11-01,2018-09-01,
# 2018-08-01,2018-07-01,2018-06-01,2018-05-01,2018-02-01,2018-01-01,2017-12-01,2017-08-01,2017-06-01,
# 2017-05-10,2017-05-01,2017-03-01,2016-09-01,2016-07-01,2016-06-01,2016-02-01,2015-11-01,2015-01-01,
# 2014-04-01-preview,2014-04-01,2014-01-01,2013-03-01,2014-02-26,2014-04'."
#
# There are cases where the stable api_versions are too old and don't return JSON response.
# If the latest stable is too old (based on the age_criteria), then return the preview versions as well.
# This is a quick fix until TODO: finding a more stable solution.
stable_api_versions = message.scan(/\d{4}-\d{2}-\d{2}[,']/).map(&:chop).sort.reverse
preview_api_versions = message.scan(/\d{4}-\d{2}-\d{2}-preview/).sort.reverse
if wrong_api_version
stable_api_versions.delete(wrong_api_version) if stable_api_versions.include?(wrong_api_version)
preview_api_versions.delete(wrong_api_version) if preview_api_versions.include?(wrong_api_version)
end
age_criteria = 2
n_a_l = Helpers.normalize_api_list(age_criteria, stable_api_versions, preview_api_versions)
n_a_l.first
end
end
end
end
class HTTPClientError < StandardError
class MissingCredentials < StandardError; end
end
# Create necessary Azure environment variables and provide access to them
#
# @example:
# my_env = AzureEnvironments.get_endpoint('azure_cloud')
# my_env.resource_manager_endpoint_url => 'https://management.azure.com/'
#
# For graph api endpoint urls:
# https://docs.microsoft.com/en-us/graph/deployments
# For graph api endpoint api versions:
# https://docs.microsoft.com/en-us/azure/active-directory/develop/microsoft-graph-intro
class AzureEnvironments
# Following data can be modified if necessary.
# TODO: Update API versions if there is a newer version available.
ENDPOINTS = {
'azure_cloud' => {
resource_manager_endpoint_url: MicrosoftRestAzure::AzureEnvironments::AzureCloud.resource_manager_endpoint_url,
active_directory_endpoint_url: MicrosoftRestAzure::AzureEnvironments::AzureCloud.active_directory_endpoint_url,
storage_endpoint_suffix: MicrosoftRestAzure::AzureEnvironments::AzureCloud.storage_endpoint_suffix,
key_vault_dns_suffix: MicrosoftRestAzure::AzureEnvironments::AzureCloud.key_vault_dns_suffix,
resource_manager_endpoint_api_version: '2020-01-01',
graph_api_endpoint_url: 'https://graph.microsoft.com',
graph_api_endpoint_api_version: 'v1.0',
},
# The latest version can be acquired from the error message if the current ones don't work.
'azure_china_cloud' => {
resource_manager_endpoint_url: MicrosoftRestAzure::AzureEnvironments::AzureChinaCloud.resource_manager_endpoint_url,
active_directory_endpoint_url: MicrosoftRestAzure::AzureEnvironments::AzureChinaCloud.active_directory_endpoint_url,
storage_endpoint_suffix: MicrosoftRestAzure::AzureEnvironments::AzureChinaCloud.storage_endpoint_suffix,
key_vault_dns_suffix: MicrosoftRestAzure::AzureEnvironments::AzureChinaCloud.key_vault_dns_suffix,
resource_manager_endpoint_api_version: '2020-01-01',
graph_api_endpoint_url: 'https://microsoftgraph.chinacloudapi.cn',
graph_api_endpoint_url_api_version: 'v1.0',
},
'azure_us_government_L4' => {
resource_manager_endpoint_url: MicrosoftRestAzure::AzureEnvironments::AzureUSGovernment.resource_manager_endpoint_url,
active_directory_endpoint_url: MicrosoftRestAzure::AzureEnvironments::AzureUSGovernment.active_directory_endpoint_url,
storage_endpoint_suffix: MicrosoftRestAzure::AzureEnvironments::AzureUSGovernment.storage_endpoint_suffix,
key_vault_dns_suffix: MicrosoftRestAzure::AzureEnvironments::AzureUSGovernment.key_vault_dns_suffix,
resource_manager_endpoint_api_version: '2020-01-01',
graph_api_endpoint_url: 'https://graph.microsoft.us',
graph_api_endpoint_url_api_version: 'v1.0',
},
'azure_us_government_L5' => {
resource_manager_endpoint_url: MicrosoftRestAzure::AzureEnvironments::AzureUSGovernment.resource_manager_endpoint_url,
active_directory_endpoint_url: MicrosoftRestAzure::AzureEnvironments::AzureUSGovernment.active_directory_endpoint_url,
storage_endpoint_suffix: MicrosoftRestAzure::AzureEnvironments::AzureUSGovernment.storage_endpoint_suffix,
key_vault_dns_suffix: MicrosoftRestAzure::AzureEnvironments::AzureUSGovernment.key_vault_dns_suffix,
resource_manager_endpoint_api_version: '2020-01-01',
graph_api_endpoint_url: 'https://dod-graph.microsoft.us',
graph_api_endpoint_url_api_version: 'v1.0',
},
'azure_german_cloud' => {
resource_manager_endpoint_url: MicrosoftRestAzure::AzureEnvironments::AzureGermanCloud.resource_manager_endpoint_url,
active_directory_endpoint_url: MicrosoftRestAzure::AzureEnvironments::AzureGermanCloud.active_directory_endpoint_url,
storage_endpoint_suffix: MicrosoftRestAzure::AzureEnvironments::AzureGermanCloud.storage_endpoint_suffix,
key_vault_dns_suffix: MicrosoftRestAzure::AzureEnvironments::AzureGermanCloud.key_vault_dns_suffix,
resource_manager_endpoint_api_version: '2020-01-01',
graph_api_endpoint_url: 'https://graph.microsoft.de',
graph_api_endpoint_url_api_version: 'v1.0',
},
}.freeze
# @return [String] the resource management endpoint
# Used for getting short descriptions of resources including resource_id.
attr_reader :resource_manager_endpoint_url
# @return [String] the resource management endpoint latest api version
attr_reader :resource_manager_endpoint_api_version
# @return [String] the Active Directory login endpoint
# Used for authentication.
attr_reader :active_directory_endpoint_url
# @return [String] the graph api endpoint url
attr_reader :graph_api_endpoint_url
# @return [String] the graph api endpoint api version, e.g. v1.0
attr_reader :graph_api_endpoint_api_version
# @return [String] the endpoint suffix for storage accounts
attr_reader :storage_endpoint_suffix
# @return [String] the endpoint dns suffix for key vaults
attr_reader :key_vault_dns_suffix
def initialize(options)
required_properties = %i(resource_manager_endpoint_url resource_manager_endpoint_api_version)
required_supplied_properties = required_properties & options.keys
if required_supplied_properties.nil? || required_supplied_properties.empty? \
|| (required_supplied_properties & required_properties) != required_properties
raise ArgumentError, "#{required_properties} are the required properties but provided properties are #{options}"
end
required_supplied_properties.each do |prop|
if options[prop].nil? || !options[prop].is_a?(String) || options[prop].empty?
raise ArgumentError, "Value of the '#{prop}' property must be of type String and non empty."
end
end
options.each do |k, v|
instance_variable_set("@#{k}", v) unless v.nil?
end
end
# Provide access to the endpoint properties.
def self.get_endpoint(endpoint)
options = ENDPOINTS[endpoint]
new(options)
end
end
# Make Hash return {} when accessing undefined keys.
class HashRecursive < Hash
def self.recursive
new { |hash, key| hash[key] = recursive }
end
end
module Validators
# @see https://github.com/inspec/inspec-aws/blob/master/libraries/aws_backend.rb#L209
#
# @param opts [Hash] The parameters to be validated.
# @param resource_name [String] The name of the method/resource that the parameters are validated in.
# @param allow [Array] The list of optional parameters.
# @param required [Array] The list of required parameters.
# @param require_any_of [Array] The list of parameters that at least one of them are required.
def self.validate_parameters(resource_name: nil, allow: [], required: nil, require_any_of: nil, opts: {}, skip_length: false) # rubocop:disable Metrics/ParameterLists
raise ArgumentError, "Parameters must be provided with as a Hash object. Provided #{opts.class}" unless opts.is_a?(Hash)
if required
allow += Validators.validate_params_required(resource_name, required, opts)
end
if require_any_of
allow += Validators.validate_params_require_any_of(resource_name, require_any_of, opts)
end
Validators.validate_params_allow(allow, opts, skip_length)
true
end
# @return [String] Provided parameter within require only one of parameters.
# @param require_only_one_of [Array]
def self.validate_params_only_one_of(resource_name = nil, require_only_one_of, opts)
# At least one of them has to exist.
Validators.validate_params_require_any_of(resource_name, require_only_one_of, opts)
provided = require_only_one_of.select { |i| opts.key?(i) }
if provided.size > 1
raise ArgumentError, "Either one of #{require_only_one_of} is required. Provided: #{provided}."
end
# There should be only one parameter at this point.
provided.first
end
# @return [Array] Required parameters
# @param required [Array]
def self.validate_params_required(resource_name = nil, required, opts)
raise ArgumentError, "#{resource_name}: `#{required.uniq - opts.keys.uniq}` must be provided" unless opts.is_a?(Hash) && required.all? { |req| opts.key?(req) && !opts[req].nil? && opts[req] != '' }
required
end
# @return [Array] Require any of parameters
# @param require_any_of [Array]
def self.validate_params_require_any_of(resource_name = nil, require_any_of, opts)
raise ArgumentError, "#{resource_name}: One of `#{require_any_of}` must be provided." unless opts.is_a?(Hash) && require_any_of.any? { |req| opts.key?(req) && !opts[req].nil? && opts[req] != '' }
require_any_of
end
# @return [Array] Allowed parameters
# @param allow [Array]
def self.validate_params_allow(allow, opts, skip_length = false) # rubocop:disable Style/OptionalBooleanParameter TODO: Fix this.
if !opts[:resource_data] && !skip_length
raise ArgumentError, 'Arguments or values can not be longer than 500 characters.' if opts.any? { |k, v| k.size > 100 || v.to_s.size > 500 } # rubocop:disable Style/SoleNestedConditional TODO: Fix this.
end
raise ArgumentError, 'Scalar arguments not supported.' unless defined?(opts.keys)
raise ArgumentError, "Unexpected arguments found: #{opts.keys.uniq - allow.uniq}" unless opts.keys.all? { |a| allow.include?(a) }
raise ArgumentError, 'Provided parameter should not be empty.' unless opts.values.all? do |a|
return true if a.instance_of?(Integer)
return true if [TrueClass, FalseClass].include?(a.class)
!a.empty?
end
end
def self.validate_resource_uri(resource_uri)
resource_uri_format = '/subscriptions/{subscription_id}/resourceGroups/{resource_group}/providers/'\
'Microsoft.Compute/virtualMachines/{resource_name}'
raise ArgumentError, "Resource URI should be in the format of #{resource_uri_format}. Found: #{resource_uri}" \
unless resource_uri.start_with?('/subscriptions/') || resource_uri.include?('providers')
end
end
module Helpers
# Convert provided data into Odata query format.
# @see
# https://www.odata.org/getting-started/basic-tutorial/
#
# This is a very simple approach, and tested with a very limited data.
# The result of the new operators should be tested thoroughly before using this method.
#
# Supported key words:
# - substring_of => substring_of_name: 'Mc'
# - starts_with => starts_with_name: 'J'
#
# @param data [Array, Hash] The data to be used in the query statement.
# @return [String] The query string in the Odata format.
#
def self.odata_query(data)
supported_types = [Hash, Array]
unless supported_types.include?(data.class)
raise ArgumentError, "Data should be #{supported_types}. Provided #{data.class}."
end
# This approach works for $filter.
if data.is_a?(Hash)
# TODO: implement 'ne' operator
query = data.each_with_object([]) do |(k, v), acc|
v = v.delete_suffix('/').delete_prefix('/')
if k.to_s.start_with?('substring_of_')
acc << "substringof('#{v}',#{k.to_s[13..-1].camelcase(:lower)})"
elsif k.to_s.start_with?('starts_with_')
acc << "startswith(#{k.to_s[12..-1].camelcase(:lower)},'#{v}')"
else
acc << "#{k.to_s.camelcase(:lower)} eq '#{v}'"
end
end.join(' and ')
end
# This works for `$select, $expand`.
if data.is_a?(Array)
query = data.join(',')
end
query
end
# Disassemble resource_id and extract the resource_group, provider and resource_provider.
#
# This is the one and only method where the `resource_provider` is defined differently from the rest.
# Example: the resource type for the virtual machines is `Microsoft.Compute/virtualMachines` in all other methods.
# However, it is divided into 2 entities here as `provider` and `resource_provider`.
# provider => `Microsoft.Compute`
# resource_provider => `virtualMachines`
#
# This can be used for acquiring the latest/default api version for a specific provider.
#
# @see https://docs.microsoft.com/en-us/rest/api/resources/resources/getbyid
#
# @return [Array] [resource_group, provider, resource_provider]
# @param resource_uri [String] The URI of the resource,
# /subscriptions/{subscription_id}/resourceGroups/{resource_group}/providers/
# Microsoft.Compute/virtualMachines/{resource_name}
def self.res_group_provider_type_from_uri(resource_uri)
Validators.validate_resource_uri(resource_uri)
subscription_resource_group, provider_resource_type = resource_uri.split('/providers/')
resource_group = subscription_resource_group.split('/').last
interim_array = provider_resource_type.split('/')
provider = interim_array[0]
# interim array can be one of two
# 1- provider/resource_provider/resource/name
# 2- provider/parent_resource_type/parent_resource_name/resource_provider/resource_name
# For the second case, the desired resource_provider is provide/parent_resource_type/resource_provider
# E.g.
# if resource_id: "/subscriptions/{subscription_id}/resourceGroups/{resource_group}/
# providers/Microsoft.Compute/virtualMachines/{vm_name}/extensions/{extension_name}"
# provider => "Microsoft.Compute"
# resource_provider => "virtualMachines/extensions"
resource_type = [interim_array[1], interim_array[3]].compact.join('/')
[resource_group, provider, resource_type]
end
# Decide whether to include preview api-versions in the api_Version list.
# If the latest stable is too old (based on the age_criteria) and there is a newer preview,
# then return the preview versions as well.
def self.normalize_api_list(age_criteria, stable_versions, preview_versions)
return stable_versions if preview_versions.empty?
return preview_versions if stable_versions.empty?
r_stable_versions = stable_versions.sort.reverse
return_list = r_stable_versions
r_preview_versions = preview_versions.sort.reverse
latest_stable_year = r_stable_versions.first[0..3].to_i
latest_preview_year = r_preview_versions.first[0..3].to_i
if latest_stable_year < (Time.now.year - age_criteria) && latest_preview_year > latest_stable_year
return_list += r_preview_versions
end
return_list.sort.reverse
end
# Deprecation message for the old resources.
def self.resource_deprecation_message(old_resource_name, new_resource_class)
"DEPRECATION: `#{old_resource_name}` uses the new resource `#{new_resource_class}` under the hood. "\
"#{old_resource_name} will be deprecated soon and it is advised to switch to the fully backward compatible new resource. "\
'Please see the documentation for the additional features available.'
end
def self.construct_url(input_list)
raise ArgumentError, "An array has to be provided. Found: #{input_list.class}." unless input_list.is_a?(Array)
input_list.each_with_object([]) { |input, list| list << input.delete_suffix('/').delete_prefix('/') }.join('/')
end
end
# Inspired from: https://gist.github.com/EdvardM/9639051
#
# Applies a given Ruby method on a given Hash keys recursively.
# E.g.: Convert all keys to snakecase
# RecursiveMethodHelper.method_recursive(hash, :snakecase)
#
# @param hash [Hash] The hash that the Ruby method will be run on its keys.
# @param method [Symbol] The Ruby method that will be called on the keys of the given Hash.
# @return [Hash] The original hash with updates keys recursively with the given Ruby method.
module RecursiveMethodHelper
def self.method_recursive(hash, method)
{}.tap do |h|
hash.each { |key, value| h[key.public_send(method.to_sym)] = RecursiveMethodHelper.transform(value, method) }
end
end
def self.transform(thing, method)
case thing
when Hash then method_recursive(thing, method)
when Array then thing.map { |v| transform(v, method) }
else; thing
end
end
end