gitlab-fog-azure-rm 2.1.0 → 2.2.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: 4d92167cf3193f73d9c97ae76465057206f076c4f2a14e1f8a12c677d77ca2f0
4
- data.tar.gz: 04fce79fa98d5f6dbb512dc974678be18ccb45cc84921c6914e61a60f4a6a62c
3
+ metadata.gz: 6c0db6ddb892cbf22fdd3bd0a6aa859dc54256572f87ff5fa4af0ab478127ba1
4
+ data.tar.gz: ac3ee5c92ab35dae5050dbf71a68c5d4f74cb0549c1b2c6cd55bfb3676099141
5
5
  SHA512:
6
- metadata.gz: a4eca952fa0daa27b5149654960ae75b4b875d128114388f2cc539b3a3e1bdd2874e03456f60b8da7a2b05f5d994cdcd58962940e2abc1c328bdddfec2f86960
7
- data.tar.gz: b503003e15965b9076b1ad75b25fd1282dfae71f4eacd806be13c6bc004a9bad5843c491c568fe4ff7cc42e09f9d6ac367c4fc7db9b27efa62288badb5f3477b
6
+ metadata.gz: a876f5d3cd330701883c5185313faba45a00403577388a102d658b51a88cdf56bb1d46130668ad5b5e54557416048830dbc21bb458f844218e8cb9777ed7aca3
7
+ data.tar.gz: a6c70245610e9ad32f587005888bdfd8da48a6d0b52c48fdf5db0a9a7d7141bf33bf9ba74bda4c6a7793cc37648f65d3cb9bcfc5033d819874a22c26a69ae25e
data/.rubocop.yml CHANGED
@@ -6,3 +6,7 @@ AllCops:
6
6
  Exclude:
7
7
  - 'lib/azure/**/*'
8
8
  - 'vendor/**/*'
9
+
10
+ Style/Documentation:
11
+ Exclude:
12
+ - 'test/**/*'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  ## Unreleased
2
2
 
3
+ ## 2.2.0
4
+
5
+ - Add support for workflow and managed identities !54
6
+
3
7
  ## 2.1.0
4
8
 
5
9
  - Drop IPAddr patches !52
@@ -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 'webmock'
25
26
  spec.add_development_dependency 'webrick', '~> 1.8'
26
27
 
27
28
  spec.add_dependency 'faraday', "~> 2.0"
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: trues
2
+
3
+ require 'json'
4
+
5
+ module Fog
6
+ module AzureRM
7
+ module Identity
8
+ # BaseClient is responsible for fetching credentials and refreshing
9
+ # them when necessary.
10
+ class BaseClient
11
+ include Fog::AzureRM::Utilities::General
12
+ attr_accessor :credentials
13
+
14
+ FetchCredentialsError = Class.new(RuntimeError)
15
+
16
+ DEFAULT_TIMEOUT_S = 30
17
+
18
+ def fetch_credentials
19
+ raise NotImplementedError
20
+ end
21
+
22
+ def fetch_credentials_if_needed
23
+ @credentials = fetch_credentials if @credentials.nil? || refresh_needed?
24
+
25
+ credentials
26
+ end
27
+
28
+ def refresh_needed?
29
+ return true unless @credentials
30
+
31
+ @credentials.refresh_needed?
32
+ end
33
+
34
+ protected
35
+
36
+ def process_token_response(response)
37
+ # If we get an unauthorized error, raising an exception allows the admin
38
+ # diagnose the error with the federated credentials.
39
+ raise FetchCredentialsError, response.to_s unless response.success?
40
+
41
+ body = ::JSON.parse(response.body)
42
+ access_token = body['access_token']
43
+
44
+ return unless access_token
45
+
46
+ expires_at = ::Time.now
47
+ expires_on = body['expires_on']
48
+ expires_at = ::Time.at(expires_on.to_i) if expires_on
49
+
50
+ Credentials.new(access_token, expires_at)
51
+ rescue ::JSON::ParserError # rubocop:disable Lint/SuppressedException
52
+ end
53
+
54
+ private
55
+
56
+ def get(url, params: nil, headers: nil)
57
+ Faraday.get(url, params, headers) do |req|
58
+ req.options.timeout = DEFAULT_TIMEOUT_S
59
+ end
60
+ rescue ::Faraday::Error => e
61
+ raise FetchCredentialsError, e.to_s
62
+ end
63
+
64
+ def post(url, body: nil, headers: nil)
65
+ Faraday.post(url, body, headers) do |req|
66
+ req.options.timeout = DEFAULT_TIMEOUT_S
67
+ end
68
+ rescue ::Faraday::Error => e
69
+ raise FetchCredentialsError, e.to_s
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,23 @@
1
+ module Fog
2
+ module AzureRM
3
+ module Identity
4
+ # Credentials stores the access token and its expiry.
5
+ class Credentials
6
+ attr_accessor :token, :expires_at
7
+
8
+ EXPIRATION_BUFFER = 600 # 10 minutes
9
+
10
+ def initialize(token, expires_at)
11
+ @token = token
12
+ @expires_at = expires_at
13
+ end
14
+
15
+ def refresh_needed?
16
+ return true unless expires_at
17
+
18
+ Time.now >= expires_at + EXPIRATION_BUFFER
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative 'base_client'
5
+
6
+ module Fog
7
+ module AzureRM
8
+ # DefaultCredentials attempts to resolve the credentials necessary to access
9
+ # the Azure service.
10
+ class DefaultCredentials
11
+ def initialize(options)
12
+ @options = options
13
+ @credential_client = nil
14
+ @credentials = nil
15
+ end
16
+
17
+ def fetch_credentials_if_needed
18
+ return unless credential_client
19
+
20
+ credential_client.fetch_credentials_if_needed
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :options
26
+
27
+ def credential_client
28
+ return @credential_client if @credential_client
29
+
30
+ clients = [
31
+ Fog::AzureRM::Identity::WorkflowIdentityClient,
32
+ Fog::AzureRM::Identity::ManagedIdentityClient
33
+ ]
34
+
35
+ credentials = nil
36
+ clients.each do |klass|
37
+ client = klass.new(options)
38
+
39
+ begin
40
+ credentials = client.fetch_credentials
41
+ rescue Fog::AzureRM::Identity::BaseClient::FetchCredentialsError
42
+ next
43
+ end
44
+
45
+ if credentials
46
+ @credential_client = client
47
+ break
48
+ end
49
+ end
50
+
51
+ return unless credentials
52
+
53
+ @credentials = credentials
54
+ @credentials
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'faraday'
5
+
6
+ module Fog
7
+ module AzureRM
8
+ module Identity
9
+ IDENTITY_ENDPOINT = 'http://169.254.169.254/metadata/identity/oauth2/token'
10
+ API_VERSION = '2018-02-01'
11
+
12
+ # ManagedIdentityClient fetches temporary credentials from the instance metadata endpoint.
13
+ class ManagedIdentityClient < BaseClient
14
+ include Fog::AzureRM::Utilities::General
15
+
16
+ attr_reader :resource
17
+
18
+ def initialize(options)
19
+ super()
20
+ @environment = options[:environment]
21
+ @resource = storage_resource(@environment)
22
+ end
23
+
24
+ # This method obtains a token via the Azure Instance Metadata Service (IMDS) endpoint:
25
+ # https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http
26
+ def fetch_credentials
27
+ url = "#{identity_endpoint}?api-version=#{api_version}&resource=#{CGI.escape(resource)}"
28
+
29
+ client_id = ENV['AZURE_CLIENT_ID']
30
+ url += "&client_id=#{client_id}" if client_id
31
+
32
+ headers = { 'Metadata' => 'true' }
33
+ headers['X-IDENTITY-HEADER'] = ENV['IDENTITY_HEADER'] if ENV['IDENTITY_HEADER']
34
+
35
+ response = get(url, headers: headers)
36
+ process_token_response(response)
37
+ end
38
+
39
+ private
40
+
41
+ def identity_endpoint
42
+ ENV['IDENTITY_ENDPOINT'] || IDENTITY_ENDPOINT
43
+ end
44
+
45
+ def api_version
46
+ ENV['IDENTITY_ENDPOINT'] ? '2019-08-01' : API_VERSION
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Fog
6
+ module AzureRM
7
+ module Identity
8
+ # WorkflowIdentityClient attempts to fetch credentials for Azure Workflow Identity
9
+ # via the following environment variables:
10
+ #
11
+ # - AZURE_AUTHORITY_HOST - This can be used to override the default authority URL.
12
+ # - AZURE_TENANT_ID
13
+ # - AZURE_CLIENT_ID
14
+ # - AZURE_FEDERATED_TOKEN_FILE - This is a filename that stores the JWT token that
15
+ # is exchanged for an OAuth2 token.
16
+ class WorkflowIdentityClient < BaseClient
17
+ include Fog::AzureRM::Utilities::General
18
+
19
+ attr_accessor :environment, :resource, :authority, :tenant_id, :client_id, :token_file
20
+
21
+ def initialize(options)
22
+ super()
23
+ @environment = options[:environment]
24
+ @resource = storage_resource(@environment)
25
+ @authority = ENV['AZURE_AUTHORITY_HOST'] || authority_url(@environment)
26
+ @tenant_id = ENV['AZURE_TENANT_ID']
27
+ @client_id = ENV['AZURE_CLIENT_ID']
28
+ @token_file = ENV['AZURE_FEDERATED_TOKEN_FILE']
29
+
30
+ normalize_authority!
31
+ end
32
+
33
+ def fetch_credentials
34
+ return unless authority && tenant_id && client_id
35
+ return unless ::File.exist?(token_file) && ::File.readable?(token_file)
36
+
37
+ oidc_token = ::File.read(token_file)
38
+ token_url = "#{authority}/#{tenant_id}/oauth2/v2.0/token"
39
+ scope = "#{storage_resource(@environment)}/.default"
40
+
41
+ data = {
42
+ client_id: client_id,
43
+ grant_type: 'client_credentials',
44
+ client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
45
+ client_assertion: oidc_token,
46
+ scope: scope
47
+ }
48
+
49
+ response = post(token_url, body: data)
50
+
51
+ process_token_response(response)
52
+ rescue ::Faraday::Error => e
53
+ raise FetchCredentialsError, e.to_s
54
+ end
55
+
56
+ private
57
+
58
+ # The Azure Python SDK handles an authority with or without a scheme, so let's
59
+ # do this too: https://github.com/Azure/azure-sdk-for-python/pull/11050
60
+ def normalize_authority!
61
+ parsed = URI.parse(authority)
62
+
63
+ unless parsed.scheme
64
+ @authority = "https://#{authority.strip.chomp('/')}"
65
+ return
66
+ end
67
+
68
+ # rubocop:disable Style/IfUnlessModifier
69
+ unless parsed.scheme == 'https'
70
+ raise ArgumentError, "'#{authority}' is an invalid authority. The value must be a TLS protected (https) URL."
71
+ end
72
+ # rubocop:enable Style/IfUnlessModifier
73
+
74
+ @authority = authority.strip.chomp('/')
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,5 @@
1
+ require_relative 'identity/base_client'
2
+ require_relative 'identity/credentials'
3
+ require_relative 'identity/default_credentials'
4
+ require_relative 'identity/managed_identity_client'
5
+ require_relative 'identity/workflow_identity_client'
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
1
5
  module Fog
2
6
  module AzureRM
3
7
  # This class registers models, requests and collections
@@ -80,6 +84,8 @@ module Fog
80
84
  class Real
81
85
  include Fog::AzureRM::Utilities::General
82
86
 
87
+ attr_accessor :options
88
+
83
89
  def initialize(options)
84
90
  begin
85
91
  require 'azure/storage/common'
@@ -93,19 +99,19 @@ module Fog
93
99
  raise e.message
94
100
  end
95
101
 
96
- return unless @azure_storage_account_name != options[:azure_storage_account_name] ||
97
- @azure_storage_access_key != options[:azure_storage_access_key] ||
98
- @azure_storage_token_signer != options[:azure_storage_token_signer]
102
+ options[:environment] = options[:environment] || ENV['AZURE_ENVIRONMENT'] || ENVIRONMENT_AZURE_CLOUD
103
+ @environment = options[:environment]
104
+ @options = options
99
105
 
100
106
  @azure_storage_account_name = options[:azure_storage_account_name]
101
107
  @azure_storage_access_key = options[:azure_storage_access_key]
102
- @azure_storage_token_signer = options[:azure_storage_token_signer]
108
+
109
+ load_credentials
110
+
111
+ @azure_storage_token_signer = token_signer
103
112
  @azure_storage_endpoint = options[:azure_storage_endpoint]
104
113
  @azure_storage_domain = options[:azure_storage_domain]
105
114
 
106
- options[:environment] = 'AzureCloud' if options[:environment].nil?
107
- @environment = options[:environment]
108
-
109
115
  storage_blob_host =
110
116
  @azure_storage_endpoint ||
111
117
  if @azure_storage_domain.nil? || @azure_storage_domain.empty?
@@ -128,6 +134,26 @@ module Fog
128
134
 
129
135
  private
130
136
 
137
+ def load_credentials
138
+ return options[:azure_storage_token_signer] if options[:azure_storage_token_signer]
139
+ return if @azure_storage_access_key && !@azure_storage_access_key.empty?
140
+
141
+ @credential_client = Fog::AzureRM::DefaultCredentials.new(options)
142
+ @credentials = @credential_client.fetch_credentials_if_needed
143
+ end
144
+
145
+ def token_signer
146
+ return options[:azure_storage_token_signer] if options[:azure_storage_token_signer]
147
+ return unless @credentials
148
+
149
+ access_token_signer(@credentials.token)
150
+ end
151
+
152
+ def access_token_signer(access_token)
153
+ cred = Azure::Storage::Common::Core::TokenCredential.new(access_token)
154
+ Azure::Storage::Common::Core::Auth::TokenSigner.new(cred)
155
+ end
156
+
131
157
  def signature_client(requested_expiry)
132
158
  access_key = @azure_storage_access_key.to_s
133
159
  user_delegation_key = user_delegation_key(requested_expiry)
@@ -148,6 +174,11 @@ module Fog
148
174
  def user_delegation_key(requested_expiry)
149
175
  return nil unless @azure_storage_token_signer
150
176
 
177
+ if @credential_client
178
+ @credentials = @credential_client.fetch_credentials_if_needed
179
+ @azure_storage_token_signer = token_signer
180
+ end
181
+
151
182
  @user_delegation_key_mutex ||= Mutex.new
152
183
  @user_delegation_key_mutex.synchronize do
153
184
  if @user_delegation_key_expiry.nil? || @user_delegation_key_expiry < requested_expiry
@@ -89,6 +89,28 @@ module Fog
89
89
  end
90
90
  end
91
91
 
92
+ # Per https://learn.microsoft.com/en-us/azure/storage/blobs/authorize-access-azure-active-directory,
93
+ # all endpoints use the same resource URL.
94
+ def storage_resource(_environment = ENVIRONMENT_AZURE_CLOUD)
95
+ 'https://storage.azure.com'
96
+ end
97
+
98
+ # https://learn.microsoft.com/en-us/entra/identity-platform/authentication-national-cloud#microsoft-entra-authentication-endpoints
99
+ def authority_url(environment = ENVIRONMENT_AZURE_CLOUD)
100
+ case environment
101
+ when ENVIRONMENT_AZURE_CHINA_CLOUD
102
+ 'https://login.chinacloudapi.cn'
103
+ when ENVIRONMENT_AZURE_US_GOVERNMENT
104
+ 'https://login.microsoftonline.us'
105
+ # This region is deprecated:
106
+ # https://learn.microsoft.com/en-us/entra/identity-platform/authentication-national-cloud#azure-germany-microsoft-cloud-deutschland
107
+ when ENVIRONMENT_AZURE_GERMAN_CLOUD
108
+ 'https://login.microsoftonline.de'
109
+ else
110
+ 'https://login.microsoftonline.com'
111
+ end
112
+ end
113
+
92
114
  def get_blob_endpoint(storage_account_name, enable_https = false, environment = ENVIRONMENT_AZURE_CLOUD)
93
115
  protocol = enable_https ? 'https' : 'http'
94
116
  "#{protocol}://#{storage_account_name}.blob#{storage_endpoint_suffix(environment)}"
@@ -1,5 +1,5 @@
1
1
  module Fog
2
2
  module AzureRM
3
- VERSION = '2.1.0'.freeze
3
+ VERSION = '2.2.0'.freeze
4
4
  end
5
5
  end
data/lib/fog/azurerm.rb CHANGED
@@ -9,6 +9,7 @@ require 'fog/json'
9
9
  require 'fog/azurerm/models/storage/sku_name'
10
10
  require 'fog/azurerm/models/storage/sku_tier'
11
11
  require 'fog/azurerm/models/storage/kind'
12
+ require 'fog/azurerm/identity'
12
13
 
13
14
  module Fog
14
15
  # Main AzureRM fog Provider Module
data/rakefile CHANGED
@@ -8,6 +8,7 @@ task :test do
8
8
  Dir.glob('test/test_*.rb').each { |file| require File.expand_path file, __dir__ }
9
9
  Dir.glob('test/models/**/test_*.rb').each { |file| require File.expand_path file, __dir__ }
10
10
  Dir.glob('test/requests/**/test_*.rb').each { |file| require File.expand_path file, __dir__ }
11
+ Dir.glob('test/unit/**/test_*.rb').each { |file| require File.expand_path file, __dir__ }
11
12
  end
12
13
 
13
14
  task :integration do
@@ -112,7 +112,7 @@ class TestDirectory < Minitest::Test
112
112
  # Set private
113
113
  result = @directory.public = false
114
114
  assert !result
115
- assert_equal nil, @directory.attributes[:acl]
115
+ assert_nil @directory.attributes[:acl]
116
116
  end
117
117
 
118
118
  def test_public_url_method_with_public_success
@@ -104,6 +104,55 @@ class TestGetBlobHttpsUrl < Minitest::Test
104
104
  end
105
105
  end
106
106
 
107
+ def test_get_blob_https_url_with_managed_identity
108
+ token_response = {
109
+ 'access_token' => 'fake_token',
110
+ 'expires_on' => (Time.now + 3600).to_i.to_s
111
+ }
112
+
113
+ stub_request(:get, "#{Fog::AzureRM::Identity::IDENTITY_ENDPOINT}?api-version=#{Fog::AzureRM::Identity::API_VERSION}&resource=https://storage.azure.com")
114
+ .with(headers: { 'Metadata' => 'true' })
115
+ .to_return(status: 200, body: token_response.to_json)
116
+
117
+ service = Fog::AzureRM::Storage.new(storage_account_managed_identity)
118
+
119
+ requested_expiry = Time.now + 60
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
133
+
134
+ stub_request(:post, 'https://mockaccount.blob.core.windows.net?comp=userdelegationkey&restype=service')
135
+ .to_return(status: 200, headers: { 'Content-Type': 'application/xml' }, body: response)
136
+
137
+ url = service.get_blob_https_url('test_container', 'test_blob', requested_expiry)
138
+
139
+ parsed = URI.parse(url)
140
+
141
+ assert_equal 'https', parsed.scheme
142
+ assert_equal 'mockaccount.blob.core.windows.net', parsed.host
143
+ assert_equal '/test_container/test_blob', parsed.path
144
+
145
+ params = parsed.query.split('&').to_h { |x| x.split('=') }
146
+
147
+ assert_equal 'r', params['sp']
148
+ assert_equal '2018-11-09', params['sv']
149
+ assert_equal 'b', params['sr']
150
+ assert_equal 'https', params['spr']
151
+ assert_equal '2024-09-19T00%3A00%3A00Z', params['skt']
152
+ assert_equal '2024-09-26T00%3A00%3A00Z', params['ske']
153
+ assert_equal 'b', params['sks']
154
+ end
155
+
107
156
  def test_get_blob_https_url_with_token_signer_success
108
157
  service = Fog::AzureRM::Storage.new(storage_account_credentials_with_token_signer)
109
158
  blob_client = service.instance_variable_get(:@blob_client)
@@ -132,7 +181,7 @@ class TestGetBlobHttpsUrl < Minitest::Test
132
181
  # second request within new expiry
133
182
  stubbed_times << ref_time + 10.5 * DAY
134
183
  requested_expiries << stubbed_times.last + 1 * DAY
135
- # no additonal expected_user_delegation_key_starts
184
+ # no additional expected_user_delegation_key_starts
136
185
 
137
186
  user_delegation_key_starts = []
138
187
  mock_user_delegation_key = lambda do |start, expiry|
data/test/test_helper.rb CHANGED
@@ -1,3 +1,6 @@
1
+ require 'webmock/minitest'
2
+ WebMock.disable_net_connect! allow: %w[127.0.0.1]
3
+
1
4
  if ENV['COVERAGE']
2
5
  require 'simplecov'
3
6
  SimpleCov.start do
@@ -42,6 +45,12 @@ def storage_account_credentials
42
45
  }
43
46
  end
44
47
 
48
+ def storage_account_managed_identity
49
+ {
50
+ azure_storage_account_name: 'mockaccount'
51
+ }
52
+ end
53
+
45
54
  def storage_account_credentials_with_endpoint
46
55
  storage_account_credentials.merge(
47
56
  {
@@ -0,0 +1,97 @@
1
+ require File.expand_path '../test_helper', __dir__
2
+
3
+ class TestDefaultCredentials < Minitest::Test
4
+ def setup
5
+ @options = { environment: 'AzureCloud' }
6
+ @default_credentials = Fog::AzureRM::DefaultCredentials.new(@options)
7
+ end
8
+
9
+ def test_initialize
10
+ assert_instance_of Fog::AzureRM::DefaultCredentials, @default_credentials
11
+ assert_nil @default_credentials.instance_variable_get(:@credential_client)
12
+ assert_nil @default_credentials.instance_variable_get(:@credentials)
13
+ end
14
+
15
+ def test_fetch_credentials_if_needed_with_no_credential_client
16
+ @default_credentials.stub(:credential_client, nil) do
17
+ assert_nil @default_credentials.fetch_credentials_if_needed
18
+ end
19
+ end
20
+
21
+ def test_fetch_credentials_if_needed_with_credential_client
22
+ mock_client = Minitest::Mock.new
23
+ mock_client.expect :fetch_credentials_if_needed, 'fake_credentials'
24
+ @default_credentials.instance_variable_set(:@credential_client, mock_client)
25
+
26
+ assert_equal 'fake_credentials', @default_credentials.fetch_credentials_if_needed
27
+ mock_client.verify
28
+ end
29
+
30
+ def test_credential_client_with_workflow_identity
31
+ mock_workflow_client = Minitest::Mock.new
32
+ mock_workflow_client.expect :fetch_credentials, 'fake_workflow_credentials'
33
+
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
37
+ end
38
+
39
+ mock_workflow_client.verify
40
+ end
41
+
42
+ 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
53
+ end
54
+ end
55
+
56
+ mock_workflow_client.verify
57
+ mock_managed_client.verify
58
+ end
59
+
60
+ 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
65
+
66
+ mock_managed_client = Minitest::Mock.new
67
+ mock_managed_client.expect :fetch_credentials, 'fake_managed_credentials'
68
+
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
73
+ end
74
+ end
75
+
76
+ mock_workflow_client.verify
77
+ mock_managed_client.verify
78
+ end
79
+
80
+ def test_credential_client_with_no_credentials
81
+ mock_workflow_client = Minitest::Mock.new
82
+ mock_workflow_client.expect :fetch_credentials, nil
83
+
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
89
+ assert_nil @default_credentials.send(:credential_client)
90
+ assert_nil @default_credentials.instance_variable_get(:@credential_client)
91
+ end
92
+ end
93
+
94
+ mock_workflow_client.verify
95
+ mock_managed_client.verify
96
+ end
97
+ end
@@ -0,0 +1,74 @@
1
+ require File.expand_path '../test_helper', __dir__
2
+
3
+ class TestManagedIdentityClient < Minitest::Test
4
+ def setup
5
+ @options = { environment: 'AzureCloud' }
6
+ @client = Fog::AzureRM::Identity::ManagedIdentityClient.new(@options)
7
+ end
8
+
9
+ def teardown
10
+ ENV.delete('AZURE_CLIENT_ID')
11
+ ENV.delete('IDENTITY_HEADER')
12
+ ENV.delete('IDENTITY_ENDPOINT')
13
+ end
14
+
15
+ def test_initialize
16
+ assert_equal 'https://storage.azure.com', @client.resource
17
+ end
18
+
19
+ def test_fetch_credentials_success
20
+ token_response = {
21
+ 'access_token' => 'fake_token',
22
+ 'expires_on' => (Time.now + 3600).to_i.to_s
23
+ }
24
+
25
+ stub_request(:get, "#{Fog::AzureRM::Identity::IDENTITY_ENDPOINT}?api-version=#{Fog::AzureRM::Identity::API_VERSION}&resource=#{CGI.escape(@client.resource)}")
26
+ .with(headers: { 'Metadata' => 'true' })
27
+ .to_return(status: 200, body: token_response.to_json)
28
+
29
+ credentials = @client.fetch_credentials
30
+
31
+ assert_equal 'fake_token', credentials.token
32
+ assert_instance_of Time, credentials.expires_at
33
+ end
34
+
35
+ def test_fetch_credentials_timeout
36
+ stub_request(:get, "#{Fog::AzureRM::Identity::IDENTITY_ENDPOINT}?api-version=#{Fog::AzureRM::Identity::API_VERSION}&resource=#{CGI.escape(@client.resource)}")
37
+ .with(headers: { 'Metadata' => 'true' })
38
+ .to_raise(Faraday::Error)
39
+
40
+ assert_raises(Fog::AzureRM::Identity::BaseClient::FetchCredentialsError) do
41
+ @client.fetch_credentials
42
+ end
43
+ end
44
+
45
+ def test_fetch_credentials_unauthorized
46
+ stub_request(:get, "#{Fog::AzureRM::Identity::IDENTITY_ENDPOINT}?api-version=#{Fog::AzureRM::Identity::API_VERSION}&resource=#{CGI.escape(@client.resource)}")
47
+ .with(headers: { 'Metadata' => 'true' })
48
+ .to_return(status: 401)
49
+
50
+ assert_raises(Fog::AzureRM::Identity::BaseClient::FetchCredentialsError) do
51
+ @client.fetch_credentials
52
+ end
53
+ end
54
+
55
+ def test_fetch_credentials_with_env_vars
56
+ token_response = {
57
+ 'access_token' => 'fake_token',
58
+ 'expires_on' => (Time.now + 3600).to_i.to_s
59
+ }
60
+
61
+ ENV['AZURE_CLIENT_ID'] = 'fake_client_id'
62
+ ENV['IDENTITY_HEADER'] = 'fake_identity_header'
63
+ ENV['IDENTITY_ENDPOINT'] = 'http://localhost:8080/metadata/identity/oauth2/token'
64
+
65
+ stub_request(:get, "http://localhost:8080/metadata/identity/oauth2/token?api-version=2019-08-01&client_id=fake_client_id&resource=#{CGI.escape(@client.resource)}")
66
+ .with(headers: { 'Metadata' => 'true', 'X-Identity-Header' => 'fake_identity_header' })
67
+ .to_return(status: 200, body: token_response.to_json)
68
+
69
+ credentials = @client.fetch_credentials
70
+
71
+ assert_equal 'fake_token', credentials.token
72
+ assert_instance_of Time, credentials.expires_at
73
+ end
74
+ end
@@ -0,0 +1,147 @@
1
+ require File.expand_path '../test_helper', __dir__
2
+
3
+ class TestWorkflowIdentityClient < Minitest::Test
4
+ def setup
5
+ @options = { environment: 'AzureCloud' }
6
+ @client = Fog::AzureRM::Identity::WorkflowIdentityClient.new(@options)
7
+ end
8
+
9
+ def teardown
10
+ ENV.delete('AZURE_TENANT_ID')
11
+ ENV.delete('AZURE_CLIENT_ID')
12
+ ENV.delete('AZURE_FEDERATED_TOKEN_FILE')
13
+ ENV.delete('AZURE_AUTHORITY_HOST')
14
+ end
15
+
16
+ def test_initialize
17
+ assert_equal 'https://storage.azure.com', @client.resource
18
+ assert_equal 'https://login.microsoftonline.com', @client.authority
19
+ assert_nil @client.tenant_id
20
+ assert_nil @client.client_id
21
+ end
22
+
23
+ def test_fetch_credentials_success
24
+ ENV['AZURE_TENANT_ID'] = 'fake_tenant_id'
25
+ ENV['AZURE_CLIENT_ID'] = 'fake_client_id'
26
+ ENV['AZURE_FEDERATED_TOKEN_FILE'] = 'fake_token_file'
27
+
28
+ @client = Fog::AzureRM::Identity::WorkflowIdentityClient.new(@options)
29
+
30
+ token_response = {
31
+ 'access_token' => 'fake_access_token',
32
+ 'expires_on' => (Time.now + 3600).to_i.to_s
33
+ }
34
+
35
+ File.stub :exist?, true do
36
+ File.stub :readable?, true do
37
+ File.stub :read, 'fake_oidc_token' do
38
+ stub_request(:post, "#{@client.authority}/fake_tenant_id/oauth2/v2.0/token")
39
+ .to_return(status: 200, body: token_response.to_json)
40
+
41
+ credentials = @client.fetch_credentials
42
+
43
+ assert_equal 'fake_access_token', credentials.token
44
+ assert_instance_of Time, credentials.expires_at
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ def test_fetch_credentials_failure
51
+ ENV['AZURE_TENANT_ID'] = 'fake_tenant_id'
52
+ ENV['AZURE_CLIENT_ID'] = 'fake_client_id'
53
+ ENV['AZURE_FEDERATED_TOKEN_FILE'] = 'fake_token_file'
54
+
55
+ @client = Fog::AzureRM::Identity::WorkflowIdentityClient.new(@options)
56
+
57
+ File.stub :exist?, true do
58
+ File.stub :readable?, true do
59
+ File.stub :read, 'fake_oidc_token' do
60
+ stub_request(:post, "#{@client.authority}/fake_tenant_id/oauth2/v2.0/token")
61
+ .to_raise(Faraday::Error)
62
+
63
+ assert_raises(Fog::AzureRM::Identity::BaseClient::FetchCredentialsError) do
64
+ @client.fetch_credentials
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ def test_fetch_credentials_unauthorized
72
+ ENV['AZURE_TENANT_ID'] = 'fake_tenant_id'
73
+ ENV['AZURE_CLIENT_ID'] = 'fake_client_id'
74
+ ENV['AZURE_FEDERATED_TOKEN_FILE'] = 'fake_token_file'
75
+
76
+ @client = Fog::AzureRM::Identity::WorkflowIdentityClient.new(@options)
77
+
78
+ File.stub :exist?, true do
79
+ File.stub :readable?, true do
80
+ File.stub :read, 'fake_oidc_token' do
81
+ stub_request(:post, "#{@client.authority}/fake_tenant_id/oauth2/v2.0/token")
82
+ .to_return(status: 403, body: '{"error":"invalid_client"}')
83
+
84
+ assert_raises(Fog::AzureRM::Identity::BaseClient::FetchCredentialsError) { @client.fetch_credentials }
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ def test_fetch_credentials_missing_env_vars
91
+ ENV['AZURE_TENANT_ID'] = nil
92
+ ENV['AZURE_CLIENT_ID'] = nil
93
+ ENV['AZURE_FEDERATED_TOKEN_FILE'] = nil
94
+
95
+ assert_nil @client.fetch_credentials
96
+ end
97
+
98
+ def test_fetch_credentials_missing_token_file
99
+ ENV['AZURE_TENANT_ID'] = 'fake_tenant_id'
100
+ ENV['AZURE_CLIENT_ID'] = 'fake_client_id'
101
+ ENV['AZURE_FEDERATED_TOKEN_FILE'] = 'nonexistent_file'
102
+
103
+ File.stub :exist?, false do
104
+ assert_nil @client.fetch_credentials
105
+ end
106
+ end
107
+
108
+ def test_authority
109
+ ENV['AZURE_TENANT_ID'] = 'fake_tenant_id'
110
+ ENV['AZURE_CLIENT_ID'] = 'fake_client_id'
111
+ ENV['AZURE_FEDERATED_TOKEN_FILE'] = 'fake_token_file'
112
+
113
+ # Test with default AzureCloud environment
114
+ client = Fog::AzureRM::Identity::WorkflowIdentityClient.new(@options)
115
+ assert_equal 'https://login.microsoftonline.com', client.authority
116
+ assert_equal 'https://storage.azure.com', client.resource
117
+
118
+ # Test with AzureUSGovernment environment
119
+ gov_options = { environment: 'AzureUSGovernment' }
120
+ gov_client = Fog::AzureRM::Identity::WorkflowIdentityClient.new(gov_options)
121
+ assert_equal 'https://login.microsoftonline.us', gov_client.authority
122
+ assert_equal 'https://storage.azure.com', gov_client.resource
123
+
124
+ # Test with AzureChina environment
125
+ china_options = { environment: 'AzureChinaCloud' }
126
+ china_client = Fog::AzureRM::Identity::WorkflowIdentityClient.new(china_options)
127
+ assert_equal 'https://login.chinacloudapi.cn', china_client.authority
128
+ assert_equal 'https://storage.azure.com', china_client.resource
129
+
130
+ # Test with AzureGermanCloud environment
131
+ german_options = { environment: 'AzureGermanCloud' }
132
+ german_client = Fog::AzureRM::Identity::WorkflowIdentityClient.new(german_options)
133
+ assert_equal 'https://login.microsoftonline.de', german_client.authority
134
+ assert_equal 'https://storage.azure.com', german_client.resource
135
+ end
136
+
137
+ def test_authority_normalize
138
+ ENV['AZURE_TENANT_ID'] = 'fake_tenant_id'
139
+ ENV['AZURE_CLIENT_ID'] = 'fake_client_id'
140
+ ENV['AZURE_FEDERATED_TOKEN_FILE'] = 'fake_token_file'
141
+ ENV['AZURE_AUTHORITY_HOST'] = 'login.microsoftonline.com'
142
+
143
+ # Test with default AzureCloud environment
144
+ client = Fog::AzureRM::Identity::WorkflowIdentityClient.new(@options)
145
+ assert_equal 'https://login.microsoftonline.com', client.authority
146
+ end
147
+ 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.1.0
4
+ version: 2.2.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-08-17 00:00:00.000000000 Z
21
+ date: 2024-11-21 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: webmock
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: webrick
95
109
  requirement: !ruby/object:Gem::Requirement
@@ -321,6 +335,12 @@ files:
321
335
  - lib/fog/azurerm/custom_fog_errors.rb
322
336
  - lib/fog/azurerm/docs/storage.md
323
337
  - lib/fog/azurerm/docs/structure.md
338
+ - lib/fog/azurerm/identity.rb
339
+ - lib/fog/azurerm/identity/base_client.rb
340
+ - lib/fog/azurerm/identity/credentials.rb
341
+ - lib/fog/azurerm/identity/default_credentials.rb
342
+ - lib/fog/azurerm/identity/managed_identity_client.rb
343
+ - lib/fog/azurerm/identity/workflow_identity_client.rb
324
344
  - lib/fog/azurerm/identity_encoding_filter.rb
325
345
  - lib/fog/azurerm/models/storage/directories.rb
326
346
  - lib/fog/azurerm/models/storage/directory.rb
@@ -430,6 +450,9 @@ files:
430
450
  - test/requests/storage/test_save_page_blob.rb
431
451
  - test/requests/storage/test_wait_blob_copy_operation_to_finish.rb
432
452
  - test/test_helper.rb
453
+ - test/unit/test_default_credentials.rb
454
+ - test/unit/test_managed_identity_client.rb
455
+ - test/unit/test_workflow_identity_client.rb
433
456
  homepage: https://gitlab.com/gitlab-org/gitlab-fog-azure-rm
434
457
  licenses:
435
458
  - MIT
@@ -450,7 +473,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
450
473
  - !ruby/object:Gem::Version
451
474
  version: '0'
452
475
  requirements: []
453
- rubygems_version: 3.3.27
476
+ rubygems_version: 3.5.18
454
477
  signing_key:
455
478
  specification_version: 4
456
479
  summary: Module for the 'fog' gem to support Azure Blob Storage with CarrierWave and