diff options
author | Serdar Sutay <serdar@opscode.com> | 2014-07-30 16:03:42 -0700 |
---|---|---|
committer | Serdar Sutay <serdar@opscode.com> | 2014-08-12 16:18:18 -0700 |
commit | ebe9a7f262f23ff7bd9f94afa9b0c1a07cbd2a73 (patch) | |
tree | 09e965ef64c6a9db49a4d2118dfa70ac3e649290 /spec/unit/provider/user | |
parent | 0c90f9868fb8a4576145645ca507f2452286ded3 (diff) | |
download | chef-ebe9a7f262f23ff7bd9f94afa9b0c1a07cbd2a73.tar.gz |
* Dscl user provider changes to support Mac 10.7, 10.8 & 10.9.
* Make the dscl user provider password handling idempotent.
* Refactor / modernize dscl user provider unit tests.
* Functional tests for dscl user provider.
Diffstat (limited to 'spec/unit/provider/user')
-rw-r--r-- | spec/unit/provider/user/dscl_spec.rb | 926 | ||||
-rw-r--r-- | spec/unit/provider/user/useradd_spec.rb | 1 |
2 files changed, 663 insertions, 264 deletions
diff --git a/spec/unit/provider/user/dscl_spec.rb b/spec/unit/provider/user/dscl_spec.rb index c7657aa6e5..0ff5a26ea6 100644 --- a/spec/unit/provider/user/dscl_spec.rb +++ b/spec/unit/provider/user/dscl_spec.rb @@ -20,142 +20,226 @@ ShellCmdResult = Struct.new(:stdout, :stderr, :exitstatus) require 'spec_helper' require 'ostruct' +require 'mixlib/shellout' describe Chef::Provider::User::Dscl do - before do - @node = Chef::Node.new - @events = Chef::EventDispatch::Dispatcher.new - @run_context = Chef::RunContext.new(@node, {}, @events) - @new_resource = Chef::Resource::User.new("toor") - @provider = Chef::Provider::User::Dscl.new(@new_resource, @run_context) - end + let(:node) { + node = Chef::Node.new + node.stub(:[]).with(:platform_version).and_return(mac_version) + node.stub(:[]).with(:platform).and_return("mac_os_x") + node + } + + let(:events) { + Chef::EventDispatch::Dispatcher.new + } + + let(:run_context) { + Chef::RunContext.new(node, {}, events) + } + + let(:new_resource) { + r = Chef::Resource::User.new("toor") + r.password(password) + r.salt(salt) + r.iterations(iterations) + r + } + + let(:provider) { + Chef::Provider::User::Dscl.new(new_resource, run_context) + } + + let(:mac_version) { + "10.9.1" + } + + let(:password) { nil } + let(:salt) { nil } + let(:iterations) { nil } + + let(:salted_sha512_password) { + "0f543f021c63255e64e121a3585601b8ecfedf6d2\ +705ddac69e682a33db5dbcdb9b56a2520bc8fff63a\ +2ba6b7984c0737ff0b7949455071581f7affcd536d\ +402b6cdb097" + } + + let(:salted_sha512_pbkdf2_password) { + "c734b6e4787c3727bb35e29fdd92b97c\ +1de12df509577a045728255ec7c6c5f5\ +c18efa05ed02b682ffa7ebc05119900e\ +b1d4880833aa7a190afc13e2bf0936b8\ +20123e8c98f0f9bcac2a629d9163caac\ +9464a8c234f3919082400b4f939bb77b\ +c5adbbac718b7eb99463a7b679571e0f\ +1c9fef2ef08d0b9e9c2bcf644eed2ffc" + } + + let(:salted_sha512_pbkdf2_salt) { + "2d942d8364a9ccf2b8e5cb7ed1ff58f78\ +e29dbfee7f9db58859144d061fd0058" + } + + let(:salted_sha512_pbkdf2_iterations) { + 25000 + } + + let(:vagrant_sha_512) { + "6f75d7190441facc34291ebbea1fc756b242d4f\ +e9bcff141bccb84f1979e27e539539aa31f9f7dcc92c0cea959\ +ea18e18b720e358e7fbe3cfbeaa561456f6ba008937a30" + } + + let(:vagrant_sha_512_pbkdf2) { + "12601a90db17cbf\ +8ba4808e6382fb0d3b9d8a6c1a190477bf680ab21afb\ +6065467136e55cc208a6f74156e3daf20fb13369ef4b\ +7bafa047d80359fb46a48a4adccd548ebb33851b093\ +47cca84341a7f93a27147343f89fb843fb46c0017d2\ +64afa4976baacf941b915bd1ec1ca24c30b3e759e02\ +403e02f59fe7ff5938a7636c" + } + + let(:vagrant_sha_512_pbkdf2_salt) { + "ee954be472fdc60ddf89484781433993625f006af6ec810c08f49a7e413946a1" + } + + let(:vagrant_sha_512_pbkdf2_iterations) { + 34482 + } describe "when shelling out to dscl" do it "should run dscl with the supplied cmd /Path args" do shell_return = ShellCmdResult.new('stdout', 'err', 0) - @provider.should_receive(:shell_out).with("dscl . -cmd /Path args").and_return(shell_return) - @provider.safe_dscl("cmd /Path args").should == 'stdout' + provider.should_receive(:shell_out).with("dscl . -cmd /Path args").and_return(shell_return) + provider.run_dscl("cmd /Path args").should == 'stdout' end it "returns an empty string from delete commands" do shell_return = ShellCmdResult.new('out', 'err', 23) - @provider.should_receive(:shell_out).with("dscl . -delete /Path args").and_return(shell_return) - @provider.safe_dscl("delete /Path args").should == "" + provider.should_receive(:shell_out).with("dscl . -delete /Path args").and_return(shell_return) + provider.run_dscl("delete /Path args").should == "" end it "should raise an exception for any other command" do shell_return = ShellCmdResult.new('out', 'err', 23) - @provider.should_receive(:shell_out).with('dscl . -cmd /Path arguments').and_return(shell_return) - lambda { @provider.safe_dscl("cmd /Path arguments") }.should raise_error(Chef::Exceptions::DsclCommandFailed) + provider.should_receive(:shell_out).with('dscl . -cmd /Path arguments').and_return(shell_return) + lambda { provider.run_dscl("cmd /Path arguments") }.should raise_error(Chef::Exceptions::DsclCommandFailed) end it "raises an exception when dscl reports 'no such key'" do shell_return = ShellCmdResult.new("No such key: ", 'err', 23) - @provider.should_receive(:shell_out).with('dscl . -cmd /Path args').and_return(shell_return) - lambda { @provider.safe_dscl("cmd /Path args") }.should raise_error(Chef::Exceptions::DsclCommandFailed) + provider.should_receive(:shell_out).with('dscl . -cmd /Path args').and_return(shell_return) + lambda { provider.run_dscl("cmd /Path args") }.should raise_error(Chef::Exceptions::DsclCommandFailed) end it "raises an exception when dscl reports 'eDSRecordNotFound'" do shell_return = ShellCmdResult.new("<dscl_cmd> DS Error: -14136 (eDSRecordNotFound)", 'err', -14136) - @provider.should_receive(:shell_out).with('dscl . -cmd /Path args').and_return(shell_return) - lambda { @provider.safe_dscl("cmd /Path args") }.should raise_error(Chef::Exceptions::DsclCommandFailed) + provider.should_receive(:shell_out).with('dscl . -cmd /Path args').and_return(shell_return) + lambda { provider.run_dscl("cmd /Path args") }.should raise_error(Chef::Exceptions::DsclCommandFailed) end end describe "get_free_uid" do before do - @provider.stub(:safe_dscl).and_return("\nwheel 200\nstaff 201\n") + provider.should_receive(:run_dscl).with("list /Users uid").and_return("\nwheel 200\nstaff 201\nbrahms 500\nchopin 501\n") end - it "should run safe_dscl with list /Users uid" do - @provider.should_receive(:safe_dscl).with("list /Users uid") - @provider.get_free_uid + describe "when resource is configured as system" do + before do + new_resource.system(true) + end + + it "should return the first unused uid number on or above 500" do + provider.get_free_uid.should eq(202) + end end it "should return the first unused uid number on or above 200" do - @provider.get_free_uid.should == 202 + provider.get_free_uid.should eq(502) end it "should raise an exception when the search limit is exhausted" do search_limit = 1 - lambda { @provider.get_free_uid(search_limit) }.should raise_error(RuntimeError) + lambda { provider.get_free_uid(search_limit) }.should raise_error(RuntimeError) end end describe "uid_used?" do - before do - @provider.stub(:safe_dscl).and_return("\naj 500\n") - end - - it "should run safe_dscl with list /Users uid" do - @provider.should_receive(:safe_dscl).with("list /Users uid") - @provider.uid_used?(500) + it "should return false if not given any valid uid number" do + provider.uid_used?(nil).should be_false end - it "should return true for a used uid number" do - @provider.uid_used?(500).should be_true - end + describe "when called with a user id" do + before do + provider.should_receive(:run_dscl).with("list /Users uid").and_return("\naj 500\n") + end - it "should return false for an unused uid number" do - @provider.uid_used?(501).should be_false - end + it "should return true for a used uid number" do + provider.uid_used?(500).should be_true + end - it "should return false if not given any valid uid number" do - @provider.uid_used?(nil).should be_false + it "should return false for an unused uid number" do + provider.uid_used?(501).should be_false + end end end describe "when determining the uid to set" do it "raises RequestedUIDUnavailable if the requested uid is already in use" do - @provider.stub(:uid_used?).and_return(true) - @provider.should_receive(:get_free_uid).and_return(501) - lambda { @provider.set_uid }.should raise_error(Chef::Exceptions::RequestedUIDUnavailable) + provider.stub(:uid_used?).and_return(true) + provider.should_receive(:get_free_uid).and_return(501) + lambda { provider.dscl_set_uid }.should raise_error(Chef::Exceptions::RequestedUIDUnavailable) end it "finds a valid, unused uid when none is specified" do - @provider.should_receive(:safe_dscl).with("list /Users uid").and_return('') - @provider.should_receive(:safe_dscl).with("create /Users/toor UniqueID 501") - @provider.should_receive(:get_free_uid).and_return(501) - @provider.set_uid - @new_resource.uid.should == 501 + provider.should_receive(:run_dscl).with("list /Users uid").and_return('') + provider.should_receive(:run_dscl).with("create /Users/toor UniqueID 501") + provider.should_receive(:get_free_uid).and_return(501) + provider.dscl_set_uid + new_resource.uid.should eq(501) end it "sets the uid specified in the resource" do - @new_resource.uid(1000) - @provider.should_receive(:safe_dscl).with("create /Users/toor UniqueID 1000").and_return(true) - @provider.should_receive(:safe_dscl).with("list /Users uid").and_return('') - @provider.set_uid + new_resource.uid(1000) + provider.should_receive(:run_dscl).with("create /Users/toor UniqueID 1000").and_return(true) + provider.should_receive(:run_dscl).with("list /Users uid").and_return('') + provider.dscl_set_uid end end describe "when modifying the home directory" do + let(:current_resource) { + new_resource.dup + } + before do - @new_resource.supports({ :manage_home => true }) - @new_resource.home('/Users/toor') + new_resource.supports({ :manage_home => true }) + new_resource.home('/Users/toor') - @current_resource = @new_resource.dup - @provider.current_resource = @current_resource + provider.current_resource = current_resource end it "deletes the home directory when resource#home is nil" do - @new_resource.instance_variable_set(:@home, nil) - @provider.should_receive(:safe_dscl).with("delete /Users/toor NFSHomeDirectory").and_return(true) - @provider.modify_home + new_resource.instance_variable_set(:@home, nil) + provider.should_receive(:run_dscl).with("delete /Users/toor NFSHomeDirectory").and_return(true) + provider.dscl_set_home end it "raises InvalidHomeDirectory when the resource's home directory doesn't look right" do - @new_resource.home('epic-fail') - lambda { @provider.modify_home }.should raise_error(Chef::Exceptions::InvalidHomeDirectory) + new_resource.home('epic-fail') + lambda { provider.dscl_set_home }.should raise_error(Chef::Exceptions::InvalidHomeDirectory) end it "moves the users home to the new location if it exists and the target location is different" do - @new_resource.supports(:manage_home => true) + new_resource.supports(:manage_home => true) current_home = CHEF_SPEC_DATA + '/old_home_dir' current_home_files = [current_home + '/my-dot-emacs', current_home + '/my-dot-vim'] - @current_resource.home(current_home) - @new_resource.gid(23) + current_resource.home(current_home) + new_resource.gid(23) ::File.stub(:exists?).with('/old/home/toor').and_return(true) ::File.stub(:exists?).with('/Users/toor').and_return(true) @@ -165,316 +249,630 @@ describe Chef::Provider::User::Dscl do FileUtils.should_receive(:mv).with(current_home_files, "/Users/toor", :force => true) FileUtils.should_receive(:chown_R).with('toor','23','/Users/toor') - @provider.should_receive(:safe_dscl).with("create /Users/toor NFSHomeDirectory '/Users/toor'") - @provider.modify_home + provider.should_receive(:run_dscl).with("create /Users/toor NFSHomeDirectory '/Users/toor'") + provider.dscl_set_home end it "should raise an exception when the systems user template dir (skel) cannot be found" do ::File.stub(:exists?).and_return(false,false,false) - lambda { @provider.modify_home }.should raise_error(Chef::Exceptions::User) + lambda { provider.dscl_set_home }.should raise_error(Chef::Exceptions::User) end it "should run ditto to copy any missing files from skel to the new home dir" do ::File.should_receive(:exists?).with("/System/Library/User\ Template/English.lproj").and_return(true) FileUtils.should_receive(:chown_R).with('toor', '', '/Users/toor') - @provider.should_receive(:shell_out!).with("ditto '/System/Library/User Template/English.lproj' '/Users/toor'") - @provider.ditto_home + provider.should_receive(:shell_out!).with("ditto '/System/Library/User Template/English.lproj' '/Users/toor'") + provider.ditto_home end it "creates the user's NFSHomeDirectory and home directory" do - @provider.should_receive(:safe_dscl).with("create /Users/toor NFSHomeDirectory '/Users/toor'").and_return(true) - @provider.should_receive(:ditto_home) - @provider.modify_home + provider.should_receive(:run_dscl).with("create /Users/toor NFSHomeDirectory '/Users/toor'").and_return(true) + provider.should_receive(:ditto_home) + provider.dscl_set_home end end - describe "osx_shadow_hash?" do - it "should return true when the string is a shadow hash" do - @provider.osx_shadow_hash?("0"*8*155).should eql(true) + describe "resource_requirements" do + let(:dscl_exists) { true } + let(:plutil_exists) { true } + + before do + ::File.stub(:exists?).with("/usr/bin/dscl").and_return(dscl_exists) + ::File.stub(:exists?).with("/usr/bin/plutil").and_return(plutil_exists) end - it "should return false otherwise" do - @provider.osx_shadow_hash?("any other string").should eql(false) + def run_requirements + provider.define_resource_requirements + provider.action = :create + provider.process_resource_requirements end - end - describe "when detecting the format of a password" do - it "detects a OS X salted sha1" do - @provider.osx_salted_sha1?("0"*48).should eql(true) - @provider.osx_salted_sha1?("any other string").should eql(false) + describe "when dscl doesn't exist" do + let(:dscl_exists) { false } + + it "should raise an error" do + lambda { run_requirements }.should raise_error + end end - end - describe "guid" do - it "should run safe_dscl with read /Users/user GeneratedUID to get the users GUID" do - expected_uuid = "b398449e-cee0-45e0-80f8-b0b5b1bfdeaa" - @provider.should_receive(:safe_dscl).with("read /Users/toor GeneratedUID").and_return(expected_uuid + "\n") - @provider.guid.should == expected_uuid + describe "when plutil doesn't exist" do + let(:plutil_exists) { false } + + it "should raise an error" do + lambda { run_requirements }.should raise_error + end + end + + describe "when on Mac 10.6" do + let(:mac_version) { + "10.6.5" + } + + it "should raise an error" do + lambda { run_requirements }.should raise_error + end + end + + describe "when on Mac 10.7" do + let(:mac_version) { + "10.7.5" + } + + describe "when password is SALTED-SHA512" do + let(:password) { salted_sha512_password } + + it "should not raise an error" do + lambda { run_requirements }.should_not raise_error + end + end + + describe "when password is SALTED-SHA512-PBKDF2" do + let(:password) { salted_sha512_pbkdf2_password } + + it "should raise an error" do + lambda { run_requirements }.should raise_error + end + end + end + + [ "10.9", "10.10"].each do |version| + describe "when on Mac #{version}" do + let(:mac_version) { + "#{version}.2" + } + + describe "when password is SALTED-SHA512" do + let(:password) { salted_sha512_password } + + it "should raise an error" do + lambda { run_requirements }.should raise_error + end + end + + describe "when password is SALTED-SHA512-PBKDF2" do + let(:password) { salted_sha512_pbkdf2_password } + + describe "when salt and iteration is not set" do + it "should raise an error" do + lambda { run_requirements }.should raise_error + end + end + + describe "when salt and iteration is set" do + let(:salt) { salted_sha512_pbkdf2_salt } + let(:iterations) { salted_sha512_pbkdf2_iterations } + + it "should not raise an error" do + lambda { run_requirements }.should_not raise_error + end + end + end + end end end - describe "shadow_hash_set?" do + describe "load_current_resource" do + # set this to any of the user plist files under spec/data + let(:user_plist_file) { nil } + + before do + provider.should_receive(:shell_out).with("plutil -convert xml1 -o - /var/db/dslocal/nodes/Default/users/toor.plist") do + if user_plist_file.nil? + ShellCmdResult.new('Can not find the file', 'Sorry!!', 1) + else + ShellCmdResult.new(File.read(File.join(CHEF_SPEC_DATA, "mac_users/#{user_plist_file}.plist.xml")), "", 0) + end + end - it "should run safe_dscl with read /Users/user to see if the AuthenticationAuthority key exists" do - @provider.should_receive(:safe_dscl).with("read /Users/toor") - @provider.shadow_hash_set? + if !user_plist_file.nil? + provider.should_receive(:convert_binary_plist_to_xml).and_return(File.read(File.join(CHEF_SPEC_DATA, "mac_users/#{user_plist_file}.shadow.xml"))) + end end - describe "when the user account has an AuthenticationAuthority key" do - it "uses the shadow hash when there is a ShadowHash field in the AuthenticationAuthority key" do - @provider.should_receive(:safe_dscl).with("read /Users/toor").and_return("\nAuthenticationAuthority: ;ShadowHash;\n") - @provider.shadow_hash_set?.should be_true + describe "when user is not there" do + it "shouldn't raise an error" do + lambda { provider.load_current_resource }.should_not raise_error end - it "does not use the shadow hash when there is no ShadowHash field in the AuthenticationAuthority key" do - @provider.should_receive(:safe_dscl).with("read /Users/toor").and_return("\nAuthenticationAuthority: \n") - @provider.shadow_hash_set?.should be_false + it "should set @user_exists" do + provider.load_current_resource + provider.instance_variable_get(:@user_exists).should be_false end + it "should set username" do + provider.load_current_resource + provider.current_resource.username.should eq("toor") + end end - describe "with no AuthenticationAuthority key in the user account" do - it "does not use the shadow hash" do - @provider.should_receive(:safe_dscl).with("read /Users/toor").and_return("") - @provider.shadow_hash_set?.should eql(false) + describe "when user is there" do + let(:password) { "something" } # Load password during load_current_resource + + describe "on 10.7" do + let(:mac_version) { + "10.7.5" + } + + let(:user_plist_file) { "10.7" } + + it "collects the user data correctly" do + provider.load_current_resource + provider.current_resource.comment.should eq("vagrant") + provider.current_resource.uid.should eq("11112222-3333-4444-AAAA-BBBBCCCCDDDD") + provider.current_resource.gid.should eq("80") + provider.current_resource.home.should eq("/Users/vagrant") + provider.current_resource.shell.should eq("/bin/bash") + provider.current_resource.password.should eq(vagrant_sha_512) + end + + describe "when a plain password is set that is same" do + let(:password) { "vagrant" } + + it "diverged_password? should report false" do + provider.load_current_resource + pending + provider.diverged_password?.should be_false + end + end + + describe "when a plain password is set that is different" do + let(:password) { "not_vagrant" } + + it "diverged_password? should report true" do + provider.load_current_resource + pending + provider.diverged_password?.should be_true + end + end + + describe "when iterations change" do + let(:password) { vagrant_sha_512 } + let(:iterations) { 12345 } + + it "diverged_password? should report false" do + provider.load_current_resource + provider.diverged_password?.should be_false + end + end + + describe "when shadow hash changes" do + let(:password) { salted_sha512_password } + + it "diverged_password? should report true" do + provider.load_current_resource + provider.diverged_password?.should be_true + end + end + + describe "when salt change" do + let(:password) { vagrant_sha_512 } + let(:salt) { "SOMETHINGRANDOM" } + + it "diverged_password? should report false" do + provider.load_current_resource + provider.diverged_password?.should be_false + end + end + end + + describe "on 10.8" do + let(:mac_version) { + "10.8.3" + } + + let(:user_plist_file) { "10.8" } + + it "collects the user data correctly" do + provider.load_current_resource + provider.current_resource.comment.should eq("vagrant") + provider.current_resource.uid.should eq("11112222-3333-4444-AAAA-BBBBCCCCDDDD") + provider.current_resource.gid.should eq("80") + provider.current_resource.home.should eq("/Users/vagrant") + provider.current_resource.shell.should eq("/bin/bash") + provider.current_resource.password.should eq("ea4c2d265d801ba0ec0dfccd\ +253dfc1de91cbe0806b4acc1ed7fe22aebcf6beb5344d0f442e590\ +ffa04d679075da3afb119e41b72b5eaf08ee4aa54693722646d5\ +19ee04843deb8a3e977428d33f625e83887913e5c13b70035961\ +5e00ad7bc3e7a0c98afc3e19d1360272454f8d33a9214d2fbe8b\ +e68d1f9821b26689312366") + provider.current_resource.salt.should eq("f994ef2f73b7c5594ebd1553300976b20733ce0e24d659783d87f3d81cbbb6a9") + provider.current_resource.iterations.should eq(39840) + end + end + + describe "on 10.7 upgraded to 10.8" do + # In this scenario user password is still in 10.7 format + let(:mac_version) { + "10.8.3" + } + + let(:user_plist_file) { "10.7-8" } + + it "collects the user data correctly" do + provider.load_current_resource + provider.current_resource.comment.should eq("vagrant") + provider.current_resource.uid.should eq("11112222-3333-4444-AAAA-BBBBCCCCDDDD") + provider.current_resource.gid.should eq("80") + provider.current_resource.home.should eq("/Users/vagrant") + provider.current_resource.shell.should eq("/bin/bash") + provider.current_resource.password.should eq("6f75d7190441facc34291ebbea1fc756b242d4f\ +e9bcff141bccb84f1979e27e539539aa31f9f7dcc92c0cea959\ +ea18e18b720e358e7fbe3cfbeaa561456f6ba008937a30") + end + + describe "when a plain text password is set" do + it "reports password needs to be updated" do + provider.load_current_resource + provider.diverged_password?.should be_true + end + end + + describe "when a salted-sha512-pbkdf2 shadow is set" do + let(:password) { salted_sha512_pbkdf2_password } + let(:salt) { salted_sha512_pbkdf2_salt } + let(:iterations) { salted_sha512_pbkdf2_iterations } + + it "reports password needs to be updated" do + provider.load_current_resource + provider.diverged_password?.should be_true + end + end + end + + describe "on 10.9" do + let(:mac_version) { + "10.9.1" + } + + let(:user_plist_file) { "10.9" } + + it "collects the user data correctly" do + provider.load_current_resource + provider.current_resource.comment.should eq("vagrant") + provider.current_resource.uid.should eq("11112222-3333-4444-AAAA-BBBBCCCCDDDD") + provider.current_resource.gid.should eq("80") + provider.current_resource.home.should eq("/Users/vagrant") + provider.current_resource.shell.should eq("/bin/bash") + provider.current_resource.password.should eq(vagrant_sha_512_pbkdf2) + provider.current_resource.salt.should eq(vagrant_sha_512_pbkdf2_salt) + provider.current_resource.iterations.should eq(vagrant_sha_512_pbkdf2_iterations) + end + + describe "when a plain password is set that is same" do + let(:password) { "vagrant" } + + it "diverged_password? should report false" do + provider.load_current_resource + provider.diverged_password?.should be_false + end + end + + describe "when a plain password is set that is different" do + let(:password) { "not_vagrant" } + + it "diverged_password? should report true" do + provider.load_current_resource + provider.diverged_password?.should be_true + end + end + + describe "when iterations change" do + let(:password) { vagrant_sha_512_pbkdf2 } + let(:salt) { vagrant_sha_512_pbkdf2_salt } + let(:iterations) { 12345 } + + it "diverged_password? should report true" do + provider.load_current_resource + provider.diverged_password?.should be_true + end + end + + describe "when shadow hash changes" do + let(:password) { salted_sha512_pbkdf2_password } + let(:salt) { vagrant_sha_512_pbkdf2_salt } + let(:iterations) { vagrant_sha_512_pbkdf2_iterations } + + it "diverged_password? should report true" do + provider.load_current_resource + provider.diverged_password?.should be_true + end + end + + describe "when salt change" do + let(:password) { vagrant_sha_512_pbkdf2 } + let(:salt) { salted_sha512_pbkdf2_salt } + let(:iterations) { vagrant_sha_512_pbkdf2_iterations } + + it "diverged_password? should report true" do + provider.load_current_resource + provider.diverged_password?.should be_true + end + end end end end - describe "when setting or modifying the user password" do - before do - @new_resource.password("password") - @output = StringIO.new + describe "salted_sha512_pbkdf2?" do + it "should return true when the string is a salted_sha512_pbkdf2 hash" do + provider.salted_sha512_pbkdf2?(salted_sha512_pbkdf2_password).should be_true end - describe "when using a salted sha1 for the password" do - before do - @new_resource.password("F"*48) - end - - it "should write a shadow hash file with the expected salted sha1" do - uuid = "B398449E-CEE0-45E0-80F8-B0B5B1BFDEAA" - File.should_receive(:open).with('/var/db/shadow/hash/B398449E-CEE0-45E0-80F8-B0B5B1BFDEAA', "w", 384).and_yield(@output) - @provider.should_receive(:safe_dscl).with("read /Users/toor GeneratedUID").and_return(uuid) - @provider.should_receive(:safe_dscl).with("read /Users/toor").and_return("\nAuthenticationAuthority: ;ShadowHash;\n") - expected_salted_sha1 = @new_resource.password - expected_shadow_hash = "00000000"*155 - expected_shadow_hash[168] = expected_salted_sha1 - @provider.modify_password - @output.string.strip.should == expected_shadow_hash - end - end - - describe "when given a shadow hash file for the password" do - it "should write the shadow hash file directly to /var/db/shadow/hash/GUID" do - shadow_hash = '0123456789ABCDE0123456789ABCDEF' * 40 - raise 'oops' unless shadow_hash.size == 1240 - @new_resource.password shadow_hash - uuid = "B398449E-CEE0-45E0-80F8-B0B5B1BFDEAA" - File.should_receive(:open).with('/var/db/shadow/hash/B398449E-CEE0-45E0-80F8-B0B5B1BFDEAA', "w", 384).and_yield(@output) - @provider.should_receive(:safe_dscl).with("read /Users/toor GeneratedUID").and_return(uuid) - @provider.should_receive(:safe_dscl).with("read /Users/toor").and_return("\nAuthenticationAuthority: ;ShadowHash;\n") - @provider.modify_password - @output.string.strip.should == shadow_hash - end - end - - describe "when given a string for the password" do - it "should output a salted sha1 and shadow hash file from the specified password" do - uuid = "B398449E-CEE0-45E0-80F8-B0B5B1BFDEAA" - File.should_receive(:open).with('/var/db/shadow/hash/B398449E-CEE0-45E0-80F8-B0B5B1BFDEAA', "w", 384).and_yield(@output) - @new_resource.password("password") - OpenSSL::Random.stub(:random_bytes).and_return("\377\377\377\377\377\377\377\377") - expected_salted_sha1 = "F"*8+"SHA1-"*8 - expected_shadow_hash = "00000000"*155 - expected_shadow_hash[168] = expected_salted_sha1 - @provider.should_receive(:safe_dscl).with("read /Users/toor GeneratedUID").and_return(uuid) - @provider.should_receive(:safe_dscl).with("read /Users/toor").and_return("\nAuthenticationAuthority: ;ShadowHash;\n") - @provider.modify_password - @output.string.strip.should match(/^0{168}(FFFFFFFF1C1AA7935D4E1190AFEC92343F31F7671FBF126D)0{1071}$/) - end - end - - it "should write the output directly to the shadow hash file at /var/db/shadow/hash/GUID" do - shadow_file = StringIO.new - uuid = "B398449E-CEE0-45E0-80F8-B0B5B1BFDEAA" - File.should_receive(:open).with("/var/db/shadow/hash/B398449E-CEE0-45E0-80F8-B0B5B1BFDEAA",'w',0600).and_yield(shadow_file) - @provider.should_receive(:safe_dscl).with("read /Users/toor GeneratedUID").and_return(uuid) - @provider.should_receive(:safe_dscl).with("read /Users/toor").and_return("\nAuthenticationAuthority: ;ShadowHash;\n") - @provider.modify_password - shadow_file.string.should match(/^0{168}[0-9A-F]{48}0{1071}$/) - end - - it "should run safe_dscl append /Users/user AuthenticationAuthority ;ShadowHash; when no shadow hash set" do - shadow_file = StringIO.new - uuid = "B398449E-CEE0-45E0-80F8-B0B5B1BFDEAA" - File.should_receive(:open).with("/var/db/shadow/hash/B398449E-CEE0-45E0-80F8-B0B5B1BFDEAA",'w',0600).and_yield(shadow_file) - @provider.should_receive(:safe_dscl).with("read /Users/toor GeneratedUID").and_return(uuid) - @provider.should_receive(:safe_dscl).with("read /Users/toor").and_return("\nAuthenticationAuthority:\n") - @provider.should_receive(:safe_dscl).with("append /Users/toor AuthenticationAuthority ';ShadowHash;'") - @provider.modify_password - shadow_file.string.should match(/^0{168}[0-9A-F]{48}0{1071}$/) + it "should return false otherwise" do + provider.salted_sha512_pbkdf2?(salted_sha512_password).should be_false + provider.salted_sha512_pbkdf2?("any other string").should be_false end end - describe "load_current_resource" do - it "should raise an error if the required binary /usr/bin/dscl doesn't exist" do - ::File.should_receive(:exists?).with("/usr/bin/dscl").and_return(false) - lambda { @provider.load_current_resource }.should raise_error(Chef::Exceptions::User) + describe "salted_sha512?" do + it "should return true when the string is a salted_sha512_pbkdf2 hash" do + provider.salted_sha512_pbkdf2?(salted_sha512_pbkdf2_password).should be_true end - it "shouldn't raise an error if /usr/bin/dscl exists" do - ::File.stub(:exists?).and_return(true) - lambda { @provider.load_current_resource }.should_not raise_error + it "should return false otherwise" do + provider.salted_sha512?(salted_sha512_pbkdf2_password).should be_false + provider.salted_sha512?("any other string").should be_false + end + end + + describe "prepare_password_shadow_info" do + describe "when on Mac 10.7" do + let(:mac_version) { + "10.7.1" + } + + describe "when the password is plain text" do + let(:password) { "vagrant" } + + it "password_shadow_info should have salted-sha-512 format" do + pending + shadow_info = provider.prepare_password_shadow_info + shadow_info.should have_key("SALTED-SHA512") + info = shadow_info["SALTED-SHA512"].string.unpack('H*').first + provider.salted_sha512?(info).should be_true + end + end + + describe "when the password is salted-sha-512" do + let(:password) { vagrant_sha_512 } + + it "password_shadow_info should have salted-sha-512 format" do + shadow_info = provider.prepare_password_shadow_info + shadow_info.should have_key("SALTED-SHA512") + info = shadow_info["SALTED-SHA512"].string.unpack('H*').first + provider.salted_sha512?(info).should be_true + info.should eq(vagrant_sha_512) + end + end + end + + ["10.8", "10.9", "10.10"].each do |version| + describe "when on Mac #{version}" do + let(:mac_version) { + "#{version}.1" + } + + describe "when the password is plain text" do + let(:password) { "vagrant" } + + it "password_shadow_info should have salted-sha-512 format" do + shadow_info = provider.prepare_password_shadow_info + shadow_info.should have_key("SALTED-SHA512-PBKDF2") + shadow_info["SALTED-SHA512-PBKDF2"].should have_key("entropy") + shadow_info["SALTED-SHA512-PBKDF2"].should have_key("salt") + shadow_info["SALTED-SHA512-PBKDF2"].should have_key("iterations") + info = shadow_info["SALTED-SHA512-PBKDF2"]["entropy"].string.unpack('H*').first + provider.salted_sha512_pbkdf2?(info).should be_true + end + end + + describe "when the password is salted-sha-512" do + let(:password) { vagrant_sha_512_pbkdf2 } + let(:iterations) { vagrant_sha_512_pbkdf2_iterations } + let(:salt) { vagrant_sha_512_pbkdf2_salt } + + it "password_shadow_info should have salted-sha-512 format" do + shadow_info = provider.prepare_password_shadow_info + shadow_info.should have_key("SALTED-SHA512-PBKDF2") + shadow_info["SALTED-SHA512-PBKDF2"].should have_key("entropy") + shadow_info["SALTED-SHA512-PBKDF2"].should have_key("salt") + shadow_info["SALTED-SHA512-PBKDF2"].should have_key("iterations") + info = shadow_info["SALTED-SHA512-PBKDF2"]["entropy"].string.unpack('H*').first + provider.salted_sha512_pbkdf2?(info).should be_true + info.should eq(vagrant_sha_512_pbkdf2) + end + end + end + end + end + + describe "set_password" do + before do + new_resource.password("something") + end + + it "should sleep and flush the dscl cache before saving the password" do + provider.should_receive(:prepare_password_shadow_info).and_return({ }) + mock_shellout = double("Mock::Shellout") + mock_shellout.stub(:run_command) + Mixlib::ShellOut.should_receive(:new).and_return(mock_shellout) + provider.should_receive(:read_user_info) + provider.should_receive(:dscl_set) + provider.should_receive(:sleep).with(3) + provider.should_receive(:shell_out).with("dscacheutil '-flushcache'") + provider.should_receive(:save_user_info) + provider.set_password end end describe "when the user does not yet exist and chef is creating it" do context "with a numeric gid" do before do - @new_resource.comment "#mockssuck" - @new_resource.gid 1001 + new_resource.comment "#mockssuck" + new_resource.gid 1001 end it "creates the user, comment field, sets uid, gid, configures the home directory, sets the shell, and sets the password" do - @provider.should_receive :dscl_create_user - @provider.should_receive :dscl_create_comment - @provider.should_receive :set_uid - @provider.should_receive :dscl_set_gid - @provider.should_receive :modify_home - @provider.should_receive :dscl_set_shell - @provider.should_receive :modify_password - @provider.create_user + provider.should_receive :dscl_create_user + provider.should_receive :dscl_create_comment + provider.should_receive :dscl_set_uid + provider.should_receive :dscl_set_gid + provider.should_receive :dscl_set_home + provider.should_receive :dscl_set_shell + provider.should_receive :set_password + provider.create_user end it "creates the user and sets the comment field" do - @provider.should_receive(:safe_dscl).with("create /Users/toor").and_return(true) - @provider.dscl_create_user + provider.should_receive(:run_dscl).with("create /Users/toor").and_return(true) + provider.dscl_create_user end it "sets the comment field" do - @provider.should_receive(:safe_dscl).with("create /Users/toor RealName '#mockssuck'").and_return(true) - @provider.dscl_create_comment + provider.should_receive(:run_dscl).with("create /Users/toor RealName '#mockssuck'").and_return(true) + provider.dscl_create_comment end - it "should run safe_dscl with create /Users/user PrimaryGroupID to set the users primary group" do - @provider.should_receive(:safe_dscl).with("create /Users/toor PrimaryGroupID '1001'").and_return(true) - @provider.dscl_set_gid + it "should run run_dscl with create /Users/user PrimaryGroupID to set the users primary group" do + provider.should_receive(:run_dscl).with("create /Users/toor PrimaryGroupID '1001'").and_return(true) + provider.dscl_set_gid end - it "should run safe_dscl with create /Users/user UserShell to set the users login shell" do - @provider.should_receive(:safe_dscl).with("create /Users/toor UserShell '/usr/bin/false'").and_return(true) - @provider.dscl_set_shell + it "should run run_dscl with create /Users/user UserShell to set the users login shell" do + provider.should_receive(:run_dscl).with("create /Users/toor UserShell '/usr/bin/false'").and_return(true) + provider.dscl_set_shell end end context "with a non-numeric gid" do before do - @new_resource.comment "#mockssuck" - @new_resource.gid "newgroup" + new_resource.comment "#mockssuck" + new_resource.gid "newgroup" end it "should map the group name to a numeric ID when the group exists" do - @provider.should_receive(:safe_dscl).with("read /Groups/newgroup PrimaryGroupID").ordered.and_return("PrimaryGroupID: 1001\n") - @provider.should_receive(:safe_dscl).with("create /Users/toor PrimaryGroupID '1001'").ordered.and_return(true) - @provider.dscl_set_gid + provider.should_receive(:run_dscl).with("read /Groups/newgroup PrimaryGroupID").ordered.and_return("PrimaryGroupID: 1001\n") + provider.should_receive(:run_dscl).with("create /Users/toor PrimaryGroupID '1001'").ordered.and_return(true) + provider.dscl_set_gid end it "should raise an exception when the group does not exist" do shell_return = ShellCmdResult.new("<dscl_cmd> DS Error: -14136 (eDSRecordNotFound)", 'err', -14136) - @provider.should_receive(:shell_out).with('dscl . -read /Groups/newgroup PrimaryGroupID').and_return(shell_return) - lambda { @provider.dscl_set_gid }.should raise_error(Chef::Exceptions::GroupIDNotFound) + provider.should_receive(:shell_out).with('dscl . -read /Groups/newgroup PrimaryGroupID').and_return(shell_return) + lambda { provider.dscl_set_gid }.should raise_error(Chef::Exceptions::GroupIDNotFound) end end end describe "when the user exists and chef is managing it" do before do - @current_resource = @new_resource.dup - @provider.current_resource = @current_resource + current_resource = new_resource.dup + provider.current_resource = current_resource - # These are all different from @current_resource - @new_resource.username "mud" - @new_resource.uid 2342 - @new_resource.gid 2342 - @new_resource.home '/Users/death' - @new_resource.password 'goaway' + # These are all different from current_resource + new_resource.username "mud" + new_resource.uid 2342 + new_resource.gid 2342 + new_resource.home '/Users/death' + new_resource.password 'goaway' end it "sets the user, comment field, uid, gid, moves the home directory, sets the shell, and sets the password" do - @provider.should_receive :dscl_create_user - @provider.should_receive :dscl_create_comment - @provider.should_receive :set_uid - @provider.should_receive :dscl_set_gid - @provider.should_receive :modify_home - @provider.should_receive :dscl_set_shell - @provider.should_receive :modify_password - @provider.create_user + provider.should_receive :dscl_create_user + provider.should_receive :dscl_create_comment + provider.should_receive :dscl_set_uid + provider.should_receive :dscl_set_gid + provider.should_receive :dscl_set_home + provider.should_receive :dscl_set_shell + provider.should_receive :set_password + provider.create_user end end describe "when changing the gid" do before do - @current_resource = @new_resource.dup - @provider.current_resource = @current_resource + current_resource = new_resource.dup + provider.current_resource = current_resource - # This is different from @current_resource - @new_resource.gid 2342 + # This is different from current_resource + new_resource.gid 2342 end it "sets the gid" do - @provider.should_receive :dscl_set_gid - @provider.manage_user + provider.should_receive :dscl_set_gid + provider.manage_user end end - describe "when the user exists and chef is removing it" do - it "removes the user's home directory when the resource is configured to manage home" do - @new_resource.supports({ :manage_home => true }) - @provider.should_receive(:safe_dscl).with("read /Users/toor").and_return("NFSHomeDirectory: /Users/fuuuuuuuuuuuuu") - @provider.should_receive(:safe_dscl).with("delete /Users/toor") - FileUtils.should_receive(:rm_rf).with("/Users/fuuuuuuuuuuuuu") - @provider.remove_user + describe "when the user exists" do + before do + provider.should_receive(:shell_out).with("plutil -convert xml1 -o - /var/db/dslocal/nodes/Default/users/toor.plist") do + ShellCmdResult.new(File.read(File.join(CHEF_SPEC_DATA, "mac_users/10.9.plist.xml")), "", 0) + end + provider.load_current_resource + end + + describe "when Chef is removing the user" do + it "removes the user from the groups and deletes home directory when the resource is configured to manage home" do + new_resource.supports({ :manage_home => true }) + provider.should_receive(:run_dscl).with("list /Groups").and_return("my_group\nyour_group\nreal_group\n") + provider.should_receive(:run_dscl).with("read /Groups/my_group").and_raise(Chef::Exceptions::DsclCommandFailed) # Empty group + provider.should_receive(:run_dscl).with("read /Groups/your_group").and_return("GroupMembership: not_you") + provider.should_receive(:run_dscl).with("read /Groups/real_group").and_return("GroupMembership: toor") + provider.should_receive(:run_dscl).with("delete /Groups/real_group GroupMembership 'toor'") + provider.should_receive(:run_dscl).with("delete /Users/toor") + FileUtils.should_receive(:rm_rf).with("/Users/vagrant") + provider.remove_user + end end - it "removes the user from any group memberships" do - Etc.stub(:group).and_yield(OpenStruct.new(:name => 'ragefisters', :mem => 'toor')) - @provider.should_receive(:safe_dscl).with("delete /Users/toor") - @provider.should_receive(:safe_dscl).with("delete /Groups/ragefisters GroupMembership 'toor'") - @provider.remove_user + describe "when user is not locked" do + it "determines the user as not locked" do + provider.should_not be_locked + end end - end - - describe "when discovering if a user is locked" do - it "determines the user is not locked when dscl shows an AuthenticationAuthority without a DisabledUser field" do - @provider.should_receive(:safe_dscl).with("read /Users/toor") - @provider.should_not be_locked - end + describe "when user is locked" do + before do + auth_authority = provider.instance_variable_get(:@authentication_authority) + provider.instance_variable_set(:@authentication_authority, auth_authority + ";DisabledUser;") + end - it "determines the user is locked when dscl shows an AuthenticationAuthority with a DisabledUser field" do - @provider.should_receive(:safe_dscl).with('read /Users/toor').and_return("\nAuthenticationAuthority: ;DisabledUser;\n") - @provider.should be_locked - end + it "determines the user as not locked" do + provider.should be_locked + end - it "determines the user is not locked when dscl shows no AuthenticationAuthority" do - @provider.should_receive(:safe_dscl).with('read /Users/toor').and_return("\n") - @provider.should_not be_locked + it "can unlock the user" do + provider.should_receive(:run_dscl).with("create /Users/toor AuthenticationAuthority ';ShadowHash;HASHLIST:<SALTED-SHA512-PBKDF2>'") + provider.unlock_user + end end end describe "when locking the user" do - it "should run safe_dscl with append /Users/user AuthenticationAuthority ;DisabledUser; to lock the user account" do - @provider.should_receive(:safe_dscl).with("append /Users/toor AuthenticationAuthority ';DisabledUser;'") - @provider.lock_user + it "should run run_dscl with append /Users/user AuthenticationAuthority ;DisabledUser; to lock the user account" do + provider.should_receive(:run_dscl).with("append /Users/toor AuthenticationAuthority ';DisabledUser;'") + provider.lock_user end end - describe "when unlocking the user" do - it "removes DisabledUser from the authentication string" do - @provider.should_receive(:safe_dscl).with("read /Users/toor AuthenticationAuthority").and_return("\nAuthenticationAuthority: ;ShadowHash; ;DisabledUser;\n") - @provider.should_receive(:safe_dscl).with("create /Users/toor AuthenticationAuthority ';ShadowHash;'") - @provider.unlock_user - end - end end diff --git a/spec/unit/provider/user/useradd_spec.rb b/spec/unit/provider/user/useradd_spec.rb index 64734c499f..a65da3f9e1 100644 --- a/spec/unit/provider/user/useradd_spec.rb +++ b/spec/unit/provider/user/useradd_spec.rb @@ -19,6 +19,7 @@ # require 'spec_helper' +require 'chef/provider/user/useradd' describe Chef::Provider::User::Useradd do |