diff options
author | Tyler Ball <tyleraball@gmail.com> | 2014-09-29 09:35:50 -0700 |
---|---|---|
committer | Tyler Ball <tyleraball@gmail.com> | 2014-09-29 09:35:50 -0700 |
commit | 0819eafa6fd664815856fe0081dfcafabd59dcf6 (patch) | |
tree | b24f4c971587a438c1915114c69a86e4520ec529 | |
parent | 5a88d0ef5b64280150f89332544e55144605eeb1 (diff) | |
parent | 049672e8335a7a3190fcf3acd59d63b42f1f0ba0 (diff) | |
download | chef-0819eafa6fd664815856fe0081dfcafabd59dcf6.tar.gz |
Merge pull request #2118 from opscode/tball/encrypted-data-bag-ux
Finishing encrypted data bag UX
22 files changed, 948 insertions, 724 deletions
diff --git a/DOC_CHANGES.md b/DOC_CHANGES.md index 29fca72484..b79e06b4fc 100644 --- a/DOC_CHANGES.md +++ b/DOC_CHANGES.md @@ -90,6 +90,42 @@ DSL method `data_bag_item` now takes an optional String parameter `secret`, whic If the data bag item being fetched is encrypted and no `secret` is provided, Chef looks for a secret at `Chef::Config[:encrypted_data_bag_secret]`. If `secret` is provided, but the data bag item is not encrypted, then a regular data bag item is returned (no decryption is attempted). +### Encrypted data bag UX +The user can now provide a secret for data bags in 4 ways. They are, in order of descending preference: +1. Provide the secret on the command line of `knife data bag` and `knife bootstrap` commands with `--secret` +1. Provide the location of a file containing the secret on the command line of `knife data bag` and `knife bootstrap` commands with `--secret-file` +1. Add the secret to your workstation config with `knife[:secret] = ...` +1. Add the location of a file containing the secret to your workstation config with `knife[:secret-file] = ...` + +When adding the secret information to your workstation config, it will not be used for writeable operations unless `--encrypt` is also passed on the command line. +Data bag read-only operations (`knife data bag show` and `knife bootstrap`) do not require `--encrypt` to be passed, and will attempt to use an available secret for decryption. +Unencrypted data bags will not attempt to be unencrypted, even if a secret is provided. +Trying to view an encrypted data bag without providing a secret will issue a warning and show the encrypted contents. +Trying to edit or create an encrypted data bag without providing a secret will fail. + +Here are some example scenarios: + +``` +# Providing `knife[:secret_file] = ...` in knife.rb will create and encrypt the data bag +knife data bag create BAG_NAME ITEM_NAME --encrypt + +# The same command ran with --secret will use the command line secret instead of the knife.rb secret +knife data bag create ANOTHER_BAG ITEM_NAME --encrypt --secret 'ANOTHER_SECRET' + +# The next two commands will fail, because they are using the wrong secret +knife data bag edit BAG_NAME --secret 'ANOTHER_SECRET' +knife data bag edit ANOTHER_BAG --encrypt + +# The next command will unencrypt the data and show it using the `knife[:secret_file]` without passing the --encrypt flag +knife data bag show BAG_NAME + +# To create an unencrypted data bag, simply do not provide `--secret`, `--secret-file` or `--encrypt` +knife data bag create UNENCRYPTED_BAG + +# If a secret is available from any of the 4 possible entries, it will be copied to a bootstrapped node, even if `--encrypt` is not present +knife bootstrap FQDN +``` + ### Enhanced search functionality: result filtering #### Use in recipes `Chef::Search::Query#search` can take an optional `:filter_result` argument which returns search data in the form of the Hash specified. Suppose your data looks like diff --git a/lib/chef/dsl/data_query.rb b/lib/chef/dsl/data_query.rb index 3dafbca6bf..e36784271a 100644 --- a/lib/chef/dsl/data_query.rb +++ b/lib/chef/dsl/data_query.rb @@ -20,6 +20,7 @@ require 'chef/search/query' require 'chef/data_bag' require 'chef/data_bag_item' require 'chef/encrypted_data_bag_item' +require 'chef/encrypted_data_bag_item/check_encrypted' class Chef module DSL @@ -28,6 +29,7 @@ class Chef # Provides DSL for querying data from the chef-server via search or data # bag. module DataQuery + include Chef::EncryptedDataBagItem::CheckEncrypted def search(*args, &block) # If you pass a block, or have at least the start argument, do raw result parsing @@ -78,35 +80,6 @@ class Chef raise end - private - - # Tries to autodetect if the item's raw hash appears to be encrypted. - def encrypted?(raw_data) - data = raw_data.reject { |k, _| k == "id" } # Remove the "id" key. - # Assume hashes containing only the "id" key are not encrypted. - # Otherwise, remove the keys that don't appear to be encrypted and compare - # the result with the hash. If some entry has been removed, then some entry - # doesn't appear to be encrypted and we assume the entire hash is not encrypted. - data.empty? ? false : data.reject { |_, v| !looks_like_encrypted?(v) } == data - end - - # Checks if data looks like it has been encrypted by - # Chef::EncryptedDataBagItem::Encryptor::VersionXEncryptor. Returns - # true only when there is an exact match between the VersionXEncryptor - # keys and the hash's keys. - def looks_like_encrypted?(data) - return false unless data.is_a?(Hash) && data.has_key?("version") - case data["version"] - when 1 - Chef::EncryptedDataBagItem::Encryptor::Version1Encryptor.encryptor_keys.sort == data.keys.sort - when 2 - Chef::EncryptedDataBagItem::Encryptor::Version2Encryptor.encryptor_keys.sort == data.keys.sort - when 3 - Chef::EncryptedDataBagItem::Encryptor::Version3Encryptor.encryptor_keys.sort == data.keys.sort - else - false # version means something else... assume not encrypted. - end - end end end end diff --git a/lib/chef/encrypted_data_bag_item/check_encrypted.rb b/lib/chef/encrypted_data_bag_item/check_encrypted.rb new file mode 100644 index 0000000000..b7cb5841b3 --- /dev/null +++ b/lib/chef/encrypted_data_bag_item/check_encrypted.rb @@ -0,0 +1,56 @@ +# +# Author:: Tyler Ball (<tball@getchef.com>) +# Copyright:: Copyright (c) 2010-2014 Opscode, 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 'chef/encrypted_data_bag_item/encryptor' + +class Chef::EncryptedDataBagItem + # Common code for checking if a data bag appears encrypted + module CheckEncrypted + + # Tries to autodetect if the item's raw hash appears to be encrypted. + def encrypted?(raw_data) + data = raw_data.reject { |k, _| k == "id" } # Remove the "id" key. + # Assume hashes containing only the "id" key are not encrypted. + # Otherwise, remove the keys that don't appear to be encrypted and compare + # the result with the hash. If some entry has been removed, then some entry + # doesn't appear to be encrypted and we assume the entire hash is not encrypted. + data.empty? ? false : data.reject { |_, v| !looks_like_encrypted?(v) } == data + end + + private + + # Checks if data looks like it has been encrypted by + # Chef::EncryptedDataBagItem::Encryptor::VersionXEncryptor. Returns + # true only when there is an exact match between the VersionXEncryptor + # keys and the hash's keys. + def looks_like_encrypted?(data) + return false unless data.is_a?(Hash) && data.has_key?("version") + case data["version"] + when 1 + Chef::EncryptedDataBagItem::Encryptor::Version1Encryptor.encryptor_keys.sort == data.keys.sort + when 2 + Chef::EncryptedDataBagItem::Encryptor::Version2Encryptor.encryptor_keys.sort == data.keys.sort + when 3 + Chef::EncryptedDataBagItem::Encryptor::Version3Encryptor.encryptor_keys.sort == data.keys.sort + else + false # version means something else... assume not encrypted. + end + end + + end +end diff --git a/lib/chef/knife/bootstrap.rb b/lib/chef/knife/bootstrap.rb index 36a0fc1e47..a992cf5779 100644 --- a/lib/chef/knife/bootstrap.rb +++ b/lib/chef/knife/bootstrap.rb @@ -17,11 +17,13 @@ # require 'chef/knife' +require 'chef/knife/data_bag_secret_options' require 'erubis' class Chef class Knife class Bootstrap < Knife + include DataBagSecretOptions deps do require 'chef/knife/core/bootstrap_context' @@ -157,17 +159,6 @@ class Chef Chef::Config[:knife][:hints][name] = path ? Chef::JSONCompat.parse(::File.read(path)) : Hash.new } - option :secret, - :short => "-s SECRET", - :long => "--secret ", - :description => "The secret key to use to encrypt data bag item values", - :proc => Proc.new { |s| Chef::Config[:knife][:secret] = s } - - option :secret_file, - :long => "--secret-file SECRET_FILE", - :description => "A file containing the secret key to use to encrypt data bag item values", - :proc => Proc.new { |sf| Chef::Config[:knife][:secret_file] = sf } - option :bootstrap_url, :long => "--bootstrap-url URL", :description => "URL to a custom installation script", @@ -248,7 +239,8 @@ class Chef def render_template template_file = find_template template = IO.read(template_file).chomp - context = Knife::Core::BootstrapContext.new(config, config[:run_list], Chef::Config) + secret = encryption_secret_provided_ignore_encrypt_flag? ? read_secret : nil + context = Knife::Core::BootstrapContext.new(config, config[:run_list], Chef::Config, secret) Erubis::Eruby.new(template).evaluate(context) end diff --git a/lib/chef/knife/core/bootstrap_context.rb b/lib/chef/knife/core/bootstrap_context.rb index 03e3a42e4a..e681d7a49b 100644 --- a/lib/chef/knife/core/bootstrap_context.rb +++ b/lib/chef/knife/core/bootstrap_context.rb @@ -30,10 +30,11 @@ class Chef # class BootstrapContext - def initialize(config, run_list, chef_config) + def initialize(config, run_list, chef_config, secret) @config = config @run_list = run_list @chef_config = chef_config + @secret = secret end def bootstrap_environment @@ -45,15 +46,7 @@ class Chef end def encrypted_data_bag_secret - knife_config[:secret] || begin - secret_file_path = knife_config[:secret_file] - expanded_secret_file_path = File.expand_path(secret_file_path.to_s) - if secret_file_path && File.exist?(expanded_secret_file_path) - IO.read(expanded_secret_file_path) - else - nil - end - end + @secret end def trusted_certs diff --git a/lib/chef/knife/data_bag_create.rb b/lib/chef/knife/data_bag_create.rb index bc49c68448..f8a7619a8a 100644 --- a/lib/chef/knife/data_bag_create.rb +++ b/lib/chef/knife/data_bag_create.rb @@ -18,10 +18,12 @@ # require 'chef/knife' +require 'chef/knife/data_bag_secret_options' class Chef class Knife class DataBagCreate < Knife + include DataBagSecretOptions deps do require 'chef/data_bag' @@ -31,33 +33,6 @@ class Chef banner "knife data bag create BAG [ITEM] (options)" category "data bag" - option :secret, - :short => "-s SECRET", - :long => "--secret ", - :description => "The secret key to use to encrypt data bag item values", - :proc => Proc.new { |s| Chef::Config[:knife][:secret] = s } - - option :secret_file, - :long => "--secret-file SECRET_FILE", - :description => "A file containing the secret key to use to encrypt data bag item values", - :proc => Proc.new { |sf| Chef::Config[:knife][:secret_file] = sf } - - def read_secret - if config[:secret] - config[:secret] - else - Chef::EncryptedDataBagItem.load_secret(config[:secret_file]) - end - end - - def use_encryption - if config[:secret] && config[:secret_file] - ui.fatal("please specify only one of --secret, --secret-file") - exit(1) - end - config[:secret] || config[:secret_file] - end - def run @data_bag_name, @data_bag_item_name = @name_args @@ -87,11 +62,11 @@ class Chef if @data_bag_item_name create_object({ "id" => @data_bag_item_name }, "data_bag_item[#{@data_bag_item_name}]") do |output| item = Chef::DataBagItem.from_hash( - if use_encryption - Chef::EncryptedDataBagItem.encrypt_data_bag_item(output, read_secret) - else - output - end) + if encryption_secret_provided? + Chef::EncryptedDataBagItem.encrypt_data_bag_item(output, read_secret) + else + output + end) item.data_bag(@data_bag_name) rest.post_rest("data/#{@data_bag_name}", item) end diff --git a/lib/chef/knife/data_bag_edit.rb b/lib/chef/knife/data_bag_edit.rb index b3f53af919..6ef4b33f59 100644 --- a/lib/chef/knife/data_bag_edit.rb +++ b/lib/chef/knife/data_bag_edit.rb @@ -18,10 +18,12 @@ # require 'chef/knife' +require 'chef/knife/data_bag_secret_options' class Chef class Knife class DataBagEdit < Knife + include DataBagSecretOptions deps do require 'chef/data_bag_item' @@ -31,48 +33,17 @@ class Chef banner "knife data bag edit BAG ITEM (options)" category "data bag" - option :secret, - :short => "-s SECRET", - :long => "--secret ", - :description => "The secret key to use to encrypt data bag item values", - :proc => Proc.new { |s| Chef::Config[:knife][:secret] = s } - - option :secret_file, - :long => "--secret-file SECRET_FILE", - :description => "A file containing the secret key to use to encrypt data bag item values", - :proc => Proc.new { |sf| Chef::Config[:knife][:secret_file] = sf } - - def read_secret - if config[:secret] - config[:secret] - else - Chef::EncryptedDataBagItem.load_secret(config[:secret_file]) - end - end - - def use_encryption - if config[:secret] && config[:secret_file] - stdout.puts "please specify only one of --secret, --secret-file" - exit(1) - end - config[:secret] || config[:secret_file] - end - def load_item(bag, item_name) item = Chef::DataBagItem.load(bag, item_name) - if use_encryption - Chef::EncryptedDataBagItem.new(item, read_secret).to_hash + if encrypted?(item.raw_data) + if encryption_secret_provided_ignore_encrypt_flag? + return Chef::EncryptedDataBagItem.new(item, read_secret).to_hash, true + else + ui.fatal("You cannot edit an encrypted data bag without providing the secret.") + exit(1) + end else - item - end - end - - def edit_item(item) - output = edit_data(item) - if use_encryption - Chef::EncryptedDataBagItem.encrypt_data_bag_item(output, read_secret) - else - output + return item, false end end @@ -82,11 +53,21 @@ class Chef stdout.puts opt_parser exit 1 end - item = load_item(@name_args[0], @name_args[1]) - output = edit_item(item) - rest.put_rest("data/#{@name_args[0]}/#{@name_args[1]}", output) + + item, was_encrypted = load_item(@name_args[0], @name_args[1]) + edited_item = edit_data(item) + + if was_encrypted || encryption_secret_provided? + ui.info("Encrypting data bag using provided secret.") + item_to_save = Chef::EncryptedDataBagItem.encrypt_data_bag_item(edited_item, read_secret) + else + ui.info("Saving data bag unencrypted. To encrypt it, provide an appropriate secret.") + item_to_save = edited_item + end + + rest.put_rest("data/#{@name_args[0]}/#{@name_args[1]}", item_to_save) stdout.puts("Saved data_bag_item[#{@name_args[1]}]") - ui.output(output) if config[:print_after] + ui.output(edited_item) if config[:print_after] end end end diff --git a/lib/chef/knife/data_bag_from_file.rb b/lib/chef/knife/data_bag_from_file.rb index 2ff79b6256..d1b7daa4a2 100644 --- a/lib/chef/knife/data_bag_from_file.rb +++ b/lib/chef/knife/data_bag_from_file.rb @@ -19,10 +19,12 @@ require 'chef/knife' require 'chef/util/path_helper' +require 'chef/knife/data_bag_secret_options' class Chef class Knife class DataBagFromFile < Knife + include DataBagSecretOptions deps do require 'chef/data_bag' @@ -35,38 +37,11 @@ class Chef banner "knife data bag from file BAG FILE|FOLDER [FILE|FOLDER..] (options)" category "data bag" - option :secret, - :short => "-s SECRET", - :long => "--secret ", - :description => "The secret key to use to encrypt data bag item values", - :proc => Proc.new { |s| Chef::Config[:knife][:secret] = s } - - option :secret_file, - :long => "--secret-file SECRET_FILE", - :description => "A file containing the secret key to use to encrypt data bag item values", - :proc => Proc.new { |sf| Chef::Config[:knife][:secret_file] = sf } - option :all, :short => "-a", :long => "--all", :description => "Upload all data bags or all items for specified data bags" - def read_secret - if config[:secret] - config[:secret] - else - Chef::EncryptedDataBagItem.load_secret(config[:secret_file]) - end - end - - def use_encryption - if config[:secret] && config[:secret_file] - ui.fatal("please specify only one of --secret, --secret-file") - exit(1) - end - config[:secret] || config[:secret_file] - end - def loader @loader ||= Knife::Core::ObjectLoader.new(DataBagItem, ui) end @@ -109,9 +84,8 @@ class Chef item_paths = normalize_item_paths(items) item_paths.each do |item_path| item = loader.load_from("#{data_bags_path}", data_bag, item_path) - item = if use_encryption - secret = read_secret - Chef::EncryptedDataBagItem.encrypt_data_bag_item(item, secret) + item = if encryption_secret_provided? + Chef::EncryptedDataBagItem.encrypt_data_bag_item(item, read_secret) else item end diff --git a/lib/chef/knife/data_bag_secret_options.rb b/lib/chef/knife/data_bag_secret_options.rb new file mode 100644 index 0000000000..766006089e --- /dev/null +++ b/lib/chef/knife/data_bag_secret_options.rb @@ -0,0 +1,142 @@ +# +# Author:: Tyler Ball (<tball@opscode.com>) +# Copyright:: Copyright (c) 2014 Opscode, 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 'mixlib/cli' +require 'chef/config' +require 'chef/encrypted_data_bag_item/check_encrypted' + +class Chef + class Knife + module DataBagSecretOptions + include Mixlib::CLI + include Chef::EncryptedDataBagItem::CheckEncrypted + + # The config object is populated by knife#merge_configs with knife.rb `knife[:*]` config values, but they do + # not overwrite the command line properties. It does mean, however, that `knife[:secret]` and `--secret-file` + # passed at the same time populate both `config[:secret]` and `config[:secret_file]`. We cannot differentiate + # the valid case (`knife[:secret]` in config file and `--secret-file` on CL) and the invalid case (`--secret` + # and `--secret-file` on the CL) - thats why I'm storing the CL options in a different config key if they + # are provided. + + def self.included(base) + base.option :secret, + :short => "-s SECRET", + :long => "--secret ", + :description => "The secret key to use to encrypt data bag item values. Can also be defaulted in your config with the key 'secret'", + # Need to store value from command line in separate variable - knife#merge_configs populates same keys + # on config object from + :proc => Proc.new { |s| set_cl_secret(s) } + + base.option :secret_file, + :long => "--secret-file SECRET_FILE", + :description => "A file containing the secret key to use to encrypt data bag item values. Can also be defaulted in your config with the key 'secret_file'", + :proc => Proc.new { |sf| set_cl_secret_file(sf) } + + base.option :encrypt, + :long => "--encrypt", + :description => "If 'secret' or 'secret_file' is present in your config, then encrypt data bags using it", + :boolean => true, + :default => false + end + + def encryption_secret_provided? + base_encryption_secret_provided? + end + + def encryption_secret_provided_ignore_encrypt_flag? + base_encryption_secret_provided?(false) + end + + def read_secret + # Moving the non 'compile-time' requires into here to speed up knife command loading + # IE, if we are not running 'knife data bag *' we don't need to load 'chef/encrypted_data_bag_item' + require 'chef/encrypted_data_bag_item' + + if has_cl_secret? + config[:secret] + elsif has_cl_secret_file? + Chef::EncryptedDataBagItem.load_secret(config[:secret_file]) + elsif secret = knife_config[:secret] + secret + else + secret_file = knife_config[:secret_file] + Chef::EncryptedDataBagItem.load_secret(secret_file) + end + end + + def validate_secrets + if has_cl_secret? && has_cl_secret_file? + ui.fatal("Please specify only one of --secret, --secret-file") + exit(1) + end + + if knife_config[:secret] && knife_config[:secret_file] + ui.fatal("Please specify only one of 'secret' or 'secret_file' in your config file") + exit(1) + end + end + + private + + ## + # Determine if the user has specified an appropriate secret for encrypting data bag items. + # @returns boolean + def base_encryption_secret_provided?(need_encrypt_flag = true) + validate_secrets + + return true if has_cl_secret? || has_cl_secret_file? + + if need_encrypt_flag + if config[:encrypt] + unless knife_config[:secret] || knife_config[:secret_file] + ui.fatal("No secret or secret_file specified in config, unable to encrypt item.") + exit(1) + end + return true + end + return false + elsif knife_config[:secret] || knife_config[:secret_file] + # Certain situations (show and bootstrap) don't need a --encrypt flag to use the config file secret + return true + end + return false + end + + def has_cl_secret? + Chef::Config[:knife].has_key?(:cl_secret) + end + + def self.set_cl_secret(s) + Chef::Config[:knife][:cl_secret] = s + end + + def has_cl_secret_file? + Chef::Config[:knife].has_key?(:cl_secret_file) + end + + def self.set_cl_secret_file(sf) + Chef::Config[:knife][:cl_secret_file] = sf + end + + def knife_config + Chef::Config.key?(:knife) ? Chef::Config[:knife] : {} + end + + end + end +end diff --git a/lib/chef/knife/data_bag_show.rb b/lib/chef/knife/data_bag_show.rb index 519859ca2d..36715286e8 100644 --- a/lib/chef/knife/data_bag_show.rb +++ b/lib/chef/knife/data_bag_show.rb @@ -18,10 +18,12 @@ # require 'chef/knife' +require 'chef/knife/data_bag_secret_options' class Chef class Knife class DataBagShow < Knife + include DataBagSecretOptions deps do require 'chef/data_bag' @@ -31,45 +33,29 @@ class Chef banner "knife data bag show BAG [ITEM] (options)" category "data bag" - option :secret, - :short => "-s SECRET", - :long => "--secret ", - :description => "The secret key to use to decrypt data bag item values", - :proc => Proc.new { |s| Chef::Config[:knife][:secret] = s } - - option :secret_file, - :long => "--secret-file SECRET_FILE", - :description => "A file containing the secret key to use to decrypt data bag item values", - :proc => Proc.new { |sf| Chef::Config[:knife][:secret_file] = sf } - - def read_secret - if config[:secret] - config[:secret] - else - Chef::EncryptedDataBagItem.load_secret(config[:secret_file]) - end - end - - def use_encryption - if config[:secret] && config[:secret_file] - stdout.puts "please specify only one of --secret, --secret-file" - exit(1) - end - config[:secret] || config[:secret_file] - end - def run display = case @name_args.length - when 2 - if use_encryption + when 2 # Bag and Item names provided + secret = encryption_secret_provided_ignore_encrypt_flag? ? read_secret : nil + raw_data = Chef::DataBagItem.load(@name_args[0], @name_args[1]).raw_data + encrypted = encrypted?(raw_data) + + if encrypted && secret + # Users do not need to pass --encrypt to read data, we simply try to use the provided secret + ui.info("Encrypted data bag detected, decrypting with provided secret.") raw = Chef::EncryptedDataBagItem.load(@name_args[0], @name_args[1], - read_secret) + secret) format_for_display(raw.to_hash) + elsif encrypted && !secret + ui.warn("Encrypted data bag detected, but no secret provided for decoding. Displaying encrypted data.") + format_for_display(raw_data) else - format_for_display(Chef::DataBagItem.load(@name_args[0], @name_args[1]).raw_data) + ui.info("Unencrypted data bag detected, ignoring any provided secret options.") + format_for_display(raw_data) end - when 1 + + when 1 # Only Bag name provided format_list_for_display(Chef::DataBag.load(@name_args[0])) else stdout.puts opt_parser @@ -77,7 +63,7 @@ class Chef end output(display) end + end end end - diff --git a/spec/support/shared/matchers.rb b/spec/support/shared/matchers.rb deleted file mode 100644 index 2e1c660c19..0000000000 --- a/spec/support/shared/matchers.rb +++ /dev/null @@ -1,17 +0,0 @@ - -require 'rspec/expectations' -require 'spec/support/platform_helpers' - -RSpec::Matchers.define :match_environment_variable do |varname| - match do |actual| - expected = if windows? && ENV[varname].nil? - # On Windows, if an environment variable is not set, the command - # `echo %VARNAME%` outputs %VARNAME% - "%#{varname}%" - else - ENV[varname].to_s - end - - actual == expected - end -end diff --git a/spec/support/shared/matchers/exit_with_code.rb b/spec/support/shared/matchers/exit_with_code.rb new file mode 100644 index 0000000000..957586c85d --- /dev/null +++ b/spec/support/shared/matchers/exit_with_code.rb @@ -0,0 +1,28 @@ +require 'rspec/expectations' + +# Lifted from http://stackoverflow.com/questions/1480537/how-can-i-validate-exits-and-aborts-in-rspec +RSpec::Matchers.define :exit_with_code do |exp_code| + actual = nil + match do |block| + begin + block.call + rescue SystemExit => e + actual = e.status + end + actual and actual == exp_code + end + + failure_message_for_should do |block| + "expected block to call exit(#{exp_code}) but exit" + + (actual.nil? ? " not called" : "(#{actual}) was called") + end + + failure_message_for_should_not do |block| + "expected block not to call exit(#{exp_code})" + end + + description do + "expect block to call exit(#{exp_code})" + end + +end diff --git a/spec/support/shared/matchers/match_environment_variable.rb b/spec/support/shared/matchers/match_environment_variable.rb new file mode 100644 index 0000000000..c8c905f44a --- /dev/null +++ b/spec/support/shared/matchers/match_environment_variable.rb @@ -0,0 +1,17 @@ + +require 'rspec/expectations' +require 'spec/support/platform_helpers' + +RSpec::Matchers.define :match_environment_variable do |varname| + match do |actual| + expected = if windows? && ENV[varname].nil? + # On Windows, if an environment variable is not set, the command + # `echo %VARNAME%` outputs %VARNAME% + "%#{varname}%" + else + ENV[varname].to_s + end + + actual == expected + end +end diff --git a/spec/unit/dsl/data_query_spec.rb b/spec/unit/dsl/data_query_spec.rb index 8a985437b7..78cd5569e8 100644 --- a/spec/unit/dsl/data_query_spec.rb +++ b/spec/unit/dsl/data_query_spec.rb @@ -86,123 +86,21 @@ describe Chef::DSL::DataQuery do end context "when the item is encrypted" do - let(:default_secret) { "abc123SECRET" } - - let(:encoded_data) { Chef::EncryptedDataBagItem.encrypt_data_bag_item(raw_data, default_secret) } - - let(:item) do - item = Chef::DataBagItem.new - item.data_bag(bag_name) - item.raw_data = encoded_data - item - end + let(:secret) { "abc123SECRET" } + let(:enc_data_bag) { double("Chef::EncryptedDataBagItem") } before do allow( Chef::DataBagItem ).to receive(:load).with(bag_name, item_name).and_return(item) + expect(language).to receive(:encrypted?).and_return(true) + expect( Chef::EncryptedDataBagItem ).to receive(:load_secret).and_return(secret) end - shared_examples_for "encryption detected" do - let(:encoded_data) do - Chef::Config[:data_bag_encrypt_version] = version - Chef::EncryptedDataBagItem.encrypt_data_bag_item(raw_data, default_secret) - end - - before do - allow( Chef::EncryptedDataBagItem ).to receive(:load_secret).and_return(default_secret) - end - - it "detects encrypted data bag" do - expect( encryptor ).to receive(:encryptor_keys).at_least(:once).and_call_original - expect( Chef::Log ).to receive(:debug).with(/Data bag item looks encrypted/) - language.data_bag_item(bag_name, item_name) - end - end - - context "when encryption version is 1" do - include_examples "encryption detected" do - let(:version) { 1 } - let(:encryptor) { Chef::EncryptedDataBagItem::Encryptor::Version1Encryptor } - end - end - - context "when encryption version is 2" do - include_examples "encryption detected" do - let(:version) { 2 } - let(:encryptor) { Chef::EncryptedDataBagItem::Encryptor::Version2Encryptor } - end + it "detects encrypted data bag" do + expect( Chef::EncryptedDataBagItem ).to receive(:new).with(raw_data, secret).and_return(enc_data_bag) + expect( Chef::Log ).to receive(:debug).with(/Data bag item looks encrypted/) + expect(language.data_bag_item(bag_name, item_name)).to eq(enc_data_bag) end - context "when encryption version is 3", :ruby_20_only do - include_examples "encryption detected" do - let(:version) { 3 } - let(:encryptor) { Chef::EncryptedDataBagItem::Encryptor::Version3Encryptor } - end - end - - shared_examples_for "an encrypted data bag item" do - it "returns an encrypted data bag item" do - expect( language.data_bag_item(bag_name, item_name, secret) ).to be_a_kind_of(Chef::EncryptedDataBagItem) - end - - it "decrypts the contents of the data bag item" do - expect( language.data_bag_item(bag_name, item_name, secret).to_hash ).to eql raw_data - end - end - - context "when a secret is supplied" do - include_examples "an encrypted data bag item" do - let(:secret) { default_secret } - end - end - - context "when a secret is not supplied" do - before do - allow( Chef::Config ).to receive(:[]).and_call_original - expect( Chef::Config ).to receive(:[]).with(:encrypted_data_bag_secret).and_return(path) - expect( Chef::EncryptedDataBagItem ).to receive(:load_secret).and_call_original - end - - context "when a secret is located at Chef::Config[:encrypted_data_bag_secret]" do - let(:path) { "/tmp/my_secret" } - - before do - expect( File ).to receive(:exist?).with(path).and_return(true) - expect( IO ).to receive(:read).with(path).and_return(default_secret) - end - - include_examples "an encrypted data bag item" do - let(:secret) { nil } - end - end - - shared_examples_for "no secret file" do - it "should fail to load the data bag item" do - expect( Chef::Log ).to receive(:error).with(/Failed to load secret for encrypted data bag item/) - expect( Chef::Log ).to receive(:error).with(/Failed to load data bag item/) - expect{ language.data_bag_item(bag_name, item_name) }.to raise_error(error_type, error_message) - end - end - - context "when Chef::Config[:encrypted_data_bag_secret] is not configured" do - include_examples "no secret file" do - let(:path) { nil } - let(:error_type) { ArgumentError } - let(:error_message) { /No secret specified and no secret found/ } - end - end - - context "when Chef::Config[:encrypted_data_bag_secret] does not exist" do - include_examples "no secret file" do - before do - expect( File ).to receive(:exist?).with(path).and_return(false) - end - - let(:path) { "/tmp/my_secret" } - let(:error_type) { Errno::ENOENT } - let(:error_message) { /file not found/ } - end - end - end end end end diff --git a/spec/unit/encrypted_data_bag_item/check_encrypted_spec.rb b/spec/unit/encrypted_data_bag_item/check_encrypted_spec.rb new file mode 100644 index 0000000000..1da5efb36e --- /dev/null +++ b/spec/unit/encrypted_data_bag_item/check_encrypted_spec.rb @@ -0,0 +1,95 @@ +# +# Author:: Tyler Ball (<tball@getchef.com>) +# Copyright:: Copyright (c) 2010-2014 Opscode, 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 'spec_helper' +require 'chef/encrypted_data_bag_item/check_encrypted' + +class CheckEncryptedTester + include Chef::EncryptedDataBagItem::CheckEncrypted +end + +describe Chef::EncryptedDataBagItem::CheckEncrypted do + + let(:tester) { CheckEncryptedTester.new } + + it "detects the item is not encrypted when the data is empty" do + expect(tester.encrypted?({})).to eq(false) + end + + it "detects the item is not encrypted when the data only contains an id" do + expect(tester.encrypted?({id: "foo"})).to eq(false) + end + + context "when the item is encrypted" do + + let(:default_secret) { "abc123SECRET" } + let(:item_name) { "item_name" } + let(:raw_data) {{ + "id" => item_name, + "greeting" => "hello", + "nested" => { + "a1" => [1, 2, 3], + "a2" => { "b1" => true } + } + }} + + let(:version) { 1 } + let(:encoded_data) do + Chef::Config[:data_bag_encrypt_version] = version + Chef::EncryptedDataBagItem.encrypt_data_bag_item(raw_data, default_secret) + end + + it "does not detect encryption when the item version is unknown" do + # It shouldn't be possible for someone to normally encrypt an item with an unknown version - they would have to + # do something funky like encrypting it and then manually changing the version + modified_encoded_data = encoded_data + modified_encoded_data["greeting"]["version"] = 4 + expect(tester.encrypted?(modified_encoded_data)).to eq(false) + end + + shared_examples_for "encryption detected" do + it "detects encrypted data bag" do + expect( encryptor ).to receive(:encryptor_keys).at_least(:once).and_call_original + expect(tester.encrypted?(encoded_data)).to eq(true) + end + end + + context "when encryption version is 1" do + include_examples "encryption detected" do + let(:version) { 1 } + let(:encryptor) { Chef::EncryptedDataBagItem::Encryptor::Version1Encryptor } + end + end + + context "when encryption version is 2" do + include_examples "encryption detected" do + let(:version) { 2 } + let(:encryptor) { Chef::EncryptedDataBagItem::Encryptor::Version2Encryptor } + end + end + + context "when encryption version is 3", :ruby_20_only do + include_examples "encryption detected" do + let(:version) { 3 } + let(:encryptor) { Chef::EncryptedDataBagItem::Encryptor::Version3Encryptor } + end + end + + end + +end diff --git a/spec/unit/knife/bootstrap_spec.rb b/spec/unit/knife/bootstrap_spec.rb index a1c5d168dd..d5c668753e 100644 --- a/spec/unit/knife/bootstrap_spec.rb +++ b/spec/unit/knife/bootstrap_spec.rb @@ -30,6 +30,7 @@ describe Chef::Knife::Bootstrap do k.merge_configs k.ui.stub(:stderr).and_return(stderr) + allow(k).to receive(:encryption_secret_provided_ignore_encrypt_flag?).and_return(false) k end @@ -221,10 +222,6 @@ describe Chef::Knife::Bootstrap do k end - # Include a data bag secret in the options to prevent Bootstrap from - # attempting to access /etc/chef/encrypted_data_bag_secret, which - # can fail when the file exists but can't be accessed by the user - # running the tests. let(:options){ ["--bootstrap-no-proxy", setting, "-s", "foo"] } let(:template_file) { File.expand_path(File.join(CHEF_SPEC_DATA, "bootstrap", "no_proxy.erb")) } let(:rendered_template) do @@ -290,7 +287,6 @@ describe Chef::Knife::Bootstrap do describe "specifying the encrypted data bag secret key" do let(:secret) { "supersekret" } - let(:secret_file) { File.join(CHEF_SPEC_DATA, 'bootstrap', 'encrypted_data_bag_secret') } let(:options) { [] } let(:bootstrap_template) { File.expand_path(File.join(CHEF_SPEC_DATA, "bootstrap", "secret.erb")) } let(:rendered_template) do @@ -299,60 +295,18 @@ describe Chef::Knife::Bootstrap do knife.render_template end - context "via --secret" do - let(:options){ ["--secret", secret] } - - it "creates a secret file" do - rendered_template.should match(%r{#{secret}}) - end - - it "renders the client.rb with an encrypted_data_bag_secret entry" do - rendered_template.should match(%r{encrypted_data_bag_secret\s*"/etc/chef/encrypted_data_bag_secret"}) - end - end - - context "via --secret-file" do - let(:options) { ["--secret-file", secret_file] } - let(:secret) { IO.read(secret_file) } - - it "creates a secret file" do - rendered_template.should match(%r{#{secret}}) - end - - it "renders the client.rb with an encrypted_data_bag_secret entry" do - rendered_template.should match(%r{encrypted_data_bag_secret\s*"/etc/chef/encrypted_data_bag_secret"}) - end + it "creates a secret file" do + expect(knife).to receive(:encryption_secret_provided_ignore_encrypt_flag?).and_return(true) + expect(knife).to receive(:read_secret).and_return(secret) + rendered_template.should match(%r{#{secret}}) end - context "secret via config" do - before do - Chef::Config[:knife][:secret] = secret - end - - it "creates a secret file" do - rendered_template.should match(%r{#{secret}}) - end - - it "renders the client.rb with an encrypted_data_bag_secret entry" do - rendered_template.should match(%r{encrypted_data_bag_secret\s*"/etc/chef/encrypted_data_bag_secret"}) - end + it "renders the client.rb with an encrypted_data_bag_secret entry" do + expect(knife).to receive(:encryption_secret_provided_ignore_encrypt_flag?).and_return(true) + expect(knife).to receive(:read_secret).and_return(secret) + rendered_template.should match(%r{encrypted_data_bag_secret\s*"/etc/chef/encrypted_data_bag_secret"}) end - context "secret-file via config" do - let(:secret) { IO.read(secret_file) } - - before do - Chef::Config[:knife][:secret_file] = secret_file - end - - it "creates a secret file" do - rendered_template.should match(%r{#{secret}}) - end - - it "renders the client.rb with an encrypted_data_bag_secret entry" do - rendered_template.should match(%r{encrypted_data_bag_secret\s*"/etc/chef/encrypted_data_bag_secret"}) - end - end end describe "when transferring trusted certificates" do diff --git a/spec/unit/knife/core/bootstrap_context_spec.rb b/spec/unit/knife/core/bootstrap_context_spec.rb index 6427071a6b..266991a7dd 100644 --- a/spec/unit/knife/core/bootstrap_context_spec.rb +++ b/spec/unit/knife/core/bootstrap_context_spec.rb @@ -29,9 +29,10 @@ describe Chef::Knife::Core::BootstrapContext do :validation_client_name => 'chef-validator-testing' } end - let(:secret_file) { File.join(CHEF_SPEC_DATA, 'bootstrap', 'encrypted_data_bag_secret') } - subject(:bootstrap_context) { described_class.new(config, run_list, chef_config) } + let(:secret) { nil } + + subject(:bootstrap_context) { described_class.new(config, run_list, chef_config, secret) } it "runs chef with the first-boot.json in the _default environment" do bootstrap_context.start_chef.should eq "chef-client -j /etc/chef/first-boot.json -E _default" @@ -105,39 +106,9 @@ EXPECTED end describe "when an encrypted_data_bag_secret is provided" do - context "via config[:secret]" do - let(:chef_config) do - { - :knife => {:secret => "supersekret" } - } - end - it "reads the encrypted_data_bag_secret" do - bootstrap_context.encrypted_data_bag_secret.should eq "supersekret" - end - end - - context "via config[:secret_file]" do - let(:chef_config) do - { - :knife => {:secret_file => secret_file} - } - end - it "reads the encrypted_data_bag_secret" do - bootstrap_context.encrypted_data_bag_secret.should eq IO.read(secret_file) - end - end - - context "via config[:secret_file] with short home path" do - let(:chef_config) do - home_path = File.expand_path("~") - shorted_secret_file_path = secret_file.gsub(home_path, "~") - { - :knife => {:secret_file => shorted_secret_file_path} - } - end - it "reads the encrypted_data_bag_secret" do - bootstrap_context.encrypted_data_bag_secret.should eq IO.read(secret_file) - end + let(:secret) { "supersekret" } + it "reads the encrypted_data_bag_secret" do + bootstrap_context.encrypted_data_bag_secret.should eq "supersekret" end end diff --git a/spec/unit/knife/data_bag_create_spec.rb b/spec/unit/knife/data_bag_create_spec.rb index 984be8e58a..c31c88577d 100644 --- a/spec/unit/knife/data_bag_create_spec.rb +++ b/spec/unit/knife/data_bag_create_spec.rb @@ -20,97 +20,89 @@ require 'spec_helper' require 'tempfile' -module ChefSpecs - class ChefRest - attr_reader :args_received - def initialize - @args_received = [] - end - - def post_rest(*args) - @args_received << args - end +describe Chef::Knife::DataBagCreate do + let(:knife) do + k = Chef::Knife::DataBagCreate.new + allow(k).to receive(:rest).and_return(rest) + allow(k.ui).to receive(:stdout).and_return(stdout) + k end -end + let(:rest) { double("Chef::REST") } + let(:stdout) { StringIO.new } -describe Chef::Knife::DataBagCreate do - before do - Chef::Config[:node_name] = "webmonkey.example.com" - @knife = Chef::Knife::DataBagCreate.new - @rest = ChefSpecs::ChefRest.new - @knife.stub(:rest).and_return(@rest) - @stdout = StringIO.new - @knife.ui.stub(:stdout).and_return(@stdout) - end + let(:bag_name) { "sudoing_admins" } + let(:item_name) { "ME" } + + let(:secret) { "abc123SECRET" } + let(:raw_hash) {{ "login_name" => "alphaomega", "id" => item_name }} - it "creates a data bag when given one argument" do - @knife.name_args = ['sudoing_admins'] - @rest.should_receive(:post_rest).with("data", {"name" => "sudoing_admins"}) - @knife.ui.should_receive(:info).with("Created data_bag[sudoing_admins]") + let(:config) { {} } - @knife.run + before do + Chef::Config[:node_name] = "webmonkey.example.com" + knife.name_args = [bag_name, item_name] + allow(knife).to receive(:config).and_return(config) end it "tries to create a data bag with an invalid name when given one argument" do - @knife.name_args = ['invalid&char'] - @knife.should_receive(:exit).with(1) - - @knife.run + knife.name_args = ['invalid&char'] + expect(Chef::DataBag).to receive(:validate_name!).with(knife.name_args[0]).and_raise(Chef::Exceptions::InvalidDataBagName) + expect {knife.run}.to exit_with_code(1) end - it "creates a data bag item when given two arguments" do - @knife.name_args = ['sudoing_admins', 'ME'] - user_supplied_hash = {"login_name" => "alphaomega", "id" => "ME"} - data_bag_item = Chef::DataBagItem.from_hash(user_supplied_hash) - data_bag_item.data_bag("sudoing_admins") - @knife.should_receive(:create_object).and_yield(user_supplied_hash) - @rest.should_receive(:post_rest).with("data", {'name' => 'sudoing_admins'}).ordered - @rest.should_receive(:post_rest).with("data/sudoing_admins", data_bag_item).ordered + context "when given one argument" do + before do + knife.name_args = [bag_name] + end + + it "creates a data bag" do + expect(rest).to receive(:post_rest).with("data", {"name" => bag_name}) + expect(knife.ui).to receive(:info).with("Created data_bag[#{bag_name}]") - @knife.run + knife.run + end end - describe "encrypted data bag items" do - before(:each) do - @secret = "abc123SECRET" - @plain_data = {"login_name" => "alphaomega", "id" => "ME"} - @enc_data = Chef::EncryptedDataBagItem.encrypt_data_bag_item(@plain_data, - @secret) - @knife.name_args = ['sudoing_admins', 'ME'] - @knife.should_receive(:create_object).and_yield(@plain_data) - data_bag_item = Chef::DataBagItem.from_hash(@enc_data) - data_bag_item.data_bag("sudoing_admins") - - # Random IV is used each time the data bag item is encrypted, so values - # will not be equal if we re-encrypt. - Chef::EncryptedDataBagItem.should_receive(:encrypt_data_bag_item).and_return(@enc_data) - - @rest.should_receive(:post_rest).with("data", {'name' => 'sudoing_admins'}).ordered - @rest.should_receive(:post_rest).with("data/sudoing_admins", data_bag_item).ordered - - @secret_file = Tempfile.new("encrypted_data_bag_secret_file_test") - @secret_file.puts(@secret) - @secret_file.flush + context "no secret is specified for encryption" do + let(:item) do + item = Chef::DataBagItem.from_hash(raw_hash) + item.data_bag(bag_name) + item end - after do - @secret_file.close - @secret_file.unlink + it "creates a data bag item" do + expect(knife).to receive(:create_object).and_yield(raw_hash) + expect(knife).to receive(:encryption_secret_provided?).and_return(false) + expect(rest).to receive(:post_rest).with("data", {'name' => bag_name}).ordered + expect(rest).to receive(:post_rest).with("data/#{bag_name}", item).ordered + + knife.run end + end + + context "a secret is specified for encryption" do + let(:encoded_data) { Chef::EncryptedDataBagItem.encrypt_data_bag_item(raw_hash, secret) } - it "creates an encrypted data bag item via --secret" do - @knife.stub(:config).and_return({:secret => @secret}) - @knife.run + let(:item) do + item = Chef::DataBagItem.from_hash(encoded_data) + item.data_bag(bag_name) + item end - it "creates an encrypted data bag item via --secret_file" do - secret_file = Tempfile.new("encrypted_data_bag_secret_file_test") - secret_file.puts(@secret) - secret_file.flush - @knife.stub(:config).and_return({:secret_file => secret_file.path}) - @knife.run + it "creates an encrypted data bag item" do + expect(knife).to receive(:create_object).and_yield(raw_hash) + expect(knife).to receive(:encryption_secret_provided?).and_return(true) + expect(knife).to receive(:read_secret).and_return(secret) + expect(Chef::EncryptedDataBagItem) + .to receive(:encrypt_data_bag_item) + .with(raw_hash, secret) + .and_return(encoded_data) + expect(rest).to receive(:post_rest).with("data", {"name" => bag_name}).ordered + expect(rest).to receive(:post_rest).with("data/#{bag_name}", item).ordered + + knife.run end end diff --git a/spec/unit/knife/data_bag_edit_spec.rb b/spec/unit/knife/data_bag_edit_spec.rb index 866ca99174..6f19b5e63e 100644 --- a/spec/unit/knife/data_bag_edit_spec.rb +++ b/spec/unit/knife/data_bag_edit_spec.rb @@ -21,73 +21,107 @@ require 'tempfile' describe Chef::Knife::DataBagEdit do before do - @plain_data = {"login_name" => "alphaomega", "id" => "item_name"} - @edited_data = { - "login_name" => "rho", "id" => "item_name", - "new_key" => "new_value" } + Chef::Config[:node_name] = "webmonkey.example.com" + knife.name_args = [bag_name, item_name] + allow(knife).to receive(:config).and_return(config) + end + + let(:knife) do + k = Chef::Knife::DataBagEdit.new + allow(k).to receive(:rest).and_return(rest) + allow(k.ui).to receive(:stdout).and_return(stdout) + k + end + + let(:raw_hash) { {"login_name" => "alphaomega", "id" => "item_name"} } + let(:db) { Chef::DataBagItem.from_hash(raw_hash)} + let(:raw_edited_hash) { {"login_name" => "rho", "id" => "item_name", "new_key" => "new_value"} } + + let(:rest) { double("Chef::REST") } + let(:stdout) { StringIO.new } - Chef::Config[:node_name] = "webmonkey.example.com" + let(:bag_name) { "sudoing_admins" } + let(:item_name) { "ME" } - @knife = Chef::Knife::DataBagEdit.new - @rest = double('chef-rest-mock') - @knife.stub(:rest).and_return(@rest) + let(:secret) { "abc123SECRET" } - @stdout = StringIO.new - @knife.stub(:stdout).and_return(@stdout) - @log = Chef::Log - @knife.name_args = ['bag_name', 'item_name'] + let(:config) { {} } + + let(:is_encrypted?) { false } + let(:transmitted_hash) { raw_edited_hash } + let(:data_to_edit) { db } + + shared_examples_for "editing a data bag" do + it "correctly edits then uploads the data bag" do + expect(Chef::DataBagItem).to receive(:load).with(bag_name, item_name).and_return(db) + expect(knife).to receive(:encrypted?).with(db.raw_data).and_return(is_encrypted?) + expect(knife).to receive(:edit_data).with(data_to_edit).and_return(raw_edited_hash) + expect(rest).to receive(:put_rest).with("data/#{bag_name}/#{item_name}", transmitted_hash).ordered + + knife.run + end end it "requires data bag and item arguments" do - @knife.name_args = [] - lambda { @knife.run }.should raise_error(SystemExit) - @stdout.string.should match(/^You must supply the data bag and an item to edit/) + knife.name_args = [] + expect(stdout).to receive(:puts).twice.with(anything) + expect {knife.run}.to exit_with_code(1) + expect(stdout.string).to eq("") end - it "saves edits on a data bag item" do - Chef::DataBagItem.stub(:load).with('bag_name', 'item_name').and_return(@plain_data) - @knife.should_receive(:edit_data).with(@plain_data).and_return(@edited_data) - @rest.should_receive(:put_rest).with("data/bag_name/item_name", @edited_data).ordered - @knife.run + context "when no secret is provided" do + include_examples "editing a data bag" end - describe "encrypted data bag items" do - before(:each) do - @secret = "abc123SECRET" - @enc_data = Chef::EncryptedDataBagItem.encrypt_data_bag_item(@plain_data, - @secret) - @enc_edited_data = Chef::EncryptedDataBagItem.encrypt_data_bag_item(@edited_data, - @secret) - Chef::DataBagItem.stub(:load).with('bag_name', 'item_name').and_return(@enc_data) - - # Random IV is used each time the data bag item is encrypted, so values - # will not be equal if we encrypt same value twice. - Chef::EncryptedDataBagItem.should_receive(:encrypt_data_bag_item).and_return(@enc_edited_data) - - @secret_file = Tempfile.new("encrypted_data_bag_secret_file_test") - @secret_file.puts(@secret) - @secret_file.flush + context "when config[:print_after] is set" do + let(:config) { {:print_after => true} } + before do + expect(knife.ui).to receive(:output).with(raw_edited_hash) end - after do - @secret_file.close - @secret_file.unlink + include_examples "editing a data bag" + end + + context "when a secret is provided" do + let!(:enc_raw_hash) { Chef::EncryptedDataBagItem.encrypt_data_bag_item(raw_hash, secret) } + let!(:enc_edited_hash) { Chef::EncryptedDataBagItem.encrypt_data_bag_item(raw_edited_hash, secret) } + let(:transmitted_hash) { enc_edited_hash } + + before(:each) do + expect(knife).to receive(:read_secret).at_least(1).times.and_return(secret) + expect(Chef::EncryptedDataBagItem).to receive(:encrypt_data_bag_item).with(raw_edited_hash, secret).and_return(enc_edited_hash) end - it "decrypts and encrypts via --secret" do - @knife.stub(:config).and_return({:secret => @secret}) - @knife.should_receive(:edit_data).with(@plain_data).and_return(@edited_data) - @rest.should_receive(:put_rest).with("data/bag_name/item_name", @enc_edited_data).ordered + context "the data bag starts encrypted" do + let(:is_encrypted?) { true } + let(:db) { Chef::DataBagItem.from_hash(enc_raw_hash) } + # If the data bag is encrypted, it gets passed to `edit` as a hash. Otherwise, it gets passed as a DataBag + let (:data_to_edit) { raw_hash } + + before(:each) do + expect(knife).to receive(:encryption_secret_provided_ignore_encrypt_flag?).and_return(true) + end - @knife.run + include_examples "editing a data bag" end - it "decrypts and encrypts via --secret_file" do - @knife.stub(:config).and_return({:secret_file => @secret_file.path}) - @knife.should_receive(:edit_data).with(@plain_data).and_return(@edited_data) - @rest.should_receive(:put_rest).with("data/bag_name/item_name", @enc_edited_data).ordered + context "the data bag starts unencrypted" do + before(:each) do + expect(knife).to receive(:encryption_secret_provided_ignore_encrypt_flag?).exactly(0).times + expect(knife).to receive(:encryption_secret_provided?).and_return(true) + end - @knife.run + include_examples "editing a data bag" end end + + it "fails to edit an encrypted data bag if the secret is missing" do + expect(Chef::DataBagItem).to receive(:load).with(bag_name, item_name).and_return(db) + expect(knife).to receive(:encrypted?).with(db.raw_data).and_return(true) + expect(knife).to receive(:encryption_secret_provided_ignore_encrypt_flag?).and_return(false) + + expect(knife.ui).to receive(:fatal).with("You cannot edit an encrypted data bag without providing the secret.") + expect {knife.run}.to exit_with_code(1) + end + end diff --git a/spec/unit/knife/data_bag_from_file_spec.rb b/spec/unit/knife/data_bag_from_file_spec.rb index 1ad6b4712c..662af3f0d6 100644 --- a/spec/unit/knife/data_bag_from_file_spec.rb +++ b/spec/unit/knife/data_bag_from_file_spec.rb @@ -26,168 +26,145 @@ Chef::Knife::DataBagFromFile.load_deps describe Chef::Knife::DataBagFromFile do before :each do - Chef::Config[:node_name] = "webmonkey.example.com" - @knife = Chef::Knife::DataBagFromFile.new - @rest = double("Chef::REST") - @knife.stub(:rest).and_return(@rest) - @stdout = StringIO.new - @knife.ui.stub(:stdout).and_return(@stdout) - @tmp_dir = Dir.mktmpdir - @db_folder = File.join(@tmp_dir, 'data_bags', 'bag_name') - FileUtils.mkdir_p(@db_folder) - @db_file = Tempfile.new(["data_bag_from_file_test", ".json"], @db_folder) - @db_file2 = Tempfile.new(["data_bag_from_file_test2", ".json"], @db_folder) - @db_folder2 = File.join(@tmp_dir, 'data_bags', 'bag_name2') - FileUtils.mkdir_p(@db_folder2) - @db_file3 = Tempfile.new(["data_bag_from_file_test3", ".json"], @db_folder2) - @plain_data = { - "id" => "item_name", - "greeting" => "hello", - "nested" => { "a1" => [1, 2, 3], "a2" => { "b1" => true }} - } - @db_file.write(@plain_data.to_json) - @db_file.flush - @knife.instance_variable_set(:@name_args, ['bag_name', @db_file.path]) + Chef::Config[:node_name] = "webmonkey.example.com" + FileUtils.mkdir_p([db_folder, db_folder2]) + db_file.write(plain_data.to_json) + db_file.flush + allow(knife).to receive(:config).and_return(config) + allow(Chef::Knife::Core::ObjectLoader).to receive(:new).and_return(loader) end # We have to explicitly clean up Tempfile on Windows because it said so. after :each do - @db_file.close - @db_file2.close - @db_file3.close - FileUtils.rm_rf(@db_folder) - FileUtils.rm_rf(@db_folder2) - FileUtils.remove_entry_secure @tmp_dir + db_file.close + db_file2.close + db_file3.close + FileUtils.rm_rf(db_folder) + FileUtils.rm_rf(db_folder2) + FileUtils.remove_entry_secure tmp_dir end + let(:knife) do + k = Chef::Knife::DataBagFromFile.new + allow(k).to receive(:rest).and_return(rest) + allow(k.ui).to receive(:stdout).and_return(stdout) + k + end + + let(:tmp_dir) { Dir.mktmpdir } + let(:db_folder) { File.join(tmp_dir, data_bags_path, bag_name) } + let(:db_file) { Tempfile.new(["data_bag_from_file_test", ".json"], db_folder) } + let(:db_file2) { Tempfile.new(["data_bag_from_file_test2", ".json"], db_folder) } + let(:db_folder2) { File.join(tmp_dir, data_bags_path, bag_name2) } + let(:db_file3) { Tempfile.new(["data_bag_from_file_test3", ".json"], db_folder2) } + + def new_bag_expects(b = bag_name, d = plain_data) + data_bag = double + expect(data_bag).to receive(:data_bag).with(b) + expect(data_bag).to receive(:raw_data=).with(d) + expect(data_bag).to receive(:save) + expect(data_bag).to receive(:data_bag) + expect(data_bag).to receive(:id) + data_bag + end + + let(:loader) { double("Knife::Core::ObjectLoader") } + + let(:data_bags_path) { "data_bags" } + let(:plain_data) { { + "id" => "item_name", + "greeting" => "hello", + "nested" => { "a1" => [1, 2, 3], "a2" => { "b1" => true }} + } } + let(:enc_data) { Chef::EncryptedDataBagItem.encrypt_data_bag_item(plain_data, secret) } + + let(:rest) { double("Chef::REST") } + let(:stdout) { StringIO.new } + + let(:bag_name) { "sudoing_admins" } + let(:bag_name2) { "sudoing_admins2" } + let(:item_name) { "ME" } + + let(:secret) { "abc123SECRET" } + + let(:config) { {} } + it "loads from a file and saves" do - @knife.loader.should_receive(:load_from).with("data_bags", 'bag_name', @db_file.path).and_return(@plain_data) - dbag = Chef::DataBagItem.new - Chef::DataBagItem.stub(:new).and_return(dbag) - dbag.should_receive(:save) - @knife.run - - dbag.data_bag.should == 'bag_name' - dbag.raw_data.should == @plain_data + knife.name_args = [bag_name, db_file.path] + expect(loader).to receive(:load_from).with(data_bags_path, bag_name, db_file.path).and_return(plain_data) + expect(Chef::DataBagItem).to receive(:new).and_return(new_bag_expects) + + knife.run end - it "loads all from a mutiple files and saves" do - @knife.name_args = [ 'bag_name', @db_file.path, @db_file2.path ] - @knife.loader.should_receive(:load_from).with("data_bags", 'bag_name', @db_file.path).and_return(@plain_data) - @knife.loader.should_receive(:load_from).with("data_bags", 'bag_name', @db_file2.path).and_return(@plain_data) - dbag = Chef::DataBagItem.new - Chef::DataBagItem.stub(:new).and_return(dbag) - dbag.should_receive(:save).twice - @knife.run - - dbag.data_bag.should == 'bag_name' - dbag.raw_data.should == @plain_data + it "loads all from multiple files and saves" do + knife.name_args = [ bag_name, db_file.path, db_file2.path ] + expect(loader).to receive(:load_from).with(data_bags_path, bag_name, db_file.path).and_return(plain_data) + expect(loader).to receive(:load_from).with(data_bags_path, bag_name, db_file2.path).and_return(plain_data) + expect(Chef::DataBagItem).to receive(:new).twice.and_return(new_bag_expects, new_bag_expects) + + knife.run end it "loads all from a folder and saves" do - @knife.name_args = [ 'bag_name', @db_folder ] - @knife.loader.should_receive(:load_from).with("data_bags", 'bag_name', @db_file.path).and_return(@plain_data) - @knife.loader.should_receive(:load_from).with("data_bags", 'bag_name', @db_file2.path).and_return(@plain_data) - dbag = Chef::DataBagItem.new - Chef::DataBagItem.stub(:new).and_return(dbag) - dbag.should_receive(:save).twice - @knife.run + knife.name_args = [ bag_name, db_folder ] + expect(loader).to receive(:load_from).with(data_bags_path, bag_name, db_file.path).and_return(plain_data) + expect(loader).to receive(:load_from).with(data_bags_path, bag_name, db_file2.path).and_return(plain_data) + expect(Chef::DataBagItem).to receive(:new).twice.and_return(new_bag_expects, new_bag_expects) + + knife.run end describe "loading all data bags" do - before do - @pwd = Dir.pwd - Dir.chdir(@tmp_dir) - end - - after do - Dir.chdir(@pwd) - end - it "loads all data bags when -a or --all options is provided" do - @knife.name_args = [] - @knife.stub(:config).and_return({:all => true}) - @knife.loader.should_receive(:load_from).with("data_bags", "bag_name", File.basename(@db_file.path)). - and_return(@plain_data) - @knife.loader.should_receive(:load_from).with("data_bags", "bag_name", File.basename(@db_file2.path)). - and_return(@plain_data) - @knife.loader.should_receive(:load_from).with("data_bags", "bag_name2", File.basename(@db_file3.path)). - and_return(@plain_data) - dbag = Chef::DataBagItem.new - Chef::DataBagItem.stub(:new).and_return(dbag) - dbag.should_receive(:save).exactly(3).times - @knife.run + knife.name_args = [] + config[:all] = true + expect(loader).to receive(:find_all_object_dirs).with("./#{data_bags_path}").and_return([bag_name, bag_name2]) + expect(loader).to receive(:find_all_objects).with("./#{data_bags_path}/#{bag_name}").and_return([File.basename(db_file.path), File.basename(db_file2.path)]) + expect(loader).to receive(:find_all_objects).with("./#{data_bags_path}/#{bag_name2}").and_return([File.basename(db_file3.path)]) + expect(loader).to receive(:load_from).with(data_bags_path, bag_name, File.basename(db_file.path)).and_return(plain_data) + expect(loader).to receive(:load_from).with(data_bags_path, bag_name, File.basename(db_file2.path)).and_return(plain_data) + expect(loader).to receive(:load_from).with(data_bags_path, bag_name2, File.basename(db_file3.path)).and_return(plain_data) + expect(Chef::DataBagItem).to receive(:new).exactly(3).times.and_return(new_bag_expects, new_bag_expects, new_bag_expects(bag_name2)) + + knife.run end it "loads all data bags items when -a or --all options is provided" do - @knife.name_args = ["bag_name2"] - @knife.stub(:config).and_return({:all => true}) - @knife.loader.should_receive(:load_from).with("data_bags", "bag_name2", File.basename(@db_file3.path)). - and_return(@plain_data) - dbag = Chef::DataBagItem.new - Chef::DataBagItem.stub(:new).and_return(dbag) - dbag.should_receive(:save) - @knife.run - dbag.data_bag.should == 'bag_name2' - dbag.raw_data.should == @plain_data + knife.name_args = [bag_name2] + config[:all] = true + expect(loader).to receive(:find_all_objects).with("./#{data_bags_path}/#{bag_name2}").and_return([File.basename(db_file3.path)]) + expect(loader).to receive(:load_from).with(data_bags_path, bag_name2, File.basename(db_file3.path)).and_return(plain_data) + expect(Chef::DataBagItem).to receive(:new).and_return(new_bag_expects(bag_name2)) + + knife.run end end describe "encrypted data bag items" do before(:each) do - @secret = "abc123SECRET" - @enc_data = Chef::EncryptedDataBagItem.encrypt_data_bag_item(@plain_data, - @secret) - - # Random IV is used each time the data bag item is encrypted, so values - # will not be equal if we re-encrypt. - Chef::EncryptedDataBagItem.should_receive(:encrypt_data_bag_item).and_return(@enc_data) - - @secret_file = Tempfile.new("encrypted_data_bag_secret_file_test") - @secret_file.puts(@secret) - @secret_file.flush - end - - after do - @secret_file.close - @secret_file.unlink + expect(knife).to receive(:encryption_secret_provided?).and_return(true) + expect(knife).to receive(:read_secret).and_return(secret) + expect(Chef::EncryptedDataBagItem).to receive(:encrypt_data_bag_item).with(plain_data, secret).and_return(enc_data) end it "encrypts values when given --secret" do - @knife.stub(:config).and_return({:secret => @secret}) - - @knife.loader.should_receive(:load_from).with("data_bags", "bag_name", @db_file.path).and_return(@plain_data) - dbag = Chef::DataBagItem.new - Chef::DataBagItem.stub(:new).and_return(dbag) - dbag.should_receive(:save) - @knife.run - dbag.data_bag.should == 'bag_name' - dbag.raw_data.should == @enc_data - end - - it "encrypts values when given --secret_file" do - @knife.stub(:config).and_return({:secret_file => @secret_file.path}) + knife.name_args = [bag_name, db_file.path] + expect(loader).to receive(:load_from).with(data_bags_path, bag_name, db_file.path).and_return(plain_data) + expect(Chef::DataBagItem).to receive(:new).and_return(new_bag_expects(bag_name, enc_data)) - @knife.loader.stub(:load_from).with("data_bags", 'bag_name', @db_file.path).and_return(@plain_data) - dbag = Chef::DataBagItem.new - Chef::DataBagItem.stub(:new).and_return(dbag) - dbag.should_receive(:save) - @knife.run - dbag.data_bag.should == 'bag_name' - dbag.raw_data.should == @enc_data + knife.run end end describe "command line parsing" do it "prints help if given no arguments" do - @knife.instance_variable_set(:@name_args, []) - lambda { @knife.run }.should raise_error(SystemExit) - help_text = "knife data bag from file BAG FILE|FOLDER [FILE|FOLDER..] (options)" - help_text_regex = Regexp.new("^#{Regexp.escape(help_text)}") - @stdout.string.should match(help_text_regex) + knife.name_args = [bag_name] + expect {knife.run}.to exit_with_code(1) + expect(stdout.string).to start_with("knife data bag from file BAG FILE|FOLDER [FILE|FOLDER..] (options)") end end diff --git a/spec/unit/knife/data_bag_secret_options_spec.rb b/spec/unit/knife/data_bag_secret_options_spec.rb new file mode 100644 index 0000000000..0a2d8ca4bf --- /dev/null +++ b/spec/unit/knife/data_bag_secret_options_spec.rb @@ -0,0 +1,165 @@ +# +# Author:: Tyler Ball (<tball@opscode.com>) +# Copyright:: Copyright (c) 2009-2014 Opscode, 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 'spec_helper' +require 'chef/knife' +require 'chef/config' +require 'tempfile' + +class ExampleDataBagCommand < Chef::Knife + include Chef::Knife::DataBagSecretOptions +end + +describe Chef::Knife::DataBagSecretOptions do + let(:example_db) do + k = ExampleDataBagCommand.new + allow(k.ui).to receive(:stdout).and_return(stdout) + k + end + + let(:stdout) { StringIO.new } + + let(:secret) { "abc123SECRET" } + let(:secret_file) do + sfile = Tempfile.new("encrypted_data_bag_secret") + sfile.puts(secret) + sfile.flush + sfile + end + + after do + secret_file.close + secret_file.unlink + end + + describe "#validate_secrets" do + + it "throws an error when provided with both --secret and --secret-file on the CL" do + Chef::Config[:knife][:cl_secret_file] = secret_file.path + Chef::Config[:knife][:cl_secret] = secret + expect(example_db).to receive(:exit).with(1) + expect(example_db.ui).to receive(:fatal).with("Please specify only one of --secret, --secret-file") + + example_db.validate_secrets + end + + it "throws an error when provided with `secret` and `secret_file` in knife.rb" do + Chef::Config[:knife][:secret_file] = secret_file.path + Chef::Config[:knife][:secret] = secret + expect(example_db).to receive(:exit).with(1) + expect(example_db.ui).to receive(:fatal).with("Please specify only one of 'secret' or 'secret_file' in your config file") + + example_db.validate_secrets + end + + end + + describe "#read_secret" do + + it "returns the secret first" do + Chef::Config[:knife][:cl_secret] = secret + expect(example_db).to receive(:config).and_return({ :secret => secret }) + expect(example_db.read_secret).to eq(secret) + end + + it "returns the secret_file only if secret does not exist" do + Chef::Config[:knife][:cl_secret_file] = secret_file.path + expect(example_db).to receive(:config).and_return({ :secret_file => secret_file.path }) + expect(Chef::EncryptedDataBagItem).to receive(:load_secret).with(secret_file.path).and_return("secret file contents") + expect(example_db.read_secret).to eq("secret file contents") + end + + it "returns the secret from the knife.rb config" do + Chef::Config[:knife][:secret_file] = secret_file.path + Chef::Config[:knife][:secret] = secret + expect(example_db.read_secret).to eq(secret) + end + + it "returns the secret_file from the knife.rb config only if the secret does not exist" do + Chef::Config[:knife][:secret_file] = secret_file.path + expect(Chef::EncryptedDataBagItem).to receive(:load_secret).with(secret_file.path).and_return("secret file contents") + expect(example_db.read_secret).to eq("secret file contents") + end + + end + + describe "#encryption_secret_provided?" do + + it "returns true if the secret is passed on the CL" do + Chef::Config[:knife][:cl_secret] = secret + expect(example_db.encryption_secret_provided?).to eq(true) + end + + it "returns true if the secret_file is passed on the CL" do + Chef::Config[:knife][:cl_secret_file] = secret_file.path + expect(example_db.encryption_secret_provided?).to eq(true) + end + + it "returns true if --encrypt is passed on the CL and :secret is in config" do + expect(example_db).to receive(:config).and_return({ :encrypt => true }) + Chef::Config[:knife][:secret] = secret + expect(example_db.encryption_secret_provided?).to eq(true) + end + + it "returns true if --encrypt is passed on the CL and :secret_file is in config" do + expect(example_db).to receive(:config).and_return({ :encrypt => true }) + Chef::Config[:knife][:secret_file] = secret_file.path + expect(example_db.encryption_secret_provided?).to eq(true) + end + + it "throws an error if --encrypt is passed and there is not :secret or :secret_file in the config" do + expect(example_db).to receive(:config).and_return({ :encrypt => true }) + expect(example_db).to receive(:exit).with(1) + expect(example_db.ui).to receive(:fatal).with("No secret or secret_file specified in config, unable to encrypt item.") + example_db.encryption_secret_provided? + end + + it "returns false if no secret is passed" do + expect(example_db).to receive(:config).and_return({}) + expect(example_db.encryption_secret_provided?).to eq(false) + end + + it "returns false if --encrypt is not provided and :secret is in the config" do + expect(example_db).to receive(:config).and_return({}) + Chef::Config[:knife][:secret] = secret + expect(example_db.encryption_secret_provided?).to eq(false) + end + + it "returns false if --encrypt is not provided and :secret_file is in the config" do + expect(example_db).to receive(:config).and_return({}) + Chef::Config[:knife][:secret_file] = secret_file.path + expect(example_db.encryption_secret_provided?).to eq(false) + end + + it "returns true if --encrypt is not provided, :secret is in the config and need_encrypt_flag is false" do + Chef::Config[:knife][:secret] = secret + expect(example_db.encryption_secret_provided_ignore_encrypt_flag?).to eq(true) + end + + it "returns true if --encrypt is not provided, :secret_file is in the config and need_encrypt_flag is false" do + Chef::Config[:knife][:secret_file] = secret_file.path + expect(example_db.encryption_secret_provided_ignore_encrypt_flag?).to eq(true) + end + + it "returns false if --encrypt is not provided and need_encrypt_flag is false" do + expect(example_db.encryption_secret_provided_ignore_encrypt_flag?).to eq(false) + end + + end + +end diff --git a/spec/unit/knife/data_bag_show_spec.rb b/spec/unit/knife/data_bag_show_spec.rb index 4aa642fc4b..1125d99c2a 100644 --- a/spec/unit/knife/data_bag_show_spec.rb +++ b/spec/unit/knife/data_bag_show_spec.rb @@ -25,97 +25,99 @@ require 'chef/json_compat' require 'tempfile' describe Chef::Knife::DataBagShow do + before do - Chef::Config[:node_name] = "webmonkey.example.com" - @knife = Chef::Knife::DataBagShow.new - @knife.config[:format] = 'json' - @rest = double("Chef::REST") - allow(@knife).to receive(:rest).and_return(@rest) - @stdout = StringIO.new - allow(@knife.ui).to receive(:stdout).and_return(@stdout) + Chef::Config[:node_name] = "webmonkey.example.com" + knife.name_args = [bag_name, item_name] + allow(knife).to receive(:config).and_return(config) end - - it "prints the ids of the data bag items when given a bag name" do - @knife.instance_variable_set(:@name_args, ['bag_o_data']) - data_bag_contents = { "baz"=>"http://localhost:4000/data/bag_o_data/baz", - "qux"=>"http://localhost:4000/data/bag_o_data/qux"} - expect(Chef::DataBag).to receive(:load).and_return(data_bag_contents) - expected = %q|[ - "baz", - "qux" -]| - @knife.run - expect(@stdout.string.strip).to eq(expected) + let(:knife) do + k = Chef::Knife::DataBagShow.new + allow(k).to receive(:rest).and_return(rest) + allow(k.ui).to receive(:stdout).and_return(stdout) + k end - it "prints the contents of the data bag item when given a bag and item name" do - @knife.instance_variable_set(:@name_args, ['bag_o_data', 'an_item']) - data_item = Chef::DataBagItem.new.tap {|item| item.raw_data = {"id" => "an_item", "zsh" => "victory_through_tabbing"}} + let(:rest) { double("Chef::REST") } + let(:stdout) { StringIO.new } - expect(Chef::DataBagItem).to receive(:load).with('bag_o_data', 'an_item').and_return(data_item) - - @knife.run - expect(Chef::JSONCompat.from_json(@stdout.string)).to eq(data_item.raw_data) - end + let(:bag_name) { "sudoing_admins" } + let(:item_name) { "ME" } - it "should pretty print the data bag contents" do - @knife.instance_variable_set(:@name_args, ['bag_o_data', 'an_item']) - data_item = Chef::DataBagItem.new.tap {|item| item.raw_data = {"id" => "an_item", "zsh" => "victory_through_tabbing"}} + let(:data_bag_contents) { { "id" => "id", "baz"=>"http://localhost:4000/data/bag_o_data/baz", + "qux"=>"http://localhost:4000/data/bag_o_data/qux"} } + let(:enc_hash) {Chef::EncryptedDataBagItem.encrypt_data_bag_item(data_bag_contents, secret)} + let(:data_bag) {Chef::DataBagItem.from_hash(data_bag_contents)} + let(:data_bag_with_encoded_hash) { Chef::DataBagItem.from_hash(enc_hash) } + let(:enc_data_bag) { Chef::EncryptedDataBagItem.new(enc_hash, secret) } - expect(Chef::DataBagItem).to receive(:load).with('bag_o_data', 'an_item').and_return(data_item) + let(:secret) { "abc123SECRET" } + # + # let(:raw_hash) {{ "login_name" => "alphaomega", "id" => item_name }} + # + let(:config) { {format: "json"} } - @knife.run - expect(@stdout.string).to eql("{\n \"id\": \"an_item\",\n \"zsh\": \"victory_through_tabbing\"\n}\n") - end + context "Data bag to show is encrypted" do + before do + allow(knife).to receive(:encrypted?).and_return(true) + end - describe "encrypted data bag items" do - before(:each) do - @secret = "abc123SECRET" - @plain_data = { - "id" => "item_name", - "greeting" => "hello", - "nested" => { "a1" => [1, 2, 3], "a2" => { "b1" => true }} - } - @enc_data = Chef::EncryptedDataBagItem.encrypt_data_bag_item(@plain_data, - @secret) - @knife.instance_variable_set(:@name_args, ['bag_name', 'item_name']) - - @secret_file = Tempfile.new("encrypted_data_bag_secret_file_test") - @secret_file.puts(@secret) - @secret_file.flush + it "decrypts and displays the encrypted data bag when the secret is provided" do + expect(knife).to receive(:encryption_secret_provided_ignore_encrypt_flag?).and_return(true) + expect(knife).to receive(:read_secret).and_return(secret) + expect(Chef::DataBagItem).to receive(:load).with(bag_name, item_name).and_return(data_bag_with_encoded_hash) + expect(knife.ui).to receive(:info).with("Encrypted data bag detected, decrypting with provided secret.") + expect(Chef::EncryptedDataBagItem).to receive(:load).with(bag_name, item_name, secret).and_return(enc_data_bag) + + expected = %q|baz: http://localhost:4000/data/bag_o_data/baz +id: id +qux: http://localhost:4000/data/bag_o_data/qux| + knife.run + expect(stdout.string.strip).to eq(expected) end - after do - @secret_file.close - @secret_file.unlink + it "displays the encrypted data bag when the secret is not provided" do + expect(knife).to receive(:encryption_secret_provided_ignore_encrypt_flag?).and_return(false) + expect(Chef::DataBagItem).to receive(:load).with(bag_name, item_name).and_return(data_bag_with_encoded_hash) + expect(knife.ui).to receive(:warn).with("Encrypted data bag detected, but no secret provided for decoding. Displaying encrypted data.") + + knife.run + expect(stdout.string.strip).to include("baz", "qux", "cipher") end + end - it "prints the decrypted contents of an item when given --secret" do - allow(@knife).to receive(:config).and_return({:secret => @secret}) - expect(Chef::EncryptedDataBagItem).to receive(:load). - with('bag_name', 'item_name', @secret). - and_return(Chef::EncryptedDataBagItem.new(@enc_data, @secret)) - @knife.run - expect(Chef::JSONCompat.from_json(@stdout.string)).to eq(@plain_data) + context "Data bag to show is not encrypted" do + before do + allow(knife).to receive(:encrypted?).and_return(false) + expect(knife).to receive(:read_secret).exactly(0).times end - it "prints the decrypted contents of an item when given --secret_file" do - allow(@knife).to receive(:config).and_return({:secret_file => @secret_file.path}) - expect(Chef::EncryptedDataBagItem).to receive(:load). - with('bag_name', 'item_name', @secret). - and_return(Chef::EncryptedDataBagItem.new(@enc_data, @secret)) - @knife.run - expect(Chef::JSONCompat.from_json(@stdout.string)).to eq(@plain_data) + it "displays the data bag" do + expect(Chef::DataBagItem).to receive(:load).with(bag_name, item_name).and_return(data_bag) + expect(knife.ui).to receive(:info).with("Unencrypted data bag detected, ignoring any provided secret options.") + + expected = %q|baz: http://localhost:4000/data/bag_o_data/baz +id: id +qux: http://localhost:4000/data/bag_o_data/qux| + knife.run + expect(stdout.string.strip).to eq(expected) end end - describe "command line parsing" do - it "prints help if given no arguments" do - @knife.instance_variable_set(:@name_args, []) - expect { @knife.run }.to raise_error(SystemExit) - expect(@stdout.string).to match(/^knife data bag show BAG \[ITEM\] \(options\)/) - end + it "displays the list of items in the data bag when only one @name_arg is provided" do + knife.name_args = [bag_name] + expect(Chef::DataBag).to receive(:load).with(bag_name).and_return({}) + + knife.run + expect(stdout.string.strip).to eq("") + end + + it "raises an error when no @name_args are provided" do + knife.name_args = [] + + expect {knife.run}.to exit_with_code(1) + expect(stdout.string).to start_with("knife data bag show BAG [ITEM] (options)") end end |