gitlab-fog-azure-rm 2.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 61aaa2ad5deaa2d7cfc03c52d87f504147a30bb21656fa8d74a0a1e2c1335b7f
4
- data.tar.gz: 170d555337f12f9262ecc0a482871d7e88429f0dd968a3d4ee3c0ec3d42300dd
3
+ metadata.gz: faf6ef8f40417712f91769e2ac0dea345c7451f1457919399c1aa574cf6a44a5
4
+ data.tar.gz: 4596861b6a717a743ff2d250f91f3bb4cafd911cb57e0efeeae14368e04162eb
5
5
  SHA512:
6
- metadata.gz: c5bf104672cd6913ce0c701bc8da7da11d0e80a3a383adf9be76188712b5d2cdcc3f13ea1512c12775226999c266c357ca9b0a1bb01e03196773beb4a1f6c1d9
7
- data.tar.gz: 354cd43f816764b22005eb1d0c4e89eecfd55d31b776107904e8f3b1c704b09bd596244517dba24869a2552258cbe98477daaec61ab7fd0042ecd9d21e58dc11
6
+ metadata.gz: f59d2e0764335c68f69462f380781e3a6f395467959f8af92f076dd4492f968d82bdc52b471f93a3fb1568e939c277e70cf6eac8fc79ed65e144b53eb1d966a2
7
+ data.tar.gz: ea77ec4d01cff9bebec6642ad94517c0df51246de8ab41f48241f8281841737c08da6f20ec17e9c79a9ad5d6f56bc51940c8cac169a17d226f0037c84ea5c706
data/.gitlab-ci.yml CHANGED
@@ -4,26 +4,29 @@ include:
4
4
  default:
5
5
  image: "ruby:${RUBY_VERSION}"
6
6
 
7
+ variables:
8
+ BUNDLE_PATH: vendor
9
+
7
10
  cache:
8
11
  paths:
9
12
  - vendor/ruby
10
13
 
11
14
  before_script:
12
15
  - ruby -v
13
- - bundle install -j $(nproc) --path vendor
16
+ - bundle install -j $(nproc)
14
17
 
15
18
  test:
16
19
  script:
17
20
  - bundle exec rake test
18
21
  parallel:
19
22
  matrix:
20
- - RUBY_VERSION: [ "2.7", "3.0", "3.1", "3.2" ]
23
+ - RUBY_VERSION: [ "3.2", "3.3", "3.4", "4.0" ]
21
24
 
22
25
  rubocop:
23
26
  script:
24
27
  - bundle exec rubocop
25
28
  variables:
26
- RUBY_VERSION: "3.1"
29
+ RUBY_VERSION: "4.0"
27
30
 
28
31
  integration:
29
32
  services:
@@ -32,4 +35,4 @@ integration:
32
35
  script:
33
36
  - bundle exec rake integration
34
37
  variables:
35
- RUBY_VERSION: "3.2"
38
+ RUBY_VERSION: "4.0"
data/.rubocop_todo.yml CHANGED
@@ -1,41 +1,71 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2020-08-07 04:33:26 UTC using RuboCop version 0.89.0.
3
+ # on 2026-03-31 03:13:36 UTC using RuboCop version 1.86.0.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
7
7
  # versions of RuboCop, may require this file to be generated again.
8
8
 
9
9
  # Offense count: 1
10
- # Configuration parameters: Include.
11
- # Include: **/*.gemspec
10
+ # This cop supports safe autocorrection (--autocorrect).
11
+ Gemspec/DeprecatedAttributeAssignment:
12
+ Exclude:
13
+ - 'gitlab-fog-azure-rm.gemspec'
14
+
15
+ # Offense count: 8
16
+ # Configuration parameters: EnforcedStyle, AllowedGems.
17
+ # SupportedStyles: Gemfile, gems.rb, gemspec
18
+ Gemspec/DevelopmentDependencies:
19
+ Exclude:
20
+ - 'gitlab-fog-azure-rm.gemspec'
21
+
22
+ # Offense count: 1
23
+ # This cop supports safe autocorrection (--autocorrect).
24
+ Gemspec/RequireMFA:
25
+ Exclude:
26
+ - 'gitlab-fog-azure-rm.gemspec'
27
+
28
+ # Offense count: 1
12
29
  Gemspec/RequiredRubyVersion:
13
30
  Exclude:
14
31
  - 'gitlab-fog-azure-rm.gemspec'
15
32
 
16
- # Offense count: 23
17
- # Cop supports --auto-correct.
33
+ # Offense count: 21
34
+ # This cop supports safe autocorrection (--autocorrect).
18
35
  Layout/EmptyLineAfterGuardClause:
19
- Enabled: false
36
+ Exclude:
37
+ - 'lib/fog/azurerm/models/storage/directories.rb'
38
+ - 'lib/fog/azurerm/models/storage/files.rb'
39
+ - 'lib/fog/azurerm/requests/storage/delete_blob.rb'
40
+ - 'lib/fog/azurerm/requests/storage/delete_container.rb'
41
+ - 'lib/fog/azurerm/requests/storage/get_blob.rb'
42
+ - 'lib/fog/azurerm/requests/storage/get_blob_properties.rb'
43
+ - 'lib/fog/azurerm/requests/storage/get_container_properties.rb'
44
+ - 'lib/fog/azurerm/requests/storage/list_blobs.rb'
45
+ - 'lib/fog/azurerm/requests/storage/list_containers.rb'
46
+ - 'lib/fog/azurerm/requests/storage/save_page_blob.rb'
47
+ - 'lib/fog/azurerm/utilities/general.rb'
48
+ - 'test/integration/blob.rb'
49
+ - 'test/requests/storage/test_list_blobs.rb'
50
+ - 'test/requests/storage/test_list_containers.rb'
51
+ - 'test/requests/storage/test_save_page_blob.rb'
52
+
53
+ # Offense count: 1
54
+ # This cop supports safe autocorrection (--autocorrect).
55
+ Layout/EmptyLinesAfterModuleInclusion:
56
+ Exclude:
57
+ - 'lib/fog/azurerm/identity/base_client.rb'
20
58
 
21
59
  # Offense count: 1
22
- # Cop supports --auto-correct.
60
+ # This cop supports safe autocorrection (--autocorrect).
23
61
  # Configuration parameters: AllowAliasSyntax, AllowedMethods.
24
62
  # AllowedMethods: alias_method, public, protected, private
25
63
  Layout/EmptyLinesAroundAttributeAccessor:
26
64
  Exclude:
27
65
  - 'lib/fog/azurerm/models/storage/file.rb'
28
66
 
29
- # Offense count: 2
30
- # Configuration parameters: EnforcedStyle.
31
- # SupportedStyles: native, lf, crlf
32
- Layout/EndOfLine:
33
- Exclude:
34
- - 'lib/fog/azurerm/models/resources/azure_resource.rb'
35
- - 'lib/fog/azurerm/models/resources/azure_resources.rb'
36
-
37
- # Offense count: 13
38
- # Cop supports --auto-correct.
67
+ # Offense count: 6
68
+ # This cop supports safe autocorrection (--autocorrect).
39
69
  # Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
40
70
  # SupportedHashRocketStyles: key, separator, table
41
71
  # SupportedColonStyles: key, separator, table
@@ -43,10 +73,9 @@ Layout/EndOfLine:
43
73
  Layout/HashAlignment:
44
74
  Exclude:
45
75
  - 'lib/fog/azurerm/models/storage/file.rb'
46
- - 'test/test_helper.rb'
47
76
 
48
77
  # Offense count: 1
49
- # Cop supports --auto-correct.
78
+ # This cop supports safe autocorrection (--autocorrect).
50
79
  # Configuration parameters: EnforcedStyle.
51
80
  # SupportedStyles: space, no_space
52
81
  Layout/SpaceAroundEqualsInParameterDefault:
@@ -54,62 +83,77 @@ Layout/SpaceAroundEqualsInParameterDefault:
54
83
  - 'lib/fog/azurerm/requests/storage/release_container_lease.rb'
55
84
 
56
85
  # Offense count: 1
57
- # Cop supports --auto-correct.
86
+ # This cop supports safe autocorrection (--autocorrect).
58
87
  Layout/SpaceBeforeComma:
59
88
  Exclude:
60
89
  - 'gitlab-fog-azure-rm.gemspec'
61
90
 
91
+ # Offense count: 18
92
+ # This cop supports safe autocorrection (--autocorrect).
93
+ Lint/AmbiguousOperatorPrecedence:
94
+ Exclude:
95
+ - 'test/integration/blob.rb'
96
+ - 'test/models/storage/test_file.rb'
97
+ - 'test/requests/storage/test_get_blob_https_url.rb'
98
+
62
99
  # Offense count: 2
63
100
  Lint/DuplicateMethods:
64
101
  Exclude:
65
102
  - 'lib/fog/azurerm/models/storage/directory.rb'
66
103
  - 'lib/fog/azurerm/models/storage/file.rb'
67
104
 
105
+ # Offense count: 3
106
+ # Configuration parameters: AllowComments, AllowEmptyLambdas.
107
+ Lint/EmptyBlock:
108
+ Exclude:
109
+ - 'test/requests/storage/test_get_blob.rb'
110
+
111
+ # Offense count: 8
112
+ # This cop supports unsafe autocorrection (--autocorrect-all).
113
+ Lint/NonAtomicFileOperation:
114
+ Exclude:
115
+ - 'test/integration/blob.rb'
116
+
68
117
  # Offense count: 2
69
118
  Lint/RescueException:
70
119
  Exclude:
71
120
  - 'lib/fog/azurerm/models/storage/directories.rb'
72
121
  - 'lib/fog/azurerm/requests/storage/check_container_exists.rb'
73
122
 
74
- # Offense count: 3
75
- Lint/UselessAssignment:
76
- Exclude:
77
- - 'lib/fog/azurerm/storage.rb'
78
- - 'test/integration/container.rb'
79
-
80
123
  # Offense count: 1
124
+ # This cop supports safe autocorrection (--autocorrect).
81
125
  # Configuration parameters: CheckForMethodsWithNoSideEffects.
82
126
  Lint/Void:
83
127
  Exclude:
84
128
  - 'lib/fog/azurerm/models/storage/directory.rb'
85
129
 
86
- # Offense count: 30
87
- # Configuration parameters: IgnoredMethods.
130
+ # Offense count: 38
131
+ # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
88
132
  Metrics/AbcSize:
89
133
  Max: 66
90
134
 
91
- # Offense count: 11
135
+ # Offense count: 14
92
136
  # Configuration parameters: CountComments, CountAsOne.
93
137
  Metrics/ClassLength:
94
- Max: 250
138
+ Max: 232
95
139
 
96
- # Offense count: 9
97
- # Configuration parameters: IgnoredMethods.
140
+ # Offense count: 6
141
+ # Configuration parameters: AllowedMethods, AllowedPatterns.
98
142
  Metrics/CyclomaticComplexity:
99
143
  Max: 14
100
144
 
101
145
  # Offense count: 98
102
- # Configuration parameters: CountComments, CountAsOne, ExcludedMethods.
146
+ # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
103
147
  Metrics/MethodLength:
104
- Max: 115
148
+ Max: 110
105
149
 
106
150
  # Offense count: 1
107
- # Configuration parameters: CountKeywordArgs.
151
+ # Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
108
152
  Metrics/ParameterLists:
109
153
  Max: 6
110
154
 
111
- # Offense count: 9
112
- # Configuration parameters: IgnoredMethods.
155
+ # Offense count: 6
156
+ # Configuration parameters: AllowedMethods, AllowedPatterns.
113
157
  Metrics/PerceivedComplexity:
114
158
  Max: 14
115
159
 
@@ -120,14 +164,21 @@ Naming/ConstantName:
120
164
  - 'lib/fog/azurerm/models/storage/sku_name.rb'
121
165
  - 'lib/fog/azurerm/models/storage/sku_tier.rb'
122
166
 
123
- # Offense count: 38
124
- # Cop supports --auto-correct.
167
+ # Offense count: 33
168
+ # Configuration parameters: Mode, AllowedMethods, AllowedPatterns, AllowBangMethods, WaywardPredicates.
169
+ # AllowedMethods: call
170
+ # WaywardPredicates: infinite?, nonzero?
171
+ Naming/PredicateMethod:
172
+ Enabled: false
173
+
174
+ # Offense count: 33
175
+ # This cop supports safe autocorrection (--autocorrect).
125
176
  # Configuration parameters: PreferredName.
126
177
  Naming/RescuedExceptionsVariableName:
127
178
  Enabled: false
128
179
 
129
- # Offense count: 13
130
- # Cop supports --auto-correct.
180
+ # Offense count: 10
181
+ # This cop supports safe autocorrection (--autocorrect).
131
182
  # Configuration parameters: EnforcedStyle.
132
183
  # SupportedStyles: separated, grouped
133
184
  Style/AccessorGrouping:
@@ -135,41 +186,84 @@ Style/AccessorGrouping:
135
186
  - 'lib/fog/azurerm/requests/storage/save_page_blob.rb'
136
187
  - 'test/test_helper.rb'
137
188
 
189
+ # Offense count: 1
190
+ # This cop supports safe autocorrection (--autocorrect).
191
+ # Configuration parameters: EnforcedStyle, AllowedParentClasses.
192
+ # SupportedStyles: class_keyword, class_new, class_definition
193
+ Style/EmptyClassDefinition:
194
+ Exclude:
195
+ - 'lib/fog/azurerm/identity/base_client.rb'
196
+
138
197
  # Offense count: 2
139
- # Cop supports --auto-correct.
198
+ # This cop supports safe autocorrection (--autocorrect).
140
199
  Style/ExpandPathArguments:
141
200
  Exclude:
142
201
  - 'gitlab-fog-azure-rm.gemspec'
143
202
  - 'lib/fog/azurerm/utilities/general.rb'
144
203
 
145
204
  # Offense count: 2
146
- # Cop supports --auto-correct.
205
+ # This cop supports safe autocorrection (--autocorrect).
147
206
  Style/ExplicitBlockArgument:
148
207
  Exclude:
149
208
  - 'lib/fog/azurerm/models/storage/files.rb'
150
209
 
151
- # Offense count: 117
152
- # Cop supports --auto-correct.
210
+ # Offense count: 4
211
+ # This cop supports safe autocorrection (--autocorrect).
212
+ # Configuration parameters: AllowedVars, DefaultToNil.
213
+ Style/FetchEnvVar:
214
+ Exclude:
215
+ - 'lib/fog/azurerm/identity/managed_identity_client.rb'
216
+ - 'lib/fog/azurerm/identity/workflow_identity_client.rb'
217
+
218
+ # Offense count: 3
219
+ # This cop supports unsafe autocorrection (--autocorrect-all).
220
+ Style/FileNull:
221
+ Exclude:
222
+ - 'test/requests/storage/test_create_block_blob.rb'
223
+ - 'test/requests/storage/test_save_page_blob.rb'
224
+
225
+ # Offense count: 1
226
+ # This cop supports safe autocorrection (--autocorrect).
227
+ Style/FileWrite:
228
+ Exclude:
229
+ - 'test/integration/blob.rb'
230
+
231
+ # Offense count: 119
232
+ # This cop supports unsafe autocorrection (--autocorrect-all).
153
233
  # Configuration parameters: EnforcedStyle.
154
234
  # SupportedStyles: always, always_true, never
155
235
  Style/FrozenStringLiteralComment:
156
236
  Enabled: false
157
237
 
158
- # Offense count: 3
159
- # Cop supports --auto-correct.
238
+ # Offense count: 2
239
+ # This cop supports safe autocorrection (--autocorrect).
160
240
  Style/IfUnlessModifier:
161
241
  Exclude:
162
242
  - 'lib/fog/azurerm/requests/storage/compare_container_blobs.rb'
163
243
  - 'lib/fog/azurerm/requests/storage/wait_blob_copy_operation_to_finish.rb'
164
244
 
245
+ # Offense count: 2
246
+ # This cop supports unsafe autocorrection (--autocorrect-all).
247
+ Style/MapIntoArray:
248
+ Exclude:
249
+ - 'lib/fog/azurerm/models/storage/directories.rb'
250
+ - 'lib/fog/azurerm/models/storage/storage_accounts.rb'
251
+
165
252
  # Offense count: 1
166
- # Cop supports --auto-correct.
167
- Style/MultilineIfModifier:
253
+ # This cop supports safe autocorrection (--autocorrect).
254
+ # Configuration parameters: AllowMethodComparison, ComparisonsThreshold.
255
+ Style/MultipleComparison:
168
256
  Exclude:
169
- - 'lib/fog/azurerm/models/resources/dependency.rb'
257
+ - 'lib/fog/azurerm/models/storage/file.rb'
258
+
259
+ # Offense count: 1
260
+ # This cop supports safe autocorrection (--autocorrect).
261
+ Style/NegatedIfElseCondition:
262
+ Exclude:
263
+ - 'lib/fog/azurerm/models/storage/storage_accounts.rb'
170
264
 
171
265
  # Offense count: 1
172
- # Cop supports --auto-correct.
266
+ # This cop supports safe autocorrection (--autocorrect).
173
267
  # Configuration parameters: AllowedMethods.
174
268
  # AllowedMethods: be, be_a, be_an, be_between, be_falsey, be_kind_of, be_instance_of, be_truthy, be_within, eq, eql, end_with, include, match, raise_error, respond_to, start_with
175
269
  Style/NestedParenthesizedCalls:
@@ -177,8 +271,8 @@ Style/NestedParenthesizedCalls:
177
271
  - 'test/test_helper.rb'
178
272
 
179
273
  # Offense count: 1
180
- # Cop supports --auto-correct.
181
- # Configuration parameters: AutoCorrect, EnforcedStyle, IgnoredMethods.
274
+ # This cop supports unsafe autocorrection (--autocorrect-all).
275
+ # Configuration parameters: EnforcedStyle, AllowedMethods, AllowedPatterns.
182
276
  # SupportedStyles: predicate, comparison
183
277
  Style/NumericPredicate:
184
278
  Exclude:
@@ -186,13 +280,21 @@ Style/NumericPredicate:
186
280
  - 'lib/fog/azurerm/requests/storage/get_blob.rb'
187
281
 
188
282
  # Offense count: 2
283
+ # Configuration parameters: AllowedClasses.
284
+ Style/OneClassPerFile:
285
+ Exclude:
286
+ - 'test/test_helper.rb'
287
+
288
+ # Offense count: 3
289
+ # Configuration parameters: AllowedMethods.
290
+ # AllowedMethods: respond_to_missing?
189
291
  Style/OptionalBooleanParameter:
190
292
  Exclude:
191
293
  - 'lib/fog/azurerm/async_response.rb'
192
294
  - 'lib/fog/azurerm/utilities/general.rb'
193
295
 
194
- # Offense count: 36
195
- # Cop supports --auto-correct.
296
+ # Offense count: 33
297
+ # This cop supports safe autocorrection (--autocorrect).
196
298
  # Configuration parameters: PreferredDelimiters.
197
299
  Style/PercentLiteralDelimiters:
198
300
  Exclude:
@@ -202,70 +304,74 @@ Style/PercentLiteralDelimiters:
202
304
  - 'lib/fog/azurerm/requests/storage/copy_blob.rb'
203
305
  - 'lib/fog/azurerm/requests/storage/copy_blob_from_uri.rb'
204
306
  - 'test/api_stub/requests/storage/file.rb'
205
- - 'test/test_helper.rb'
206
307
 
207
- # Offense count: 48
208
- # Cop supports --auto-correct.
209
- # Configuration parameters: EnforcedStyle.
308
+ # Offense count: 46
309
+ # This cop supports unsafe autocorrection (--autocorrect-all).
310
+ # Configuration parameters: EnforcedStyle, AllowedCompactTypes.
210
311
  # SupportedStyles: compact, exploded
211
312
  Style/RaiseArgs:
212
313
  Enabled: false
213
314
 
214
315
  # Offense count: 1
215
- # Cop supports --auto-correct.
316
+ # This cop supports safe autocorrection (--autocorrect).
216
317
  Style/RedundantBegin:
217
318
  Exclude:
218
319
  - 'lib/fog/azurerm/storage.rb'
219
320
 
220
- # Offense count: 16
221
- # Cop supports --auto-correct.
321
+ # Offense count: 5
322
+ # This cop supports safe autocorrection (--autocorrect).
323
+ # Configuration parameters: AllowMultipleReturnValues.
324
+ Style/RedundantReturn:
325
+ Exclude:
326
+ - 'test/models/storage/test_files.rb'
327
+ - 'test/requests/storage/test_list_blobs.rb'
328
+ - 'test/requests/storage/test_list_containers.rb'
329
+ - 'test/requests/storage/test_save_page_blob.rb'
330
+
331
+ # Offense count: 9
332
+ # This cop supports safe autocorrection (--autocorrect).
222
333
  # Configuration parameters: EnforcedStyle.
223
334
  # SupportedStyles: implicit, explicit
224
335
  Style/RescueStandardError:
225
336
  Exclude:
226
337
  - 'lib/fog/azurerm/models/storage/files.rb'
227
- - 'lib/fog/azurerm/requests/storage/create_block_blob.rb'
228
- - 'lib/fog/azurerm/requests/storage/multipart_save_block_blob.rb'
229
338
  - 'lib/fog/azurerm/requests/storage/save_page_blob.rb'
230
339
  - 'lib/fog/azurerm/requests/storage/wait_blob_copy_operation_to_finish.rb'
231
340
  - 'test/integration/blob.rb'
232
341
  - 'test/integration/container.rb'
233
- - 'test/integration/storage_account.rb'
234
342
 
235
- # Offense count: 3
236
- # Cop supports --auto-correct.
237
- # Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods.
238
- # AllowedMethods: present?, blank?, presence, try, try!
239
- Style/SafeNavigation:
343
+ # Offense count: 1
344
+ # This cop supports safe autocorrection (--autocorrect).
345
+ # Configuration parameters: AllowModifier.
346
+ Style/SoleNestedConditional:
240
347
  Exclude:
241
- - 'test/integration/blob.rb'
242
- - 'test/integration/container.rb'
243
- - 'test/integration/storage_account.rb'
348
+ - 'lib/fog/azurerm/async_response.rb'
244
349
 
245
350
  # Offense count: 2
246
- # Cop supports --auto-correct.
351
+ # This cop supports unsafe autocorrection (--autocorrect-all).
352
+ # Configuration parameters: Mode.
247
353
  Style/StringConcatenation:
248
354
  Exclude:
249
355
  - 'test/integration/blob.rb'
250
356
 
251
- # Offense count: 1
252
- # Cop supports --auto-correct.
357
+ # Offense count: 10
358
+ # This cop supports safe autocorrection (--autocorrect).
253
359
  # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline.
254
360
  # SupportedStyles: single_quotes, double_quotes
255
361
  Style/StringLiterals:
256
362
  Exclude:
257
363
  - 'gitlab-fog-azure-rm.gemspec'
258
364
 
259
- # Offense count: 9
260
- # Cop supports --auto-correct.
365
+ # Offense count: 8
366
+ # This cop supports safe autocorrection (--autocorrect).
261
367
  # Configuration parameters: MinSize.
262
368
  # SupportedStyles: percent, brackets
263
369
  Style/SymbolArray:
264
370
  EnforcedStyle: brackets
265
371
 
266
- # Offense count: 257
267
- # Cop supports --auto-correct.
268
- # Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
372
+ # Offense count: 194
373
+ # This cop supports safe autocorrection (--autocorrect).
374
+ # Configuration parameters: AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
269
375
  # URISchemes: http, https
270
376
  Layout/LineLength:
271
- Max: 1242
377
+ Max: 201
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## Unreleased
2
2
 
3
+ ## 2.5.1
4
+
5
+ - Fix thread-safety race in SAS signer credential refresh !64
6
+
7
+ ## 2.5.0
8
+
9
+ - Support Ruby 4 !61
10
+
3
11
  ## 2.4.0
4
12
 
5
13
  - Refresh blob client whenever credentials needs to be refreshed !58
@@ -15,19 +15,20 @@ Gem::Specification.new do |spec|
15
15
  spec.homepage = 'https://gitlab.com/gitlab-org/gitlab-fog-azure-rm'
16
16
  spec.rdoc_options = %w[--charset=UTF-8]
17
17
  spec.extra_rdoc_files = %w[README.md]
18
- spec.required_ruby_version = '>= 2.7.0'
18
+ spec.required_ruby_version = '>= 3.2.0'
19
19
  spec.post_install_message = 'Thanks for installing!'
20
20
  spec.add_development_dependency 'codeclimate-test-reporter' , '~> 1.0.0'
21
- spec.add_development_dependency 'minitest', '~> 5.8.4'
21
+ spec.add_development_dependency 'minitest', '~> 5.27'
22
22
  spec.add_development_dependency 'rake', '~> 13.0'
23
- spec.add_development_dependency 'rubocop', '~> 0.89.1'
23
+ spec.add_development_dependency 'rubocop', '~> 1.86.0'
24
24
  spec.add_development_dependency 'simplecov'
25
25
  spec.add_development_dependency 'timecop'
26
26
  spec.add_development_dependency 'webmock'
27
27
  spec.add_development_dependency 'webrick', '~> 1.8'
28
28
 
29
+ spec.add_dependency 'base64'
29
30
  spec.add_dependency 'faraday', "~> 2.0"
30
- spec.add_dependency "faraday-follow_redirects", "~> 0.3.0"
31
+ spec.add_dependency "faraday-follow_redirects", "~> 0.5.0"
31
32
  spec.add_dependency "faraday-net_http_persistent", "~> 2.0"
32
33
  spec.add_dependency 'fog-core', '~> 2.1'
33
34
  spec.add_dependency 'fog-json', '~> 1.2'
@@ -114,7 +114,9 @@ module Azure
114
114
  # @return [String] a string with the canonicalized resource.
115
115
  def canonicalized_resource(uri)
116
116
  resource = '/' + account_name + (uri.path.empty? ? '/' : uri.path)
117
- params = CGI.parse(uri.query.to_s).map { |k,v| [k.downcase, v] }
117
+ params = URI.decode_www_form(uri.query.to_s)
118
+ .each_with_object({}) { |(key, value), hash| (hash[key] ||= []) << value }
119
+ .map { |k,v| [k.downcase, v] }
118
120
  params.sort_by! { |k,_| k }
119
121
  params.map! { |k,v| '%s:%s' % [k, v.map(&:strip).sort.join(',')] }
120
122
  [resource, *params].join("\n")
@@ -18,9 +18,9 @@ module Fog
18
18
  # This class provides the mock implementation for unit tests.
19
19
  class Mock
20
20
  def get_blob_url(_container_name, _blob_name, options = {})
21
- url = 'https://mockaccount.blob.core.windows.net/test_container/test_blob'
21
+ url = +'https://mockaccount.blob.core.windows.net/test_container/test_blob'
22
22
  url.sub!('https:', 'http:') if options[:scheme] == 'http'
23
- url
23
+ url.freeze
24
24
  end
25
25
  end
26
26
  end
@@ -24,9 +24,9 @@ module Fog
24
24
  # This class provides the mock implementation for unit tests.
25
25
  class Mock
26
26
  def get_container_url(_container_name, options = {})
27
- url = 'https://mockaccount.blob.core.windows.net/test_container?comp=list&restype=container'
27
+ url = +'https://mockaccount.blob.core.windows.net/test_container?comp=list&restype=container'
28
28
  url.sub!('https:', 'http:') if options[:scheme] == 'http'
29
- url
29
+ url.freeze
30
30
  end
31
31
  end
32
32
  end
@@ -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
- @user_delegation_key_mutex = Mutex.new
104
- @blob_client_mutex = Mutex.new
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
- refresh_blob_client
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
- access_key = @azure_storage_access_key.to_s
147
- user_delegation_key = user_delegation_key(requested_expiry)
148
-
149
- # invalidate cache when the delegation key changes
150
- unless @signature_client_delegation_key == user_delegation_key
151
- @signature_client_delegation_key = user_delegation_key
152
- @signature_client = nil
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
- def user_delegation_key(requested_expiry)
163
- return nil unless @azure_storage_token_signer
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
- changed = new_credentials != @credentials
231
+ return if new_credentials.nil? || new_credentials == @credentials
168
232
 
169
- if changed
170
- @credentials = new_credentials
171
- @azure_storage_token_signer = token_signer
172
- refresh_blob_client
173
- end
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
- @user_delegation_key_mutex.synchronize do
177
- if @user_delegation_key_expiry.nil? || @user_delegation_key_expiry < requested_expiry
178
- start = Time.now
179
- expiry = start + Azure::Storage::Blob::BlobConstants::MAX_USER_DELEGATION_KEY_SECONDS
180
-
181
- @user_delegation_key = @blob_client.get_user_delegation_key(
182
- start,
183
- expiry
184
- )
185
- @user_delegation_key_expiry = expiry
186
- end
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
- 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
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: @azure_storage_token_signer
294
+ signer: signer
218
295
  }.compact)
219
296
  end
220
297
  end
@@ -149,7 +149,7 @@ module Fog
149
149
  end
150
150
 
151
151
  data['last_modified'] = Time.parse(data['last_modified'])
152
- data['etag'].delete!('"')
152
+ data['etag'] = data['etag'].delete('"')
153
153
  data
154
154
  end
155
155
 
@@ -1,5 +1,5 @@
1
1
  module Fog
2
2
  module AzureRM
3
- VERSION = '2.4.0'.freeze
3
+ VERSION = '2.5.1'.freeze
4
4
  end
5
5
  end
@@ -49,7 +49,7 @@ class TestGetBlob < Minitest::Test
49
49
  end
50
50
 
51
51
  def test_get_blob_with_block_given_success
52
- data = ''
52
+ data = +''
53
53
  @blob_client.stub :get_blob_properties, @raw_cloud_blob do
54
54
  @blob_client.stub :get_blob, @blob_with_content do
55
55
  @service.get_blob('test_container', 'test_blob') do |chunk, _remaining_bytes, _total_bytes|
@@ -61,7 +61,7 @@ class TestGetBlob < Minitest::Test
61
61
  end
62
62
 
63
63
  def test_get_blob_with_block_given_with_emtpy_blob_success
64
- data = ''
64
+ data = +''
65
65
  empty_raw_cloud_blob = @raw_cloud_blob
66
66
  empty_raw_cloud_blob.properties[:content_length] = 0
67
67
  empty_blob_with_content = [
@@ -79,7 +79,7 @@ class TestGetBlob < Minitest::Test
79
79
  end
80
80
 
81
81
  def test_get_blob_with_block_given_with_emtpy_range_success
82
- data = ''
82
+ data = +''
83
83
  options = {
84
84
  start_range: 1024,
85
85
  end_range: 1024
@@ -132,7 +132,7 @@ class TestGetBlob < Minitest::Test
132
132
  end
133
133
 
134
134
  def test_get_blob_with_block_given_mock
135
- data = ''
135
+ data = +''
136
136
  @mock_service.get_blob('test_container', 'test_blob') do |chunk, _remaining_bytes, _total_bytes|
137
137
  data << chunk
138
138
  end
@@ -118,18 +118,7 @@ class TestGetBlobHttpsUrl < Minitest::Test
118
118
 
119
119
  requested_expiry = Time.now + 60
120
120
 
121
- response = <<~MSG
122
- <UserDelegationKey>
123
- <SignedOid>f81d4fae-7dec-11d0-a765-00a0c91e6bf6</SignedOid>
124
- <SignedTid>72f988bf-86f1-41af-91ab-2d7cd011db47</SignedTid>
125
- <SignedStart>2024-09-19T00:00:00Z</SignedStart>
126
- <SignedExpiry>2024-09-26T00:00:00Z</SignedExpiry>
127
- <SignedService>b</SignedService>
128
- <SignedVersion>2020-02-10</SignedVersion>
129
- <Value>UDELEGATIONKEYXYZ....</Value>
130
- <SignedKey>rL7...ABC</SignedKey>
131
- </UserDelegationKey>
132
- MSG
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
- stubbed_times = []
163
- requested_expiries = []
164
- expected_user_delegation_key_starts = []
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
- # initial request
167
- stubbed_times << ref_time
168
- requested_expiries << stubbed_times.last + 1 * HOUR
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 = 'test-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+\ntest-expiry/m])
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 = <<~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
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.4.0
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: 2025-09-30 00:00:00.000000000 Z
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
@@ -40,14 +40,14 @@ dependencies:
40
40
  requirements:
41
41
  - - "~>"
42
42
  - !ruby/object:Gem::Version
43
- version: 5.8.4
43
+ version: '5.27'
44
44
  type: :development
45
45
  prerelease: false
46
46
  version_requirements: !ruby/object:Gem::Requirement
47
47
  requirements:
48
48
  - - "~>"
49
49
  - !ruby/object:Gem::Version
50
- version: 5.8.4
50
+ version: '5.27'
51
51
  - !ruby/object:Gem::Dependency
52
52
  name: rake
53
53
  requirement: !ruby/object:Gem::Requirement
@@ -68,14 +68,14 @@ dependencies:
68
68
  requirements:
69
69
  - - "~>"
70
70
  - !ruby/object:Gem::Version
71
- version: 0.89.1
71
+ version: 1.86.0
72
72
  type: :development
73
73
  prerelease: false
74
74
  version_requirements: !ruby/object:Gem::Requirement
75
75
  requirements:
76
76
  - - "~>"
77
77
  - !ruby/object:Gem::Version
78
- version: 0.89.1
78
+ version: 1.86.0
79
79
  - !ruby/object:Gem::Dependency
80
80
  name: simplecov
81
81
  requirement: !ruby/object:Gem::Requirement
@@ -132,6 +132,20 @@ dependencies:
132
132
  - - "~>"
133
133
  - !ruby/object:Gem::Version
134
134
  version: '1.8'
135
+ - !ruby/object:Gem::Dependency
136
+ name: base64
137
+ requirement: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ type: :runtime
143
+ prerelease: false
144
+ version_requirements: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - ">="
147
+ - !ruby/object:Gem::Version
148
+ version: '0'
135
149
  - !ruby/object:Gem::Dependency
136
150
  name: faraday
137
151
  requirement: !ruby/object:Gem::Requirement
@@ -152,14 +166,14 @@ dependencies:
152
166
  requirements:
153
167
  - - "~>"
154
168
  - !ruby/object:Gem::Version
155
- version: 0.3.0
169
+ version: 0.5.0
156
170
  type: :runtime
157
171
  prerelease: false
158
172
  version_requirements: !ruby/object:Gem::Requirement
159
173
  requirements:
160
174
  - - "~>"
161
175
  - !ruby/object:Gem::Version
162
- version: 0.3.0
176
+ version: 0.5.0
163
177
  - !ruby/object:Gem::Dependency
164
178
  name: faraday-net_http_persistent
165
179
  requirement: !ruby/object:Gem::Requirement
@@ -462,6 +476,7 @@ files:
462
476
  - test/requests/storage/test_release_blob_lease.rb
463
477
  - test/requests/storage/test_release_container_lease.rb
464
478
  - test/requests/storage/test_save_page_blob.rb
479
+ - test/requests/storage/test_storage_thread_safety.rb
465
480
  - test/requests/storage/test_wait_blob_copy_operation_to_finish.rb
466
481
  - test/test_helper.rb
467
482
  - test/unit/test_credentials.rb
@@ -482,7 +497,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
482
497
  requirements:
483
498
  - - ">="
484
499
  - !ruby/object:Gem::Version
485
- version: 2.7.0
500
+ version: 3.2.0
486
501
  required_rubygems_version: !ruby/object:Gem::Requirement
487
502
  requirements:
488
503
  - - ">="