kitchen-azurerm 2.0.0 → 2.1.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: '0853e2f879329c6b0664e4931574d1ca3a8b8998fc1a5e56424eab212b2f95b1'
4
- data.tar.gz: e00246cb4d2f291492df022874e9755fb6dc7745067428dd2f1ac291e57fc32f
3
+ metadata.gz: f429ccdaffbdba117dea3b15b9e8beff46b7e5d1e0e1b4e23f0ab2565b652685
4
+ data.tar.gz: cd8ac4c9961e845d121331825a74bc20c85d0317db726f58d5954aedb8b5ffe0
5
5
  SHA512:
6
- metadata.gz: c8016844395a76393d56fcf31d30b7cb4e14512566c0da7a3a73aeb5fb120f4232d6c1c678feb5ca4d7cd9b159613991b0c759764ff549245e82e5055d8a3ba3
7
- data.tar.gz: ec9337480b1b58d0b0a5dc461c99e8d1abbf97e9f0b95dfc8c41b34acf3a4580b0701dfaf0323dd7660164f27602bc9ef912b29cab7cbba5e66a6a99fd7b5cbe
6
+ metadata.gz: 03a1ed78caa550f245b466c9cfb218dda27d45f50e2b158371e4b14b9f1d45e30d478c277a0ccf437a00855d8dd2f8b8d883d51578b66d750d51c355859d1073
7
+ data.tar.gz: 181aa2863481530c6d2d78d2d050918dedbc71f51723928eb166dd8ae78d7dd966090f5a112a6202b74d160fba62cf695b29e86335853038672dc342e8577628
@@ -19,13 +19,45 @@ module Kitchen
19
19
  # @!attribute [r] token_audience
20
20
  # @return [String] audience/resource the access token is requested for.
21
21
  Environment = Struct.new(:name, :resource_manager_url, :authentication_endpoint, :token_audience) do
22
- # The OAuth2 token URL for a tenant in this cloud.
22
+ # The OAuth2 v1.0 token URL for a tenant in this cloud.
23
23
  #
24
24
  # @param tenant_id [String]
25
25
  # @return [String]
26
26
  def token_url(tenant_id)
27
27
  "#{authentication_endpoint.chomp("/")}/#{tenant_id}/oauth2/token"
28
28
  end
29
+
30
+ # The OAuth2 v2.0 token URL for a tenant in this cloud.
31
+ #
32
+ # Federated (assertion-based) client credentials are documented
33
+ # against v2.0, which takes a +scope+ rather than a +resource+.
34
+ #
35
+ # @param tenant_id [String]
36
+ # @return [String]
37
+ def token_url_v2(tenant_id)
38
+ "#{authentication_endpoint.chomp("/")}/#{tenant_id}/oauth2/v2.0/token"
39
+ end
40
+
41
+ # The v2.0 scope covering every permission this driver needs.
42
+ #
43
+ # @return [String] e.g. +"https://management.azure.com/.default"+
44
+ def default_scope
45
+ "#{resource_manager_url.chomp("/")}/.default"
46
+ end
47
+
48
+ # A copy of this cloud pointed at a different Entra ID authority.
49
+ #
50
+ # Platforms that issue federated tokens - AKS in particular - set
51
+ # +AZURE_AUTHORITY_HOST+ to say where those tokens should be
52
+ # exchanged.
53
+ #
54
+ # @param authority [String, nil] the authority URL, or nil to keep this one.
55
+ # @return [Environment]
56
+ def with_authority(authority)
57
+ return self if authority.to_s.empty?
58
+
59
+ self.class.new(name, resource_manager_url, authority, token_audience).freeze
60
+ end
29
61
  end
30
62
 
31
63
  # Every supported cloud, keyed by its downcased name so that lookups are
@@ -120,6 +120,67 @@ module Kitchen
120
120
  end
121
121
  end
122
122
 
123
+ # Authenticates with a federated token issued by another identity
124
+ # provider - GitHub Actions, GitLab, Azure DevOps, or a Kubernetes
125
+ # service account - rather than a stored secret.
126
+ #
127
+ # The platform writes a short-lived signed assertion to a file and
128
+ # rotates it; the file is therefore re-read on every token fetch rather
129
+ # than cached, or a long +kitchen test+ run would start presenting an
130
+ # assertion that has since expired.
131
+ class WorkloadIdentityToken < TokenProvider
132
+ # @return [String] the client assertion type required by Entra ID.
133
+ ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer".freeze
134
+
135
+ # @param environment [Environments::Environment]
136
+ # @param tenant_id [String]
137
+ # @param client_id [String]
138
+ # @param token_file [String] path to the federated token file.
139
+ def initialize(environment:, tenant_id:, client_id:, token_file:)
140
+ super(environment:)
141
+ @tenant_id = tenant_id
142
+ @client_id = client_id
143
+ @token_file = token_file
144
+ end
145
+
146
+ # @return [String] path to the federated token file.
147
+ attr_reader :token_file
148
+
149
+ # @return [Array(String, Integer)]
150
+ # @raise [OperationError] if the assertion file cannot be read.
151
+ def fetch_token
152
+ response = Http.request(
153
+ method: :post,
154
+ url: environment.token_url_v2(@tenant_id),
155
+ headers: { "Content-Type" => "application/x-www-form-urlencoded" },
156
+ body: URI.encode_www_form(
157
+ grant_type: "client_credentials",
158
+ client_id: @client_id,
159
+ client_assertion_type: ASSERTION_TYPE,
160
+ client_assertion: assertion,
161
+ scope: environment.default_scope
162
+ )
163
+ )
164
+
165
+ token_from(response, "the workload identity token endpoint")
166
+ end
167
+
168
+ private
169
+
170
+ # The current federated assertion, read fresh each time.
171
+ #
172
+ # @return [String]
173
+ # @raise [OperationError] if the file is missing or unreadable.
174
+ def assertion
175
+ File.read(token_file).strip
176
+ rescue SystemCallError => e
177
+ raise OperationError.new(
178
+ "Could not read the federated token file at #{token_file} (#{e.class}). " \
179
+ "AZURE_FEDERATED_TOKEN_FILE must point at a readable assertion issued by your CI platform."
180
+ )
181
+ end
182
+ end
183
+
123
184
  # Authenticates as a managed identity, via the Instance Metadata Service.
124
185
  #
125
186
  # This replaces the legacy MSI extension endpoint on port 50342 that the
@@ -74,16 +74,24 @@ module Kitchen
74
74
 
75
75
  # Endpoints for the configured cloud.
76
76
  #
77
+ # +AZURE_AUTHORITY_HOST+ overrides the Entra ID endpoint, which is how
78
+ # platforms that issue federated tokens point at their own authority.
79
+ #
77
80
  # @return [Azure::Environments::Environment]
78
81
  # @raise [Kitchen::UserError] if the cloud name is not recognised.
79
82
  def azure_environment
80
- @azure_environment ||= Azure::Environments.fetch(environment)
83
+ @azure_environment ||= Azure::Environments.fetch(environment).with_authority(ENV["AZURE_AUTHORITY_HOST"])
81
84
  end
82
85
 
83
86
  # Selects a token provider based on which credentials resolved.
84
87
  #
88
+ # In precedence order:
89
+ #
90
+ # * +AZURE_FEDERATED_TOKEN_FILE+ + +client_id+ + +tenant_id+ - workload
91
+ # identity federation, the secretless option for CI.
85
92
  # * +client_id+ + +client_secret+ + +tenant_id+ - service principal.
86
- # * +client_id+ + +tenant_id+ - user-assigned managed identity.
93
+ # * +AZURE_USE_MSI+ - managed identity, explicitly.
94
+ # * +client_id+ without a secret - user-assigned managed identity.
87
95
  # * +tenant_id+ only - system-assigned managed identity.
88
96
  # * none of the above - falls back to the +az login+ token cache.
89
97
  #
@@ -104,11 +112,16 @@ module Kitchen
104
112
 
105
113
  # @return [Azure::TokenProvider]
106
114
  def build_token_provider
107
- if client_id && client_secret && tenant_id!
115
+ if federated_token_file && client_id && tenant_id!
116
+ debug "Authenticating with workload identity federation (#{federated_token_file})."
117
+ Azure::WorkloadIdentityToken.new(environment: azure_environment, tenant_id:, client_id:,
118
+ token_file: federated_token_file)
119
+ elsif client_id && client_secret && tenant_id!
108
120
  Azure::ServicePrincipalToken.new(environment: azure_environment, tenant_id:, client_id:, client_secret:)
109
- elsif client_id && tenant_id!
121
+ elsif use_managed_identity?
110
122
  Azure::ManagedIdentityToken.new(environment: azure_environment, client_id:)
111
123
  elsif tenant_id!
124
+ # A tenant with no client credentials means a system-assigned identity.
112
125
  Azure::ManagedIdentityToken.new(environment: azure_environment)
113
126
  else
114
127
  warn("Using tenant id set through `az login`.")
@@ -116,6 +129,27 @@ module Kitchen
116
129
  end
117
130
  end
118
131
 
132
+ # Whether to authenticate as a managed identity.
133
+ #
134
+ # A +client_id+ with no accompanying secret means a user-assigned managed
135
+ # identity, which is how the Azure SDKs read the same combination.
136
+ # Crucially this does not require a tenant: the instance metadata service
137
+ # never sees one, so demanding it only prevented the identity from being
138
+ # used at all.
139
+ #
140
+ # @return [Boolean]
141
+ def use_managed_identity?
142
+ return true unless ENV["AZURE_USE_MSI"].to_s.empty?
143
+
144
+ !client_id.nil? && client_secret.nil?
145
+ end
146
+
147
+ # @return [String, nil] path to a federated token file, if one is configured.
148
+ def federated_token_file
149
+ value = ENV["AZURE_FEDERATED_TOKEN_FILE"]
150
+ value unless value.to_s.empty?
151
+ end
152
+
119
153
  # @return [Kitchen::Logger] the shared Test Kitchen logger.
120
154
  def logger
121
155
  Kitchen.logger
@@ -6,6 +6,6 @@ module Kitchen
6
6
  # driver and, with it, the whole Azure SDK.
7
7
  #
8
8
  # @return [String]
9
- AZURERM_VERSION = "2.0.0".freeze
9
+ AZURERM_VERSION = "2.1.0".freeze
10
10
  end
11
11
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kitchen-azurerm
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stuart Preston