summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTyler Cloke <tylercloke@gmail.com>2015-06-05 15:16:01 -0700
committerTyler Cloke <tylercloke@gmail.com>2015-06-05 15:16:01 -0700
commiteb590e371b5e5d3f4d1d201dd3f58e4351116f41 (patch)
tree04c47c21618a4889b79d2eaf78eebd457d652ff4
parent20f7e1c78c55d2a16d5033bc2bbe5904d225adb0 (diff)
parentfd18d4bdfd79a269a3220936efd2ec8f69353c29 (diff)
downloadchef-eb590e371b5e5d3f4d1d201dd3f58e4351116f41.tar.gz
Merge pull request #3438 from chef/tc/api-versioning
API V1 Support
-rw-r--r--CHANGELOG.md2
-rw-r--r--lib/chef/api_client.rb156
-rw-r--r--lib/chef/exceptions.rb3
-rw-r--r--lib/chef/formatters/error_inspectors/api_error_formatting.rb17
-rw-r--r--lib/chef/http/authenticator.rb9
-rw-r--r--lib/chef/knife.rb20
-rw-r--r--lib/chef/knife/client_create.rb86
-rw-r--r--lib/chef/knife/osc_user_create.rb97
-rw-r--r--lib/chef/knife/osc_user_delete.rb51
-rw-r--r--lib/chef/knife/osc_user_edit.rb58
-rw-r--r--lib/chef/knife/osc_user_list.rb47
-rw-r--r--lib/chef/knife/osc_user_reregister.rb64
-rw-r--r--lib/chef/knife/osc_user_show.rb54
-rw-r--r--lib/chef/knife/user_create.rb131
-rw-r--r--lib/chef/knife/user_delete.rb54
-rw-r--r--lib/chef/knife/user_edit.rb44
-rw-r--r--lib/chef/knife/user_list.rb3
-rw-r--r--lib/chef/knife/user_reregister.rb47
-rw-r--r--lib/chef/knife/user_show.rb31
-rw-r--r--lib/chef/mixin/api_version_request_handling.rb66
-rw-r--r--lib/chef/osc_user.rb194
-rw-r--r--lib/chef/rest.rb1
-rw-r--r--lib/chef/user.rb235
-rw-r--r--spec/support/shared/unit/api_versioning.rb77
-rw-r--r--spec/support/shared/unit/knife_shared.rb40
-rw-r--r--spec/support/shared/unit/user_and_client_shared.rb115
-rw-r--r--spec/unit/api_client_spec.rb203
-rw-r--r--spec/unit/formatters/error_inspectors/api_error_formatting_spec.rb22
-rw-r--r--spec/unit/http/authenticator_spec.rb13
-rw-r--r--spec/unit/knife/client_create_spec.rb173
-rw-r--r--spec/unit/knife/osc_user_create_spec.rb93
-rw-r--r--spec/unit/knife/osc_user_delete_spec.rb44
-rw-r--r--spec/unit/knife/osc_user_edit_spec.rb52
-rw-r--r--spec/unit/knife/osc_user_list_spec.rb37
-rw-r--r--spec/unit/knife/osc_user_reregister_spec.rb58
-rw-r--r--spec/unit/knife/osc_user_show_spec.rb46
-rw-r--r--spec/unit/knife/user_create_spec.rb228
-rw-r--r--spec/unit/knife/user_delete_spec.rb42
-rw-r--r--spec/unit/knife/user_edit_spec.rb43
-rw-r--r--spec/unit/knife/user_list_spec.rb10
-rw-r--r--spec/unit/knife/user_reregister_spec.rb55
-rw-r--r--spec/unit/knife/user_show_spec.rb46
-rw-r--r--spec/unit/knife_spec.rb14
-rw-r--r--spec/unit/mixin/api_version_request_handling_spec.rb127
-rw-r--r--spec/unit/osc_user_spec.rb276
-rw-r--r--spec/unit/rest_spec.rb10
-rw-r--r--spec/unit/user_spec.rb499
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