gitlab-fog-azure-rm 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
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