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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c0db6ddb892cbf22fdd3bd0a6aa859dc54256572f87ff5fa4af0ab478127ba1
4
- data.tar.gz: ac3ee5c92ab35dae5050dbf71a68c5d4f74cb0549c1b2c6cd55bfb3676099141
3
+ metadata.gz: 61aaa2ad5deaa2d7cfc03c52d87f504147a30bb21656fa8d74a0a1e2c1335b7f
4
+ data.tar.gz: 170d555337f12f9262ecc0a482871d7e88429f0dd968a3d4ee3c0ec3d42300dd
5
5
  SHA512:
6
- metadata.gz: a876f5d3cd330701883c5185313faba45a00403577388a102d658b51a88cdf56bb1d46130668ad5b5e54557416048830dbc21bb458f844218e8cb9777ed7aca3
7
- data.tar.gz: a6c70245610e9ad32f587005888bdfd8da48a6d0b52c48fdf5db0a9a7d7141bf33bf9ba74bda4c6a7793cc37648f65d3cb9bcfc5033d819874a22c26a69ae25e
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
@@ -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 = "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
@@ -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
@@ -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
- storage_blob_host =
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
- @credentials = @credential_client.fetch_credentials_if_needed
179
- @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
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
@@ -1,5 +1,5 @@
1
1
  module Fog
2
2
  module AzureRM
3
- VERSION = '2.2.0'.freeze
3
+ VERSION = '2.4.0'.freeze
4
4
  end
5
5
  end
@@ -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 '2018-11-09', params['sv']
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
@@ -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
@@ -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.2.0
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: 2024-11-21 00:00:00.000000000 Z
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.18
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