diff options
31 files changed, 1122 insertions, 1118 deletions
diff --git a/lib/chef/knife/osc_user_create.rb b/lib/chef/knife/osc_user_create.rb index c368296040..6c3415473f 100644 --- a/lib/chef/knife/osc_user_create.rb +++ b/lib/chef/knife/osc_user_create.rb @@ -27,7 +27,7 @@ class Chef class OscUserCreate < Knife deps do - require 'chef/osc_user' + require 'chef/user' require 'chef/json_compat' end @@ -69,7 +69,7 @@ class Chef exit 1 end - user = Chef::OscUser.new + user = Chef::User.new user.name(@user_name) user.admin(config[:admin]) user.password config[:user_password] @@ -79,7 +79,7 @@ class Chef end output = edit_data(user) - user = Chef::OscUser.from_hash(output).create + user = Chef::User.from_hash(output).create ui.info("Created #{user}") if user.private_key diff --git a/lib/chef/knife/osc_user_delete.rb b/lib/chef/knife/osc_user_delete.rb index d6fbd4a6a9..5cd4f10413 100644 --- a/lib/chef/knife/osc_user_delete.rb +++ b/lib/chef/knife/osc_user_delete.rb @@ -28,7 +28,7 @@ class Chef class OscUserDelete < Knife deps do - require 'chef/osc_user' + require 'chef/user' require 'chef/json_compat' end @@ -43,7 +43,7 @@ class Chef exit 1 end - delete_object(Chef::OscUser, @user_name) + delete_object(Chef::User, @user_name) end end diff --git a/lib/chef/knife/osc_user_edit.rb b/lib/chef/knife/osc_user_edit.rb index 4c38674d08..526475db05 100644 --- a/lib/chef/knife/osc_user_edit.rb +++ b/lib/chef/knife/osc_user_edit.rb @@ -28,7 +28,7 @@ class Chef class OscUserEdit < Knife deps do - require 'chef/osc_user' + require 'chef/user' require 'chef/json_compat' end @@ -43,10 +43,10 @@ class Chef exit 1 end - original_user = Chef::OscUser.load(@user_name).to_hash + original_user = Chef::User.load(@user_name).to_hash edited_user = edit_data(original_user) if original_user != edited_user - user = Chef::OscUser.from_hash(edited_user) + user = Chef::User.from_hash(edited_user) user.update ui.msg("Saved #{user}.") else diff --git a/lib/chef/knife/osc_user_list.rb b/lib/chef/knife/osc_user_list.rb index 92f049cd19..84fca31899 100644 --- a/lib/chef/knife/osc_user_list.rb +++ b/lib/chef/knife/osc_user_list.rb @@ -28,7 +28,7 @@ class Chef class OscUserList < Knife deps do - require 'chef/osc_user' + require 'chef/user' require 'chef/json_compat' end @@ -40,7 +40,7 @@ class Chef :description => "Show corresponding URIs" def run - output(format_list_for_display(Chef::OscUser.list)) + output(format_list_for_display(Chef::User.list)) end end end diff --git a/lib/chef/knife/osc_user_reregister.rb b/lib/chef/knife/osc_user_reregister.rb index a71e0aa677..163b286fe0 100644 --- a/lib/chef/knife/osc_user_reregister.rb +++ b/lib/chef/knife/osc_user_reregister.rb @@ -28,7 +28,7 @@ class Chef class OscUserReregister < Knife deps do - require 'chef/osc_user' + require 'chef/user' require 'chef/json_compat' end @@ -48,7 +48,7 @@ class Chef exit 1 end - user = Chef::OscUser.load(@user_name).reregister + user = Chef::User.load(@user_name).reregister Chef::Log.debug("Updated user data: #{user.inspect}") key = user.private_key if config[:file] diff --git a/lib/chef/knife/osc_user_show.rb b/lib/chef/knife/osc_user_show.rb index 6a41ddae88..cb3a77585a 100644 --- a/lib/chef/knife/osc_user_show.rb +++ b/lib/chef/knife/osc_user_show.rb @@ -30,7 +30,7 @@ class Chef include Knife::Core::MultiAttributeReturnOption deps do - require 'chef/osc_user' + require 'chef/user' require 'chef/json_compat' end @@ -45,7 +45,7 @@ class Chef exit 1 end - user = Chef::OscUser.load(@user_name) + user = Chef::User.load(@user_name) output(format_for_display(user)) end diff --git a/lib/chef/knife/user_create.rb b/lib/chef/knife/user_create.rb index e73f6be8b6..995573cd03 100644 --- a/lib/chef/knife/user_create.rb +++ b/lib/chef/knife/user_create.rb @@ -27,7 +27,7 @@ class Chef attr_accessor :user_field deps do - require 'chef/user' + require 'chef/user_v1' require 'chef/json_compat' end @@ -61,11 +61,11 @@ class Chef banner "knife user create USERNAME DISPLAY_NAME FIRST_NAME LAST_NAME EMAIL PASSWORD (options)" def user - @user_field ||= Chef::User.new + @user_field ||= Chef::UserV1.new end def create_user_from_hash(hash) - Chef::User.from_hash(hash).create + Chef::UserV1.from_hash(hash).create end def osc_11_warning diff --git a/lib/chef/knife/user_delete.rb b/lib/chef/knife/user_delete.rb index 803be6b90c..828cd51588 100644 --- a/lib/chef/knife/user_delete.rb +++ b/lib/chef/knife/user_delete.rb @@ -23,7 +23,7 @@ class Chef class UserDelete < Knife deps do - require 'chef/user' + require 'chef/user_v1' require 'chef/json_compat' end @@ -55,7 +55,7 @@ EOF if Kernel.block_given? object = block.call else - object = Chef::User.load(user_name) + object = Chef::UserV1.load(user_name) object.destroy end @@ -77,10 +77,10 @@ EOF # 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) + # delete_object(Chef::UserV1, @user_name) # # Also delete our override of delete_object above - object = Chef::User.load(@user_name) + object = Chef::UserV1.load(@user_name) # OSC 11 case if object.username.nil? diff --git a/lib/chef/knife/user_edit.rb b/lib/chef/knife/user_edit.rb index d194f6697b..c3a4326ee8 100644 --- a/lib/chef/knife/user_edit.rb +++ b/lib/chef/knife/user_edit.rb @@ -23,7 +23,7 @@ class Chef class UserEdit < Knife deps do - require 'chef/user' + require 'chef/user_v1' require 'chef/json_compat' end @@ -56,7 +56,7 @@ EOF exit 1 end - original_user = Chef::User.load(@user_name).to_hash + original_user = Chef::UserV1.load(@user_name).to_hash # DEPRECATION NOTE # Remove this if statement and corrosponding code post OSC 11 support. # @@ -68,7 +68,7 @@ EOF 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 = Chef::UserV1.from_hash(edited_user) user.update ui.msg("Saved #{user}.") else diff --git a/lib/chef/knife/user_list.rb b/lib/chef/knife/user_list.rb index 7ae43dadc9..6a130392b9 100644 --- a/lib/chef/knife/user_list.rb +++ b/lib/chef/knife/user_list.rb @@ -25,7 +25,7 @@ class Chef class UserList < Knife deps do - require 'chef/user' + require 'chef/user_v1' require 'chef/json_compat' end @@ -37,7 +37,7 @@ class Chef :description => "Show corresponding URIs" def run - output(format_list_for_display(Chef::User.list)) + output(format_list_for_display(Chef::UserV1.list)) end end diff --git a/lib/chef/knife/user_reregister.rb b/lib/chef/knife/user_reregister.rb index eab2245025..09fd1cd2d6 100644 --- a/lib/chef/knife/user_reregister.rb +++ b/lib/chef/knife/user_reregister.rb @@ -23,7 +23,7 @@ class Chef class UserReregister < Knife deps do - require 'chef/user' + require 'chef/user_v1' require 'chef/json_compat' end @@ -61,7 +61,7 @@ EOF exit 1 end - user = Chef::User.load(@user_name) + user = Chef::UserV1.load(@user_name) # DEPRECATION NOTE # Remove this if statement and corrosponding code post OSC 11 support. diff --git a/lib/chef/knife/user_show.rb b/lib/chef/knife/user_show.rb index f5e81e9972..3a2443471a 100644 --- a/lib/chef/knife/user_show.rb +++ b/lib/chef/knife/user_show.rb @@ -25,7 +25,7 @@ class Chef include Knife::Core::MultiAttributeReturnOption deps do - require 'chef/user' + require 'chef/user_v1' require 'chef/json_compat' end @@ -58,7 +58,7 @@ EOF exit 1 end - user = Chef::User.load(@user_name) + user = Chef::UserV1.load(@user_name) # DEPRECATION NOTE # Remove this if statement and corrosponding code post OSC 11 support. diff --git a/lib/chef/osc_user.rb b/lib/chef/osc_user.rb deleted file mode 100644 index 52bfd11108..0000000000 --- a/lib/chef/osc_user.rb +++ /dev/null @@ -1,194 +0,0 @@ -# -# 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/user.rb b/lib/chef/user.rb index 717deb63c3..bc9705c092 100644 --- a/lib/chef/user.rb +++ b/lib/chef/user.rb @@ -21,85 +21,44 @@ 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) +# TODO +# DEPRECATION NOTE +# This class will be replaced by Chef::UserV1 in Chef 13. 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. # -# 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. +# Chef::UserV1 now supports Chef Server 12 and will be moved to this namespace in Chef 13. # -# Exception: self.list is backwards compatible with OSC 11 +# New development should occur in Chef::UserV1. +# This file and corrosponding osc_user knife files +# should be removed once client support for Open Source Chef Server 11 expires. class Chef class User include Chef::Mixin::FromFile include Chef::Mixin::ParamsValidate - include Chef::Mixin::ApiVersionRequestHandling - - SUPPORTED_API_VERSIONS = [0,1] def initialize - @username = nil - @display_name = nil - @first_name = nil - @middle_name = nil - @last_name = nil - @email = nil - @password = nil + @name = '' @public_key = nil @private_key = nil - @create_key = nil @password = nil + @admin = false end - 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"}) + def chef_rest_v0 + @chef_rest_v0 ||= Chef::ServerAPI.new(Chef::Config[:chef_server_url], {:api_version => "0"}) end - def username(arg=nil) - set_or_return(:username, arg, + def name(arg=nil) + set_or_return(:name, arg, :regex => /^[a-z0-9\-_]+$/) end - def display_name(arg=nil) - set_or_return(:display_name, - arg, :kind_of => String) - end - - def first_name(arg=nil) - set_or_return(:first_name, - arg, :kind_of => String) - end - - def middle_name(arg=nil) - set_or_return(:middle_name, - arg, :kind_of => String) - end - - def last_name(arg=nil) - set_or_return(:last_name, - arg, :kind_of => String) - end - - def email(arg=nil) - set_or_return(:email, - arg, :kind_of => String) - end - - def password(arg=nil) - set_or_return(:password, - arg, :kind_of => String) - end - - def create_key(arg=nil) - set_or_return(:create_key, arg, - :kind_of => [TrueClass, FalseClass]) + def admin(arg=nil) + set_or_return(:admin, + arg, :kind_of => [TrueClass, FalseClass]) end def public_key(arg=nil) @@ -119,17 +78,12 @@ class Chef def to_hash result = { - "username" => @username + "name" => @name, + "public_key" => @public_key, + "admin" => @admin } - 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["private_key"] = @private_key if @private_key + result["password"] = @password if @password result end @@ -138,86 +92,21 @@ class Chef end def destroy - # will default to the current API version (Chef::Authenticator::DEFAULT_SERVER_API_VERSION) - Chef::REST.new(Chef::Config[:chef_server_url]).delete("users/#{@username}") + chef_rest_v0.delete_rest("users/#{@name}") end def create - # 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 - + payload = {:name => self.name, :admin => self.admin, :password => self.password } + payload[:public_key] = public_key if public_key + new_user = chef_rest_v0.post_rest("users", payload) Chef::User.from_hash(self.to_hash.merge(new_user)) end def update(new_key=false) - 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 + payload = {:name => name, :admin => admin} + payload[:private_key] = new_key if new_key + payload[:password] = password if password + updated_user = chef_rest_v0.put_rest("users/#{name}", payload) Chef::User.from_hash(self.to_hash.merge(updated_user)) end @@ -233,47 +122,30 @@ class Chef end end - # Note: remove after API v0 no longer supported by client (and knife command). def reregister - 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 + reregistered_self = chef_rest_v0.put_rest("users/#{name}", { :name => name, :admin => admin, :private_key => true }) + private_key(reregistered_self["private_key"]) self end def to_s - "user[#{@username}]" + "user[#{@name}]" + end + + def inspect + "Chef::User name:'#{name}' admin:'#{admin.inspect}'" + + "public_key:'#{public_key}' private_key:#{private_key}" end # Class Methods def self.from_hash(user_hash) user = Chef::User.new - 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'] if user_hash.key?('public_key') + user.name user_hash['name'] 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.password user_hash['password'] if user_hash.key?('password') + user.public_key user_hash['public_key'] + user.admin user_hash['admin'] user end @@ -286,19 +158,12 @@ class Chef end def self.list(inflate=false) - response = Chef::REST.new(Chef::Config[:chef_server_url]).get('users') + response = Chef::ServerAPI.new(Chef::Config[:chef_server_url], {:api_version => "0"}).get_rest('users') users = if response.is_a?(Array) - # 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 - + transform_ohc_list_response(response) # OHC/OPC + else + response # OSC + end if inflate users.inject({}) do |user_map, (name, _url)| user_map[name] = Chef::User.load(name) @@ -309,9 +174,8 @@ class Chef end end - 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}") + def self.load(name) + response = Chef::ServerAPI.new(Chef::Config[:chef_server_url], {:api_version => "0"}).get_rest("users/#{name}") Chef::User.from_hash(response) end @@ -319,7 +183,7 @@ class Chef # [ { "user" => { "username" => USERNAME }}, ...] # into the form # { "USERNAME" => "URI" } - def self.transform_list_response(response) + def self.transform_ohc_list_response(response) new_response = Hash.new response.each do |u| name = u['user']['username'] @@ -328,7 +192,6 @@ class Chef new_response end - private_class_method :transform_list_response - + private_class_method :transform_ohc_list_response end end diff --git a/lib/chef/user_v1.rb b/lib/chef/user_v1.rb new file mode 100644 index 0000000000..31cb0576a2 --- /dev/null +++ b/lib/chef/user_v1.rb @@ -0,0 +1,335 @@ +# +# 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' +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::UserV1 is no longer expected to support Open Source Chef 11 Server requests. +# The object that handles those requests remain in the Chef::User namespace. +# This code will be moved to the Chef::User namespace as of Chef 13. +# +# Exception: self.list is backwards compatible with OSC 11 +class Chef + class UserV1 + + include Chef::Mixin::FromFile + include Chef::Mixin::ParamsValidate + include Chef::Mixin::ApiVersionRequestHandling + + SUPPORTED_API_VERSIONS = [0,1] + + def initialize + @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 + end + + 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 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) + 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 = { + "username" => @username + } + 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 + + def to_json(*a) + Chef::JSONCompat.to_json(to_hash, *a) + end + + def destroy + # 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 + # 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::UserV1.from_hash(self.to_hash.merge(new_user)) + end + + def update(new_key=false) + 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::UserV1.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 + + # Note: remove after API v0 no longer supported by client (and knife command). + def reregister + 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[#{@username}]" + end + + # Class Methods + + def self.from_hash(user_hash) + user = Chef::UserV1.new + 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'] 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 + + def self.from_json(json) + Chef::UserV1.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('users') + users = if response.is_a?(Array) + # 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::UserV1.load(name) + user_map + end + else + users + end + end + + 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::UserV1.from_hash(response) + end + + # Gross. Transforms an API response in the form of: + # [ { "user" => { "username" => USERNAME }}, ...] + # into the form + # { "USERNAME" => "URI" } + def self.transform_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_list_response + + end +end diff --git a/spec/support/shared/unit/api_versioning.rb b/spec/support/shared/unit/api_versioning.rb index a4f353de60..05a4117f8e 100644 --- a/spec/support/shared/unit/api_versioning.rb +++ b/spec/support/shared/unit/api_versioning.rb @@ -26,7 +26,7 @@ shared_examples_for "version handling" 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 + context "when the server does not support the min or max server API version that Chef::UserV1 supports" do before do allow(object).to receive(:server_client_api_version_intersection).and_return([]) end @@ -34,7 +34,7 @@ shared_examples_for "version handling" do 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 # when the server does not support the min or max server API version that Chef::UserV1 supports end # version handling shared_examples_for "user and client reregister" do diff --git a/spec/unit/knife/osc_user_create_spec.rb b/spec/unit/knife/osc_user_create_spec.rb index 1b17d0d22f..e4ed78fe2b 100644 --- a/spec/unit/knife/osc_user_create_spec.rb +++ b/spec/unit/knife/osc_user_create_spec.rb @@ -36,19 +36,19 @@ describe Chef::Knife::OscUserCreate do @knife.name_args = [ 'a_user' ] @knife.config[:user_password] = "foobar" - @user = Chef::OscUser.new + @user = Chef::User.new @user.name "a_user" - @user_with_private_key = Chef::OscUser.new + @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::OscUser).to receive(:new).and_return(@user) - allow(Chef::OscUser).to receive(:from_hash).and_return(@user) + 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) end it "creates a new user" do - expect(Chef::OscUser).to receive(:new).and_return(@user) + 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 diff --git a/spec/unit/knife/osc_user_delete_spec.rb b/spec/unit/knife/osc_user_delete_spec.rb index 0e16393ffe..4a3ec4228f 100644 --- a/spec/unit/knife/osc_user_delete_spec.rb +++ b/spec/unit/knife/osc_user_delete_spec.rb @@ -31,7 +31,7 @@ describe Chef::Knife::OscUserDelete do end it 'deletes the user' do - expect(@knife).to receive(:delete_object).with(Chef::OscUser, 'my_user') + expect(@knife).to receive(:delete_object).with(Chef::User, 'my_user') @knife.run end diff --git a/spec/unit/knife/osc_user_edit_spec.rb b/spec/unit/knife/osc_user_edit_spec.rb index 71a9192389..279f2e30ef 100644 --- a/spec/unit/knife/osc_user_edit_spec.rb +++ b/spec/unit/knife/osc_user_edit_spec.rb @@ -38,7 +38,7 @@ describe Chef::Knife::OscUserEdit do it 'loads and edits the user' do data = { :name => "my_user" } - allow(Chef::OscUser).to receive(:load).with("my_user").and_return(data) + 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 end diff --git a/spec/unit/knife/osc_user_list_spec.rb b/spec/unit/knife/osc_user_list_spec.rb index 59a15be058..f496a414b8 100644 --- a/spec/unit/knife/osc_user_list_spec.rb +++ b/spec/unit/knife/osc_user_list_spec.rb @@ -30,7 +30,7 @@ describe Chef::Knife::OscUserList do end it 'lists the users' do - expect(Chef::OscUser).to receive(:list) + expect(Chef::User).to receive(:list) expect(@knife).to receive(:format_list_for_display) @knife.run end diff --git a/spec/unit/knife/osc_user_reregister_spec.rb b/spec/unit/knife/osc_user_reregister_spec.rb index 406bbf1f3e..989eb180f1 100644 --- a/spec/unit/knife/osc_user_reregister_spec.rb +++ b/spec/unit/knife/osc_user_reregister_spec.rb @@ -29,7 +29,7 @@ describe Chef::Knife::OscUserReregister do @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) + allow(Chef::User).to receive(:load).and_return(@user_mock) @stdout = StringIO.new allow(@knife.ui).to receive(:stdout).and_return(@stdout) end diff --git a/spec/unit/knife/osc_user_show_spec.rb b/spec/unit/knife/osc_user_show_spec.rb index 67b9b45809..18d2086099 100644 --- a/spec/unit/knife/osc_user_show_spec.rb +++ b/spec/unit/knife/osc_user_show_spec.rb @@ -32,7 +32,7 @@ describe Chef::Knife::OscUserShow do end it 'loads and displays the user' do - expect(Chef::OscUser).to receive(:load).with('my_user').and_return(@user_mock) + 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 diff --git a/spec/unit/knife/user_create_spec.rb b/spec/unit/knife/user_create_spec.rb index 49d62cc2d7..fa5c8324b4 100644 --- a/spec/unit/knife/user_create_spec.rb +++ b/spec/unit/knife/user_create_spec.rb @@ -186,7 +186,7 @@ describe Chef::Knife::UserCreate do 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"}))) + allow(knife).to receive(:create_user_from_hash).and_return(Chef::UserV1.from_hash(knife.user.to_hash.merge({"private_key" => "some_private_key"}))) end context "when --file is passed" do diff --git a/spec/unit/knife/user_delete_spec.rb b/spec/unit/knife/user_delete_spec.rb index e49c781358..a24160624a 100644 --- a/spec/unit/knife/user_delete_spec.rb +++ b/spec/unit/knife/user_delete_spec.rb @@ -26,7 +26,7 @@ describe Chef::Knife::UserDelete do before(:each) do Chef::Knife::UserDelete.load_deps knife.name_args = [ 'my_user' ] - allow(Chef::User).to receive(:load).and_return(user) + allow(Chef::UserV1).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) @@ -51,7 +51,7 @@ describe Chef::Knife::UserDelete do end it 'deletes the user' do - #expect(knife).to receive(:delete_object).with(Chef::User, 'my_user') + #expect(knife).to receive(:delete_object).with(Chef::UserV1, 'my_user') expect(knife).to receive(:delete_object).with('my_user') knife.run end diff --git a/spec/unit/knife/user_edit_spec.rb b/spec/unit/knife/user_edit_spec.rb index 15a7726b20..a21d982d29 100644 --- a/spec/unit/knife/user_edit_spec.rb +++ b/spec/unit/knife/user_edit_spec.rb @@ -36,7 +36,7 @@ describe Chef::Knife::UserEdit do 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}) + allow(Chef::UserV1).to receive(:load).and_return({"username" => nil}) end it "displays the osc warning" do @@ -52,7 +52,7 @@ describe Chef::Knife::UserEdit do it 'loads and edits the user' do data = { "username" => "my_user" } - allow(Chef::User).to receive(:load).with("my_user").and_return(data) + allow(Chef::UserV1).to receive(:load).with("my_user").and_return(data) expect(knife).to receive(:edit_data).with(data).and_return(data) knife.run end diff --git a/spec/unit/knife/user_list_spec.rb b/spec/unit/knife/user_list_spec.rb index 9990cc802d..fa2bac426e 100644 --- a/spec/unit/knife/user_list_spec.rb +++ b/spec/unit/knife/user_list_spec.rb @@ -29,7 +29,7 @@ describe Chef::Knife::UserList do end it 'lists the users' do - expect(Chef::User).to receive(:list) + expect(Chef::UserV1).to receive(:list) expect(knife).to receive(:format_list_for_display) knife.run end diff --git a/spec/unit/knife/user_reregister_spec.rb b/spec/unit/knife/user_reregister_spec.rb index 412a6ec374..89aa6726cd 100644 --- a/spec/unit/knife/user_reregister_spec.rb +++ b/spec/unit/knife/user_reregister_spec.rb @@ -26,7 +26,7 @@ describe Chef::Knife::UserReregister do before do Chef::Knife::UserReregister.load_deps knife.name_args = [ 'a_user' ] - allow(Chef::User).to receive(:load).and_return(user_mock) + allow(Chef::UserV1).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') diff --git a/spec/unit/knife/user_show_spec.rb b/spec/unit/knife/user_show_spec.rb index 43392a3a5c..7c39e428c0 100644 --- a/spec/unit/knife/user_show_spec.rb +++ b/spec/unit/knife/user_show_spec.rb @@ -35,7 +35,7 @@ describe Chef::Knife::UserShow do 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(Chef::UserV1).to receive(:load).with('my_user').and_return(user_mock) allow(user_mock).to receive(:username).and_return(nil) end @@ -51,7 +51,7 @@ describe Chef::Knife::UserShow do end it 'loads and displays the user' do - expect(Chef::User).to receive(:load).with('my_user').and_return(user_mock) + expect(Chef::UserV1).to receive(:load).with('my_user').and_return(user_mock) expect(knife).to receive(:format_for_display).with(user_mock) knife.run end diff --git a/spec/unit/osc_user_spec.rb b/spec/unit/osc_user_spec.rb deleted file mode 100644 index 16731b47bd..0000000000 --- a/spec/unit/osc_user_spec.rb +++ /dev/null @@ -1,276 +0,0 @@ -# -# 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 equivalent 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/user_spec.rb b/spec/unit/user_spec.rb index 5222c951b3..777a4e6ef0 100644 --- a/spec/unit/user_spec.rb +++ b/spec/unit/user_spec.rb @@ -16,6 +16,11 @@ # 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/user' @@ -26,141 +31,98 @@ 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 "username" do - it "should let you set the username to a string" do - expect(@user.username("ops_master")).to eq("ops_master") + 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 username" do - @user.username "ops_master" - expect(@user.username).to eq("ops_master") + 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.username "Bar" }.to raise_error(ArgumentError) + expect { @user.name "Bar" }.to raise_error(ArgumentError) # slashes - expect { @user.username "foo/bar" }.to raise_error(ArgumentError) + expect { @user.name "foo/bar" }.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) + expect { @user.name "foo&" }.to raise_error(ArgumentError) end it "should not accept spaces" do - expect { @user.username "ops master" }.to raise_error(ArgumentError) + 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.username Hash.new }.to raise_error(ArgumentError) + expect { @user.name Hash.new }.to raise_error(ArgumentError) end end - describe "boolean fields" do - describe "create_key" do - it_should_behave_like "boolean fields with no constraints" do - let(:method) { :create_key } - end + describe "admin" do + it "should let you set the admin bit" do + expect(@user.admin(true)).to eq(true) end - end - describe "string fields" do - describe "public_key" do - it_should_behave_like "string fields with no contraints" do - let(:method) { :public_key } - end + it "should return the current admin value" do + @user.admin true + expect(@user.admin).to eq(true) end - describe "private_key" do - it_should_behave_like "string fields with no contraints" do - let(:method) { :private_key } - end + it "should default to false" do + expect(@user.admin).to eq(false) end - describe "display_name" do - it_should_behave_like "string fields with no contraints" do - let(:method) { :display_name } - 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 "first_name" do - it_should_behave_like "string fields with no contraints" do - let(:method) { :first_name } - 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 - describe "middle_name" do - it_should_behave_like "string fields with no contraints" do - let(:method) { :middle_name } - end + it "should return the current public key" do + @user.public_key("super public") + expect(@user.public_key).to eq("super public") end - describe "last_name" do - it_should_behave_like "string fields with no contraints" do - let(:method) { :last_name } - 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 "email" do - it_should_behave_like "string fields with no contraints" do - let(:method) { :email } - 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 - describe "password" do - it_should_behave_like "string fields with no contraints" do - let(:method) { :password } - 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.username("black") + @user.name("black") + @user.public_key("crowes") @json = @user.to_json end @@ -168,62 +130,16 @@ describe Chef::User do expect(@json).to match(/^\{.+\}$/) end - 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 first name when present" do - @user.first_name("char") - expect(@user.to_json).to include(%{"first_name":"char"}) - end - - 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") + it "includes the name value" do + expect(@json).to include(%q{"name":"black"}) end - it "includes the public key when present" do - @user.public_key("crowes") - expect(@user.to_json).to include(%{"public_key":"crowes"}) + it "includes the public key value" do + expect(@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") + it "includes the 'admin' flag" do + expect(@json).to include(%q{"admin":false}) end it "includes the private key when present" do @@ -251,18 +167,11 @@ describe Chef::User do describe "when deserializing from JSON" do before(:each) do - user = { - "username" => "mr_spinks", - "display_name" => "displayed", - "first_name" => "char", - "middle_name" => "man", - "last_name" => "der", - "email" => "charmander@pokemon.poke", - "password" => "password", + user = { "name" => "mr_spinks", "public_key" => "turtles", "private_key" => "pandas", - "create_key" => false - } + "password" => "password", + "admin" => true } @user = Chef::User.from_json(Chef::JSONCompat.to_json(user)) end @@ -270,277 +179,34 @@ describe Chef::User do expect(@user).to be_a_kind_of(Chef::User) end - it "preserves the username" do - expect(@user.username).to eq("mr_spinks") - end - - it "preserves the display name if present" do - expect(@user.display_name).to eq("displayed") + it "preserves the name" do + expect(@user.name).to eq("mr_spinks") end - it "preserves the first name if present" do - expect(@user.first_name).to eq("char") - end - - it "preserves the middle name if present" do - expect(@user.middle_name).to eq("man") + it "preserves the public key" do + expect(@user.public_key).to eq("turtles") end - it "preserves the last name if present" do - expect(@user.last_name).to eq("der") + it "preserves the admin status" do + expect(@user.admin).to be_truthy end - it "preserves the email if present" do - expect(@user.email).to eq("charmander@pokemon.poke") + 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 - 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.username "foobar" - @http_client = double("Chef::REST mock") - allow(Chef::REST).to receive(:new).and_return(@http_client) + @user.name "foobar" + @http_client = double("Chef::ServerAPI mock") + allow(Chef::ServerAPI).to receive(:new).and_return(@http_client) end describe "list" do @@ -552,31 +218,57 @@ 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).with("users").and_return(@ohc_response) + allow(@http_client).to receive(:get_rest).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).with("users").and_return(@ohc_response) + allow(@http_client).to receive(:get_rest).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).with("users/foobar").and_return({"username" => "foobar", "admin" => true, "public_key" => "pubkey"}) + expect(@http_client).to receive(:get_rest).with("users/foobar").and_return({"name" => "foobar", "admin" => true, "public_key" => "pubkey"}) user = Chef::User.load("foobar") - expect(user.username).to eq("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).with("users/foobar") + expect(@http_client).to receive(:delete_rest).with("users/foobar") @user.destroy end end diff --git a/spec/unit/user_v1_spec.rb b/spec/unit/user_v1_spec.rb new file mode 100644 index 0000000000..8fd370a010 --- /dev/null +++ b/spec/unit/user_v1_spec.rb @@ -0,0 +1,584 @@ +# +# 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' + +require 'chef/user_v1' +require 'tempfile' + +describe Chef::UserV1 do + before(:each) do + @user = Chef::UserV1.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::UserV1" do + expect(@user).to be_a_kind_of(Chef::UserV1) + end + end + + 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 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.username "Bar" }.to raise_error(ArgumentError) + # slashes + expect { @user.username "foo/bar" }.to raise_error(ArgumentError) + # ? + expect { @user.username "foo?" }.to raise_error(ArgumentError) + # & + expect { @user.username "foo&" }.to raise_error(ArgumentError) + end + + + it "should not accept spaces" do + 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.username Hash.new }.to raise_error(ArgumentError) + end + end + + 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 + + describe "string fields" do + describe "public_key" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :public_key } + end + end + + describe "private_key" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :private_key } + end + end + + describe "display_name" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :display_name } + end + end + + describe "first_name" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :first_name } + end + end + + describe "middle_name" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :middle_name } + end + end + + describe "last_name" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :last_name } + end + end + + describe "email" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :email } + end + end + + 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.username("black") + @json = @user.to_json + end + + it "serializes as a JSON object" do + expect(@json).to match(/^\{.+\}$/) + end + + 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 first name when present" do + @user.first_name("char") + expect(@user.to_json).to include(%{"first_name":"char"}) + end + + 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 + @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 equivalent to Chef::JSONCompat.to_json" do + let(:jsonable) { @user } + end + end + + describe "when deserializing from JSON" do + before(:each) do + 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", + "create_key" => false + } + @user = Chef::UserV1.from_json(Chef::JSONCompat.to_json(user)) + end + + it "should deserialize to a Chef::UserV1 object" do + expect(@user).to be_a_kind_of(Chef::UserV1) + end + + it "preserves the username" do + expect(@user.username).to eq("mr_spinks") + end + + it "preserves the display name if present" do + expect(@user.display_name).to eq("displayed") + end + + it "preserves the first name if present" do + expect(@user.first_name).to eq("char") + end + + 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::UserV1.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::UserV1.new + @user.username "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::UserV1).to receive(:load).with("admin").and_return(@user) + @osc_inflated_response = { "admin" => @user } + end + + it "lists all clients on an OHC/OPC server" do + allow(@http_client).to receive(:get).with("users").and_return(@ohc_response) + # We expect that Chef::UserV1.list will give a consistent response + # so OHC API responses should be transformed to OSC-style output. + expect(Chef::UserV1.list).to eq(@osc_response) + end + + it "inflate all clients on an OHC/OPC server" do + allow(@http_client).to receive(:get).with("users").and_return(@ohc_response) + expect(Chef::UserV1.list(true)).to eq(@osc_inflated_response) + end + end + + describe "read" do + it "loads a named user from the API" do + expect(@http_client).to receive(:get).with("users/foobar").and_return({"username" => "foobar", "admin" => true, "public_key" => "pubkey"}) + user = Chef::UserV1.load("foobar") + expect(user.username).to eq("foobar") + expect(user.public_key).to eq("pubkey") + end + end + + describe "destroy" do + it "deletes the specified user via the API" do + expect(@http_client).to receive(:delete).with("users/foobar") + @user.destroy + end + end + end +end |