summaryrefslogtreecommitdiff
path: root/lib/chef
diff options
context:
space:
mode:
authortylercloke <tylercloke@gmail.com>2015-04-07 17:31:34 -0700
committertylercloke <tylercloke@gmail.com>2015-04-20 15:19:06 -0700
commit855954929adf26aecaad8cd0f549f03353825d6c (patch)
treef4da617c866b40e6102cfbe3d2ac936e80802cba /lib/chef
parentde7dc03e8da3c30f9b833b25e58f4e087f978c1a (diff)
downloadchef-tc/chef_key_object.tar.gz
Implemented the Key object corrosponding to key rotation API endpoint.tc/chef_key_object
Diffstat (limited to 'lib/chef')
-rw-r--r--lib/chef/exceptions.rb3
-rw-r--r--lib/chef/key.rb251
2 files changed, 254 insertions, 0 deletions
diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb
index eea6a2f239..cfedbfd0d9 100644
--- a/lib/chef/exceptions.rb
+++ b/lib/chef/exceptions.rb
@@ -69,6 +69,9 @@ class Chef
class ValidationFailed < ArgumentError; end
class InvalidPrivateKey < ArgumentError; end
class ConfigurationError < ArgumentError; end
+ class MissingKeyAttribute < ArgumentError; end
+ class InvalidKeyArgument < ArgumentError; end
+ class InvalidKeyAttribute < ArgumentError; end
class RedirectLimitExceeded < RuntimeError; end
class AmbiguousRunlistSpecification < ArgumentError; end
class CookbookFrozen < ArgumentError; end
diff --git a/lib/chef/key.rb b/lib/chef/key.rb
new file mode 100644
index 0000000000..1828713386
--- /dev/null
+++ b/lib/chef/key.rb
@@ -0,0 +1,251 @@
+#
+# Author:: Tyler Cloke (tyler@chef.io)
+# Copyright:: Copyright (c) 2015 Chef Software, Inc
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/json_compat'
+require 'chef/mixin/params_validate'
+require 'chef/exceptions'
+
+class Chef
+ # Class for interacting with a chef key object. Can be used to create new keys,
+ # save to server, load keys from server, list keys, delete keys, etc.
+ #
+ # @author Tyler Cloke
+ #
+ # @attr [String] actor the name of the client or user that this key is for
+ # @attr [String] name the name of the key
+ # @attr [String] public_key the RSA string of this key
+ # @attr [String] private_key the RSA string of the private key if returned via a POST or PUT
+ # @attr [String] expiration_date the ISO formatted string YYYY-MM-DDTHH:MM:SSZ, i.e. 2020-12-24T21:00:00Z
+ # @attr [String] rest Chef::REST object, initialized and cached via chef_rest method
+ # @attr [string] api_base either "users" or "clients", initialized and cached via api_base method
+ #
+ # @attr_reader [String] actor_field_name must be either 'client' or 'user'
+ class Key
+
+ include Chef::Mixin::ParamsValidate
+
+ attr_reader :actor_field_name
+
+ def initialize(actor, actor_field_name)
+ # Actor that the key is for, either a client or a user.
+ @actor = actor
+
+ unless actor_field_name == "user" || actor_field_name == "client"
+ raise Chef::Exceptions::InvalidKeyArgument, "the second argument to initialize must be either 'user' or 'client'"
+ end
+
+ @actor_field_name = actor_field_name
+
+ @name = nil
+ @public_key = nil
+ @private_key = nil
+ @expiration_date = nil
+ @create_key = nil
+ end
+
+ def chef_rest
+ @rest ||= if @actor_field_name == "user"
+ Chef::REST.new(Chef::Config[:chef_server_root])
+ else
+ Chef::REST.new(Chef::Config[:chef_server_url])
+ end
+ end
+
+ def api_base
+ @api_base ||= if @actor_field_name == "user"
+ "users"
+ else
+ "clients"
+ end
+ end
+
+ def actor(arg=nil)
+ set_or_return(:actor, arg,
+ :regex => /^[a-z0-9\-_]+$/)
+ end
+
+ def name(arg=nil)
+ set_or_return(:name, arg,
+ :kind_of => String)
+ end
+
+ def public_key(arg=nil)
+ raise Chef::Exceptions::InvalidKeyAttribute, "you cannot set the public_key if create_key is true" if !arg.nil? && @create_key
+ set_or_return(:public_key, arg,
+ :kind_of => String)
+ end
+
+ def private_key(arg=nil)
+ set_or_return(:private_key, arg,
+ :kind_of => String)
+ end
+
+ def delete_public_key
+ @public_key = nil
+ end
+
+ def create_key(arg=nil)
+ raise Chef::Exceptions::InvalidKeyAttribute, "you cannot set create_key to true if the public_key field exists" if arg == true && !@public_key.nil?
+ set_or_return(:create_key, arg,
+ :kind_of => [TrueClass, FalseClass])
+ end
+
+ def expiration_date(arg=nil)
+ set_or_return(:expiration_date, arg,
+ :regex => /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z|infinity)$/)
+ end
+
+ def to_hash
+ result = {
+ @actor_field_name => @actor
+ }
+ result["name"] = @name if @name
+ result["public_key"] = @public_key if @public_key
+ result["private_key"] = @private_key if @private_key
+ result["expiration_date"] = @expiration_date if @expiration_date
+ result["create_key"] = @create_key if @create_key
+ result
+ end
+
+ def to_json(*a)
+ Chef::JSONCompat.to_json(to_hash, *a)
+ end
+
+ def create
+ # if public_key is undefined and create_key is false, we cannot create
+ if @public_key.nil? && !@create_key
+ raise Chef::Exceptions::MissingKeyAttribute, "either public_key must be defined or create_key must be true"
+ end
+
+ # defaults the key name to the fingerprint of the key
+ if @name.nil?
+ # if they didn't pass a public_key,
+ #then they must supply a name because we can't generate a fingerprint
+ unless @public_key.nil?
+ @name = fingerprint
+ else
+ raise Chef::Exceptions::MissingKeyAttribute, "a name cannot be auto-generated if no public key passed, either pass a public key or supply a name"
+ end
+ end
+
+ payload = {"name" => @name}
+ payload['public_key'] = @public_key unless @public_key.nil?
+ payload['create_key'] = @create_key if @create_key
+ payload['expiration_date'] = @expiration_date unless @expiration_date.nil?
+ new_key = chef_rest.post_rest("#{api_base}/#{@actor}/keys", payload)
+ Chef::Key.from_hash(new_key)
+ end
+
+ def fingerprint
+ self.class.generate_fingerprint(@public_key)
+ end
+
+ def update
+ if @name.nil?
+ raise Chef::Exceptions::MissingKeyAttribute, "the name field must be populated when update is called"
+ end
+
+ new_key = chef_rest.put_rest("#{api_base}/#{@actor}/keys/#{@name}", to_hash)
+ Chef::Key.from_hash(self.to_hash.merge(new_key))
+ end
+
+ def save
+ create
+ rescue Net::HTTPServerException => e
+ if e.response.code == "409"
+ update
+ else
+ raise e
+ end
+ end
+
+ def destroy
+ if @name.nil?
+ raise Chef::Exceptions::MissingKeyAttribute, "the name field must be populated when delete is called"
+ end
+
+ chef_rest.delete_rest("#{api_base}/#{@actor}/keys/#{@name}")
+ end
+
+ # Class methods
+ def self.from_hash(key_hash)
+ if key_hash.has_key?("user")
+ key = Chef::Key.new(key_hash["user"], "user")
+ else
+ key = Chef::Key.new(key_hash["client"], "client")
+ end
+ key.name key_hash['name'] if key_hash.key?('name')
+ key.public_key key_hash['public_key'] if key_hash.key?('public_key')
+ key.private_key key_hash['private_key'] if key_hash.key?('private_key')
+ key.create_key key_hash['create_key'] if key_hash.key?('create_key')
+ key.expiration_date key_hash['expiration_date'] if key_hash.key?('expiration_date')
+ key
+ end
+
+ def self.from_json(json)
+ Chef::Key.from_hash(Chef::JSONCompat.from_json(json))
+ end
+
+ class << self
+ alias_method :json_create, :from_json
+ end
+
+ def self.list_by_user(actor, inflate=false)
+ keys = Chef::REST.new(Chef::Config[:chef_server_root]).get_rest("users/#{actor}/keys")
+ self.list(keys, actor, :load_by_user, inflate)
+ end
+
+ def self.list_by_client(actor, inflate=false)
+ keys = Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("clients/#{actor}/keys")
+ self.list(keys, actor, :load_by_client, inflate)
+ end
+
+ def self.load_by_user(actor, key_name)
+ response = Chef::REST.new(Chef::Config[:chef_server_root]).get_rest("users/#{actor}/keys/#{key_name}")
+ Chef::Key.from_hash(response)
+ end
+
+ def self.load_by_client(actor, key_name)
+ response = Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("clients/#{actor}/keys/#{key_name}")
+ Chef::Key.from_hash(response)
+ end
+
+ def self.generate_fingerprint(public_key)
+ openssl_key_object = OpenSSL::PKey::RSA.new(public_key)
+ data_string = OpenSSL::ASN1::Sequence([
+ OpenSSL::ASN1::Integer.new(openssl_key_object.public_key.n),
+ OpenSSL::ASN1::Integer.new(openssl_key_object.public_key.e)
+ ])
+ OpenSSL::Digest::SHA1.hexdigest(data_string.to_der).scan(/../).join(':')
+ end
+
+ private
+
+ def self.list(keys, actor, load_method_symbol, inflate)
+ if inflate
+ keys.inject({}) do |key_map, result|
+ name = result["name"]
+ key_map[name] = Chef::Key.send(load_method_symbol, actor, name)
+ key_map
+ end
+ else
+ keys
+ end
+ end
+ end
+end