gitlab-fog-azure-rm 2.2.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 +4 -4
- data/CHANGELOG.md +8 -0
- data/gitlab-fog-azure-rm.gemspec +1 -0
- data/lib/azure/storage/blob/blob_service.rb +3 -0
- data/lib/azure/storage/blob/default.rb +1 -1
- data/lib/azure/storage/common/core/auth/shared_access_signature_generator.rb +13 -2
- data/lib/azure/storage/common/default.rb +1 -1
- data/lib/fog/azurerm/identity/credentials.rb +1 -1
- data/lib/fog/azurerm/identity/default_credentials.rb +2 -6
- data/lib/fog/azurerm/storage.rb +43 -21
- data/lib/fog/azurerm/version.rb +1 -1
- data/test/requests/storage/test_get_blob_https_url.rb +71 -1
- data/test/test_helper.rb +1 -0
- data/test/unit/test_credentials.rb +44 -0
- data/test/unit/test_default_credentials.rb +28 -44
- data/test/unit/test_shared_access_signature_generator.rb +198 -0
- metadata +19 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 61aaa2ad5deaa2d7cfc03c52d87f504147a30bb21656fa8d74a0a1e2c1335b7f
|
4
|
+
data.tar.gz: 170d555337f12f9262ecc0a482871d7e88429f0dd968a3d4ee3c0ec3d42300dd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c5bf104672cd6913ce0c701bc8da7da11d0e80a3a383adf9be76188712b5d2cdcc3f13ea1512c12775226999c266c357ca9b0a1bb01e03196773beb4a1f6c1d9
|
7
|
+
data.tar.gz: 354cd43f816764b22005eb1d0c4e89eecfd55d31b776107904e8f3b1c704b09bd596244517dba24869a2552258cbe98477daaec61ab7fd0042ecd9d21e58dc11
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
## Unreleased
|
2
2
|
|
3
|
+
## 2.4.0
|
4
|
+
|
5
|
+
- Refresh blob client whenever credentials needs to be refreshed !58
|
6
|
+
|
7
|
+
## 2.3.0
|
8
|
+
|
9
|
+
- Bump default Azure Blob and SAS API versions to 2025-07-05 !56
|
10
|
+
|
3
11
|
## 2.2.0
|
4
12
|
|
5
13
|
- Add support for workflow and managed identities !54
|
data/gitlab-fog-azure-rm.gemspec
CHANGED
@@ -22,6 +22,7 @@ Gem::Specification.new do |spec|
|
|
22
22
|
spec.add_development_dependency 'rake', '~> 13.0'
|
23
23
|
spec.add_development_dependency 'rubocop', '~> 0.89.1'
|
24
24
|
spec.add_development_dependency 'simplecov'
|
25
|
+
spec.add_development_dependency 'timecop'
|
25
26
|
spec.add_development_dependency 'webmock'
|
26
27
|
spec.add_development_dependency 'webrick', '~> 1.8'
|
27
28
|
|
@@ -27,6 +27,7 @@ require "azure/storage/blob/page"
|
|
27
27
|
require "azure/storage/blob/block"
|
28
28
|
require "azure/storage/blob/append"
|
29
29
|
require "azure/storage/blob/blob"
|
30
|
+
require 'azure/storage/blob/default'
|
30
31
|
|
31
32
|
module Azure::Storage
|
32
33
|
include Azure::Storage::Common::Service
|
@@ -126,6 +127,8 @@ module Azure::Storage
|
|
126
127
|
#
|
127
128
|
# Accepted key/value pairs in options parameter are:
|
128
129
|
#
|
130
|
+
|
131
|
+
# * +:api_version+ - String. Override the default Blob service API version.
|
129
132
|
# * +:use_development_storage+ - TrueClass|FalseClass. Whether to use storage emulator.
|
130
133
|
# * +:development_storage_proxy_uri+ - String. Used with +:use_development_storage+ if emulator is hosted other than localhost.
|
131
134
|
# * +:storage_connection_string+ - String. The storage connection string.
|
@@ -29,7 +29,7 @@ require "rbconfig"
|
|
29
29
|
module Azure::Storage::Blob
|
30
30
|
module Default
|
31
31
|
# Default REST service (STG) version number
|
32
|
-
STG_VERSION = "
|
32
|
+
STG_VERSION = "2025-07-05"
|
33
33
|
|
34
34
|
# The number of default concurrent requests for parallel operation.
|
35
35
|
DEFAULT_PARALLEL_OPERATION_THREAD_COUNT = 1
|
@@ -215,6 +215,7 @@ module Azure::Storage::Common::Core
|
|
215
215
|
end
|
216
216
|
|
217
217
|
# Construct the plaintext to the spec required for signatures
|
218
|
+
# See https://learn.microsoft.com/en-us/rest/api/storageservices/create-user-delegation-sas
|
218
219
|
# @return [String]
|
219
220
|
def signable_string_for_service(service_type, path, options)
|
220
221
|
# Order is significant
|
@@ -236,8 +237,17 @@ module Azure::Storage::Common::Core
|
|
236
237
|
@user_delegation_key.signed_start,
|
237
238
|
@user_delegation_key.signed_expiry,
|
238
239
|
@user_delegation_key.signed_service,
|
239
|
-
@user_delegation_key.signed_version
|
240
|
+
@user_delegation_key.signed_version,
|
241
|
+
# User delegation fields (supported since 2020-02-10)
|
242
|
+
options[:signed_authorized_user_object_id] || "", # saoid
|
243
|
+
options[:signed_unauthorized_user_object_id] || "", # suoid
|
244
|
+
options[:signed_correlation_id] || "", # scid
|
245
|
+
# For 2025-07-05 USER DELEGATION SAS ONLY, add two empty lines after correlation ID
|
246
|
+
# This is the critical difference that was causing the signature mismatch with 2018-11-09.
|
247
|
+
"",
|
248
|
+
""
|
240
249
|
]
|
250
|
+
|
241
251
|
end
|
242
252
|
|
243
253
|
signable_fields.concat [
|
@@ -248,7 +258,8 @@ module Azure::Storage::Common::Core
|
|
248
258
|
|
249
259
|
signable_fields.concat [
|
250
260
|
options[:resource],
|
251
|
-
options[:timestamp]
|
261
|
+
options[:timestamp] || "",
|
262
|
+
options[:signed_encryption_scope] || ""
|
252
263
|
] if service_type == Azure::Storage::Common::ServiceType::BLOB
|
253
264
|
|
254
265
|
signable_fields.concat [
|
@@ -30,7 +30,7 @@ require "azure/storage/common/version"
|
|
30
30
|
module Azure::Storage::Common
|
31
31
|
module Default
|
32
32
|
# Default REST service (STG) version number. This is used only for SAS generator.
|
33
|
-
STG_VERSION = "
|
33
|
+
STG_VERSION = "2025-07-05"
|
34
34
|
|
35
35
|
# The number of default concurrent requests for parallel operation.
|
36
36
|
DEFAULT_PARALLEL_OPERATION_THREAD_COUNT = 1
|
@@ -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.
|
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
|
-
|
52
|
-
|
53
|
-
@credentials = credentials
|
54
|
-
@credentials
|
50
|
+
@credential_client
|
55
51
|
end
|
56
52
|
end
|
57
53
|
end
|
data/lib/fog/azurerm/storage.rb
CHANGED
@@ -13,6 +13,7 @@ module Fog
|
|
13
13
|
recognizes :azure_storage_endpoint
|
14
14
|
recognizes :azure_storage_domain
|
15
15
|
recognizes :environment
|
16
|
+
recognizes :api_version
|
16
17
|
|
17
18
|
recognizes :debug
|
18
19
|
|
@@ -99,12 +100,16 @@ module Fog
|
|
99
100
|
raise e.message
|
100
101
|
end
|
101
102
|
|
103
|
+
@user_delegation_key_mutex = Mutex.new
|
104
|
+
@blob_client_mutex = Mutex.new
|
105
|
+
|
102
106
|
options[:environment] = options[:environment] || ENV['AZURE_ENVIRONMENT'] || ENVIRONMENT_AZURE_CLOUD
|
103
107
|
@environment = options[:environment]
|
104
108
|
@options = options
|
105
109
|
|
106
110
|
@azure_storage_account_name = options[:azure_storage_account_name]
|
107
111
|
@azure_storage_access_key = options[:azure_storage_access_key]
|
112
|
+
@api_version = options[:api_version]
|
108
113
|
|
109
114
|
load_credentials
|
110
115
|
|
@@ -112,24 +117,7 @@ module Fog
|
|
112
117
|
@azure_storage_endpoint = options[:azure_storage_endpoint]
|
113
118
|
@azure_storage_domain = options[:azure_storage_domain]
|
114
119
|
|
115
|
-
|
116
|
-
@azure_storage_endpoint ||
|
117
|
-
if @azure_storage_domain.nil? || @azure_storage_domain.empty?
|
118
|
-
get_blob_endpoint(@azure_storage_account_name, true, @environment)
|
119
|
-
else
|
120
|
-
get_blob_endpoint_with_domain(@azure_storage_account_name, true, @azure_storage_domain)
|
121
|
-
end
|
122
|
-
|
123
|
-
azure_client = Azure::Storage::Common::Client.create({
|
124
|
-
storage_account_name: @azure_storage_account_name,
|
125
|
-
storage_access_key: @azure_storage_access_key,
|
126
|
-
signer: @azure_storage_token_signer
|
127
|
-
}.compact)
|
128
|
-
azure_client.storage_blob_host = storage_blob_host
|
129
|
-
@blob_client = Azure::Storage::Blob::BlobService.new(client: azure_client)
|
130
|
-
@blob_client.with_filter(Fog::AzureRM::IdentityEncodingFilter.new)
|
131
|
-
@blob_client.with_filter(Azure::Storage::Common::Core::Filter::ExponentialRetryPolicyFilter.new)
|
132
|
-
@blob_client.with_filter(Azure::Core::Http::DebugFilter.new) if @debug
|
120
|
+
refresh_blob_client
|
133
121
|
end
|
134
122
|
|
135
123
|
private
|
@@ -175,11 +163,16 @@ module Fog
|
|
175
163
|
return nil unless @azure_storage_token_signer
|
176
164
|
|
177
165
|
if @credential_client
|
178
|
-
|
179
|
-
|
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
|
180
174
|
end
|
181
175
|
|
182
|
-
@user_delegation_key_mutex ||= Mutex.new
|
183
176
|
@user_delegation_key_mutex.synchronize do
|
184
177
|
if @user_delegation_key_expiry.nil? || @user_delegation_key_expiry < requested_expiry
|
185
178
|
start = Time.now
|
@@ -195,6 +188,35 @@ module Fog
|
|
195
188
|
|
196
189
|
@user_delegation_key
|
197
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
|
198
220
|
end
|
199
221
|
end
|
200
222
|
end
|
data/lib/fog/azurerm/version.rb
CHANGED
@@ -145,7 +145,7 @@ class TestGetBlobHttpsUrl < Minitest::Test
|
|
145
145
|
params = parsed.query.split('&').to_h { |x| x.split('=') }
|
146
146
|
|
147
147
|
assert_equal 'r', params['sp']
|
148
|
-
assert_equal '
|
148
|
+
assert_equal '2025-07-05', params['sv']
|
149
149
|
assert_equal 'b', params['sr']
|
150
150
|
assert_equal 'https', params['spr']
|
151
151
|
assert_equal '2024-09-19T00%3A00%3A00Z', params['skt']
|
@@ -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
@@ -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
|
-
|
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,
|
35
|
-
|
36
|
-
assert_equal
|
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
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
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,
|
70
|
-
Fog::AzureRM::Identity::ManagedIdentityClient.stub :new,
|
71
|
-
|
72
|
-
assert_equal
|
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
|
-
|
82
|
-
|
71
|
+
workflow_client = Struct.new(:fetch_credentials_if_needed).new
|
72
|
+
managed_client = Struct.new(:fetch_credentials_if_needed).new
|
83
73
|
|
84
|
-
|
85
|
-
|
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
|
@@ -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
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gitlab-fog-azure-rm
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.4.0
|
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:
|
21
|
+
date: 2025-09-30 00:00:00.000000000 Z
|
22
22
|
dependencies:
|
23
23
|
- !ruby/object:Gem::Dependency
|
24
24
|
name: codeclimate-test-reporter
|
@@ -90,6 +90,20 @@ dependencies:
|
|
90
90
|
- - ">="
|
91
91
|
- !ruby/object:Gem::Version
|
92
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'
|
93
107
|
- !ruby/object:Gem::Dependency
|
94
108
|
name: webmock
|
95
109
|
requirement: !ruby/object:Gem::Requirement
|
@@ -450,8 +464,10 @@ files:
|
|
450
464
|
- test/requests/storage/test_save_page_blob.rb
|
451
465
|
- test/requests/storage/test_wait_blob_copy_operation_to_finish.rb
|
452
466
|
- test/test_helper.rb
|
467
|
+
- test/unit/test_credentials.rb
|
453
468
|
- test/unit/test_default_credentials.rb
|
454
469
|
- test/unit/test_managed_identity_client.rb
|
470
|
+
- test/unit/test_shared_access_signature_generator.rb
|
455
471
|
- test/unit/test_workflow_identity_client.rb
|
456
472
|
homepage: https://gitlab.com/gitlab-org/gitlab-fog-azure-rm
|
457
473
|
licenses:
|
@@ -473,7 +489,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
473
489
|
- !ruby/object:Gem::Version
|
474
490
|
version: '0'
|
475
491
|
requirements: []
|
476
|
-
rubygems_version: 3.5.
|
492
|
+
rubygems_version: 3.5.22
|
477
493
|
signing_key:
|
478
494
|
specification_version: 4
|
479
495
|
summary: Module for the 'fog' gem to support Azure Blob Storage with CarrierWave and
|