summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarc A. Paradise <marc.paradise@gmail.com>2021-07-27 13:56:27 -0400
committerMarc A. Paradise <marc.paradise@gmail.com>2021-07-27 16:00:55 -0400
commit84aa9bdc504026489946c3bfec8bc81e91c720f1 (patch)
tree50869b92c4b6b4e0c455209ba82745a2a297972c
parentc75e8810ae3b82745ec2000b8cba42deb02e2e46 (diff)
downloadchef-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.rb2
-rw-r--r--lib/chef/secret_fetcher/azure_key_vault.rb40
-rw-r--r--spec/unit/secret_fetcher/azure_key_vault_spec.rb37
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