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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4d92167cf3193f73d9c97ae76465057206f076c4f2a14e1f8a12c677d77ca2f0
4
- data.tar.gz: 04fce79fa98d5f6dbb512dc974678be18ccb45cc84921c6914e61a60f4a6a62c
3
+ metadata.gz: fa5ac286afc5ba0f3066194079050eb837ec86bb0e23a0e879ae34b6ccd18242
4
+ data.tar.gz: cc8affe1faf1d79f910c7d7e232386b592472981738a3122922ad370d4406294
5
5
  SHA512:
6
- metadata.gz: a4eca952fa0daa27b5149654960ae75b4b875d128114388f2cc539b3a3e1bdd2874e03456f60b8da7a2b05f5d994cdcd58962940e2abc1c328bdddfec2f86960
7
- data.tar.gz: b503003e15965b9076b1ad75b25fd1282dfae71f4eacd806be13c6bc004a9bad5843c491c568fe4ff7cc42e09f9d6ac367c4fc7db9b27efa62288badb5f3477b
6
+ metadata.gz: 90faba724967de441a00eef32538b14dfbfbbce8a225d7052c965bd69110ab434316484da41b6b6de6e8ad3c7bf3cb5009cd25d634419f402a5fa31616f4764b
7
+ data.tar.gz: 7e9a772cdd278514751f4e3cd167ebe54c4c63b451c95ea701f48b475b5810638a8b847c6724e3ccf707152dbd4e509d45d2fe5ce897a2173ea88b9d5ea80c93
data/.rubocop.yml CHANGED
@@ -6,3 +6,7 @@ AllCops:
6
6
  Exclude:
7
7
  - 'lib/azure/**/*'
8
8
  - 'vendor/**/*'
9
+
10
+ Style/Documentation:
11
+ Exclude:
12
+ - 'test/**/*'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## Unreleased
2
2
 
3
+ ## 2.3.0
4
+
5
+ - Bump default Azure Blob and SAS API versions to 2025-07-05 !56
6
+
7
+ ## 2.2.0
8
+
9
+ - Add support for workflow and managed identities !54
10
+
3
11
  ## 2.1.0
4
12
 
5
13
  - Drop IPAddr patches !52
@@ -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 = "2018-11-09"
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 = "2018-11-09"
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
@@ -0,0 +1,5 @@
1
+ require_relative 'identity/base_client'
2
+ require_relative 'identity/credentials'
3
+ require_relative 'identity/default_credentials'
4
+ require_relative 'identity/managed_identity_client'
5
+ require_relative 'identity/workflow_identity_client'
@@ -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
- return unless @azure_storage_account_name != options[:azure_storage_account_name] ||
97
- @azure_storage_access_key != options[:azure_storage_access_key] ||
98
- @azure_storage_token_signer != options[:azure_storage_token_signer]
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
- @azure_storage_token_signer = options[:azure_storage_token_signer]
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)}"
@@ -1,5 +1,5 @@
1
1
  module Fog
2
2
  module AzureRM
3
- VERSION = '2.1.0'.freeze
3
+ VERSION = '2.3.0'.freeze
4
4
  end
5
5
  end
data/lib/fog/azurerm.rb CHANGED
@@ -9,6 +9,7 @@ require 'fog/json'
9
9
  require 'fog/azurerm/models/storage/sku_name'
10
10
  require 'fog/azurerm/models/storage/sku_tier'
11
11
  require 'fog/azurerm/models/storage/kind'
12
+ require 'fog/azurerm/identity'
12
13
 
13
14
  module Fog
14
15
  # Main AzureRM fog Provider Module
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
- assert_equal nil, @directory.attributes[:acl]
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 additonal expected_user_delegation_key_starts
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.1.0
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: 2024-08-17 00:00:00.000000000 Z
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.3.27
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.