gitlab-fog-azure-rm 2.1.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +4 -0
- data/CHANGELOG.md +8 -0
- data/gitlab-fog-azure-rm.gemspec +1 -0
- data/lib/azure/storage/blob/blob_service.rb +3 -0
- data/lib/azure/storage/blob/default.rb +1 -1
- data/lib/azure/storage/common/core/auth/shared_access_signature_generator.rb +13 -2
- data/lib/azure/storage/common/default.rb +1 -1
- data/lib/fog/azurerm/identity/base_client.rb +74 -0
- data/lib/fog/azurerm/identity/credentials.rb +23 -0
- data/lib/fog/azurerm/identity/default_credentials.rb +58 -0
- data/lib/fog/azurerm/identity/managed_identity_client.rb +51 -0
- data/lib/fog/azurerm/identity/workflow_identity_client.rb +79 -0
- data/lib/fog/azurerm/identity.rb +5 -0
- data/lib/fog/azurerm/storage.rb +41 -8
- data/lib/fog/azurerm/utilities/general.rb +22 -0
- data/lib/fog/azurerm/version.rb +1 -1
- data/lib/fog/azurerm.rb +1 -0
- data/rakefile +1 -0
- data/test/models/storage/test_directory.rb +1 -1
- data/test/requests/storage/test_get_blob_https_url.rb +50 -1
- data/test/test_helper.rb +9 -0
- data/test/unit/test_default_credentials.rb +97 -0
- data/test/unit/test_managed_identity_client.rb +74 -0
- data/test/unit/test_shared_access_signature_generator.rb +198 -0
- data/test/unit/test_workflow_identity_client.rb +147 -0
- metadata +27 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fa5ac286afc5ba0f3066194079050eb837ec86bb0e23a0e879ae34b6ccd18242
|
4
|
+
data.tar.gz: cc8affe1faf1d79f910c7d7e232386b592472981738a3122922ad370d4406294
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 90faba724967de441a00eef32538b14dfbfbbce8a225d7052c965bd69110ab434316484da41b6b6de6e8ad3c7bf3cb5009cd25d634419f402a5fa31616f4764b
|
7
|
+
data.tar.gz: 7e9a772cdd278514751f4e3cd167ebe54c4c63b451c95ea701f48b475b5810638a8b847c6724e3ccf707152dbd4e509d45d2fe5ce897a2173ea88b9d5ea80c93
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
data/gitlab-fog-azure-rm.gemspec
CHANGED
@@ -22,6 +22,7 @@ Gem::Specification.new do |spec|
|
|
22
22
|
spec.add_development_dependency 'rake', '~> 13.0'
|
23
23
|
spec.add_development_dependency 'rubocop', '~> 0.89.1'
|
24
24
|
spec.add_development_dependency 'simplecov'
|
25
|
+
spec.add_development_dependency 'webmock'
|
25
26
|
spec.add_development_dependency 'webrick', '~> 1.8'
|
26
27
|
|
27
28
|
spec.add_dependency 'faraday', "~> 2.0"
|
@@ -27,6 +27,7 @@ require "azure/storage/blob/page"
|
|
27
27
|
require "azure/storage/blob/block"
|
28
28
|
require "azure/storage/blob/append"
|
29
29
|
require "azure/storage/blob/blob"
|
30
|
+
require 'azure/storage/blob/default'
|
30
31
|
|
31
32
|
module Azure::Storage
|
32
33
|
include Azure::Storage::Common::Service
|
@@ -126,6 +127,8 @@ module Azure::Storage
|
|
126
127
|
#
|
127
128
|
# Accepted key/value pairs in options parameter are:
|
128
129
|
#
|
130
|
+
|
131
|
+
# * +:api_version+ - String. Override the default Blob service API version.
|
129
132
|
# * +:use_development_storage+ - TrueClass|FalseClass. Whether to use storage emulator.
|
130
133
|
# * +:development_storage_proxy_uri+ - String. Used with +:use_development_storage+ if emulator is hosted other than localhost.
|
131
134
|
# * +:storage_connection_string+ - String. The storage connection string.
|
@@ -29,7 +29,7 @@ require "rbconfig"
|
|
29
29
|
module Azure::Storage::Blob
|
30
30
|
module Default
|
31
31
|
# Default REST service (STG) version number
|
32
|
-
STG_VERSION = "
|
32
|
+
STG_VERSION = "2025-07-05"
|
33
33
|
|
34
34
|
# The number of default concurrent requests for parallel operation.
|
35
35
|
DEFAULT_PARALLEL_OPERATION_THREAD_COUNT = 1
|
@@ -215,6 +215,7 @@ module Azure::Storage::Common::Core
|
|
215
215
|
end
|
216
216
|
|
217
217
|
# Construct the plaintext to the spec required for signatures
|
218
|
+
# See https://learn.microsoft.com/en-us/rest/api/storageservices/create-user-delegation-sas
|
218
219
|
# @return [String]
|
219
220
|
def signable_string_for_service(service_type, path, options)
|
220
221
|
# Order is significant
|
@@ -236,8 +237,17 @@ module Azure::Storage::Common::Core
|
|
236
237
|
@user_delegation_key.signed_start,
|
237
238
|
@user_delegation_key.signed_expiry,
|
238
239
|
@user_delegation_key.signed_service,
|
239
|
-
@user_delegation_key.signed_version
|
240
|
+
@user_delegation_key.signed_version,
|
241
|
+
# User delegation fields (supported since 2020-02-10)
|
242
|
+
options[:signed_authorized_user_object_id] || "", # saoid
|
243
|
+
options[:signed_unauthorized_user_object_id] || "", # suoid
|
244
|
+
options[:signed_correlation_id] || "", # scid
|
245
|
+
# For 2025-07-05 USER DELEGATION SAS ONLY, add two empty lines after correlation ID
|
246
|
+
# This is the critical difference that was causing the signature mismatch with 2018-11-09.
|
247
|
+
"",
|
248
|
+
""
|
240
249
|
]
|
250
|
+
|
241
251
|
end
|
242
252
|
|
243
253
|
signable_fields.concat [
|
@@ -248,7 +258,8 @@ module Azure::Storage::Common::Core
|
|
248
258
|
|
249
259
|
signable_fields.concat [
|
250
260
|
options[:resource],
|
251
|
-
options[:timestamp]
|
261
|
+
options[:timestamp] || "",
|
262
|
+
options[:signed_encryption_scope] || ""
|
252
263
|
] if service_type == Azure::Storage::Common::ServiceType::BLOB
|
253
264
|
|
254
265
|
signable_fields.concat [
|
@@ -30,7 +30,7 @@ require "azure/storage/common/version"
|
|
30
30
|
module Azure::Storage::Common
|
31
31
|
module Default
|
32
32
|
# Default REST service (STG) version number. This is used only for SAS generator.
|
33
|
-
STG_VERSION = "
|
33
|
+
STG_VERSION = "2025-07-05"
|
34
34
|
|
35
35
|
# The number of default concurrent requests for parallel operation.
|
36
36
|
DEFAULT_PARALLEL_OPERATION_THREAD_COUNT = 1
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: trues
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Fog
|
6
|
+
module AzureRM
|
7
|
+
module Identity
|
8
|
+
# BaseClient is responsible for fetching credentials and refreshing
|
9
|
+
# them when necessary.
|
10
|
+
class BaseClient
|
11
|
+
include Fog::AzureRM::Utilities::General
|
12
|
+
attr_accessor :credentials
|
13
|
+
|
14
|
+
FetchCredentialsError = Class.new(RuntimeError)
|
15
|
+
|
16
|
+
DEFAULT_TIMEOUT_S = 30
|
17
|
+
|
18
|
+
def fetch_credentials
|
19
|
+
raise NotImplementedError
|
20
|
+
end
|
21
|
+
|
22
|
+
def fetch_credentials_if_needed
|
23
|
+
@credentials = fetch_credentials if @credentials.nil? || refresh_needed?
|
24
|
+
|
25
|
+
credentials
|
26
|
+
end
|
27
|
+
|
28
|
+
def refresh_needed?
|
29
|
+
return true unless @credentials
|
30
|
+
|
31
|
+
@credentials.refresh_needed?
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
def process_token_response(response)
|
37
|
+
# If we get an unauthorized error, raising an exception allows the admin
|
38
|
+
# diagnose the error with the federated credentials.
|
39
|
+
raise FetchCredentialsError, response.to_s unless response.success?
|
40
|
+
|
41
|
+
body = ::JSON.parse(response.body)
|
42
|
+
access_token = body['access_token']
|
43
|
+
|
44
|
+
return unless access_token
|
45
|
+
|
46
|
+
expires_at = ::Time.now
|
47
|
+
expires_on = body['expires_on']
|
48
|
+
expires_at = ::Time.at(expires_on.to_i) if expires_on
|
49
|
+
|
50
|
+
Credentials.new(access_token, expires_at)
|
51
|
+
rescue ::JSON::ParserError # rubocop:disable Lint/SuppressedException
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def get(url, params: nil, headers: nil)
|
57
|
+
Faraday.get(url, params, headers) do |req|
|
58
|
+
req.options.timeout = DEFAULT_TIMEOUT_S
|
59
|
+
end
|
60
|
+
rescue ::Faraday::Error => e
|
61
|
+
raise FetchCredentialsError, e.to_s
|
62
|
+
end
|
63
|
+
|
64
|
+
def post(url, body: nil, headers: nil)
|
65
|
+
Faraday.post(url, body, headers) do |req|
|
66
|
+
req.options.timeout = DEFAULT_TIMEOUT_S
|
67
|
+
end
|
68
|
+
rescue ::Faraday::Error => e
|
69
|
+
raise FetchCredentialsError, e.to_s
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Fog
|
2
|
+
module AzureRM
|
3
|
+
module Identity
|
4
|
+
# Credentials stores the access token and its expiry.
|
5
|
+
class Credentials
|
6
|
+
attr_accessor :token, :expires_at
|
7
|
+
|
8
|
+
EXPIRATION_BUFFER = 600 # 10 minutes
|
9
|
+
|
10
|
+
def initialize(token, expires_at)
|
11
|
+
@token = token
|
12
|
+
@expires_at = expires_at
|
13
|
+
end
|
14
|
+
|
15
|
+
def refresh_needed?
|
16
|
+
return true unless expires_at
|
17
|
+
|
18
|
+
Time.now >= expires_at + EXPIRATION_BUFFER
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require_relative 'base_client'
|
5
|
+
|
6
|
+
module Fog
|
7
|
+
module AzureRM
|
8
|
+
# DefaultCredentials attempts to resolve the credentials necessary to access
|
9
|
+
# the Azure service.
|
10
|
+
class DefaultCredentials
|
11
|
+
def initialize(options)
|
12
|
+
@options = options
|
13
|
+
@credential_client = nil
|
14
|
+
@credentials = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def fetch_credentials_if_needed
|
18
|
+
return unless credential_client
|
19
|
+
|
20
|
+
credential_client.fetch_credentials_if_needed
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_reader :options
|
26
|
+
|
27
|
+
def credential_client
|
28
|
+
return @credential_client if @credential_client
|
29
|
+
|
30
|
+
clients = [
|
31
|
+
Fog::AzureRM::Identity::WorkflowIdentityClient,
|
32
|
+
Fog::AzureRM::Identity::ManagedIdentityClient
|
33
|
+
]
|
34
|
+
|
35
|
+
credentials = nil
|
36
|
+
clients.each do |klass|
|
37
|
+
client = klass.new(options)
|
38
|
+
|
39
|
+
begin
|
40
|
+
credentials = client.fetch_credentials
|
41
|
+
rescue Fog::AzureRM::Identity::BaseClient::FetchCredentialsError
|
42
|
+
next
|
43
|
+
end
|
44
|
+
|
45
|
+
if credentials
|
46
|
+
@credential_client = client
|
47
|
+
break
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
return unless credentials
|
52
|
+
|
53
|
+
@credentials = credentials
|
54
|
+
@credentials
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'faraday'
|
5
|
+
|
6
|
+
module Fog
|
7
|
+
module AzureRM
|
8
|
+
module Identity
|
9
|
+
IDENTITY_ENDPOINT = 'http://169.254.169.254/metadata/identity/oauth2/token'
|
10
|
+
API_VERSION = '2018-02-01'
|
11
|
+
|
12
|
+
# ManagedIdentityClient fetches temporary credentials from the instance metadata endpoint.
|
13
|
+
class ManagedIdentityClient < BaseClient
|
14
|
+
include Fog::AzureRM::Utilities::General
|
15
|
+
|
16
|
+
attr_reader :resource
|
17
|
+
|
18
|
+
def initialize(options)
|
19
|
+
super()
|
20
|
+
@environment = options[:environment]
|
21
|
+
@resource = storage_resource(@environment)
|
22
|
+
end
|
23
|
+
|
24
|
+
# This method obtains a token via the Azure Instance Metadata Service (IMDS) endpoint:
|
25
|
+
# https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http
|
26
|
+
def fetch_credentials
|
27
|
+
url = "#{identity_endpoint}?api-version=#{api_version}&resource=#{CGI.escape(resource)}"
|
28
|
+
|
29
|
+
client_id = ENV['AZURE_CLIENT_ID']
|
30
|
+
url += "&client_id=#{client_id}" if client_id
|
31
|
+
|
32
|
+
headers = { 'Metadata' => 'true' }
|
33
|
+
headers['X-IDENTITY-HEADER'] = ENV['IDENTITY_HEADER'] if ENV['IDENTITY_HEADER']
|
34
|
+
|
35
|
+
response = get(url, headers: headers)
|
36
|
+
process_token_response(response)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def identity_endpoint
|
42
|
+
ENV['IDENTITY_ENDPOINT'] || IDENTITY_ENDPOINT
|
43
|
+
end
|
44
|
+
|
45
|
+
def api_version
|
46
|
+
ENV['IDENTITY_ENDPOINT'] ? '2019-08-01' : API_VERSION
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Fog
|
6
|
+
module AzureRM
|
7
|
+
module Identity
|
8
|
+
# WorkflowIdentityClient attempts to fetch credentials for Azure Workflow Identity
|
9
|
+
# via the following environment variables:
|
10
|
+
#
|
11
|
+
# - AZURE_AUTHORITY_HOST - This can be used to override the default authority URL.
|
12
|
+
# - AZURE_TENANT_ID
|
13
|
+
# - AZURE_CLIENT_ID
|
14
|
+
# - AZURE_FEDERATED_TOKEN_FILE - This is a filename that stores the JWT token that
|
15
|
+
# is exchanged for an OAuth2 token.
|
16
|
+
class WorkflowIdentityClient < BaseClient
|
17
|
+
include Fog::AzureRM::Utilities::General
|
18
|
+
|
19
|
+
attr_accessor :environment, :resource, :authority, :tenant_id, :client_id, :token_file
|
20
|
+
|
21
|
+
def initialize(options)
|
22
|
+
super()
|
23
|
+
@environment = options[:environment]
|
24
|
+
@resource = storage_resource(@environment)
|
25
|
+
@authority = ENV['AZURE_AUTHORITY_HOST'] || authority_url(@environment)
|
26
|
+
@tenant_id = ENV['AZURE_TENANT_ID']
|
27
|
+
@client_id = ENV['AZURE_CLIENT_ID']
|
28
|
+
@token_file = ENV['AZURE_FEDERATED_TOKEN_FILE']
|
29
|
+
|
30
|
+
normalize_authority!
|
31
|
+
end
|
32
|
+
|
33
|
+
def fetch_credentials
|
34
|
+
return unless authority && tenant_id && client_id
|
35
|
+
return unless ::File.exist?(token_file) && ::File.readable?(token_file)
|
36
|
+
|
37
|
+
oidc_token = ::File.read(token_file)
|
38
|
+
token_url = "#{authority}/#{tenant_id}/oauth2/v2.0/token"
|
39
|
+
scope = "#{storage_resource(@environment)}/.default"
|
40
|
+
|
41
|
+
data = {
|
42
|
+
client_id: client_id,
|
43
|
+
grant_type: 'client_credentials',
|
44
|
+
client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
|
45
|
+
client_assertion: oidc_token,
|
46
|
+
scope: scope
|
47
|
+
}
|
48
|
+
|
49
|
+
response = post(token_url, body: data)
|
50
|
+
|
51
|
+
process_token_response(response)
|
52
|
+
rescue ::Faraday::Error => e
|
53
|
+
raise FetchCredentialsError, e.to_s
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
# The Azure Python SDK handles an authority with or without a scheme, so let's
|
59
|
+
# do this too: https://github.com/Azure/azure-sdk-for-python/pull/11050
|
60
|
+
def normalize_authority!
|
61
|
+
parsed = URI.parse(authority)
|
62
|
+
|
63
|
+
unless parsed.scheme
|
64
|
+
@authority = "https://#{authority.strip.chomp('/')}"
|
65
|
+
return
|
66
|
+
end
|
67
|
+
|
68
|
+
# rubocop:disable Style/IfUnlessModifier
|
69
|
+
unless parsed.scheme == 'https'
|
70
|
+
raise ArgumentError, "'#{authority}' is an invalid authority. The value must be a TLS protected (https) URL."
|
71
|
+
end
|
72
|
+
# rubocop:enable Style/IfUnlessModifier
|
73
|
+
|
74
|
+
@authority = authority.strip.chomp('/')
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
data/lib/fog/azurerm/storage.rb
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
1
5
|
module Fog
|
2
6
|
module AzureRM
|
3
7
|
# This class registers models, requests and collections
|
@@ -9,6 +13,7 @@ module Fog
|
|
9
13
|
recognizes :azure_storage_endpoint
|
10
14
|
recognizes :azure_storage_domain
|
11
15
|
recognizes :environment
|
16
|
+
recognizes :api_version
|
12
17
|
|
13
18
|
recognizes :debug
|
14
19
|
|
@@ -80,6 +85,8 @@ module Fog
|
|
80
85
|
class Real
|
81
86
|
include Fog::AzureRM::Utilities::General
|
82
87
|
|
88
|
+
attr_accessor :options
|
89
|
+
|
83
90
|
def initialize(options)
|
84
91
|
begin
|
85
92
|
require 'azure/storage/common'
|
@@ -93,19 +100,20 @@ module Fog
|
|
93
100
|
raise e.message
|
94
101
|
end
|
95
102
|
|
96
|
-
|
97
|
-
|
98
|
-
|
103
|
+
options[:environment] = options[:environment] || ENV['AZURE_ENVIRONMENT'] || ENVIRONMENT_AZURE_CLOUD
|
104
|
+
@environment = options[:environment]
|
105
|
+
@options = options
|
99
106
|
|
100
107
|
@azure_storage_account_name = options[:azure_storage_account_name]
|
101
108
|
@azure_storage_access_key = options[:azure_storage_access_key]
|
102
|
-
@
|
109
|
+
@api_version = options[:api_version]
|
110
|
+
|
111
|
+
load_credentials
|
112
|
+
|
113
|
+
@azure_storage_token_signer = token_signer
|
103
114
|
@azure_storage_endpoint = options[:azure_storage_endpoint]
|
104
115
|
@azure_storage_domain = options[:azure_storage_domain]
|
105
116
|
|
106
|
-
options[:environment] = 'AzureCloud' if options[:environment].nil?
|
107
|
-
@environment = options[:environment]
|
108
|
-
|
109
117
|
storage_blob_host =
|
110
118
|
@azure_storage_endpoint ||
|
111
119
|
if @azure_storage_domain.nil? || @azure_storage_domain.empty?
|
@@ -120,7 +128,7 @@ module Fog
|
|
120
128
|
signer: @azure_storage_token_signer
|
121
129
|
}.compact)
|
122
130
|
azure_client.storage_blob_host = storage_blob_host
|
123
|
-
@blob_client = Azure::Storage::Blob::BlobService.new(client: azure_client)
|
131
|
+
@blob_client = Azure::Storage::Blob::BlobService.new(client: azure_client, api_version: @api_version)
|
124
132
|
@blob_client.with_filter(Fog::AzureRM::IdentityEncodingFilter.new)
|
125
133
|
@blob_client.with_filter(Azure::Storage::Common::Core::Filter::ExponentialRetryPolicyFilter.new)
|
126
134
|
@blob_client.with_filter(Azure::Core::Http::DebugFilter.new) if @debug
|
@@ -128,6 +136,26 @@ module Fog
|
|
128
136
|
|
129
137
|
private
|
130
138
|
|
139
|
+
def load_credentials
|
140
|
+
return options[:azure_storage_token_signer] if options[:azure_storage_token_signer]
|
141
|
+
return if @azure_storage_access_key && !@azure_storage_access_key.empty?
|
142
|
+
|
143
|
+
@credential_client = Fog::AzureRM::DefaultCredentials.new(options)
|
144
|
+
@credentials = @credential_client.fetch_credentials_if_needed
|
145
|
+
end
|
146
|
+
|
147
|
+
def token_signer
|
148
|
+
return options[:azure_storage_token_signer] if options[:azure_storage_token_signer]
|
149
|
+
return unless @credentials
|
150
|
+
|
151
|
+
access_token_signer(@credentials.token)
|
152
|
+
end
|
153
|
+
|
154
|
+
def access_token_signer(access_token)
|
155
|
+
cred = Azure::Storage::Common::Core::TokenCredential.new(access_token)
|
156
|
+
Azure::Storage::Common::Core::Auth::TokenSigner.new(cred)
|
157
|
+
end
|
158
|
+
|
131
159
|
def signature_client(requested_expiry)
|
132
160
|
access_key = @azure_storage_access_key.to_s
|
133
161
|
user_delegation_key = user_delegation_key(requested_expiry)
|
@@ -148,6 +176,11 @@ module Fog
|
|
148
176
|
def user_delegation_key(requested_expiry)
|
149
177
|
return nil unless @azure_storage_token_signer
|
150
178
|
|
179
|
+
if @credential_client
|
180
|
+
@credentials = @credential_client.fetch_credentials_if_needed
|
181
|
+
@azure_storage_token_signer = token_signer
|
182
|
+
end
|
183
|
+
|
151
184
|
@user_delegation_key_mutex ||= Mutex.new
|
152
185
|
@user_delegation_key_mutex.synchronize do
|
153
186
|
if @user_delegation_key_expiry.nil? || @user_delegation_key_expiry < requested_expiry
|
@@ -89,6 +89,28 @@ module Fog
|
|
89
89
|
end
|
90
90
|
end
|
91
91
|
|
92
|
+
# Per https://learn.microsoft.com/en-us/azure/storage/blobs/authorize-access-azure-active-directory,
|
93
|
+
# all endpoints use the same resource URL.
|
94
|
+
def storage_resource(_environment = ENVIRONMENT_AZURE_CLOUD)
|
95
|
+
'https://storage.azure.com'
|
96
|
+
end
|
97
|
+
|
98
|
+
# https://learn.microsoft.com/en-us/entra/identity-platform/authentication-national-cloud#microsoft-entra-authentication-endpoints
|
99
|
+
def authority_url(environment = ENVIRONMENT_AZURE_CLOUD)
|
100
|
+
case environment
|
101
|
+
when ENVIRONMENT_AZURE_CHINA_CLOUD
|
102
|
+
'https://login.chinacloudapi.cn'
|
103
|
+
when ENVIRONMENT_AZURE_US_GOVERNMENT
|
104
|
+
'https://login.microsoftonline.us'
|
105
|
+
# This region is deprecated:
|
106
|
+
# https://learn.microsoft.com/en-us/entra/identity-platform/authentication-national-cloud#azure-germany-microsoft-cloud-deutschland
|
107
|
+
when ENVIRONMENT_AZURE_GERMAN_CLOUD
|
108
|
+
'https://login.microsoftonline.de'
|
109
|
+
else
|
110
|
+
'https://login.microsoftonline.com'
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
92
114
|
def get_blob_endpoint(storage_account_name, enable_https = false, environment = ENVIRONMENT_AZURE_CLOUD)
|
93
115
|
protocol = enable_https ? 'https' : 'http'
|
94
116
|
"#{protocol}://#{storage_account_name}.blob#{storage_endpoint_suffix(environment)}"
|
data/lib/fog/azurerm/version.rb
CHANGED
data/lib/fog/azurerm.rb
CHANGED
data/rakefile
CHANGED
@@ -8,6 +8,7 @@ task :test do
|
|
8
8
|
Dir.glob('test/test_*.rb').each { |file| require File.expand_path file, __dir__ }
|
9
9
|
Dir.glob('test/models/**/test_*.rb').each { |file| require File.expand_path file, __dir__ }
|
10
10
|
Dir.glob('test/requests/**/test_*.rb').each { |file| require File.expand_path file, __dir__ }
|
11
|
+
Dir.glob('test/unit/**/test_*.rb').each { |file| require File.expand_path file, __dir__ }
|
11
12
|
end
|
12
13
|
|
13
14
|
task :integration do
|
@@ -112,7 +112,7 @@ class TestDirectory < Minitest::Test
|
|
112
112
|
# Set private
|
113
113
|
result = @directory.public = false
|
114
114
|
assert !result
|
115
|
-
|
115
|
+
assert_nil @directory.attributes[:acl]
|
116
116
|
end
|
117
117
|
|
118
118
|
def test_public_url_method_with_public_success
|
@@ -104,6 +104,55 @@ class TestGetBlobHttpsUrl < Minitest::Test
|
|
104
104
|
end
|
105
105
|
end
|
106
106
|
|
107
|
+
def test_get_blob_https_url_with_managed_identity
|
108
|
+
token_response = {
|
109
|
+
'access_token' => 'fake_token',
|
110
|
+
'expires_on' => (Time.now + 3600).to_i.to_s
|
111
|
+
}
|
112
|
+
|
113
|
+
stub_request(:get, "#{Fog::AzureRM::Identity::IDENTITY_ENDPOINT}?api-version=#{Fog::AzureRM::Identity::API_VERSION}&resource=https://storage.azure.com")
|
114
|
+
.with(headers: { 'Metadata' => 'true' })
|
115
|
+
.to_return(status: 200, body: token_response.to_json)
|
116
|
+
|
117
|
+
service = Fog::AzureRM::Storage.new(storage_account_managed_identity)
|
118
|
+
|
119
|
+
requested_expiry = Time.now + 60
|
120
|
+
|
121
|
+
response = <<~MSG
|
122
|
+
<UserDelegationKey>
|
123
|
+
<SignedOid>f81d4fae-7dec-11d0-a765-00a0c91e6bf6</SignedOid>
|
124
|
+
<SignedTid>72f988bf-86f1-41af-91ab-2d7cd011db47</SignedTid>
|
125
|
+
<SignedStart>2024-09-19T00:00:00Z</SignedStart>
|
126
|
+
<SignedExpiry>2024-09-26T00:00:00Z</SignedExpiry>
|
127
|
+
<SignedService>b</SignedService>
|
128
|
+
<SignedVersion>2020-02-10</SignedVersion>
|
129
|
+
<Value>UDELEGATIONKEYXYZ....</Value>
|
130
|
+
<SignedKey>rL7...ABC</SignedKey>
|
131
|
+
</UserDelegationKey>
|
132
|
+
MSG
|
133
|
+
|
134
|
+
stub_request(:post, 'https://mockaccount.blob.core.windows.net?comp=userdelegationkey&restype=service')
|
135
|
+
.to_return(status: 200, headers: { 'Content-Type': 'application/xml' }, body: response)
|
136
|
+
|
137
|
+
url = service.get_blob_https_url('test_container', 'test_blob', requested_expiry)
|
138
|
+
|
139
|
+
parsed = URI.parse(url)
|
140
|
+
|
141
|
+
assert_equal 'https', parsed.scheme
|
142
|
+
assert_equal 'mockaccount.blob.core.windows.net', parsed.host
|
143
|
+
assert_equal '/test_container/test_blob', parsed.path
|
144
|
+
|
145
|
+
params = parsed.query.split('&').to_h { |x| x.split('=') }
|
146
|
+
|
147
|
+
assert_equal 'r', params['sp']
|
148
|
+
assert_equal '2025-07-05', params['sv']
|
149
|
+
assert_equal 'b', params['sr']
|
150
|
+
assert_equal 'https', params['spr']
|
151
|
+
assert_equal '2024-09-19T00%3A00%3A00Z', params['skt']
|
152
|
+
assert_equal '2024-09-26T00%3A00%3A00Z', params['ske']
|
153
|
+
assert_equal 'b', params['sks']
|
154
|
+
end
|
155
|
+
|
107
156
|
def test_get_blob_https_url_with_token_signer_success
|
108
157
|
service = Fog::AzureRM::Storage.new(storage_account_credentials_with_token_signer)
|
109
158
|
blob_client = service.instance_variable_get(:@blob_client)
|
@@ -132,7 +181,7 @@ class TestGetBlobHttpsUrl < Minitest::Test
|
|
132
181
|
# second request within new expiry
|
133
182
|
stubbed_times << ref_time + 10.5 * DAY
|
134
183
|
requested_expiries << stubbed_times.last + 1 * DAY
|
135
|
-
# no
|
184
|
+
# no additional expected_user_delegation_key_starts
|
136
185
|
|
137
186
|
user_delegation_key_starts = []
|
138
187
|
mock_user_delegation_key = lambda do |start, expiry|
|
data/test/test_helper.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
require 'webmock/minitest'
|
2
|
+
WebMock.disable_net_connect! allow: %w[127.0.0.1]
|
3
|
+
|
1
4
|
if ENV['COVERAGE']
|
2
5
|
require 'simplecov'
|
3
6
|
SimpleCov.start do
|
@@ -42,6 +45,12 @@ def storage_account_credentials
|
|
42
45
|
}
|
43
46
|
end
|
44
47
|
|
48
|
+
def storage_account_managed_identity
|
49
|
+
{
|
50
|
+
azure_storage_account_name: 'mockaccount'
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
45
54
|
def storage_account_credentials_with_endpoint
|
46
55
|
storage_account_credentials.merge(
|
47
56
|
{
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require File.expand_path '../test_helper', __dir__
|
2
|
+
|
3
|
+
class TestDefaultCredentials < Minitest::Test
|
4
|
+
def setup
|
5
|
+
@options = { environment: 'AzureCloud' }
|
6
|
+
@default_credentials = Fog::AzureRM::DefaultCredentials.new(@options)
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_initialize
|
10
|
+
assert_instance_of Fog::AzureRM::DefaultCredentials, @default_credentials
|
11
|
+
assert_nil @default_credentials.instance_variable_get(:@credential_client)
|
12
|
+
assert_nil @default_credentials.instance_variable_get(:@credentials)
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_fetch_credentials_if_needed_with_no_credential_client
|
16
|
+
@default_credentials.stub(:credential_client, nil) do
|
17
|
+
assert_nil @default_credentials.fetch_credentials_if_needed
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_fetch_credentials_if_needed_with_credential_client
|
22
|
+
mock_client = Minitest::Mock.new
|
23
|
+
mock_client.expect :fetch_credentials_if_needed, 'fake_credentials'
|
24
|
+
@default_credentials.instance_variable_set(:@credential_client, mock_client)
|
25
|
+
|
26
|
+
assert_equal 'fake_credentials', @default_credentials.fetch_credentials_if_needed
|
27
|
+
mock_client.verify
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_credential_client_with_workflow_identity
|
31
|
+
mock_workflow_client = Minitest::Mock.new
|
32
|
+
mock_workflow_client.expect :fetch_credentials, 'fake_workflow_credentials'
|
33
|
+
|
34
|
+
Fog::AzureRM::Identity::WorkflowIdentityClient.stub :new, mock_workflow_client do
|
35
|
+
credentials = @default_credentials.send(:credential_client)
|
36
|
+
assert_equal 'fake_workflow_credentials', credentials
|
37
|
+
end
|
38
|
+
|
39
|
+
mock_workflow_client.verify
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_credential_client_with_managed_identity
|
43
|
+
mock_workflow_client = Minitest::Mock.new
|
44
|
+
mock_workflow_client.expect :fetch_credentials, nil
|
45
|
+
|
46
|
+
mock_managed_client = Minitest::Mock.new
|
47
|
+
mock_managed_client.expect :fetch_credentials, 'fake_managed_credentials'
|
48
|
+
|
49
|
+
Fog::AzureRM::Identity::WorkflowIdentityClient.stub :new, mock_workflow_client do
|
50
|
+
Fog::AzureRM::Identity::ManagedIdentityClient.stub :new, mock_managed_client do
|
51
|
+
credentials = @default_credentials.send(:credential_client)
|
52
|
+
assert_equal 'fake_managed_credentials', credentials
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
mock_workflow_client.verify
|
57
|
+
mock_managed_client.verify
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_credential_client_with_failed_workflow_identity
|
61
|
+
mock_workflow_client = Minitest::Mock.new
|
62
|
+
def mock_workflow_client.fetch_credentials
|
63
|
+
raise ::Fog::AzureRM::Identity::BaseClient::FetchCredentialsError, 'Failed to fetch credentials'
|
64
|
+
end
|
65
|
+
|
66
|
+
mock_managed_client = Minitest::Mock.new
|
67
|
+
mock_managed_client.expect :fetch_credentials, 'fake_managed_credentials'
|
68
|
+
|
69
|
+
Fog::AzureRM::Identity::WorkflowIdentityClient.stub :new, mock_workflow_client do
|
70
|
+
Fog::AzureRM::Identity::ManagedIdentityClient.stub :new, mock_managed_client do
|
71
|
+
credentials = @default_credentials.send(:credential_client)
|
72
|
+
assert_equal 'fake_managed_credentials', credentials
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
mock_workflow_client.verify
|
77
|
+
mock_managed_client.verify
|
78
|
+
end
|
79
|
+
|
80
|
+
def test_credential_client_with_no_credentials
|
81
|
+
mock_workflow_client = Minitest::Mock.new
|
82
|
+
mock_workflow_client.expect :fetch_credentials, nil
|
83
|
+
|
84
|
+
mock_managed_client = Minitest::Mock.new
|
85
|
+
mock_managed_client.expect :fetch_credentials, nil
|
86
|
+
|
87
|
+
Fog::AzureRM::Identity::WorkflowIdentityClient.stub :new, mock_workflow_client do
|
88
|
+
Fog::AzureRM::Identity::ManagedIdentityClient.stub :new, mock_managed_client do
|
89
|
+
assert_nil @default_credentials.send(:credential_client)
|
90
|
+
assert_nil @default_credentials.instance_variable_get(:@credential_client)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
mock_workflow_client.verify
|
95
|
+
mock_managed_client.verify
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require File.expand_path '../test_helper', __dir__
|
2
|
+
|
3
|
+
class TestManagedIdentityClient < Minitest::Test
|
4
|
+
def setup
|
5
|
+
@options = { environment: 'AzureCloud' }
|
6
|
+
@client = Fog::AzureRM::Identity::ManagedIdentityClient.new(@options)
|
7
|
+
end
|
8
|
+
|
9
|
+
def teardown
|
10
|
+
ENV.delete('AZURE_CLIENT_ID')
|
11
|
+
ENV.delete('IDENTITY_HEADER')
|
12
|
+
ENV.delete('IDENTITY_ENDPOINT')
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_initialize
|
16
|
+
assert_equal 'https://storage.azure.com', @client.resource
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_fetch_credentials_success
|
20
|
+
token_response = {
|
21
|
+
'access_token' => 'fake_token',
|
22
|
+
'expires_on' => (Time.now + 3600).to_i.to_s
|
23
|
+
}
|
24
|
+
|
25
|
+
stub_request(:get, "#{Fog::AzureRM::Identity::IDENTITY_ENDPOINT}?api-version=#{Fog::AzureRM::Identity::API_VERSION}&resource=#{CGI.escape(@client.resource)}")
|
26
|
+
.with(headers: { 'Metadata' => 'true' })
|
27
|
+
.to_return(status: 200, body: token_response.to_json)
|
28
|
+
|
29
|
+
credentials = @client.fetch_credentials
|
30
|
+
|
31
|
+
assert_equal 'fake_token', credentials.token
|
32
|
+
assert_instance_of Time, credentials.expires_at
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_fetch_credentials_timeout
|
36
|
+
stub_request(:get, "#{Fog::AzureRM::Identity::IDENTITY_ENDPOINT}?api-version=#{Fog::AzureRM::Identity::API_VERSION}&resource=#{CGI.escape(@client.resource)}")
|
37
|
+
.with(headers: { 'Metadata' => 'true' })
|
38
|
+
.to_raise(Faraday::Error)
|
39
|
+
|
40
|
+
assert_raises(Fog::AzureRM::Identity::BaseClient::FetchCredentialsError) do
|
41
|
+
@client.fetch_credentials
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_fetch_credentials_unauthorized
|
46
|
+
stub_request(:get, "#{Fog::AzureRM::Identity::IDENTITY_ENDPOINT}?api-version=#{Fog::AzureRM::Identity::API_VERSION}&resource=#{CGI.escape(@client.resource)}")
|
47
|
+
.with(headers: { 'Metadata' => 'true' })
|
48
|
+
.to_return(status: 401)
|
49
|
+
|
50
|
+
assert_raises(Fog::AzureRM::Identity::BaseClient::FetchCredentialsError) do
|
51
|
+
@client.fetch_credentials
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_fetch_credentials_with_env_vars
|
56
|
+
token_response = {
|
57
|
+
'access_token' => 'fake_token',
|
58
|
+
'expires_on' => (Time.now + 3600).to_i.to_s
|
59
|
+
}
|
60
|
+
|
61
|
+
ENV['AZURE_CLIENT_ID'] = 'fake_client_id'
|
62
|
+
ENV['IDENTITY_HEADER'] = 'fake_identity_header'
|
63
|
+
ENV['IDENTITY_ENDPOINT'] = 'http://localhost:8080/metadata/identity/oauth2/token'
|
64
|
+
|
65
|
+
stub_request(:get, "http://localhost:8080/metadata/identity/oauth2/token?api-version=2019-08-01&client_id=fake_client_id&resource=#{CGI.escape(@client.resource)}")
|
66
|
+
.with(headers: { 'Metadata' => 'true', 'X-Identity-Header' => 'fake_identity_header' })
|
67
|
+
.to_return(status: 200, body: token_response.to_json)
|
68
|
+
|
69
|
+
credentials = @client.fetch_credentials
|
70
|
+
|
71
|
+
assert_equal 'fake_token', credentials.token
|
72
|
+
assert_instance_of Time, credentials.expires_at
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,198 @@
|
|
1
|
+
require File.expand_path '../test_helper', __dir__
|
2
|
+
require 'azure/storage/common'
|
3
|
+
require 'base64'
|
4
|
+
require 'uri'
|
5
|
+
|
6
|
+
class TestSharedAccessSignatureGeneration < Minitest::Test
|
7
|
+
def setup
|
8
|
+
@access_account_name = 'testaccount'
|
9
|
+
@access_key_base64 = Base64.strict_encode64('test-access-key')
|
10
|
+
@path = '/container/blob.txt'
|
11
|
+
@service_type = Azure::Storage::Common::ServiceType::BLOB
|
12
|
+
|
13
|
+
@service_options = {
|
14
|
+
service: 'b',
|
15
|
+
permissions: 'c',
|
16
|
+
start: '2025-09-10T17:00:00Z',
|
17
|
+
expiry: '2025-09-10T18:00:00Z',
|
18
|
+
resource: 'b',
|
19
|
+
protocol: 'https',
|
20
|
+
ip_range: '192.168.1.1-192.168.1.10'
|
21
|
+
}
|
22
|
+
|
23
|
+
@user_delegation_key = Azure::Storage::Common::Service::UserDelegationKey.new do |key|
|
24
|
+
key.signed_oid = 'user-object-id-123'
|
25
|
+
key.signed_tid = 'tenant-id-456'
|
26
|
+
key.signed_start = '2025-09-10T17:00:00Z'
|
27
|
+
key.signed_expiry = '2025-09-10T19:00:00Z'
|
28
|
+
key.signed_service = 'b'
|
29
|
+
key.signed_version = '2020-08-04'
|
30
|
+
key.value = Base64.strict_encode64('user-delegation-key-value')
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_service_sas_signature_string_format
|
35
|
+
# Test regular service SAS (no user delegation key)
|
36
|
+
sas = Azure::Storage::Common::Core::Auth::SharedAccessSignature.new(@access_account_name, @access_key_base64)
|
37
|
+
|
38
|
+
result = sas.signable_string_for_service(@service_type, @path, @service_options)
|
39
|
+
lines = result.split("\n", -1) # Preserve training newlines
|
40
|
+
|
41
|
+
# Verify service SAS structure
|
42
|
+
assert_equal 'c', lines[0] # permissions
|
43
|
+
assert_equal '2025-09-10T17:00:00Z', lines[1] # start
|
44
|
+
assert_equal '2025-09-10T18:00:00Z', lines[2] # expiry
|
45
|
+
assert_equal '/blob/testaccount/container/blob.txt', lines[3] # canonicalized resource
|
46
|
+
assert_equal '', lines[4] # identifier (empty for service SAS)
|
47
|
+
assert_equal '192.168.1.1-192.168.1.10', lines[5] # ip_range
|
48
|
+
assert_equal 'https', lines[6] # protocol
|
49
|
+
assert_equal Azure::Storage::Common::Default::STG_VERSION, lines[7] # version
|
50
|
+
assert_equal 'b', lines[8] # resource
|
51
|
+
assert_equal '', lines[9] # timestamp (empty)
|
52
|
+
assert_equal '', lines[10] # encryption scope (empty)
|
53
|
+
# Response headers (all empty)
|
54
|
+
assert_equal '', lines[11] # cache_control
|
55
|
+
assert_equal '', lines[12] # content_disposition
|
56
|
+
assert_equal '', lines[13] # content_encoding
|
57
|
+
assert_equal '', lines[14] # content_language
|
58
|
+
assert_equal '', lines[15] # content_type
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_user_delegation_sas_signature_string_format
|
62
|
+
# Test user delegation SAS - always includes new 2025-07-05 format
|
63
|
+
sas = Azure::Storage::Common::Core::Auth::SharedAccessSignature.new(@access_account_name, @access_key_base64, @user_delegation_key)
|
64
|
+
|
65
|
+
result = sas.signable_string_for_service(@service_type, @path, @service_options)
|
66
|
+
lines = result.split("\n", -1) # Preserve training newlines
|
67
|
+
|
68
|
+
# Verify user delegation SAS structure
|
69
|
+
assert_equal 'c', lines[0] # permissions
|
70
|
+
assert_equal '2025-09-10T17:00:00Z', lines[1] # start
|
71
|
+
assert_equal '2025-09-10T18:00:00Z', lines[2] # expiry
|
72
|
+
assert_equal '/blob/testaccount/container/blob.txt', lines[3] # canonicalized resource
|
73
|
+
|
74
|
+
# User delegation key fields
|
75
|
+
assert_equal 'user-object-id-123', lines[4] # signed_oid
|
76
|
+
assert_equal 'tenant-id-456', lines[5] # signed_tid
|
77
|
+
assert_equal '2025-09-10T17:00:00Z', lines[6] # signed_start
|
78
|
+
assert_equal '2025-09-10T19:00:00Z', lines[7] # signed_expiry
|
79
|
+
assert_equal 'b', lines[8] # signed_service
|
80
|
+
assert_equal '2020-08-04', lines[9] # signed_version
|
81
|
+
|
82
|
+
# New fields for user delegation SAS (empty by default)
|
83
|
+
assert_equal '', lines[10] # saoid
|
84
|
+
assert_equal '', lines[11] # suoid
|
85
|
+
assert_equal '', lines[12] # scid
|
86
|
+
|
87
|
+
# Critical: Two empty lines - this was causing the signature mismatch!
|
88
|
+
assert_equal '', lines[13] # First empty line
|
89
|
+
assert_equal '', lines[14] # Second empty line
|
90
|
+
|
91
|
+
# Continue with remaining fields
|
92
|
+
assert_equal '192.168.1.1-192.168.1.10', lines[15] # ip_range
|
93
|
+
assert_equal 'https', lines[16] # protocol
|
94
|
+
assert_equal Azure::Storage::Common::Default::STG_VERSION, lines[17] # version
|
95
|
+
assert_equal 'b', lines[18] # resource
|
96
|
+
assert_equal '', lines[19] # timestamp
|
97
|
+
assert_equal '', lines[20] # encryption scope
|
98
|
+
# Response headers (all empty)
|
99
|
+
assert_equal '', lines[21] # cache_control
|
100
|
+
assert_equal '', lines[22] # content_disposition
|
101
|
+
assert_equal '', lines[23] # content_encoding
|
102
|
+
assert_equal '', lines[24] # content_language
|
103
|
+
assert_equal '', lines[25] # content_type
|
104
|
+
end
|
105
|
+
|
106
|
+
def test_user_delegation_sas_with_optional_fields
|
107
|
+
# Test user delegation SAS with optional fields set
|
108
|
+
sas = Azure::Storage::Common::Core::Auth::SharedAccessSignature.new(@access_account_name, @access_key_base64, @user_delegation_key)
|
109
|
+
|
110
|
+
options_with_extras = @service_options.merge({
|
111
|
+
signed_authorized_user_object_id: 'authorized-user-789',
|
112
|
+
signed_unauthorized_user_object_id: 'unauthorized-user-000',
|
113
|
+
signed_correlation_id: 'correlation-abc-123',
|
114
|
+
signed_encryption_scope: 'test-encryption-scope',
|
115
|
+
timestamp: '2025-09-10T17:30:00Z',
|
116
|
+
cache_control: 'no-cache',
|
117
|
+
content_type: 'application/json'
|
118
|
+
})
|
119
|
+
|
120
|
+
result = sas.signable_string_for_service(@service_type, @path, options_with_extras)
|
121
|
+
lines = result.split("\n", -1) # Preserve training newlines
|
122
|
+
|
123
|
+
# Verify the optional fields are included
|
124
|
+
assert_equal 'authorized-user-789', lines[10] # saoid
|
125
|
+
assert_equal 'unauthorized-user-000', lines[11] # suoid
|
126
|
+
assert_equal 'correlation-abc-123', lines[12] # scid
|
127
|
+
assert_equal '2025-09-10T17:30:00Z', lines[19] # timestamp
|
128
|
+
assert_equal 'test-encryption-scope', lines[20] # encryption scope
|
129
|
+
assert_equal 'no-cache', lines[21] # cache_control
|
130
|
+
assert_equal 'application/json', lines[25] # content_type
|
131
|
+
end
|
132
|
+
|
133
|
+
def test_table_service_signature_format
|
134
|
+
# Test table service (no blob-specific fields)
|
135
|
+
sas = Azure::Storage::Common::Core::Auth::SharedAccessSignature.new(@access_account_name, @access_key_base64)
|
136
|
+
|
137
|
+
table_options = {
|
138
|
+
service: 't',
|
139
|
+
permissions: 'rau',
|
140
|
+
expiry: '2025-09-10T18:00:00Z',
|
141
|
+
startpk: 'partition1',
|
142
|
+
startrk: 'row1',
|
143
|
+
endpk: 'partition2',
|
144
|
+
endrk: 'row2'
|
145
|
+
}
|
146
|
+
|
147
|
+
result = sas.signable_string_for_service(Azure::Storage::Common::ServiceType::TABLE, 'mytable', table_options)
|
148
|
+
lines = result.split("\n", -1) # Preserve training newlines
|
149
|
+
|
150
|
+
# Table service should not have blob-specific fields
|
151
|
+
assert_equal 'rau', lines[0] # permissions
|
152
|
+
assert_equal '/table/testaccount/mytable', lines[3] # canonicalized resource
|
153
|
+
|
154
|
+
# Should end with table-specific fields (no response headers for table)
|
155
|
+
expected_line_count = 12 # Basic fields + table fields, no blob fields
|
156
|
+
assert_equal expected_line_count, lines.length
|
157
|
+
assert_equal 'partition1', lines[8] # startpk
|
158
|
+
assert_equal 'row1', lines[9] # startrk
|
159
|
+
assert_equal 'partition2', lines[10] # endpk
|
160
|
+
assert_equal 'row2', lines[11] # endrk
|
161
|
+
end
|
162
|
+
|
163
|
+
def test_canonicalized_resource_format
|
164
|
+
sas = Azure::Storage::Common::Core::Auth::SharedAccessSignature.new(@access_account_name, @access_key_base64)
|
165
|
+
|
166
|
+
# Test with leading slash
|
167
|
+
result = sas.canonicalized_resource('blob', '/container/blob.txt')
|
168
|
+
assert_equal '/blob/testaccount/container/blob.txt', result
|
169
|
+
|
170
|
+
# Test without leading slash
|
171
|
+
result = sas.canonicalized_resource('blob', 'container/blob.txt')
|
172
|
+
assert_equal '/blob/testaccount/container/blob.txt', result
|
173
|
+
|
174
|
+
# Test table resource
|
175
|
+
result = sas.canonicalized_resource('table', 'mytable')
|
176
|
+
assert_equal '/table/testaccount/mytable', result
|
177
|
+
end
|
178
|
+
|
179
|
+
def test_sas_token_generation_creates_valid_query_string
|
180
|
+
# Test that generate_service_sas_token creates a valid query string
|
181
|
+
sas = Azure::Storage::Common::Core::Auth::SharedAccessSignature.new(@access_account_name, @access_key_base64)
|
182
|
+
|
183
|
+
token = sas.generate_service_sas_token(@path, @service_options)
|
184
|
+
|
185
|
+
# Parse the query string
|
186
|
+
params = URI.decode_www_form(token).to_h
|
187
|
+
|
188
|
+
# Verify expected parameters are present
|
189
|
+
assert_equal 'c', params['sp'] # permissions
|
190
|
+
assert_equal '2025-09-10T17:00:00Z', params['st'] # start
|
191
|
+
assert_equal '2025-09-10T18:00:00Z', params['se'] # expiry
|
192
|
+
assert_equal 'b', params['sr'] # resource
|
193
|
+
assert_equal 'https', params['spr'] # protocol
|
194
|
+
assert_equal '192.168.1.1-192.168.1.10', params['sip'] # ip_range
|
195
|
+
assert params.key?('sig') # signature should be present
|
196
|
+
refute_empty params['sig'] # signature should not be empty
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
require File.expand_path '../test_helper', __dir__
|
2
|
+
|
3
|
+
class TestWorkflowIdentityClient < Minitest::Test
|
4
|
+
def setup
|
5
|
+
@options = { environment: 'AzureCloud' }
|
6
|
+
@client = Fog::AzureRM::Identity::WorkflowIdentityClient.new(@options)
|
7
|
+
end
|
8
|
+
|
9
|
+
def teardown
|
10
|
+
ENV.delete('AZURE_TENANT_ID')
|
11
|
+
ENV.delete('AZURE_CLIENT_ID')
|
12
|
+
ENV.delete('AZURE_FEDERATED_TOKEN_FILE')
|
13
|
+
ENV.delete('AZURE_AUTHORITY_HOST')
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_initialize
|
17
|
+
assert_equal 'https://storage.azure.com', @client.resource
|
18
|
+
assert_equal 'https://login.microsoftonline.com', @client.authority
|
19
|
+
assert_nil @client.tenant_id
|
20
|
+
assert_nil @client.client_id
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_fetch_credentials_success
|
24
|
+
ENV['AZURE_TENANT_ID'] = 'fake_tenant_id'
|
25
|
+
ENV['AZURE_CLIENT_ID'] = 'fake_client_id'
|
26
|
+
ENV['AZURE_FEDERATED_TOKEN_FILE'] = 'fake_token_file'
|
27
|
+
|
28
|
+
@client = Fog::AzureRM::Identity::WorkflowIdentityClient.new(@options)
|
29
|
+
|
30
|
+
token_response = {
|
31
|
+
'access_token' => 'fake_access_token',
|
32
|
+
'expires_on' => (Time.now + 3600).to_i.to_s
|
33
|
+
}
|
34
|
+
|
35
|
+
File.stub :exist?, true do
|
36
|
+
File.stub :readable?, true do
|
37
|
+
File.stub :read, 'fake_oidc_token' do
|
38
|
+
stub_request(:post, "#{@client.authority}/fake_tenant_id/oauth2/v2.0/token")
|
39
|
+
.to_return(status: 200, body: token_response.to_json)
|
40
|
+
|
41
|
+
credentials = @client.fetch_credentials
|
42
|
+
|
43
|
+
assert_equal 'fake_access_token', credentials.token
|
44
|
+
assert_instance_of Time, credentials.expires_at
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_fetch_credentials_failure
|
51
|
+
ENV['AZURE_TENANT_ID'] = 'fake_tenant_id'
|
52
|
+
ENV['AZURE_CLIENT_ID'] = 'fake_client_id'
|
53
|
+
ENV['AZURE_FEDERATED_TOKEN_FILE'] = 'fake_token_file'
|
54
|
+
|
55
|
+
@client = Fog::AzureRM::Identity::WorkflowIdentityClient.new(@options)
|
56
|
+
|
57
|
+
File.stub :exist?, true do
|
58
|
+
File.stub :readable?, true do
|
59
|
+
File.stub :read, 'fake_oidc_token' do
|
60
|
+
stub_request(:post, "#{@client.authority}/fake_tenant_id/oauth2/v2.0/token")
|
61
|
+
.to_raise(Faraday::Error)
|
62
|
+
|
63
|
+
assert_raises(Fog::AzureRM::Identity::BaseClient::FetchCredentialsError) do
|
64
|
+
@client.fetch_credentials
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def test_fetch_credentials_unauthorized
|
72
|
+
ENV['AZURE_TENANT_ID'] = 'fake_tenant_id'
|
73
|
+
ENV['AZURE_CLIENT_ID'] = 'fake_client_id'
|
74
|
+
ENV['AZURE_FEDERATED_TOKEN_FILE'] = 'fake_token_file'
|
75
|
+
|
76
|
+
@client = Fog::AzureRM::Identity::WorkflowIdentityClient.new(@options)
|
77
|
+
|
78
|
+
File.stub :exist?, true do
|
79
|
+
File.stub :readable?, true do
|
80
|
+
File.stub :read, 'fake_oidc_token' do
|
81
|
+
stub_request(:post, "#{@client.authority}/fake_tenant_id/oauth2/v2.0/token")
|
82
|
+
.to_return(status: 403, body: '{"error":"invalid_client"}')
|
83
|
+
|
84
|
+
assert_raises(Fog::AzureRM::Identity::BaseClient::FetchCredentialsError) { @client.fetch_credentials }
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def test_fetch_credentials_missing_env_vars
|
91
|
+
ENV['AZURE_TENANT_ID'] = nil
|
92
|
+
ENV['AZURE_CLIENT_ID'] = nil
|
93
|
+
ENV['AZURE_FEDERATED_TOKEN_FILE'] = nil
|
94
|
+
|
95
|
+
assert_nil @client.fetch_credentials
|
96
|
+
end
|
97
|
+
|
98
|
+
def test_fetch_credentials_missing_token_file
|
99
|
+
ENV['AZURE_TENANT_ID'] = 'fake_tenant_id'
|
100
|
+
ENV['AZURE_CLIENT_ID'] = 'fake_client_id'
|
101
|
+
ENV['AZURE_FEDERATED_TOKEN_FILE'] = 'nonexistent_file'
|
102
|
+
|
103
|
+
File.stub :exist?, false do
|
104
|
+
assert_nil @client.fetch_credentials
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def test_authority
|
109
|
+
ENV['AZURE_TENANT_ID'] = 'fake_tenant_id'
|
110
|
+
ENV['AZURE_CLIENT_ID'] = 'fake_client_id'
|
111
|
+
ENV['AZURE_FEDERATED_TOKEN_FILE'] = 'fake_token_file'
|
112
|
+
|
113
|
+
# Test with default AzureCloud environment
|
114
|
+
client = Fog::AzureRM::Identity::WorkflowIdentityClient.new(@options)
|
115
|
+
assert_equal 'https://login.microsoftonline.com', client.authority
|
116
|
+
assert_equal 'https://storage.azure.com', client.resource
|
117
|
+
|
118
|
+
# Test with AzureUSGovernment environment
|
119
|
+
gov_options = { environment: 'AzureUSGovernment' }
|
120
|
+
gov_client = Fog::AzureRM::Identity::WorkflowIdentityClient.new(gov_options)
|
121
|
+
assert_equal 'https://login.microsoftonline.us', gov_client.authority
|
122
|
+
assert_equal 'https://storage.azure.com', gov_client.resource
|
123
|
+
|
124
|
+
# Test with AzureChina environment
|
125
|
+
china_options = { environment: 'AzureChinaCloud' }
|
126
|
+
china_client = Fog::AzureRM::Identity::WorkflowIdentityClient.new(china_options)
|
127
|
+
assert_equal 'https://login.chinacloudapi.cn', china_client.authority
|
128
|
+
assert_equal 'https://storage.azure.com', china_client.resource
|
129
|
+
|
130
|
+
# Test with AzureGermanCloud environment
|
131
|
+
german_options = { environment: 'AzureGermanCloud' }
|
132
|
+
german_client = Fog::AzureRM::Identity::WorkflowIdentityClient.new(german_options)
|
133
|
+
assert_equal 'https://login.microsoftonline.de', german_client.authority
|
134
|
+
assert_equal 'https://storage.azure.com', german_client.resource
|
135
|
+
end
|
136
|
+
|
137
|
+
def test_authority_normalize
|
138
|
+
ENV['AZURE_TENANT_ID'] = 'fake_tenant_id'
|
139
|
+
ENV['AZURE_CLIENT_ID'] = 'fake_client_id'
|
140
|
+
ENV['AZURE_FEDERATED_TOKEN_FILE'] = 'fake_token_file'
|
141
|
+
ENV['AZURE_AUTHORITY_HOST'] = 'login.microsoftonline.com'
|
142
|
+
|
143
|
+
# Test with default AzureCloud environment
|
144
|
+
client = Fog::AzureRM::Identity::WorkflowIdentityClient.new(@options)
|
145
|
+
assert_equal 'https://login.microsoftonline.com', client.authority
|
146
|
+
end
|
147
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gitlab-fog-azure-rm
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shaffan Chaudhry
|
@@ -15,10 +15,9 @@ authors:
|
|
15
15
|
- Azeem Sajid
|
16
16
|
- Maham Nazir
|
17
17
|
- Abbas Sheikh
|
18
|
-
autorequire:
|
19
18
|
bindir: bin
|
20
19
|
cert_chain: []
|
21
|
-
date:
|
20
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
22
21
|
dependencies:
|
23
22
|
- !ruby/object:Gem::Dependency
|
24
23
|
name: codeclimate-test-reporter
|
@@ -90,6 +89,20 @@ dependencies:
|
|
90
89
|
- - ">="
|
91
90
|
- !ruby/object:Gem::Version
|
92
91
|
version: '0'
|
92
|
+
- !ruby/object:Gem::Dependency
|
93
|
+
name: webmock
|
94
|
+
requirement: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
type: :development
|
100
|
+
prerelease: false
|
101
|
+
version_requirements: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
93
106
|
- !ruby/object:Gem::Dependency
|
94
107
|
name: webrick
|
95
108
|
requirement: !ruby/object:Gem::Requirement
|
@@ -224,7 +237,6 @@ dependencies:
|
|
224
237
|
version: 1.10.8
|
225
238
|
description: This is a stripped-down fork of fog-azure-rm that enables Azure Blob
|
226
239
|
Storage to be used with CarrierWave and Fog.
|
227
|
-
email:
|
228
240
|
executables: []
|
229
241
|
extensions: []
|
230
242
|
extra_rdoc_files:
|
@@ -321,6 +333,12 @@ files:
|
|
321
333
|
- lib/fog/azurerm/custom_fog_errors.rb
|
322
334
|
- lib/fog/azurerm/docs/storage.md
|
323
335
|
- lib/fog/azurerm/docs/structure.md
|
336
|
+
- lib/fog/azurerm/identity.rb
|
337
|
+
- lib/fog/azurerm/identity/base_client.rb
|
338
|
+
- lib/fog/azurerm/identity/credentials.rb
|
339
|
+
- lib/fog/azurerm/identity/default_credentials.rb
|
340
|
+
- lib/fog/azurerm/identity/managed_identity_client.rb
|
341
|
+
- lib/fog/azurerm/identity/workflow_identity_client.rb
|
324
342
|
- lib/fog/azurerm/identity_encoding_filter.rb
|
325
343
|
- lib/fog/azurerm/models/storage/directories.rb
|
326
344
|
- lib/fog/azurerm/models/storage/directory.rb
|
@@ -430,6 +448,10 @@ files:
|
|
430
448
|
- test/requests/storage/test_save_page_blob.rb
|
431
449
|
- test/requests/storage/test_wait_blob_copy_operation_to_finish.rb
|
432
450
|
- test/test_helper.rb
|
451
|
+
- test/unit/test_default_credentials.rb
|
452
|
+
- test/unit/test_managed_identity_client.rb
|
453
|
+
- test/unit/test_shared_access_signature_generator.rb
|
454
|
+
- test/unit/test_workflow_identity_client.rb
|
433
455
|
homepage: https://gitlab.com/gitlab-org/gitlab-fog-azure-rm
|
434
456
|
licenses:
|
435
457
|
- MIT
|
@@ -450,8 +472,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
450
472
|
- !ruby/object:Gem::Version
|
451
473
|
version: '0'
|
452
474
|
requirements: []
|
453
|
-
rubygems_version: 3.
|
454
|
-
signing_key:
|
475
|
+
rubygems_version: 3.7.2
|
455
476
|
specification_version: 4
|
456
477
|
summary: Module for the 'fog' gem to support Azure Blob Storage with CarrierWave and
|
457
478
|
Fog.
|