armrest 0.2.1 → 0.2.3

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: 8fa3acf6609a37ab249c81102bb915108499631571af0b5884c4d66b33f28376
4
- data.tar.gz: 3691659a2058ccd4944397cad38a9c22194fdc96afb89288e6f21957aa2b2131
3
+ metadata.gz: 24b470eeaa9f31c4bcc4efa090b045b0ff58d3de74999af265a95cbe3d96d09a
4
+ data.tar.gz: 94aa0baf6c919f13788cf577440a0a071737ed85cdcfe7fec54438ab9ae36ebc
5
5
  SHA512:
6
- metadata.gz: bf768b1fb7c039488df49baa4e9d68cf6af33db34d238ec49d8063dd215b2b82d8abe8a39b8d04d3806f9c3874aa7d1623992f8e1bd689f8ccff49b2ffcfcbc0
7
- data.tar.gz: 17d2bd2bacc9ed0727cca2d9c5b870e0557c86a32aed60af2c10bc73ccb55e0832d08e7343fa6534bb4b33375f309b2ce286921d74362d0f766dec113ae71df8
6
+ metadata.gz: 140df22800c83018178cc845d41c1bfd82b6129c250a993eadeb8df749396beff3059e2f41c89f0d92ef6b265c69c47fe59c0250d94b6f8ff94eae1f33afbaf2
7
+ data.tar.gz: 825260ac4874f45aba874b55c22f93cbf7f24251abb91347e15c1c8e476a27d4885feed2975aa7630ec931f57568ae3539bad782b7d267858c8498d25995fc38
data/CHANGELOG.md CHANGED
@@ -3,6 +3,14 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  This project *loosely tries* to adhere to [Semantic Versioning](http://semver.org/), even before v1.0.
5
5
 
6
+ ## [0.2.3] - 2025-06-25
7
+ - [#9](https://github.com/boltops-tools/armrest/pull/9) Add OIDC authentication provider and update changelog for 0.2.3 release
8
+ - Add federated workload identity support via OIDC auth provider.
9
+ - Update app_credentials method to bypass app_credentials when ARM_USE_OIDC is true
10
+
11
+ ## [0.2.2] - 2023-12-31
12
+ - [#8](https://github.com/boltops-tools/armrest/pull/8) cli auth scope for vault secrets
13
+
6
14
  ## [0.2.1] - 2023-12-22
7
15
  - [#6](https://github.com/boltops-tools/armrest/pull/6) Adjust the AZ timeout
8
16
 
data/README.md CHANGED
@@ -35,19 +35,39 @@ Refer to the [boltops-tools/terraspace_plugin_azurerm](https://github.com/boltop
35
35
 
36
36
  ## Usage: CLI
37
37
 
38
- The main purpose of gem is to be a Ruby library that Terraspace can interact with. The CLI interface was only built to help quickly test the code with live resources. It's essentially a way to QA. Here are some examples:
38
+ The main purpose of gem is to be a Ruby library that Terraspace can interact with. The CLI interface was only built to help quickly test the code with live resources. It's essentially a way to QA. Here are some examples:
39
39
 
40
40
  Auth:
41
41
 
42
42
  armrest auth app
43
43
  armrest auth msi
44
44
  armrest auth cli
45
+ armrest auth oidc
45
46
 
46
- The auth chain is: app -> msi -> cli
47
+ The auth chain is: app -> msi -> cli -> oidc
47
48
 
48
- armrest auth
49
+ You can disable MSI with `ARMREST_DISABLE_MSI=1`, and you can also enable or disable OIDC explicitly using environment variables (`ARM_USE_OIDC` or `AZURE_USE_OIDC`).
49
50
 
50
- You can disable MSI with `ARMREST_DISABLE_MSI=1`.
51
+ ### OIDC Authentication
52
+
53
+ The OIDC authentication provider allows you to authenticate using OpenID Connect tokens. This is particularly useful for environments like GitHub Actions or Azure DevOps pipelines.
54
+
55
+ #### Configuration
56
+
57
+ You can configure OIDC authentication using the following environment variables:
58
+
59
+ * `ARM_OIDC_TOKEN` or `AZURE_OIDC_TOKEN`: Directly provide the OIDC token.
60
+ * `ARM_OIDC_TOKEN_FILE_PATH` or `AZURE_OIDC_TOKEN_FILE_PATH`: Path to a file containing the OIDC token.
61
+ * `ACTIONS_ID_TOKEN_REQUEST_URL` and `ACTIONS_ID_TOKEN_REQUEST_TOKEN`: GitHub Actions OIDC credentials.
62
+ * `SYSTEM_OIDCREQUESTURI` and `SYSTEM_ACCESSTOKEN`: Azure DevOps OIDC credentials.
63
+
64
+ #### Example
65
+
66
+ To use OIDC authentication, set the required environment variables and run:
67
+
68
+ armrest auth oidc
69
+
70
+ This will acquire an OIDC token and exchange it for an Azure access token.
51
71
 
52
72
  Resource Group:
53
73
 
@@ -74,4 +94,6 @@ Add to your Gemfile
74
94
 
75
95
  Gemfile
76
96
 
77
- gem "armrest"
97
+ ```ruby
98
+ gem "armrest"
99
+ ```
@@ -22,10 +22,12 @@ module Armrest::Api::Auth
22
22
  data.deep_transform_keys { |k| k.underscore } # to normalize the structure to the other classes
23
23
  end
24
24
 
25
- # Looks like az account get-access-token caches the toke in ~/.azure/accessTokens.json
26
- # and will update it only when it expires. So dont think we need to handle caching
25
+ # The az cli command itself has it's own caching. So dont think we need to handle caching
27
26
  def get_access_token
28
27
  command = "az account get-access-token -o json"
28
+ if resource.include?("vault.azure.net")
29
+ command += " --scope https://vault.azure.net/.default"
30
+ end
29
31
  logger.debug "command: #{command}"
30
32
  out = `#{command}`
31
33
  if $?.success?
@@ -0,0 +1,190 @@
1
+ module Armrest::Api::Auth
2
+ # OIDC authentication provider for Azure
3
+ class OIDC < Base
4
+ include Armrest::Logging
5
+
6
+ # Check if OIDC authentication is configured via environment variables
7
+ def self.configured?
8
+ # Check for ARM_USE_OIDC explicit flag
9
+ use_oidc = ENV['ARM_USE_OIDC'] || ENV['AZURE_USE_OIDC']
10
+ use_oidc = use_oidc.downcase if use_oidc
11
+ case use_oidc
12
+ when 'false' then return false
13
+ when 'true' then return true
14
+ when nil
15
+ return false
16
+ else
17
+ logger.warn "Unrecognized OIDC flag value: #{use_oidc}"
18
+ end
19
+ end
20
+
21
+ # Initialize with required Azure credentials
22
+ def initialize(options = {})
23
+ super
24
+ @client_id = options[:client_id] || ENV['ARM_CLIENT_ID'] || ENV['AZURE_CLIENT_ID']
25
+ @tenant_id = options[:tenant_id] || ENV['ARM_TENANT_ID'] || ENV['AZURE_TENANT_ID']
26
+ @subscription_id = options[:subscription_id] || ENV['ARM_SUBSCRIPTION_ID'] || ENV['AZURE_SUBSCRIPTION_ID']
27
+
28
+ # Service connection ID for Azure DevOps
29
+ @service_connection_id = options[:service_connection_id] ||
30
+ ENV['ARM_ADO_PIPELINE_SERVICE_CONNECTION_ID'] ||
31
+ ENV['ARM_OIDC_AZURE_SERVICE_CONNECTION_ID']
32
+
33
+ # Debug logging
34
+ logger.debug "Initialized OIDC Auth Provider with client_id: #{@client_id}, tenant_id: #{@tenant_id}"
35
+ end
36
+
37
+ # Get the authentication token
38
+ def token
39
+ @token ||= acquire_token
40
+ end
41
+
42
+ # Get the credentials
43
+ def creds
44
+ return @creds if @creds
45
+ token_info = acquire_token
46
+ @creds = {
47
+ 'access_token' => token_info['access_token'],
48
+ 'expires_on' => (Time.now.to_i + token_info['expires_in'].to_i).to_s,
49
+ 'token_type' => token_info['token_type'] || 'Bearer'
50
+ }
51
+ end
52
+
53
+ private
54
+
55
+ # Acquire token using OIDC flow
56
+ def acquire_token
57
+ # First, try to get the OIDC token from various sources
58
+ oidc_token = get_oidc_token
59
+
60
+ unless oidc_token
61
+ raise Armrest::Error, "Failed to acquire OIDC token from any source"
62
+ end
63
+
64
+ # Exchange OIDC token for an Azure access token
65
+ exchange_token_for_access_token(oidc_token)
66
+ end
67
+
68
+ # Get OIDC token from various sources
69
+ def get_oidc_token
70
+ # Try direct token
71
+ return ENV['ARM_OIDC_TOKEN'] || ENV['AZURE_OIDC_TOKEN'] if ENV['ARM_OIDC_TOKEN'] || ENV['AZURE_OIDC_TOKEN']
72
+
73
+ # Try token file
74
+ token_file = ENV['ARM_OIDC_TOKEN_FILE_PATH'] || ENV['AZURE_OIDC_TOKEN_FILE_PATH']
75
+ if token_file && File.exist?(token_file)
76
+ begin
77
+ return File.read(token_file).strip
78
+ rescue => e
79
+ logger.error "Failed to read token file: #{e.message}"
80
+ return nil
81
+ end
82
+ end
83
+
84
+ # Try GitHub Actions
85
+ if ENV['ACTIONS_ID_TOKEN_REQUEST_URL'] && ENV['ACTIONS_ID_TOKEN_REQUEST_TOKEN']
86
+ return request_github_actions_token
87
+ end
88
+
89
+ # Try custom request URL/token
90
+ if ENV['ARM_OIDC_REQUEST_URL'] && ENV['ARM_OIDC_REQUEST_TOKEN']
91
+ return request_token_from_provider(
92
+ ENV['ARM_OIDC_REQUEST_URL'],
93
+ ENV['ARM_OIDC_REQUEST_TOKEN']
94
+ )
95
+ end
96
+
97
+ # Try Azure DevOps
98
+ if ENV['SYSTEM_OIDCREQUESTURI'] && ENV['SYSTEM_ACCESSTOKEN']
99
+ return request_token_from_provider(
100
+ ENV['SYSTEM_OIDCREQUESTURI'],
101
+ ENV['SYSTEM_ACCESSTOKEN']
102
+ )
103
+ end
104
+
105
+ nil
106
+ end
107
+
108
+ # Request token from GitHub Actions
109
+ def request_github_actions_token
110
+ request_token_from_provider(
111
+ ENV['ACTIONS_ID_TOKEN_REQUEST_URL'],
112
+ ENV['ACTIONS_ID_TOKEN_REQUEST_TOKEN']
113
+ )
114
+ end
115
+
116
+ # Generic function to request a token from a provider
117
+ def request_token_from_provider(url, token)
118
+ require 'net/http'
119
+ require 'json'
120
+ require 'uri'
121
+
122
+ uri = URI.parse(url)
123
+ unless uri.scheme == 'https'
124
+ logger.error "Insecure request URL detected: #{url}"
125
+ return nil
126
+ end
127
+
128
+ request = Net::HTTP::Get.new(uri)
129
+ request['Authorization'] = "Bearer #{token}"
130
+ request['Accept'] = 'application/json'
131
+
132
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
133
+ http.request(request)
134
+ end
135
+
136
+ if response.is_a?(Net::HTTPSuccess)
137
+ json_response = JSON.parse(response.body)
138
+ return json_response['value']
139
+ else
140
+ logger.error "Failed to get OIDC token: #{response.code} - #{response.body}"
141
+ nil
142
+ end
143
+ end
144
+
145
+ # Exchange OIDC token for Azure access token
146
+ def exchange_token_for_access_token(oidc_token)
147
+ require 'net/http'
148
+ require 'json'
149
+ require 'uri'
150
+
151
+ token_endpoint = "https://login.microsoftonline.com/#{@tenant_id}/oauth2/v2.0/token"
152
+
153
+ uri = URI.parse(token_endpoint)
154
+ request = Net::HTTP::Post.new(uri)
155
+ request['Content-Type'] = 'application/x-www-form-urlencoded'
156
+
157
+ # Prepare form data
158
+ form_data = {
159
+ 'client_id' => @client_id,
160
+ 'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
161
+ 'client_assertion' => oidc_token,
162
+ 'grant_type' => 'client_credentials',
163
+ 'scope' => 'https://management.azure.com/.default'
164
+ }
165
+
166
+ # Add service connection ID for Azure DevOps if available
167
+ form_data['service_connection_id'] = @service_connection_id if @service_connection_id
168
+
169
+ request.set_form_data(form_data)
170
+
171
+ # Debug logging
172
+ logger.debug "Exchanging OIDC token for access token with endpoint: #{token_endpoint}"
173
+
174
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
175
+ http.request(request)
176
+ end
177
+
178
+ if response.is_a?(Net::HTTPSuccess)
179
+ json_response = JSON.parse(response.body)
180
+ logger.debug "Received access token (expires in #{json_response['expires_in']} seconds)"
181
+ # Return the entire JSON so the caller can read token_type, expires_in, etc.
182
+ return json_response
183
+ else
184
+ error_message = "Failed to exchange OIDC token: #{response.code} - #{response.body}"
185
+ logger.error error_message
186
+ raise Armrest::Error, error_message
187
+ end
188
+ end
189
+ end
190
+ end
data/lib/armrest/auth.rb CHANGED
@@ -2,7 +2,7 @@ module Armrest
2
2
  class Auth
3
3
  include Armrest::Logging
4
4
 
5
- def initialize(options={})
5
+ def initialize(options = {})
6
6
  @options = options
7
7
  end
8
8
 
@@ -17,24 +17,32 @@ module Armrest
17
17
  nil
18
18
  end
19
19
 
20
- private
20
+ private
21
+
21
22
  def providers
22
23
  if @options[:type]
23
24
  ["#{@options[:type]}_credentials"]
24
25
  else # full chain
25
26
  [
26
27
  :app_credentials,
28
+ :oidc_credentials,
27
29
  :msi_credentials,
28
- :cli_credentials,
30
+ :cli_credentials
29
31
  ]
30
32
  end
31
33
  end
32
34
 
33
35
  def app_credentials
34
- return unless ENV['ARM_CLIENT_ID'] || ENV['AZURE_CLIENT_ID']
36
+ return unless ENV["ARM_CLIENT_ID"] || ENV["AZURE_CLIENT_ID"]
37
+ return if %w[1 true yes].include?(ENV["ARM_USE_OIDC"])
35
38
  Armrest::Api::Auth::Login.new(@options)
36
39
  end
37
40
 
41
+ def oidc_credentials
42
+ return unless Armrest::Api::Auth::OIDC.configured?
43
+ Armrest::Api::Auth::OIDC.new(@options)
44
+ end
45
+
38
46
  def msi_credentials
39
47
  api = Armrest::Api::Auth::Metadata.new(@options)
40
48
  api if api.available?
@@ -9,7 +9,7 @@ module Armrest
9
9
  end
10
10
 
11
11
  def self.camelize_map
12
- { cli: "CLI", msi: "MSI", version: "VERSION" }
12
+ { cli: "CLI", oidc: "OIDC", msi: "MSI", version: "VERSION" }
13
13
  end
14
14
  end
15
15
 
@@ -0,0 +1,9 @@
1
+ ## Examples
2
+
3
+ armrest auth
4
+ armrest auth app
5
+ armrest auth msi
6
+ armrest auth cli
7
+ armrest auth oidc
8
+
9
+ When using "armrest auth oidc", the OIDC provider is used if configured. Otherwise, it will report "Unable to authenticate".
data/lib/armrest/cli.rb CHANGED
@@ -23,7 +23,7 @@ module Armrest
23
23
  long_desc Help.text(:storage_account)
24
24
  subcommand "storage_account", StorageAccount
25
25
 
26
- desc "auth [TYPE]", "Auth to Azure API. When type is not provided the full credentials chain is checked"
26
+ desc "auth [TYPE]", "Auth to Azure API. When TYPE is not provided, the full credentials chain is checked. Available TYPEs: app, msi, cli, oidc."
27
27
  long_desc Help.text(:auth)
28
28
  def auth(type=nil)
29
29
  Auth.new(options.merge(type: type)).run
@@ -1,3 +1,3 @@
1
1
  module Armrest
2
- VERSION = "0.2.1"
2
+ VERSION = "0.2.3"
3
3
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: armrest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tung Nguyen
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2023-12-22 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activesupport
@@ -192,7 +191,6 @@ dependencies:
192
191
  - - ">="
193
192
  - !ruby/object:Gem::Version
194
193
  version: '0'
195
- description:
196
194
  email:
197
195
  - tongueroo@gmail.com
198
196
  executables:
@@ -215,6 +213,7 @@ files:
215
213
  - lib/armrest/api/auth/cli.rb
216
214
  - lib/armrest/api/auth/login.rb
217
215
  - lib/armrest/api/auth/metadata.rb
216
+ - lib/armrest/api/auth/oidc.rb
218
217
  - lib/armrest/api/base.rb
219
218
  - lib/armrest/api/handle_response.rb
220
219
  - lib/armrest/api/main.rb
@@ -230,6 +229,7 @@ files:
230
229
  - lib/armrest/cli/blob_container.rb
231
230
  - lib/armrest/cli/blob_service.rb
232
231
  - lib/armrest/cli/help.rb
232
+ - lib/armrest/cli/help/auth.md
233
233
  - lib/armrest/cli/help/blob_service/set_properties.md
234
234
  - lib/armrest/cli/help/completion.md
235
235
  - lib/armrest/cli/help/completion_script.md
@@ -257,7 +257,6 @@ homepage: https://github.com/boltops-tools/armrest
257
257
  licenses:
258
258
  - Apache-2.0
259
259
  metadata: {}
260
- post_install_message:
261
260
  rdoc_options: []
262
261
  require_paths:
263
262
  - lib
@@ -272,8 +271,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
272
271
  - !ruby/object:Gem::Version
273
272
  version: '0'
274
273
  requirements: []
275
- rubygems_version: 3.4.20
276
- signing_key:
274
+ rubygems_version: 3.6.7
277
275
  specification_version: 4
278
276
  summary: Ruby Azure REST API Library
279
277
  test_files: