gitlab-fog-azure-rm 2.5.0 → 2.5.1
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/CHANGELOG.md +4 -0
- data/lib/fog/azurerm/storage.rb +124 -47
- data/lib/fog/azurerm/version.rb +1 -1
- data/test/requests/storage/test_get_blob_https_url.rb +54 -48
- data/test/requests/storage/test_storage_thread_safety.rb +138 -0
- data/test/test_helper.rb +15 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: faf6ef8f40417712f91769e2ac0dea345c7451f1457919399c1aa574cf6a44a5
|
|
4
|
+
data.tar.gz: 4596861b6a717a743ff2d250f91f3bb4cafd911cb57e0efeeae14368e04162eb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f59d2e0764335c68f69462f380781e3a6f395467959f8af92f076dd4492f968d82bdc52b471f93a3fb1568e939c277e70cf6eac8fc79ed65e144b53eb1d966a2
|
|
7
|
+
data.tar.gz: ea77ec4d01cff9bebec6642ad94517c0df51246de8ab41f48241f8281841737c08da6f20ec17e9c79a9ad5d6f56bc51940c8cac169a17d226f0037c84ea5c706
|
data/CHANGELOG.md
CHANGED
data/lib/fog/azurerm/storage.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
+
require 'time'
|
|
4
5
|
|
|
5
6
|
module Fog
|
|
6
7
|
module AzureRM
|
|
@@ -87,6 +88,25 @@ module Fog
|
|
|
87
88
|
|
|
88
89
|
attr_accessor :options
|
|
89
90
|
|
|
91
|
+
# Immutable snapshot of a built SharedAccessSignature and the delegation
|
|
92
|
+
# key expiry it is valid until. It is published by a single reference
|
|
93
|
+
# assignment so signature_client can read it on a lock-free fast path.
|
|
94
|
+
# A nil expiry means the signature never expires (shared-key auth, which
|
|
95
|
+
# signs with the access key rather than a user delegation key).
|
|
96
|
+
class SignatureState
|
|
97
|
+
attr_reader :client, :expiry
|
|
98
|
+
|
|
99
|
+
def initialize(client, expiry)
|
|
100
|
+
@client = client
|
|
101
|
+
@expiry = expiry
|
|
102
|
+
freeze
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def covers?(requested_expiry)
|
|
106
|
+
expiry.nil? || expiry >= requested_expiry
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
90
110
|
def initialize(options)
|
|
91
111
|
begin
|
|
92
112
|
require 'azure/storage/common'
|
|
@@ -100,8 +120,8 @@ module Fog
|
|
|
100
120
|
raise e.message
|
|
101
121
|
end
|
|
102
122
|
|
|
103
|
-
@
|
|
104
|
-
@
|
|
123
|
+
@credential_mutex = Mutex.new
|
|
124
|
+
@signature_mutex = Mutex.new
|
|
105
125
|
|
|
106
126
|
options[:environment] = options[:environment] || ENV['AZURE_ENVIRONMENT'] || ENVIRONMENT_AZURE_CLOUD
|
|
107
127
|
@environment = options[:environment]
|
|
@@ -117,7 +137,7 @@ module Fog
|
|
|
117
137
|
@azure_storage_endpoint = options[:azure_storage_endpoint]
|
|
118
138
|
@azure_storage_domain = options[:azure_storage_domain]
|
|
119
139
|
|
|
120
|
-
|
|
140
|
+
@blob_client = build_blob_client(@azure_storage_token_signer)
|
|
121
141
|
end
|
|
122
142
|
|
|
123
143
|
private
|
|
@@ -142,62 +162,119 @@ module Fog
|
|
|
142
162
|
Azure::Storage::Common::Core::Auth::TokenSigner.new(cred)
|
|
143
163
|
end
|
|
144
164
|
|
|
165
|
+
# Returns a SharedAccessSignature for signing a URL that expires at
|
|
166
|
+
# requested_expiry.
|
|
167
|
+
#
|
|
168
|
+
# A user delegation key is valid for up to a week
|
|
169
|
+
# (MAX_USER_DELEGATION_KEY_SECONDS), so the built signature is cached in
|
|
170
|
+
# a frozen @signature_state snapshot and reused on a lock-free fast path.
|
|
171
|
+
# This keeps signing off the lock in the common case, so an in-flight
|
|
172
|
+
# credential or delegation-key refresh on another thread never stalls it.
|
|
173
|
+
#
|
|
174
|
+
# Only the slow path (no cached signature, or the cached delegation key
|
|
175
|
+
# no longer covers the requested expiry) takes @signature_mutex, where
|
|
176
|
+
# the delegation-key fetch and the snapshot rebuild happen atomically.
|
|
177
|
+
# Holding the lock there also collapses a burst of concurrent refetches
|
|
178
|
+
# into a single fetch.
|
|
145
179
|
def signature_client(requested_expiry)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
180
|
+
# Keep the blob client's credentials fresh without blocking signing;
|
|
181
|
+
# see maybe_refresh_credentials.
|
|
182
|
+
maybe_refresh_credentials
|
|
183
|
+
|
|
184
|
+
state = @signature_state
|
|
185
|
+
return state.client if state&.covers?(requested_expiry)
|
|
186
|
+
|
|
187
|
+
@signature_mutex.synchronize do
|
|
188
|
+
# Re-check: another thread may have rebuilt the snapshot while we
|
|
189
|
+
# were waiting for the lock.
|
|
190
|
+
state = @signature_state
|
|
191
|
+
return state.client if state&.covers?(requested_expiry)
|
|
192
|
+
|
|
193
|
+
delegation_key = fetch_user_delegation_key(requested_expiry)
|
|
194
|
+
client = Azure::Storage::Common::Core::Auth::SharedAccessSignature.new(
|
|
195
|
+
@azure_storage_account_name,
|
|
196
|
+
@azure_storage_access_key.to_s,
|
|
197
|
+
delegation_key
|
|
198
|
+
)
|
|
199
|
+
@signature_state = SignatureState.new(client, @user_delegation_key_expiry)
|
|
200
|
+
client
|
|
153
201
|
end
|
|
154
|
-
|
|
155
|
-
@signature_client ||= Azure::Storage::Common::Core::Auth::SharedAccessSignature.new(
|
|
156
|
-
@azure_storage_account_name,
|
|
157
|
-
access_key,
|
|
158
|
-
user_delegation_key
|
|
159
|
-
)
|
|
160
202
|
end
|
|
161
203
|
|
|
162
|
-
|
|
163
|
-
|
|
204
|
+
# Refreshes the credential-derived state (@credentials, the token signer
|
|
205
|
+
# and the blob client) when the access token is near expiry. Only the
|
|
206
|
+
# SAS URL requests reach this, through signature_client; the other blob
|
|
207
|
+
# operations read @blob_client directly and do not refresh, so this does
|
|
208
|
+
# not by itself keep their token fresh across rotation.
|
|
209
|
+
#
|
|
210
|
+
# The refresh is best-effort and non-blocking: a token fetch can be slow
|
|
211
|
+
# (or hang), so only one thread performs it at a time via a try-lock, and
|
|
212
|
+
# every other thread proceeds with the current, still-valid credentials
|
|
213
|
+
# rather than waiting. This keeps a slow token endpoint from stalling all
|
|
214
|
+
# callers that share the instance.
|
|
215
|
+
#
|
|
216
|
+
# The new signer and client are built into locals first and @credentials
|
|
217
|
+
# is published last. If building raises, @credentials is unchanged so
|
|
218
|
+
# refresh_needed? stays true and the next call retries, rather than
|
|
219
|
+
# leaving the swap half-applied with a stale signer or client.
|
|
220
|
+
def maybe_refresh_credentials
|
|
221
|
+
return unless @credential_client
|
|
222
|
+
|
|
223
|
+
current = @credentials
|
|
224
|
+
return unless current.nil? || current.refresh_needed?
|
|
225
|
+
return unless @credential_mutex.try_lock
|
|
226
|
+
|
|
227
|
+
begin
|
|
228
|
+
return unless @credentials.nil? || @credentials.refresh_needed?
|
|
164
229
|
|
|
165
|
-
if @credential_client
|
|
166
230
|
new_credentials = @credential_client.fetch_credentials_if_needed
|
|
167
|
-
|
|
231
|
+
return if new_credentials.nil? || new_credentials == @credentials
|
|
168
232
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
233
|
+
signer = access_token_signer(new_credentials.token)
|
|
234
|
+
client = build_blob_client(signer)
|
|
235
|
+
|
|
236
|
+
@azure_storage_token_signer = signer
|
|
237
|
+
@blob_client = client
|
|
238
|
+
@credentials = new_credentials
|
|
239
|
+
ensure
|
|
240
|
+
@credential_mutex.unlock
|
|
174
241
|
end
|
|
242
|
+
end
|
|
175
243
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
244
|
+
# Returns the cached user delegation key, fetching a new one when the
|
|
245
|
+
# cache is empty or no longer covers requested_expiry. Must be called
|
|
246
|
+
# while holding @signature_mutex. Returns nil for shared-key auth, where
|
|
247
|
+
# the SAS is signed with the access key instead.
|
|
248
|
+
def fetch_user_delegation_key(requested_expiry)
|
|
249
|
+
return nil unless @azure_storage_token_signer
|
|
250
|
+
|
|
251
|
+
if @user_delegation_key_expiry.nil? || @user_delegation_key_expiry < requested_expiry
|
|
252
|
+
start = Time.now
|
|
253
|
+
expiry = start + Azure::Storage::Blob::BlobConstants::MAX_USER_DELEGATION_KEY_SECONDS
|
|
254
|
+
|
|
255
|
+
@user_delegation_key = @blob_client.get_user_delegation_key(start, expiry)
|
|
256
|
+
# Cache against the expiry Azure granted (signed_expiry), not the one
|
|
257
|
+
# we requested: Azure uses its own clock and may return an earlier
|
|
258
|
+
# time, and keying on the request would over-claim validity and emit
|
|
259
|
+
# SAS URLs that Azure rejects.
|
|
260
|
+
@user_delegation_key_expiry = Time.parse(@user_delegation_key.signed_expiry)
|
|
187
261
|
end
|
|
188
262
|
|
|
189
263
|
@user_delegation_key
|
|
190
264
|
end
|
|
191
265
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
266
|
+
# Builds and returns a fully configured blob client. The caller publishes
|
|
267
|
+
# it with a single reference assignment (see initialize and
|
|
268
|
+
# maybe_refresh_credentials), so a concurrent reader of @blob_client never
|
|
269
|
+
# observes a client that is missing its filters mid-build.
|
|
270
|
+
def build_blob_client(signer)
|
|
271
|
+
azure_client = create_azure_client(signer)
|
|
272
|
+
azure_client.storage_blob_host = storage_blob_host
|
|
273
|
+
client = Azure::Storage::Blob::BlobService.new(client: azure_client, api_version: @api_version)
|
|
274
|
+
client.with_filter(Fog::AzureRM::IdentityEncodingFilter.new)
|
|
275
|
+
client.with_filter(Azure::Storage::Common::Core::Filter::ExponentialRetryPolicyFilter.new)
|
|
276
|
+
client.with_filter(Azure::Core::Http::DebugFilter.new) if @debug
|
|
277
|
+
client
|
|
201
278
|
end
|
|
202
279
|
|
|
203
280
|
def storage_blob_host
|
|
@@ -210,11 +287,11 @@ module Fog
|
|
|
210
287
|
end
|
|
211
288
|
end
|
|
212
289
|
|
|
213
|
-
def create_azure_client
|
|
290
|
+
def create_azure_client(signer)
|
|
214
291
|
Azure::Storage::Common::Client.create({
|
|
215
292
|
storage_account_name: @azure_storage_account_name,
|
|
216
293
|
storage_access_key: @azure_storage_access_key,
|
|
217
|
-
signer:
|
|
294
|
+
signer: signer
|
|
218
295
|
}.compact)
|
|
219
296
|
end
|
|
220
297
|
end
|
data/lib/fog/azurerm/version.rb
CHANGED
|
@@ -118,18 +118,7 @@ class TestGetBlobHttpsUrl < Minitest::Test
|
|
|
118
118
|
|
|
119
119
|
requested_expiry = Time.now + 60
|
|
120
120
|
|
|
121
|
-
response =
|
|
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
|
|
121
|
+
response = user_delegation_key_response
|
|
133
122
|
|
|
134
123
|
stub_request(:post, 'https://mockaccount.blob.core.windows.net?comp=userdelegationkey&restype=service')
|
|
135
124
|
.to_return(status: 200, headers: { 'Content-Type': 'application/xml' }, body: response)
|
|
@@ -159,29 +148,19 @@ class TestGetBlobHttpsUrl < Minitest::Test
|
|
|
159
148
|
|
|
160
149
|
ref_time = Time.now
|
|
161
150
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
151
|
+
# Each entry: [Time.now offset from ref_time, requested-expiry offset from
|
|
152
|
+
# that time, whether a fresh delegation key is expected]. A delegation key
|
|
153
|
+
# is valid for one WEEK from its start.
|
|
154
|
+
plan = [
|
|
155
|
+
[0, 1 * HOUR, true], # initial request
|
|
156
|
+
[5.5 * DAY, 1 * DAY, false], # second request during expiry window
|
|
157
|
+
[6.5 * DAY, 1 * DAY, true], # request extending past current expiry
|
|
158
|
+
[10.5 * DAY, 1 * DAY, false] # second request within new expiry
|
|
159
|
+
]
|
|
165
160
|
|
|
166
|
-
|
|
167
|
-
stubbed_times
|
|
168
|
-
|
|
169
|
-
expected_user_delegation_key_starts << stubbed_times.last
|
|
170
|
-
|
|
171
|
-
# second request during expiry window
|
|
172
|
-
stubbed_times << ref_time + 5.5 * DAY
|
|
173
|
-
requested_expiries << stubbed_times.last + 1 * DAY
|
|
174
|
-
# no additonal expected_user_delegation_key_starts
|
|
175
|
-
|
|
176
|
-
# request extending past current expiry
|
|
177
|
-
stubbed_times << ref_time + 6.5 * DAY
|
|
178
|
-
requested_expiries << stubbed_times.last + 1 * DAY
|
|
179
|
-
expected_user_delegation_key_starts << stubbed_times.last
|
|
180
|
-
|
|
181
|
-
# second request within new expiry
|
|
182
|
-
stubbed_times << ref_time + 10.5 * DAY
|
|
183
|
-
requested_expiries << stubbed_times.last + 1 * DAY
|
|
184
|
-
# no additional expected_user_delegation_key_starts
|
|
161
|
+
stubbed_times = plan.map { |now_offset,| ref_time + now_offset }
|
|
162
|
+
requested_expiries = stubbed_times.each_with_index.map { |time, i| time + plan[i][1] }
|
|
163
|
+
expected_user_delegation_key_starts = stubbed_times.select.with_index { |_, i| plan[i][2] }
|
|
185
164
|
|
|
186
165
|
user_delegation_key_starts = []
|
|
187
166
|
mock_user_delegation_key = lambda do |start, expiry|
|
|
@@ -190,7 +169,7 @@ class TestGetBlobHttpsUrl < Minitest::Test
|
|
|
190
169
|
|
|
191
170
|
key = Azure::Storage::Common::Service::UserDelegationKey.new
|
|
192
171
|
key.signed_start = "start-#{start.to_i}"
|
|
193
|
-
key.signed_expiry =
|
|
172
|
+
key.signed_expiry = expiry.utc.iso8601 # Azure grants the full requested window here
|
|
194
173
|
key.value = 'delegation-key'
|
|
195
174
|
key
|
|
196
175
|
end
|
|
@@ -201,7 +180,7 @@ class TestGetBlobHttpsUrl < Minitest::Test
|
|
|
201
180
|
end
|
|
202
181
|
|
|
203
182
|
requested_expiries.each do
|
|
204
|
-
mock_token_signer.expect(:sign, 'test-signature', [/\Ar\n.+test_blob\n.+\nstart-\d+\
|
|
183
|
+
mock_token_signer.expect(:sign, 'test-signature', [/\Ar\n.+test_blob\n.+\nstart-\d+\n\S+/m])
|
|
205
184
|
end
|
|
206
185
|
|
|
207
186
|
Time.stub :now, -> { stubbed_times.first } do
|
|
@@ -220,6 +199,42 @@ class TestGetBlobHttpsUrl < Minitest::Test
|
|
|
220
199
|
assert_equal expected_user_delegation_key_starts, user_delegation_key_starts
|
|
221
200
|
end
|
|
222
201
|
|
|
202
|
+
# Regression: the delegation-key cache keys on the expiry Azure granted
|
|
203
|
+
# (signed_expiry), not the requested one. Keying on the request would serve a
|
|
204
|
+
# cached key for a window Azure already ended, so this asserts a refetch.
|
|
205
|
+
def test_delegation_key_cache_uses_signed_expiry_not_requested
|
|
206
|
+
service = Fog::AzureRM::Storage.new(storage_account_credentials_with_token_signer)
|
|
207
|
+
blob_client = service.instance_variable_get(:@blob_client)
|
|
208
|
+
|
|
209
|
+
ref_time = Time.now
|
|
210
|
+
granted_expiry = (ref_time + 2 * DAY).utc # Azure grants far less than the 1-week request
|
|
211
|
+
|
|
212
|
+
starts = []
|
|
213
|
+
mock_key = lambda do |start, _expiry|
|
|
214
|
+
starts << start
|
|
215
|
+
key = Azure::Storage::Common::Service::UserDelegationKey.new
|
|
216
|
+
key.signed_start = "start-#{start.to_i}"
|
|
217
|
+
key.signed_expiry = granted_expiry.iso8601
|
|
218
|
+
key.value = 'delegation-key'
|
|
219
|
+
key
|
|
220
|
+
end
|
|
221
|
+
mock_new_signer = ->(_token) { mock_token_signer }
|
|
222
|
+
2.times { mock_token_signer.expect(:sign, 'test-signature', [String]) }
|
|
223
|
+
|
|
224
|
+
Time.stub :now, -> { ref_time } do
|
|
225
|
+
blob_client.stub :get_user_delegation_key, mock_key do
|
|
226
|
+
Azure::Core::Auth::Signer.stub :new, mock_new_signer do
|
|
227
|
+
service.get_blob_https_url('test_container', 'test_blob', ref_time + 1 * HOUR)
|
|
228
|
+
# Requested expiry is past Azure's granted window but within start+MAX.
|
|
229
|
+
service.get_blob_https_url('test_container', 'test_blob', ref_time + 3 * DAY)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
assert_equal 2, starts.length,
|
|
235
|
+
'expected a refetch once the requested expiry exceeded the granted signed_expiry'
|
|
236
|
+
end
|
|
237
|
+
|
|
223
238
|
def test_get_blob_https_url_mock
|
|
224
239
|
assert_equal "#{@url}?#{@token}", @mock_service.get_blob_https_url('test_container', 'test_blob', Time.now.utc + 3600)
|
|
225
240
|
assert_equal "#{@url}?#{@token}", @mock_service.get_object_url('test_container', 'test_blob', Time.now.utc + 3600)
|
|
@@ -247,18 +262,9 @@ class TestGetBlobHttpsUrl < Minitest::Test
|
|
|
247
262
|
)
|
|
248
263
|
|
|
249
264
|
# Setup user delegation key responses
|
|
250
|
-
delegation_response =
|
|
251
|
-
|
|
252
|
-
|
|
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
|
|
265
|
+
delegation_response = user_delegation_key_response(
|
|
266
|
+
value: 'UDELEGATIONKEY_INITIAL', signed_key: 'rL7...INITIAL'
|
|
267
|
+
)
|
|
262
268
|
|
|
263
269
|
stub_request(:post, 'https://mockaccount.blob.core.windows.net?comp=userdelegationkey&restype=service')
|
|
264
270
|
.to_return(
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
require File.expand_path '../../test_helper', __dir__
|
|
2
|
+
require 'uri'
|
|
3
|
+
require 'azure/storage/common'
|
|
4
|
+
require 'azure/storage/blob'
|
|
5
|
+
require 'azure/core/auth/signer'
|
|
6
|
+
|
|
7
|
+
# Tests for the concurrency guarantees of SAS signing. Each drives the public
|
|
8
|
+
# get_blob_https_url (which calls signature_client and maybe_refresh_credentials)
|
|
9
|
+
# and checks one property in isolation, so no private methods or stress loops
|
|
10
|
+
# are needed.
|
|
11
|
+
class TestStorageThreadSafety < Minitest::Test
|
|
12
|
+
CONTAINER = 'test_container'.freeze
|
|
13
|
+
BLOB = 'test_blob'.freeze
|
|
14
|
+
|
|
15
|
+
# A blob client stand-in that needs no network. Optionally parks on its first
|
|
16
|
+
# with_filter call so a test can inspect state while a rebuild is in progress.
|
|
17
|
+
class FakeBlobClient
|
|
18
|
+
def initialize(&on_first_filter)
|
|
19
|
+
@filters = []
|
|
20
|
+
@on_first_filter = on_first_filter
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def with_filter(filter)
|
|
24
|
+
@on_first_filter.call if @filters.empty? && @on_first_filter
|
|
25
|
+
@filters << filter
|
|
26
|
+
self
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def generate_uri(path, *)
|
|
30
|
+
URI.parse("https://mockaccount.blob.core.windows.net/#{path}")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def get_user_delegation_key(*)
|
|
34
|
+
key = Azure::Storage::Common::Service::UserDelegationKey.new
|
|
35
|
+
key.value = 'ZGVsZWdhdGlvbg==' # any string; Base64.decode64 is lenient
|
|
36
|
+
key.signed_expiry = '2999-12-31T23:59:59Z' # far future so the cached key covers the request
|
|
37
|
+
key
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Stands in for @signature_mutex and fails the test if the lock is taken.
|
|
42
|
+
class RaisingMutex
|
|
43
|
+
def synchronize
|
|
44
|
+
raise 'signature_client took the lock when it should have hit the cache'
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def setup
|
|
49
|
+
@service = Fog::AzureRM::Storage.new(storage_account_credentials_with_token_signer)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def in_one_hour
|
|
53
|
+
Time.now.utc + 3600
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def credentials(expires_at)
|
|
57
|
+
Fog::AzureRM::Identity::Credentials.new('token', expires_at)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def credential_client(&fetch)
|
|
61
|
+
client = Object.new
|
|
62
|
+
client.define_singleton_method(:fetch_credentials_if_needed, &fetch)
|
|
63
|
+
client
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# A cached signature that still covers the requested expiry is returned
|
|
67
|
+
# without taking @signature_mutex, so signing never waits on a refresh.
|
|
68
|
+
def test_cached_signature_is_returned_without_locking
|
|
69
|
+
@service.instance_variable_set(:@blob_client, FakeBlobClient.new)
|
|
70
|
+
@service.get_blob_https_url(CONTAINER, BLOB, in_one_hour) # builds and caches it
|
|
71
|
+
|
|
72
|
+
@service.instance_variable_set(:@signature_mutex, RaisingMutex.new)
|
|
73
|
+
|
|
74
|
+
url = @service.get_blob_https_url(CONTAINER, BLOB, in_one_hour)
|
|
75
|
+
assert_match %r{\Ahttps://mockaccount\.blob\.core\.windows\.net/#{CONTAINER}/#{BLOB}\?}, url
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# While one thread is stuck in a slow token fetch, another caller must still
|
|
79
|
+
# return instead of blocking on the credential lock.
|
|
80
|
+
def test_a_slow_credential_refresh_does_not_block_other_callers
|
|
81
|
+
entered_fetch = Queue.new
|
|
82
|
+
finish_fetch = Queue.new
|
|
83
|
+
fresh = credentials(Time.now + 3600)
|
|
84
|
+
|
|
85
|
+
slow_client = credential_client do
|
|
86
|
+
entered_fetch << true
|
|
87
|
+
finish_fetch.pop # simulate a slow or hanging token endpoint
|
|
88
|
+
fresh
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
Azure::Storage::Blob::BlobService.stub(:new, ->(*, **) { FakeBlobClient.new }) do
|
|
92
|
+
@service.instance_variable_set(:@blob_client, FakeBlobClient.new)
|
|
93
|
+
@service.instance_variable_set(:@credential_client, slow_client)
|
|
94
|
+
@service.instance_variable_set(:@credentials, credentials(Time.now - 1))
|
|
95
|
+
|
|
96
|
+
refreshing = Thread.new { @service.get_blob_https_url(CONTAINER, BLOB, in_one_hour) }
|
|
97
|
+
entered_fetch.pop # the refresh now holds @credential_mutex inside the fetch
|
|
98
|
+
|
|
99
|
+
other = Thread.new { @service.get_blob_https_url(CONTAINER, BLOB, in_one_hour) }
|
|
100
|
+
returned = other.join(5)
|
|
101
|
+
|
|
102
|
+
begin
|
|
103
|
+
refute_nil returned, 'a second caller blocked while one thread was refreshing credentials'
|
|
104
|
+
ensure
|
|
105
|
+
finish_fetch << true
|
|
106
|
+
[refreshing, other].each(&:join)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# A rebuilt blob client is published only once it is fully built, so a
|
|
112
|
+
# concurrent reader never sees one that is missing its filters.
|
|
113
|
+
def test_rebuilt_blob_client_is_published_only_when_fully_built
|
|
114
|
+
original = @service.instance_variable_get(:@blob_client)
|
|
115
|
+
fresh = credentials(Time.now + 3600)
|
|
116
|
+
@service.instance_variable_set(:@credential_client, credential_client { fresh })
|
|
117
|
+
@service.instance_variable_set(:@credentials, credentials(Time.now - 1))
|
|
118
|
+
|
|
119
|
+
mid_build = Queue.new
|
|
120
|
+
resume = Queue.new
|
|
121
|
+
half_built = FakeBlobClient.new do
|
|
122
|
+
mid_build << true
|
|
123
|
+
resume.pop
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
Azure::Storage::Blob::BlobService.stub(:new, ->(*, **) { half_built }) do
|
|
127
|
+
builder = Thread.new { @service.get_blob_https_url(CONTAINER, BLOB, in_one_hour) }
|
|
128
|
+
mid_build.pop # the new client exists but its filters are not applied yet
|
|
129
|
+
observed = @service.instance_variable_get(:@blob_client)
|
|
130
|
+
resume << true
|
|
131
|
+
builder.join
|
|
132
|
+
|
|
133
|
+
refute_same half_built, observed, 'the half-built client was published before its filters were applied'
|
|
134
|
+
assert_same original, observed
|
|
135
|
+
assert_same half_built, @service.instance_variable_get(:@blob_client)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
data/test/test_helper.rb
CHANGED
|
@@ -79,6 +79,21 @@ def mock_token_signer
|
|
|
79
79
|
@mock_token_signer ||= Minitest::Mock.new(Azure::Core::Auth::Signer.new('access-token'))
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
+
def user_delegation_key_response(value: 'UDELEGATIONKEYXYZ....', signed_key: 'rL7...ABC')
|
|
83
|
+
<<~MSG
|
|
84
|
+
<UserDelegationKey>
|
|
85
|
+
<SignedOid>f81d4fae-7dec-11d0-a765-00a0c91e6bf6</SignedOid>
|
|
86
|
+
<SignedTid>72f988bf-86f1-41af-91ab-2d7cd011db47</SignedTid>
|
|
87
|
+
<SignedStart>2024-09-19T00:00:00Z</SignedStart>
|
|
88
|
+
<SignedExpiry>2024-09-26T00:00:00Z</SignedExpiry>
|
|
89
|
+
<SignedService>b</SignedService>
|
|
90
|
+
<SignedVersion>2020-02-10</SignedVersion>
|
|
91
|
+
<Value>#{value}</Value>
|
|
92
|
+
<SignedKey>#{signed_key}</SignedKey>
|
|
93
|
+
</UserDelegationKey>
|
|
94
|
+
MSG
|
|
95
|
+
end
|
|
96
|
+
|
|
82
97
|
# Mock Class for Blob
|
|
83
98
|
class MockBlob
|
|
84
99
|
def initialize
|
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.5.
|
|
4
|
+
version: 2.5.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shaffan Chaudhry
|
|
@@ -18,7 +18,7 @@ authors:
|
|
|
18
18
|
autorequire:
|
|
19
19
|
bindir: bin
|
|
20
20
|
cert_chain: []
|
|
21
|
-
date: 2026-
|
|
21
|
+
date: 2026-09-10 00:00:00.000000000 Z
|
|
22
22
|
dependencies:
|
|
23
23
|
- !ruby/object:Gem::Dependency
|
|
24
24
|
name: codeclimate-test-reporter
|
|
@@ -476,6 +476,7 @@ files:
|
|
|
476
476
|
- test/requests/storage/test_release_blob_lease.rb
|
|
477
477
|
- test/requests/storage/test_release_container_lease.rb
|
|
478
478
|
- test/requests/storage/test_save_page_blob.rb
|
|
479
|
+
- test/requests/storage/test_storage_thread_safety.rb
|
|
479
480
|
- test/requests/storage/test_wait_blob_copy_operation_to_finish.rb
|
|
480
481
|
- test/test_helper.rb
|
|
481
482
|
- test/unit/test_credentials.rb
|