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)