diff --git a/lib/chef-vault/item_keys.rb b/lib/chef-vault/item_keys.rb index c706d5c..6927255 100644 --- a/lib/chef-vault/item_keys.rb +++ b/lib/chef-vault/item_keys.rb @@ -28,9 +28,33 @@ def initialize(vault, name) @raw_data["admins"] = [] @raw_data["clients"] = [] @raw_data["search_query"] = [] + @raw_data["mode"] = "default" + @cache = {} # write-back cache for keys + end + + def [](key) + # return options immediately + return @raw_data[key] if %w{id admins clients search_query mode}.include?(key) + # check if the key is in the write-back cache + ckey = @cache[key] + return ckey unless ckey.nil? + # check if the key is saved in sparse mode + skey = sparse_key(sparse_id(key)) + if skey + skey[key] + else + # fallback to raw data + @raw_data[key] + end end def include?(key) + # check if the key is in the write-back cache + ckey = @cache[key] + return (ckey ? true : false) unless ckey.nil? + # check if the key is saved in sparse mode + return true unless sparse_key(sparse_id(key)).nil? + # fallback to non-sparse mode if sparse key is not found @raw_data.keys.include?(key) end @@ -40,16 +64,24 @@ def add(chef_key, data_bag_shared_secret) raise ChefVault::Exceptions::V1Format, "cannot manage a v1 vault. See UPGRADE.md for help" end - self[chef_key.name] = ChefVault::ItemKeys.encode_key(chef_key.key, data_bag_shared_secret) + @cache[chef_key.name] = ChefVault::ItemKeys.encode_key(chef_key.key, data_bag_shared_secret) @raw_data[type] << chef_key.name unless @raw_data[type].include?(chef_key.name) @raw_data[type] end def delete(chef_key) - raw_data.delete(chef_key.name) + @cache[chef_key.name] = false raw_data[chef_key.type].delete(chef_key.name) end + def mode(mode = nil) + if mode + @raw_data["mode"] = mode + else + @raw_data["mode"] + end + end + def search_query(search_query = nil) if search_query @raw_data["search_query"] = search_query @@ -67,9 +99,8 @@ def admins end def save(item_id = @raw_data["id"]) - if Chef::Config[:solo_legacy_mode] - save_solo(item_id) - else + # create data bag if not running in solo mode + unless Chef::Config[:solo_legacy_mode] begin Chef::DataBag.load(data_bag) rescue Net::HTTPServerException => http_error @@ -79,9 +110,53 @@ def save(item_id = @raw_data["id"]) chef_data_bag.create end end + end + # write cached keys to data + @cache.each do |key, val| + # delete across all modes on key deletion + if val == false + # sparse mode key deletion + if Chef::Config[:solo_legacy_mode] + delete_solo(sparse_id(key)) + else + begin + Chef::DataBagItem.from_hash("data_bag" => data_bag, + "id" => sparse_id(key)) + .destroy(data_bag, sparse_id(key)) + rescue Net::HTTPServerException => http_error + raise http_error unless http_error.response.code == "404" + end + end + # default mode key deletion + @raw_data.delete(key) + else + if @raw_data["mode"] == "sparse" + # sparse mode key creation + skey = Chef::DataBagItem.from_hash( + "data_bag" => data_bag, + "id" => sparse_id(key), + key => val + ) + if Chef::Config[:solo_legacy_mode] + save_solo(skey.id, skey.raw_data) + else + skey.save + end + else + # default mode key creation + @raw_data[key] = val + end + end + end + # save raw data + if Chef::Config[:solo_legacy_mode] + save_solo(item_id) + else super end + # clear write-back cache + @cache = {} end def destroy @@ -129,6 +204,22 @@ def self.load(vault, name) # @private + def sparse_id(key, item_id = @raw_data["id"]) + "#{item_id}_key_#{key}" + end + + def sparse_key(sid) + if Chef::Config[:solo_legacy_mode] + load_solo(sid) + else + begin + Chef::DataBagItem.load(@data_bag, sid) + rescue Net::HTTPServerException => http_error + nil if http_error.response.code == "404" + end + end + end + def self.encode_key(key_string, data_bag_shared_secret) public_key = OpenSSL::PKey::RSA.new(key_string) Base64.encode64(public_key.public_encrypt(data_bag_shared_secret)) diff --git a/lib/chef-vault/mixins.rb b/lib/chef-vault/mixins.rb index 8071115..f11e9e7 100644 --- a/lib/chef-vault/mixins.rb +++ b/lib/chef-vault/mixins.rb @@ -22,7 +22,7 @@ def find_solo_path(item_id) [data_bag_path, data_bag_item_path] end - def save_solo(item_id = @raw_data["id"]) + def save_solo(item_id = @raw_data["id"], raw_data = @raw_data) data_bag_path, data_bag_item_path = find_solo_path(item_id) FileUtils.mkdir(data_bag_path) unless File.exist?(data_bag_path) @@ -32,5 +32,15 @@ def save_solo(item_id = @raw_data["id"]) raw_data end + + def delete_solo(item_id = @raw_data["id"]) + _data_bag_path, data_bag_item_path = find_solo_path(item_id) + FileUtils.rm(data_bag_item_path) if File.exist?(data_bag_item_path) + end + + def load_solo(item_id = @raw_data["id"]) + _data_bag_path, data_bag_item_path = find_solo_path(item_id) + JSON.parse(File.read(data_bag_item_path)) if File.exist?(data_bag_item_path) + end end end diff --git a/spec/chef-vault/item_keys_spec.rb b/spec/chef-vault/item_keys_spec.rb index 45d6cff..9820064 100644 --- a/spec/chef-vault/item_keys_spec.rb +++ b/spec/chef-vault/item_keys_spec.rb @@ -2,6 +2,9 @@ describe "#new" do let(:keys) { ChefVault::ItemKeys.new("foo", "bar") } let(:shared_secret) { "super_secret" } + let(:public_key_string) do + "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyMXT9IOV9pkQsxsnhSx8\n8RX6GW3caxkjcXFfHg6E7zUVBFAsfw4B1D+eHAks3qrDB7UrUxsmCBXwU4dQHaQy\ngAn5Sv0Jc4CejDNL2EeCBLZ4TF05odHmuzyDdPkSZP6utpR7+uF7SgVQedFGySIB\nih86aM+HynhkJqgJYhoxkrdo/JcWjpk7YEmWb6p4esnvPWOpbcjIoFs4OjavWBOF\niTfpkS0SkygpLi/iQu9RQfd4hDMWCc6yh3Th/1nVMUd+xQCdUK5wxluAWSv8U0zu\nhiIlZNazpCGHp+3QdP3f6rebmQA8pRM8qT5SlOvCYPk79j+IMUVSYrR4/DTZ+VM+\naQIDAQAB\n-----END PUBLIC KEY-----\n" + end it "'foo' is assigned to @data_bag" do expect(keys.data_bag).to eq "foo" @@ -19,10 +22,7 @@ expect(keys["admins"]).to eq [] end - describe "key mgmt operations" do - let(:public_key_string) do - "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyMXT9IOV9pkQsxsnhSx8\n8RX6GW3caxkjcXFfHg6E7zUVBFAsfw4B1D+eHAks3qrDB7UrUxsmCBXwU4dQHaQy\ngAn5Sv0Jc4CejDNL2EeCBLZ4TF05odHmuzyDdPkSZP6utpR7+uF7SgVQedFGySIB\nih86aM+HynhkJqgJYhoxkrdo/JcWjpk7YEmWb6p4esnvPWOpbcjIoFs4OjavWBOF\niTfpkS0SkygpLi/iQu9RQfd4hDMWCc6yh3Th/1nVMUd+xQCdUK5wxluAWSv8U0zu\nhiIlZNazpCGHp+3QdP3f6rebmQA8pRM8qT5SlOvCYPk79j+IMUVSYrR4/DTZ+VM+\naQIDAQAB\n-----END PUBLIC KEY-----\n" - end + shared_context "key mgmt operations" do shared_examples_for "proper key management" do let(:chef_key) { ChefVault::Actor.new(type, name) } @@ -41,6 +41,7 @@ keys.add(chef_key, shared_secret) expect(keys[name]).to eq("encrypted_result") expect(keys[type].include?(name)).to eq(true) + expect(keys.include?(name)).to eq(true) end end @@ -53,6 +54,7 @@ keys.delete(chef_key) expect(keys.has_key?(chef_key.name)).to eq(false) expect(keys[type].include?(name)).to eq(false) + expect(keys.include?(name)).to eq(false) end end end @@ -75,10 +77,37 @@ before { server.start_background } after { server.stop } + include_context "key mgmt operations" + describe "#save" do + let(:client_name) { "client_name" } + let(:chef_key) { ChefVault::Actor.new("clients", client_name) } + + before do + allow(chef_key).to receive(:key) { public_key_string } + end + it "should save the key data" do + keys.add(chef_key, shared_secret) keys.save("bar") expect(Chef::DataBagItem.load("foo", "bar").to_hash).to include("id" => "bar") + expect(keys[client_name]).not_to be_empty + keys.delete(chef_key) + keys.save("bar") + expect(keys[client_name]).to be_nil + end + + it "should save the key data in sparse mode" do + keys.add(chef_key, shared_secret) + keys.mode("sparse") + keys.save("bar") + expect(Chef::DataBagItem.load("foo", "bar").to_hash).to include("id" => "bar") + expect(Chef::DataBagItem.load("foo", "bar_key_client_name").to_hash).to include("id" => "bar_key_client_name") + expect(keys[client_name]).not_to be_empty + keys.delete(chef_key) + keys.save("bar") + expect(keys[client_name]).to be_nil + keys.mode("default") end end end @@ -87,6 +116,8 @@ before { Chef::Config[:solo_legacy_mode] = true } after { Chef::Config[:solo_legacy_mode] = false } + include_context "key mgmt operations" + describe "#find_solo_path" do context "when data_bag_path is an array" do before do @@ -119,15 +150,36 @@ end describe "#save" do + let(:client_name) { "client_name" } + let(:chef_key) { ChefVault::Actor.new("clients", client_name) } let(:data_bag_path) { Dir.mktmpdir("vault_item_keys") } + before do Chef::Config[:data_bag_path] = data_bag_path + allow(chef_key).to receive(:key) { public_key_string } end it "should save the key data" do - expect(File).to receive(:exist?).with(File.join(data_bag_path, "foo")).and_call_original + keys.add(chef_key, shared_secret) keys.save("bar") expect(File.read(File.join(data_bag_path, "foo", "bar.json"))).to match(/"id":.*"bar"/) + expect(keys[client_name]).not_to be_empty + keys.delete(chef_key) + keys.save("bar") + expect(keys[client_name]).to be_nil + end + + it "should save the key data in sparse mode" do + keys.add(chef_key, shared_secret) + keys.mode("sparse") + keys.save("bar") + expect(File.read(File.join(data_bag_path, "foo", "bar.json"))).to match(/"id":.*"bar"/) + expect(File.read(File.join(data_bag_path, "foo", "bar_key_client_name.json"))).to match(/"id":.*"bar_key_client_name"/) + expect(keys[client_name]).not_to be_empty + keys.delete(chef_key) + keys.save("bar") + expect(keys[client_name]).to be_nil + keys.mode("default") end end end