diff options
-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 |