diff options
author | Marc A. Paradise <marc.paradise@gmail.com> | 2021-07-27 13:56:27 -0400 |
---|---|---|
committer | Marc A. Paradise <marc.paradise@gmail.com> | 2021-07-27 16:00:55 -0400 |
commit | 84aa9bdc504026489946c3bfec8bc81e91c720f1 (patch) | |
tree | 50869b92c4b6b4e0c455209ba82745a2a297972c | |
parent | c75e8810ae3b82745ec2000b8cba42deb02e2e46 (diff) | |
download | chef-84aa9bdc504026489946c3bfec8bc81e91c720f1.tar.gz |
Allow az vault name to be included in secret name
This modifies the `:azure_key_vault` fetcher so that it's possible
to fetch a secret by embedding the vault name in the secret name
instead of providing it in configuration. This continues down the path
of making secrets accessible with less typing and sane default
expectations.
Example:
```
file "/home/ubuntu/test2" do
content secret(name: "test-chef-infra-secrets/test-secret-1", service: :azure_key_vault)
end
```
Specifying vault name via configuration is still supported, but if it is
specified in the secret name as well that will take precedence.
Signed-off-by: Marc A. Paradise <marc.paradise@gmail.com>
-rw-r--r-- | lib/chef/exceptions.rb | 2 | ||||
-rw-r--r-- | lib/chef/secret_fetcher/azure_key_vault.rb | 40 | ||||
-rw-r--r-- | spec/unit/secret_fetcher/azure_key_vault_spec.rb | 37 |
3 files changed, 53 insertions, 26 deletions
diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb index 8e9bced8bb..249a90e34b 100644 --- a/lib/chef/exceptions.rb +++ b/lib/chef/exceptions.rb @@ -308,8 +308,6 @@ class Chef super("No secret service provided. Supported services are: :#{fetcher_service_names.join(" :")}") end end - - class MissingVaultName < RuntimeError; end end # Exception class for collecting multiple failures. Used when running diff --git a/lib/chef/secret_fetcher/azure_key_vault.rb b/lib/chef/secret_fetcher/azure_key_vault.rb index 734bd94df1..1d2bc2af04 100644 --- a/lib/chef/secret_fetcher/azure_key_vault.rb +++ b/lib/chef/secret_fetcher/azure_key_vault.rb @@ -8,23 +8,30 @@ class Chef # In this initial iteration this authenticates via token obtained from the OAuth2 /token # endpoint. # - # Usage Example: + # Validation of required configuration (vault name) is not performed until + # `fetch` time, to allow for embedding the vault name in with the secret + # name, such as "my_vault/secretkey1". # - # fetcher = SecretFetcher.for_service(:azure_key_vault) + # @example + # + # fetcher = SecretFetcher.for_service(:azure_key_vault, { vault: "my_vault" }, run_context ) # fetcher.fetch("secretkey1", "v1") + # + # @example + # + # fetcher = SecretFetcher.for_service(:azure_key_vault, {}, run_context ) + # fetcher.fetch("my_vault/secretkey1", "v1") class AzureKeyVault < Base - def validate! - @vault = config[:vault] - if @vault.nil? - raise Chef::Exceptions::Secret::MissingVaultName.new("You must provide a vault name to service options as vault: 'vault_name'") - end - end def do_fetch(name, version) token = fetch_token + vault, name = resolve_vault_and_secret_name(name) + if vault.nil? + raise Chef::Exceptions::Secret::ConfigurationInvalid.new("You must provide a vault name to fetcher options as vault: 'vault_name' or in the secret name as 'vault_name/secret_name'") + end # Note that `version` is optional after the final `/`. If nil/"", the latest secret version will be fetched. - secret_uri = URI.parse("https://#{@vault}.vault.azure.net/secrets/#{name}/#{version}?api-version=7.2") + secret_uri = URI.parse("https://#{vault}.vault.azure.net/secrets/#{name}/#{version}?api-version=7.2") http = Net::HTTP.new(secret_uri.host, secret_uri.port) http.use_ssl = true @@ -41,6 +48,21 @@ class Chef end end + # Determine the vault name and secret name from the provided name. + # If it is not in the provided name in the form "vault_name/secret_name" + # it will determine the vault name from `config[:vault]`. + # @param name [String] the secret name or vault and secret name in the form "vault_name/secret_name" + # @return Array[String, String] vault and secret name respectively + def resolve_vault_and_secret_name(name) + # We support a simplified approach where the vault name is not passed i + # into configuration, but + if name.include?("/") + name.split("/", 2) + else + [config[:vault], name] + end + end + def fetch_token token_uri = URI.parse("http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fvault.azure.net") http = Net::HTTP.new(token_uri.host, token_uri.port) diff --git a/spec/unit/secret_fetcher/azure_key_vault_spec.rb b/spec/unit/secret_fetcher/azure_key_vault_spec.rb index d41973992b..d0fc2727bc 100644 --- a/spec/unit/secret_fetcher/azure_key_vault_spec.rb +++ b/spec/unit/secret_fetcher/azure_key_vault_spec.rb @@ -22,20 +22,11 @@ require "chef/secret_fetcher" require "chef/secret_fetcher/azure_key_vault" describe Chef::SecretFetcher::AzureKeyVault do - let(:config) { { vault: "myvault" } } + let(:config) { { vault: "my_vault" } } let(:fetcher) { Chef::SecretFetcher::AzureKeyVault.new(config, nil) } - context "when validating configuration and configuration is missing :vault" do - context "and configuration does not have a 'vault'" do - let(:config) { {} } - it "raises a MissingVaultError error on validate!" do - expect { fetcher.validate! }.to raise_error(Chef::Exceptions::Secret::MissingVaultName) - end - end - end - context "when performing a fetch" do - let(:body) { "" } + let(:body) { '{ "value" : "my secret value" }' } let(:response_mock) { double("response", body: body) } let(:http_mock) { double("http", :get => response_mock, :use_ssl= => nil) } @@ -44,20 +35,36 @@ describe Chef::SecretFetcher::AzureKeyVault do allow(Net::HTTP).to receive(:new).and_return(http_mock) end - context "and a valid response is received" do + context "and vault name is only provided in the secret name" do let(:body) { '{ "value" : "my secret value" }' } - it "returns the expected response" do - expect(fetcher.fetch("value")).to eq "my secret value" + let(:config) { {} } + it "fetches the value" do + expect(fetcher.fetch("my_vault/value")).to eq "my secret value" end end + context "and vault name is not provided in the secret name" do + context "and vault name is not provided in config" do + let(:config) { {} } + it "raises a ConfigurationInvalid exception" do + expect { fetcher.fetch("value") }.to raise_error(Chef::Exceptions::Secret::ConfigurationInvalid) + end + end + + context "and vault name is provided in config" do + let(:config) { { vault: "my_vault" } } + it "fetches the value" do + expect(fetcher.fetch("value")).to eq "my secret value" + end + end + end context "and an error response is received in the body" do + let(:config) { { vault: "my_vault" } } let(:body) { '{ "error" : { "code" : 404, "message" : "secret not found" } }' } it "raises FetchFailed" do expect { fetcher.fetch("value") }.to raise_error(Chef::Exceptions::Secret::FetchFailed) end end - end end |