diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/chef/exceptions.rb | 1 | ||||
-rw-r--r-- | lib/chef/formatters/error_inspectors/api_error_formatting.rb | 15 | ||||
-rw-r--r-- | lib/chef/http/authenticator.rb | 9 | ||||
-rw-r--r-- | lib/chef/knife.rb | 20 | ||||
-rw-r--r-- | lib/chef/knife/user_create.rb | 74 | ||||
-rw-r--r-- | lib/chef/mixin/api_version_request_handling.rb | 53 | ||||
-rw-r--r-- | lib/chef/rest.rb | 1 | ||||
-rw-r--r-- | lib/chef/user.rb | 158 | ||||
-rw-r--r-- | lib/chef/versioned_rest.rb | 27 |
9 files changed, 297 insertions, 61 deletions
diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb index c0f4158db4..197d720776 100644 --- a/lib/chef/exceptions.rb +++ b/lib/chef/exceptions.rb @@ -73,6 +73,7 @@ class Chef class KeyCommandInputError < ArgumentError; end class InvalidKeyArgument < ArgumentError; end class InvalidKeyAttribute < ArgumentError; end + class InvalidUserAttribute < ArgumentError; end class RedirectLimitExceeded < RuntimeError; end class AmbiguousRunlistSpecification < ArgumentError; end class CookbookFrozen < ArgumentError; end diff --git a/lib/chef/formatters/error_inspectors/api_error_formatting.rb b/lib/chef/formatters/error_inspectors/api_error_formatting.rb index ec25f4e903..d68b38e41f 100644 --- a/lib/chef/formatters/error_inspectors/api_error_formatting.rb +++ b/lib/chef/formatters/error_inspectors/api_error_formatting.rb @@ -68,13 +68,16 @@ E end def describe_406_error(error_description, response) - if Chef::JSONCompat.from_json(response.body)["error"] == "invalid-x-ops-server-api-version" - min_version = Chef::JSONCompat.from_json(response.body)["min_version"] - max_version = Chef::JSONCompat.from_json(response.body)["max_version"] + if response["x-ops-server-api-version"] + version_header = Chef::JSONCompat.from_json(response["x-ops-server-api-version"]) + client_api_version = version_header["request_version"] + min_server_version = version_header["min_version"] + max_server_version = version_header["max_version"] + error_description.section("Incompatible server API version:",<<-E) -This version of Chef is not supported by the Chef server you sent this request to -This version of Chef requires a server API version of #{Chef::HTTP::Authenticator::SERVER_API_VERSION} -The Chef server you sent the request to supports a min API version of #{min_version} and a max API version of #{max_version} +This version of the API that this Chef request specified is not supported by the Chef server you sent this request to +The Chef server you sent the request to supports a min API version of #{min_server_version} and a max API version of #{max_server_version} +Chef just made a request with an API version of #{client_api_version} Please either update your Chef client or server to be a compatible set E else diff --git a/lib/chef/http/authenticator.rb b/lib/chef/http/authenticator.rb index 4ec35add34..b5367d4bc5 100644 --- a/lib/chef/http/authenticator.rb +++ b/lib/chef/http/authenticator.rb @@ -24,7 +24,7 @@ class Chef class HTTP class Authenticator - SERVER_API_VERSION = "0" + DEFAULT_SERVER_API_VERSION = "0" attr_reader :signing_key_filename attr_reader :raw_key @@ -39,11 +39,16 @@ class Chef @signing_key_filename = opts[:signing_key_filename] @key = load_signing_key(opts[:signing_key_filename], opts[:raw_key]) @auth_credentials = AuthCredentials.new(opts[:client_name], @key) + if opts[:api_version] + @api_version = opts[:api_version] + else + @api_version = DEFAULT_SERVER_API_VERSION + end end def handle_request(method, url, headers={}, data=false) headers.merge!(authentication_headers(method, url, data)) if sign_requests? - headers.merge!({'X-Ops-Server-API-Version' => SERVER_API_VERSION}) + headers.merge!({'X-Ops-Server-API-Version' => @api_version}) [method, url, headers, data] end diff --git a/lib/chef/knife.rb b/lib/chef/knife.rb index 71215e7fbf..4a93697a1b 100644 --- a/lib/chef/knife.rb +++ b/lib/chef/knife.rb @@ -487,11 +487,13 @@ class Chef ui.error "Service temporarily unavailable" ui.info "Response: #{format_rest_error(response)}" when Net::HTTPNotAcceptable - min_version = Chef::JSONCompat.from_json(response.body)["min_version"] - max_version = Chef::JSONCompat.from_json(response.body)["max_version"] + version_header = Chef::JSONCompat.from_json(response["x-ops-server-api-version"]) + client_api_version = version_header["request_version"] + min_server_version = version_header["min_version"] + max_server_version = version_header["max_version"] ui.error "The version of Chef that Knife is using is not supported by the Chef server you sent this request to" - ui.info "This version of Chef requires a server API version of #{Chef::HTTP::Authenticator::SERVER_API_VERSION}" - ui.info "The Chef server you sent the request to supports a min API verson of #{min_version} and a max API version of #{max_version}" + ui.info "The request that Knife sent was using API version #{client_api_version}" + ui.info "The Chef server you sent the request to supports a min API verson of #{min_server_version} and a max API version of #{max_server_version}" ui.info "Please either update your Chef client or server to be a compatible set" else ui.error response.message @@ -549,6 +551,16 @@ class Chef self.msg("Deleted #{obj_name}") end + # helper method for testing if a field exists + # and returning the usage and proper error if not + def test_mandatory_field(field, fieldname) + if field.nil? + show_usage + ui.fatal("You must specify a #{fieldname}") + exit 1 + end + end + def rest @rest ||= begin require 'chef/rest' diff --git a/lib/chef/knife/user_create.rb b/lib/chef/knife/user_create.rb index 4130f06878..992bbbe3c9 100644 --- a/lib/chef/knife/user_create.rb +++ b/lib/chef/knife/user_create.rb @@ -1,6 +1,7 @@ # -# Author:: Steven Danna (<steve@opscode.com>) -# Copyright:: Copyright (c) 2012 Opscode, Inc. +# Author:: Steven Danna (<steve@chef.io>) +# 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"); @@ -22,6 +23,8 @@ class Chef class Knife class UserCreate < Knife + attr_accessor :user_field + deps do require 'chef/user' require 'chef/json_compat' @@ -30,61 +33,78 @@ class Chef option :file, :short => "-f FILE", :long => "--file FILE", - :description => "Write the private key to a file" + :description => "Write the private key to a file, if returned by the server. A private key will be returned when both --user-key and --no-key are NOT passed. In that case, the server will generate a default key for you and return the private key it creates." option :admin, :short => "-a", :long => "--admin", - :description => "Create the user as an admin", + :description => "Create the user as an admin (only relevant for Open Source Chef Server 11).", :boolean => true - option :user_password, - :short => "-p PASSWORD", - :long => "--password PASSWORD", - :description => "Password for newly created user", - :default => "" - option :user_key, :long => "--user-key FILENAME", - :description => "Public key for newly created user. By default a key will be created for you." + :description => "Public key for newly created user. Path to a public key you provide instead of having the server generate one. If --user-key is not passed, the server will create a 'default' key for you, unless you passed --no-key. Note that --user-key cannot be passed with --no-key." + + option :no_key, + :long => "--no-key", + :description => "Do not create a 'default' public key for this new user. This prevents the server generating a public key by default. Cannot be passed with --user-key (requires server API version 1)." + + banner "knife user create USERNAME DISPLAY_NAME FIRST_NAME LAST_NAME EMAIL PASSWORD (options)" - banner "knife user create USER (options)" + def user + @user_field ||= Chef::User.new + end + + def create_user_from_hash(hash) + Chef::User.from_hash(hash).create + end def run - @user_name = @name_args[0] + test_mandatory_field(@name_args[0], "username") + user.username @name_args[0] - if @user_name.nil? - show_usage - ui.fatal("You must specify a user name") - exit 1 - end + test_mandatory_field(@name_args[1], "display name") + user.display_name @name_args[1] + + test_mandatory_field(@name_args[2], "first name") + user.first_name @name_args[2] + + test_mandatory_field(@name_args[3], "last name") + user.last_name @name_args[3] - if config[:user_password].length == 0 + test_mandatory_field(@name_args[4], "email") + user.email @name_args[4] + + test_mandatory_field(@name_args[5], "password") + user.password @name_args[5] + + if config[:user_key] && config[:no_key] show_usage - ui.fatal("You must specify a non-blank password") + ui.fatal("You cannot pass --user-key and --no-key") exit 1 end - user = Chef::User.new - user.name(@user_name) user.admin(config[:admin]) - user.password config[:user_password] + + unless config[:no_key] + user.create_key(true) + end if config[:user_key] user.public_key File.read(File.expand_path(config[:user_key])) end output = edit_data(user) - user = Chef::User.from_hash(output).create + final_user = create_user_from_hash(output) ui.info("Created #{user}") - if user.private_key + if final_user.private_key if config[:file] File.open(config[:file], "w") do |f| - f.print(user.private_key) + f.print(final_user.private_key) end else - ui.msg user.private_key + ui.msg final_user.private_key end end end diff --git a/lib/chef/mixin/api_version_request_handling.rb b/lib/chef/mixin/api_version_request_handling.rb new file mode 100644 index 0000000000..ea68afc6af --- /dev/null +++ b/lib/chef/mixin/api_version_request_handling.rb @@ -0,0 +1,53 @@ +# +# Author:: Tyler Cloke (tyler@chef.io) +# Copyright:: Copyright 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. +# + +class Chef + module ApiVersionRequestHandling + # takes in an http exception, and a min and max supported API version and + # handles all the versioning cases + # + # it will raise an exception if there was a non-versioning related error + # or the server and the client are not compatible + # + # if the server does not support versioning, then it will not raise, and you + # can assume API v0 is safe to send + def handle_version_http_exception(exception, min_client_supported_version, max_client_supported_version) + + # only rescue 406 Unacceptable + return false unless exception.response.code == "406" || !exception.response["x-ops-server-api-version"] + + exception.response["x-ops-server-api-version"] + + # if the version header doesn't exist, just assume API v0 + if exception.response["x-ops-server-api-version"] + header = Chef::JSONCompat.from_json(exception.response["x-ops-server-api-version"]) + min_server_version = Integer(header["min_version"]) + max_server_version = Integer(header["max_version"]) + + # if the min API version the server supports is greater than the min version the client supports + # and the max API version the server supports is less than the max version the client supports + if min_server_version > min_client_supported_version || max_server_version < max_client_supported_version + # if it had x-ops-server-api-version header, it will error out properly, just raise it + return false + end + end + true + end + + end +end diff --git a/lib/chef/rest.rb b/lib/chef/rest.rb index 2612714a19..f87cec9b76 100644 --- a/lib/chef/rest.rb +++ b/lib/chef/rest.rb @@ -64,6 +64,7 @@ class Chef options = options.dup options[:client_name] = client_name options[:signing_key_filename] = signing_key_filename + super(url, options) @decompressor = Decompressor.new(options) diff --git a/lib/chef/user.rb b/lib/chef/user.rb index 42fa6b5fa1..4e0d12e3b5 100644 --- a/lib/chef/user.rb +++ b/lib/chef/user.rb @@ -21,31 +21,96 @@ require 'chef/mixin/from_file' require 'chef/mash' require 'chef/json_compat' require 'chef/search/query' +require 'chef/versioned_rest' +require 'chef/mixin/api_version_request_handling' +require 'chef/exceptions' class Chef class User include Chef::Mixin::FromFile include Chef::Mixin::ParamsValidate + include Chef::VersionedRest + include Chef::ApiVersionRequestHandling + + SUPPORTED_API_VERSIONS = [0,1] def initialize - @name = '' + @username = nil + @display_name = nil + @first_name = nil + @middle_name = nil + @last_name = nil + @email = nil + @password = nil @public_key = nil @private_key = nil + @create_key = nil @password = nil @admin = false end - def name(arg=nil) - set_or_return(:name, arg, + def chef_rest_v0 + @chef_rest_v0 ||= get_versioned_rest_object(Chef::Config[:chef_server_url], "0") + end + + def chef_rest_v1 + @chef_rest_v1 ||= get_versioned_rest_object(Chef::Config[:chef_server_url], "1") + end + + def chef_root_rest_v0 + @chef_root_rest_v0 ||= get_versioned_rest_object(Chef::Config[:chef_server_root], "0") + end + + def chef_root_rest_v1 + @chef_root_rest_v1 ||= get_versioned_rest_object(Chef::Config[:chef_server_root], "1") + end + + def username(arg=nil) + set_or_return(:username, arg, :regex => /^[a-z0-9\-_]+$/) end + def display_name(arg=nil) + set_or_return(:display_name, + arg, :kind_of => String) + end + + def first_name(arg=nil) + set_or_return(:first_name, + arg, :kind_of => String) + end + + def middle_name(arg=nil) + set_or_return(:middle_name, + arg, :kind_of => String) + end + + def last_name(arg=nil) + set_or_return(:last_name, + arg, :kind_of => String) + end + + def email(arg=nil) + set_or_return(:email, + arg, :kind_of => String) + end + + def password(arg=nil) + set_or_return(:password, + arg, :kind_of => String) + end + def admin(arg=nil) set_or_return(:admin, arg, :kind_of => [TrueClass, FalseClass]) end + def create_key(arg=nil) + set_or_return(:create_key, arg, + :kind_of => [TrueClass, FalseClass]) + end + def public_key(arg=nil) set_or_return(:public_key, arg, :kind_of => String) @@ -63,12 +128,17 @@ class Chef def to_hash result = { - "name" => @name, - "public_key" => @public_key, + "username" => @username, "admin" => @admin } - result["private_key"] = @private_key if @private_key + result["display_name"] = @display_name if @display_name + result["first_name"] = @first_name if @first_name + result["middle_name"] = @middle_name if @middle_name + result["last_name"] = @last_name if @last_name + result["email"] = @email if @email result["password"] = @password if @password + result["public_key"] = @public_key if @public_key + result["private_key"] = @private_key if @private_key result end @@ -77,21 +147,59 @@ class Chef end def destroy - Chef::REST.new(Chef::Config[:chef_server_url]).delete_rest("users/#{@name}") + Chef::REST.new(Chef::Config[:chef_server_url]).delete("users/#{@username}") end def create - payload = {:name => self.name, :admin => self.admin, :password => self.password } - payload[:public_key] = public_key if public_key - new_user =Chef::REST.new(Chef::Config[:chef_server_url]).post_rest("users", payload) + # try v1, fail back to v0 if v1 not supported + begin + payload = { + :username => @username, + :display_name => @display_name, + :first_name => @first_name, + :last_name => @last_name, + :email => @email, + :password => @password, + :admin => @admin + } + payload[:public_key] = @public_key if @public_key + payload[:create_key] = @create_key if @create_key + payload[:middle_name] = @middle_name if @middle_name + raise Chef::Exceptions::InvalidUserAttribute, "You cannot set both public_key and create_key for create." if @create_key && @public_key + new_user = chef_root_rest_v1.post("users", payload) + + # get the private_key out of the chef_key hash if it exists + if new_user['chef_key'] + if new_user['chef_key']['private_key'] + new_user['private_key'] = new_user['chef_key']['private_key'] + end + new_user['public_key'] = new_user['chef_key']['public_key'] + new_user.delete('chef_key') + end + rescue Net::HTTPServerException => e + raise e unless handle_version_http_exception(e, SUPPORTED_API_VERSIONS[0], SUPPORTED_API_VERSIONS[-1]) + payload = { + :username => @username, + :display_name => @display_name, + :first_name => @first_name, + :last_name => @last_name, + :email => @email, + :password => @password, + :admin => @admin + } + payload[:middle_name] = @middle_name if @middle_name + payload[:public_key] = @public_key if @public_key + new_user = chef_root_rest_v0.post("users", payload) + end + Chef::User.from_hash(self.to_hash.merge(new_user)) end def update(new_key=false) - payload = {:name => name, :admin => admin} + payload = {:username => username, :admin => admin} payload[:private_key] = new_key if new_key payload[:password] = password if password - updated_user = Chef::REST.new(Chef::Config[:chef_server_url]).put_rest("users/#{name}", payload) + updated_user = Chef::REST.new(Chef::Config[:chef_server_url]).put("users/#{username}", payload) Chef::User.from_hash(self.to_hash.merge(updated_user)) end @@ -109,17 +217,17 @@ class Chef def reregister r = Chef::REST.new(Chef::Config[:chef_server_url]) - reregistered_self = r.put_rest("users/#{name}", { :name => name, :admin => admin, :private_key => true }) + reregistered_self = r.put("users/#{username}", { :username => username, :admin => admin, :private_key => true }) private_key(reregistered_self["private_key"]) self end def to_s - "user[#{@name}]" + "user[#{@username}]" end def inspect - "Chef::User name:'#{name}' admin:'#{admin.inspect}'" + + "Chef::User username:'#{username}' admin:'#{admin.inspect}'" + "public_key:'#{public_key}' private_key:#{private_key}" end @@ -127,11 +235,17 @@ class Chef def self.from_hash(user_hash) user = Chef::User.new - user.name user_hash['name'] - user.private_key user_hash['private_key'] if user_hash.key?('private_key') - user.password user_hash['password'] if user_hash.key?('password') - user.public_key user_hash['public_key'] + user.username user_hash['username'] user.admin user_hash['admin'] + user.display_name user_hash['display_name'] if user_hash.key?('display_name') + user.first_name user_hash['first_name'] if user_hash.key?('first_name') + user.middle_name user_hash['middle_name'] if user_hash.key?('middle_name') + user.last_name user_hash['last_name'] if user_hash.key?('last_name') + user.email user_hash['email'] if user_hash.key?('email') + user.password user_hash['password'] if user_hash.key?('password') + user.public_key user_hash['public_key'] if user_hash.key?('public_key') + user.private_key user_hash['private_key'] if user_hash.key?('private_key') + user.create_key user_hash['create_key'] if user_hash.key?('create_key') user end @@ -144,7 +258,7 @@ class Chef end def self.list(inflate=false) - response = Chef::REST.new(Chef::Config[:chef_server_url]).get_rest('users') + response = Chef::REST.new(Chef::Config[:chef_server_url]).get('users') users = if response.is_a?(Array) transform_ohc_list_response(response) # OHC/OPC else @@ -160,8 +274,8 @@ class Chef end end - def self.load(name) - response = Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("users/#{name}") + def self.load(username) + response = Chef::REST.new(Chef::Config[:chef_server_url]).get("users/#{username}") Chef::User.from_hash(response) end diff --git a/lib/chef/versioned_rest.rb b/lib/chef/versioned_rest.rb new file mode 100644 index 0000000000..b37a7e2249 --- /dev/null +++ b/lib/chef/versioned_rest.rb @@ -0,0 +1,27 @@ +# +# Author:: Tyler Cloke (tyler@chef.io) +# Copyright:: Copyright 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. +# + +class Chef + module VersionedRest + # Helper for getting a sane interface to passing an API version to Chef::REST + # api_version should be a string of an integer + def get_versioned_rest_object(url, api_version) + Chef::REST.new(url, Chef::Config[:node_name], Chef::Config[:client_key], {:api_version => api_version}) + end + end +end |