armrest 0.2.2 → 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 +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +27 -5
- data/lib/armrest/api/auth/oidc.rb +190 -0
- data/lib/armrest/auth.rb +12 -4
- data/lib/armrest/autoloader.rb +1 -1
- data/lib/armrest/cli/help/auth.md +9 -0
- data/lib/armrest/cli.rb +1 -1
- data/lib/armrest/version.rb +1 -1
- metadata +5 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 24b470eeaa9f31c4bcc4efa090b045b0ff58d3de74999af265a95cbe3d96d09a
|
4
|
+
data.tar.gz: 94aa0baf6c919f13788cf577440a0a071737ed85cdcfe7fec54438ab9ae36ebc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 140df22800c83018178cc845d41c1bfd82b6129c250a993eadeb8df749396beff3059e2f41c89f0d92ef6b265c69c47fe59c0250d94b6f8ff94eae1f33afbaf2
|
7
|
+
data.tar.gz: 825260ac4874f45aba874b55c22f93cbf7f24251abb91347e15c1c8e476a27d4885feed2975aa7630ec931f57568ae3539bad782b7d267858c8498d25995fc38
|
data/CHANGELOG.md
CHANGED
@@ -3,6 +3,11 @@
|
|
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
|
+
|
6
11
|
## [0.2.2] - 2023-12-31
|
7
12
|
- [#8](https://github.com/boltops-tools/armrest/pull/8) cli auth scope for vault secrets
|
8
13
|
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
97
|
+
```ruby
|
98
|
+
gem "armrest"
|
99
|
+
```
|
@@ -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
|
-
|
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[
|
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?
|
data/lib/armrest/autoloader.rb
CHANGED
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
|
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
|
data/lib/armrest/version.rb
CHANGED
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.
|
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:
|
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.
|
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:
|