diff --git a/Gemfile b/Gemfile index e8d2bcab..e934acd8 100644 --- a/Gemfile +++ b/Gemfile @@ -28,6 +28,7 @@ source "https://rubygems.org" do gem "faraday_middleware", :require => false gem "nokogiri", "~> 1.10.4", :require => false + gem "adal", "~> 1.0", :require => false gem "dotenv", "~> 2.0", :require => false gem "minitest", "~> 5", :require => false gem "minitest-reporters", "~> 1", :require => false diff --git a/common/lib/azure/storage/common/core/auth/shared_access_signature_generator.rb b/common/lib/azure/storage/common/core/auth/shared_access_signature_generator.rb index 03815747..adbd00ed 100644 --- a/common/lib/azure/storage/common/core/auth/shared_access_signature_generator.rb +++ b/common/lib/azure/storage/common/core/auth/shared_access_signature_generator.rb @@ -67,8 +67,18 @@ class SharedAccessSignature ip_range: :sip } + USER_DELEGATION_KEY_MAPPINGS = { + signed_oid: :skoid, + signed_tid: :sktid, + signed_start: :skt, + signed_expiry: :ske, + signed_service: :sks, + signed_version: :skv + } + BLOB_KEY_MAPPINGS = { resource: :sr, + timestamp: :snapshot, cache_control: :rscc, content_disposition: :rscd, content_encoding: :rsce, @@ -103,13 +113,20 @@ class SharedAccessSignature # # @param account_name [String] The account name. Defaults to the one in the global configuration. # @param access_key [String] The access_key encoded in Base64. Defaults to the one in the global configuration. - def initialize(account_name = "", access_key = "") + # @param user_delegation_key [Azure::Storage::Common::UserDelegationKey] The user delegation key obtained from + # calling get_user_delegation_key after authenticating with an Azure Active Directory entity. If present, the + # SAS is signed with the user delegation key instead of the access key. + def initialize(account_name = "", access_key = "", user_delegation_key = nil) + if access_key.empty? && !user_delegation_key.nil? + access_key = user_delegation_key.value + end if account_name.empty? || access_key.empty? client = Azure::Storage::Common::Client.create_from_env account_name = client.storage_account_name if account_name.empty? access_key = client.storage_access_key if access_key.empty? end @account_name = account_name + @user_delegation_key = user_delegation_key @signer = Azure::Core::Auth::Signer.new(access_key) end @@ -131,10 +148,12 @@ def initialize(account_name = "", access_key = "") # * +:start+ - String. Optional. UTC Date/Time in ISO8601 format. # * +:expiry+ - String. Optional. UTC Date/Time in ISO8601 format. Default now + 30 minutes. # * +:identifier+ - String. Optional. Identifier for stored access policy. + # This option must be omitted if a user delegation key has been provided. # * +:protocol+ - String. Optional. Permitted protocols. # * +:ip_range+ - String. Optional. An IP address or a range of IP addresses from which to accept requests. # # Below options for blob serivce only + # * +:snapshot+ - String. Optional. UTC Date/Time in ISO8601 format. The blob snapshot to grant permission. # * +:cache_control+ - String. Optional. Response header override. # * +:content_disposition+ - String. Optional. Response header override. # * +:content_encoding+ - String. Optional. Response header override. @@ -175,12 +194,20 @@ def generate_service_sas_token(path, options = {}) valid_mappings.merge!(FILE_KEY_MAPPINGS) end + service_key_mappings = SERVICE_KEY_MAPPINGS + unless @user_delegation_key.nil? + valid_mappings.delete(:identifier) + USER_DELEGATION_KEY_MAPPINGS.each { |k, _| options[k] = @user_delegation_key.send(k) } + valid_mappings.merge!(USER_DELEGATION_KEY_MAPPINGS) + service_key_mappings = service_key_mappings.merge(USER_DELEGATION_KEY_MAPPINGS) + end + invalid_options = options.reject { |k, _| valid_mappings.key?(k) } raise Azure::Storage::Common::InvalidOptionsError, "invalid options #{invalid_options} provided for SAS token generate" if invalid_options.length > 0 canonicalize_time(options) - query_hash = Hash[options.map { |k, v| [SERVICE_KEY_MAPPINGS[k], v] }] + query_hash = Hash[options.map { |k, v| [service_key_mappings[k], v] }] .reject { |k, v| SERVICE_OPTIONAL_QUERY_PARAMS.include?(k) && v.to_s == "" } .merge(sig: @signer.sign(signable_string_for_service(service_type, path, options))) @@ -197,13 +224,33 @@ def signable_string_for_service(service_type, path, options) options[:permissions], options[:start], options[:expiry], - canonicalized_resource(service_type, path), - options[:identifier], + canonicalized_resource(service_type, path) + ] + + if @user_delegation_key.nil? + signable_fields.push(options[:identifier]) + else + signable_fields.concat [ + @user_delegation_key.signed_oid, + @user_delegation_key.signed_tid, + @user_delegation_key.signed_start, + @user_delegation_key.signed_expiry, + @user_delegation_key.signed_service, + @user_delegation_key.signed_version + ] + end + + signable_fields.concat [ options[:ip_range], options[:protocol], Azure::Storage::Common::Default::STG_VERSION ] + signable_fields.concat [ + options[:resource], + options[:timestamp] + ] if service_type == Azure::Storage::Common::ServiceType::BLOB + signable_fields.concat [ options[:cache_control], options[:content_disposition], diff --git a/common/lib/azure/storage/common/default.rb b/common/lib/azure/storage/common/default.rb index 4e0d3e16..197d3c4c 100644 --- a/common/lib/azure/storage/common/default.rb +++ b/common/lib/azure/storage/common/default.rb @@ -30,7 +30,7 @@ module Azure::Storage::Common module Default # Default REST service (STG) version number. This is used only for SAS generator. - STG_VERSION = "2017-11-09" + STG_VERSION = "2018-11-09" # The number of default concurrent requests for parallel operation. DEFAULT_PARALLEL_OPERATION_THREAD_COUNT = 1 diff --git a/test/integration/auth/user_delegation_key_shared_access_signature_test.rb b/test/integration/auth/user_delegation_key_shared_access_signature_test.rb new file mode 100644 index 00000000..d0fefa14 --- /dev/null +++ b/test/integration/auth/user_delegation_key_shared_access_signature_test.rb @@ -0,0 +1,76 @@ +#------------------------------------------------------------------------- +# # Copyright (c) Microsoft and contributors. All rights reserved. +# +# The MIT License(MIT) + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files(the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions : + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +#-------------------------------------------------------------------------- +require "adal" +require "azure/storage/common" +require "azure/storage/common/core/auth/shared_access_signature" +require "integration/test_helper" +require "net/http" + +describe Azure::Storage::Common::Core::Auth::SharedAccessSignature do + subject { + tenant_id = ENV.fetch("AZURE_TENANT_ID", nil) + client_id = ENV.fetch("AZURE_CLIENT_ID", nil) + client_secret = ENV.fetch("AZURE_CLIENT_SECRET", nil) + storage_account_name = SERVICE_CREATE_OPTIONS()[:storage_account_name] + + if tenant_id.nil? || client_id.nil? || client_secret.nil? + skip "AAD credentials not provided" + end + + auth_ctx = ADAL::AuthenticationContext.new("login.microsoftonline.com", tenant_id) + client_cred = ADAL::ClientCredential.new(client_id, client_secret) + token = auth_ctx.acquire_token_for_client("https://storage.azure.com/", client_cred) + access_token = token.access_token + + token_credential = Azure::Storage::Common::Core::TokenCredential.new access_token + token_signer = Azure::Storage::Common::Core::Auth::TokenSigner.new token_credential + client = Azure::Storage::Common::Client::create(storage_account_name: storage_account_name, signer: token_signer) + Azure::Storage::Blob::BlobService.new(api_version: "2018-11-09", client: client) + } + + let(:generator) { + user_delegation_key = subject.get_user_delegation_key(Time.now - 60 * 5, Time.now + 60 * 15) + storage_account_name = SERVICE_CREATE_OPTIONS()[:storage_account_name] + + Azure::Storage::Common::Core::Auth::SharedAccessSignature.new(storage_account_name, "", user_delegation_key) + } + + describe "#blob_service_sas_for_container" do + let(:container_name) { ContainerNameHelper.name } + let(:block_blob_name) { BlobNameHelper.name } + let(:content) { content = ""; 512.times.each { |i| content << "@" }; content } + + before { + subject.create_container container_name + subject.create_block_blob container_name, block_blob_name, content + } + + after { ContainerNameHelper.clean } + + it "fetches blob from sas uri" do + uri = generator.signed_uri(subject.generate_uri("#{container_name}/#{block_blob_name}"), false, service: "b", permissions: "r", expiry: (Time.now.utc + 60 * 10).iso8601) + _(Net::HTTP.get(uri)).must_equal content + end + end +end diff --git a/test/unit/core/auth/shared_access_signature_test.rb b/test/unit/core/auth/shared_access_signature_test.rb index db99a1c9..e8c396c8 100644 --- a/test/unit/core/auth/shared_access_signature_test.rb +++ b/test/unit/core/auth/shared_access_signature_test.rb @@ -66,6 +66,7 @@ _(subject.signable_string_for_service(service_type, path, service_options)).must_equal( "rwd\n#{Time.parse('2020-12-10T00:00:00Z').utc.iso8601}\n#{Time.parse('2020-12-11T00:00:00Z').utc.iso8601}\n" + "/blob/account-name/example/path\n\n168.1.5.60-168.1.5.70\nhttps,http\n#{Azure::Storage::Common::Default::STG_VERSION}\n" + + "b\n\n" + "public\ninline, filename=nyan.cat\ngzip\nEnglish\nbinary" ) end