Skip to content

Commit

Permalink
Implement SAS with user delegation key
Browse files Browse the repository at this point in the history
  • Loading branch information
c-w authored and vinjiang committed Mar 9, 2020
1 parent 89885cb commit a8a0115
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 5 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand All @@ -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.
Expand Down Expand Up @@ -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)))

Expand All @@ -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],
Expand Down
2 changes: 1 addition & 1 deletion common/lib/azure/storage/common/default.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions test/unit/core/auth/shared_access_signature_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit a8a0115

Please sign in to comment.