diff --git a/lib/train/transports/azure.rb b/lib/train/transports/azure.rb index 9549a79f..914e1183 100644 --- a/lib/train/transports/azure.rb +++ b/lib/train/transports/azure.rb @@ -3,9 +3,9 @@ require 'train/plugins' require 'ms_rest_azure' require 'azure_mgmt_resources' -require 'inifile' require 'socket' require 'timeout' +require 'train/transports/helpers/azure/file_credentials' module Train::Transports class Azure < Train.plugin(1) @@ -23,7 +23,7 @@ def connection(_ = nil) @connection ||= Connection.new(@options) end - class Connection < BaseConnection # rubocop:disable Metrics/ClassLength + class Connection < BaseConnection attr_reader :options def initialize(options) @@ -38,7 +38,7 @@ def initialize(options) @cache[:api_call] = {} if @options[:client_secret].nil? && @options[:client_id].nil? - parse_credentials_file + @options.merge!(Helpers::Azure::FileCredentials.parse(@options)) end @options[:msi_port] = @options[:msi_port].to_i unless @options[:msi_port].nil? @@ -149,40 +149,6 @@ def port_open?(port, seconds = 1) rescue Timeout::Error false end - - def parse_credentials_file # rubocop:disable Metrics/AbcSize - # If an AZURE_CRED_FILE environment variable has been specified set the - # the credentials file to that, otherwise set the one in home - azure_creds_file = @options[:credentials_file] - azure_creds_file = File.join(Dir.home, '.azure', 'credentials') if azure_creds_file.nil? - return unless File.readable?(azure_creds_file) - - credentials = IniFile.load(File.expand_path(azure_creds_file)) - if @options[:subscription_id] - id = @options[:subscription_id] - elsif !ENV['AZURE_SUBSCRIPTION_NUMBER'].nil? - subscription_number = ENV['AZURE_SUBSCRIPTION_NUMBER'].to_i - - # Check that the specified index is not greater than the number of subscriptions - if subscription_number > credentials.sections.length - raise format( - 'Your credentials file only contains %s subscriptions. You specified number %s.', - @credentials.sections.length, - subscription_number, - ) - end - id = credentials.sections[subscription_number - 1] - else - raise 'Multiple credentials detected, please set the AZURE_SUBSCRIPTION_ID environment variable.' if credentials.sections.count > 1 - id = credentials.sections[0] - end - - raise "No credentials found for subscription number #{id}" if credentials.sections.empty? || credentials[id].empty? - @options[:subscription_id] = id - @options[:tenant_id] = credentials[id]['tenant_id'] - @options[:client_id] = credentials[id]['client_id'] - @options[:client_secret] = credentials[id]['client_secret'] - end end end end diff --git a/lib/train/transports/helpers/azure/file_credentials.rb b/lib/train/transports/helpers/azure/file_credentials.rb new file mode 100644 index 00000000..187cf181 --- /dev/null +++ b/lib/train/transports/helpers/azure/file_credentials.rb @@ -0,0 +1,42 @@ +# encoding: utf-8 + +require 'inifile' +require 'train/transports/helpers/azure/file_parser' +require 'train/transports/helpers/azure/subscription_number_file_parser' +require 'train/transports/helpers/azure/subscription_id_file_parser' + +module Train::Transports + module Helpers + module Azure + class FileCredentials + DEFAULT_FILE = ::File.join(Dir.home, '.azure', 'credentials') + + def self.parse(subscription_id: nil, credentials_file: DEFAULT_FILE, **_) + return {} unless ::File.readable?(credentials_file) + credentials = IniFile.load(::File.expand_path(credentials_file)) + subscription_id = parser(subscription_id, ENV['AZURE_SUBSCRIPTION_NUMBER'], credentials).subscription_id + creds(subscription_id, credentials) + end + + def self.parser(subscription_id, subscription_number, credentials) + if subscription_id + SubscriptionIdFileParser.new(subscription_id, credentials) + elsif !subscription_number.nil? + SubscriptionNumberFileParser.new(subscription_number.to_i, credentials) + else + FileParser.new(credentials) + end + end + + def self.creds(subscription_id, credentials) + { + subscription_id: subscription_id, + tenant_id: credentials[subscription_id]['tenant_id'], + client_id: credentials[subscription_id]['client_id'], + client_secret: credentials[subscription_id]['client_secret'], + } + end + end + end + end +end diff --git a/lib/train/transports/helpers/azure/file_parser.rb b/lib/train/transports/helpers/azure/file_parser.rb new file mode 100644 index 00000000..33a87fb8 --- /dev/null +++ b/lib/train/transports/helpers/azure/file_parser.rb @@ -0,0 +1,25 @@ +# encoding: utf-8 + +module Train::Transports + module Helpers + module Azure + class FileParser + def initialize(credentials) + @credentials = credentials + + validate! + end + + def validate! + return if @credentials.sections.count == 1 + + raise 'Credentials file must have one entry. Check your credentials file. If you have more than one entry set AZURE_SUBSCRIPTION_ID environment variable.' + end + + def subscription_id + @subscription_id ||= @credentials.sections[0] + end + end + end + end +end diff --git a/lib/train/transports/helpers/azure/subscription_id_file_parser.rb b/lib/train/transports/helpers/azure/subscription_id_file_parser.rb new file mode 100644 index 00000000..fd8918d8 --- /dev/null +++ b/lib/train/transports/helpers/azure/subscription_id_file_parser.rb @@ -0,0 +1,24 @@ +# encoding: utf-8 + +module Train::Transports + module Helpers + module Azure + class SubscriptionIdFileParser + attr_reader :subscription_id + + def initialize(subscription_id, credentials) + @subscription_id = subscription_id + @credentials = credentials + + validate! + end + + def validate! + if @credentials.sections.empty? || @credentials[subscription_id].empty? + raise "No credentials found for subscription number #{subscription_id}" + end + end + end + end + end +end diff --git a/lib/train/transports/helpers/azure/subscription_number_file_parser.rb b/lib/train/transports/helpers/azure/subscription_number_file_parser.rb new file mode 100644 index 00000000..cbc102ea --- /dev/null +++ b/lib/train/transports/helpers/azure/subscription_number_file_parser.rb @@ -0,0 +1,30 @@ +# encoding: utf-8 + +module Train::Transports + module Helpers + module Azure + class SubscriptionNumberFileParser + def initialize(index, credentials) + @index = index + @credentials = credentials + + validate! + end + + def validate! + if @index == 0 + raise 'Index must be greater than 0.' + end + + if @index > @credentials.sections.length + raise "Your credentials file only contains #{@credentials.sections.length} subscriptions. You specified number #{@index}." + end + end + + def subscription_id + @subscription_id ||= @credentials.sections[@index - 1] + end + end + end + end +end diff --git a/test/unit/transports/azure_test.rb b/test/unit/transports/azure_test.rb index 151c0e1c..062df24d 100644 --- a/test/unit/transports/azure_test.rb +++ b/test/unit/transports/azure_test.rb @@ -122,59 +122,4 @@ def initialize(hash) connection.unique_identifier.must_equal 'test_tenant_id' end end - - describe 'parse_credentials_file' do - let(:cred_file) do - require 'tempfile' - file = Tempfile.new('cred_file') - info = <<-INFO - [my_subscription_id] - client_id = "my_client_id" - client_secret = "my_client_secret" - tenant_id = "my_tenant_id" - - [my_subscription_id2] - client_id = "my_client_id2" - client_secret = "my_client_secret2" - tenant_id = "my_tenant_id2" - INFO - file.write(info) - file.close - file - end - - it 'validate credentials from file' do - options[:credentials_file] = cred_file.path - options[:subscription_id] = 'my_subscription_id' - connection.send(:parse_credentials_file) - - options[:tenant_id].must_equal 'my_tenant_id' - options[:client_id].must_equal 'my_client_id' - options[:client_secret].must_equal 'my_client_secret' - options[:subscription_id].must_equal 'my_subscription_id' - end - - it 'validate credentials from file subscription override' do - options[:credentials_file] = cred_file.path - options[:subscription_id] = 'my_subscription_id2' - connection.send(:parse_credentials_file) - - options[:tenant_id].must_equal 'my_tenant_id2' - options[:client_id].must_equal 'my_client_id2' - options[:client_secret].must_equal 'my_client_secret2' - options[:subscription_id].must_equal 'my_subscription_id2' - end - - it 'validate credentials from file subscription index' do - options[:credentials_file] = cred_file.path - options[:subscription_id] = nil - ENV['AZURE_SUBSCRIPTION_NUMBER'] = '2' - connection.send(:parse_credentials_file) - - options[:tenant_id].must_equal 'my_tenant_id2' - options[:client_id].must_equal 'my_client_id2' - options[:client_secret].must_equal 'my_client_secret2' - options[:subscription_id].must_equal 'my_subscription_id2' - end - end end diff --git a/test/unit/transports/helpers/azure/file_credentials_test.rb b/test/unit/transports/helpers/azure/file_credentials_test.rb new file mode 100644 index 00000000..b085afae --- /dev/null +++ b/test/unit/transports/helpers/azure/file_credentials_test.rb @@ -0,0 +1,121 @@ +# encoding: utf-8 + +require 'helper' +require 'tempfile' +require 'train/transports/helpers/azure/file_credentials' + +describe 'parse_credentials_file' do + let(:cred_file_single_entry) do + file = Tempfile.new('cred_file') + info = <<-INFO + [my_subscription_id] + client_id = "my_client_id" + client_secret = "my_client_secret" + tenant_id = "my_tenant_id" + INFO + file.write(info) + file.close + file + end + + let(:cred_file_multiple_entries) do + file = Tempfile.new('cred_file') + info = <<-INFO + [my_subscription_id] + client_id = "my_client_id" + client_secret = "my_client_secret" + tenant_id = "my_tenant_id" + + [my_subscription_id2] + client_id = "my_client_id2" + client_secret = "my_client_secret2" + tenant_id = "my_tenant_id2" + INFO + file.write(info) + file.close + file + end + + let(:options) { { credentials_file: cred_file_multiple_entries.path } } + + it 'returns empty hash when no credentials file detected' do + result = Train::Transports::Helpers::Azure::FileCredentials.parse({}) + + assert_empty(result) + end + + it 'loads only entry from file when no subscription id given' do + options[:credentials_file] = cred_file_single_entry.path + + result = Train::Transports::Helpers::Azure::FileCredentials.parse(options) + + assert_equal('my_tenant_id', result[:tenant_id]) + assert_equal('my_client_id', result[:client_id]) + assert_equal('my_client_secret', result[:client_secret]) + assert_equal('my_subscription_id', result[:subscription_id]) + end + + it 'raises an error when no subscription id given and multiple entries' do + error = assert_raises RuntimeError do + Train::Transports::Helpers::Azure::FileCredentials.parse(options) + end + + assert_equal('Credentials file must have one entry. Check your credentials file. If you have more than one entry set AZURE_SUBSCRIPTION_ID environment variable.', error.message) + end + + it 'loads entry when subscription id is given' do + options[:subscription_id] = 'my_subscription_id' + + result = Train::Transports::Helpers::Azure::FileCredentials.parse(options) + + assert_equal('my_tenant_id', result[:tenant_id]) + assert_equal('my_client_id', result[:client_id]) + assert_equal('my_client_secret', result[:client_secret]) + assert_equal('my_subscription_id', result[:subscription_id]) + end + + it 'raises an error when subscription id not found' do + options[:subscription_id] = 'missing_subscription_id' + + error = assert_raises RuntimeError do + Train::Transports::Helpers::Azure::FileCredentials.parse(options) + end + + assert_equal('No credentials found for subscription number missing_subscription_id', error.message) + end + + it 'loads entry based on index' do + ENV['AZURE_SUBSCRIPTION_NUMBER'] = '2' + + result = Train::Transports::Helpers::Azure::FileCredentials.parse(options) + + ENV.delete('AZURE_SUBSCRIPTION_NUMBER') + + assert_equal('my_tenant_id2', result[:tenant_id]) + assert_equal('my_client_id2', result[:client_id]) + assert_equal('my_client_secret2', result[:client_secret]) + assert_equal('my_subscription_id2', result[:subscription_id]) + end + + it 'raises an error when index is out of bounds' do + ENV['AZURE_SUBSCRIPTION_NUMBER'] = '3' + + error = assert_raises RuntimeError do + Train::Transports::Helpers::Azure::FileCredentials.parse(options) + end + ENV.delete('AZURE_SUBSCRIPTION_NUMBER') + + assert_equal('Your credentials file only contains 2 subscriptions. You specified number 3.', error.message) + end + + it 'raises an error when index 0 is given' do + ENV['AZURE_SUBSCRIPTION_NUMBER'] = '0' + + error = assert_raises RuntimeError do + Train::Transports::Helpers::Azure::FileCredentials.parse(options) + end + ENV.delete('AZURE_SUBSCRIPTION_NUMBER') + + assert_equal('Index must be greater than 0.', error.message) + end +end