diff options
author | Lamont Granquist <lamont@opscode.com> | 2021-02-22 10:33:38 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-02-22 10:33:38 -0800 |
commit | 2e7c4c75e8c0a3c6e6bbf5e3f35068b0577f13ab (patch) | |
tree | b6bed8a9931d7de8e568d2e8acef85d6ff80566b | |
parent | e909d2fec12085828ac0198cf715399c91ed72e4 (diff) | |
parent | 0570757936debabc9401b22784f6781e3892e21e (diff) | |
download | chef-2e7c4c75e8c0a3c6e6bbf5e3f35068b0577f13ab.tar.gz |
Merge pull request #10187 from chef/snehal/Merge_knife_plugin_into_Chef
30 files changed, 1505 insertions, 116 deletions
diff --git a/lib/chef/group.rb b/lib/chef/group.rb new file mode 100644 index 0000000000..72f5b7b474 --- /dev/null +++ b/lib/chef/group.rb @@ -0,0 +1,75 @@ +# +# Copyright:: Copyright (c) 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_relative "org" + +class Chef + class Group + + def group(groupname) + @group ||= {} + @group[groupname] ||= chef_rest.get_rest "organizations/#{name}/groups/#{groupname}" + end + + def user_member_of_group?(username, groupname) + group = group(groupname) + group["actors"].include? username + end + + def add_user_to_group(groupname, username) + group = group(groupname) + body_hash = { + groupname: "#{groupname}", + actors: { + "users" => group["actors"].concat([username]), + "groups" => group["groups"], + }, + } + chef_rest.put_rest "organizations/#{name}/groups/#{groupname}", body_hash + end + + def remove_user_from_group(groupname, username) + group = group(groupname) + group["actors"].delete(username) + body_hash = { + groupname: "#{groupname}", + actors: { + "users" => group["actors"], + "groups" => group["groups"], + }, + } + chef_rest.put_rest "organizations/#{name}/groups/#{groupname}", body_hash + end + + def actor_delete_would_leave_admins_empty? + admins = group("admins") + if admins["groups"].empty? + # exclude 'pivotal' but don't mutate the group since we're caching it + if admins["actors"].include? "pivotal" + admins["actors"].length <= 2 + else + admins["actors"].length <= 1 + end + else + # We don't check recursively. If the admins group contains a group, + # and the user is the only member of that group, + # we'll still turn up a 'safe to delete'. + false + end + end + end +end diff --git a/lib/chef/knife.rb b/lib/chef/knife.rb index ac7a68d0fc..d277e51105 100644 --- a/lib/chef/knife.rb +++ b/lib/chef/knife.rb @@ -661,5 +661,12 @@ class Chef end Chef::Config.init_openssl end + + def root_rest + @root_rest ||= begin + require_relative "server_api" + Chef::ServerAPI.new(Chef::Config[:chef_server_root]) + end + end end end diff --git a/lib/chef/knife/org_create.rb b/lib/chef/knife/org_create.rb new file mode 100644 index 0000000000..3c1354ae22 --- /dev/null +++ b/lib/chef/knife/org_create.rb @@ -0,0 +1,70 @@ +# +# Author:: Steven Danna (<steve@chef.io>) +# Copyright:: Copyright (c) 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 + class Knife + class OrgCreate < Knife + category "CHEF ORGANIZATION MANAGEMENT" + banner "knife org create ORG_SHORT_NAME ORG_FULL_NAME (options)" + + option :filename, + long: "--filename FILENAME", + short: "-f FILENAME", + description: "Write validator private key to FILENAME rather than STDOUT" + + option :association_user, + long: "--association_user USERNAME", + short: "-a USERNAME", + description: "Invite USERNAME to the new organization after creation" + + attr_accessor :org_name, :org_full_name + + deps do + require_relative "../org" + end + + def run + @org_name, @org_full_name = @name_args + + if !org_name || !org_full_name + ui.fatal "You must specify an ORG_NAME and an ORG_FULL_NAME" + show_usage + exit 1 + end + + org = Chef::Org.from_hash({ "name" => org_name, + "full_name" => org_full_name }).create + if config[:filename] + File.open(config[:filename], "w") do |f| + f.print(org.private_key) + end + else + ui.msg org.private_key + end + + if config[:association_user] + org.associate_user(config[:association_user]) + org.add_user_to_group("admins", config[:association_user]) + org.add_user_to_group("billing-admins", config[:association_user]) + end + + ui.info("Created #{org_name}") + end + end + end +end diff --git a/lib/chef/knife/org_delete.rb b/lib/chef/knife/org_delete.rb new file mode 100644 index 0000000000..340f6c529a --- /dev/null +++ b/lib/chef/knife/org_delete.rb @@ -0,0 +1,32 @@ +# +# Author:: Steven Danna (<steve@chef.io>) +# Copyright:: Copyright (c) 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 + class Knife + class OrgDelete < Knife + category "CHEF ORGANIZATION MANAGEMENT" + banner "knife org delete ORG_NAME" + + def run + org_name = @name_args[0] + ui.confirm "Do you want to delete the organization #{org_name}" + ui.output root_rest.delete("organizations/#{org_name}") + end + end + end +end diff --git a/lib/chef/knife/org_edit.rb b/lib/chef/knife/org_edit.rb new file mode 100644 index 0000000000..1d684ca0b4 --- /dev/null +++ b/lib/chef/knife/org_edit.rb @@ -0,0 +1,48 @@ +# +# Author:: Steven Danna (<steve@chef.io>) +# Copyright:: Copyright (c) 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 + class Knife + class OrgEdit < Knife + category "CHEF ORGANIZATION MANAGEMENT" + banner "knife org edit ORG" + + def run + org_name = @name_args[0] + + if org_name.nil? + show_usage + ui.fatal("You must specify an organization name") + exit 1 + end + + original_org = root_rest.get("organizations/#{org_name}") + edited_org = edit_hash(original_org) + + if original_org == edited_org + ui.msg("Organization unchanged, not saving.") + exit + end + + ui.msg edited_org + root_rest.put("organizations/#{org_name}", edited_org) + ui.msg("Saved #{org_name}.") + end + end + end +end diff --git a/lib/chef/knife/org_list.rb b/lib/chef/knife/org_list.rb new file mode 100644 index 0000000000..85a49ee4c5 --- /dev/null +++ b/lib/chef/knife/org_list.rb @@ -0,0 +1,44 @@ +# +# Author:: Steven Danna (<steve@chef.io>) +# Copyright:: Copyright (c) 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 + class Knife + class OrgList < Knife + category "CHEF ORGANIZATION MANAGEMENT" + banner "knife org list" + + option :with_uri, + long: "--with-uri", + short: "-w", + description: "Show corresponding URIs" + + option :all_orgs, + long: "--all-orgs", + short: "-a", + description: "Show auto-generated hidden orgs in output" + + def run + results = root_rest.get("organizations") + unless config[:all_orgs] + results = results.select { |k, v| !(k.length == 20 && k =~ /^[a-z]+$/) } + end + ui.output(ui.format_list_for_display(results)) + end + end + end +end diff --git a/lib/chef/knife/org_show.rb b/lib/chef/knife/org_show.rb new file mode 100644 index 0000000000..a8bb207c1d --- /dev/null +++ b/lib/chef/knife/org_show.rb @@ -0,0 +1,31 @@ +# +# Author:: Steven Danna (<steve@chef.io>) +# Copyright:: Copyright (c) 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 + class Knife + class OrgShow < Knife + category "CHEF ORGANIZATION MANAGEMENT" + banner "knife org show ORGNAME" + + def run + org_name = @name_args[0] + ui.output root_rest.get("organizations/#{org_name}") + end + end + end +end diff --git a/lib/chef/knife/org_user_add.rb b/lib/chef/knife/org_user_add.rb new file mode 100644 index 0000000000..cd0ea88d56 --- /dev/null +++ b/lib/chef/knife/org_user_add.rb @@ -0,0 +1,62 @@ +# +# Author:: Marc Paradise (<marc@chef.io>) +# Copyright:: Copyright (c) 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 + class Knife + class OrgUserAdd < Knife + category "CHEF ORGANIZATION MANAGEMENT" + banner "knife org user add ORG_NAME USER_NAME" + attr_accessor :org_name, :username + + option :admin, + long: "--admin", + short: "-a", + description: "Add user to admin group" + + deps do + require_relative "../org" + end + + def run + @org_name, @username = @name_args + + if !org_name || !username + ui.fatal "You must specify an ORG_NAME and USER_NAME" + show_usage + exit 1 + end + + org = Chef::Org.new(@org_name) + begin + org.associate_user(@username) + rescue Net::HTTPServerException => e + if e.response.code == "409" + ui.msg "User #{username} already associated with organization #{org_name}" + else + raise e + end + end + if config[:admin] + org.add_user_to_group("admins", @username) + org.add_user_to_group("billing-admins", @username) + ui.msg "User #{username} is added to admins and billing-admins group" + end + end + end + end +end diff --git a/lib/chef/knife/org_user_remove.rb b/lib/chef/knife/org_user_remove.rb new file mode 100644 index 0000000000..50a1471443 --- /dev/null +++ b/lib/chef/knife/org_user_remove.rb @@ -0,0 +1,103 @@ +# +# Author:: Marc Paradise (<marc@getchef.com>) +# Copyright:: Copyright (c) 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 + class Knife + class OrgUserRemove < Knife + category "CHEF ORGANIZATION MANAGEMENT" + banner "knife org user remove ORG_NAME USER_NAME" + attr_accessor :org_name, :username + + option :force_remove_from_admins, + long: "--force", + short: "-f", + description: "Force removal of user from the organization's admins and billing-admins group." + + deps do + require_relative "../org" + require "chef/json_compat" + end + + def run + @org_name, @username = @name_args + + if !org_name || !username + ui.fatal "You must specify an ORG_NAME and USER_NAME" + show_usage + exit 1 + end + + org = Chef::Org.new(@org_name) + + if config[:force_remove_from_admins] + if org.actor_delete_would_leave_admins_empty? + failure_error_message(org_name, username) + ui.msg <<~EOF + You ran with --force which force removes the user from the admins and billing-admins groups. + However, removing #{username} from the admins group would leave it empty, which breaks the org. + Please add another user to org #{org_name} admins group and try again. + EOF + exit 1 + end + remove_user_from_admin_group(org, org_name, username, "admins") + remove_user_from_admin_group(org, org_name, username, "billing-admins") + end + + begin + org.dissociate_user(@username) + rescue Net::HTTPServerException => e + if e.response.code == "404" + ui.msg "User #{username} is not associated with organization #{org_name}" + exit 1 + elsif e.response.code == "403" + body = Chef::JSONCompat.from_json(e.response.body) + if body.key?("error") && body["error"] == "Please remove #{username} from this organization's admins group before removing him or her from the organization." + failure_error_message(org_name, username) + ui.msg <<~EOF + User #{username} is in the organization's admin group. Removing users from an organization without removing them from the admins group is not allowed. + Re-run this command with --force to remove this user from the admins prior to removing it from the organization. + EOF + exit 1 + else + raise e + end + else + raise e + end + end + end + + def failure_error_message(org_name, username) + ui.error "Error removing user #{username} from organization #{org_name}." + end + + def remove_user_from_admin_group(org, org_name, username, admin_group_string) + org.remove_user_from_group(admin_group_string, username) + rescue Net::HTTPServerException => e + if e.response.code == "404" + ui.warn <<~EOF + User #{username} is not in the #{admin_group_string} group for organization #{org_name}. + You probably don't need to pass --force. + EOF + else + raise e + end + end + end + end +end diff --git a/lib/chef/knife/user_create.rb b/lib/chef/knife/user_create.rb index 6d68f3ebbb..aa1d4d54f2 100644 --- a/lib/chef/knife/user_create.rb +++ b/lib/chef/knife/user_create.rb @@ -1,5 +1,4 @@ # -# Author:: Steven Danna (<steve@chef.io>) # Author:: Tyler Cloke (<tyler@chef.io>) # Copyright:: Copyright (c) Chef Software Inc. # License:: Apache License, Version 2.0 @@ -45,16 +44,22 @@ class Chef description: "API V1 (#{ChefUtils::Dist::Server::PRODUCT} 12.1+) only. Prevent server from generating a default key pair for you. Cannot be passed with --user-key.", boolean: true + option :orgname, + long: "--orgname ORGNAME", + short: "-o ORGNAME", + description: "Associate new user to an organization matching ORGNAME" + + option :passwordprompt, + long: "--prompt-for-password", + short: "-p", + description: "Prompt for user password" + banner "knife user create USERNAME DISPLAY_NAME FIRST_NAME LAST_NAME EMAIL PASSWORD (options)" def user @user_field ||= Chef::UserV1.new end - def create_user_from_hash(hash) - Chef::UserV1.from_hash(hash).create - end - def run test_mandatory_field(@name_args[0], "username") user.username @name_args[0] @@ -71,8 +76,11 @@ class Chef test_mandatory_field(@name_args[4], "email") user.email @name_args[4] - test_mandatory_field(@name_args[5], "password") - user.password @name_args[5] + password = config[:passwordprompt] ? prompt_for_password : @name_args[5] + unless password + ui.fatal "You must either provide a password or use the --prompt-for-password (-p) option" + exit 1 + end if config[:user_key] && config[:prevent_keygen] show_usage @@ -88,20 +96,48 @@ class Chef user.public_key File.read(File.expand_path(config[:user_key])) end - output = edit_hash(user) - final_user = create_user_from_hash(output) + user_hash = { + username: user.username, + first_name: user.first_name, + last_name: user.last_name, + display_name: "#{user.first_name} #{user.last_name}", + email: user.email, + password: password, + } + + # Check the file before creating the user so the api is more transactional. + if config[:file] + file = config[:file] + unless File.exist?(file) ? File.writable?(file) : File.writable?(File.dirname(file)) + ui.fatal "File #{config[:file]} is not writable. Check permissions." + exit 1 + end + end + + final_user = root_rest.post("users/", user_hash) + + if config[:orgname] + request_body = { user: user.username } + response = root_rest.post("organizations/#{config[:orgname]}/association_requests", request_body) + association_id = response["uri"].split("/").last + root_rest.put("users/#{user.username}/association_requests/#{association_id}", { response: "accept" }) + end - ui.info("Created #{user}") - if final_user.private_key + ui.info("Created #{user.username}") + if final_user["private_key"] if config[:file] File.open(config[:file], "w") do |f| - f.print(final_user.private_key) + f.print(final_user["private_key"]) end else - ui.msg final_user.private_key + ui.msg final_user["private_key"] end end end + + def prompt_for_password + ui.ask("Please enter the user's password: ", echo: false) + end end end end diff --git a/lib/chef/knife/user_delete.rb b/lib/chef/knife/user_delete.rb index 87c1f734bb..64d729c951 100644 --- a/lib/chef/knife/user_delete.rb +++ b/lib/chef/knife/user_delete.rb @@ -23,21 +23,128 @@ class Chef class UserDelete < Knife deps do - require_relative "../user_v1" + require_relative "../org" end banner "knife user delete USER (options)" + option :no_disassociate_user, + long: "--no-disassociate-user", + short: "-d", + description: "Don't disassociate the user first" + + option :remove_from_admin_groups, + long: "--remove-from-admin-groups", + short: "-R", + description: "If the user is a member of any org admin groups, attempt to remove from those groups. Ignored if --no-disassociate-user is set." + + attr_reader :username + def run - @user_name = @name_args[0] + @username = @name_args[0] + admin_memberships = [] + unremovable_memberships = [] - if @user_name.nil? + if @username.nil? show_usage ui.fatal("You must specify a user name") exit 1 end - delete_object(Chef::UserV1, @user_name) + ui.confirm "Do you want to delete the user #{username}" + + unless config[:no_disassociate_user] + ui.stderr.puts("Checking organization memberships...") + orgs = org_memberships(username) + if orgs.length > 0 + ui.stderr.puts("Checking admin group memberships for #{orgs.length} org(s).") + admin_memberships, unremovable_memberships = admin_group_memberships(orgs, username) + end + + unless admin_memberships.empty? + unless config[:remove_from_admin_groups] + error_exit_admin_group_member!(username, admin_memberships) + end + + unless unremovable_memberships.empty? + error_exit_cant_remove_admin_membership!(username, unremovable_memberships) + end + remove_from_admin_groups(admin_memberships, username) + end + disassociate_user(orgs, username) + end + + delete_user(username) + end + + def disassociate_user(orgs, username) + orgs.each { |org| org.dissociate_user(username) } + end + + def org_memberships(username) + org_data = root_rest.get("users/#{username}/organizations") + org_data.map { |org| Chef::Org.new(org["organization"]["name"]) } + end + + def remove_from_admin_groups(admin_of, username) + admin_of.each do |org| + ui.stderr.puts "Removing #{username} from admins group of '#{org.name}'" + org.remove_user_from_group("admins", username) + end + end + + def admin_group_memberships(orgs, username) + admin_of = [] + unremovable = [] + orgs.each do |org| + if org.user_member_of_group?(username, "admins") + admin_of << org + if org.actor_delete_would_leave_admins_empty? + unremovable << org + end + end + end + [admin_of, unremovable] + end + + def delete_user(username) + ui.stderr.puts "Deleting user #{username}." + root_rest.delete("users/#{username}") + end + + # Error message that says how to removed from org + # admin groups before deleting + # Further + def error_exit_admin_group_member!(username, admin_of) + message = "#{username} is in the 'admins' group of the following organization(s):\n\n" + admin_of.each { |org| message << "- #{org.name}\n" } + message << <<~EOM + + Run this command again with the --remove-from-admin-groups option to + remove the user from these admin group(s) automatically. + + EOM + ui.fatal message + exit 1 + end + + def error_exit_cant_remove_admin_membership!(username, only_admin_of) + message = <<~EOM + + #{username} is the only member of the 'admins' group of the + following organization(s): + + EOM + only_admin_of.each { |org| message << "- #{org.name}\n" } + message << <<~EOM + + Removing the only administrator of an organization can break it. + Assign additional users or groups to the admin group(s) before + deleting this user. + + EOM + ui.fatal message + exit 1 end end end diff --git a/lib/chef/knife/user_edit.rb b/lib/chef/knife/user_edit.rb index ad9dfac079..fff8c6b70f 100644 --- a/lib/chef/knife/user_edit.rb +++ b/lib/chef/knife/user_edit.rb @@ -22,12 +22,18 @@ class Chef class Knife class UserEdit < Knife - deps do - require_relative "../user_v1" - end - banner "knife user edit USER (options)" + option :input, + long: "--input FILENAME", + short: "-i FILENAME", + description: "Name of file to use for PUT or POST" + + option :filename, + long: "--filename FILENAME", + short: "-f FILENAME", + description: "Write private key to FILENAME rather than STDOUT" + def run @user_name = @name_args[0] @@ -36,17 +42,53 @@ class Chef ui.fatal("You must specify a user name") exit 1 end - - original_user = Chef::UserV1.load(@user_name).to_hash - edited_user = edit_hash(original_user) + original_user = root_rest.get("users/#{@user_name}") + edited_user = get_updated_user(original_user) if original_user != edited_user - user = Chef::UserV1.from_hash(edited_user) - user.update - ui.msg("Saved #{user}.") + result = root_rest.put("users/#{@user_name}", edited_user) + ui.msg("Saved #{@user_name}.") + unless result["private_key"].nil? + if config[:filename] + File.open(config[:filename], "w") do |f| + f.print(result["private_key"]) + end + else + ui.msg result["private_key"] + end + end else ui.msg("User unchanged, not saving.") end end end + + private + + # Check the options for ex: input or filename + # Read Or Open file to update user information + # return updated user + def get_updated_user(original_user) + if config[:input] + edited_user = JSON.parse(IO.read(config[:input])) + elsif config[:filename] + file = config[:filename] + unless File.exist?(file) ? File.writable?(file) : File.writable?(File.dirname(file)) + ui.fatal "File #{file} is not writable. Check permissions." + exit 1 + else + output = Chef::JSONCompat.to_json_pretty(original_user) + File.open(file, "w") do |f| + f.sync = true + f.puts output + f.close + raise "Please set EDITOR environment variable. See https://docs.chef.io/knife_setup/ for details." unless system("#{config[:editor]} #{f.path}") + + edited_user = JSON.parse(IO.read(f.path)) + end + end + else + edited_user = JSON.parse(edit_data(original_user, false)) + end + end end end diff --git a/lib/chef/knife/user_list.rb b/lib/chef/knife/user_list.rb index f6aa7bcfc4..3284964a47 100644 --- a/lib/chef/knife/user_list.rb +++ b/lib/chef/knife/user_list.rb @@ -22,10 +22,6 @@ class Chef class Knife class UserList < Knife - deps do - require_relative "../user_v1" - end - banner "knife user list (options)" option :with_uri, @@ -34,9 +30,9 @@ class Chef description: "Show corresponding URIs." def run - output(format_list_for_display(Chef::UserV1.list)) + results = root_rest.get("users") + output(format_list_for_display(results)) end - end end end diff --git a/lib/chef/knife/user_password.rb b/lib/chef/knife/user_password.rb new file mode 100644 index 0000000000..2da3c3e285 --- /dev/null +++ b/lib/chef/knife/user_password.rb @@ -0,0 +1,70 @@ +# +# Author:: Tyler Cloke (<tyler@getchef.com>) +# Copyright:: Copyright (c) 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 + class Knife + class UserPassword < Knife + banner "knife user password USERNAME [PASSWORD | --enable-external-auth]" + + option :enable_external_auth, + long: "--enable-external-auth", + short: "-e", + description: "Enable external authentication for this user (such as LDAP)" + + def run + # check that correct number of args was passed, should be either + # USERNAME PASSWORD or USERNAME --enable-external-auth + # + # note that you can't pass USERNAME PASSWORD --enable-external-auth + unless (@name_args.length == 2 && !config[:enable_external_auth]) || (@name_args.length == 1 && config[:enable_external_auth]) + show_usage + ui.fatal("You must pass two arguments") + ui.fatal("Note that --enable-external-auth cannot be passed with a password") + exit 1 + end + + user_name = @name_args[0] + + # note that this will be nil if config[:enable_external_auth] is true + password = @name_args[1] + + # since the API does not pass back whether recovery_authentication_enabled is + # true or false, there is no way of knowing if the user is using ldap or not, + # so we will update the user every time, instead of checking if we are actually + # changing anything before we PUT. + result = root_rest.get("users/#{user_name}") + + result["password"] = password unless password.nil? + + # if --enable-external-auth was passed, enable it, else disable it. + # there is never a situation where we would want to enable ldap + # AND change the password. changing the password means that the user + # wants to disable ldap and put user in recover (if they are using ldap). + result["recovery_authentication_enabled"] = !config[:enable_external_auth] + + begin + root_rest.put("users/#{user_name}", result) + rescue => e + raise e + end + + ui.msg("Authentication info updated for #{user_name}.") + end + end + end +end diff --git a/lib/chef/knife/user_show.rb b/lib/chef/knife/user_show.rb index e59f969e9a..ea2b06b753 100644 --- a/lib/chef/knife/user_show.rb +++ b/lib/chef/knife/user_show.rb @@ -24,12 +24,12 @@ class Chef include Knife::Core::MultiAttributeReturnOption - deps do - require_relative "../user_v1" - end - banner "knife user show USER (options)" + option :with_orgs, + long: "--with-orgs", + short: "-l" + def run @user_name = @name_args[0] @@ -39,8 +39,12 @@ class Chef exit 1 end - user = Chef::UserV1.load(@user_name) - output(format_for_display(user)) + results = root_rest.get("users/#{@user_name}") + if config[:with_orgs] + orgs = root_rest.get("users/#{@user_name}/organizations") + results["organizations"] = orgs.map { |o| o["organization"]["name"] } + end + output(format_for_display(results)) end end diff --git a/lib/chef/org.rb b/lib/chef/org.rb index e2b7c49051..8f65f3ddd1 100644 --- a/lib/chef/org.rb +++ b/lib/chef/org.rb @@ -1,6 +1,6 @@ # # Author:: Steven Danna (steve@chef.io) -# Copyright:: Copyright (c) Chef Software Inc. +# Copyright:: Copyright (c) Chef Software Inc # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,9 +19,10 @@ require_relative "json_compat" require_relative "mixin/params_validate" require_relative "server_api" +require_relative "group" class Chef - class Org + class Org < Group include Chef::Mixin::ParamsValidate diff --git a/lib/chef/user.rb b/lib/chef/user.rb index e578cc2131..4ebcb3a463 100644 --- a/lib/chef/user.rb +++ b/lib/chef/user.rb @@ -36,7 +36,6 @@ require_relative "server_api" # should be removed once client support for Open Source Chef Server 11 expires. class Chef class User - include Chef::Mixin::FromFile include Chef::Mixin::ParamsValidate diff --git a/spec/unit/knife/org_create_spec.rb b/spec/unit/knife/org_create_spec.rb new file mode 100644 index 0000000000..3c33817b55 --- /dev/null +++ b/spec/unit/knife/org_create_spec.rb @@ -0,0 +1,76 @@ +# +# Copyright:: Copyright 2014-2016 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" +require "chef/org" + +describe Chef::Knife::OrgCreate do + before :each do + Chef::Knife::OrgCreate.load_deps + @knife = Chef::Knife::OrgCreate.new + @org = double("Chef::Org") + allow(Chef::Org).to receive(:new).and_return(@org) + @key = "You don't come into cooking to get rich - Ramsay" + allow(@org).to receive(:private_key).and_return(@key) + @org_name = "ss" + @org_full_name = "secretsauce" + end + + let(:org_args) do + { + name: @org_name, + full_name: @org_full_name, + } + end + + describe "with no org_name and org_fullname" do + it "fails with an informative message" do + expect(@knife.ui).to receive(:fatal).with("You must specify an ORG_NAME and an ORG_FULL_NAME") + expect(@knife).to receive(:show_usage) + expect { @knife.run }.to raise_error(SystemExit) + end + end + + describe "with org_name and org_fullname" do + before :each do + @knife.name_args << @org_name << @org_full_name + end + + it "creates an org" do + expect(@org).to receive(:create).and_return(@org) + expect(@org).to receive(:full_name).with("secretsauce") + expect(@knife.ui).to receive(:msg).with(@key) + @knife.run + end + + context "with --assocation-user" do + before :each do + @knife.config[:association_user] = "ramsay" + end + + it "creates an org, associates a user, and adds it to the admins group" do + expect(@org).to receive(:full_name).with("secretsauce") + expect(@org).to receive(:create).and_return(@org) + expect(@org).to receive(:associate_user).with("ramsay") + expect(@org).to receive(:add_user_to_group).with("admins", "ramsay") + expect(@org).to receive(:add_user_to_group).with("billing-admins", "ramsay") + expect(@knife.ui).to receive(:msg).with(@key) + @knife.run + end + end + end +end diff --git a/spec/unit/knife/org_delete_spec.rb b/spec/unit/knife/org_delete_spec.rb new file mode 100644 index 0000000000..baa102f8c8 --- /dev/null +++ b/spec/unit/knife/org_delete_spec.rb @@ -0,0 +1,41 @@ +# +# Author:: Snehal Dwivedi (<sdwivedi@msystechnologies.com>) +# Copyright:: Copyright (c) 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" +require "chef/org" + +describe Chef::Knife::OrgDelete do + + let(:root_rest) { double("Chef::ServerAPI") } + + before :each do + @knife = Chef::Knife::OrgDelete.new + @org_name = "foobar" + @org_full_name = "secretsauce" + @knife.name_args << @org_name + @org = double("Chef::Org") + end + + it "should confirm that you want to delete and then delete organizations" do + expect(Chef::ServerAPI).to receive(:new).with(Chef::Config[:chef_server_url]).and_return(root_rest) + expect(@knife.ui).to receive(:confirm).with("Do you want to delete the organization #{@org_name}") + expect(root_rest).to receive(:delete).with("organizations/#{@org_name}") + expect(@knife.ui).to receive(:output) + @knife.run + end +end diff --git a/spec/unit/knife/org_edit_spec.rb b/spec/unit/knife/org_edit_spec.rb new file mode 100644 index 0000000000..05339e8f21 --- /dev/null +++ b/spec/unit/knife/org_edit_spec.rb @@ -0,0 +1,49 @@ +# +# Author:: Snehal Dwivedi (<sdwivedi@msystechnologies.com>) +# Copyright:: Copyright (c) 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::Knife::OrgEdit do + let(:knife) { Chef::Knife::OrgEdit.new } + let(:root_rest) { double("Chef::ServerAPI") } + + before :each do + Chef::Knife::OrgEdit.load_deps + @org_name = "foobar" + knife.name_args << @org_name + @org = double("Chef::Org") + knife.config[:disable_editing] = true + end + + it "loads and edits the organisation" do + expect(Chef::ServerAPI).to receive(:new).with(Chef::Config[:chef_server_root]).and_return(root_rest) + original_data = { "org_name" => "my_org" } + data = { "org_name" => "my_org1" } + expect(root_rest).to receive(:get).with("organizations/foobar").and_return(original_data) + expect(knife).to receive(:edit_hash).with(original_data).and_return(data) + expect(root_rest).to receive(:put).with("organizations/foobar", data) + knife.run + end + + it "prints usage and exits when a org 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/org_list_spec.rb b/spec/unit/knife/org_list_spec.rb new file mode 100644 index 0000000000..de77b4b0c7 --- /dev/null +++ b/spec/unit/knife/org_list_spec.rb @@ -0,0 +1,58 @@ +# +# Copyright:: Copyright (c) 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" +require "chef/org" + +describe Chef::Knife::OrgList do + + let(:root_rest) { double("Chef::ServerAPI") } + + let(:orgs) do + { + "org1" => "first", + "org2" => "second", + "hiddenhiddenhiddenhi" => "hidden", + } + end + + before :each do + @org = double("Chef::Org") + @knife = Chef::Knife::OrgList.new + expect(Chef::ServerAPI).to receive(:new).with(Chef::Config[:chef_server_root]).and_return(root_rest) + allow(root_rest).to receive(:get).with("organizations").and_return(orgs) + end + + describe "with no arguments" do + it "lists all non hidden orgs" do + expect(@knife.ui).to receive(:output).with(%w{org1 org2}) + @knife.run + end + + end + + describe "with all_orgs argument" do + before do + @knife.config[:all_orgs] = true + end + + it "lists all orgs including hidden orgs" do + expect(@knife.ui).to receive(:output).with(%w{hiddenhiddenhiddenhi org1 org2}) + @knife.run + end + end +end diff --git a/spec/unit/knife/org_show_spec.rb b/spec/unit/knife/org_show_spec.rb new file mode 100644 index 0000000000..2f5246dd84 --- /dev/null +++ b/spec/unit/knife/org_show_spec.rb @@ -0,0 +1,45 @@ +# +# Author:: Snehal Dwivedi (<sdwivedi@msystechnologies.com>) +# Copyright:: Copyright (c) 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" +require "chef/org" + +describe Chef::Knife::OrgShow do + + let(:root_rest) { double("Chef::ServerAPI") } + + before :each do + @knife = Chef::Knife::OrgShow.new + @org_name = "foobar" + @knife.name_args << @org_name + @org = double("Chef::Org") + expect(Chef::ServerAPI).to receive(:new).with(Chef::Config[:chef_server_root]).and_return(root_rest) + allow(@org).to receive(:root_rest).and_return(root_rest) + end + + it "should load the organisation" do + expect(root_rest).to receive(:get).with("organizations/#{@org_name}") + @knife.run + end + + it "should pretty print the output organisation" do + expect(root_rest).to receive(:get).with("organizations/#{@org_name}") + expect(@knife.ui).to receive(:output) + @knife.run + end +end diff --git a/spec/unit/knife/org_user_add_spec.rb b/spec/unit/knife/org_user_add_spec.rb new file mode 100644 index 0000000000..20e28d6919 --- /dev/null +++ b/spec/unit/knife/org_user_add_spec.rb @@ -0,0 +1,39 @@ +# +# Copyright:: Copyright (c) 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" +require "chef/org" + +describe Chef::Knife::OrgUserAdd do + context "with --admin" do + subject(:knife) { Chef::Knife::OrgUserAdd.new } + let(:org) { double("Chef::Org") } + + it "adds the user to admins and billing-admins groups" do + allow(Chef::Org).to receive(:new).and_return(org) + + knife.config[:admin] = true + knife.name_args = %w{testorg testuser} + + expect(org).to receive(:associate_user).with("testuser") + expect(org).to receive(:add_user_to_group).with("admins", "testuser") + expect(org).to receive(:add_user_to_group).with("billing-admins", "testuser") + + knife.run + end + end +end diff --git a/spec/unit/knife/user_create_spec.rb b/spec/unit/knife/user_create_spec.rb index be3d2fd99c..c69a668f7e 100644 --- a/spec/unit/knife/user_create_spec.rb +++ b/spec/unit/knife/user_create_spec.rb @@ -22,7 +22,9 @@ require "spec_helper" Chef::Knife::UserCreate.load_deps describe Chef::Knife::UserCreate do + let(:knife) { Chef::Knife::UserCreate.new } + let(:root_rest) { double("Chef::ServerAPI") } let(:stderr) do StringIO.new @@ -38,6 +40,8 @@ describe Chef::Knife::UserCreate do allow(knife.ui).to receive(:warn) end + let(:chef_root_rest_v0) { double("Chef::ServerAPI") } + context "when USERNAME isn't specified" do # from spec/support/shared/unit/knife_shared.rb it_should_behave_like "mandatory field missing" do @@ -70,34 +74,42 @@ describe Chef::Knife::UserCreate do end end - 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) { %w{some_user some_display_name some_first_name some_last_name some_email} } - let(:fieldname) { "password" } + describe "with prompt password" do + let(:name_args) { %w{some_user some_display_name some_first_name some_last_name test@email.com} } + + before :each do + @user = double("Chef::User") + @key = "You don't come into cooking to get rich - Ramsay" + allow(@user).to receive(:[]).with("private_key").and_return(@key) + knife.config[:passwordprompt] = true + knife.name_args = name_args + end + + it "creates an user" do + expect(Chef::ServerAPI).to receive(:new).with(Chef::Config[:chef_server_root]).and_return(root_rest) + expect(root_rest).to receive(:post).and_return(@user) + expect(knife.ui).to receive(:ask).with("Please enter the user's password: ", echo: false).and_return("password") + knife.run end end context "when all mandatory fields are validly specified" do before do + @user = double("Chef::User") + @key = "You don't come into cooking to get rich - Ramsay" + allow(@user).to receive(:[]).with("private_key").and_return(@key) knife.name_args = %w{some_user some_display_name some_first_name some_last_name some_email some_password} - allow(knife).to receive(:edit_hash).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 + expect(Chef::ServerAPI).to receive(:new).with(Chef::Config[:chef_server_root]).and_return(root_rest) + expect(root_rest).to receive(:post).and_return(@user) 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 @@ -105,6 +117,7 @@ describe Chef::Knife::UserCreate 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) @@ -122,6 +135,8 @@ describe Chef::Knife::UserCreate do end it "does not set user.create_key" do + expect(Chef::ServerAPI).to receive(:new).with(Chef::Config[:chef_server_root]).and_return(root_rest) + expect(root_rest).to receive(:post).and_return(@user) knife.run expect(knife.user.create_key).to be_falsey end @@ -129,6 +144,8 @@ describe Chef::Knife::UserCreate do context "when --prevent-keygen is not passed" do it "sets user.create_key to true" do + expect(Chef::ServerAPI).to receive(:new).with(Chef::Config[:chef_server_root]).and_return(root_rest) + expect(root_rest).to receive(:post).and_return(@user) knife.run expect(knife.user.create_key).to be_truthy end @@ -142,6 +159,8 @@ describe Chef::Knife::UserCreate do end it "sets user.public_key" do + expect(Chef::ServerAPI).to receive(:new).with(Chef::Config[:chef_server_root]).and_return(root_rest) + expect(root_rest).to receive(:post).and_return(@user) knife.run expect(knife.user.public_key).to eq("some_key") end @@ -149,32 +168,45 @@ describe Chef::Knife::UserCreate do context "when --user-key is not passed" do it "does not set user.public_key" do + expect(Chef::ServerAPI).to receive(:new).with(Chef::Config[:chef_server_root]).and_return(root_rest) + expect(root_rest).to receive(:post).and_return(@user) 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::UserV1.from_hash(knife.user.to_hash.merge({ "private_key" => "some_private_key" }))) + describe "with user_name, first_name, last_name, email and password" do + let(:name_args) { %w{some_user some_display_name some_first_name some_last_name test@email.com some_password} } + + before :each do + @user = double("Chef::User") + expect(Chef::ServerAPI).to receive(:new).with(Chef::Config[:chef_server_root]).and_return(root_rest) + expect(root_rest).to receive(:post).and_return(@user) + @key = "You don't come into cooking to get rich - Ramsay" + allow(@user).to receive(:[]).with("private_key").and_return(@key) + knife.name_args = name_args end - context "when --file is passed" do - before do - knife.config[:file] = "/some/path" - end + it "creates an user" do + expect(knife.ui).to receive(:msg).with(@key) + knife.run + 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 + context "with --orgname" do + before :each do + knife.config[:orgname] = "ramsay" + @uri = "http://www.example.com/1" + allow(@user).to receive(:[]).with("uri").and_return(@uri) 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") + let(:request_body) { + { user: "some_user" } + } + + it "creates an user, associates a user, and adds it to the admins group" do + + expect(root_rest).to receive(:post).with("organizations/ramsay/association_requests", request_body).and_return(@user) + expect(root_rest).to receive(:put).with("users/some_user/association_requests/1", { response: "accept" }) knife.run end end diff --git a/spec/unit/knife/user_delete_spec.rb b/spec/unit/knife/user_delete_spec.rb index 959d792b9e..4dd2665cda 100644 --- a/spec/unit/knife/user_delete_spec.rb +++ b/spec/unit/knife/user_delete_spec.rb @@ -17,30 +17,155 @@ # require "spec_helper" +require "chef/org" + +Chef::Knife::UserDelete.load_deps describe Chef::Knife::UserDelete do + subject(:knife) { Chef::Knife::UserDelete.new } + + let(:non_admin_member_org) { Chef::Org.new("non-admin-member") } + let(:solo_admin_member_org) { Chef::Org.new("solo-admin-member") } + let(:shared_admin_member_org) { Chef::Org.new("shared-admin-member") } + + let(:removable_orgs) { [non_admin_member_org, shared_admin_member_org] } + let(:non_removable_orgs) { [solo_admin_member_org] } + + let(:admin_memberships) { [ removable_orgs, non_removable_orgs ] } + let(:username) { "test_user" } + + let(:rest) { double("Chef::ServerAPI") } + let(:orgs) { [non_admin_member_org, solo_admin_member_org, shared_admin_member_org] } let(:knife) { Chef::Knife::UserDelete.new } - let(:user) { double("user_object") } - let(:stdout) { StringIO.new } + + let(:orgs_data) do + [{ "organization" => { "name" => "non-admin-member" } }, + { "organization" => { "name" => "solo-admin-member" } }, + { "organization" => { "name" => "shared-admin-member" } }, + ] + end before(:each) do - Chef::Knife::UserDelete.load_deps - knife.name_args = [ "my_user" ] - allow(Chef::UserV1).to receive(:load).and_return(user) - allow(user).to receive(:username).and_return("my_user") - allow(knife.ui).to receive(:stderr).and_return(stdout) - allow(knife.ui).to receive(:stdout).and_return(stdout) + allow(Chef::ServerAPI).to receive(:new).and_return(rest) + knife.name_args << username + knife.config[:yes] = true + end + + context "when invoked" do + before do + allow(knife).to receive(:admin_group_memberships).and_return(admin_memberships) + end + + context "with --no-disassociate-user" do + before(:each) do + knife.config[:no_disassociate_user] = true + end + + it "should bypass all checks and go directly to user deletion" do + expect(knife).to receive(:delete_user).with(username) + knife.run + end + end + + context "without --no-disassociate-user" do + before do + allow(knife).to receive(:org_memberships).and_return(orgs) + end + + context "and with --remove-from-admin-groups" do + let(:non_removable_orgs) { [ solo_admin_member_org ] } + before(:each) do + knife.config[:remove_from_admin_groups] = true + end + + context "when an associated user the only organization admin" do + let(:non_removable_orgs) { [ solo_admin_member_org ] } + + it "refuses to proceed with because the user is the only admin" do + expect(knife).to receive(:error_exit_cant_remove_admin_membership!).and_call_original + expect { knife.run }.to raise_error SystemExit + end + end + + context "when an associated user is one of many organization admins" do + let(:non_removable_orgs) { [] } + + it "should remove the user from the group, the org, and then and delete the user" do + expect(knife).to receive(:disassociate_user) + expect(knife).to receive(:remove_from_admin_groups) + expect(knife).to receive(:delete_user) + expect(knife).to receive(:error_exit_cant_remove_admin_membership!).exactly(0).times + expect(knife).to receive(:error_exit_admin_group_member!).exactly(0).times + knife.run + end + + end + end + + context "and without --remove-from-admin-groups" do + before(:each) do + knife.config[:remove_from_admin_groups] = false + end + + context "when an associated user is in admins group" do + let(:removable_orgs) { [ shared_admin_member_org ] } + let(:non_removable_orgs) { [ ] } + it "refuses to proceed with because the user is an admin" do + # Default setup + expect(knife).to receive(:error_exit_admin_group_member!).and_call_original + expect { knife.run }.to raise_error SystemExit + end + end + end + + end + end + + context "#admin_group_memberships" do + before do + expect(non_admin_member_org).to receive(:user_member_of_group?).and_return false + + expect(solo_admin_member_org).to receive(:user_member_of_group?).and_return true + expect(solo_admin_member_org).to receive(:actor_delete_would_leave_admins_empty?).and_return true + + expect(shared_admin_member_org).to receive(:user_member_of_group?).and_return true + expect(shared_admin_member_org).to receive(:actor_delete_would_leave_admins_empty?).and_return false + + end + + it "returns an array of organizations in which the user is an admin, and an array of orgs which block removal" do + expect(knife.admin_group_memberships(orgs, username)).to eq [ [solo_admin_member_org, shared_admin_member_org], [solo_admin_member_org]] + end + end + + context "#delete_user" do + it "attempts to delete the user from the system via DELETE to the /users endpoint" do + expect(rest).to receive(:delete).with("users/#{username}") + knife.delete_user(username) + end + end + + context "#disassociate_user" do + it "attempts to remove dissociate the user from each org" do + removable_orgs.each { |org| expect(org).to receive(:dissociate_user).with(username) } + knife.disassociate_user(removable_orgs, username) + end end - it "deletes the user" do - expect(knife).to receive(:delete_object).with(Chef::UserV1, "my_user") - knife.run + context "#remove_from_admin_groups" do + it "attempts to remove the given user from the organizations' groups" do + removable_orgs.each { |org| expect(org).to receive(:remove_user_from_group).with("admins", username) } + knife.remove_from_admin_groups(removable_orgs, username) + 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) + context "#org_memberships" do + it "should make a REST request to return the list of organizations that the user is a member of" do + expect(rest).to receive(:get).with("users/test_user/organizations").and_return orgs_data + result = knife.org_memberships(username) + result.each_with_index do |v, x| + expect(v.to_hash).to eq(orgs[x].to_hash) + end + end end end diff --git a/spec/unit/knife/user_edit_spec.rb b/spec/unit/knife/user_edit_spec.rb index 54a44890e0..2fde328c0c 100644 --- a/spec/unit/knife/user_edit_spec.rb +++ b/spec/unit/knife/user_edit_spec.rb @@ -20,22 +20,28 @@ require "spec_helper" describe Chef::Knife::UserEdit do let(:knife) { Chef::Knife::UserEdit.new } + let(:root_rest) { double("Chef::ServerAPI") } before(:each) do @stderr = StringIO.new @stdout = StringIO.new - - Chef::Knife::UserEdit.load_deps allow(knife.ui).to receive(:stderr).and_return(@stderr) allow(knife.ui).to receive(:stdout).and_return(@stdout) - knife.name_args = [ "my_user" ] + knife.name_args = [ "my_user2" ] knife.config[:disable_editing] = true end it "loads and edits the user" do - data = { "username" => "my_user" } - allow(Chef::UserV1).to receive(:load).with("my_user").and_return(data) - expect(knife).to receive(:edit_hash).with(data).and_return(data) + data = { "username" => "my_user2" } + edited_data = { "username" => "edit_user2" } + result = {} + @key = "You don't come into cooking to get rich - Ramsay" + allow(result).to receive(:[]).with("private_key").and_return(@key) + + expect(Chef::ServerAPI).to receive(:new).with(Chef::Config[:chef_server_root]).and_return(root_rest) + expect(root_rest).to receive(:get).with("users/my_user2").and_return(data) + expect(knife).to receive(:get_updated_user).with(data).and_return(edited_data) + expect(root_rest).to receive(:put).with("users/my_user2", edited_data).and_return(result) knife.run end diff --git a/spec/unit/knife/user_list_spec.rb b/spec/unit/knife/user_list_spec.rb index 21c07f3fb1..63df590591 100644 --- a/spec/unit/knife/user_list_spec.rb +++ b/spec/unit/knife/user_list_spec.rb @@ -18,19 +18,56 @@ require "spec_helper" +Chef::Knife::UserList.load_deps + describe Chef::Knife::UserList do + let(:knife) { Chef::Knife::UserList.new } - let(:stdout) { StringIO.new } + let(:users) do + { + "user1" => "http//test/users/user1", + "user2" => "http//test/users/user2", + } + end + + before :each do + @rest = double("Chef::ServerAPI") + allow(Chef::ServerAPI).to receive(:new).and_return(@rest) + allow(@rest).to receive(:get).with("users").and_return(users) + end + + describe "with no arguments" do + it "lists all non users" do + expect(knife.ui).to receive(:output).with(%w{user1 user2}) + knife.run + end - before(:each) do - Chef::Knife::UserList.load_deps - allow(knife.ui).to receive(:stderr).and_return(stdout) - allow(knife.ui).to receive(:stdout).and_return(stdout) + end + + describe "with all_users argument" do + before do + knife.config[:all_users] = true + end + + it "lists all users including hidden users" do + expect(knife.ui).to receive(:output).with(%w{user1 user2}) + knife.run + end end it "lists the users" do - expect(Chef::UserV1).to receive(:list) expect(knife).to receive(:format_list_for_display) knife.run end + + describe "with options with_uri argument" do + before do + knife.config[:with_uri] = true + end + + it "lists all users including hidden users" do + expect(knife.ui).to receive(:output).with(users) + knife.run + end + end end diff --git a/spec/unit/knife/user_password_spec.rb b/spec/unit/knife/user_password_spec.rb new file mode 100644 index 0000000000..098597a14c --- /dev/null +++ b/spec/unit/knife/user_password_spec.rb @@ -0,0 +1,64 @@ +# +# Author:: Snehal Dwivedi (<sdwivedi@msystechnologies.com>) +# Copyright:: Copyright 2014-2016 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" +require "chef/org" + +Chef::Knife::UserDelete.load_deps + +describe Chef::Knife::UserPassword do + + let(:root_rest) { double("Chef::ServerAPI") } + + before :each do + @knife = Chef::Knife::UserPassword.new + @user_name = "foobar" + @password = "abc123" + @user = double("Chef::User") + allow(@user).to receive(:root_rest).and_return(root_rest) + @key = "You don't come into cooking to get rich - Ramsay" + end + + describe "should change user's password" do + before :each do + @knife.name_args << @user_name << @password + end + + it "with username and password" do + result = { "password" => [], "recovery_authentication_enabled" => true } + allow(@user).to receive(:[]).with("organization") + + expect(Chef::ServerAPI).to receive(:new).with(Chef::Config[:chef_server_root]).and_return(root_rest) + expect(@user.root_rest).to receive(:get).with("users/#{@user_name}").and_return(result) + expect(@user.root_rest).to receive(:put).with("users/#{@user_name}", result) + expect(@knife.ui).to receive(:msg).with("Authentication info updated for #{@user_name}.") + + @knife.run + end + end + + describe "should not change user's password" do + + it "ails with an informative message" do + expect(@knife).to receive(:show_usage) + expect(@knife.ui).to receive(:fatal).with("You must pass two arguments") + expect(@knife.ui).to receive(:fatal).with("Note that --enable-external-auth cannot be passed with a password") + expect { @knife.run }.to raise_error(SystemExit) + end + end +end diff --git a/spec/unit/knife/user_show_spec.rb b/spec/unit/knife/user_show_spec.rb index 198b9352f3..30742d8c21 100644 --- a/spec/unit/knife/user_show_spec.rb +++ b/spec/unit/knife/user_show_spec.rb @@ -17,30 +17,75 @@ # require "spec_helper" +require "chef/org" + +Chef::Knife::UserShow.load_deps describe Chef::Knife::UserShow 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.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) + let(:root_rest) { double("Chef::ServerAPI") } + + before :each do + @user_name = "foobar" + @password = "abc123" + @user = double("Chef::User") + allow(@user).to receive(:root_rest).and_return(root_rest) + # allow(Chef::User).to receive(:new).and_return(@user) + @key = "You don't come into cooking to get rich - Ramsay" end - it "loads and displays the user" do - expect(Chef::UserV1).to receive(:load).with("my_user").and_return(user_mock) - expect(knife).to receive(:format_for_display).with(user_mock) - knife.run + describe "withot organisation argument" do + before do + knife.name_args = [ "my_user" ] + allow(user_mock).to receive(:username).and_return("my_user") + end + + it "should load the user" do + expect(Chef::ServerAPI).to receive(:new).with(Chef::Config[:chef_server_root]).and_return(root_rest) + expect(@user.root_rest).to receive(:get).with("users/my_user") + knife.run + end + + it "loads and displays the user" do + expect(Chef::ServerAPI).to receive(:new).with(Chef::Config[:chef_server_root]).and_return(root_rest) + expect(@user.root_rest).to receive(:get).with("users/my_user") + expect(knife).to receive(:format_for_display) + 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 - 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) + describe "with organisation argument" do + before :each do + @user_name = "foobar" + @org_name = "abc_org" + knife.name_args << @user_name << @org_name + @org = double("Chef::Org") + allow(Chef::Org).to receive(:new).and_return(@org) + @key = "You don't come into cooking to get rich - Ramsay" + end + + let(:orgs) do + [@org] + end + + it "should load the user with organisation" do + + result = { "organizations" => [] } + knife.config[:with_orgs] = true + + expect(Chef::ServerAPI).to receive(:new).with(Chef::Config[:chef_server_root]).and_return(root_rest) + allow(@org).to receive(:[]).with("organization").and_return({ "name" => "test" }) + expect(@user.root_rest).to receive(:get).with("users/#{@user_name}").and_return(result) + expect(@user.root_rest).to receive(:get).with("users/#{@user_name}/organizations").and_return(orgs) + knife.run + end end end diff --git a/spec/unit/org_group_spec.rb b/spec/unit/org_group_spec.rb new file mode 100644 index 0000000000..47a2587a9b --- /dev/null +++ b/spec/unit/org_group_spec.rb @@ -0,0 +1,45 @@ + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "spec_helper" +require "chef/org" + +describe Chef::Org do + let(:org) { Chef::Org.new("myorg") } + + describe "API Interactions" do + before(:each) do + Chef::Config[:chef_server_root] = "http://www.example.com" + @rest = double("rest") + allow(Chef::ServerAPI).to receive(:new).and_return(@rest) + end + + describe "group" do + it "should load group data when it's not loaded." do + expect(@rest).to receive(:get_rest).with("organizations/myorg/groups/admins").and_return({}) + org.group("admins") + end + + it "should not load group data a second time when it's already loaded." do + expect(@rest).to receive(:get_rest) + .with("organizations/myorg/groups/admins") + .and_return({ anything: "goes" }) + .exactly(:once) + admin1 = org.group("admins") + admin2 = org.group("admins") + expect(admin1).to eq admin2 + end + end + end +end |