diff options
author | Chris Doherty <cdoherty@getchef.com> | 2014-10-21 14:09:04 -0700 |
---|---|---|
committer | Chris Doherty <cdoherty@getchef.com> | 2014-12-19 16:24:03 -0800 |
commit | be28ba90bde1fe0240f313151e3d564ce57e8b29 (patch) | |
tree | ae83d0079f55c07452dba11982d1e73f76638368 /lib | |
parent | 3569079f9831b5e14e8e46b51840f3b6a069d9eb (diff) | |
download | chef-be28ba90bde1fe0240f313151e3d564ce57e8b29.tar.gz |
Enable Windows services to run as a different user (CHEF-4921). This adds :run_as_user andcdoherty-enhance-win-service
:run_as_password attributes to the windows_service resource. If a logon user is specified,
the resource will (on every run) grant the logon-as-service privilege to that user, using
secedit.exe.
Diffstat (limited to 'lib')
-rw-r--r-- | lib/chef/application/windows_service_manager.rb | 10 | ||||
-rw-r--r-- | lib/chef/provider/service.rb | 2 | ||||
-rw-r--r-- | lib/chef/provider/service/windows.rb | 100 | ||||
-rw-r--r-- | lib/chef/resource/windows_service.rb | 18 |
4 files changed, 126 insertions, 4 deletions
diff --git a/lib/chef/application/windows_service_manager.rb b/lib/chef/application/windows_service_manager.rb index 30810c51f2..de8ed657c2 100644 --- a/lib/chef/application/windows_service_manager.rb +++ b/lib/chef/application/windows_service_manager.rb @@ -16,7 +16,9 @@ # limitations under the License. # -require 'win32/service' +if RUBY_PLATFORM =~ /mswin|mingw32|windows/ + require 'win32/service' +end require 'chef/config' require 'mixlib/cli' @@ -88,6 +90,8 @@ class Chef @service_display_name = service_options[:service_display_name] @service_description = service_options[:service_description] @service_file_path = service_options[:service_file_path] + @service_start_name = service_options[:run_as_user] + @password = service_options[:run_as_password] end def run(params = ARGV) @@ -116,7 +120,9 @@ class Chef # and we don't want that, so we need to override the service type. :service_type => ::Win32::Service::SERVICE_WIN32_OWN_PROCESS, :start_type => ::Win32::Service::SERVICE_AUTO_START, - :binary_path_name => cmd + :binary_path_name => cmd, + :service_start_name => @service_start_name, + :password => @password, ) puts "Service '#{@service_name}' has successfully been installed." end diff --git a/lib/chef/provider/service.rb b/lib/chef/provider/service.rb index 968f9bff9c..75da2ddb31 100644 --- a/lib/chef/provider/service.rb +++ b/lib/chef/provider/service.rb @@ -150,7 +150,7 @@ class Chef end def reload_service - raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :restart" + raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :reload" end protected diff --git a/lib/chef/provider/service/windows.rb b/lib/chef/provider/service/windows.rb index d4c272354e..ba53f0a3c3 100644 --- a/lib/chef/provider/service/windows.rb +++ b/lib/chef/provider/service/windows.rb @@ -20,6 +20,7 @@ require 'chef/provider/service/simple' if RUBY_PLATFORM =~ /mswin|mingw32|windows/ + require 'chef/win32/error' require 'win32/service' end @@ -29,6 +30,7 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service provides :windows_service, os: "windows" include Chef::Mixin::ShellOut + include Chef::ReservedNames::Win32::API::Error rescue LoadError #Win32::Service.get_start_type AUTO_START = 'auto start' @@ -67,6 +69,22 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service def start_service if Win32::Service.exists?(@new_resource.service_name) + # reconfiguration is idempotent, so just do it. + new_config = { + service_name: @new_resource.service_name, + service_start_name: @new_resource.run_as_user, + password: @new_resource.run_as_password, + }.reject { |k,v| v.nil? || v.length == 0 } + + Win32::Service.configure(new_config) + Chef::Log.info "#{@new_resource} configured with #{new_config.inspect}" + + # it would be nice to check if the user already has the logon privilege, but that turns out to be + # nontrivial. + if new_config.has_key?(:service_start_name) + grant_service_logon(new_config[:service_start_name]) + end + state = current_state if state == RUNNING Chef::Log.debug "#{@new_resource} already started - nothing to do" @@ -79,7 +97,17 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service shell_out!(@new_resource.start_command) else spawn_command_thread do - Win32::Service.start(@new_resource.service_name) + begin + Win32::Service.start(@new_resource.service_name) + rescue SystemCallError => ex + if ex.errno == ERROR_SERVICE_LOGON_FAILED + Chef::Log.error ex.message + raise Chef::Exceptions::Service, + "Service #{@new_resource} did not start due to a logon failure (error #{ERROR_SERVICE_LOGON_FAILED}): possibly the specified user '#{@new_resource.run_as_user}' does not have the 'log on as a service' privilege, or the password is incorrect." + else + raise ex + end + end end wait_for_state(RUNNING) end @@ -209,6 +237,76 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service end private + def make_policy_text(username) + text = <<-EOS +[Unicode] +Unicode=yes +[Privilege Rights] +SeServiceLogonRight = \\\\#{canonicalize_username(username)},*S-1-5-80-0 +[Version] +signature="$CHICAGO$" +Revision=1 +EOS + end + + def grant_logfile_name(username) + Chef::Util::PathHelper.canonical_path("#{Dir.tmpdir}/logon_grant-#{clean_username_for_path(username)}-#{$$}.log", prefix=false) + end + + def grant_policyfile_name(username) + Chef::Util::PathHelper.canonical_path("#{Dir.tmpdir}/service_logon_policy-#{clean_username_for_path(username)}-#{$$}.inf", prefix=false) + end + + def grant_dbfile_name(username) + "#{ENV['TEMP']}\\secedit.sdb" + end + + def grant_service_logon(username) + logfile = grant_logfile_name(username) + policy_file = ::File.new(grant_policyfile_name(username), 'w') + policy_text = make_policy_text(username) + dbfile = grant_dbfile_name(username) # this is just an audit file. + + begin + Chef::Log.debug "Policy file text:\n#{policy_text}" + policy_file.puts(policy_text) + policy_file.close # need to flush the buffer. + + # it would be nice to do this with APIs instead, but the LSA_* APIs are + # particularly onerous and life is short. + cmd = %Q{secedit.exe /configure /db "#{dbfile}" /cfg "#{policy_file.path}" /areas USER_RIGHTS SECURITYPOLICY SERVICES /log "#{logfile}"} + Chef::Log.debug "Granting logon-as-service privilege with: #{cmd}" + runner = shell_out(cmd) + + if runner.exitstatus != 0 + Chef::Log.fatal "Logon-as-service grant failed with output: #{runner.stdout}" + raise Chef::Exceptions::Service, <<-EOS +Logon-as-service grant failed with policy file #{policy_file.path}. +You can look at #{logfile} for details, or do `secedit /analyze #{dbfile}`. +The failed command was `#{cmd}`. +EOS + end + + Chef::Log.info "Grant logon-as-service to user '#{username}' successful." + + ::File.delete(dbfile) rescue nil + ::File.delete(policy_file) + ::File.delete(logfile) rescue nil # logfile is not always present at end. + end + true + end + + # remove characters that make for broken or wonky filenames. + def clean_username_for_path(username) + username.gsub(/[\/\\. ]+/, '_') + end + + # the security policy file only seems to accept \\username, so fix .\username or .\\username. + # TODO: this probably has to be fixed to handle various valid Windows names correctly. + def canonicalize_username(username) + username.sub(/^\.?\\+/, '') + end + def current_state Win32::Service.status(@new_resource.service_name).current_state end diff --git a/lib/chef/resource/windows_service.rb b/lib/chef/resource/windows_service.rb index 2aec4d6304..8090adceb0 100644 --- a/lib/chef/resource/windows_service.rb +++ b/lib/chef/resource/windows_service.rb @@ -37,6 +37,8 @@ class Chef @resource_name = :windows_service @allowed_actions.push(:configure_startup) @startup_type = :automatic + @run_as_user = "" + @run_as_password = "" end def startup_type(arg=nil) @@ -48,6 +50,22 @@ class Chef :equal_to => [ :automatic, :manual, :disabled ] ) end + + def run_as_user(arg=nil) + set_or_return( + :run_as_user, + arg, + :kind_of => [ String ] + ) + end + + def run_as_password(arg=nil) + set_or_return( + :run_as_password, + arg, + :kind_of => [ String ] + ) + end end end end |