class ChefVault::Item

Attributes

client_key_contents[RW]

@!attribute [rw] client_key_contents

@return [String] the contents of the private key that is used to
  decrypt secrets.  Defaults to the value of Chef::Config[:client_key_contents]
client_key_path[RW]

@!attribute [rw] client_key_path

@return [String] the path to the private key that is used to
  decrypt secrets.  Defaults to the value of Chef::Config[:client_key]
encrypted_data_bag_item[RW]

@!attribute [rw] encrypted_data_bag_item

@return [nil] this attribute is not currently used
keys[RW]

@!attribute [rw] keys

@return [ChefVault::ItemKeys] the keys associated with this vault
node_name[RW]

@!attribute [rw] node_name

@return [String] the node name that is used to decrypt secrets.
  Defaults to the value of Chef::Config[:node_name]

Public Class Methods

data_bag_item_type(vault, name) click to toggle source

determines whether a data bag item is a vault, an encrypted data bag item, or a normal data bag item. An item is a vault if:

a) the data bag item contains at least one key whose value is

an hash with the key 'encrypted data'

b) the data bag that contains the item contains a second item

suffixed with _keys

if a) is false, the item is a normal data bag if a) and b) are true, the item is a vault if a) is true but b) is false, the item is an encrypted data

bag item

@param vault [String] the name of the data bag @param name [String] the name of the item in the data bag @return [Symbol] one of :vault, :encrypted or :normal

# File lib/chef-vault/item.rb, line 362
def self.data_bag_item_type(vault, name)
  # adapted from https://github.com/opscode-cookbooks/chef-vault/blob/v1.3.0/libraries/chef_vault_item.rb
  # and https://github.com/sensu/sensu-chef/blob/2.9.0/libraries/sensu_helpers.rb
  begin
    dbi = Chef::DataBagItem.load(vault, name)
  rescue Net::HTTPClientException => http_error
    if http_error.response.code == "404"
      raise ChefVault::Exceptions::ItemNotFound,
        "#{vault}/#{name} not found"
    else
      raise http_error
    end
  end
  encrypted = dbi.detect do |_, v|
    v.is_a?(Hash) && v.key?("encrypted_data")
  end

  # return a symbol describing the type of item we detected
  case
  when encrypted && Chef::DataBag.load(vault).key?("#{name}_keys")
    :vault
  when encrypted
    :encrypted
  else
    :normal
  end
end
format_output(values, item) click to toggle source
# File lib/chef-vault/item.rb, line 326
def self.format_output(values, item)
  values.split(",").each do |value|
    value.strip!
    $stdout.puts("#{value}: #{item[value]}")
  end
end
load(vault, name, opts = {}) click to toggle source

loads an existing vault item @param (see initialize) @option (see initialize)

# File lib/chef-vault/item.rb, line 304
def self.load(vault, name, opts = {})
  item = new(vault, name, opts)
  item.load_keys(vault, "#{name}_keys")

  begin
    item.raw_data =
      Chef::EncryptedDataBagItem.load(vault, name, item.secret).to_hash
  rescue Net::HTTPClientException => http_error
    if http_error.response.code == "404"
      raise ChefVault::Exceptions::ItemNotFound,
        "#{vault}/#{name} could not be found"
    else
      raise http_error
    end
  rescue Chef::Exceptions::ValidationFailed
    raise ChefVault::Exceptions::ItemNotFound,
      "#{vault}/#{name} could not be found"
  end
  format_output(opts[:values], item) if opts[:values]
  item
end
new(vault, name, opts = {}) click to toggle source

constructs a new ChefVault::Item @param vault [String] the name of the data bag that contains the vault @param name [String] the name of the item in the vault @param opts [Hash] @option opts [String] :node_name the name of the node to decrypt secrets

as. Defaults to the :node_name value of Chef::Config

@option opts [String] :client_key_path the name of the node to decrypt

secrets as.  Defaults to the :client_key value of Chef::Config

@option opts [String] :client_key_contents the private key to decrypt

secrets as.  Defaults to the :client_key_contents value of Chef::Config
Calls superclass method
# File lib/chef-vault/item.rb, line 68
def initialize(vault, name, opts = {})
  super() # Don't pass parameters
  @data_bag = vault
  @raw_data["id"] = name
  @keys = ChefVault::ItemKeys.new(vault, "#{name}_keys")
  @secret = generate_secret
  @encrypted = false
  opts = {
    node_name: Chef::Config[:node_name],
    client_key_path: Chef::Config[:client_key],
    client_key_contents: Chef::Config[:client_key_contents],
  }.merge(opts)
  @node_name = opts[:node_name]
  @client_key_path = opts[:client_key_path]
  @client_key_contents = opts[:client_key_contents]
  @current_query = search
end
vault?(vault, name) click to toggle source

determines if a data bag item looks like a vault @param vault [String] the name of the data bag @param name [String] the name of the item in the data bag @return [Boolean] true if the data bag item looks like a vault

# File lib/chef-vault/item.rb, line 342
def self.vault?(vault, name)
  :vault == data_bag_item_type(vault, name)
end

Public Instance Methods

[](key) click to toggle source
Calls superclass method
# File lib/chef-vault/item.rb, line 231
def [](key)
  reload_raw_data if @encrypted
  super
end
[]=(key, value) click to toggle source
Calls superclass method
# File lib/chef-vault/item.rb, line 226
def []=(key, value)
  reload_raw_data if @encrypted
  super
end
admins(admin_string, action = :add) click to toggle source
# File lib/chef-vault/item.rb, line 149
def admins(admin_string, action = :add)
  admin_string.split(",").each do |admin|
    admin.strip!
    admin_key = load_actor(admin, "admins")
    case action
    when :add
      keys.add(admin_key, @secret)
    when :delete
      keys.delete(admin_key)
    else
      raise ChefVault::Exceptions::KeysActionNotValid,
        "#{action} is not a valid action"
    end
  end
end
clients(search_or_client = search_results, action = :add) click to toggle source
# File lib/chef-vault/item.rb, line 92
def clients(search_or_client = search_results, action = :add)
  # for backwards compatibility, if we're handed a string
  # do a search using that string and recurse
  if search_or_client.is_a?(String)
    clients(search_results(search_or_client), action)
  elsif search_or_client.is_a?(Chef::ApiClient)
    handle_client_action(search_or_client, action)
  else
    search_or_client.each do |name|
      # rubocop:disable all
      begin
        client = load_actor(name, "clients")
        handle_client_action(client, action)
      rescue ChefVault::Exceptions::ClientNotFound
        ChefVault::Log.warn "node '#{name}' has no 'default' public key; skipping"
      end
      # rubocop:enable all
    end
  end
end
delete_client(client_name) click to toggle source
# File lib/chef-vault/item.rb, line 333
def delete_client(client_name)
  client_key = load_actor(client_name, "clients")
  keys.delete(client_key)
end
destroy() click to toggle source
Calls superclass method
# File lib/chef-vault/item.rb, line 285
def destroy
  keys.destroy

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

    FileUtils.rm("#{data_bag_item_path}.json")

    nil
  else
    super(data_bag, id)
  end
end
generate_secret(key_size = 32) click to toggle source

private

# File lib/chef-vault/item.rb, line 220
def generate_secret(key_size = 32)
  # Defaults to 32 bytes, as this is the size that a Chef
  # Encrypted Data Bag Item will digest all secrets down to anyway
  SecureRandom.random_bytes(key_size)
end
get_admins() click to toggle source
# File lib/chef-vault/item.rb, line 165
def get_admins
  keys.admins
end
get_clients() click to toggle source
# File lib/chef-vault/item.rb, line 133
def get_clients
  keys.clients
end
load_keys(vault, keys) click to toggle source

private

# File lib/chef-vault/item.rb, line 87
def load_keys(vault, keys)
  @keys = ChefVault::ItemKeys.load(vault, keys)
  @secret = secret
end
mode(mode) click to toggle source
# File lib/chef-vault/item.rb, line 145
def mode(mode)
  keys.mode(mode) if mode
end
refresh(clean_unknown_clients = false) click to toggle source

refreshes a vault by re-processing the search query and adding a secret for any nodes found (including new ones) @param clean_unknown_clients [Boolean] remove clients that can

no longer be found

@return [void]

# File lib/chef-vault/item.rb, line 395
def refresh(clean_unknown_clients = false)
  if search.empty?
    raise ChefVault::Exceptions::SearchNotFound,
      "#{@data_bag}/#{@raw_data["id"]} does not have a stored "\
      "search_query, probably because it was created with an "\
      "older version of chef-vault. Use 'knife vault update' "\
      "to update the databag with the search query."
  end

  # a bit of a misnomer; this doesn't remove unknown
  # admins, just clients which are nodes
  remove_unknown_nodes if clean_unknown_clients

  # re-process the search query to add new clients
  clients

  # save the updated keys only
  save_keys(@raw_data["id"])
end
remove(key) click to toggle source
# File lib/chef-vault/item.rb, line 169
def remove(key)
  @raw_data.delete(key)
end
rotate_keys!(clean_unknown_clients = false) click to toggle source
# File lib/chef-vault/item.rb, line 194
def rotate_keys!(clean_unknown_clients = false)
  @secret = generate_secret

  # clean existing encrypted data for clients/admins
  keys.clear_encrypted

  unless get_clients.empty?
    # a bit of a misnomer; this doesn't remove unknown
    # admins, just clients which are nodes
    remove_unknown_nodes if clean_unknown_clients
    # re-encrypt the new shared secret for all remaining clients
    clients(get_clients)
  end

  unless get_admins.empty?
    # re-encrypt the new shared secret for all admins
    get_admins.each do |admin|
      admins(admin)
    end
  end

  save
  reload_raw_data
end
save(item_id = @raw_data["id"]) click to toggle source
Calls superclass method
# File lib/chef-vault/item.rb, line 236
def save(item_id = @raw_data["id"])
  save_keys(item_id)
  # Make sure the item is encrypted before saving
  encrypt! unless @encrypted

  # Now save the encrypted data
  if Chef::Config[:solo_legacy_mode]
    save_solo(item_id)
  else
    begin
      Chef::DataBag.load(data_bag)
    rescue Net::HTTPClientException => http_error
      if http_error.response.code == "404"
        chef_data_bag = Chef::DataBag.new
        chef_data_bag.name data_bag
        chef_data_bag.create
      end
    end

    super
  end
end
save_keys(item_id = @raw_data["id"]) click to toggle source
# File lib/chef-vault/item.rb, line 259
def save_keys(item_id = @raw_data["id"])
  # validate the format of the id before attempting to save
  validate_id!(item_id)

  # ensure that the ID of the vault hasn't changed since the keys
  # data bag item was created
  keys_id = keys["id"].match(/^(.+)_keys/)[1]
  if keys_id != item_id
    raise ChefVault::Exceptions::IdMismatch,
      "id mismatch - input JSON has id '#{item_id}' but vault item has id '#{keys_id}'"
  end

  # save the keys first, raising an error if no keys were defined
  if keys.admins.empty? && keys.clients.empty?
    raise ChefVault::Exceptions::NoKeysDefined,
      "No keys defined for #{item_id}"
  end

  keys.save
end
search_results(statement = search) click to toggle source
# File lib/chef-vault/item.rb, line 113
def search_results(statement = search)
  @search_results = nil if statement != @current_query
  @current_query = statement
  @search_results ||= begin
    results_returned = false
    results = []
    query = Chef::Search::Query.new
    query.search(:node, statement, filter_result: { name: ["name"] }, rows: 10000) do |node|
      results_returned = true
      results << node["name"]
    end

    unless results_returned
      ChefVault::Log.warn "No clients were returned from search, you may not have "\
        "got what you expected!!"
    end
    results
  end
end
secret() click to toggle source
# File lib/chef-vault/item.rb, line 173
def secret
  if @keys.include?(@node_name) && !@keys[@node_name].nil?
    unless @client_key_contents.nil?
      private_key = OpenSSL::PKey::RSA.new(@client_key_contents)
    else
      private_key = OpenSSL::PKey::RSA.new(File.open(@client_key_path).read)
    end
    begin
      private_key.private_decrypt(Base64.decode64(@keys[@node_name]))
    rescue OpenSSL::PKey::RSAError
      raise ChefVault::Exceptions::SecretDecryption,
        "#{data_bag}/#{id} is encrypted for you, but your private key failed to decrypt the contents.  "\
        "(if you regenerated your client key, have an administrator of the vault run 'knife vault refresh')"
    end
  else
    raise ChefVault::Exceptions::SecretDecryption,
      "#{data_bag}/#{id} is not encrypted with your public key.  "\
      "Contact an administrator of the vault item to encrypt for you!"
  end
end
to_json(*a) click to toggle source
Calls superclass method
# File lib/chef-vault/item.rb, line 280
def to_json(*a)
  json = super
  json.gsub(self.class.name, self.class.superclass.name)
end

Private Instance Methods

add_client(client) click to toggle source

adds a client to the vault item keys @param client [Chef::ApiClient] the API client to add @return [void]

# File lib/chef-vault/item.rb, line 494
def add_client(client)
  keys.add(client, @secret)
end
client_exists?(clientname) click to toggle source

checks if a client exists on the Chef server. If we get back a 404, the client does not exist. Any other HTTP errors are re-raised. Otherwise, the client exists @param clientname [String] the name of the client @return [Boolean] whether the client exists or not

# File lib/chef-vault/item.rb, line 467
def client_exists?(clientname)
  Chef::ApiClient.load(clientname)
  true
rescue Net::HTTPClientException => http_error
  return false if http_error.response.code == "404"

  raise http_error
end
delete_client_or_node(name) click to toggle source

removes a client to the vault item keys @param name [String] the name of the API client or node to remove @return [void]

# File lib/chef-vault/item.rb, line 501
def delete_client_or_node(name)
  client = load_actor(name, "clients")
  keys.delete(client)
end
encrypt!() click to toggle source
# File lib/chef-vault/item.rb, line 417
def encrypt!
  @raw_data = Chef::EncryptedDataBagItem.encrypt_data_bag_item(self, @secret)
  @encrypted = true
end
handle_client_action(api_client, action) click to toggle source

adds or deletes an API client from the vault item keys @param api_client [Chef::ApiClient] the API client to operate on @param action [Symbol] :add or :delete @return [void]

# File lib/chef-vault/item.rb, line 480
def handle_client_action(api_client, action)
  case action
  when :add
    # TODO: next line seems to create a client from the api_client (which seems to be identical)
    client = load_actor(api_client.name, "clients")
    add_client(client)
  when :delete
    delete_client_or_node(api_client.name)
  end
end
load_actor(actor_name, type) click to toggle source
# File lib/chef-vault/item.rb, line 430
def load_actor(actor_name, type)
  ChefVault::Actor.new(type, actor_name)
end
node_exists?(nodename) click to toggle source

checks if a node exists on the Chef server by performing a search against the node index. If the search returns no results, the node does not exist. @param nodename [String] the name of the node @return [Boolean] whether the node exists or not

# File lib/chef-vault/item.rb, line 458
def node_exists?(nodename)
  search_results.include?(nodename)
end
reload_raw_data() click to toggle source
# File lib/chef-vault/item.rb, line 422
def reload_raw_data
  @raw_data =
    Chef::EncryptedDataBagItem.load(@data_bag, @raw_data["id"], secret).to_hash
  @encrypted = false

  @raw_data
end
remove_unknown_nodes() click to toggle source

removes unknown nodes by performing a node search for each of the existing clients. If the search returns nothing or the client cannot be loaded, then we remove that client from the vault @return [void]

# File lib/chef-vault/item.rb, line 439
def remove_unknown_nodes
  # build a list of clients to remove so we don't
  # mutate the clients while iterating over search results
  clients_to_remove = []
  get_clients.each do |nodename|
    clients_to_remove.push(nodename) unless node_exists?(nodename)
  end
  # now delete any flagged clients from the keys data bag
  clients_to_remove.each do |client|
    ChefVault::Log.warn "Removing unknown client '#{client}'"
    keys.delete(load_actor(client, "clients"))
  end
end