From 93b976dedafaf228b263dfadf61263016cd0dc29 Mon Sep 17 00:00:00 2001
From: Michael Fyffe <6224270+TraGicCode@users.noreply.github.com>
Date: Fri, 26 Jun 2026 11:08:08 -0500
Subject: [PATCH] (GH-158) Add soverign cloud support via new cloud_type param
---
README.md | 53 +++++++++++++++-
REFERENCE.md | 4 ++
.../functions/azure_key_vault/lookup.rb | 11 +++-
.../functions/azure_key_vault/secret.rb | 10 ++-
lib/puppet_x/tragiccode/azure.rb | 46 ++++++++++----
spec/functions/azure_key_vault_lookup_spec.rb | 48 +++++++++------
spec/functions/azure_key_vault_secret_spec.rb | 36 ++++++-----
spec/functions/tragic_code/azure_spec.rb | 61 ++++++++++++++++++-
8 files changed, 214 insertions(+), 55 deletions(-)
diff --git a/README.md b/README.md
index 8d973d5..7d06a30 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,7 @@
* [Manual Lookups](#manual-lookups)
1. [Configuration Options](#configuration-options)
* [API Versions](#api-versions)
+ * [cloud_type](#cloud_type)
* [confine_to_keys](#confine_to_keys)
* [key_replacement_token](#key_replacement_token)
* [strip_from_keys](#strip_from_keys)
@@ -33,7 +34,8 @@
* [Retrieving a Specific Version](#retrieving-a-specific-version)
* [Retrieving a Certificate](#retrieving-a-certificate)
1. [Reference](#reference)
-1. [Development and Contributing](#development-and-contributing)
+1. [Contributors](#contributors)
+1. [Contributing](#contributing)
## Description
@@ -215,6 +217,45 @@ The `vault_api_version` and `metadata_api_version` parameters pin the Azure APIs
* **Instance Metadata Service Versions**: [Azure documentation](https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service)
* **Vault API Versions**: Check the Azure Key Vault documentation for available versions
+### cloud_type
+
+The `cloud_type` parameter specifies which Azure cloud environment should be used when communicating with Azure Key Vault and Azure authentication services.
+
+By default, the module uses Azure's public cloud.
+
+Supported values:
+
+| Value | Description |
+|---------|-------------|
+| `AzureCloud` | Azure public cloud (default) |
+| `AzureUSGovernment` | Azure US Government cloud |
+| `AzureChinaCloud` | Azure China cloud |
+
+**Examples:**
+
+```yaml
+- name: 'Azure Key Vault Secrets'
+ lookup_key: azure_key_vault::lookup
+ options:
+ vault_name: "prod-key-vault"
+ vault_api_version: '2016-10-01'
+ metadata_api_version: '2018-04-02'
+ cloud_type: AzureUSGovernment # AzureCloud (Default when not specified), AzureChinaCloud, or AzureUSGovernment
+ # Other options omitted for brevity
+```
+
+When using the Puppet function directly:
+
+```puppet
+$secret = azure_key_vault::secret('production-vault', 'important-secret', {
+ metadata_api_version => '2018-04-02',
+ vault_api_version => '2016-10-01',
+ cloud_type => 'AzureUSGovernment',
+})
+```
+
+This setting controls the Azure Key Vault and authentication endpoints used by the module. Most users should leave this value set to the default (`AzureCloud`).
+
### confine_to_keys
By design, Hiera traverses the configured hierarchy for each key until one is found. This can result in many web requests to Azure Key Vault. The `confine_to_keys` option improves performance and prevents rate limiting (Azure Key Vault allows 2,000 lookups every 10 seconds per vault) by specifying regular expressions that determine when to query Key Vault.
@@ -449,4 +490,12 @@ See [REFERENCE.md](https://github.com/tragiccode/tragiccode-azure_key_vault/blob
-
\ No newline at end of file
+
+
+## Contributing
+
+1. Fork it ( )
+1. Create your feature branch (`git checkout -b my-new-feature`)
+1. Commit your changes (`git commit -am 'Add some feature'`)
+1. Push to the branch (`git push origin my-new-feature`)
+1. Create a new Pull Request
diff --git a/REFERENCE.md b/REFERENCE.md
index 7c18649..d952120 100644
--- a/REFERENCE.md
+++ b/REFERENCE.md
@@ -21,6 +21,7 @@ The azure_key_vault::lookup function.
vault_name => String,
vault_api_version => String,
Optional[metadata_api_version] => String,
+ Optional[cloud_type] => Enum["AzureCloud", "AzureUSGovernment", "AzureChinaCloud"],
confine_to_keys => Array[String],
Optional[strip_from_keys] => Array[String],
Optional[key_replacement_token] => String,
@@ -48,6 +49,7 @@ Struct[{
vault_name => String,
vault_api_version => String,
Optional[metadata_api_version] => String,
+ Optional[cloud_type] => Enum["AzureCloud", "AzureUSGovernment", "AzureChinaCloud"],
confine_to_keys => Array[String],
Optional[strip_from_keys] => Array[String],
Optional[key_replacement_token] => String,
@@ -74,6 +76,7 @@ Retrieves secrets from Azure's Key Vault.
#### `azure_key_vault::secret(String $vault_name, String $secret_name, Struct[{
vault_api_version => String,
Optional[metadata_api_version] => String,
+ Optional[cloud_type] => Enum["AzureCloud", "AzureUSGovernment", "AzureChinaCloud"],
Optional[service_principal_credentials] => Struct[{
tenant_id => String,
client_id => String,
@@ -106,6 +109,7 @@ Data type:
Struct[{
vault_api_version => String,
Optional[metadata_api_version] => String,
+ Optional[cloud_type] => Enum["AzureCloud", "AzureUSGovernment", "AzureChinaCloud"],
Optional[service_principal_credentials] => Struct[{
tenant_id => String,
client_id => String,
diff --git a/lib/puppet/functions/azure_key_vault/lookup.rb b/lib/puppet/functions/azure_key_vault/lookup.rb
index 31156cf..ba704eb 100644
--- a/lib/puppet/functions/azure_key_vault/lookup.rb
+++ b/lib/puppet/functions/azure_key_vault/lookup.rb
@@ -7,6 +7,7 @@
vault_name => String,
vault_api_version => String,
Optional[metadata_api_version] => String,
+ Optional[cloud_type] => Enum["AzureCloud", "AzureUSGovernment", "AzureChinaCloud"],
confine_to_keys => Array[String],
Optional[strip_from_keys] => Array[String],
Optional[key_replacement_token] => String,
@@ -22,6 +23,8 @@ def lookup_key(secret_name, options, context)
# This is a reserved key name in hiera
return context.not_found if secret_name == 'lookup_options'
+ cloud_type = options['cloud_type'] || 'AzureCloud'
+
confine_keys = options['confine_to_keys']
if confine_keys
raise ArgumentError, 'confine_to_keys must be an array' unless confine_keys.is_a?(Array)
@@ -85,11 +88,11 @@ def lookup_key(secret_name, options, context)
if service_principal_credentials
credentials = YAML.load_file(service_principal_credentials)
- access_token = TragicCode::Azure.get_access_token_service_principal(credentials)
+ access_token = TragicCode::Azure.get_access_token_service_principal(credentials, cloud_type)
elsif use_azure_arc_authentication
- access_token = TragicCode::Azure.get_access_token_azure_arc(metadata_api_version)
+ access_token = TragicCode::Azure.get_access_token_azure_arc(metadata_api_version, cloud_type)
else
- access_token = TragicCode::Azure.get_access_token(metadata_api_version)
+ access_token = TragicCode::Azure.get_access_token(metadata_api_version, cloud_type)
end
context.cache('access_token', access_token)
end
@@ -103,6 +106,7 @@ def lookup_key(secret_name, options, context)
options['vault_api_version'],
access_token,
'',
+ cloud_type,
)
break unless secret_value.nil?
end
@@ -113,6 +117,7 @@ def lookup_key(secret_name, options, context)
options['vault_api_version'],
access_token,
'',
+ cloud_type,
)
end
rescue RuntimeError => e
diff --git a/lib/puppet/functions/azure_key_vault/secret.rb b/lib/puppet/functions/azure_key_vault/secret.rb
index 8ac421c..c37a738 100644
--- a/lib/puppet/functions/azure_key_vault/secret.rb
+++ b/lib/puppet/functions/azure_key_vault/secret.rb
@@ -14,6 +14,7 @@
param 'Struct[{
vault_api_version => String,
Optional[metadata_api_version] => String,
+ Optional[cloud_type] => Enum["AzureCloud", "AzureUSGovernment", "AzureChinaCloud"],
Optional[service_principal_credentials] => Struct[{
tenant_id => String,
client_id => String,
@@ -26,10 +27,12 @@
end
def secret(cache, vault_name, secret_name, api_endpoint_hash, secret_version = '')
+ cloud_type = api_endpoint_hash['cloud_type'] || 'AzureCloud'
Puppet.debug("vault_name: #{vault_name}")
Puppet.debug("secret_name: #{secret_name}")
Puppet.debug("secret_version: #{secret_version}")
Puppet.debug("metadata_api_version: #{api_endpoint_hash['metadata_api_version']}")
+ Puppet.debug("cloud_type: #{cloud_type}")
Puppet.debug("vault_api_version: #{api_endpoint_hash['vault_api_version']}")
if api_endpoint_hash['service_principal_credentials']
partial_credentials = api_endpoint_hash['service_principal_credentials'].slice('tenant_id', 'client_id')
@@ -63,11 +66,11 @@ def secret(cache, vault_name, secret_name, api_endpoint_hash, secret_version = '
end
access_token = if service_principal_credentials
- TragicCode::Azure.get_access_token_service_principal(service_principal_credentials)
+ TragicCode::Azure.get_access_token_service_principal(service_principal_credentials, cloud_type)
elsif use_azure_arc_authentication
- TragicCode::Azure.get_access_token_azure_arc(metadata_api_version)
+ TragicCode::Azure.get_access_token_azure_arc(metadata_api_version, cloud_type)
else
- TragicCode::Azure.get_access_token(metadata_api_version)
+ TragicCode::Azure.get_access_token(metadata_api_version, cloud_type)
end
cache_hash[access_token_id] = access_token
end
@@ -78,6 +81,7 @@ def secret(cache, vault_name, secret_name, api_endpoint_hash, secret_version = '
api_endpoint_hash['vault_api_version'],
cache_hash[access_token_id],
secret_version,
+ cloud_type,
)
raise Puppet::Error, "The secret named #{secret_name} could not be found in a vault named #{vault_name}" if secret_value.nil?
diff --git a/lib/puppet_x/tragiccode/azure.rb b/lib/puppet_x/tragiccode/azure.rb
index 4dd6251..fbd1b97 100644
--- a/lib/puppet_x/tragiccode/azure.rb
+++ b/lib/puppet_x/tragiccode/azure.rb
@@ -1,18 +1,37 @@
require 'net/http'
require 'json'
+require 'uri'
module TragicCode
# Azure API functions
class Azure
@azure_arc_instance_metadata_endpoint_ip = '127.0.0.1'.freeze
+ def self.cloud_environments
+ {
+ 'AzureCloud' => {
+ 'vault_dns_suffix' => 'vault.azure.net',
+ 'login_endpoint' => 'login.microsoftonline.com',
+ },
+ 'AzureUSGovernment' => {
+ 'vault_dns_suffix' => 'vault.usgovcloudapi.net',
+ 'login_endpoint' => 'login.microsoftonline.us',
+ },
+ 'AzureChinaCloud' => {
+ 'vault_dns_suffix' => 'vault.azure.cn',
+ 'login_endpoint' => 'login.chinacloudapi.cn',
+ },
+ }
+ end
+
def self.normalize_object_name(object_name, replacement)
object_name.gsub(%r{[^0-9a-zA-Z-]}, replacement)
end
- def self.get_access_token_azure_arc(api_version)
+ def self.get_access_token_azure_arc(api_version, cloud_type = 'AzureCloud')
# Generate File and Read Challenge Token
- uri = URI("http://#{@azure_arc_instance_metadata_endpoint_ip}/metadata/identity/oauth2/token?api-version=#{api_version}&resource=https%3A%2F%2Fvault.azure.net")
+ vault_resource = "https://#{cloud_environments.fetch(cloud_type, cloud_environments['AzureCloud'])['vault_dns_suffix']}"
+ uri = URI("http://#{@azure_arc_instance_metadata_endpoint_ip}/metadata/identity/oauth2/token?api-version=#{api_version}&resource=#{URI.encode_www_form_component(vault_resource)}")
req = Net::HTTP::Get.new(uri.request_uri)
req['Metadata'] = 'true'
res = Net::HTTP.start(uri.hostname, uri.port) do |http|
@@ -27,15 +46,16 @@ def self.get_access_token_azure_arc(api_version)
challenge_token = File.read(challenge_token_file_path)
# Get Access Token using challenge token
- internal_get_access_token(api_version, @azure_arc_instance_metadata_endpoint_ip, { 'Authorization' => "Basic #{challenge_token}" })
+ internal_get_access_token(api_version, @azure_arc_instance_metadata_endpoint_ip, { 'Authorization' => "Basic #{challenge_token}" }, cloud_type)
end
- def self.get_access_token(api_version)
- internal_get_access_token(api_version, '169.254.169.254')
+ def self.get_access_token(api_version, cloud_type = 'AzureCloud')
+ internal_get_access_token(api_version, '169.254.169.254', {}, cloud_type)
end
- def self.internal_get_access_token(api_version, instance_metadata_service_endpoint = '169.254.169.254', extra_http_headers_hash = {})
- uri = URI("http://#{instance_metadata_service_endpoint}/metadata/identity/oauth2/token?api-version=#{api_version}&resource=https%3A%2F%2Fvault.azure.net")
+ def self.internal_get_access_token(api_version, instance_metadata_service_endpoint = '169.254.169.254', extra_http_headers_hash = {}, cloud_type = 'AzureCloud')
+ vault_resource = "https://#{cloud_environments.fetch(cloud_type, cloud_environments['AzureCloud'])['vault_dns_suffix']}"
+ uri = URI("http://#{instance_metadata_service_endpoint}/metadata/identity/oauth2/token?api-version=#{api_version}&resource=#{URI.encode_www_form_component(vault_resource)}")
req = Net::HTTP::Get.new(uri.request_uri)
req['Metadata'] = 'true'
extra_http_headers_hash.each do |key, value|
@@ -48,13 +68,14 @@ def self.internal_get_access_token(api_version, instance_metadata_service_endpoi
JSON.parse(res.body)['access_token']
end
- def self.get_access_token_service_principal(credentials)
- uri = URI("https://login.microsoftonline.com/#{credentials.fetch('tenant_id')}/oauth2/v2.0/token")
+ def self.get_access_token_service_principal(credentials, cloud_type = 'AzureCloud')
+ cloud_config = cloud_environments.fetch(cloud_type, cloud_environments['AzureCloud'])
+ uri = URI("https://#{cloud_config['login_endpoint']}/#{credentials.fetch('tenant_id')}/oauth2/v2.0/token")
data = {
'grant_type': 'client_credentials',
'client_id': credentials.fetch('client_id'),
'client_secret': credentials.fetch('client_secret'),
- 'scope': 'https://vault.azure.net/.default'
+ 'scope': "https://#{cloud_config['vault_dns_suffix']}/.default"
}
req = Net::HTTP::Post.new(uri.request_uri)
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
@@ -64,9 +85,10 @@ def self.get_access_token_service_principal(credentials)
JSON.parse(res.body)['access_token']
end
- def self.get_secret(vault_name, secret_name, vault_api_version, access_token, secret_version)
+ def self.get_secret(vault_name, secret_name, vault_api_version, access_token, secret_version, cloud_type = 'AzureCloud')
+ vault_dns_suffix = cloud_environments.fetch(cloud_type, cloud_environments['AzureCloud'])['vault_dns_suffix']
version_parameter = secret_version.empty? ? secret_version : "/#{secret_version}"
- uri = URI("https://#{vault_name}.vault.azure.net/secrets/#{secret_name}#{version_parameter}?api-version=#{vault_api_version}")
+ uri = URI("https://#{vault_name}.#{vault_dns_suffix}/secrets/#{secret_name}#{version_parameter}?api-version=#{vault_api_version}")
req = Net::HTTP::Get.new(uri.request_uri)
req['Authorization'] = "Bearer #{access_token}"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
diff --git a/spec/functions/azure_key_vault_lookup_spec.rb b/spec/functions/azure_key_vault_lookup_spec.rb
index c264a31..e753532 100644
--- a/spec/functions/azure_key_vault_lookup_spec.rb
+++ b/spec/functions/azure_key_vault_lookup_spec.rb
@@ -51,9 +51,9 @@
secret_value = 'secret_value'
expect(lookup_context).to receive(:cached_value).with('access_token').and_return(nil)
- expect(TragicCode::Azure).to receive(:get_access_token).and_return(access_token_value)
+ expect(TragicCode::Azure).to receive(:get_access_token).with(options['metadata_api_version'], 'AzureCloud').and_return(access_token_value)
expect(lookup_context).to receive(:cache).with('access_token', access_token_value).ordered
- expect(TragicCode::Azure).to receive(:get_secret).and_return(secret_value)
+ expect(TragicCode::Azure).to receive(:get_secret).with(options['vault_name'], anything, options['vault_api_version'], access_token_value, '', 'AzureCloud').and_return(secret_value)
expect(lookup_context).to receive(:cache).and_return(secret_value).ordered
expect(subject.execute('profile::windows::sqlserver::sensitive_azure_sql_user_password', options, lookup_context).unwrap).to eq secret_value
@@ -73,8 +73,8 @@
access_token_value = 'access_value'
secret_value = 'secret_value'
expect(TragicCode::Azure).to receive(:normalize_object_name).with(secret_name, '-')
- expect(TragicCode::Azure).to receive(:get_access_token).and_return(access_token_value)
- expect(TragicCode::Azure).to receive(:get_secret).and_return(secret_value)
+ expect(TragicCode::Azure).to receive(:get_access_token).with(options['metadata_api_version'], 'AzureCloud').and_return(access_token_value)
+ expect(TragicCode::Azure).to receive(:get_secret).with(options['vault_name'], anything, options['vault_api_version'], access_token_value, '', 'AzureCloud').and_return(secret_value)
expect(subject.execute('profile::windows::sqlserver::sensitive_azure_sql_user_password', options, lookup_context).unwrap).to eq secret_value
end
@@ -118,12 +118,18 @@
).and_raise_error(ArgumentError, %r{creating regexp failed with}i)
end
+ it 'errors when `cloud_type` is not a valid option' do
+ is_expected.to run.with_params(
+ 'profile::windows::sqlserver::sensitive_azure_sql_user_password', options.merge({ 'cloud_type' => 'invalid' }), lookup_context
+ ).and_raise_error(ArgumentError, %r{'cloud_type' expects a match for Enum\['AzureChinaCloud', 'AzureCloud', 'AzureUSGovernment'\], got 'invalid'}i)
+ end
+
# rubocop:disable RSpec/NamedSubject
it 'returns the key if regex matches confine_to_keys' do
access_token_value = 'access_value'
secret_value = 'secret_value'
- expect(TragicCode::Azure).to receive(:get_access_token).and_return(access_token_value)
- expect(TragicCode::Azure).to receive(:get_secret).and_return(secret_value)
+ expect(TragicCode::Azure).to receive(:get_access_token).with(any_args, 'AzureCloud').and_return(access_token_value)
+ expect(TragicCode::Azure).to receive(:get_secret).with(any_args, 'AzureCloud').and_return(secret_value)
expect(subject.execute('profile::windows::sqlserver::sensitive_azure_sql_user_password', options.merge({ 'confine_to_keys' => ['^.*sensitive_azure.*'] }), lookup_context).unwrap)
.to eq secret_value
@@ -135,8 +141,8 @@
secret_value = 'secret_value'
expect(lookup_context).to receive(:not_found)
- expect(TragicCode::Azure).to receive(:get_access_token).and_return(access_token_value)
- expect(TragicCode::Azure).to receive(:get_secret).and_return(secret_value)
+ expect(TragicCode::Azure).to receive(:get_access_token).with(any_args, 'AzureCloud').and_return(access_token_value)
+ expect(TragicCode::Azure).to receive(:get_secret).with(any_args, 'AzureCloud').and_return(secret_value)
is_expected.to run.with_params(
'profile::windows::sqlserver::sensitive_sql_user_password', options.merge({ 'confine_to_keys' => ['^sensitive_azure.*$'] }), lookup_context
@@ -171,6 +177,7 @@
options['vault_api_version'],
access_token_value,
'',
+ 'AzureCloud',
).and_return(test_case[:secret_value])
# rubocop:disable RSpec/NamedSubject
@@ -191,8 +198,8 @@
access_token_value = 'access_value'
expect(lookup_context).to receive(:not_found)
- expect(TragicCode::Azure).to receive(:get_access_token).and_return(access_token_value)
- expect(TragicCode::Azure).to receive(:get_secret).and_return(nil)
+ expect(TragicCode::Azure).to receive(:get_access_token).with(any_args, 'AzureCloud').and_return(access_token_value)
+ expect(TragicCode::Azure).to receive(:get_secret).with(any_args, 'AzureCloud').and_return(nil)
is_expected.to run.with_params(
'profile::windows::sqlserver::sensitive_azure_sql_user_password', options.merge({ 'confine_to_keys' => ['^.*sensitive_azure.*'] }), lookup_context
@@ -203,8 +210,8 @@
it 'returns the secret wrapped in the sensitive data type' do
access_token_value = 'access_value'
secret_value = 'secret_value'
- expect(TragicCode::Azure).to receive(:get_access_token).and_return(access_token_value)
- expect(TragicCode::Azure).to receive(:get_secret).and_return(secret_value)
+ expect(TragicCode::Azure).to receive(:get_access_token).with(any_args, 'AzureCloud').and_return(access_token_value)
+ expect(TragicCode::Azure).to receive(:get_secret).with(any_args, 'AzureCloud').and_return(secret_value)
expect(subject.execute('profile::windows::sqlserver::sensitive_azure_sql_user_password', options.merge({ 'confine_to_keys' => ['^.*sensitive_azure.*'] }), lookup_context))
.to be_an_instance_of(Puppet::Pops::Types::PSensitiveType::Sensitive)
@@ -216,8 +223,8 @@
# Arrange
access_token_value = 'access_value'
expect(lookup_context).to receive(:not_found)
- allow(TragicCode::Azure).to receive(:get_access_token).and_return(access_token_value)
- allow(TragicCode::Azure).to receive(:get_secret).and_return(nil)
+ allow(TragicCode::Azure).to receive(:get_access_token).with(any_args, 'AzureCloud').and_return(access_token_value)
+ allow(TragicCode::Azure).to receive(:get_secret).with(any_args, 'AzureCloud').and_return(nil)
# This works but not sure if it's a good practice
# expect(TragicCode::Azure).to receive(:get_secret).exactly(2).times.and_return(nil)
@@ -231,14 +238,15 @@
it 'returns the key if found using the last prefix' do
access_token_value = 'access_value'
secret_value = 'secret_value'
- allow(TragicCode::Azure).to receive(:get_access_token).and_return(access_token_value)
- allow(TragicCode::Azure).to receive(:get_secret).and_return(nil)
+ allow(TragicCode::Azure).to receive(:get_access_token).with(any_args, 'AzureCloud').and_return(access_token_value)
+ allow(TragicCode::Azure).to receive(:get_secret).with(any_args, 'AzureCloud').and_return(nil)
allow(TragicCode::Azure).to receive(:get_secret).with(
options['vault_name'],
'common--profile--windows--sqlserver--sensitive-azure-sql-user-password',
options['vault_api_version'],
access_token_value,
'',
+ 'AzureCloud',
).and_return('secret_value')
expect(subject.execute('profile::windows::sqlserver::sensitive_azure_sql_user_password', options.merge({ 'prefixes' => ['nodes--%{trusted.hostname}--', 'common--'] }), lookup_context).unwrap)
@@ -250,14 +258,15 @@
it 'returns the key if found using the first prefix' do
access_token_value = 'access_value'
secret_value = 'secret_value'
- allow(TragicCode::Azure).to receive(:get_access_token).and_return(access_token_value)
- allow(TragicCode::Azure).to receive(:get_secret).and_return(nil)
+ allow(TragicCode::Azure).to receive(:get_access_token).with(any_args, 'AzureCloud').and_return(access_token_value)
+ allow(TragicCode::Azure).to receive(:get_secret).with(any_args, 'AzureCloud').and_return(nil)
allow(TragicCode::Azure).to receive(:get_secret).with(
options['vault_name'],
'nodes--WIN-P-01-domain-com--profile--windows--sqlserver--sensitive-azure-sql-user-password',
options['vault_api_version'],
access_token_value,
'',
+ 'AzureCloud',
).and_return('secret_value')
expect(subject.execute('profile::windows::sqlserver::sensitive_azure_sql_user_password', options.merge({ 'prefixes' => ['nodes--WIN_P-01.domain.com--', 'common--'] }), lookup_context).unwrap)
@@ -271,13 +280,14 @@
it 'normalizes prefixes to prevent issues for users' do
access_token_value = 'access_value'
secret_value = 'secret_value'
- allow(TragicCode::Azure).to receive(:get_access_token).and_return(access_token_value)
+ allow(TragicCode::Azure).to receive(:get_access_token).with(any_args, 'AzureCloud').and_return(access_token_value)
expect(TragicCode::Azure).to receive(:get_secret).with(
options['vault_name'],
'nodes--WIN-P-01-domain-com--profile--windows--sqlserver--sensitive-azure-sql-user-password',
options['vault_api_version'],
access_token_value,
'',
+ 'AzureCloud',
).and_return('secret_value')
expect(subject.execute('profile::windows::sqlserver::sensitive_azure_sql_user_password', options.merge({ 'prefixes' => ['nodes--WIN_P-01.domain.com--'] }), lookup_context).unwrap)
diff --git a/spec/functions/azure_key_vault_secret_spec.rb b/spec/functions/azure_key_vault_secret_spec.rb
index 72c045b..f9f916c 100644
--- a/spec/functions/azure_key_vault_secret_spec.rb
+++ b/spec/functions/azure_key_vault_secret_spec.rb
@@ -45,12 +45,20 @@
vault_name, secret_name, bad_hash
).and_raise_error(%r{metadata_api_version and service_principal_credentials cannot be used together})
end
+
+ it 'errors when `cloud_type` is not a valid option' do
+ bad_hash = api_versions_hash
+ bad_hash.delete('metadata_api_version')
+ is_expected.to run.with_params(
+ vault_name, secret_name, api_versions_hash.merge({ 'cloud_type' => 'invalid' })
+ ).and_raise_error(ArgumentError, %r{'cloud_type' expects a match for Enum\['AzureChinaCloud', 'AzureCloud', 'AzureUSGovernment'\], got 'invalid'}i)
+ end
end
context 'when getting the latest version of a secret' do
it 'defaults to using an empty string as the latest version' do
- expect(TragicCode::Azure).to receive(:get_access_token).with(api_versions_hash['metadata_api_version']).and_return(access_token)
- expect(TragicCode::Azure).to receive(:get_secret).with(vault_name, secret_name, api_versions_hash['vault_api_version'], access_token, '').and_return(secret_value)
+ expect(TragicCode::Azure).to receive(:get_access_token).with(api_versions_hash['metadata_api_version'], 'AzureCloud').and_return(access_token)
+ expect(TragicCode::Azure).to receive(:get_secret).with(vault_name, secret_name, api_versions_hash['vault_api_version'], access_token, '', 'AzureCloud').and_return(secret_value)
is_expected.to run.with_params(vault_name, secret_name, api_versions_hash)
end
@@ -58,8 +66,8 @@
context 'when getting a specific version of a secret' do
it 'uses the secret version when retreiving the secret' do
- expect(TragicCode::Azure).to receive(:get_access_token).with(api_versions_hash['metadata_api_version']).and_return(access_token)
- expect(TragicCode::Azure).to receive(:get_secret).with(vault_name, secret_name, api_versions_hash['vault_api_version'], access_token, secret_version).and_return(secret_value)
+ expect(TragicCode::Azure).to receive(:get_access_token).with(api_versions_hash['metadata_api_version'], 'AzureCloud').and_return(access_token)
+ expect(TragicCode::Azure).to receive(:get_secret).with(vault_name, secret_name, api_versions_hash['vault_api_version'], access_token, secret_version, 'AzureCloud').and_return(secret_value)
is_expected.to run.with_params(vault_name, secret_name, api_versions_hash, secret_version)
end
@@ -67,8 +75,8 @@
context 'when getting a secret that does not exist in the vault' do
it 'throws an error' do
- expect(TragicCode::Azure).to receive(:get_access_token).with(api_versions_hash['metadata_api_version']).and_return(access_token)
- expect(TragicCode::Azure).to receive(:get_secret).with(vault_name, secret_name, api_versions_hash['vault_api_version'], access_token, secret_version).and_return(nil)
+ expect(TragicCode::Azure).to receive(:get_access_token).with(api_versions_hash['metadata_api_version'], 'AzureCloud').and_return(access_token)
+ expect(TragicCode::Azure).to receive(:get_secret).with(vault_name, secret_name, api_versions_hash['vault_api_version'], access_token, secret_version, 'AzureCloud').and_return(nil)
is_expected.to run.with_params(
vault_name, secret_name, api_versions_hash, secret_version
@@ -78,15 +86,15 @@
# rubocop:disable RSpec/NamedSubject
it 'returns the secret' do
- expect(TragicCode::Azure).to receive(:get_access_token).with(api_versions_hash['metadata_api_version']).and_return(access_token)
- expect(TragicCode::Azure).to receive(:get_secret).with(vault_name, secret_name, api_versions_hash['vault_api_version'], access_token, '').and_return(secret_value)
+ expect(TragicCode::Azure).to receive(:get_access_token).with(api_versions_hash['metadata_api_version'], 'AzureCloud').and_return(access_token)
+ expect(TragicCode::Azure).to receive(:get_secret).with(vault_name, secret_name, api_versions_hash['vault_api_version'], access_token, '', 'AzureCloud').and_return(secret_value)
expect(subject.execute(vault_name, secret_name, api_versions_hash).unwrap).to eq secret_value
end
it 'returns the secret wrapped in the sensitive data type' do
- expect(TragicCode::Azure).to receive(:get_access_token).with(api_versions_hash['metadata_api_version']).and_return(access_token)
- expect(TragicCode::Azure).to receive(:get_secret).with(vault_name, secret_name, api_versions_hash['vault_api_version'], access_token, '').and_return(secret_value)
+ expect(TragicCode::Azure).to receive(:get_access_token).with(api_versions_hash['metadata_api_version'], 'AzureCloud').and_return(access_token)
+ expect(TragicCode::Azure).to receive(:get_secret).with(vault_name, secret_name, api_versions_hash['vault_api_version'], access_token, '', 'AzureCloud').and_return(secret_value)
expect(subject.execute(vault_name, secret_name, api_versions_hash)).to be_an_instance_of(Puppet::Pops::Types::PSensitiveType::Sensitive)
end
@@ -94,8 +102,8 @@
# rubocop:disable RSpec/NamedSubject
it 'retrieves access_token from cache' do
- expect(TragicCode::Azure).to receive(:get_access_token).with(api_versions_hash['metadata_api_version']).and_return(access_token).once
- expect(TragicCode::Azure).to receive(:get_secret).with(vault_name, secret_name, api_versions_hash['vault_api_version'], access_token, '').and_return(secret_value).twice
+ expect(TragicCode::Azure).to receive(:get_access_token).with(api_versions_hash['metadata_api_version'], 'AzureCloud').and_return(access_token).once
+ expect(TragicCode::Azure).to receive(:get_secret).with(vault_name, secret_name, api_versions_hash['vault_api_version'], access_token, '', 'AzureCloud').and_return(secret_value).twice
subject.execute(vault_name, secret_name, api_versions_hash)
subject.execute(vault_name, secret_name, api_versions_hash)
@@ -115,8 +123,8 @@
end
it 'returns the secret' do
- expect(TragicCode::Azure).to receive(:get_access_token_service_principal).with(api_versions_hash['service_principal_credentials']).and_return(access_token)
- expect(TragicCode::Azure).to receive(:get_secret).with(vault_name, secret_name, api_versions_hash['vault_api_version'], access_token, '').and_return(secret_value)
+ expect(TragicCode::Azure).to receive(:get_access_token_service_principal).with(api_versions_hash['service_principal_credentials'], 'AzureCloud').and_return(access_token)
+ expect(TragicCode::Azure).to receive(:get_secret).with(vault_name, secret_name, api_versions_hash['vault_api_version'], access_token, '', 'AzureCloud').and_return(secret_value)
is_expected.to run.with_params(vault_name, secret_name, api_versions_hash).and_return(PuppetSensitive.new(secret_value))
end
diff --git a/spec/functions/tragic_code/azure_spec.rb b/spec/functions/tragic_code/azure_spec.rb
index 07faf8c..c8ecea8 100644
--- a/spec/functions/tragic_code/azure_spec.rb
+++ b/spec/functions/tragic_code/azure_spec.rb
@@ -15,9 +15,22 @@
context '.get_access_token' do
it 'returns a bearer token' do
stub_request(:get, %r{169.254.169.254})
+ .with(query: hash_including('resource' => 'https://vault.azure.net'))
.to_return(body: '{"access_token": "token"}', status: 200)
expect(described_class.get_access_token('api')).to eq('token')
end
+ it 'returns a bearer token for AzureUSGovernment' do
+ stub_request(:get, %r{169.254.169.254})
+ .with(query: hash_including('resource' => 'https://vault.usgovcloudapi.net'))
+ .to_return(body: '{"access_token": "token"}', status: 200)
+ expect(described_class.get_access_token('api', 'AzureUSGovernment')).to eq('token')
+ end
+ it 'returns a bearer token for AzureChinaCloud' do
+ stub_request(:get, %r{169.254.169.254})
+ .with(query: hash_including('resource' => 'https://vault.azure.cn'))
+ .to_return(body: '{"access_token": "token"}', status: 200)
+ expect(described_class.get_access_token('api', 'AzureChinaCloud')).to eq('token')
+ end
it 'errors when the response is not 2xx' do
stub_request(:get, %r{169.254.169.254})
.to_return(body: 'some_error', status: 400)
@@ -30,6 +43,7 @@
allow(File).to receive(:read).and_return('magical-token-from-file')
stub_request(:get, %r{127.0.0.1})
+ .with(query: hash_including('resource' => 'https://vault.azure.net'))
.to_return(
body: '{"access_token": "token"}',
status: 401,
@@ -37,12 +51,30 @@
)
stub_request(:get, %r{127.0.0.1})
- .with(headers: { 'Authorization' => 'Basic magical-token-from-file' })
+ .with(headers: { 'Authorization' => 'Basic magical-token-from-file' }, query: hash_including('resource' => 'https://vault.azure.net'))
.to_return(body: '{"access_token": "token"}', status: 200)
expect(described_class.get_access_token_azure_arc('api')).to eq('token')
end
+ it 'returns a bearer token for AzureUSGovernment' do
+ allow(File).to receive(:read).and_return('magical-token-from-file')
+
+ stub_request(:get, %r{127.0.0.1})
+ .with(query: hash_including('resource' => 'https://vault.usgovcloudapi.net'))
+ .to_return(
+ body: '{"access_token": "token"}',
+ status: 401,
+ headers: { 'Www-Authenticate' => 'Basic realm=C:\\ProgramData\\AzureConnectedMachineAgent\\Tokens\\f1da0584-97f4-42fd-a671-879ad3de86fa.key' },
+ )
+
+ stub_request(:get, %r{127.0.0.1})
+ .with(headers: { 'Authorization' => 'Basic magical-token-from-file' }, query: hash_including('resource' => 'https://vault.usgovcloudapi.net'))
+ .to_return(body: '{"access_token": "token"}', status: 200)
+
+ expect(described_class.get_access_token_azure_arc('api', 'AzureUSGovernment')).to eq('token')
+ end
+
it 'throws error with response body when response is not 401 (unauthorized) when attempting to generate secret file' do
stub_request(:get, %r{127.0.0.1})
.to_return(body: 'some_error', status: 200)
@@ -74,9 +106,22 @@
it 'returns a bearer token' do
stub_request(:post, %r{login.microsoftonline.com})
+ .with(body: hash_including('scope' => 'https://vault.azure.net/.default'))
.to_return(body: '{"access_token": "token"}', status: 200)
expect(described_class.get_access_token_service_principal(credentials)).to eq('token')
end
+ it 'returns a bearer token for AzureUSGovernment' do
+ stub_request(:post, %r{login.microsoftonline.us})
+ .with(body: hash_including('scope' => 'https://vault.usgovcloudapi.net/.default'))
+ .to_return(body: '{"access_token": "token"}', status: 200)
+ expect(described_class.get_access_token_service_principal(credentials, 'AzureUSGovernment')).to eq('token')
+ end
+ it 'returns a bearer token for AzureChinaCloud' do
+ stub_request(:post, %r{login.chinacloudapi.cn})
+ .with(body: hash_including('scope' => 'https://vault.azure.cn/.default'))
+ .to_return(body: '{"access_token": "token"}', status: 200)
+ expect(described_class.get_access_token_service_principal(credentials, 'AzureChinaCloud')).to eq('token')
+ end
it 'errors when the response is not 2xx' do
stub_request(:post, %r{login.microsoftonline.com})
.to_return(body: 'some_error', status: 400)
@@ -87,10 +132,22 @@
context '.get_secret' do
it 'returns a secret' do
vault_name = 'vault'
- stub_request(:get, %r{vault}i)
+ stub_request(:get, %r{vault.vault.azure.net}i)
.to_return(body: '{"value": "secret"}', status: 200)
expect(described_class.get_secret(vault_name, 'secret_name', 'api', 'token', '')).to eq('secret')
end
+ it 'returns a secret for AzureUSGovernment' do
+ vault_name = 'vault'
+ stub_request(:get, %r{vault.vault.usgovcloudapi.net}i)
+ .to_return(body: '{"value": "secret"}', status: 200)
+ expect(described_class.get_secret(vault_name, 'secret_name', 'api', 'token', '', 'AzureUSGovernment')).to eq('secret')
+ end
+ it 'returns a secret for AzureChinaCloud' do
+ vault_name = 'vault'
+ stub_request(:get, %r{vault.vault.azure.cn}i)
+ .to_return(body: '{"value": "secret"}', status: 200)
+ expect(described_class.get_secret(vault_name, 'secret_name', 'api', 'token', '', 'AzureChinaCloud')).to eq('secret')
+ end
it 'errors when the response is not 2xx' do
vault_name = 'vault'
stub_request(:get, %r{vault}i)