Class: Azure::Utility::Credentials

Inherits:
Object
  • Object
show all
Defined in:
lib/azure/utility/credentials.rb

Overview

Credentials

Constant Summary collapse

AZURE_SERVICE_PRINCIPAL =
'1950a258-227b-4e31-a9cf-717495945fc2'
CONFIG_PATH =
"#{ENV['HOME']}/.azure/credentials"

Instance Method Summary collapse

Constructor Details

#initializeCredentials

Returns a new instance of Credentials.



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/azure/utility/credentials.rb', line 128

def initialize
  cli = Options.new
  cli.parse_options
  CustomLogger.log.debug "Command line options: #{cli.config.inspect}"

  username = cli.config[:username] || username_stdin
  password = cli.config[:password] || password_stdin

  # Get Bearer token for user and pass through to main method
  token = azure_authenticate(username, password)
  if token.nil?
    error_message = 'Unable to acquire token from Azure AD provider.'
    CustomLogger.log.error error_message
    raise error_message
  end
  created_credentials = create_all_objects(token, cli.config)
  CustomLogger.log.debug "Credential details: #{created_credentials.inspect}"
  create_file(created_credentials, cli.config)
  CustomLogger.log.info 'Done!'
end

Instance Method Details

#assign_service_principal_to_role_id(subscription_id, token, service_principal_object_id, role_definition_id) ⇒ Object



327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'lib/azure/utility/credentials.rb', line 327

def assign_service_principal_to_role_id(subscription_id, token, service_principal_object_id, role_definition_id)
  CustomLogger.log.info 'Attempting to assign service principal to role'
  url = "https://management.azure.com/subscriptions/#{subscription_id}/providers/Microsoft.Authorization/roleAssignments/#{service_principal_object_id}?api-version=2015-07-01"
  payload_json = <<-PAYLOADEOH
  {
      "properties": {
          "roleDefinitionId": "#{role_definition_id}",
          "principalId": "#{service_principal_object_id}"
      }
  }
  PAYLOADEOH
  azure_call(:put, url, payload_json, token)
end

#azure_authenticate(username, password) ⇒ Object



346
347
348
349
350
351
352
353
# File 'lib/azure/utility/credentials.rb', line 346

def azure_authenticate(username, password)
  CustomLogger.log.info 'Authenticating to Azure Active Directory'
  url = 'https://login.windows.net/Common/oauth2/token'
  data = "resource=https%3A%2F%2Fmanagement.core.windows.net%2F&client_id=#{AZURE_SERVICE_PRINCIPAL}" \
    "&grant_type=password&username=#{username}&scope=openid&password=#{password}"
  response = http_post(url, data)
  JSON.parse(response.body)['access_token']
end

#azure_call(method, url, data, token) ⇒ Object



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
# File 'lib/azure/utility/credentials.rb', line 368

def azure_call(method, url, data, token)
  uri = URI(url)
  response = nil
  Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
    case method
    when :put
      request = Net::HTTP::Put.new uri
    when :delete
      request = Net::HTTP::Delete.new uri
    when :get
      request = Net::HTTP::Get.new uri
    when :post
      request = Net::HTTP::Post.new uri
    when :patch
      request = Net::HTTP::Patch.new uri
    end
    request.body = data
    request['Authorization'] = "Bearer #{token}"
    request['Content-Type'] = 'application/json'
    CustomLogger.log.debug "Request: #{request.uri} (#{method}) #{data}"
    response = http.request request
    CustomLogger.log.debug "Response: #{response.body}"
  end
  JSON.parse(response.body) unless response.nil?
end

#create_all_objects(token, config) ⇒ Object



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
# File 'lib/azure/utility/credentials.rb', line 234

def create_all_objects(token, config)
  tenant_id = get_tenant_id(token).first['tenantId']
  subscriptions = Array(config[:subscription_id])
  subscriptions = get_subscriptions(token) if subscriptions.empty?
  identifier = SecureRandom.hex(2)
  credentials = []
  subscriptions.each do |subscription|
    new_application_name = "azure_#{identifier}_#{subscription}"

    if config[:type] == 'azurecli' then
      new_client_secret = SecureRandom.uuid
    else
      new_client_secret = SecureRandom.urlsafe_base64(16, true)
    end

    application_id = create_application(tenant_id, token, new_application_name, new_client_secret)['appId']
    service_principal_object_id = create_service_principal(tenant_id, token, application_id)['objectId']
    role_name = config[:role] || 'Contributor'
    role_definition_id = get_role_definition(subscription, token, role_name).first['id']
    success = false
    counter = 0
    until success || counter > 5
      counter += 1
      CustomLogger.log.info "Waiting for service principal to be available in directory (retry #{counter})"
      sleep 2
      assigned_role = assign_service_principal_to_role_id(subscription, token, service_principal_object_id, role_definition_id)
      success = true unless assigned_role['error']
    end
    raise 'Failed to assign Service Principal to Role' unless success
    CustomLogger.log.info "Assigned service principal to role #{role_name} in subscription #{subscription}"
    new_credentials = {}
    new_credentials[:subscription_id] = subscription
    new_credentials[:client_id] = application_id
    new_credentials[:client_secret] = new_client_secret
    new_credentials[:tenant_id] = tenant_id
    credentials.push(new_credentials)
  end
  credentials
end

#create_application(tenant_id, token, new_application_name, new_client_secret) ⇒ Object



291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
# File 'lib/azure/utility/credentials.rb', line 291

def create_application(tenant_id, token, new_application_name, new_client_secret)
  CustomLogger.log.info "Creating application #{new_application_name} in tenant #{tenant_id}"
  url = "https://graph.windows.net/#{tenant_id}/applications?api-version=1.42-previewInternal"
  payload_json = <<-JSONEOH
  {
      "availableToOtherTenants": false,
      "displayName": "#{new_application_name}",
      "homepage": "https://management.core.windows.net",
      "identifierUris": [
          "https://#{tenant_id}/#{new_application_name}"
      ],
      "passwordCredentials": [
          {
          "startDate": "#{Time.now.utc.iso8601}",
          "endDate": "#{(Time.now + (24 * 60 * 60 * 365 * 10)).utc.iso8601}",
          "keyId": "#{SecureRandom.uuid}",
          "value": "#{new_client_secret}"
          }
      ]
  }
  JSONEOH
  azure_call(:post, url, payload_json, token)
end

#create_file(created_credentials, config) ⇒ Object



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
# File 'lib/azure/utility/credentials.rb', line 159

def create_file(created_credentials, config)
  file_name = config[:output_file] || './credentials'
  file_name_expanded = File.expand_path(file_name)
  CustomLogger.log.info "Creating credentials file at #{file_name_expanded}"
  output = ''

  style = config[:type] || 'chef'
  case style
  when 'chef' # ref: https://github.com/pendrica/chef-provisioning-azurerm#configuration
    created_credentials.each do |s|
      subscription_template = <<~CHEFEOH
        [#{s[:subscription_id]}]
        client_id = "#{s[:client_id]}"
        client_secret = "#{s[:client_secret]}"
        tenant_id = "#{s[:tenant_id]}"

      CHEFEOH
      output += subscription_template
    end
  when 'terraform' # ref: https://www.terraform.io/docs/providers/azurerm/index.html
    created_credentials.each do |s|
      subscription_template = <<~TFEOH
        provider "azurerm" {
          subscription_id = "#{s[:subscription_id]}"
          client_id       = "#{s[:client_id]}"
          client_secret   = "#{s[:client_secret]}"
          tenant_id       = "#{s[:tenant_id]}"
        }

      TFEOH
      output += subscription_template
    end
  when 'puppet' # ref: https://github.com/puppetlabs/puppetlabs-azure#installing-the-azure-module
    created_credentials.each do |s|
      subscription_template = <<~PPEOH
        azure: {
          subscription_id: "#{s[:subscription_id]}"
          tenant_id: "#{s[:tenant_id]}"
          client_id: "#{s[:client_id]}"
          client_secret: "#{s[:client_secret]}"
        }

      PPEOH
      output += subscription_template
    end
  when 'azurecli'
    created_credentials.each do |s|
      subscription_template = <<~AZURECLIEOH
        [default]
        subscription_id=#{s[:subscription_id]}
        tenant=#{s[:tenant_id]}
        client_id=#{s[:client_id]}
        secret=#{s[:client_secret]}

      AZURECLIEOH
      output += subscription_template
    end
  else # generic credentials output
    created_credentials.each do |s|
      subscription_template = <<~GENERICEOH
        azure_subscription_id = "#{s[:subscription_id]}"
        azure_tenant_id = "#{s[:tenant_id]}"
        azure_client_id = "#{s[:client_id]}"
        azure_client_secret = "#{s[:client_secret]}"

      GENERICEOH
      output += subscription_template
    end
  end
  File.open(file_name_expanded, 'w') do |file|
    file.write(output)
  end
  puts output if config[:out_to_screen]
end

#create_service_principal(tenant_id, token, application_id) ⇒ Object



315
316
317
318
319
320
321
322
323
324
325
# File 'lib/azure/utility/credentials.rb', line 315

def create_service_principal(tenant_id, token, application_id)
  CustomLogger.log.info 'Creating service principal for application'
  url = "https://graph.windows.net/#{tenant_id}/servicePrincipals?api-version=1.42-previewInternal"
  payload_json = <<-PAYLOADEOH
  {
      "appId": "#{application_id}",
      "accountEnabled": true
  }
  PAYLOADEOH
  azure_call(:post, url, payload_json, token)
end

#get_role_definition(tenant_id, token, role_name) ⇒ Object



341
342
343
344
# File 'lib/azure/utility/credentials.rb', line 341

def get_role_definition(tenant_id, token, role_name)
  role_definitions = azure_call(:get, "https://management.azure.com/subscriptions/#{tenant_id}/providers/Microsoft.Authorization/roleDefinitions?$filter=roleName%20eq%20\'#{role_name}\'&api-version=2015-07-01", nil, token)
  role_definitions['value']
end

#get_subscriptions(token) ⇒ Object



274
275
276
277
278
279
280
281
282
283
# File 'lib/azure/utility/credentials.rb', line 274

def get_subscriptions(token)
  CustomLogger.log.info 'Retrieving subscriptions info'
  subscriptions = []
  subscriptions_call = azure_call(:get, 'https://management.azure.com/subscriptions?api-version=2015-01-01', nil, token)
  subscriptions_call['value'].each do |subscription|
    subscriptions.push subscription['subscriptionId']
  end
  CustomLogger.log.debug "SubscriptionIDs returned: #{subscriptions.inspect}"
  subscriptions
end

#get_tenant_id(token) ⇒ Object



285
286
287
288
289
# File 'lib/azure/utility/credentials.rb', line 285

def get_tenant_id(token)
  CustomLogger.log.info 'Retrieving tenant info'
  tenants = azure_call(:get, 'https://management.azure.com/tenants?api-version=2015-01-01', nil, token)
  tenants['value']
end

#http_post(url, data) ⇒ Object



355
356
357
358
359
360
361
362
363
364
365
366
# File 'lib/azure/utility/credentials.rb', line 355

def http_post(url, data)
  uri = URI(url)
  response = nil
  Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
    request = Net::HTTP::Post.new uri
    CustomLogger.log.debug "Request: #{request.uri} (#{request.method}) #{data}"
    request.body = data
    response = http.request request
    CustomLogger.log.debug "Response: #{response.body}"
  end
  response
end

#password_stdinObject



154
155
156
157
# File 'lib/azure/utility/credentials.rb', line 154

def password_stdin
  print 'Enter your password: '
  STDIN.noecho(&:gets).chomp
end

#username_stdinObject



149
150
151
152
# File 'lib/azure/utility/credentials.rb', line 149

def username_stdin
  print 'Enter your Azure AD username ([email protected]): '
  STDIN.gets.chomp
end