Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vault creation, list, and destruction in sparse mode #252

Merged
merged 4 commits into from
Jan 20, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions features/isvault.feature
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ Feature: determine if a data bag item is a vault
And I check if the data bag item 'test/item' is a vault
Then the exit status should be 0

Scenario: detect vault item with keys in sparse mode
Given a local mode chef repo with nodes 'one,two,three'
And I create a vault item 'test/item' with keys in sparse mode containing the JSON '{"foo": "bar"}' encrypted for 'one,two,three'
And I check if the data bag item 'test/item' is a vault
Then the exit status should be 0

Scenario: detect non-vault item (encrypted data bag)
Given a local mode chef repo with nodes 'one,two,three'
And I create an empty data bag 'test'
Expand Down
27 changes: 19 additions & 8 deletions features/step_definitions/chef-vault.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
require "json"

Given(/^I create a vault item '(.+)\/(.+)' containing the JSON '(.+)' encrypted for '(.+)'(?: with '(.+)' as admins?)?$/) do |vault, item, json, nodelist, admins|
Given(/^I create a vault item '(.+)\/(.+)'( with keys in sparse mode)? containing the JSON '(.+)' encrypted for '(.+)'(?: with '(.+)' as admins?)?$/) do |vault, item, sparse, json, nodelist, admins|
write_file "item.json", json
query = nodelist.split(/,/).map { |e| "name:#{e}" }.join(" OR ")
adminarg = admins.nil? ? "-A admin" : "-A #{admins}"
run_simple "knife vault create #{vault} #{item} -z -c knife.rb #{adminarg} -S '#{query}' -J item.json", false
sparseopt = sparse.nil? ? "" : "-K sparse"
run_simple "knife vault create #{vault} #{item} -z -c knife.rb #{adminarg} #{sparseopt} -S '#{query}' -J item.json", false
end

Given(/^I update the vault item '(.+)\/(.+)' to be encrypted for '(.+)'( with the clean option)?$/) do |vault, item, nodelist, cleanopt|
Expand Down Expand Up @@ -41,18 +42,28 @@
run_simple "knife vault show #{vault} #{item} -z -c knife.rb -u #{node} -k #{node}.pem", false
end

Then(/^the vault item '(.+)\/(.+)' should( not)? be encrypted for '(.+)'$/) do |vault, item, neg, nodelist|
Then(/^the vault item '(.+)\/(.+)' should( not)? be encrypted for '(.+)'( with keys in sparse mode)?$/) do |vault, item, neg, nodelist, sparse|
nodes = nodelist.split(/,/)
command = "knife data bag show #{vault} #{item}_keys -z -c knife.rb -F json"
run_simple(command)
output = last_command_started.stdout
data = JSON.parse(output)
nodes.each do |node|
if neg
expect(data).not_to include(node)
else
expect(data).to include(node)
if sparse
expect(data).to include("mode" => "sparse")
nodes.each do |node|
command = "knife data bag show #{vault} #{item}_key_#{node} -z -c knife.rb -F json"
run_simple(command, fail_on_error: false)
if neg
error = last_command_started.stderr
expect(error).to include("ERROR: The object you are looking for could not be found")
else
data = JSON.parse(last_command_started.stdout)
expect(data).to include("id" => "#{item}_key_#{node}")
end
end
else
expect(data).to include("mode" => "default")
nodes.each { |node| neg ? (expect(data).not_to include(node)) : (expect(data).to include(node)) }
end
end

Expand Down
10 changes: 10 additions & 0 deletions features/vault_create.feature
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ Feature: knife vault create
And 'alice' should be an admin for the vault item 'test/item'
And 'bob' should not be an admin for the vault item 'test/item'

Scenario: create vault with several admins in sparse mode
Given a local mode chef repo with nodes 'one,two' with admins 'alice,bob'
And I create a vault item 'test/item' with keys in sparse mode containing the JSON '{"foo": "bar"}' encrypted for 'one,two,three' with 'alice' as admin
Then the vault item 'test/item' should be encrypted for 'one,two' with keys in sparse mode
And the vault item 'test/item' should not be encrypted for 'three' with keys in sparse mode
And 'one,two' should be a client for the vault item 'test/item'
And 'three' should not be a client for the vault item 'test/item'
And 'alice' should be an admin for the vault item 'test/item'
And 'bob' should not be an admin for the vault item 'test/item'

Scenario: create vault with an unknown admin
Given a local mode chef repo with nodes 'one,two'
And I create a vault item 'test/item' containing the JSON '{"foo": "bar"}' encrypted for 'one,two,three' with 'alice' as admin
Expand Down
6 changes: 6 additions & 0 deletions features/vault_list.feature
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ Feature: list data bags that are vaults
And I list the vaults
Then the output should match /(?m:^test$)/

Scenario: List bags that are vaults with keys in sparse mode
Given a local mode chef repo with nodes 'one,two,three'
And I create a vault item 'test/item' with keys in sparse mode containing the JSON '{"foo": "bar"}' encrypted for 'one,two,three'
And I list the vaults
Then the output should match /(?m:^test$)/

Scenario: Skip data bags that are not vaults
Given a local mode chef repo with nodes 'one,two,three'
And I create a vault item 'test/item' containing the JSON '{"foo": "bar"}' encrypted for 'one,two,three'
Expand Down
19 changes: 14 additions & 5 deletions lib/chef-vault/item_keys.rb
Original file line number Diff line number Diff line change
Expand Up @@ -161,14 +161,23 @@ def save(item_id = @raw_data["id"])

def destroy
if Chef::Config[:solo_legacy_mode]
data_bag_path = File.join(Chef::Config[:data_bag_path],
data_bag)
data_bag_path = File.join(Chef::Config[:data_bag_path], data_bag)
data_bag_item_path = File.join(data_bag_path, @raw_data["id"])

data_bag_sparse_keys_path = File.join(data_bag_path, sparse_id("*"))
# destroy all sparse keys
FileUtils.rm(Dir.glob("#{data_bag_sparse_keys_path}.json"))
# destroy this metadata
FileUtils.rm("#{data_bag_item_path}.json")

nil
else
# destroy all sparse keys
rgx = Regexp.new("^#{sparse_id(".*")}")
items = Chef::DataBag.load(data_bag).keys.select { |item| item =~ rgx }
items.each do |id|
Chef::DataBagItem.from_hash("data_bag" => data_bag, "id" => id)
.destroy(data_bag, id)
end
# destroy this metadata
super(data_bag, id)
end
end
Expand Down Expand Up @@ -205,7 +214,7 @@ def self.load(vault, name)
# @private

def sparse_id(key, item_id = @raw_data["id"])
"#{item_id}_key_#{key}"
"#{item_id.chomp("_keys")}_key_#{key}"
end

def sparse_key(sid)
Expand Down
19 changes: 15 additions & 4 deletions lib/chef/knife/vault_base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,12 @@ def configure_chef

def bag_is_vault?(bagname)
bag = Chef::DataBag.load(bagname)
# vaults have at even number of keys >= 2
return false unless bag.keys.size >= 2 && 0 == bag.keys.size % 2
# a data bag is a vault if and only if:
# - it has at least one item with item_keys
# - every item has a matching item_keys
# - item_keys has zero or more keys in sparse mode
# vaults have a number of keys >= 2
return false unless bag.keys.size >= 2
# partition into those that end in _keys
keylike, notkeylike = split_vault_keys(bag)
# there must be an equal number of keyline and not-keylike items
Expand All @@ -63,8 +67,15 @@ def bag_is_vault?(bagname)
end

def split_vault_keys(bag)
# partition into those that end in _keys
bag.keys.partition { |k| k =~ /_keys$/ }
# get all item keys
keys = bag.keys.select { |k| k =~ /_keys$/ }
# get all sparse keys
r = Regexp.union(keys.map { |k| Regexp.new("^#{k.chomp('_keys')}_key_.*") })
sparse = bag.keys.select { |k| k =~ r }
# the rest
items = bag.keys - keys - sparse
# return item keys and items
[keys, items]
end
end
end
Expand Down
7 changes: 7 additions & 0 deletions lib/chef/knife/vault_create.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ class VaultCreate < Knife

banner "knife vault create VAULT ITEM VALUES (options)"

option :keys_mode,
:short => "-K KEYS_MODE",
:long => "--keys-mode KEYS_MODE",
:description => "Mode in which to save vault keys"

option :search,
:short => "-S SEARCH",
:long => "--search SEARCH",
Expand Down Expand Up @@ -57,6 +62,7 @@ def run
search = config[:search]
json_file = config[:json]
file = config[:file]
keys_mode = config[:keys_mode]

set_mode(config[:vault_mode])

Expand Down Expand Up @@ -91,6 +97,7 @@ def run
vault_item.clients if search
vault_item.clients(clients) if clients
vault_item.admins(admins) if admins
vault_item.keys.mode(keys_mode) if keys_mode

vault_item.save
end
Expand Down
58 changes: 53 additions & 5 deletions spec/chef-vault/item_keys_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,8 @@

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
let(:chef_key) { ChefVault::Actor.new("clients", client_name) }
before { allow(chef_key).to receive(:key) { public_key_string } }

it "should save the key data" do
keys.add(chef_key, shared_secret)
Expand All @@ -110,6 +107,29 @@
keys.mode("default")
end
end

describe "#destroy" do
let(:client_name) { "client_name" }
let(:chef_key) { ChefVault::Actor.new("clients", client_name) }
before { allow(chef_key).to receive(:key) { public_key_string } }

it "should destroy the keys" do
keys.add(chef_key, shared_secret)
keys.save("bar")
keys.destroy
expect { Chef::DataBagItem.load("foo", "bar") }.to raise_error(Net::HTTPServerException)
end

it "should destroy the keys in sparse mode" do
keys.add(chef_key, shared_secret)
keys.mode("sparse")
keys.save("bar")
keys.destroy
expect { Chef::DataBagItem.load("foo", "bar") }.to raise_error(Net::HTTPServerException)
expect { Chef::DataBagItem.load("foo", "bar_key_client_name") }.to raise_error(Net::HTTPServerException)
keys.mode("default")
end
end
end

context "when running with chef-solo" do
Expand Down Expand Up @@ -182,6 +202,34 @@
keys.mode("default")
end
end

describe "#destroy" 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 destroy the keys" do
keys.add(chef_key, shared_secret)
keys.save("bar")
keys.destroy
expect(File.exist?(File.join(data_bag_path, "foo", "bar.json"))).to be(false)
end

it "should destroy the keys in sparse mode" do
keys.add(chef_key, shared_secret)
keys.mode("sparse")
keys.save("bar")
keys.destroy
expect(File.exist?(File.join(data_bag_path, "foo", "bar.json"))).to be(false)
expect(File.exist?(File.join(data_bag_path, "foo", "bar_key_client_name.json"))).to be(false)
keys.mode("default")
end
end
end
end
end