diff options
author | Tyler Cloke <tylercloke@gmail.com> | 2015-06-05 15:16:01 -0700 |
---|---|---|
committer | Tyler Cloke <tylercloke@gmail.com> | 2015-06-05 15:16:01 -0700 |
commit | eb590e371b5e5d3f4d1d201dd3f58e4351116f41 (patch) | |
tree | 04c47c21618a4889b79d2eaf78eebd457d652ff4 | |
parent | 20f7e1c78c55d2a16d5033bc2bbe5904d225adb0 (diff) | |
parent | fd18d4bdfd79a269a3220936efd2ec8f69353c29 (diff) | |
download | chef-eb590e371b5e5d3f4d1d201dd3f58e4351116f41.tar.gz |
Merge pull request #3438 from chef/tc/api-versioning
API V1 Support
47 files changed, 3346 insertions, 447 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index f21f2e1710..6c0bbb0677 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ Allow knife sub-command loader to match platform specific gems. [pr#3281](https://github.com/chef/chef/pull/3281) * [**Steve Lowe**](https://github.com/SteveLowe): Fix copying ntfs dacl and sacl when they are nil. [pr#3066](https://github.com/chef/chef/pull/3066) +* [Issue #3010](https://github.com/chef/chef/issues/3010) Fixed `knife user` for use with current and future versions of Chef Server 12, with continued backwards compatible support for use with Open Source Server 11. +* [pr#3438](https://github.com/chef/chef/pull/3438) Server API V1 support. Vast improvements to and testing expansion for Chef::User, Chef::ApiClient, and related knife commands. Deprecated Open Source Server 11 user support to the Chef::OscUser and knife osc_user namespace, but with backwards compatible support via knife user. * [Issue #2247](https://github.com/chef/chef/issues/2247): `powershell_script` returns 0 for scripts with syntax errors * [pr#3080](https://github.com/chef/chef/pull/3080): Issue 2247: `powershell_script` exit status should be nonzero for syntax errors diff --git a/lib/chef/api_client.rb b/lib/chef/api_client.rb index ce9ceb312c..ad31fb7d7b 100644 --- a/lib/chef/api_client.rb +++ b/lib/chef/api_client.rb @@ -1,7 +1,7 @@ # -# Author:: Adam Jacob (<adam@opscode.com>) -# Author:: Nuo Yan (<nuo@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Author:: Adam Jacob (<adam@chef.io>) +# Author:: Nuo Yan (<nuo@chef.io>) +# Copyright:: Copyright (c) 2008, 2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,12 +23,18 @@ require 'chef/mixin/from_file' require 'chef/mash' require 'chef/json_compat' require 'chef/search/query' +require 'chef/exceptions' +require 'chef/mixin/api_version_request_handling' +require 'chef/server_api' class Chef class ApiClient include Chef::Mixin::FromFile include Chef::Mixin::ParamsValidate + include Chef::Mixin::ApiVersionRequestHandling + + SUPPORTED_API_VERSIONS = [0,1] # Create a new Chef::ApiClient object. def initialize @@ -37,6 +43,25 @@ class Chef @private_key = nil @admin = false @validator = false + @create_key = nil + end + + def chef_rest_v0 + @chef_rest_v0 ||= Chef::ServerAPI.new(Chef::Config[:chef_server_url], {:api_version => "0"}) + end + + def chef_rest_v1 + @chef_rest_v1 ||= Chef::ServerAPI.new(Chef::Config[:chef_server_url], {:api_version => "1"}) + end + + # will default to the current version (Chef::Authenticator::DEFAULT_SERVER_API_VERSION) + def http_api + @http_api ||= Chef::REST.new(Chef::Config[:chef_server_url]) + end + + # will default to the current version (Chef::Authenticator::DEFAULT_SERVER_API_VERSION) + def self.http_api + Chef::REST.new(Chef::Config[:chef_server_url]) end # Gets or sets the client name. @@ -88,7 +113,8 @@ class Chef ) end - # Gets or sets the private key. + # Private key. The server will return it as a string. + # Set to true under API V0 to have the server regenerate the default key. # # @params [Optional String] The string representation of the private key. # @return [String] The current value. @@ -96,7 +122,19 @@ class Chef set_or_return( :private_key, arg, - :kind_of => [String, FalseClass] + :kind_of => [String, TrueClass, FalseClass] + ) + end + + # Used to ask server to generate key pair under api V1 + # + # @params [Optional True/False] Should be true or false - default is false. + # @return [True/False] The current value + def create_key(arg=nil) + set_or_return( + :create_key, + arg, + :kind_of => [ TrueClass, FalseClass ] ) end @@ -107,13 +145,14 @@ class Chef def to_hash result = { "name" => @name, - "public_key" => @public_key, "validator" => @validator, "admin" => @admin, 'json_class' => self.class.name, "chef_type" => "client" } - result["private_key"] = @private_key if @private_key + result["private_key"] = @private_key unless @private_key.nil? + result["public_key"] = @public_key unless @public_key.nil? + result["create_key"] = @create_key unless @create_key.nil? result end @@ -127,10 +166,11 @@ class Chef def self.from_hash(o) client = Chef::ApiClient.new client.name(o["name"] || o["clientname"]) - client.private_key(o["private_key"]) if o.key?("private_key") - client.public_key(o["public_key"]) client.admin(o["admin"]) client.validator(o["validator"]) + client.private_key(o["private_key"]) if o.key?("private_key") + client.public_key(o["public_key"]) if o.key?("public_key") + client.create_key(o["create_key"]) if o.key?("create_key") client end @@ -142,10 +182,6 @@ class Chef from_hash(Chef::JSONCompat.parse(j)) end - def self.http_api - Chef::REST.new(Chef::Config[:chef_server_url]) - end - def self.reregister(name) api_client = load(name) api_client.reregister @@ -182,11 +218,11 @@ class Chef # Save this client via the REST API, returns a hash including the private key def save begin - http_api.put("clients/#{name}", { :name => self.name, :admin => self.admin, :validator => self.validator}) + update rescue Net::HTTPServerException => e # If that fails, go ahead and try and update it if e.response.code == "404" - http_api.post("clients", {:name => self.name, :admin => self.admin, :validator => self.validator }) + create else raise e end @@ -194,18 +230,95 @@ class Chef end def reregister - reregistered_self = http_api.put("clients/#{name}", { :name => name, :admin => admin, :validator => validator, :private_key => true }) + # Try API V0 and if it fails due to V0 not being supported, raise the proper error message. + # reregister only supported in API V0 or lesser. + reregistered_self = chef_rest_v0.put("clients/#{name}", { :name => name, :admin => admin, :validator => validator, :private_key => true }) if reregistered_self.respond_to?(:[]) private_key(reregistered_self["private_key"]) else private_key(reregistered_self.private_key) end self + rescue Net::HTTPServerException => e + # if there was a 406 related to versioning, give error explaining that + # only API version 0 is supported for reregister command + if e.response.code == "406" && e.response["x-ops-server-api-version"] + version_header = Chef::JSONCompat.from_json(e.response["x-ops-server-api-version"]) + min_version = version_header["min_version"] + max_version = version_header["max_version"] + error_msg = reregister_only_v0_supported_error_msg(max_version, min_version) + raise Chef::Exceptions::OnlyApiVersion0SupportedForAction.new(error_msg) + else + raise e + end + end + + # Updates the client via the REST API + def update + # NOTE: API V1 dropped support for updating client keys via update (aka PUT), + # but this code never supported key updating in the first place. Since + # it was never implemented, we will simply ignore that functionality + # as it is being deprecated. + # Delete this comment after V0 support is dropped. + payload = { :name => name } + payload[:validator] = validator unless validator.nil? + + # DEPRECATION + # This field is ignored in API V1, but left for backwards-compat, + # can remove after API V0 is no longer supported. + payload[:admin] = admin unless admin.nil? + + begin + new_client = chef_rest_v1.put("clients/#{name}", payload) + rescue Net::HTTPServerException => e + # rescue API V0 if 406 and the server supports V0 + supported_versions = server_client_api_version_intersection(e, SUPPORTED_API_VERSIONS) + raise e unless supported_versions && supported_versions.include?(0) + new_client = chef_rest_v0.put("clients/#{name}", payload) + end + + new_client end # Create the client via the REST API def create - http_api.post("clients", self) + payload = { + :name => name, + :validator => validator, + # this field is ignored in API V1, but left for backwards-compat, + # can remove after OSC 11 support is finished? + :admin => admin + } + begin + # try API V1 + raise Chef::Exceptions::InvalidClientAttribute, "You cannot set both public_key and create_key for create." if !create_key.nil? && !public_key.nil? + + payload[:public_key] = public_key unless public_key.nil? + payload[:create_key] = create_key unless create_key.nil? + + new_client = chef_rest_v1.post("clients", payload) + + # get the private_key out of the chef_key hash if it exists + if new_client['chef_key'] + if new_client['chef_key']['private_key'] + new_client['private_key'] = new_client['chef_key']['private_key'] + end + new_client['public_key'] = new_client['chef_key']['public_key'] + new_client.delete('chef_key') + end + + rescue Net::HTTPServerException => e + # rescue API V0 if 406 and the server supports V0 + supported_versions = server_client_api_version_intersection(e, SUPPORTED_API_VERSIONS) + raise e unless supported_versions && supported_versions.include?(0) + + # under API V0, a key pair will always be created unless public_key is + # passed on initial POST + payload[:public_key] = public_key unless public_key.nil? + + new_client = chef_rest_v0.post("clients", payload) + end + Chef::ApiClient.from_hash(self.to_hash.merge(new_client)) end # As a string @@ -213,14 +326,5 @@ class Chef "client[#{@name}]" end - def inspect - "Chef::ApiClient name:'#{name}' admin:'#{admin.inspect}' validator:'#{validator}' " + - "public_key:'#{public_key}' private_key:'#{private_key}'" - end - - def http_api - @http_api ||= Chef::REST.new(Chef::Config[:chef_server_url]) - end - end end diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb index c0f4158db4..dd0bac3cf9 100644 --- a/lib/chef/exceptions.rb +++ b/lib/chef/exceptions.rb @@ -73,10 +73,13 @@ class Chef class KeyCommandInputError < ArgumentError; end class InvalidKeyArgument < ArgumentError; end class InvalidKeyAttribute < ArgumentError; end + class InvalidUserAttribute < ArgumentError; end + class InvalidClientAttribute < ArgumentError; end class RedirectLimitExceeded < RuntimeError; end class AmbiguousRunlistSpecification < ArgumentError; end class CookbookFrozen < ArgumentError; end class CookbookNotFound < RuntimeError; end + class OnlyApiVersion0SupportedForAction < RuntimeError; end # Cookbook loader used to raise an argument error when cookbook not found. # for back compat, need to raise an error that inherits from ArgumentError class CookbookNotFoundInRepo < 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..05ee3132a7 100644 --- a/lib/chef/formatters/error_inspectors/api_error_formatting.rb +++ b/lib/chef/formatters/error_inspectors/api_error_formatting.rb @@ -68,14 +68,17 @@ 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} -Please either update your Chef client or server to be a compatible set +This version of the API that this Chef request specified is not supported by the Chef server you sent this request to. +The server 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 describe_http_error(error_description) diff --git a/lib/chef/http/authenticator.rb b/lib/chef/http/authenticator.rb index 4ec35add34..bffa9c4b3a 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 = "1" 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/client_create.rb b/lib/chef/knife/client_create.rb index 477a400e8a..570c1ee950 100644 --- a/lib/chef/knife/client_create.rb +++ b/lib/chef/knife/client_create.rb @@ -28,58 +28,82 @@ class Chef end option :file, - :short => "-f FILE", - :long => "--file FILE", - :description => "Write the key to a file" + :short => "-f FILE", + :long => "--file FILE", + :description => "Write the private key to a file if the server generated one." option :admin, - :short => "-a", - :long => "--admin", - :description => "Create the client as an admin", - :boolean => true + :short => "-a", + :long => "--admin", + :description => "Open Source Chef 11 only. Create the client as an admin.", + :boolean => true option :validator, - :long => "--validator", - :description => "Create the client as a validator", - :boolean => true + :long => "--validator", + :description => "Create the client as a validator.", + :boolean => true - banner "knife client create CLIENT (options)" + option :public_key, + :short => "-p FILE", + :long => "--public-key", + :description => "Set the initial default key for the client from a file on disk (cannot pass with --prevent-keygen)." + + option :prevent_keygen, + :short => "-k", + :long => "--prevent-keygen", + :description => "API V1 only. Prevent server from generating a default key pair for you. Cannot be passed with --public-key.", + :boolean => true + + banner "knife client create CLIENTNAME (options)" + + def client + @client_field ||= Chef::ApiClient.new + end + + def create_client(client) + # should not be using save :( bad behavior + client.save + end def run - @client_name = @name_args[0] + test_mandatory_field(@name_args[0], "client name") + client.name @name_args[0] - if @client_name.nil? + if config[:public_key] && config[:prevent_keygen] show_usage - ui.fatal("You must specify a client name") + ui.fatal("You cannot pass --public-key and --prevent-keygen") exit 1 end - client_hash = { - "name" => @client_name, - "admin" => !!config[:admin], - "validator" => !!config[:validator] - } + if !config[:prevent_keygen] && !config[:public_key] + client.create_key(true) + end + + if config[:admin] + client.admin(true) + end - output = Chef::ApiClient.from_hash(edit_hash(client_hash)) + if config[:validator] + client.validator(true) + end - # Chef::ApiClient.save will try to create a client and if it - # exists will update it instead silently. - client = output.save + if config[:public_key] + client.public_key File.read(File.expand_path(config[:public_key])) + end - # We only get a private_key on client creation, not on client update. - if client['private_key'] - ui.info("Created #{output}") + output = edit_data(client) + final_client = create_client(output) + ui.info("Created #{output}") + # output private_key if one + if final_client.private_key if config[:file] File.open(config[:file], "w") do |f| - f.print(client['private_key']) + f.print(final_client.private_key) end else - puts client['private_key'] + puts final_client.private_key end - else - ui.error "Client '#{client['name']}' already exists" - exit 1 end end end diff --git a/lib/chef/knife/osc_user_create.rb b/lib/chef/knife/osc_user_create.rb new file mode 100644 index 0000000000..c368296040 --- /dev/null +++ b/lib/chef/knife/osc_user_create.rb @@ -0,0 +1,97 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2012 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/knife' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_create.rb. +class Chef + class Knife + class OscUserCreate < Knife + + deps do + require 'chef/osc_user' + require 'chef/json_compat' + end + + option :file, + :short => "-f FILE", + :long => "--file FILE", + :description => "Write the private key to a file" + + option :admin, + :short => "-a", + :long => "--admin", + :description => "Create the user as an admin", + :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." + + banner "knife osc_user create USER (options)" + + def run + @user_name = @name_args[0] + + if @user_name.nil? + show_usage + ui.fatal("You must specify a user name") + exit 1 + end + + if config[:user_password].length == 0 + show_usage + ui.fatal("You must specify a non-blank password") + exit 1 + end + + user = Chef::OscUser.new + user.name(@user_name) + user.admin(config[:admin]) + user.password config[:user_password] + + if config[:user_key] + user.public_key File.read(File.expand_path(config[:user_key])) + end + + output = edit_data(user) + user = Chef::OscUser.from_hash(output).create + + ui.info("Created #{user}") + if user.private_key + if config[:file] + File.open(config[:file], "w") do |f| + f.print(user.private_key) + end + else + ui.msg user.private_key + end + end + end + end + end +end diff --git a/lib/chef/knife/osc_user_delete.rb b/lib/chef/knife/osc_user_delete.rb new file mode 100644 index 0000000000..d6fbd4a6a9 --- /dev/null +++ b/lib/chef/knife/osc_user_delete.rb @@ -0,0 +1,51 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2012 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/knife' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in the user_delete.rb. + +class Chef + class Knife + class OscUserDelete < Knife + + deps do + require 'chef/osc_user' + require 'chef/json_compat' + end + + banner "knife osc_user delete USER (options)" + + def run + @user_name = @name_args[0] + + if @user_name.nil? + show_usage + ui.fatal("You must specify a user name") + exit 1 + end + + delete_object(Chef::OscUser, @user_name) + end + + end + end +end diff --git a/lib/chef/knife/osc_user_edit.rb b/lib/chef/knife/osc_user_edit.rb new file mode 100644 index 0000000000..4c38674d08 --- /dev/null +++ b/lib/chef/knife/osc_user_edit.rb @@ -0,0 +1,58 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2012 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/knife' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_edit.rb. + +class Chef + class Knife + class OscUserEdit < Knife + + deps do + require 'chef/osc_user' + require 'chef/json_compat' + end + + banner "knife osc_user edit USER (options)" + + def run + @user_name = @name_args[0] + + if @user_name.nil? + show_usage + ui.fatal("You must specify a user name") + exit 1 + end + + original_user = Chef::OscUser.load(@user_name).to_hash + edited_user = edit_data(original_user) + if original_user != edited_user + user = Chef::OscUser.from_hash(edited_user) + user.update + ui.msg("Saved #{user}.") + else + ui.msg("User unchaged, not saving.") + end + end + end + end +end diff --git a/lib/chef/knife/osc_user_list.rb b/lib/chef/knife/osc_user_list.rb new file mode 100644 index 0000000000..92f049cd19 --- /dev/null +++ b/lib/chef/knife/osc_user_list.rb @@ -0,0 +1,47 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2012 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/knife' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_list.rb. + +class Chef + class Knife + class OscUserList < Knife + + deps do + require 'chef/osc_user' + require 'chef/json_compat' + end + + banner "knife osc_user list (options)" + + option :with_uri, + :short => "-w", + :long => "--with-uri", + :description => "Show corresponding URIs" + + def run + output(format_list_for_display(Chef::OscUser.list)) + end + end + end +end diff --git a/lib/chef/knife/osc_user_reregister.rb b/lib/chef/knife/osc_user_reregister.rb new file mode 100644 index 0000000000..a71e0aa677 --- /dev/null +++ b/lib/chef/knife/osc_user_reregister.rb @@ -0,0 +1,64 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2012 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/knife' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_reregister.rb. + +class Chef + class Knife + class OscUserReregister < Knife + + deps do + require 'chef/osc_user' + require 'chef/json_compat' + end + + banner "knife osc_user reregister USER (options)" + + option :file, + :short => "-f FILE", + :long => "--file FILE", + :description => "Write the private key to a file" + + def run + @user_name = @name_args[0] + + if @user_name.nil? + show_usage + ui.fatal("You must specify a user name") + exit 1 + end + + user = Chef::OscUser.load(@user_name).reregister + Chef::Log.debug("Updated user data: #{user.inspect}") + key = user.private_key + if config[:file] + File.open(config[:file], "w") do |f| + f.print(key) + end + else + ui.msg key + end + end + end + end +end diff --git a/lib/chef/knife/osc_user_show.rb b/lib/chef/knife/osc_user_show.rb new file mode 100644 index 0000000000..6a41ddae88 --- /dev/null +++ b/lib/chef/knife/osc_user_show.rb @@ -0,0 +1,54 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2009 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/knife' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_show.rb. + +class Chef + class Knife + class OscUserShow < Knife + + include Knife::Core::MultiAttributeReturnOption + + deps do + require 'chef/osc_user' + require 'chef/json_compat' + end + + banner "knife osc_user show USER (options)" + + def run + @user_name = @name_args[0] + + if @user_name.nil? + show_usage + ui.fatal("You must specify a user name") + exit 1 + end + + user = Chef::OscUser.load(@user_name) + output(format_for_display(user)) + end + + end + end +end diff --git a/lib/chef/knife/user_create.rb b/lib/chef/knife/user_create.rb index 4130f06878..e73f6be8b6 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) 2012, 2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,11 +18,14 @@ # require 'chef/knife' +require 'chef/knife/osc_user_create' class Chef class Knife class UserCreate < Knife + attr_accessor :user_field + deps do require 'chef/user' require 'chef/json_compat' @@ -30,63 +34,118 @@ 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 the server generated one." + + option :user_key, + :long => "--user-key FILENAME", + :description => "Set the initial default key for the user from a file on disk (cannot pass with --prevent-keygen)." + + option :prevent_keygen, + :short => "-k", + :long => "--prevent-keygen", + :description => "API V1 only. Prevent server from generating a default key pair for you. Cannot be passed with --user-key.", + :boolean => true option :admin, :short => "-a", :long => "--admin", - :description => "Create the user as an admin", + :description => "DEPRECATED: Open Source Chef 11 only. Create the user as an admin.", :boolean => true option :user_password, :short => "-p PASSWORD", :long => "--password PASSWORD", - :description => "Password for newly created user", + :description => "DEPRECATED: Open Source Chef 11 only. 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." + banner "knife user create USERNAME DISPLAY_NAME FIRST_NAME LAST_NAME EMAIL PASSWORD (options)" + + def user + @user_field ||= Chef::User.new + end + + def create_user_from_hash(hash) + Chef::User.from_hash(hash).create + end + + def osc_11_warning +<<-EOF +IF YOU ARE USING CHEF SERVER 12+, PLEASE FOLLOW THE INSTRUCTIONS UNDER knife user create --help. +You only passed a single argument to knife user create. +For backwards compatibility, when only a single argument is passed, +knife user create assumes you want Open Source 11 Server user creation. +knife user create for Open Source 11 Server is being deprecated. +Open Source 11 Server user commands now live under the knife osc_user namespace. +For backwards compatibility, we will forward this request to knife osc_user create. +If you are using an Open Source 11 Server, please use that command to avoid this warning. +EOF + end - banner "knife user create USER (options)" + def run_osc_11_user_create + # run osc_user_create with our input + ARGV.delete("user") + ARGV.unshift("osc_user") + Chef::Knife.run(ARGV, Chef::Application::Knife.options) + end def run - @user_name = @name_args[0] + # DEPRECATION NOTE + # Remove this if statement and corrosponding code post OSC 11 support. + # + # If only 1 arg is passed, assume OSC 11 case. + if @name_args.length == 1 + ui.warn(osc_11_warning) + run_osc_11_user_create + else # EC / CS 12 user create - if @user_name.nil? - show_usage - ui.fatal("You must specify a user name") - exit 1 - end + test_mandatory_field(@name_args[0], "username") + user.username @name_args[0] - if config[:user_password].length == 0 - show_usage - ui.fatal("You must specify a non-blank password") - exit 1 - end + test_mandatory_field(@name_args[1], "display name") + user.display_name @name_args[1] - user = Chef::User.new - user.name(@user_name) - user.admin(config[:admin]) - user.password config[:user_password] + test_mandatory_field(@name_args[2], "first name") + user.first_name @name_args[2] - if config[:user_key] - user.public_key File.read(File.expand_path(config[:user_key])) - end + test_mandatory_field(@name_args[3], "last name") + user.last_name @name_args[3] + + 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[:prevent_keygen] + show_usage + ui.fatal("You cannot pass --user-key and --prevent-keygen") + exit 1 + end + + if !config[:prevent_keygen] && !config[:user_key] + user.create_key(true) + end - output = edit_data(user) - user = Chef::User.from_hash(output).create + if config[:user_key] + user.public_key File.read(File.expand_path(config[:user_key])) + end - ui.info("Created #{user}") - if user.private_key - if config[:file] - File.open(config[:file], "w") do |f| - f.print(user.private_key) + output = edit_data(user) + final_user = create_user_from_hash(output) + + ui.info("Created #{user}") + if final_user.private_key + if config[:file] + File.open(config[:file], "w") do |f| + f.print(final_user.private_key) + end + else + ui.msg final_user.private_key end - else - ui.msg user.private_key end end + + end end end diff --git a/lib/chef/knife/user_delete.rb b/lib/chef/knife/user_delete.rb index b7af11bec8..803be6b90c 100644 --- a/lib/chef/knife/user_delete.rb +++ b/lib/chef/knife/user_delete.rb @@ -29,6 +29,40 @@ class Chef banner "knife user delete USER (options)" + def osc_11_warning +<<-EOF +The Chef Server you are using does not support the username field. +This means it is an Open Source 11 Server. +knife user delete for Open Source 11 Server is being deprecated. +Open Source 11 Server user commands now live under the knife osc_user namespace. +For backwards compatibility, we will forward this request to knife osc_user delete. +If you are using an Open Source 11 Server, please use that command to avoid this warning. +EOF + end + + def run_osc_11_user_delete + # run osc_user_delete with our input + ARGV.delete("user") + ARGV.unshift("osc_user") + Chef::Knife.run(ARGV, Chef::Application::Knife.options) + end + + # DEPRECATION NOTE + # Delete this override method after OSC 11 support is dropped + def delete_object(user_name) + confirm("Do you really want to delete #{user_name}") + + if Kernel.block_given? + object = block.call + else + object = Chef::User.load(user_name) + object.destroy + end + + output(format_for_display(object)) if config[:print_after] + self.msg("Deleted #{user_name}") + end + def run @user_name = @name_args[0] @@ -38,9 +72,25 @@ class Chef exit 1 end - delete_object(Chef::User, @user_name) - end + # DEPRECATION NOTE + # + # Below is modification of Chef::Knife.delete_object to detect OSC 11 server. + # When OSC 11 is deprecated, simply delete all this and go back to: + # + # delete_object(Chef::User, @user_name) + # + # Also delete our override of delete_object above + object = Chef::User.load(@user_name) + # OSC 11 case + if object.username.nil? + ui.warn(osc_11_warning) + run_osc_11_user_delete + else # proceed with EC / CS delete + delete_object(@user_name) + end + + end end end end diff --git a/lib/chef/knife/user_edit.rb b/lib/chef/knife/user_edit.rb index ae319c8872..dd2fc02743 100644 --- a/lib/chef/knife/user_edit.rb +++ b/lib/chef/knife/user_edit.rb @@ -29,6 +29,24 @@ class Chef banner "knife user edit USER (options)" + def osc_11_warning +<<-EOF +The Chef Server you are using does not support the username field. +This means it is an Open Source 11 Server. +knife user edit for Open Source 11 Server is being deprecated. +Open Source 11 Server user commands now live under the knife oc_user namespace. +For backwards compatibility, we will forward this request to knife osc_user edit. +If you are using an Open Source 11 Server, please use that command to avoid this warning. +EOF + end + + def run_osc_11_user_edit + # run osc_user_create with our input + ARGV.delete("user") + ARGV.unshift("osc_user") + Chef::Knife.run(ARGV, Chef::Application::Knife.options) + end + def run @user_name = @name_args[0] @@ -39,14 +57,26 @@ class Chef end original_user = Chef::User.load(@user_name).to_hash - edited_user = edit_data(original_user) - if original_user != edited_user - user = Chef::User.from_hash(edited_user) - user.update - ui.msg("Saved #{user}.") - else - ui.msg("User unchaged, not saving.") + + # DEPRECATION NOTE + # Remove this if statement and corrosponding code post OSC 11 support. + # + # if username is nil, we are in the OSC 11 case, + # forward to deprecated command + if original_user["username"].nil? + ui.warn(osc_11_warning) + run_osc_11_user_edit + else # EC / CS 12 user create + edited_user = edit_data(original_user) + if original_user != edited_user + user = Chef::User.from_hash(edited_user) + user.update + ui.msg("Saved #{user}.") + else + ui.msg("User unchaged, not saving.") + end end + end end end diff --git a/lib/chef/knife/user_list.rb b/lib/chef/knife/user_list.rb index 5d2e735a73..7ae43dadc9 100644 --- a/lib/chef/knife/user_list.rb +++ b/lib/chef/knife/user_list.rb @@ -18,6 +18,8 @@ require 'chef/knife' +# NOTE: only knife user command that is backwards compatible with OSC 11, +# so no deprecation warnings are necessary. class Chef class Knife class UserList < Knife @@ -37,6 +39,7 @@ class Chef def run output(format_list_for_display(Chef::User.list)) end + end end end diff --git a/lib/chef/knife/user_reregister.rb b/lib/chef/knife/user_reregister.rb index 946150e6e4..eab2245025 100644 --- a/lib/chef/knife/user_reregister.rb +++ b/lib/chef/knife/user_reregister.rb @@ -29,6 +29,24 @@ class Chef banner "knife user reregister USER (options)" + def osc_11_warning +<<-EOF +The Chef Server you are using does not support the username field. +This means it is an Open Source 11 Server. +knife user reregister for Open Source 11 Server is being deprecated. +Open Source 11 Server user commands now live under the knife osc_user namespace. +For backwards compatibility, we will forward this request to knife osc_user reregister. +If you are using an Open Source 11 Server, please use that command to avoid this warning. +EOF + end + + def run_osc_11_user_reregister + # run osc_user_edit with our input + ARGV.delete("user") + ARGV.unshift("osc_user") + Chef::Knife.run(ARGV, Chef::Application::Knife.options) + end + option :file, :short => "-f FILE", :long => "--file FILE", @@ -43,16 +61,29 @@ class Chef exit 1 end - user = Chef::User.load(@user_name).reregister - Chef::Log.debug("Updated user data: #{user.inspect}") - key = user.private_key - if config[:file] - File.open(config[:file], "w") do |f| - f.print(key) + user = Chef::User.load(@user_name) + + # DEPRECATION NOTE + # Remove this if statement and corrosponding code post OSC 11 support. + # + # if username is nil, we are in the OSC 11 case, + # forward to deprecated command + if user.username.nil? + ui.warn(osc_11_warning) + run_osc_11_user_reregister + else # EC / CS 12 case + user.reregister + Chef::Log.debug("Updated user data: #{user.inspect}") + key = user.private_key + if config[:file] + File.open(config[:file], "w") do |f| + f.print(key) + end + else + ui.msg key end - else - ui.msg key end + end end end diff --git a/lib/chef/knife/user_show.rb b/lib/chef/knife/user_show.rb index 61ea101e4c..f5e81e9972 100644 --- a/lib/chef/knife/user_show.rb +++ b/lib/chef/knife/user_show.rb @@ -31,6 +31,24 @@ class Chef banner "knife user show USER (options)" + def osc_11_warning +<<-EOF +The Chef Server you are using does not support the username field. +This means it is an Open Source 11 Server. +knife user show for Open Source 11 Server is being deprecated. +Open Source 11 Server user commands now live under the knife osc_user namespace. +For backwards compatibility, we will forward this request to knife osc_user show. +If you are using an Open Source 11 Server, please use that command to avoid this warning. +EOF + end + + def run_osc_11_user_show + # run osc_user_edit with our input + ARGV.delete("user") + ARGV.unshift("osc_user") + Chef::Knife.run(ARGV, Chef::Application::Knife.options) + end + def run @user_name = @name_args[0] @@ -41,7 +59,18 @@ class Chef end user = Chef::User.load(@user_name) - output(format_for_display(user)) + + # DEPRECATION NOTE + # Remove this if statement and corrosponding code post OSC 11 support. + # + # if username is nil, we are in the OSC 11 case, + # forward to deprecated command + if user.username.nil? + ui.warn(osc_11_warning) + run_osc_11_user_show + else + output(format_for_display(user)) + 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..20ab3bf452 --- /dev/null +++ b/lib/chef/mixin/api_version_request_handling.rb @@ -0,0 +1,66 @@ +# +# 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 Mixin + module ApiVersionRequestHandling + # Input: + # exeception: + # Net::HTTPServerException that may or may not contain the x-ops-server-api-version header + # supported_client_versions: + # An array of Integers that represent the API versions the client supports. + # + # Output: + # nil: + # If the execption was not a 406 or the server does not support versioning + # Array of length zero: + # If there was no intersection between supported client versions and supported server versions + # Arrary of Integers: + # If there was an intersection of supported versions, the array returns will contain that intersection + def server_client_api_version_intersection(exception, supported_client_versions) + # return empty array unless 406 Unacceptable with proper header + return nil if exception.response.code != "406" || exception.response["x-ops-server-api-version"].nil? + + # intersection of versions the server and client support, will be of length zero if no intersection + server_supported_client_versions = Array.new + + 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"]) + + supported_client_versions.each do |version| + if version >= min_server_version && version <= max_server_version + server_supported_client_versions.push(version) + end + end + server_supported_client_versions + end + + def reregister_only_v0_supported_error_msg(max_version, min_version) + <<-EOH +The reregister command only supports server API version 0. +The server that received the request supports a min version of #{min_version} and a max version of #{max_version}. +User keys are now managed via the key rotation commmands. +Please refer to the documentation on how to manage your keys via the key rotation commands: +https://docs.chef.io/server_security.html#key-rotation +EOH + end + + end + end +end diff --git a/lib/chef/osc_user.rb b/lib/chef/osc_user.rb new file mode 100644 index 0000000000..52bfd11108 --- /dev/null +++ b/lib/chef/osc_user.rb @@ -0,0 +1,194 @@ +# +# Author:: Steven Danna (steve@opscode.com) +# Copyright:: Copyright 2012 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/config' +require 'chef/mixin/params_validate' +require 'chef/mixin/from_file' +require 'chef/mash' +require 'chef/json_compat' +require 'chef/search/query' + +# TODO +# DEPRECATION NOTE +# This class was previously Chef::User. It is the code to support the User object +# corrosponding to the Open Source Chef Server 11 and only still exists to support +# users still on OSC 11. +# +# Chef::User now supports Chef Server 12. +# +# New development should occur in Chef::User. +# This file and corrosponding osc_user knife files +# should be removed once client support for Open Source Chef Server 11 expires. +class Chef + class OscUser + + include Chef::Mixin::FromFile + include Chef::Mixin::ParamsValidate + + def initialize + @name = '' + @public_key = nil + @private_key = nil + @password = nil + @admin = false + end + + def name(arg=nil) + set_or_return(:name, arg, + :regex => /^[a-z0-9\-_]+$/) + end + + def admin(arg=nil) + set_or_return(:admin, + arg, :kind_of => [TrueClass, FalseClass]) + end + + def public_key(arg=nil) + 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 password(arg=nil) + set_or_return(:password, + arg, :kind_of => String) + end + + def to_hash + result = { + "name" => @name, + "public_key" => @public_key, + "admin" => @admin + } + result["private_key"] = @private_key if @private_key + result["password"] = @password if @password + result + end + + def to_json(*a) + Chef::JSONCompat.to_json(to_hash, *a) + end + + def destroy + Chef::REST.new(Chef::Config[:chef_server_url]).delete_rest("users/#{@name}") + 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) + Chef::OscUser.from_hash(self.to_hash.merge(new_user)) + end + + def update(new_key=false) + payload = {:name => name, :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) + Chef::OscUser.from_hash(self.to_hash.merge(updated_user)) + end + + def save(new_key=false) + begin + create + rescue Net::HTTPServerException => e + if e.response.code == "409" + update(new_key) + else + raise e + end + end + end + + 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 }) + private_key(reregistered_self["private_key"]) + self + end + + def to_s + "user[#{@name}]" + end + + def inspect + "Chef::OscUser name:'#{name}' admin:'#{admin.inspect}'" + + "public_key:'#{public_key}' private_key:#{private_key}" + end + + # Class Methods + + def self.from_hash(user_hash) + user = Chef::OscUser.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.admin user_hash['admin'] + user + end + + def self.from_json(json) + Chef::OscUser.from_hash(Chef::JSONCompat.from_json(json)) + end + + class << self + alias_method :json_create, :from_json + end + + def self.list(inflate=false) + response = Chef::REST.new(Chef::Config[:chef_server_url]).get_rest('users') + users = if response.is_a?(Array) + transform_ohc_list_response(response) # OHC/OPC + else + response # OSC + end + if inflate + users.inject({}) do |user_map, (name, _url)| + user_map[name] = Chef::OscUser.load(name) + user_map + end + else + users + end + end + + def self.load(name) + response = Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("users/#{name}") + Chef::OscUser.from_hash(response) + end + + # Gross. Transforms an API response in the form of: + # [ { "user" => { "username" => USERNAME }}, ...] + # into the form + # { "USERNAME" => "URI" } + def self.transform_ohc_list_response(response) + new_response = Hash.new + response.each do |u| + name = u['user']['username'] + new_response[name] = Chef::Config[:chef_server_url] + "/users/#{name}" + end + new_response + end + + private_class_method :transform_ohc_list_response + 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..717deb63c3 100644 --- a/lib/chef/user.rb +++ b/lib/chef/user.rb @@ -21,29 +21,85 @@ require 'chef/mixin/from_file' require 'chef/mash' require 'chef/json_compat' require 'chef/search/query' +require 'chef/mixin/api_version_request_handling' +require 'chef/exceptions' +require 'chef/server_api' +# OSC 11 BACKWARDS COMPATIBILITY NOTE (remove after OSC 11 support ends) +# +# In general, Chef::User is no longer expected to support Open Source Chef 11 Server requests. +# The object that handles those requests has been moved to the Chef::OscUser namespace. +# +# Exception: self.list is backwards compatible with OSC 11 class Chef class User include Chef::Mixin::FromFile include Chef::Mixin::ParamsValidate + include Chef::Mixin::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_root_rest_v0 + @chef_root_rest_v0 ||= Chef::ServerAPI.new(Chef::Config[:chef_server_root], {:api_version => "0"}) + end + + def chef_root_rest_v1 + @chef_root_rest_v1 ||= Chef::ServerAPI.new(Chef::Config[:chef_server_root], {:api_version => "1"}) + end + + def username(arg=nil) + set_or_return(:username, arg, :regex => /^[a-z0-9\-_]+$/) end - def admin(arg=nil) - set_or_return(:admin, - arg, :kind_of => [TrueClass, FalseClass]) + 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 create_key(arg=nil) + set_or_return(:create_key, arg, + :kind_of => [TrueClass, FalseClass]) end def public_key(arg=nil) @@ -63,12 +119,17 @@ class Chef def to_hash result = { - "name" => @name, - "public_key" => @public_key, - "admin" => @admin + "username" => @username } - result["private_key"] = @private_key if @private_key - result["password"] = @password if @password + result["display_name"] = @display_name unless @display_name.nil? + result["first_name"] = @first_name unless @first_name.nil? + result["middle_name"] = @middle_name unless @middle_name.nil? + result["last_name"] = @last_name unless @last_name.nil? + result["email"] = @email unless @email.nil? + result["password"] = @password unless @password.nil? + result["public_key"] = @public_key unless @public_key.nil? + result["private_key"] = @private_key unless @private_key.nil? + result["create_key"] = @create_key unless @create_key.nil? result end @@ -77,21 +138,86 @@ class Chef end def destroy - Chef::REST.new(Chef::Config[:chef_server_url]).delete_rest("users/#{@name}") + # will default to the current API version (Chef::Authenticator::DEFAULT_SERVER_API_VERSION) + 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 + } + payload[:public_key] = @public_key unless @public_key.nil? + payload[:create_key] = @create_key unless @create_key.nil? + payload[:middle_name] = @middle_name unless @middle_name.nil? + raise Chef::Exceptions::InvalidUserAttribute, "You cannot set both public_key and create_key for create." if !@create_key.nil? && !@public_key.nil? + 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 + # rescue API V0 if 406 and the server supports V0 + supported_versions = server_client_api_version_intersection(e, SUPPORTED_API_VERSIONS) + raise e unless supported_versions && supported_versions.include?(0) + payload = { + :username => @username, + :display_name => @display_name, + :first_name => @first_name, + :last_name => @last_name, + :email => @email, + :password => @password + } + payload[:middle_name] = @middle_name unless @middle_name.nil? + payload[:public_key] = @public_key unless @public_key.nil? + # under API V0, the server will create a key pair if public_key isn't passed + 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[: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) + begin + payload = {:username => username} + payload[:display_name] = display_name unless display_name.nil? + payload[:first_name] = first_name unless first_name.nil? + payload[:middle_name] = middle_name unless middle_name.nil? + payload[:last_name] = last_name unless last_name.nil? + payload[:email] = email unless email.nil? + payload[:password] = password unless password.nil? + + # API V1 will fail if these key fields are defined, and try V0 below if relevant 400 is returned + payload[:public_key] = public_key unless public_key.nil? + payload[:private_key] = new_key if new_key + + updated_user = chef_root_rest_v1.put("users/#{username}", payload) + rescue Net::HTTPServerException => e + if e.response.code == "400" + # if a 400 is returned but the error message matches the error related to private / public key fields, try V0 + # else, raise the 400 + error = Chef::JSONCompat.from_json(e.response.body)["error"].first + error_match = /Since Server API v1, all keys must be updated via the keys endpoint/.match(error) + if error_match.nil? + raise e + end + else # for other types of errors, test for API versioning errors right away + supported_versions = server_client_api_version_intersection(e, SUPPORTED_API_VERSIONS) + raise e unless supported_versions && supported_versions.include?(0) + end + updated_user = chef_root_rest_v0.put("users/#{username}", payload) + end Chef::User.from_hash(self.to_hash.merge(updated_user)) end @@ -107,31 +233,47 @@ class Chef end end + # Note: remove after API v0 no longer supported by client (and knife command). 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 }) - private_key(reregistered_self["private_key"]) + begin + payload = self.to_hash.merge({"private_key" => true}) + reregistered_self = chef_root_rest_v0.put("users/#{username}", payload) + private_key(reregistered_self["private_key"]) + # only V0 supported for reregister + rescue Net::HTTPServerException => e + # if there was a 406 related to versioning, give error explaining that + # only API version 0 is supported for reregister command + if e.response.code == "406" && e.response["x-ops-server-api-version"] + version_header = Chef::JSONCompat.from_json(e.response["x-ops-server-api-version"]) + min_version = version_header["min_version"] + max_version = version_header["max_version"] + error_msg = reregister_only_v0_supported_error_msg(max_version, min_version) + raise Chef::Exceptions::OnlyApiVersion0SupportedForAction.new(error_msg) + else + raise e + end + end self end def to_s - "user[#{@name}]" - end - - def inspect - "Chef::User name:'#{name}' admin:'#{admin.inspect}'" + - "public_key:'#{public_key}' private_key:#{private_key}" + "user[#{@username}]" end # Class Methods 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.username user_hash['username'] + 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'] - user.admin user_hash['admin'] + 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,12 +286,19 @@ 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 - response # OSC - end + # EC 11 / CS 12 V0, V1 + # GET /organizations/<org>/users + transform_list_response(response) + else + # OSC 11 + # GET /users + # EC 11 / CS 12 V0, V1 + # GET /users + response # OSC + end + if inflate users.inject({}) do |user_map, (name, _url)| user_map[name] = Chef::User.load(name) @@ -160,8 +309,9 @@ 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) + # will default to the current API version (Chef::Authenticator::DEFAULT_SERVER_API_VERSION) + response = Chef::REST.new(Chef::Config[:chef_server_url]).get("users/#{username}") Chef::User.from_hash(response) end @@ -169,7 +319,7 @@ class Chef # [ { "user" => { "username" => USERNAME }}, ...] # into the form # { "USERNAME" => "URI" } - def self.transform_ohc_list_response(response) + def self.transform_list_response(response) new_response = Hash.new response.each do |u| name = u['user']['username'] @@ -178,6 +328,7 @@ class Chef new_response end - private_class_method :transform_ohc_list_response + private_class_method :transform_list_response + end end diff --git a/spec/support/shared/unit/api_versioning.rb b/spec/support/shared/unit/api_versioning.rb new file mode 100644 index 0000000000..a4f353de60 --- /dev/null +++ b/spec/support/shared/unit/api_versioning.rb @@ -0,0 +1,77 @@ +# +# 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/exceptions" + +shared_examples_for "version handling" do + let(:response_406) { OpenStruct.new(:code => '406') } + let(:exception_406) { Net::HTTPServerException.new("406 Not Acceptable", response_406) } + + before do + allow(rest_v1).to receive(http_verb).and_raise(exception_406) + end + + context "when the server does not support the min or max server API version that Chef::User supports" do + before do + allow(object).to receive(:server_client_api_version_intersection).and_return([]) + end + + it "raises the original exception" do + expect{ object.send(method) }.to raise_error(exception_406) + end + end # when the server does not support the min or max server API version that Chef::User supports +end # version handling + +shared_examples_for "user and client reregister" do + let(:response_406) { OpenStruct.new(:code => '406') } + let(:exception_406) { Net::HTTPServerException.new("406 Not Acceptable", response_406) } + let(:generic_exception) { Exception.new } + let(:min_version) { "2" } + let(:max_version) { "5" } + let(:return_hash_406) { + { + "min_version" => min_version, + "max_version" => max_version, + "request_version" => "30" + } + } + + context "when V0 is not supported by the server" do + context "when the exception is 406 and returns x-ops-server-api-version header" do + before do + allow(rest_v0).to receive(:put).and_raise(exception_406) + allow(response_406).to receive(:[]).with('x-ops-server-api-version').and_return(Chef::JSONCompat.to_json(return_hash_406)) + end + + it "raises an error about only V0 being supported" do + expect(object).to receive(:reregister_only_v0_supported_error_msg).with(max_version, min_version) + expect{ object.reregister }.to raise_error(Chef::Exceptions::OnlyApiVersion0SupportedForAction) + end + + end + context "when the exception is not versioning related" do + before do + allow(rest_v0).to receive(:put).and_raise(generic_exception) + end + + it "raises the original error" do + expect{ object.reregister }.to raise_error(generic_exception) + end + end + end +end diff --git a/spec/support/shared/unit/knife_shared.rb b/spec/support/shared/unit/knife_shared.rb new file mode 100644 index 0000000000..8c9010f3cf --- /dev/null +++ b/spec/support/shared/unit/knife_shared.rb @@ -0,0 +1,40 @@ +# +# 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. +# + + +shared_examples_for "mandatory field missing" do + context "when field is nil" do + before do + knife.name_args = name_args + end + + it "exits 1" do + expect { knife.run }.to raise_error(SystemExit) + end + + it "prints the usage" do + expect(knife).to receive(:show_usage) + expect { knife.run }.to raise_error(SystemExit) + end + + it "prints a relevant error message" do + expect { knife.run }.to raise_error(SystemExit) + expect(stderr.string).to match /You must specify a #{fieldname}/ + end + end +end diff --git a/spec/support/shared/unit/user_and_client_shared.rb b/spec/support/shared/unit/user_and_client_shared.rb new file mode 100644 index 0000000000..bc5ffa07c2 --- /dev/null +++ b/spec/support/shared/unit/user_and_client_shared.rb @@ -0,0 +1,115 @@ +# +# 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. +# + +shared_examples_for "user or client create" do + + context "when server API V1 is valid on the Chef Server receiving the request" do + + it "creates a new object via the API" do + expect(rest_v1).to receive(:post).with(url, payload).and_return({}) + object.create + end + + it "creates a new object via the API with a public_key when it exists" do + object.public_key "some_public_key" + expect(rest_v1).to receive(:post).with(url, payload.merge({:public_key => "some_public_key"})).and_return({}) + object.create + end + + context "raise error when create_key and public_key are both set" do + + before do + object.public_key "key" + object.create_key true + end + + it "rasies the proper error" do + expect { object.create }.to raise_error(error) + end + end + + context "when create_key == true" do + before do + object.create_key true + end + + it "creates a new object via the API with create_key" do + expect(rest_v1).to receive(:post).with(url, payload.merge({:create_key => true})).and_return({}) + object.create + end + end + + context "when chef_key is returned by the server" do + let(:chef_key) { + { + "chef_key" => { + "public_key" => "some_public_key" + } + } + } + + it "puts the public key into the objectr returned by create" do + expect(rest_v1).to receive(:post).with(url, payload).and_return(payload.merge(chef_key)) + new_object = object.create + expect(new_object.public_key).to eq("some_public_key") + end + + context "when private_key is returned in chef_key" do + let(:chef_key) { + { + "chef_key" => { + "public_key" => "some_public_key", + "private_key" => "some_private_key" + } + } + } + + it "puts the private key into the object returned by create" do + expect(rest_v1).to receive(:post).with(url, payload).and_return(payload.merge(chef_key)) + new_object = object.create + expect(new_object.private_key).to eq("some_private_key") + end + end + end # when chef_key is returned by the server + + end # when server API V1 is valid on the Chef Server receiving the request + + context "when server API V1 is not valid on the Chef Server receiving the request" do + + context "when the server supports API V0" do + before do + allow(object).to receive(:server_client_api_version_intersection).and_return([0]) + allow(rest_v1).to receive(:post).and_raise(exception_406) + end + + it "creates a new object via the API" do + expect(rest_v0).to receive(:post).with(url, payload).and_return({}) + object.create + end + + it "creates a new object via the API with a public_key when it exists" do + object.public_key "some_public_key" + expect(rest_v0).to receive(:post).with(url, payload.merge({:public_key => "some_public_key"})).and_return({}) + object.create + end + + end # when the server supports API V0 + end # when server API V1 is not valid on the Chef Server receiving the request + +end # user or client create + diff --git a/spec/unit/api_client_spec.rb b/spec/unit/api_client_spec.rb index 7668e31f5a..ba0eca3284 100644 --- a/spec/unit/api_client_spec.rb +++ b/spec/unit/api_client_spec.rb @@ -53,6 +53,20 @@ describe Chef::ApiClient do expect { @client.admin(Hash.new) }.to raise_error(ArgumentError) end + it "has an create_key flag attribute" do + @client.create_key(true) + expect(@client.create_key).to be_truthy + end + + it "create_key defaults to false" do + expect(@client.create_key).to be_falsey + end + + it "allows only boolean values for the create_key flag" do + expect { @client.create_key(false) }.not_to raise_error + expect { @client.create_key(Hash.new) }.to raise_error(ArgumentError) + end + it "has a 'validator' flag attribute" do @client.validator(true) expect(@client.validator).to be_truthy @@ -115,6 +129,12 @@ describe Chef::ApiClient do expect(@json).to include(%q{"validator":false}) end + it "includes the 'create_key' flag when present" do + @client.create_key(true) + @json = @client.to_json + expect(@json).to include(%q{"create_key":true}) + end + it "includes the private key when present" do @client.private_key("monkeypants") expect(@client.to_json).to include(%q{"private_key":"monkeypants"}) @@ -131,7 +151,7 @@ describe Chef::ApiClient do describe "when deserializing from JSON (string) using ApiClient#from_json" do let(:client_string) do - "{\"name\":\"black\",\"public_key\":\"crowes\",\"private_key\":\"monkeypants\",\"admin\":true,\"validator\":true}" + "{\"name\":\"black\",\"public_key\":\"crowes\",\"private_key\":\"monkeypants\",\"admin\":true,\"validator\":true,\"create_key\":true}" end let(:client) do @@ -158,6 +178,10 @@ describe Chef::ApiClient do expect(client.admin).to be_truthy end + it "preserves the create_key status" do + expect(client.create_key).to be_truthy + end + it "preserves the 'validator' status" do expect(client.validator).to be_truthy end @@ -175,6 +199,7 @@ describe Chef::ApiClient do "private_key" => "monkeypants", "admin" => true, "validator" => true, + "create_key" => true, "json_class" => "Chef::ApiClient" } end @@ -199,6 +224,10 @@ describe Chef::ApiClient do expect(client.admin).to be_truthy end + it "preserves the create_key status" do + expect(client.create_key).to be_truthy + end + it "preserves the 'validator' status" do expect(client.validator).to be_truthy end @@ -214,14 +243,16 @@ describe Chef::ApiClient do before(:each) do client = { - "name" => "black", - "clientname" => "black", - "public_key" => "crowes", - "private_key" => "monkeypants", - "admin" => true, - "validator" => true, - "json_class" => "Chef::ApiClient" + "name" => "black", + "clientname" => "black", + "public_key" => "crowes", + "private_key" => "monkeypants", + "admin" => true, + "create_key" => true, + "validator" => true, + "json_class" => "Chef::ApiClient" } + @http_client = double("Chef::REST mock") allow(Chef::REST).to receive(:new).and_return(@http_client) expect(@http_client).to receive(:get).with("clients/black").and_return(client) @@ -244,6 +275,10 @@ describe Chef::ApiClient do expect(@client.admin).to be_a_kind_of(TrueClass) end + it "preserves the create_key status" do + expect(@client.create_key).to be_a_kind_of(TrueClass) + end + it "preserves the 'validator' status" do expect(@client.validator).to be_a_kind_of(TrueClass) end @@ -297,24 +332,34 @@ describe Chef::ApiClient do end context "and the client exists" do + let(:chef_rest_v0_mock) { double('chef rest root v0 object') } + let(:payload) { + {:name => "lost-my-key", :admin => false, :validator => false, :private_key => true} + } + before do @api_client_without_key = Chef::ApiClient.new @api_client_without_key.name("lost-my-key") - expect(@http_client).to receive(:get).with("clients/lost-my-key").and_return(@api_client_without_key) - end + allow(@api_client_without_key).to receive(:chef_rest_v0).and_return(chef_rest_v0_mock) + #allow(@api_client_with_key).to receive(:http_api).and_return(_api_mock) + allow(chef_rest_v0_mock).to receive(:put).with("clients/lost-my-key", payload).and_return(@api_client_with_key) + allow(chef_rest_v0_mock).to receive(:get).with("clients/lost-my-key").and_return(@api_client_without_key) + allow(@http_client).to receive(:get).with("clients/lost-my-key").and_return(@api_client_without_key) + end context "and the client exists on a Chef 11-like server" do before do @api_client_with_key = Chef::ApiClient.new @api_client_with_key.name("lost-my-key") @api_client_with_key.private_key("the new private key") - expect(@http_client).to receive(:put). - with("clients/lost-my-key", :name => "lost-my-key", :admin => false, :validator => false, :private_key => true). - and_return(@api_client_with_key) + allow(@api_client_with_key).to receive(:chef_rest_v0).and_return(chef_rest_v0_mock) end it "returns an ApiClient with a private key" do + expect(chef_rest_v0_mock).to receive(:put).with("clients/lost-my-key", payload). + and_return(@api_client_with_key) + response = Chef::ApiClient.reregister("lost-my-key") # no sane == method for ApiClient :'( expect(response).to eq(@api_client_without_key) @@ -327,7 +372,7 @@ describe Chef::ApiClient do context "and the client exists on a Chef 10-like server" do before do @api_client_with_key = {"name" => "lost-my-key", "private_key" => "the new private key"} - expect(@http_client).to receive(:put). + expect(chef_rest_v0_mock).to receive(:put). with("clients/lost-my-key", :name => "lost-my-key", :admin => false, :validator => false, :private_key => true). and_return(@api_client_with_key) end @@ -345,4 +390,134 @@ describe Chef::ApiClient do end end + + describe "Versioned API Interactions" do + let(:response_406) { OpenStruct.new(:code => '406') } + let(:exception_406) { Net::HTTPServerException.new("406 Not Acceptable", response_406) } + let(:payload) { + { + :name => "some_name", + :validator => true, + :admin => true + } + } + + before do + @client = Chef::ApiClient.new + allow(@client).to receive(:chef_rest_v0).and_return(double('chef rest root v0 object')) + allow(@client).to receive(:chef_rest_v1).and_return(double('chef rest root v1 object')) + @client.name "some_name" + @client.validator true + @client.admin true + end + + describe "create" do + + # from spec/support/shared/unit/user_and_client_shared.rb + it_should_behave_like "user or client create" do + let(:object) { @client } + let(:error) { Chef::Exceptions::InvalidClientAttribute } + let(:rest_v0) { @client.chef_rest_v0 } + let(:rest_v1) { @client.chef_rest_v1 } + let(:url) { "clients" } + end + + context "when API V1 is not supported by the server" do + # from spec/support/shared/unit/api_versioning.rb + it_should_behave_like "version handling" do + let(:object) { @client } + let(:method) { :create } + let(:http_verb) { :post } + let(:rest_v1) { @client.chef_rest_v1 } + end + end + + end # create + + describe "update" do + context "when a valid client is defined" do + + shared_examples_for "client updating" do + it "updates the client" do + expect(rest). to receive(:put).with("clients/some_name", payload) + @client.update + end + + context "when only the name field exists" do + + before do + # needed since there is no way to set to nil via code + @client.instance_variable_set(:@validator, nil) + @client.instance_variable_set(:@admin, nil) + end + + after do + @client.validator true + @client.admin true + end + + it "updates the client with only the name" do + expect(rest). to receive(:put).with("clients/some_name", {:name => "some_name"}) + @client.update + end + end + + end + + context "when API V1 is supported by the server" do + + it_should_behave_like "client updating" do + let(:rest) { @client.chef_rest_v1 } + end + + end # when API V1 is supported by the server + + context "when API V1 is not supported by the server" do + context "when no version is supported" do + # from spec/support/shared/unit/api_versioning.rb + it_should_behave_like "version handling" do + let(:object) { @client } + let(:method) { :create } + let(:http_verb) { :post } + let(:rest_v1) { @client.chef_rest_v1 } + end + end # when no version is supported + + context "when API V0 is supported" do + + before do + allow(@client.chef_rest_v1).to receive(:put).and_raise(exception_406) + allow(@client).to receive(:server_client_api_version_intersection).and_return([0]) + end + + it_should_behave_like "client updating" do + let(:rest) { @client.chef_rest_v0 } + end + + end + + end # when API V1 is not supported by the server + end # when a valid client is defined + end # update + + # DEPRECATION + # This can be removed after API V0 support is gone + describe "reregister" do + context "when server API V0 is valid on the Chef Server receiving the request" do + it "creates a new object via the API" do + expect(@client.chef_rest_v0).to receive(:put).with("clients/#{@client.name}", payload.merge({:private_key => true})).and_return({}) + @client.reregister + end + end # when server API V0 is valid on the Chef Server receiving the request + + context "when server API V0 is not supported by the Chef Server" do + # from spec/support/shared/unit/api_versioning.rb + it_should_behave_like "user and client reregister" do + let(:object) { @client } + let(:rest_v0) { @client.chef_rest_v0 } + end + end # when server API V0 is not supported by the Chef Server + end # reregister + + end end diff --git a/spec/unit/formatters/error_inspectors/api_error_formatting_spec.rb b/spec/unit/formatters/error_inspectors/api_error_formatting_spec.rb index 4b3b8bff44..b8c2de2b8b 100644 --- a/spec/unit/formatters/error_inspectors/api_error_formatting_spec.rb +++ b/spec/unit/formatters/error_inspectors/api_error_formatting_spec.rb @@ -29,19 +29,21 @@ describe Chef::Formatters::APIErrorFormatting do context "when describe_406_error is called" do - context "when response.body['error'] == 'invalid-x-ops-server-api-version'" do + context "when response['x-ops-server-api-version'] exists" do let(:min_version) { "2" } let(:max_version) { "5" } + let(:request_version) { "30" } let(:return_hash) { { - "error" => "invalid-x-ops-server-api-version", "min_version" => min_version, - "max_version" => max_version + "max_version" => max_version, + "request_version" => request_version } } before do - allow(Chef::JSONCompat).to receive(:from_json).and_return(return_hash) + # mock out the header + allow(response).to receive(:[]).with('x-ops-server-api-version').and_return(Chef::JSONCompat.to_json(return_hash)) end it "prints an error about client and server API version incompatibility with a min API version" do @@ -53,17 +55,17 @@ describe Chef::Formatters::APIErrorFormatting do expect(error_description).to receive(:section).with("Incompatible server API version:",/a max API version of #{max_version}/) class_instance.describe_406_error(error_description, response) end + + it "prints an error describing the request API version" do + expect(error_description).to receive(:section).with("Incompatible server API version:",/a request with an API version of #{request_version}/) + class_instance.describe_406_error(error_description, response) + end end context "when response.body['error'] != 'invalid-x-ops-server-api-version'" do - let(:return_hash) { - { - "error" => "some-other-error" - } - } before do - allow(Chef::JSONCompat).to receive(:from_json).and_return(return_hash) + allow(response).to receive(:[]).with('x-ops-server-api-version').and_return(nil) end it "forwards the error_description to describe_http_error" do diff --git a/spec/unit/http/authenticator_spec.rb b/spec/unit/http/authenticator_spec.rb index 82d38d6c2b..48bbdcf76c 100644 --- a/spec/unit/http/authenticator_spec.rb +++ b/spec/unit/http/authenticator_spec.rb @@ -32,10 +32,19 @@ describe Chef::HTTP::Authenticator do context "when handle_request is called" do shared_examples_for "merging the server API version into the headers" do - it "merges X-Ops-Server-API-Version into the headers" do + it "merges the default version of X-Ops-Server-API-Version into the headers" do # headers returned expect(class_instance.handle_request(method, url, headers, data)[2]). - to include({'X-Ops-Server-API-Version' => Chef::HTTP::Authenticator::SERVER_API_VERSION}) + to include({'X-Ops-Server-API-Version' => Chef::HTTP::Authenticator::DEFAULT_SERVER_API_VERSION}) + end + + context "when api_version is set to something other than the default" do + let(:class_instance) { Chef::HTTP::Authenticator.new({:api_version => '-10'}) } + + it "merges the requested version of X-Ops-Server-API-Version into the headers" do + expect(class_instance.handle_request(method, url, headers, data)[2]). + to include({'X-Ops-Server-API-Version' => '-10'}) + end end end diff --git a/spec/unit/knife/client_create_spec.rb b/spec/unit/knife/client_create_spec.rb index 10d386b5ff..8fecfc885f 100644 --- a/spec/unit/knife/client_create_spec.rb +++ b/spec/unit/knife/client_create_spec.rb @@ -22,6 +22,8 @@ Chef::Knife::ClientCreate.load_deps describe Chef::Knife::ClientCreate do let(:stderr) { StringIO.new } + let(:stdout) { StringIO.new } + let(:default_client_hash) do { @@ -32,84 +34,153 @@ describe Chef::Knife::ClientCreate do end let(:client) do - c = double("Chef::ApiClient") - allow(c).to receive(:save).and_return({"private_key" => ""}) - allow(c).to receive(:to_s).and_return("client[adam]") - c + Chef::ApiClient.new end let(:knife) do k = Chef::Knife::ClientCreate.new - k.name_args = [ "adam" ] - k.ui.config[:disable_editing] = true + k.name_args = [] + allow(k).to receive(:client).and_return(client) + allow(k).to receive(:edit_data).with(client).and_return(client) allow(k.ui).to receive(:stderr).and_return(stderr) - allow(k.ui).to receive(:stdout).and_return(stderr) + allow(k.ui).to receive(:stdout).and_return(stdout) k end + before do + allow(client).to receive(:to_s).and_return("client[adam]") + allow(knife).to receive(:create_client).and_return(client) + end + before(:each) do Chef::Config[:node_name] = "webmonkey.example.com" end describe "run" do - it "should create and save the ApiClient" do - expect(Chef::ApiClient).to receive(:from_hash).and_return(client) - expect(client).to receive(:save) - knife.run + context "when nothing is passed" do + # from spec/support/shared/unit/knife_shared.rb + it_should_behave_like "mandatory field missing" do + let(:name_args) { [] } + let(:fieldname) { 'client name' } + end end - it "should print a message upon creation" do - expect(Chef::ApiClient).to receive(:from_hash).and_return(client) - expect(client).to receive(:save) - knife.run - expect(stderr.string).to match /Created client.*adam/i - end + context "when clientname is passed" do + before do + knife.name_args = ['adam'] + end - it "should set the Client name" do - expect(Chef::ApiClient).to receive(:from_hash).with(hash_including("name" => "adam")).and_return(client) - knife.run - end + context "when public_key and prevent_keygen are passed" do + before do + knife.config[:public_key] = "some_key" + knife.config[:prevent_keygen] = true + end + + it "prints the usage" do + expect(knife).to receive(:show_usage) + expect { knife.run }.to raise_error(SystemExit) + end + + it "prints a relevant error message" do + expect { knife.run }.to raise_error(SystemExit) + expect(stderr.string).to match /You cannot pass --public-key and --prevent-keygen/ + end + end - it "by default it is not an admin" do - expect(Chef::ApiClient).to receive(:from_hash).with(hash_including("admin" => false)).and_return(client) - knife.run - end + it "should create the ApiClient" do + expect(knife).to receive(:create_client) + knife.run + end - it "by default it is not a validator" do - expect(Chef::ApiClient).to receive(:from_hash).with(hash_including("validator" => false)).and_return(client) - knife.run - end + it "should print a message upon creation" do + expect(knife).to receive(:create_client) + knife.run + expect(stderr.string).to match /Created client.*adam/i + end - it "should allow you to edit the data" do - expect(knife).to receive(:edit_hash).with(default_client_hash).and_return(default_client_hash) - allow(Chef::ApiClient).to receive(:from_hash).and_return(client) - knife.run - end + it "should set the Client name" do + knife.run + expect(client.name).to eq("adam") + end - describe "with -f or --file" do - it "should write the private key to a file" do - knife.config[:file] = "/tmp/monkeypants" - allow_any_instance_of(Chef::ApiClient).to receive(:save).and_return({ 'private_key' => "woot" }) - filehandle = double("Filehandle") - expect(filehandle).to receive(:print).with('woot') - expect(File).to receive(:open).with("/tmp/monkeypants", "w").and_yield(filehandle) + it "by default it is not an admin" do knife.run + expect(client.admin).to be_falsey end - end - describe "with -a or --admin" do - it "should create an admin client" do - knife.config[:admin] = true - expect(Chef::ApiClient).to receive(:from_hash).with(hash_including("admin" => true)).and_return(client) + it "by default it is not a validator" do knife.run + expect(client.admin).to be_falsey end - end - describe "with --validator" do - it "should create an validator client" do - knife.config[:validator] = true - expect(Chef::ApiClient).to receive(:from_hash).with(hash_including("validator" => true)).and_return(client) + it "by default it should set create_key to true" do knife.run + expect(client.create_key).to be_truthy + end + + it "should allow you to edit the data" do + expect(knife).to receive(:edit_data).with(client).and_return(client) + knife.run + end + + describe "with -f or --file" do + before do + client.private_key "woot" + end + + it "should write the private key to a file" do + knife.config[:file] = "/tmp/monkeypants" + filehandle = double("Filehandle") + expect(filehandle).to receive(:print).with('woot') + expect(File).to receive(:open).with("/tmp/monkeypants", "w").and_yield(filehandle) + knife.run + end + end + + describe "with -a or --admin" do + before do + knife.config[:admin] = true + end + + it "should create an admin client" do + knife.run + expect(client.admin).to be_truthy + end + end + + describe "with -p or --public-key" do + before do + knife.config[:public_key] = 'some_key' + allow(File).to receive(:read).and_return('some_key') + allow(File).to receive(:expand_path) + end + + it "sets the public key" do + knife.run + expect(client.public_key).to eq('some_key') + end + end + + describe "with -k or --prevent-keygen" do + before do + knife.config[:prevent_keygen] = true + end + + it "does not set create_key" do + knife.run + expect(client.create_key).to be_falsey + end + end + + describe "with --validator" do + before do + knife.config[:validator] = true + end + + it "should create an validator client" do + knife.run + expect(client.validator).to be_truthy + end end end end diff --git a/spec/unit/knife/osc_user_create_spec.rb b/spec/unit/knife/osc_user_create_spec.rb new file mode 100644 index 0000000000..1b17d0d22f --- /dev/null +++ b/spec/unit/knife/osc_user_create_spec.rb @@ -0,0 +1,93 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2012 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' + +Chef::Knife::OscUserCreate.load_deps + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_create_spec.rb. + +describe Chef::Knife::OscUserCreate do + before(:each) do + @knife = Chef::Knife::OscUserCreate.new + + @stdout = StringIO.new + @stderr = StringIO.new + allow(@knife.ui).to receive(:stdout).and_return(@stdout) + allow(@knife.ui).to receive(:stderr).and_return(@stderr) + + @knife.name_args = [ 'a_user' ] + @knife.config[:user_password] = "foobar" + @user = Chef::OscUser.new + @user.name "a_user" + @user_with_private_key = Chef::OscUser.new + @user_with_private_key.name "a_user" + @user_with_private_key.private_key 'private_key' + allow(@user).to receive(:create).and_return(@user_with_private_key) + allow(Chef::OscUser).to receive(:new).and_return(@user) + allow(Chef::OscUser).to receive(:from_hash).and_return(@user) + allow(@knife).to receive(:edit_data).and_return(@user.to_hash) + end + + it "creates a new user" do + expect(Chef::OscUser).to receive(:new).and_return(@user) + expect(@user).to receive(:create) + @knife.run + expect(@stderr.string).to match /created user.+a_user/i + end + + it "sets the password" do + @knife.config[:user_password] = "a_password" + expect(@user).to receive(:password).with("a_password") + @knife.run + end + + it "exits with an error if password is blank" do + @knife.config[:user_password] = '' + expect { @knife.run }.to raise_error SystemExit + expect(@stderr.string).to match /You must specify a non-blank password/ + end + + it "sets the user name" do + expect(@user).to receive(:name).with("a_user") + @knife.run + end + + it "sets the public key if given" do + @knife.config[:user_key] = "/a/filename" + allow(File).to receive(:read).with(File.expand_path("/a/filename")).and_return("a_key") + expect(@user).to receive(:public_key).with("a_key") + @knife.run + end + + it "allows you to edit the data" do + expect(@knife).to receive(:edit_data).with(@user) + @knife.run + end + + it "writes the private key to a file when --file is specified" do + @knife.config[:file] = "/tmp/a_file" + filehandle = double("filehandle") + expect(filehandle).to receive(:print).with('private_key') + expect(File).to receive(:open).with("/tmp/a_file", "w").and_yield(filehandle) + @knife.run + end +end diff --git a/spec/unit/knife/osc_user_delete_spec.rb b/spec/unit/knife/osc_user_delete_spec.rb new file mode 100644 index 0000000000..0e16393ffe --- /dev/null +++ b/spec/unit/knife/osc_user_delete_spec.rb @@ -0,0 +1,44 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2012 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' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_delete_spec.rb. + +describe Chef::Knife::OscUserDelete do + before(:each) do + Chef::Knife::OscUserDelete.load_deps + @knife = Chef::Knife::OscUserDelete.new + @knife.name_args = [ 'my_user' ] + end + + it 'deletes the user' do + expect(@knife).to receive(:delete_object).with(Chef::OscUser, 'my_user') + @knife.run + end + + it 'prints usage and exits when a user name is not provided' do + @knife.name_args = [] + expect(@knife).to receive(:show_usage) + expect(@knife.ui).to receive(:fatal) + expect { @knife.run }.to raise_error(SystemExit) + end +end diff --git a/spec/unit/knife/osc_user_edit_spec.rb b/spec/unit/knife/osc_user_edit_spec.rb new file mode 100644 index 0000000000..71a9192389 --- /dev/null +++ b/spec/unit/knife/osc_user_edit_spec.rb @@ -0,0 +1,52 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2012 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' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_edit_spec.rb. + +describe Chef::Knife::OscUserEdit do + before(:each) do + @stderr = StringIO.new + @stdout = StringIO.new + + Chef::Knife::OscUserEdit.load_deps + @knife = Chef::Knife::OscUserEdit.new + allow(@knife.ui).to receive(:stderr).and_return(@stderr) + allow(@knife.ui).to receive(:stdout).and_return(@stdout) + @knife.name_args = [ 'my_user' ] + @knife.config[:disable_editing] = true + end + + it 'loads and edits the user' do + data = { :name => "my_user" } + allow(Chef::OscUser).to receive(:load).with("my_user").and_return(data) + expect(@knife).to receive(:edit_data).with(data).and_return(data) + @knife.run + end + + it 'prints usage and exits when a user name is not provided' do + @knife.name_args = [] + expect(@knife).to receive(:show_usage) + expect(@knife.ui).to receive(:fatal) + expect { @knife.run }.to raise_error(SystemExit) + end +end diff --git a/spec/unit/knife/osc_user_list_spec.rb b/spec/unit/knife/osc_user_list_spec.rb new file mode 100644 index 0000000000..59a15be058 --- /dev/null +++ b/spec/unit/knife/osc_user_list_spec.rb @@ -0,0 +1,37 @@ +# +# Author:: Steven Danna +# Copyright:: Copyright (c) 2012 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' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_list_spec.rb. + +describe Chef::Knife::OscUserList do + before(:each) do + Chef::Knife::OscUserList.load_deps + @knife = Chef::Knife::OscUserList.new + end + + it 'lists the users' do + expect(Chef::OscUser).to receive(:list) + expect(@knife).to receive(:format_list_for_display) + @knife.run + end +end diff --git a/spec/unit/knife/osc_user_reregister_spec.rb b/spec/unit/knife/osc_user_reregister_spec.rb new file mode 100644 index 0000000000..406bbf1f3e --- /dev/null +++ b/spec/unit/knife/osc_user_reregister_spec.rb @@ -0,0 +1,58 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2012 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' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_reregister_spec.rb. + +describe Chef::Knife::OscUserReregister do + before(:each) do + Chef::Knife::OscUserReregister.load_deps + @knife = Chef::Knife::OscUserReregister.new + @knife.name_args = [ 'a_user' ] + @user_mock = double('user_mock', :private_key => "private_key") + allow(Chef::OscUser).to receive(:load).and_return(@user_mock) + @stdout = StringIO.new + allow(@knife.ui).to receive(:stdout).and_return(@stdout) + end + + it 'prints usage and exits when a user name is not provided' do + @knife.name_args = [] + expect(@knife).to receive(:show_usage) + expect(@knife.ui).to receive(:fatal) + expect { @knife.run }.to raise_error(SystemExit) + end + + it 'reregisters the user and prints the key' do + expect(@user_mock).to receive(:reregister).and_return(@user_mock) + @knife.run + expect(@stdout.string).to match( /private_key/ ) + end + + it 'writes the private key to a file when --file is specified' do + expect(@user_mock).to receive(:reregister).and_return(@user_mock) + @knife.config[:file] = '/tmp/a_file' + filehandle = StringIO.new + expect(File).to receive(:open).with('/tmp/a_file', 'w').and_yield(filehandle) + @knife.run + expect(filehandle.string).to eq("private_key") + end +end diff --git a/spec/unit/knife/osc_user_show_spec.rb b/spec/unit/knife/osc_user_show_spec.rb new file mode 100644 index 0000000000..67b9b45809 --- /dev/null +++ b/spec/unit/knife/osc_user_show_spec.rb @@ -0,0 +1,46 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2012 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' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur user_show_spec.rb. + +describe Chef::Knife::OscUserShow do + before(:each) do + Chef::Knife::OscUserShow.load_deps + @knife = Chef::Knife::OscUserShow.new + @knife.name_args = [ 'my_user' ] + @user_mock = double('user_mock') + end + + it 'loads and displays the user' do + expect(Chef::OscUser).to receive(:load).with('my_user').and_return(@user_mock) + expect(@knife).to receive(:format_for_display).with(@user_mock) + @knife.run + end + + it 'prints usage and exits when a user name is not provided' do + @knife.name_args = [] + expect(@knife).to receive(:show_usage) + expect(@knife.ui).to receive(:fatal) + expect { @knife.run }.to raise_error(SystemExit) + end +end diff --git a/spec/unit/knife/user_create_spec.rb b/spec/unit/knife/user_create_spec.rb index ad8821cd0e..49d62cc2d7 100644 --- a/spec/unit/knife/user_create_spec.rb +++ b/spec/unit/knife/user_create_spec.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) 2012, 2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,68 +22,193 @@ require 'spec_helper' Chef::Knife::UserCreate.load_deps describe Chef::Knife::UserCreate do + let(:knife) { Chef::Knife::UserCreate.new } + + let(:stderr) { + StringIO.new + } + + let(:stdout) { + StringIO.new + } + before(:each) do - @knife = Chef::Knife::UserCreate.new - - @stdout = StringIO.new - @stderr = StringIO.new - allow(@knife.ui).to receive(:stdout).and_return(@stdout) - allow(@knife.ui).to receive(:stderr).and_return(@stderr) - - @knife.name_args = [ 'a_user' ] - @knife.config[:user_password] = "foobar" - @user = Chef::User.new - @user.name "a_user" - @user_with_private_key = Chef::User.new - @user_with_private_key.name "a_user" - @user_with_private_key.private_key 'private_key' - allow(@user).to receive(:create).and_return(@user_with_private_key) - allow(Chef::User).to receive(:new).and_return(@user) - allow(Chef::User).to receive(:from_hash).and_return(@user) - allow(@knife).to receive(:edit_data).and_return(@user.to_hash) + allow(knife.ui).to receive(:stdout).and_return(stdout) + allow(knife.ui).to receive(:stderr).and_return(stderr) + allow(knife.ui).to receive(:warn) end - it "creates a new user" do - expect(Chef::User).to receive(:new).and_return(@user) - expect(@user).to receive(:create) - @knife.run - expect(@stderr.string).to match /created user.+a_user/i - end + # delete this once OSC11 support is gone + context "when only one name_arg is passed" do + before do + knife.name_args = ['some_user'] + allow(knife).to receive(:run_osc_11_user_create).and_raise(SystemExit) + end + + it "displays the osc warning" do + expect(knife.ui).to receive(:warn).with(knife.osc_11_warning) + expect{ knife.run }.to raise_error(SystemExit) + end + + it "calls knife osc_user create" do + expect(knife).to receive(:run_osc_11_user_create) + expect{ knife.run }.to raise_error(SystemExit) + end - it "sets the password" do - @knife.config[:user_password] = "a_password" - expect(@user).to receive(:password).with("a_password") - @knife.run end - it "exits with an error if password is blank" do - @knife.config[:user_password] = '' - expect { @knife.run }.to raise_error SystemExit - expect(@stderr.string).to match /You must specify a non-blank password/ + context "when USERNAME isn't specified" do + # from spec/support/shared/unit/knife_shared.rb + it_should_behave_like "mandatory field missing" do + let(:name_args) { [] } + let(:fieldname) { 'username' } + end end - it "sets the user name" do - expect(@user).to receive(:name).with("a_user") - @knife.run + # uncomment once OSC11 support is gone, + # pending doesn't work for shared_examples_for by default + # + # context "when DISPLAY_NAME isn't specified" do + # # from spec/support/shared/unit/knife_shared.rb + # it_should_behave_like "mandatory field missing" do + # let(:name_args) { ['some_user'] } + # let(:fieldname) { 'display name' } + # end + # end + + context "when FIRST_NAME isn't specified" do + # from spec/support/shared/unit/knife_shared.rb + it_should_behave_like "mandatory field missing" do + let(:name_args) { ['some_user', 'some_display_name'] } + let(:fieldname) { 'first name' } + end end - it "sets the public key if given" do - @knife.config[:user_key] = "/a/filename" - allow(File).to receive(:read).with(File.expand_path("/a/filename")).and_return("a_key") - expect(@user).to receive(:public_key).with("a_key") - @knife.run + context "when LAST_NAME isn't specified" do + # from spec/support/shared/unit/knife_shared.rb + it_should_behave_like "mandatory field missing" do + let(:name_args) { ['some_user', 'some_display_name', 'some_first_name'] } + let(:fieldname) { 'last name' } + end end - it "allows you to edit the data" do - expect(@knife).to receive(:edit_data).with(@user) - @knife.run + context "when EMAIL isn't specified" do + # from spec/support/shared/unit/knife_shared.rb + it_should_behave_like "mandatory field missing" do + let(:name_args) { ['some_user', 'some_display_name', 'some_first_name', 'some_last_name'] } + let(:fieldname) { 'email' } + end end - it "writes the private key to a file when --file is specified" do - @knife.config[:file] = "/tmp/a_file" - filehandle = double("filehandle") - expect(filehandle).to receive(:print).with('private_key') - expect(File).to receive(:open).with("/tmp/a_file", "w").and_yield(filehandle) - @knife.run + context "when PASSWORD isn't specified" do + # from spec/support/shared/unit/knife_shared.rb + it_should_behave_like "mandatory field missing" do + let(:name_args) { ['some_user', 'some_display_name', 'some_first_name', 'some_last_name', 'some_email'] } + let(:fieldname) { 'password' } + end end + + context "when all mandatory fields are validly specified" do + before do + knife.name_args = ['some_user', 'some_display_name', 'some_first_name', 'some_last_name', 'some_email', 'some_password'] + allow(knife).to receive(:edit_data).and_return(knife.user.to_hash) + allow(knife).to receive(:create_user_from_hash).and_return(knife.user) + end + + before(:each) do + # reset the user field every run + knife.user_field = nil + end + + it "sets all the mandatory fields" do + knife.run + expect(knife.user.username).to eq('some_user') + expect(knife.user.display_name).to eq('some_display_name') + expect(knife.user.first_name).to eq('some_first_name') + expect(knife.user.last_name).to eq('some_last_name') + expect(knife.user.email).to eq('some_email') + expect(knife.user.password).to eq('some_password') + end + + context "when user_key and prevent_keygen are passed" do + before do + knife.config[:user_key] = "some_key" + knife.config[:prevent_keygen] = true + end + it "prints the usage" do + expect(knife).to receive(:show_usage) + expect { knife.run }.to raise_error(SystemExit) + end + + it "prints a relevant error message" do + expect { knife.run }.to raise_error(SystemExit) + expect(stderr.string).to match /You cannot pass --user-key and --prevent-keygen/ + end + end + + context "when --prevent-keygen is passed" do + before do + knife.config[:prevent_keygen] = true + end + + it "does not set user.create_key" do + knife.run + expect(knife.user.create_key).to be_falsey + end + end + + context "when --prevent-keygen is not passed" do + it "sets user.create_key to true" do + knife.run + expect(knife.user.create_key).to be_truthy + end + end + + context "when --user-key is passed" do + before do + knife.config[:user_key] = 'some_key' + allow(File).to receive(:read).and_return('some_key') + allow(File).to receive(:expand_path) + end + + it "sets user.public_key" do + knife.run + expect(knife.user.public_key).to eq('some_key') + end + end + + context "when --user-key is not passed" do + it "does not set user.public_key" do + knife.run + expect(knife.user.public_key).to be_nil + end + end + + context "when a private_key is returned" do + before do + allow(knife).to receive(:create_user_from_hash).and_return(Chef::User.from_hash(knife.user.to_hash.merge({"private_key" => "some_private_key"}))) + end + + context "when --file is passed" do + before do + knife.config[:file] = '/some/path' + end + + it "creates a new file of the path passed" do + filehandle = double('filehandle') + expect(filehandle).to receive(:print).with('some_private_key') + expect(File).to receive(:open).with('/some/path', 'w').and_yield(filehandle) + knife.run + end + end + + context "when --file is not passed" do + it "prints the private key to stdout" do + expect(knife.ui).to receive(:msg).with('some_private_key') + knife.run + end + end + end + + end # when all mandatory fields are validly specified end diff --git a/spec/unit/knife/user_delete_spec.rb b/spec/unit/knife/user_delete_spec.rb index 94cfbf3db1..e49c781358 100644 --- a/spec/unit/knife/user_delete_spec.rb +++ b/spec/unit/knife/user_delete_spec.rb @@ -19,21 +19,47 @@ require 'spec_helper' describe Chef::Knife::UserDelete do + let(:knife) { Chef::Knife::UserDelete.new } + let(:user) { double('user_object') } + let(:stdout) { StringIO.new } + before(:each) do Chef::Knife::UserDelete.load_deps - @knife = Chef::Knife::UserDelete.new - @knife.name_args = [ 'my_user' ] + knife.name_args = [ 'my_user' ] + allow(Chef::User).to receive(:load).and_return(user) + allow(user).to receive(:username).and_return('my_user') + allow(knife.ui).to receive(:stderr).and_return(stdout) + allow(knife.ui).to receive(:stdout).and_return(stdout) + end + + # delete this once OSC11 support is gone + context "when the username field is not supported by the server" do + before do + allow(knife).to receive(:run_osc_11_user_delete).and_raise(SystemExit) + allow(user).to receive(:username).and_return(nil) + end + + it "displays the osc warning" do + expect(knife.ui).to receive(:warn).with(knife.osc_11_warning) + expect{ knife.run }.to raise_error(SystemExit) + end + + it "forwards the command to knife osc_user edit" do + expect(knife).to receive(:run_osc_11_user_delete) + expect{ knife.run }.to raise_error(SystemExit) + end end it 'deletes the user' do - expect(@knife).to receive(:delete_object).with(Chef::User, 'my_user') - @knife.run + #expect(knife).to receive(:delete_object).with(Chef::User, 'my_user') + expect(knife).to receive(:delete_object).with('my_user') + knife.run end it 'prints usage and exits when a user name is not provided' do - @knife.name_args = [] - expect(@knife).to receive(:show_usage) - expect(@knife.ui).to receive(:fatal) - expect { @knife.run }.to raise_error(SystemExit) + knife.name_args = [] + expect(knife).to receive(:show_usage) + expect(knife.ui).to receive(:fatal) + expect { knife.run }.to raise_error(SystemExit) end end diff --git a/spec/unit/knife/user_edit_spec.rb b/spec/unit/knife/user_edit_spec.rb index 0eb75cfa9b..15a7726b20 100644 --- a/spec/unit/knife/user_edit_spec.rb +++ b/spec/unit/knife/user_edit_spec.rb @@ -19,29 +19,48 @@ require 'spec_helper' describe Chef::Knife::UserEdit do + let(:knife) { Chef::Knife::UserEdit.new } + before(:each) do @stderr = StringIO.new @stdout = StringIO.new Chef::Knife::UserEdit.load_deps - @knife = Chef::Knife::UserEdit.new - allow(@knife.ui).to receive(:stderr).and_return(@stderr) - allow(@knife.ui).to receive(:stdout).and_return(@stdout) - @knife.name_args = [ 'my_user' ] - @knife.config[:disable_editing] = true + allow(knife.ui).to receive(:stderr).and_return(@stderr) + allow(knife.ui).to receive(:stdout).and_return(@stdout) + knife.name_args = [ 'my_user' ] + knife.config[:disable_editing] = true + end + + # delete this once OSC11 support is gone + context "when the username field is not supported by the server" do + before do + allow(knife).to receive(:run_osc_11_user_edit).and_raise(SystemExit) + allow(Chef::User).to receive(:load).and_return({"username" => nil}) + end + + it "displays the osc warning" do + expect(knife.ui).to receive(:warn).with(knife.osc_11_warning) + expect{ knife.run }.to raise_error(SystemExit) + end + + it "forwards the command to knife osc_user edit" do + expect(knife).to receive(:run_osc_11_user_edit) + expect{ knife.run }.to raise_error(SystemExit) + end end it 'loads and edits the user' do - data = { :name => "my_user" } + data = { "username" => "my_user" } allow(Chef::User).to receive(:load).with("my_user").and_return(data) - expect(@knife).to receive(:edit_data).with(data).and_return(data) - @knife.run + expect(knife).to receive(:edit_data).with(data).and_return(data) + knife.run end it 'prints usage and exits when a user name is not provided' do - @knife.name_args = [] - expect(@knife).to receive(:show_usage) - expect(@knife.ui).to receive(:fatal) - expect { @knife.run }.to raise_error(SystemExit) + knife.name_args = [] + expect(knife).to receive(:show_usage) + expect(knife.ui).to receive(:fatal) + expect { knife.run }.to raise_error(SystemExit) end end diff --git a/spec/unit/knife/user_list_spec.rb b/spec/unit/knife/user_list_spec.rb index db097a5c16..9990cc802d 100644 --- a/spec/unit/knife/user_list_spec.rb +++ b/spec/unit/knife/user_list_spec.rb @@ -19,14 +19,18 @@ require 'spec_helper' describe Chef::Knife::UserList do + let(:knife) { Chef::Knife::UserList.new } + let(:stdout) { StringIO.new } + before(:each) do Chef::Knife::UserList.load_deps - @knife = Chef::Knife::UserList.new + allow(knife.ui).to receive(:stderr).and_return(stdout) + allow(knife.ui).to receive(:stdout).and_return(stdout) end it 'lists the users' do expect(Chef::User).to receive(:list) - expect(@knife).to receive(:format_list_for_display) - @knife.run + expect(knife).to receive(:format_list_for_display) + knife.run end end diff --git a/spec/unit/knife/user_reregister_spec.rb b/spec/unit/knife/user_reregister_spec.rb index 1268716f40..412a6ec374 100644 --- a/spec/unit/knife/user_reregister_spec.rb +++ b/spec/unit/knife/user_reregister_spec.rb @@ -19,35 +19,56 @@ require 'spec_helper' describe Chef::Knife::UserReregister do - before(:each) do + let(:knife) { Chef::Knife::UserReregister.new } + let(:user_mock) { double('user_mock', :private_key => "private_key") } + let(:stdout) { StringIO.new } + + before do Chef::Knife::UserReregister.load_deps - @knife = Chef::Knife::UserReregister.new - @knife.name_args = [ 'a_user' ] - @user_mock = double('user_mock', :private_key => "private_key") - allow(Chef::User).to receive(:load).and_return(@user_mock) - @stdout = StringIO.new - allow(@knife.ui).to receive(:stdout).and_return(@stdout) + knife.name_args = [ 'a_user' ] + allow(Chef::User).to receive(:load).and_return(user_mock) + allow(knife.ui).to receive(:stdout).and_return(stdout) + allow(knife.ui).to receive(:stderr).and_return(stdout) + allow(user_mock).to receive(:username).and_return('a_user') + end + + # delete this once OSC11 support is gone + context "when the username field is not supported by the server" do + before do + allow(knife).to receive(:run_osc_11_user_reregister).and_raise(SystemExit) + allow(user_mock).to receive(:username).and_return(nil) + end + + it "displays the osc warning" do + expect(knife.ui).to receive(:warn).with(knife.osc_11_warning) + expect{ knife.run }.to raise_error(SystemExit) + end + + it "forwards the command to knife osc_user edit" do + expect(knife).to receive(:run_osc_11_user_reregister) + expect{ knife.run }.to raise_error(SystemExit) + end end it 'prints usage and exits when a user name is not provided' do - @knife.name_args = [] - expect(@knife).to receive(:show_usage) - expect(@knife.ui).to receive(:fatal) - expect { @knife.run }.to raise_error(SystemExit) + knife.name_args = [] + expect(knife).to receive(:show_usage) + expect(knife.ui).to receive(:fatal) + expect { knife.run }.to raise_error(SystemExit) end it 'reregisters the user and prints the key' do - expect(@user_mock).to receive(:reregister).and_return(@user_mock) - @knife.run - expect(@stdout.string).to match( /private_key/ ) + expect(user_mock).to receive(:reregister).and_return(user_mock) + knife.run + expect(stdout.string).to match( /private_key/ ) end it 'writes the private key to a file when --file is specified' do - expect(@user_mock).to receive(:reregister).and_return(@user_mock) - @knife.config[:file] = '/tmp/a_file' + expect(user_mock).to receive(:reregister).and_return(user_mock) + knife.config[:file] = '/tmp/a_file' filehandle = StringIO.new expect(File).to receive(:open).with('/tmp/a_file', 'w').and_yield(filehandle) - @knife.run + knife.run expect(filehandle.string).to eq("private_key") end end diff --git a/spec/unit/knife/user_show_spec.rb b/spec/unit/knife/user_show_spec.rb index f97cbc3f13..43392a3a5c 100644 --- a/spec/unit/knife/user_show_spec.rb +++ b/spec/unit/knife/user_show_spec.rb @@ -19,23 +19,47 @@ require 'spec_helper' describe Chef::Knife::UserShow do - before(:each) do + let(:knife) { Chef::Knife::UserShow.new } + let(:user_mock) { double('user_mock') } + let(:stdout) { StringIO.new } + + before do Chef::Knife::UserShow.load_deps - @knife = Chef::Knife::UserShow.new - @knife.name_args = [ 'my_user' ] - @user_mock = double('user_mock') + knife.name_args = [ 'my_user' ] + allow(user_mock).to receive(:username).and_return('my_user') + allow(knife.ui).to receive(:stderr).and_return(stdout) + allow(knife.ui).to receive(:stdout).and_return(stdout) + end + + # delete this once OSC11 support is gone + context "when the username field is not supported by the server" do + before do + allow(knife).to receive(:run_osc_11_user_show).and_raise(SystemExit) + allow(Chef::User).to receive(:load).with('my_user').and_return(user_mock) + allow(user_mock).to receive(:username).and_return(nil) + end + + it "displays the osc warning" do + expect(knife.ui).to receive(:warn).with(knife.osc_11_warning) + expect{ knife.run }.to raise_error(SystemExit) + end + + it "forwards the command to knife osc_user edit" do + expect(knife).to receive(:run_osc_11_user_show) + expect{ knife.run }.to raise_error(SystemExit) + end end it 'loads and displays the user' do - expect(Chef::User).to receive(:load).with('my_user').and_return(@user_mock) - expect(@knife).to receive(:format_for_display).with(@user_mock) - @knife.run + expect(Chef::User).to receive(:load).with('my_user').and_return(user_mock) + expect(knife).to receive(:format_for_display).with(user_mock) + knife.run end it 'prints usage and exits when a user name is not provided' do - @knife.name_args = [] - expect(@knife).to receive(:show_usage) - expect(@knife.ui).to receive(:fatal) - expect { @knife.run }.to raise_error(SystemExit) + knife.name_args = [] + expect(knife).to receive(:show_usage) + expect(knife.ui).to receive(:fatal) + expect { knife.run }.to raise_error(SystemExit) end end diff --git a/spec/unit/knife_spec.rb b/spec/unit/knife_spec.rb index 3e8a43eaf5..022256f370 100644 --- a/spec/unit/knife_spec.rb +++ b/spec/unit/knife_spec.rb @@ -140,7 +140,7 @@ describe Chef::Knife do 'X-Chef-Version' => Chef::VERSION, "Host"=>"api.opscode.piab", "X-REMOTE-REQUEST-ID"=>request_id, - 'X-Ops-Server-API-Version' => Chef::HTTP::Authenticator::SERVER_API_VERSION}} + 'X-Ops-Server-API-Version' => Chef::HTTP::Authenticator::DEFAULT_SERVER_API_VERSION}} let(:request_id) {"1234"} @@ -399,11 +399,17 @@ describe Chef::Knife do it "formats 406s (non-supported API version error) nicely" do response = Net::HTTPNotAcceptable.new("1.1", "406", "Not Acceptable") response.instance_variable_set(:@read, true) # I hate you, net/http. - allow(response).to receive(:body).and_return(Chef::JSONCompat.to_json(:error => "sad trombone", :min_version => "0", :max_version => "1")) + + # set the header + response["x-ops-server-api-version"] = Chef::JSONCompat.to_json(:min_version => "0", :max_version => "1", :request_version => "10000000") + + allow(response).to receive(:body).and_return(Chef::JSONCompat.to_json(:error => "sad trombone")) allow(knife).to receive(:run).and_raise(Net::HTTPServerException.new("406 Not Acceptable", response)) + knife.run_with_pretty_exceptions - expect(stderr.string).to include('The version of Chef that Knife is using is not supported by the Chef server you sent this request to') - expect(stderr.string).to include("This version of Chef requires a server API version of #{Chef::HTTP::Authenticator::SERVER_API_VERSION}") + expect(stderr.string).to include('The request that Knife sent was using API version 10000000') + expect(stderr.string).to include('The Chef server you sent the request to supports a min API verson of 0 and a max API version of 1') + expect(stderr.string).to include('Please either update your Chef client or server to be a compatible set') end it "formats 500s nicely" do diff --git a/spec/unit/mixin/api_version_request_handling_spec.rb b/spec/unit/mixin/api_version_request_handling_spec.rb new file mode 100644 index 0000000000..cc5340e424 --- /dev/null +++ b/spec/unit/mixin/api_version_request_handling_spec.rb @@ -0,0 +1,127 @@ +# +# 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. +# + +require 'spec_helper' + +describe Chef::Mixin::ApiVersionRequestHandling do + let(:dummy_class) { Class.new { include Chef::Mixin::ApiVersionRequestHandling } } + let(:object) { dummy_class.new } + + describe ".server_client_api_version_intersection" do + let(:default_supported_client_versions) { [0,1,2] } + + + context "when the response code is not 406" do + let(:response) { OpenStruct.new(:code => '405') } + let(:exception) { Net::HTTPServerException.new("405 Something Else", response) } + + it "returns nil" do + expect(object.server_client_api_version_intersection(exception, default_supported_client_versions)). + to be_nil + end + + end # when the response code is not 406 + + context "when the response code is 406" do + let(:response) { OpenStruct.new(:code => '406') } + let(:exception) { Net::HTTPServerException.new("406 Not Acceptable", response) } + + context "when x-ops-server-api-version header does not exist" do + it "returns nil" do + expect(object.server_client_api_version_intersection(exception, default_supported_client_versions)). + to be_nil + end + end # when x-ops-server-api-version header does not exist + + context "when x-ops-server-api-version header exists" do + let(:min_server_version) { 2 } + let(:max_server_version) { 4 } + let(:return_hash) { + { + "min_version" => min_server_version, + "max_version" => max_server_version + } + } + + before(:each) do + allow(response).to receive(:[]).with('x-ops-server-api-version').and_return(Chef::JSONCompat.to_json(return_hash)) + end + + context "when there is no intersection between client and server versions" do + shared_examples_for "no intersection between client and server versions" do + it "return an array" do + expect(object.server_client_api_version_intersection(exception, supported_client_versions)). + to be_a_kind_of(Array) + end + + it "returns an empty array" do + expect(object.server_client_api_version_intersection(exception, supported_client_versions).length). + to eq(0) + end + + end + + context "when all the versions are higher than the max" do + it_should_behave_like "no intersection between client and server versions" do + let(:supported_client_versions) { [5,6,7] } + end + end + + context "when all the versions are lower than the min" do + it_should_behave_like "no intersection between client and server versions" do + let(:supported_client_versions) { [0,1] } + end + end + + end # when there is no intersection between client and server versions + + context "when there is an intersection between client and server versions" do + context "when multiple versions intersect" do + let(:supported_client_versions) { [1,2,3,4,5] } + + it "includes all of the intersection" do + expect(object.server_client_api_version_intersection(exception, supported_client_versions)). + to eq([2,3,4]) + end + end # when multiple versions intersect + + context "when only the min client version intersects" do + let(:supported_client_versions) { [0,1,2] } + + it "includes the intersection" do + expect(object.server_client_api_version_intersection(exception, supported_client_versions)). + to eq([2]) + end + end # when only the min client version intersects + + context "when only the max client version intersects" do + let(:supported_client_versions) { [4,5,6] } + + it "includes the intersection" do + expect(object.server_client_api_version_intersection(exception, supported_client_versions)). + to eq([4]) + end + end # when only the max client version intersects + + end # when there is an intersection between client and server versions + + end # when x-ops-server-api-version header exists + end # when the response code is 406 + + end # .server_client_api_version_intersection +end # Chef::Mixin::ApiVersionRequestHandling diff --git a/spec/unit/osc_user_spec.rb b/spec/unit/osc_user_spec.rb new file mode 100644 index 0000000000..678486a16d --- /dev/null +++ b/spec/unit/osc_user_spec.rb @@ -0,0 +1,276 @@ +# +# Author:: Steven Danna (steve@opscode.com) +# Copyright:: Copyright (c) 2012 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. +# + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_spec.rb. + +require 'spec_helper' + +require 'chef/osc_user' +require 'tempfile' + +describe Chef::OscUser do + before(:each) do + @user = Chef::OscUser.new + end + + describe "initialize" do + it "should be a Chef::OscUser" do + expect(@user).to be_a_kind_of(Chef::OscUser) + end + end + + describe "name" do + it "should let you set the name to a string" do + expect(@user.name("ops_master")).to eq("ops_master") + end + + it "should return the current name" do + @user.name "ops_master" + expect(@user.name).to eq("ops_master") + end + + # It is not feasible to check all invalid characters. Here are a few + # that we probably care about. + it "should not accept invalid characters" do + # capital letters + expect { @user.name "Bar" }.to raise_error(ArgumentError) + # slashes + expect { @user.name "foo/bar" }.to raise_error(ArgumentError) + # ? + expect { @user.name "foo?" }.to raise_error(ArgumentError) + # & + expect { @user.name "foo&" }.to raise_error(ArgumentError) + end + + + it "should not accept spaces" do + expect { @user.name "ops master" }.to raise_error(ArgumentError) + end + + it "should throw an ArgumentError if you feed it anything but a string" do + expect { @user.name Hash.new }.to raise_error(ArgumentError) + end + end + + describe "admin" do + it "should let you set the admin bit" do + expect(@user.admin(true)).to eq(true) + end + + it "should return the current admin value" do + @user.admin true + expect(@user.admin).to eq(true) + end + + it "should default to false" do + expect(@user.admin).to eq(false) + end + + it "should throw an ArgumentError if you feed it anything but true or false" do + expect { @user.name Hash.new }.to raise_error(ArgumentError) + end + end + + describe "public_key" do + it "should let you set the public key" do + expect(@user.public_key("super public")).to eq("super public") + end + + it "should return the current public key" do + @user.public_key("super public") + expect(@user.public_key).to eq("super public") + end + + it "should throw an ArgumentError if you feed it something lame" do + expect { @user.public_key Hash.new }.to raise_error(ArgumentError) + end + end + + describe "private_key" do + it "should let you set the private key" do + expect(@user.private_key("super private")).to eq("super private") + end + + it "should return the private key" do + @user.private_key("super private") + expect(@user.private_key).to eq("super private") + end + + it "should throw an ArgumentError if you feed it something lame" do + expect { @user.private_key Hash.new }.to raise_error(ArgumentError) + end + end + + describe "when serializing to JSON" do + before(:each) do + @user.name("black") + @user.public_key("crowes") + @json = @user.to_json + end + + it "serializes as a JSON object" do + expect(@json).to match(/^\{.+\}$/) + end + + it "includes the name value" do + expect(@json).to include(%q{"name":"black"}) + end + + it "includes the public key value" do + expect(@json).to include(%{"public_key":"crowes"}) + end + + it "includes the 'admin' flag" do + expect(@json).to include(%q{"admin":false}) + end + + it "includes the private key when present" do + @user.private_key("monkeypants") + expect(@user.to_json).to include(%q{"private_key":"monkeypants"}) + end + + it "does not include the private key if not present" do + expect(@json).not_to include("private_key") + end + + it "includes the password if present" do + @user.password "password" + expect(@user.to_json).to include(%q{"password":"password"}) + end + + it "does not include the password if not present" do + expect(@json).not_to include("password") + end + + include_examples "to_json equalivent to Chef::JSONCompat.to_json" do + let(:jsonable) { @user } + end + end + + describe "when deserializing from JSON" do + before(:each) do + user = { "name" => "mr_spinks", + "public_key" => "turtles", + "private_key" => "pandas", + "password" => "password", + "admin" => true } + @user = Chef::OscUser.from_json(Chef::JSONCompat.to_json(user)) + end + + it "should deserialize to a Chef::OscUser object" do + expect(@user).to be_a_kind_of(Chef::OscUser) + end + + it "preserves the name" do + expect(@user.name).to eq("mr_spinks") + end + + it "preserves the public key" do + expect(@user.public_key).to eq("turtles") + end + + it "preserves the admin status" do + expect(@user.admin).to be_truthy + end + + it "includes the private key if present" do + expect(@user.private_key).to eq("pandas") + end + + it "includes the password if present" do + expect(@user.password).to eq("password") + end + + end + + describe "API Interactions" do + before (:each) do + @user = Chef::OscUser.new + @user.name "foobar" + @http_client = double("Chef::REST mock") + allow(Chef::REST).to receive(:new).and_return(@http_client) + end + + describe "list" do + before(:each) do + Chef::Config[:chef_server_url] = "http://www.example.com" + @osc_response = { "admin" => "http://www.example.com/users/admin"} + @ohc_response = [ { "user" => { "username" => "admin" }} ] + allow(Chef::OscUser).to receive(:load).with("admin").and_return(@user) + @osc_inflated_response = { "admin" => @user } + end + + it "lists all clients on an OSC server" do + allow(@http_client).to receive(:get_rest).with("users").and_return(@osc_response) + expect(Chef::OscUser.list).to eq(@osc_response) + end + + it "inflate all clients on an OSC server" do + allow(@http_client).to receive(:get_rest).with("users").and_return(@osc_response) + expect(Chef::OscUser.list(true)).to eq(@osc_inflated_response) + end + + it "lists all clients on an OHC/OPC server" do + allow(@http_client).to receive(:get_rest).with("users").and_return(@ohc_response) + # We expect that Chef::OscUser.list will give a consistent response + # so OHC API responses should be transformed to OSC-style output. + expect(Chef::OscUser.list).to eq(@osc_response) + end + + it "inflate all clients on an OHC/OPC server" do + allow(@http_client).to receive(:get_rest).with("users").and_return(@ohc_response) + expect(Chef::OscUser.list(true)).to eq(@osc_inflated_response) + end + end + + describe "create" do + it "creates a new user via the API" do + @user.password "password" + expect(@http_client).to receive(:post_rest).with("users", {:name => "foobar", :admin => false, :password => "password"}).and_return({}) + @user.create + end + end + + describe "read" do + it "loads a named user from the API" do + expect(@http_client).to receive(:get_rest).with("users/foobar").and_return({"name" => "foobar", "admin" => true, "public_key" => "pubkey"}) + user = Chef::OscUser.load("foobar") + expect(user.name).to eq("foobar") + expect(user.admin).to eq(true) + expect(user.public_key).to eq("pubkey") + end + end + + describe "update" do + it "updates an existing user on via the API" do + expect(@http_client).to receive(:put_rest).with("users/foobar", {:name => "foobar", :admin => false}).and_return({}) + @user.update + end + end + + describe "destroy" do + it "deletes the specified user via the API" do + expect(@http_client).to receive(:delete_rest).with("users/foobar") + @user.destroy + end + end + end +end diff --git a/spec/unit/rest_spec.rb b/spec/unit/rest_spec.rb index b4f8f336a9..3b04981610 100644 --- a/spec/unit/rest_spec.rb +++ b/spec/unit/rest_spec.rb @@ -69,8 +69,8 @@ describe Chef::REST do rest end - let(:standard_read_headers) {{"Accept"=>"application/json", "Accept"=>"application/json", "Accept-Encoding"=>"gzip;q=1.0,deflate;q=0.6,identity;q=0.3", "X-REMOTE-REQUEST-ID"=>request_id, 'X-Ops-Server-API-Version' => Chef::HTTP::Authenticator::SERVER_API_VERSION}} - let(:standard_write_headers) {{"Accept"=>"application/json", "Content-Type"=>"application/json", "Accept"=>"application/json", "Accept-Encoding"=>"gzip;q=1.0,deflate;q=0.6,identity;q=0.3", "X-REMOTE-REQUEST-ID"=>request_id, 'X-Ops-Server-API-Version' => Chef::HTTP::Authenticator::SERVER_API_VERSION}} + let(:standard_read_headers) {{"Accept"=>"application/json", "Accept"=>"application/json", "Accept-Encoding"=>"gzip;q=1.0,deflate;q=0.6,identity;q=0.3", "X-REMOTE-REQUEST-ID"=>request_id, 'X-Ops-Server-API-Version' => Chef::HTTP::Authenticator::DEFAULT_SERVER_API_VERSION}} + let(:standard_write_headers) {{"Accept"=>"application/json", "Content-Type"=>"application/json", "Accept"=>"application/json", "Accept-Encoding"=>"gzip;q=1.0,deflate;q=0.6,identity;q=0.3", "X-REMOTE-REQUEST-ID"=>request_id, 'X-Ops-Server-API-Version' => Chef::HTTP::Authenticator::DEFAULT_SERVER_API_VERSION}} before(:each) do Chef::Log.init(log_stringio) @@ -292,7 +292,7 @@ describe Chef::REST do 'Accept-Encoding' => Chef::REST::RESTRequest::ENCODING_GZIP_DEFLATE, 'Host' => host_header, 'X-REMOTE-REQUEST-ID' => request_id, - 'X-Ops-Server-API-Version' => Chef::HTTP::Authenticator::SERVER_API_VERSION + 'X-Ops-Server-API-Version' => Chef::HTTP::Authenticator::DEFAULT_SERVER_API_VERSION } end @@ -575,7 +575,7 @@ describe Chef::REST do 'Accept-Encoding' => Chef::REST::RESTRequest::ENCODING_GZIP_DEFLATE, 'Host' => host_header, 'X-REMOTE-REQUEST-ID'=> request_id, - 'X-Ops-Server-API-Version' => Chef::HTTP::Authenticator::SERVER_API_VERSION + 'X-Ops-Server-API-Version' => Chef::HTTP::Authenticator::DEFAULT_SERVER_API_VERSION } expect(Net::HTTP::Get).to receive(:new).with("/?foo=bar", expected_headers).and_return(request_mock) rest.streaming_request(url, {}) @@ -587,7 +587,7 @@ describe Chef::REST do 'Accept-Encoding' => Chef::REST::RESTRequest::ENCODING_GZIP_DEFLATE, 'Host' => host_header, 'X-REMOTE-REQUEST-ID'=> request_id, - 'X-Ops-Server-API-Version' => Chef::HTTP::Authenticator::SERVER_API_VERSION + 'X-Ops-Server-API-Version' => Chef::HTTP::Authenticator::DEFAULT_SERVER_API_VERSION } expect(Net::HTTP::Get).to receive(:new).with("/?foo=bar", expected_headers).and_return(request_mock) rest.streaming_request(url, {}) diff --git a/spec/unit/user_spec.rb b/spec/unit/user_spec.rb index d451531b16..57822df7e3 100644 --- a/spec/unit/user_spec.rb +++ b/spec/unit/user_spec.rb @@ -26,98 +26,141 @@ describe Chef::User do @user = Chef::User.new end + shared_examples_for "string fields with no contraints" do + it "should let you set the public key" do + expect(@user.send(method, "some_string")).to eq("some_string") + end + + it "should return the current public key" do + @user.send(method, "some_string") + expect(@user.send(method)).to eq("some_string") + end + + it "should throw an ArgumentError if you feed it something lame" do + expect { @user.send(method, Hash.new) }.to raise_error(ArgumentError) + end + end + + shared_examples_for "boolean fields with no constraints" do + it "should let you set the field" do + expect(@user.send(method, true)).to eq(true) + end + + it "should return the current field value" do + @user.send(method, true) + expect(@user.send(method)).to eq(true) + end + + it "should return the false value when false" do + @user.send(method, false) + expect(@user.send(method)).to eq(false) + end + + it "should throw an ArgumentError if you feed it anything but true or false" do + expect { @user.send(method, Hash.new) }.to raise_error(ArgumentError) + end + end + describe "initialize" do it "should be a Chef::User" do expect(@user).to be_a_kind_of(Chef::User) end end - describe "name" do - it "should let you set the name to a string" do - expect(@user.name("ops_master")).to eq("ops_master") + describe "username" do + it "should let you set the username to a string" do + expect(@user.username("ops_master")).to eq("ops_master") end - it "should return the current name" do - @user.name "ops_master" - expect(@user.name).to eq("ops_master") + it "should return the current username" do + @user.username "ops_master" + expect(@user.username).to eq("ops_master") end # It is not feasible to check all invalid characters. Here are a few # that we probably care about. it "should not accept invalid characters" do # capital letters - expect { @user.name "Bar" }.to raise_error(ArgumentError) + expect { @user.username "Bar" }.to raise_error(ArgumentError) # slashes - expect { @user.name "foo/bar" }.to raise_error(ArgumentError) + expect { @user.username "foo/bar" }.to raise_error(ArgumentError) # ? - expect { @user.name "foo?" }.to raise_error(ArgumentError) + expect { @user.username "foo?" }.to raise_error(ArgumentError) # & - expect { @user.name "foo&" }.to raise_error(ArgumentError) + expect { @user.username "foo&" }.to raise_error(ArgumentError) end it "should not accept spaces" do - expect { @user.name "ops master" }.to raise_error(ArgumentError) + expect { @user.username "ops master" }.to raise_error(ArgumentError) end it "should throw an ArgumentError if you feed it anything but a string" do - expect { @user.name Hash.new }.to raise_error(ArgumentError) + expect { @user.username Hash.new }.to raise_error(ArgumentError) end end - describe "admin" do - it "should let you set the admin bit" do - expect(@user.admin(true)).to eq(true) - end - - it "should return the current admin value" do - @user.admin true - expect(@user.admin).to eq(true) + describe "boolean fields" do + describe "create_key" do + it_should_behave_like "boolean fields with no constraints" do + let(:method) { :create_key } + end end + end - it "should default to false" do - expect(@user.admin).to eq(false) + describe "string fields" do + describe "public_key" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :public_key } + end end - it "should throw an ArgumentError if you feed it anything but true or false" do - expect { @user.name Hash.new }.to raise_error(ArgumentError) + describe "private_key" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :private_key } + end end - end - describe "public_key" do - it "should let you set the public key" do - expect(@user.public_key("super public")).to eq("super public") + describe "display_name" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :display_name } + end end - it "should return the current public key" do - @user.public_key("super public") - expect(@user.public_key).to eq("super public") + describe "first_name" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :first_name } + end end - it "should throw an ArgumentError if you feed it something lame" do - expect { @user.public_key Hash.new }.to raise_error(ArgumentError) + describe "middle_name" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :middle_name } + end end - end - describe "private_key" do - it "should let you set the private key" do - expect(@user.private_key("super private")).to eq("super private") + describe "last_name" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :last_name } + end end - it "should return the private key" do - @user.private_key("super private") - expect(@user.private_key).to eq("super private") + describe "email" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :email } + end end - it "should throw an ArgumentError if you feed it something lame" do - expect { @user.private_key Hash.new }.to raise_error(ArgumentError) + describe "password" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :password } + end end end describe "when serializing to JSON" do before(:each) do - @user.name("black") - @user.public_key("crowes") + @user.username("black") @json = @user.to_json end @@ -125,16 +168,62 @@ describe Chef::User do expect(@json).to match(/^\{.+\}$/) end - it "includes the name value" do - expect(@json).to include(%q{"name":"black"}) + it "includes the username value" do + expect(@json).to include(%q{"username":"black"}) + end + + it "includes the display name when present" do + @user.display_name("get_displayed") + expect(@user.to_json).to include(%{"display_name":"get_displayed"}) + end + + it "does not include the display name if not present" do + expect(@json).not_to include("display_name") end - it "includes the public key value" do - expect(@json).to include(%{"public_key":"crowes"}) + it "includes the first name when present" do + @user.first_name("char") + expect(@user.to_json).to include(%{"first_name":"char"}) end - it "includes the 'admin' flag" do - expect(@json).to include(%q{"admin":false}) + it "does not include the first name if not present" do + expect(@json).not_to include("first_name") + end + + it "includes the middle name when present" do + @user.middle_name("man") + expect(@user.to_json).to include(%{"middle_name":"man"}) + end + + it "does not include the middle name if not present" do + expect(@json).not_to include("middle_name") + end + + it "includes the last name when present" do + @user.last_name("der") + expect(@user.to_json).to include(%{"last_name":"der"}) + end + + it "does not include the last name if not present" do + expect(@json).not_to include("last_name") + end + + it "includes the email when present" do + @user.email("charmander@pokemon.poke") + expect(@user.to_json).to include(%{"email":"charmander@pokemon.poke"}) + end + + it "does not include the email if not present" do + expect(@json).not_to include("email") + end + + it "includes the public key when present" do + @user.public_key("crowes") + expect(@user.to_json).to include(%{"public_key":"crowes"}) + end + + it "does not include the public key if not present" do + expect(@json).not_to include("public_key") end it "includes the private key when present" do @@ -162,11 +251,18 @@ describe Chef::User do describe "when deserializing from JSON" do before(:each) do - user = { "name" => "mr_spinks", + user = { + "username" => "mr_spinks", + "display_name" => "displayed", + "first_name" => "char", + "middle_name" => "man", + "last_name" => "der", + "email" => "charmander@pokemon.poke", + "password" => "password", "public_key" => "turtles", "private_key" => "pandas", - "password" => "password", - "admin" => true } + "create_key" => false + } @user = Chef::User.from_json(Chef::JSONCompat.to_json(user)) end @@ -174,32 +270,275 @@ describe Chef::User do expect(@user).to be_a_kind_of(Chef::User) end - it "preserves the name" do - expect(@user.name).to eq("mr_spinks") + it "preserves the username" do + expect(@user.username).to eq("mr_spinks") end - it "preserves the public key" do - expect(@user.public_key).to eq("turtles") + it "preserves the display name if present" do + expect(@user.display_name).to eq("displayed") end - it "preserves the admin status" do - expect(@user.admin).to be_truthy + it "preserves the first name if present" do + expect(@user.first_name).to eq("char") end - it "includes the private key if present" do - expect(@user.private_key).to eq("pandas") + it "preserves the middle name if present" do + expect(@user.middle_name).to eq("man") + end + + it "preserves the last name if present" do + expect(@user.last_name).to eq("der") + end + + it "preserves the email if present" do + expect(@user.email).to eq("charmander@pokemon.poke") end it "includes the password if present" do expect(@user.password).to eq("password") end + it "preserves the public key if present" do + expect(@user.public_key).to eq("turtles") + end + + it "includes the private key if present" do + expect(@user.private_key).to eq("pandas") + end + + it "includes the create key status if not nil" do + expect(@user.create_key).to be_falsey + end end + describe "Versioned API Interactions" do + let(:response_406) { OpenStruct.new(:code => '406') } + let(:exception_406) { Net::HTTPServerException.new("406 Not Acceptable", response_406) } + + before (:each) do + @user = Chef::User.new + allow(@user).to receive(:chef_root_rest_v0).and_return(double('chef rest root v0 object')) + allow(@user).to receive(:chef_root_rest_v1).and_return(double('chef rest root v1 object')) + end + + describe "update" do + before do + # populate all fields that are valid between V0 and V1 + @user.username "some_username" + @user.display_name "some_display_name" + @user.first_name "some_first_name" + @user.middle_name "some_middle_name" + @user.last_name "some_last_name" + @user.email "some_email" + @user.password "some_password" + end + + let(:payload) { + { + :username => "some_username", + :display_name => "some_display_name", + :first_name => "some_first_name", + :middle_name => "some_middle_name", + :last_name => "some_last_name", + :email => "some_email", + :password => "some_password" + } + } + + context "when server API V1 is valid on the Chef Server receiving the request" do + context "when the user submits valid data" do + it "properly updates the user" do + expect(@user.chef_root_rest_v1).to receive(:put).with("users/some_username", payload).and_return({}) + @user.update + end + end + end + + context "when server API V1 is not valid on the Chef Server receiving the request" do + let(:payload) { + { + :username => "some_username", + :display_name => "some_display_name", + :first_name => "some_first_name", + :middle_name => "some_middle_name", + :last_name => "some_last_name", + :email => "some_email", + :password => "some_password", + :public_key => "some_public_key" + } + } + + before do + @user.public_key "some_public_key" + allow(@user.chef_root_rest_v1).to receive(:put) + end + + context "when the server returns a 400" do + let(:response_400) { OpenStruct.new(:code => '400') } + let(:exception_400) { Net::HTTPServerException.new("400 Bad Request", response_400) } + + context "when the 400 was due to public / private key fields no longer being supported" do + let(:response_body_400) { '{"error":["Since Server API v1, all keys must be updated via the keys endpoint. "]}' } + + before do + allow(response_400).to receive(:body).and_return(response_body_400) + allow(@user.chef_root_rest_v1).to receive(:put).and_raise(exception_400) + end + + it "proceeds with the V0 PUT since it can handle public / private key fields" do + expect(@user.chef_root_rest_v0).to receive(:put).with("users/some_username", payload).and_return({}) + @user.update + end + + it "does not call server_client_api_version_intersection, since we know to proceed with V0 in this case" do + expect(@user).to_not receive(:server_client_api_version_intersection) + allow(@user.chef_root_rest_v0).to receive(:put).and_return({}) + @user.update + end + end # when the 400 was due to public / private key fields + + context "when the 400 was NOT due to public / private key fields no longer being supported" do + let(:response_body_400) { '{"error":["Some other error. "]}' } + + before do + allow(response_400).to receive(:body).and_return(response_body_400) + allow(@user.chef_root_rest_v1).to receive(:put).and_raise(exception_400) + end + + it "will not proceed with the V0 PUT since the original bad request was not key related" do + expect(@user.chef_root_rest_v0).to_not receive(:put).with("users/some_username", payload) + expect { @user.update }.to raise_error(exception_400) + end + + it "raises the original error" do + expect { @user.update }.to raise_error(exception_400) + end + + end + end # when the server returns a 400 + + context "when the server returns a 406" do + # from spec/support/shared/unit/api_versioning.rb + it_should_behave_like "version handling" do + let(:object) { @user } + let(:method) { :update } + let(:http_verb) { :put } + let(:rest_v1) { @user.chef_root_rest_v1 } + end + + context "when the server supports API V0" do + before do + allow(@user).to receive(:server_client_api_version_intersection).and_return([0]) + allow(@user.chef_root_rest_v1).to receive(:put).and_raise(exception_406) + end + + it "properly updates the user" do + expect(@user.chef_root_rest_v0).to receive(:put).with("users/some_username", payload).and_return({}) + @user.update + end + end # when the server supports API V0 + end # when the server returns a 406 + + end # when server API V1 is not valid on the Chef Server receiving the request + end # update + + describe "create" do + let(:payload) { + { + :username => "some_username", + :display_name => "some_display_name", + :first_name => "some_first_name", + :last_name => "some_last_name", + :email => "some_email", + :password => "some_password" + } + } + before do + @user.username "some_username" + @user.display_name "some_display_name" + @user.first_name "some_first_name" + @user.last_name "some_last_name" + @user.email "some_email" + @user.password "some_password" + end + + # from spec/support/shared/unit/user_and_client_shared.rb + it_should_behave_like "user or client create" do + let(:object) { @user } + let(:error) { Chef::Exceptions::InvalidUserAttribute } + let(:rest_v0) { @user.chef_root_rest_v0 } + let(:rest_v1) { @user.chef_root_rest_v1 } + let(:url) { "users" } + end + + context "when handling API V1" do + it "creates a new user via the API with a middle_name when it exists" do + @user.middle_name "some_middle_name" + expect(@user.chef_root_rest_v1).to receive(:post).with("users", payload.merge({:middle_name => "some_middle_name"})).and_return({}) + @user.create + end + end # when server API V1 is valid on the Chef Server receiving the request + + context "when API V1 is not supported by the server" do + # from spec/support/shared/unit/api_versioning.rb + it_should_behave_like "version handling" do + let(:object) { @user } + let(:method) { :create } + let(:http_verb) { :post } + let(:rest_v1) { @user.chef_root_rest_v1 } + end + end + + context "when handling API V0" do + before do + allow(@user).to receive(:server_client_api_version_intersection).and_return([0]) + allow(@user.chef_root_rest_v1).to receive(:post).and_raise(exception_406) + end + + it "creates a new user via the API with a middle_name when it exists" do + @user.middle_name "some_middle_name" + expect(@user.chef_root_rest_v0).to receive(:post).with("users", payload.merge({:middle_name => "some_middle_name"})).and_return({}) + @user.create + end + end # when server API V1 is not valid on the Chef Server receiving the request + + end # create + + # DEPRECATION + # This can be removed after API V0 support is gone + describe "reregister" do + let(:payload) { + { + "username" => "some_username", + } + } + + before do + @user.username "some_username" + end + + context "when server API V0 is valid on the Chef Server receiving the request" do + it "creates a new object via the API" do + expect(@user.chef_root_rest_v0).to receive(:put).with("users/#{@user.username}", payload.merge({"private_key" => true})).and_return({}) + @user.reregister + end + end # when server API V0 is valid on the Chef Server receiving the request + + context "when server API V0 is not supported by the Chef Server" do + # from spec/support/shared/unit/api_versioning.rb + it_should_behave_like "user and client reregister" do + let(:object) { @user } + let(:rest_v0) { @user.chef_root_rest_v0 } + end + end # when server API V0 is not supported by the Chef Server + end # reregister + + end # Versioned API Interactions + describe "API Interactions" do before (:each) do @user = Chef::User.new - @user.name "foobar" + @user.username "foobar" @http_client = double("Chef::REST mock") allow(Chef::REST).to receive(:new).and_return(@http_client) end @@ -213,57 +552,31 @@ describe Chef::User do @osc_inflated_response = { "admin" => @user } end - it "lists all clients on an OSC server" do - allow(@http_client).to receive(:get_rest).with("users").and_return(@osc_response) - expect(Chef::User.list).to eq(@osc_response) - end - - it "inflate all clients on an OSC server" do - allow(@http_client).to receive(:get_rest).with("users").and_return(@osc_response) - expect(Chef::User.list(true)).to eq(@osc_inflated_response) - end - it "lists all clients on an OHC/OPC server" do - allow(@http_client).to receive(:get_rest).with("users").and_return(@ohc_response) + allow(@http_client).to receive(:get).with("users").and_return(@ohc_response) # We expect that Chef::User.list will give a consistent response # so OHC API responses should be transformed to OSC-style output. expect(Chef::User.list).to eq(@osc_response) end it "inflate all clients on an OHC/OPC server" do - allow(@http_client).to receive(:get_rest).with("users").and_return(@ohc_response) + allow(@http_client).to receive(:get).with("users").and_return(@ohc_response) expect(Chef::User.list(true)).to eq(@osc_inflated_response) end end - describe "create" do - it "creates a new user via the API" do - @user.password "password" - expect(@http_client).to receive(:post_rest).with("users", {:name => "foobar", :admin => false, :password => "password"}).and_return({}) - @user.create - end - end - describe "read" do it "loads a named user from the API" do - expect(@http_client).to receive(:get_rest).with("users/foobar").and_return({"name" => "foobar", "admin" => true, "public_key" => "pubkey"}) + expect(@http_client).to receive(:get).with("users/foobar").and_return({"username" => "foobar", "admin" => true, "public_key" => "pubkey"}) user = Chef::User.load("foobar") - expect(user.name).to eq("foobar") - expect(user.admin).to eq(true) + expect(user.username).to eq("foobar") expect(user.public_key).to eq("pubkey") end end - describe "update" do - it "updates an existing user on via the API" do - expect(@http_client).to receive(:put_rest).with("users/foobar", {:name => "foobar", :admin => false}).and_return({}) - @user.update - end - end - describe "destroy" do it "deletes the specified user via the API" do - expect(@http_client).to receive(:delete_rest).with("users/foobar") + expect(@http_client).to receive(:delete).with("users/foobar") @user.destroy end end |