azure-credentials 0.1.0

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