# # Author:: Marc Paradise # Copyright:: Copyright (c) Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require_relative "../../spec_helper" require "chef/secret_fetcher" require "chef/secret_fetcher/azure_key_vault" require "net/http/responses" describe Chef::SecretFetcher::AzureKeyVault do let(:config) { { vault: "my-vault" } } let(:fetcher) { Chef::SecretFetcher::AzureKeyVault.new(config, nil) } let(:secrets_response_body) { '{ "value" : "my secret value" }' } let(:secrets_response_mock) do rm = Net::HTTPSuccess.new("1.0", "400", "OK") allow(rm).to receive(:body).and_return(secrets_response_body) rm end let(:token_response_body) { %Q({"access_token":"#{access_token}","client_id":"#{client_id}","expires_in":"86294","expires_on":"1627761860","ext_expires_in":"86399","not_before":"1627675160","resource":"https://vault.azure.net","token_type":"Bearer"}) } let(:access_token) { "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Im5PbzNaRHJPRFhFSzFqS1doWHNsSFJfS1hFZyIsImtpZCI6Im5PbzNaRHJPRFhFSzFqS1doWHNsSFJfS1hFZyJ9.eyJhdWQiOiJodHRwczovL3ZhdWx0LmF6dXJlLm5ldCIsImlzcyI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0L2E5ZTY2ZDhkLTA1ZTAtNGMwMC1iOWRkLWM0Yjc3M2U5MWNhNi8iLCJpYXQiOjE2Mjc2NzUxNjAsIm5iZiI6MTYyNzY3NTE2MCwiZXhwIjoxNjI3NzYxODYwLCJhaW8iOiJFMlpnWUhCWGplaTdWS214eEh6bjdoSWpNZFlMQUE9PSIsImFwcGlkIjoiNjU2Mjc1MjEtMzYzYi00ZDk2LTkyMTctMjcIsIm9pZCI6IjNiZjI1NjVhLWY4NWQtNDBiNy1hZWJkLTNlZDA1ZDA0N2FmNiIsInJoIjoiMC5BUk1BalczbXFlQUZBRXk1M2NTM2Mta2NwaUYxWW1VN05wWk5raGNuRGpuZEwxb1RBQUEuIiwic3ViIjoiM2JmMjU2NWEtZjg1ZC00MGI3LWFlYmQtM2VkMDVkMDQ3YWY2IiwidGlkIjoiYTllNjZkOGQtMDVlMC00YzAwLWI5ZGQtYzRiNzczZTkxY2E2IiwidXRpIjoibXlzeHpSRTV3ay1ibTFlYkNqc09BQSIsInZlciI6IjEuMCIsInhtc19taXJpZCI6Ii9zdWJzY3JpcHRpb25zLzYzNDJkZDZkLTc1NTQtNDJjOS04NTM2LTdkZmU3MmY1MWZhZC9yZXNvdXJjZWdyb3Vwcy9pbWFnZS1waXBlbGluZS1ydW5uZXItcWEtZWFzdHVzMi1yZy9wcm92aWRlcnMvTWljcm9zb2Z0Lk1hbmFnZWRJZGVudGl0eS91c2VyQXNzaWduZWRJZGVudGl0aWVzL2ltYWdlLXBpcGVsaW5lLXJ1bm5lci1xYS1lYXN0dXMyLW1pIn0.BquzjN6d0g4zlvkbkdVwNEfRxIXSmxYwCHMk6UG3iza2fVioiOrcoP4Cp9P5--AB4G_CAhIXaP7YIZs3mq05QiDjSvkVAM0t67UPGhEr66sNXkV72iZBnKca_auh6EHsjPfxeVHkE1wdrsncrYdKhzgO4IAj8Jg4N5qjcE2q-OkliadmEuTwrhPhq" } let(:token_response_mock) do rm = Net::HTTPSuccess.new("1.0", "400", "OK") allow(rm).to receive(:body).and_return(token_response_body) rm end let(:client_id) { SecureRandom.uuid } let(:http_mock) { instance_double("Net::HTTP", :use_ssl= => nil) } let(:token_uri) { URI.parse("http://169.254.169.254/metadata/identity/oauth2/token") } let(:vault_name) { "my-vault" } let(:secret_name) { "my-secret" } let(:vault_secret_uri) { URI.parse("https://#{vault_name}.vault.azure.net/secrets/#{secret_name}/?api-version=7.2") } before do # Cache these up front so we can pass into allow statements without hitting: # URI received :parse with unexpected arguments token_uri vault_secret_uri end before do allow(Net::HTTP).to receive(:new).and_return(http_mock) allow(URI).to receive(:parse).with("http://169.254.169.254/metadata/identity/oauth2/token").and_return(token_uri) allow(URI).to receive(:parse).with("https://#{vault_name}.vault.azure.net/secrets/#{secret_name}/?api-version=7.2").and_return(vault_secret_uri) allow(http_mock).to receive(:get).with(token_uri, { "Metadata" => "true" }).and_return(token_response_mock) allow(http_mock).to receive(:get).with(vault_secret_uri, { "Authorization" => "Bearer #{access_token}", "Content-Type" => "application/json" }).and_return(secrets_response_mock) end describe "#validate!" do it "raises error when more than one is provided: :object_id, :client_id, :mi_res_id" do expect { Chef::SecretFetcher::AzureKeyVault.new({ object_id: "abc", client_id: "abc", mi_res_id: "abc" }, nil).validate! }.to raise_error(Chef::Exceptions::Secret::ConfigurationInvalid) expect { Chef::SecretFetcher::AzureKeyVault.new({ object_id: "abc", client_id: "abc" }, nil).validate! }.to raise_error(Chef::Exceptions::Secret::ConfigurationInvalid) expect { Chef::SecretFetcher::AzureKeyVault.new({ object_id: "abc", mi_res_id: "abc" }, nil).validate! }.to raise_error(Chef::Exceptions::Secret::ConfigurationInvalid) expect { Chef::SecretFetcher::AzureKeyVault.new({ client_id: "abc", mi_res_id: "abc" }, nil).validate! }.to raise_error(Chef::Exceptions::Secret::ConfigurationInvalid) end end describe "#fetch_token" do context "when Net::HTTPBadRequest is returned and the error description contains \"Identity not found\"" do let(:token_response_mock) { Net::HTTPBadRequest.new("1.0", "400", "Bad Request") } before do allow(fetcher).to receive(:fetch_token).and_call_original allow(token_response_mock).to receive(:body).and_return('{"error":"invalid_request","error_description":"Identity not found"}') end it "raises Chef::Exceptions::Secret::Azure::IdentityNotFound" do expect { fetcher.send(:fetch_token) }.to raise_error(Chef::Exceptions::Secret::Azure::IdentityNotFound) end end context "when :object_id is provided" do let(:object_id) { SecureRandom.uuid } let(:config) { { vault: "my-vault", object_id: object_id } } it "adds client_id to request params" do fetcher.send(:fetch_token) expect(token_uri.query).to match(/object_id=#{object_id}/) end end context "when :client_id is provided" do let(:config) { { vault: "my-vault", client_id: client_id } } it "adds client_id to request params" do fetcher.send(:fetch_token) expect(token_uri.query).to match(/client_id=#{client_id}/) end end context "when :mi_res_id is provided" do let(:mi_res_id) { SecureRandom.uuid } let(:config) { { vault: "my-vault", mi_res_id: mi_res_id } } it "adds client_id to request params" do fetcher.send(:fetch_token) expect(token_uri.query).to match(/mi_res_id=#{mi_res_id}/) end end end describe "#fetch" do context "when vault name is only provided in the secret name" do let(:secrets_response_body) { '{ "value" : "my secret value" }' } let(:config) { {} } it "fetches the value" do expect(fetcher.fetch("my-vault/my-secret")).to eq "my secret value" end end context "when 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("my-secret") }.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("my-secret")).to eq "my secret value" end end end context "when an error response is received in the response body" do let(:config) { { vault: "my-vault" } } let(:secrets_response_body) { '{ "error" : { "code" : 404, "message" : "secret not found" } }' } it "raises FetchFailed" do expect { fetcher.fetch("my-secret") }.to raise_error(Chef::Exceptions::Secret::FetchFailed) end end end end