azure-credentials 0.1.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d531fc1840c126f12cd8b6621738283dab8f0ed2
4
+ data.tar.gz: a8a8fca37836e2843346d32aff8cd1eed4b8b7e8
5
+ SHA512:
6
+ metadata.gz: d2735f8b0437ce5df4c8dde431b8eabdb8fd3a85c458ce8f6cab0fb19ceba4342f8837bf441a087cd8724de790faa8c9f7aa05aa92a8a0c20dd9e70a4e60b54f
7
+ data.tar.gz: 0c6cf03b9f6a8ff7a4a5f506faa785a44838278753a9c168345679b08ff22b35e07e8ce0cb163a58208951ff7c0b9727f6d0b85290691b93f1c7bad8d768bb1f
data/LICENSE ADDED
@@ -0,0 +1,14 @@
1
+
2
+ Copyright 2016 Pendrica Ltd
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
@@ -0,0 +1,4 @@
1
+ # azure-credentials
2
+
3
+ A little assistance for those struggling to create Service Principals in Azure.
4
+
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'azure/utility/credentials'
4
+ Azure::Utility::Credentials.new
@@ -0,0 +1,372 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+ require 'json'
4
+ require 'securerandom'
5
+ require 'time'
6
+ require 'logger'
7
+ require 'mixlib/cli'
8
+
9
+ module Azure
10
+ module Utility
11
+ #
12
+ # Options
13
+ #
14
+ class Options
15
+ include Mixlib::CLI
16
+
17
+ option :username,
18
+ short: '-u',
19
+ long: '--username USERNAME',
20
+ description: 'Enter the username (must be an Azure AD user)',
21
+ required: false
22
+
23
+ option :password,
24
+ short: '-p',
25
+ long: '--password PASSWORD',
26
+ description: 'Enter the password for the Azure AD user',
27
+ required: false
28
+
29
+ option :subscription_id,
30
+ short: '-s',
31
+ long: '--subscription ID',
32
+ description: 'Enter the Subscription ID to work against (default: process all subscriptions within the Azure tenant)',
33
+ required: false,
34
+ default: nil
35
+
36
+ option :role,
37
+ short: '-r',
38
+ long: '--role ROLENAME',
39
+ description: 'Enter the built-in Azure role to add the service principal to on your subscription (default: Contributor)',
40
+ in: %w(Contributor Owner),
41
+ default: 'Contributor',
42
+ required: false
43
+
44
+ option :type,
45
+ short: '-t',
46
+ long: '--type OUTPUTTYPE',
47
+ description: 'Set the output type (default: chef)',
48
+ in: %w(chef puppet terraform generic),
49
+ required: false,
50
+ default: 'chef'
51
+
52
+ option :log_level,
53
+ short: '-l',
54
+ long: '--log_level LEVEL',
55
+ description: 'Set the log level (debug, info, warn, error, fatal)',
56
+ default: :info,
57
+ required: false,
58
+ in: %w(debug info warn error fatal),
59
+ proc: proc { |l| l.to_sym }
60
+
61
+ option :output_file,
62
+ short: '-o',
63
+ long: '--output FILENAME',
64
+ description: 'Enter the filename to save the credentials to',
65
+ default: './credentials',
66
+ required: false
67
+
68
+ option :out_to_screen,
69
+ short: '-v',
70
+ long: '--verbose',
71
+ description: 'Display the credentials in STDOUT after creation? (warning: will contain secrets)',
72
+ default: false,
73
+ required: false
74
+
75
+ option :help,
76
+ short: '-h',
77
+ long: '--help',
78
+ description: 'Show this message',
79
+ on: :tail,
80
+ boolean: true,
81
+ show_options: true,
82
+ exit: 0
83
+ end
84
+
85
+ #
86
+ # Logger
87
+ #
88
+ class CustomLogger
89
+ def self.log
90
+ if @logger.nil?
91
+ cli = Options.new
92
+ cli.parse_options
93
+ @logger = Logger.new STDOUT
94
+ @logger.level = logger_level_for(cli.config[:log_level])
95
+ @logger.formatter = proc do |severity, datetime, _progname, msg|
96
+ "#{severity} [#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{msg}\n"
97
+ end
98
+ end
99
+ @logger
100
+ end
101
+
102
+ def self.logger_level_for(sym)
103
+ case sym
104
+ when :debug
105
+ return Logger::DEBUG
106
+ when :info
107
+ return Logger::INFO
108
+ when :warn
109
+ return Logger::WARN
110
+ when :error
111
+ return Logger::ERROR
112
+ when :fatal
113
+ return Logger::FATAL
114
+ end
115
+ end
116
+ end
117
+
118
+ #
119
+ # Credentials
120
+ #
121
+ class Credentials
122
+ AZURE_SERVICE_PRINCIPAL = '1950a258-227b-4e31-a9cf-717495945fc2'.freeze
123
+ CONFIG_PATH = "#{ENV['HOME']}/.azure/credentials".freeze
124
+
125
+ def initialize
126
+ cli = Options.new
127
+ cli.parse_options
128
+ CustomLogger.log.debug "Command line options: #{cli.config.inspect}"
129
+
130
+ username = cli.config[:username] || username_stdin
131
+ password = cli.config[:password] || password_stdin
132
+
133
+ # Get Bearer token for user and pass through to main method
134
+ token = azure_authenticate(username, password)
135
+ if token.nil?
136
+ error_message = 'Unable to acquire token from Azure AD provider.'
137
+ CustomLogger.log.error error_message
138
+ raise error_message
139
+ end
140
+ created_credentials = create_all_objects(token, cli.config)
141
+ CustomLogger.log.debug "Credential details: #{created_credentials.inspect}"
142
+ create_file(created_credentials, cli.config)
143
+ CustomLogger.log.info 'Done!'
144
+ end
145
+
146
+ def username_stdin
147
+ print 'Enter your Azure AD username (user@domain.com): '
148
+ STDIN.gets.chomp
149
+ end
150
+
151
+ def password_stdin
152
+ print 'Enter your password: '
153
+ STDIN.noecho(&:gets).chomp
154
+ end
155
+
156
+ def create_file(created_credentials, config)
157
+ file_name = config[:output_file] || './credentials'
158
+ file_name_expanded = File.expand_path(file_name)
159
+ CustomLogger.log.info "Creating credentials file at #{file_name_expanded}"
160
+ output = ''
161
+
162
+ style = config[:type] || 'chef'
163
+ case style
164
+ when 'chef' # ref: https://github.com/pendrica/chef-provisioning-azurerm#configuration
165
+ created_credentials.each do |s|
166
+ subscription_template = <<-EOH
167
+ [#{s[:subscription_id]}]
168
+ client_id = "#{s[:client_id]}"
169
+ client_secret = "#{s[:client_secret]}"
170
+ tenant_id = "#{s[:tenant_id]}"
171
+
172
+ EOH
173
+ output += subscription_template
174
+ end
175
+ when 'terraform' # ref: https://www.terraform.io/docs/providers/azurerm/index.html
176
+ created_credentials.each do |s|
177
+ subscription_template = <<-EOH
178
+ provider "azurerm" {
179
+ subscription_id = "#{s[:subscription_id]}"
180
+ client_id = "#{s[:client_id]}"
181
+ client_secret = "#{s[:client_secret]}"
182
+ tenant_id = "#{s[:tenant_id]}"
183
+ }
184
+
185
+ EOH
186
+ output += subscription_template
187
+ end
188
+ when 'puppet' # ref: https://github.com/puppetlabs/puppetlabs-azure#installing-the-azure-module
189
+ created_credentials.each do |s|
190
+ subscription_template = <<-EOH
191
+ azure: {
192
+ subscription_id: "#{s[:subscription_id]}"
193
+ tenant_id: '#{s[:tenant_id]}'
194
+ client_id: '#{s[:client_id]}'
195
+ client_secret: '#{s[:client_secret]}'
196
+ }
197
+
198
+ EOH
199
+ output += subscription_template
200
+ end
201
+ else # generic credentials output
202
+ created_credentials.each do |s|
203
+ subscription_template = <<-EOH
204
+ azure_subscription_id = "#{s[:subscription_id]}"
205
+ azure_tenant_id = "#{s[:tenant_id]}"
206
+ azure_client_id = "#{s[:client_id]}"
207
+ azure_client_secret = "#{s[:client_secret]}"
208
+
209
+ EOH
210
+ output += subscription_template
211
+ end
212
+ end
213
+ File.open(file_name_expanded, 'w') do |file|
214
+ file.write(output)
215
+ end
216
+ puts output if config[:out_to_screen]
217
+ end
218
+
219
+ def create_all_objects(token, config)
220
+ tenant_id = get_tenant_id(token).first['tenantId']
221
+ subscriptions = Array(config[:subscription_id])
222
+ subscriptions = get_subscriptions(token) if subscriptions.empty?
223
+ identifier = SecureRandom.hex(2)
224
+ credentials = []
225
+ subscriptions.each do |subscription|
226
+ new_application_name = "azure_#{identifier}_#{subscription}"
227
+ new_client_secret = SecureRandom.urlsafe_base64(16, true)
228
+ application_id = create_application(tenant_id, token, new_application_name, new_client_secret)['appId']
229
+ service_principal_object_id = create_service_principal(tenant_id, token, application_id)['objectId']
230
+ role_name = config[:role] || 'Contributor'
231
+ role_definition_id = get_role_definition(subscription, token, role_name).first['id']
232
+ success = false
233
+ counter = 0
234
+ until success || counter > 5
235
+ counter += 1
236
+ CustomLogger.log.info "Waiting for service principal to be available in directory (retry #{counter})"
237
+ sleep 2
238
+ assigned_role = assign_service_principal_to_role_id(subscription, token, service_principal_object_id, role_definition_id)
239
+ success = true unless assigned_role['error']
240
+ end
241
+ raise 'Failed to assign Service Principal to Role' unless success
242
+ CustomLogger.log.info "Assigned service principal to role #{role_name} in subscription #{subscription}"
243
+ new_credentials = {}
244
+ new_credentials[:subscription_id] = subscription
245
+ new_credentials[:client_id] = application_id
246
+ new_credentials[:client_secret] = new_client_secret
247
+ new_credentials[:tenant_id] = tenant_id
248
+ credentials.push(new_credentials)
249
+ end
250
+ credentials
251
+ end
252
+
253
+ def get_subscriptions(token)
254
+ CustomLogger.log.info 'Retrieving subscriptions info'
255
+ subscriptions = []
256
+ subscriptions_call = azure_call(:get, 'https://management.azure.com/subscriptions?api-version=2015-01-01', nil, token)
257
+ subscriptions_call['value'].each do |subscription|
258
+ subscriptions.push subscription['subscriptionId']
259
+ end
260
+ CustomLogger.log.debug "SubscriptionIDs returned: #{subscriptions.inspect}"
261
+ subscriptions
262
+ end
263
+
264
+ def get_tenant_id(token)
265
+ CustomLogger.log.info 'Retrieving tenant info'
266
+ tenants = azure_call(:get, 'https://management.azure.com/tenants?api-version=2015-01-01', nil, token)
267
+ tenants['value']
268
+ end
269
+
270
+ def create_application(tenant_id, token, new_application_name, new_client_secret)
271
+ CustomLogger.log.info "Creating application #{new_application_name} in tenant #{tenant_id}"
272
+ url = "https://graph.windows.net/#{tenant_id}/applications?api-version=1.42-previewInternal"
273
+ payload_json = <<-EOH
274
+ {
275
+ "availableToOtherTenants": false,
276
+ "displayName": "#{new_application_name}",
277
+ "homepage": "https://management.core.windows.net",
278
+ "identifierUris": [
279
+ "https://#{tenant_id}/#{new_application_name}"
280
+ ],
281
+ "passwordCredentials": [
282
+ {
283
+ "startDate": "#{Time.now.utc.iso8601}",
284
+ "endDate": "#{(Time.now + (24 * 60 * 60 * 365 * 10)).utc.iso8601}",
285
+ "keyId": "#{SecureRandom.uuid}",
286
+ "value": "#{new_client_secret}"
287
+ }
288
+ ]
289
+ }
290
+ EOH
291
+ azure_call(:post, url, payload_json, token)
292
+ end
293
+
294
+ def create_service_principal(tenant_id, token, application_id)
295
+ CustomLogger.log.info 'Creating service principal for application'
296
+ url = "https://graph.windows.net/#{tenant_id}/servicePrincipals?api-version=1.42-previewInternal"
297
+ payload_json = <<-EOH
298
+ {
299
+ "appId": "#{application_id}",
300
+ "accountEnabled": true
301
+ }
302
+ EOH
303
+ azure_call(:post, url, payload_json, token)
304
+ end
305
+
306
+ def assign_service_principal_to_role_id(subscription_id, token, service_principal_object_id, role_definition_id)
307
+ CustomLogger.log.info 'Attempting to assign service principal to role'
308
+ url = "https://management.azure.com/subscriptions/#{subscription_id}/providers/Microsoft.Authorization/roleAssignments/#{service_principal_object_id}?api-version=2015-07-01"
309
+ payload_json = <<-EOH
310
+ {
311
+ "properties": {
312
+ "roleDefinitionId": "#{role_definition_id}",
313
+ "principalId": "#{service_principal_object_id}"
314
+ }
315
+ }
316
+ EOH
317
+ azure_call(:put, url, payload_json, token)
318
+ end
319
+
320
+ def get_role_definition(tenant_id, token, role_name)
321
+ 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)
322
+ role_definitions['value']
323
+ end
324
+
325
+ def azure_authenticate(username, password)
326
+ CustomLogger.log.info 'Authenticating to Azure Active Directory'
327
+ url = 'https://login.windows.net/Common/oauth2/token'
328
+ data = "resource=https%3A%2F%2Fmanagement.core.windows.net%2F&client_id=#{AZURE_SERVICE_PRINCIPAL}" \
329
+ "&grant_type=password&username=#{username}&scope=openid&password=#{password}"
330
+ response = http_post(url, data)
331
+ JSON.parse(response.body)['access_token']
332
+ end
333
+
334
+ def http_post(url, data)
335
+ uri = URI(url)
336
+ response = nil
337
+ Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
338
+ request = Net::HTTP::Post.new uri
339
+ request.body = data
340
+ response = http.request request
341
+ end
342
+ response
343
+ end
344
+
345
+ def azure_call(method, url, data, token)
346
+ uri = URI(url)
347
+ response = nil
348
+ Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
349
+ case method
350
+ when :put
351
+ request = Net::HTTP::Put.new uri
352
+ when :delete
353
+ request = Net::HTTP::Delete.new uri
354
+ when :get
355
+ request = Net::HTTP::Get.new uri
356
+ when :post
357
+ request = Net::HTTP::Post.new uri
358
+ when :patch
359
+ request = Net::HTTP::Patch.new uri
360
+ end
361
+ request.body = data
362
+ request['Authorization'] = "Bearer #{token}"
363
+ request['Content-Type'] = 'application/json'
364
+ CustomLogger.log.debug "Request: #{request.uri} (#{method}) #{data}"
365
+ response = http.request request
366
+ CustomLogger.log.debug "Response: #{response.body}"
367
+ end
368
+ JSON.parse(response.body) unless response.nil?
369
+ end
370
+ end
371
+ end
372
+ end
metadata ADDED
@@ -0,0 +1,147 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: azure-credentials
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Stuart Preston
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-03-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: json
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.8'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.8.2
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '1.8'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.8.2
33
+ - !ruby/object:Gem::Dependency
34
+ name: mixlib-cli
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1'
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 1.5.0
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '1'
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 1.5.0
53
+ - !ruby/object:Gem::Dependency
54
+ name: bundler
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '1.7'
60
+ type: :development
61
+ prerelease: false
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '1.7'
67
+ - !ruby/object:Gem::Dependency
68
+ name: rake
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '10.0'
74
+ type: :development
75
+ prerelease: false
76
+ version_requirements: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: '10.0'
81
+ - !ruby/object:Gem::Dependency
82
+ name: rubocop
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
+ - !ruby/object:Gem::Dependency
96
+ name: rspec
97
+ requirement: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - "~>"
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - "~>"
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ description: Utility to generate AzureRM credentials files in various formats using
110
+ Azure AD user credentials.
111
+ email:
112
+ - stuart@pendrica.com
113
+ executables:
114
+ - azure-credentials
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - LICENSE
119
+ - README.md
120
+ - bin/azure-credentials
121
+ - lib/azure/utility/credentials.rb
122
+ homepage: https://github.com/pendrica/azure-credentials
123
+ licenses:
124
+ - Apache-2.0
125
+ metadata: {}
126
+ post_install_message:
127
+ rdoc_options: []
128
+ require_paths:
129
+ - lib
130
+ required_ruby_version: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ version: '0'
135
+ required_rubygems_version: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ requirements: []
141
+ rubyforge_project:
142
+ rubygems_version: 2.5.2
143
+ signing_key:
144
+ specification_version: 4
145
+ summary: AzureRM credential generator
146
+ test_files: []
147
+ has_rdoc: