gitlab-fog-azure-rm 2.3.0 → 2.4.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: fa5ac286afc5ba0f3066194079050eb837ec86bb0e23a0e879ae34b6ccd18242
4
- data.tar.gz: cc8affe1faf1d79f910c7d7e232386b592472981738a3122922ad370d4406294
3
+ metadata.gz: 61aaa2ad5deaa2d7cfc03c52d87f504147a30bb21656fa8d74a0a1e2c1335b7f
4
+ data.tar.gz: 170d555337f12f9262ecc0a482871d7e88429f0dd968a3d4ee3c0ec3d42300dd
5
5
  SHA512:
6
- metadata.gz: 90faba724967de441a00eef32538b14dfbfbbce8a225d7052c965bd69110ab434316484da41b6b6de6e8ad3c7bf3cb5009cd25d634419f402a5fa31616f4764b
7
- data.tar.gz: 7e9a772cdd278514751f4e3cd167ebe54c4c63b451c95ea701f48b475b5810638a8b847c6724e3ccf707152dbd4e509d45d2fe5ce897a2173ea88b9d5ea80c93
6
+ metadata.gz: c5bf104672cd6913ce0c701bc8da7da11d0e80a3a383adf9be76188712b5d2cdcc3f13ea1512c12775226999c266c357ca9b0a1bb01e03196773beb4a1f6c1d9
7
+ data.tar.gz: 354cd43f816764b22005eb1d0c4e89eecfd55d31b776107904e8f3b1c704b09bd596244517dba24869a2552258cbe98477daaec61ab7fd0042ecd9d21e58dc11
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  ## Unreleased
2
2
 
3
+ ## 2.4.0
4
+
5
+ - Refresh blob client whenever credentials needs to be refreshed !58
6
+
3
7
  ## 2.3.0
4
8
 
5
9
  - Bump default Azure Blob and SAS API versions to 2025-07-05 !56
@@ -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 'timecop'
25
26
  spec.add_development_dependency 'webmock'
26
27
  spec.add_development_dependency 'webrick', '~> 1.8'
27
28
 
@@ -15,7 +15,7 @@ module Fog
15
15
  def refresh_needed?
16
16
  return true unless expires_at
17
17
 
18
- Time.now >= expires_at + EXPIRATION_BUFFER
18
+ Time.now >= expires_at - EXPIRATION_BUFFER
19
19
  end
20
20
  end
21
21
  end
@@ -11,7 +11,6 @@ module Fog
11
11
  def initialize(options)
12
12
  @options = options
13
13
  @credential_client = nil
14
- @credentials = nil
15
14
  end
16
15
 
17
16
  def fetch_credentials_if_needed
@@ -37,7 +36,7 @@ module Fog
37
36
  client = klass.new(options)
38
37
 
39
38
  begin
40
- credentials = client.fetch_credentials
39
+ credentials = client.fetch_credentials_if_needed
41
40
  rescue Fog::AzureRM::Identity::BaseClient::FetchCredentialsError
42
41
  next
43
42
  end
@@ -48,10 +47,7 @@ module Fog
48
47
  end
49
48
  end
50
49
 
51
- return unless credentials
52
-
53
- @credentials = credentials
54
- @credentials
50
+ @credential_client
55
51
  end
56
52
  end
57
53
  end
@@ -100,6 +100,9 @@ module Fog
100
100
  raise e.message
101
101
  end
102
102
 
103
+ @user_delegation_key_mutex = Mutex.new
104
+ @blob_client_mutex = Mutex.new
105
+
103
106
  options[:environment] = options[:environment] || ENV['AZURE_ENVIRONMENT'] || ENVIRONMENT_AZURE_CLOUD
104
107
  @environment = options[:environment]
105
108
  @options = options
@@ -114,24 +117,7 @@ module Fog
114
117
  @azure_storage_endpoint = options[:azure_storage_endpoint]
115
118
  @azure_storage_domain = options[:azure_storage_domain]
116
119
 
117
- storage_blob_host =
118
- @azure_storage_endpoint ||
119
- if @azure_storage_domain.nil? || @azure_storage_domain.empty?
120
- get_blob_endpoint(@azure_storage_account_name, true, @environment)
121
- else
122
- get_blob_endpoint_with_domain(@azure_storage_account_name, true, @azure_storage_domain)
123
- end
124
-
125
- azure_client = Azure::Storage::Common::Client.create({
126
- storage_account_name: @azure_storage_account_name,
127
- storage_access_key: @azure_storage_access_key,
128
- signer: @azure_storage_token_signer
129
- }.compact)
130
- azure_client.storage_blob_host = storage_blob_host
131
- @blob_client = Azure::Storage::Blob::BlobService.new(client: azure_client, api_version: @api_version)
132
- @blob_client.with_filter(Fog::AzureRM::IdentityEncodingFilter.new)
133
- @blob_client.with_filter(Azure::Storage::Common::Core::Filter::ExponentialRetryPolicyFilter.new)
134
- @blob_client.with_filter(Azure::Core::Http::DebugFilter.new) if @debug
120
+ refresh_blob_client
135
121
  end
136
122
 
137
123
  private
@@ -177,11 +163,16 @@ module Fog
177
163
  return nil unless @azure_storage_token_signer
178
164
 
179
165
  if @credential_client
180
- @credentials = @credential_client.fetch_credentials_if_needed
181
- @azure_storage_token_signer = token_signer
166
+ new_credentials = @credential_client.fetch_credentials_if_needed
167
+ changed = new_credentials != @credentials
168
+
169
+ if changed
170
+ @credentials = new_credentials
171
+ @azure_storage_token_signer = token_signer
172
+ refresh_blob_client
173
+ end
182
174
  end
183
175
 
184
- @user_delegation_key_mutex ||= Mutex.new
185
176
  @user_delegation_key_mutex.synchronize do
186
177
  if @user_delegation_key_expiry.nil? || @user_delegation_key_expiry < requested_expiry
187
178
  start = Time.now
@@ -197,6 +188,35 @@ module Fog
197
188
 
198
189
  @user_delegation_key
199
190
  end
191
+
192
+ def refresh_blob_client
193
+ @blob_client_mutex.synchronize do
194
+ azure_client = create_azure_client
195
+ azure_client.storage_blob_host = storage_blob_host
196
+ @blob_client = Azure::Storage::Blob::BlobService.new(client: azure_client, api_version: @api_version)
197
+ @blob_client.with_filter(Fog::AzureRM::IdentityEncodingFilter.new)
198
+ @blob_client.with_filter(Azure::Storage::Common::Core::Filter::ExponentialRetryPolicyFilter.new)
199
+ @blob_client.with_filter(Azure::Core::Http::DebugFilter.new) if @debug
200
+ end
201
+ end
202
+
203
+ def storage_blob_host
204
+ return @azure_storage_endpoint if @azure_storage_endpoint
205
+
206
+ if @azure_storage_domain.nil? || @azure_storage_domain.empty?
207
+ get_blob_endpoint(@azure_storage_account_name, true, @environment)
208
+ else
209
+ get_blob_endpoint_with_domain(@azure_storage_account_name, true, @azure_storage_domain)
210
+ end
211
+ end
212
+
213
+ def create_azure_client
214
+ Azure::Storage::Common::Client.create({
215
+ storage_account_name: @azure_storage_account_name,
216
+ storage_access_key: @azure_storage_access_key,
217
+ signer: @azure_storage_token_signer
218
+ }.compact)
219
+ end
200
220
  end
201
221
  end
202
222
  end
@@ -1,5 +1,5 @@
1
1
  module Fog
2
2
  module AzureRM
3
- VERSION = '2.3.0'.freeze
3
+ VERSION = '2.4.0'.freeze
4
4
  end
5
5
  end
@@ -224,4 +224,74 @@ class TestGetBlobHttpsUrl < Minitest::Test
224
224
  assert_equal "#{@url}?#{@token}", @mock_service.get_blob_https_url('test_container', 'test_blob', Time.now.utc + 3600)
225
225
  assert_equal "#{@url}?#{@token}", @mock_service.get_object_url('test_container', 'test_blob', Time.now.utc + 3600)
226
226
  end
227
+
228
+ def test_blob_client_refreshes_when_credentials_expire
229
+ # Setup an expired initial token
230
+ initial_token_response = {
231
+ 'access_token' => 'initial_token',
232
+ 'expires_on' => (Time.now + ::Fog::AzureRM::Identity::Credentials::EXPIRATION_BUFFER + 10).to_i
233
+ }
234
+
235
+ # Setup refreshed token response
236
+ refreshed_token_response = {
237
+ 'access_token' => 'refreshed_token',
238
+ 'expires_on' => (Time.now + ::Fog::AzureRM::Identity::Credentials::EXPIRATION_BUFFER * 3).to_i
239
+ }
240
+
241
+ # Mock the identity endpoint to return initial token first, then refreshed token
242
+ stub_request(:get, "#{Fog::AzureRM::Identity::IDENTITY_ENDPOINT}?api-version=#{Fog::AzureRM::Identity::API_VERSION}&resource=https://storage.azure.com")
243
+ .with(headers: { 'Metadata' => 'true' })
244
+ .to_return(
245
+ { status: 200, body: initial_token_response.to_json },
246
+ { status: 200, body: refreshed_token_response.to_json }
247
+ )
248
+
249
+ # Setup user delegation key responses
250
+ delegation_response = <<~MSG
251
+ <UserDelegationKey>
252
+ <SignedOid>f81d4fae-7dec-11d0-a765-00a0c91e6bf6</SignedOid>
253
+ <SignedTid>72f988bf-86f1-41af-91ab-2d7cd011db47</SignedTid>
254
+ <SignedStart>2024-09-19T00:00:00Z</SignedStart>
255
+ <SignedExpiry>2024-09-26T00:00:00Z</SignedExpiry>
256
+ <SignedService>b</SignedService>
257
+ <SignedVersion>2020-02-10</SignedVersion>
258
+ <Value>UDELEGATIONKEY_INITIAL</Value>
259
+ <SignedKey>rL7...INITIAL</SignedKey>
260
+ </UserDelegationKey>
261
+ MSG
262
+
263
+ stub_request(:post, 'https://mockaccount.blob.core.windows.net?comp=userdelegationkey&restype=service')
264
+ .to_return(
265
+ { status: 200, headers: { 'Content-Type': 'application/xml' }, body: delegation_response }
266
+ )
267
+
268
+ service = Fog::AzureRM::Storage.new(storage_account_managed_identity)
269
+ initial_blob_client = service.instance_variable_get(:@blob_client)
270
+ refute_nil initial_blob_client
271
+
272
+ # First request should not refresh the blob client and token
273
+ requested_expiry = Time.now + 60
274
+ url1 = service.get_blob_https_url('test_container', 'test_blob', requested_expiry)
275
+ refute_nil url1
276
+ assert url1.include?('https://mockaccount.blob.core.windows.net/test_container/test_blob')
277
+
278
+ second_blob_client = service.instance_variable_get(:@blob_client)
279
+ assert_equal initial_blob_client.object_id, second_blob_client.object_id
280
+
281
+ Timecop.travel(Time.now + ::Fog::AzureRM::Identity::Credentials::EXPIRATION_BUFFER) do
282
+ credentials = service.instance_variable_get(:@credentials)
283
+ assert credentials.refresh_needed?
284
+
285
+ # Second request should tigger credential refresh and blob client update
286
+ url2 = service.get_blob_https_url('test_container', 'test_blob', requested_expiry)
287
+ third_blob_client = service.instance_variable_get(:@blob_client)
288
+ credentials = service.instance_variable_get(:@credentials)
289
+ refute credentials.refresh_needed?
290
+
291
+ refute_nil url2
292
+ assert url2.include?('https://mockaccount.blob.core.windows.net/test_container/test_blob')
293
+ refute_nil third_blob_client
294
+ refute_equal third_blob_client.object_id, second_blob_client.object_id
295
+ end
296
+ end
227
297
  end
data/test/test_helper.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require 'timecop'
1
2
  require 'webmock/minitest'
2
3
  WebMock.disable_net_connect! allow: %w[127.0.0.1]
3
4
 
@@ -0,0 +1,44 @@
1
+ require File.expand_path '../test_helper', __dir__
2
+
3
+ class TestCredentials < Minitest::Test
4
+ def setup
5
+ @token = 'test-access-token'
6
+ @expires_at = Time.now + 3600 # 1 hour from now
7
+ @credentials = Fog::AzureRM::Identity::Credentials.new(@token, @expires_at)
8
+ end
9
+
10
+ def test_initialize
11
+ assert_equal @token, @credentials.token
12
+ assert_equal @expires_at, @credentials.expires_at
13
+ end
14
+
15
+ def test_refresh_needed_when_expires_at_is_nil
16
+ credentials = Fog::AzureRM::Identity::Credentials.new(@token, nil)
17
+ assert credentials.refresh_needed?
18
+ end
19
+
20
+ def test_refresh_needed_when_token_expires_soon
21
+ # Token expires in 5 minutes (less than 10 minute buffer)
22
+ expires_soon = Time.now + 300
23
+ credentials = Fog::AzureRM::Identity::Credentials.new(@token, expires_soon)
24
+ assert credentials.refresh_needed?
25
+ end
26
+
27
+ def test_refresh_not_needed_when_token_has_time_left
28
+ # Token expires in 15 minutes (more than 10 minute buffer)
29
+ expires_later = Time.now + 900
30
+ credentials = Fog::AzureRM::Identity::Credentials.new(@token, expires_later)
31
+ refute credentials.refresh_needed?
32
+ end
33
+
34
+ def test_refresh_needed_exactly_at_buffer_time
35
+ # Token expires exactly at buffer time (10 minutes)
36
+ expires_at_buffer = Time.now + Fog::AzureRM::Identity::Credentials::EXPIRATION_BUFFER
37
+ credentials = Fog::AzureRM::Identity::Credentials.new(@token, expires_at_buffer)
38
+ assert credentials.refresh_needed?
39
+ end
40
+
41
+ def test_expiration_buffer_constant
42
+ assert_equal 600, Fog::AzureRM::Identity::Credentials::EXPIRATION_BUFFER
43
+ end
44
+ end
@@ -9,7 +9,6 @@ class TestDefaultCredentials < Minitest::Test
9
9
  def test_initialize
10
10
  assert_instance_of Fog::AzureRM::DefaultCredentials, @default_credentials
11
11
  assert_nil @default_credentials.instance_variable_get(:@credential_client)
12
- assert_nil @default_credentials.instance_variable_get(:@credentials)
13
12
  end
14
13
 
15
14
  def test_fetch_credentials_if_needed_with_no_credential_client
@@ -28,70 +27,55 @@ class TestDefaultCredentials < Minitest::Test
28
27
  end
29
28
 
30
29
  def test_credential_client_with_workflow_identity
31
- mock_workflow_client = Minitest::Mock.new
32
- mock_workflow_client.expect :fetch_credentials, 'fake_workflow_credentials'
30
+ workflow_client = Struct.new(:fetch_credentials_if_needed).new('fake_workflow_credentials')
33
31
 
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
32
+ Fog::AzureRM::Identity::WorkflowIdentityClient.stub :new, workflow_client do
33
+ client = @default_credentials.send(:credential_client)
34
+ assert_equal workflow_client, client
35
+ assert_equal workflow_client, @default_credentials.instance_variable_get(:@credential_client)
37
36
  end
38
-
39
- mock_workflow_client.verify
40
37
  end
41
38
 
42
39
  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
40
+ workflow_client = Struct.new(:fetch_credentials_if_needed).new
41
+ managed_client = Struct.new(:fetch_credentials_if_needed).new('fake_managed_credentials')
42
+
43
+ Fog::AzureRM::Identity::WorkflowIdentityClient.stub :new, workflow_client do
44
+ Fog::AzureRM::Identity::ManagedIdentityClient.stub :new, managed_client do
45
+ client = @default_credentials.send(:credential_client)
46
+ assert_equal managed_client, client
47
+ assert_equal managed_client, @default_credentials.instance_variable_get(:@credential_client)
53
48
  end
54
49
  end
55
-
56
- mock_workflow_client.verify
57
- mock_managed_client.verify
58
50
  end
59
51
 
60
52
  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
53
+ workflow_client = Class.new do
54
+ def fetch_credentials_if_needed
55
+ raise ::Fog::AzureRM::Identity::BaseClient::FetchCredentialsError, 'Failed to fetch credentials'
56
+ end
57
+ end.new
65
58
 
66
- mock_managed_client = Minitest::Mock.new
67
- mock_managed_client.expect :fetch_credentials, 'fake_managed_credentials'
59
+ managed_client = Struct.new(:fetch_credentials_if_needed).new('fake_managed_credentials')
68
60
 
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
61
+ Fog::AzureRM::Identity::WorkflowIdentityClient.stub :new, workflow_client do
62
+ Fog::AzureRM::Identity::ManagedIdentityClient.stub :new, managed_client do
63
+ client = @default_credentials.send(:credential_client)
64
+ assert_equal managed_client, client
65
+ assert_equal managed_client, @default_credentials.instance_variable_get(:@credential_client)
73
66
  end
74
67
  end
75
-
76
- mock_workflow_client.verify
77
- mock_managed_client.verify
78
68
  end
79
69
 
80
70
  def test_credential_client_with_no_credentials
81
- mock_workflow_client = Minitest::Mock.new
82
- mock_workflow_client.expect :fetch_credentials, nil
71
+ workflow_client = Struct.new(:fetch_credentials_if_needed).new
72
+ managed_client = Struct.new(:fetch_credentials_if_needed).new
83
73
 
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
74
+ Fog::AzureRM::Identity::WorkflowIdentityClient.stub :new, workflow_client do
75
+ Fog::AzureRM::Identity::ManagedIdentityClient.stub :new, managed_client do
89
76
  assert_nil @default_credentials.send(:credential_client)
90
77
  assert_nil @default_credentials.instance_variable_get(:@credential_client)
91
78
  end
92
79
  end
93
-
94
- mock_workflow_client.verify
95
- mock_managed_client.verify
96
80
  end
97
81
  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.3.0
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shaffan Chaudhry
@@ -15,9 +15,10 @@ authors:
15
15
  - Azeem Sajid
16
16
  - Maham Nazir
17
17
  - Abbas Sheikh
18
+ autorequire:
18
19
  bindir: bin
19
20
  cert_chain: []
20
- date: 1980-01-02 00:00:00.000000000 Z
21
+ date: 2025-09-30 00:00:00.000000000 Z
21
22
  dependencies:
22
23
  - !ruby/object:Gem::Dependency
23
24
  name: codeclimate-test-reporter
@@ -89,6 +90,20 @@ dependencies:
89
90
  - - ">="
90
91
  - !ruby/object:Gem::Version
91
92
  version: '0'
93
+ - !ruby/object:Gem::Dependency
94
+ name: timecop
95
+ requirement: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ type: :development
101
+ prerelease: false
102
+ version_requirements: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
92
107
  - !ruby/object:Gem::Dependency
93
108
  name: webmock
94
109
  requirement: !ruby/object:Gem::Requirement
@@ -237,6 +252,7 @@ dependencies:
237
252
  version: 1.10.8
238
253
  description: This is a stripped-down fork of fog-azure-rm that enables Azure Blob
239
254
  Storage to be used with CarrierWave and Fog.
255
+ email:
240
256
  executables: []
241
257
  extensions: []
242
258
  extra_rdoc_files:
@@ -448,6 +464,7 @@ files:
448
464
  - test/requests/storage/test_save_page_blob.rb
449
465
  - test/requests/storage/test_wait_blob_copy_operation_to_finish.rb
450
466
  - test/test_helper.rb
467
+ - test/unit/test_credentials.rb
451
468
  - test/unit/test_default_credentials.rb
452
469
  - test/unit/test_managed_identity_client.rb
453
470
  - test/unit/test_shared_access_signature_generator.rb
@@ -472,7 +489,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
472
489
  - !ruby/object:Gem::Version
473
490
  version: '0'
474
491
  requirements: []
475
- rubygems_version: 3.7.2
492
+ rubygems_version: 3.5.22
493
+ signing_key:
476
494
  specification_version: 4
477
495
  summary: Module for the 'fog' gem to support Azure Blob Storage with CarrierWave and
478
496
  Fog.