diff --git a/.travis.yml b/.travis.yml index 3991bc22a1..d8780a278f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,9 +12,9 @@ matrix: - rvm: 2.3.0 env: LATEST_RUNTIME=true before_install: + - gem install bundler -v 1.14.1 - if [ "$LATEST_RUNTIME" == "true" ] ; then ./scripts/latest_runtime.sh ; fi script: - - gem install bundler - if [ "$INTEG_RECORDED" == "true" ] ; then bundle install --gemfile=Gemfile && bundle exec rake arm:spec ; fi - bundle exec rake arm:build - unset BUNDLE_GEMFILE diff --git a/azure_sdk/azure_sdk.gemspec b/azure_sdk/azure_sdk.gemspec index e7c0ac6935..4c6fa9abe7 100644 --- a/azure_sdk/azure_sdk.gemspec +++ b/azure_sdk/azure_sdk.gemspec @@ -70,5 +70,5 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency 'azure_mgmt_subscriptions', "~>#{version}" spec.add_runtime_dependency 'azure_mgmt_traffic_manager', "~>#{version}" spec.add_runtime_dependency 'azure_mgmt_web', "~>#{version}" - spec.add_runtime_dependency 'azure-storage', "~>0.12.1.preview" + spec.add_runtime_dependency 'azure-storage', "~>0.12.3.preview" end diff --git a/runtime/ms_rest_azure/CHANGELOG.md b/runtime/ms_rest_azure/CHANGELOG.md index ed26ce06d4..8bed22082f 100644 --- a/runtime/ms_rest_azure/CHANGELOG.md +++ b/runtime/ms_rest_azure/CHANGELOG.md @@ -1,3 +1,6 @@ +##Unreleased ms_rest_azure version 0.8.x +* Enable Managed Service Identity authentication features into ms-rest-azure runtime for azure_mgmt_* sdks.[Issue #884](https://github.com/Azure/azure-sdk-for-ruby/issues/884) [PR #889](https://github.com/Azure/azure-sdk-for-ruby/pull/889) + ##2017.07.10 ms_rest_azure version 0.8.1 * [Bug Fix] Fixed the issue with the polling status object to handle the response code and provisioning status correctly.[Issue #817](https://github.com/Azure/azure-sdk-for-ruby/issues/817) [PR #828](https://github.com/Azure/azure-sdk-for-ruby/pull/828) diff --git a/runtime/ms_rest_azure/lib/ms_rest_azure.rb b/runtime/ms_rest_azure/lib/ms_rest_azure.rb index 07183432b7..d06734f98e 100644 --- a/runtime/ms_rest_azure/lib/ms_rest_azure.rb +++ b/runtime/ms_rest_azure/lib/ms_rest_azure.rb @@ -11,6 +11,7 @@ require 'ms_rest_azure/azure_service_client.rb' require 'ms_rest_azure/cloud_error_data.rb' require 'ms_rest_azure/credentials/application_token_provider.rb' +require 'ms_rest_azure/credentials/msi_token_provider.rb' require 'ms_rest_azure/polling_state.rb' require 'ms_rest_azure/sub_resource.rb' require 'ms_rest_azure/resource.rb' diff --git a/runtime/ms_rest_azure/lib/ms_rest_azure/credentials/application_token_provider.rb b/runtime/ms_rest_azure/lib/ms_rest_azure/credentials/application_token_provider.rb index df119dc779..036193ead0 100644 --- a/runtime/ms_rest_azure/lib/ms_rest_azure/credentials/application_token_provider.rb +++ b/runtime/ms_rest_azure/lib/ms_rest_azure/credentials/application_token_provider.rb @@ -45,7 +45,7 @@ class ApplicationTokenProvider < MsRest::TokenProvider # @param tenant_id [String] tenant id (also known as domain). # @param client_id [String] client id. # @param client_secret [String] client secret. - # @param settings [ActiveDirectoryServiceSettings] client secret. + # @param settings [ActiveDirectoryServiceSettings] active directory setting. def initialize(tenant_id, client_id, client_secret, settings = ActiveDirectoryServiceSettings.get_azure_settings) fail ArgumentError, 'Tenant id cannot be nil' if tenant_id.nil? fail ArgumentError, 'Client id cannot be nil' if client_id.nil? @@ -81,7 +81,7 @@ def token_expired end # - # Retrieves a new authenticaion token. + # Retrieves a new authentication token. # # @return [String] new authentication token. def acquire_token diff --git a/runtime/ms_rest_azure/lib/ms_rest_azure/credentials/msi_token_provider.rb b/runtime/ms_rest_azure/lib/ms_rest_azure/credentials/msi_token_provider.rb new file mode 100644 index 0000000000..f81785d4b0 --- /dev/null +++ b/runtime/ms_rest_azure/lib/ms_rest_azure/credentials/msi_token_provider.rb @@ -0,0 +1,112 @@ +# encoding: utf-8 +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. + +module MsRestAzure + # + # Class that provides access to authentication token via Managed Service Identity. + # + class MSITokenProvider < MsRest::TokenProvider + + private + + TOKEN_ACQUIRE_URL = 'http://localhost:{port}/oauth2/token' + REQUEST_BODY_PATTERN = 'authority={authentication_endpoint}{tenant_id}&resource={resource_uri}' + DEFAULT_SCHEME = 'Bearer' + + # @return [MSIActiveDirectoryServiceSettings] settings. + attr_accessor :settings + + # @return [String] tenant id (also known as domain). + attr_accessor :tenant_id + + # @return [Integer] port number where MSI service is running. + attr_accessor :port + + # @return [String] auth token. + attr_accessor :token + + # @return [Time] the date when the current token expires. + attr_accessor :token_expires_on + + # @return [Integer] the amount of time we refresh token before it expires. + attr_reader :expiration_threshold + + # @return [String] the type of token. + attr_reader :token_type + + public + + # + # Creates and initialize new instance of the MSITokenProvider class. + # @param tenant_id [String] tenant id (also known as domain). + # @param port [Integer] port number where MSI service is running. + # @param settings [ActiveDirectoryServiceSettings] active directory setting. + def initialize(tenant_id, port = 50342, settings = ActiveDirectoryServiceSettings.get_azure_settings) + fail ArgumentError, 'Tenant id cannot be nil' if tenant_id.nil? + fail ArgumentError, 'Port cannot be nil' if port.nil? + fail ArgumentError, 'Port must be an Integer' unless port.is_a? Integer + fail ArgumentError, 'Azure AD settings cannot be nil' if settings.nil? + + @tenant_id = tenant_id + @port = port + @settings = settings + + @expiration_threshold = 5 * 60 + end + + # + # Returns the string value which needs to be attached + # to HTTP request header in order to be authorized. + # + # @return [String] authentication headers. + def get_authentication_header + acquire_token if token_expired + "#{token_type} #{token}" + end + + private + + # + # Checks whether token is about to expire. + # + # @return [Bool] True if token is about to expire, false otherwise. + def token_expired + @token.nil? || Time.now >= @token_expires_on + expiration_threshold + end + + # + # Retrieves a new authentication token. + # + # @return [String] new authentication token. + def acquire_token + token_acquire_url = TOKEN_ACQUIRE_URL.dup + token_acquire_url['{port}'] = @port.to_s + + url = URI.parse(token_acquire_url) + + connection = Faraday.new(:url => url, :ssl => MsRest.ssl_options) do |builder| + builder.adapter Faraday.default_adapter + end + + request_body = REQUEST_BODY_PATTERN.dup + request_body['{authentication_endpoint}'] = ERB::Util.url_encode(@settings.authentication_endpoint) + request_body['{tenant_id}'] = ERB::Util.url_encode(@tenant_id) + request_body['{resource_uri}'] = ERB::Util.url_encode(@settings.token_audience) + + response = connection.post do |request| + request.headers['content-type'] = 'application/x-www-form-urlencoded' + request.body = request_body + end + + fail AzureOperationError, + 'Couldn\'t acquire access token from Managed Service Identity, please verify your tenant id, port and settings' unless response.status == 200 + + response_body = JSON.load(response.body) + @token = response_body['access_token'] + @token_expires_on = Time.at(Integer(response_body['expires_on'])) + @token_type = response_body['token_type'] + end + end + +end diff --git a/runtime/ms_rest_azure/spec/msi_token_provider_spec.rb b/runtime/ms_rest_azure/spec/msi_token_provider_spec.rb new file mode 100644 index 0000000000..7d901fc8bb --- /dev/null +++ b/runtime/ms_rest_azure/spec/msi_token_provider_spec.rb @@ -0,0 +1,46 @@ +# encoding: utf-8 +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. + +require 'rspec' +require 'ms_rest_azure' + +module MsRestAzure + + describe MSITokenProvider do + it 'should throw error if nil data is passed into constructor' do + expect { MSITokenProvider.new(nil) }.to raise_error(ArgumentError) + expect { MSITokenProvider.new('tenant_id',nil) }.to raise_error(ArgumentError) + expect { MSITokenProvider.new('tenant_id','port') }.to raise_error(ArgumentError) + expect { MSITokenProvider.new('tenant_id',50431,nil) }.to raise_error(ArgumentError) + end + + it 'should set defaults for managed service identity' do + tenant = 'xxxx-xxxx-xxxxx-xxxxx' + azure_cloud = MsRestAzure::AzureEnvironments::AzureCloud + + token_provider = MSITokenProvider.new(tenant) + expect(token_provider.send(:tenant_id)).to eq(tenant) + expect(token_provider.send(:port)).to eq(50342) + settings = token_provider.send(:settings) + expect(settings.authentication_endpoint).to eq(azure_cloud.active_directory_endpoint_url) + expect(settings.token_audience).to eq(azure_cloud.active_directory_resource_id) + end + + it 'should set customs for managed service identity' do + tenant = 'xxxx-xxxx-xxxxx-xxxxx' + port = 50333 + settings = ActiveDirectoryServiceSettings.new() + settings.authentication_endpoint = 'https://login.microsoftonline.com/' + settings.token_audience = 'https://vault.azure.net' + + token_provider = MSITokenProvider.new(tenant, port, settings) + expect(token_provider.send(:tenant_id)).to eq(tenant) + expect(token_provider.send(:port)).to eq(port) + settings = token_provider.send(:settings) + expect(settings.authentication_endpoint).to eq(settings.authentication_endpoint) + expect(settings.token_audience).to eq(settings.token_audience) + end + end + +end