OS X is an interesting operating system. It is a Unix, but is primarily used for workstations. As such, many system settings can, and should, be done as a non-privileged user. Some tasks, however, require administrative privileges. OS X uses sudo to escalate privileges. This is done by a nice GUI pop-up requesting the user password when done through another GUI element. However, one must use sudo $COMMAND when working at the Terminal.

The Homebrew package manager tries to do everything as a non-privileged user. The installation script will invoke some commands with sudo - namely to create and set the correct permissions on /usr/local (its default installation location). Once that is complete, brew install will not require privileged access for installing packages. In fact, the Homebrew project recommends never using sudo with the brew commands.

In Chef 12 the default provider for the package resource is homebrew. This originally came from the homebrew cookbook. In order to not use sudo when managing packages, there’s a helper method (mixin) that attempts to determine what non-privileged user should run the brew install command. This is also ported to Chef 12. The method can also take an argument that specifies a particular user that should run the brew command.

When managing an OS X system with Chef, it is often easier to just run chef-client as root, rather than be around when sudo prompts for a password. This means that we need a way to execute other commands for managing OS X as a non-privileged user. We can reuse the mixin to do this. I’ll demonstrate this using plain old Ruby with pry, which is installed in ChefDK, and I’ll start it up with sudo. Then, I’ll show a short recipe with chef-apply.

% which pry
/opt/chefdk/embedded/bin/pry
% sudo pry

Paste in the following Ruby code:

require 'chef'
include Chef::Mixin::HomebrewUser
include Chef::Mixin::ShellOut

find_homebrew_uid #=> 501

The method find_homebrew_uid is the helper we want. As we can see, rather than returning 0 (for root), it returns 501, which is the UID of the jtimberman user on my system. To prove that I’m executing in a process owned by root:

Process.uid #=> 0

Or, I can shell out to the whoami command using Chef’s shell_out method - which is the same method Chef would use to run brew install.

shell_out('whoami').stdout #=> "root\n"

The shell_out method can take a :user attribute:

shell_out('whoami', :user => find_homebrew_uid).stdout #=> "jtimberman\n"

So this can be used to install packages with brew, and is exactly what Chef 12 does.

shell_out('brew install coreutils', :user => find_homebrew_uid)

Or, it can be used to run defaults(1) settings that require running as a specific user, rather than root

# Turn off iPhoto face detection, please
shell_out('defaults write com.apple.iPhoto PKFaceDetectionEnabled 0', 
          :user => find_homebrew_uid)
# before...
jtimberman@localhost% defaults read com.apple.iPhoto PKFaceDetectionEnabled
1
# after!
jtimberman@localhost% defaults read com.apple.iPhoto PKFaceDetectionEnabled
0

Putting this together in a Chef recipe that gets run by root, we can disable face detection in iPhoto like this:

Chef::Resource::Execute.send(:include, Chef::Mixin::HomebrewUser)

execute 'defaults write com.apple.iPhoto PKFaceDetectionEnabled 0' do
  user find_homebrew_uid
end

The first line makes the method available on all execute resources. To make the method available to all resources, use Chef::Resource.send, and to make it available across everything in all recipes, use Chef::Recipe.send. Otherwise we would get a NoMethodError exception.

The execute resource takes a user attribute, so we use the find_homebrew_uid method here to set the user. And we can observe the same results as above:

jtimberman@localhost% defaults write com.apple.iPhoto PKFaceDetectionEnabled 1
jtimberman@localhost% defaults read com.apple.iPhoto PKFaceDetectionEnabled
1
jtimberman@localhost% sudo chef-apply nofaces.rb
Recipe: (chef-apply cookbook)::(chef-apply recipe)
* execute[defaults write com.apple.iPhoto PKFaceDetectionEnabled 0] action run
- execute defaults write com.apple.iPhoto PKFaceDetectionEnabled 0
jtimberman@localhost% defaults read com.apple.iPhoto PKFaceDetectionEnabled
0

Those who have read the workstation management posts on this blog in the past may be aware that I have a cookbook that can manage OS X “defaults(1)” settings. I plan to make updates to the resource in that cookbook that will leverage this method.