Skip to content

Commit

Permalink
Merge pull request chef#2288 from opscode/cdoherty-enhance-win-service
Browse files Browse the repository at this point in the history
Enable logon-as-service in windows_service (CHEF-4921).
  • Loading branch information
randomcamel committed Dec 22, 2014
2 parents 399674d + be28ba9 commit 8b37ebb
Show file tree
Hide file tree
Showing 9 changed files with 383 additions and 64 deletions.
10 changes: 8 additions & 2 deletions lib/chef/application/windows_service_manager.rb
Expand Up @@ -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'

Expand Down Expand Up @@ -88,6 +90,8 @@ def initialize(service_options)
@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)
Expand Down Expand Up @@ -116,7 +120,9 @@ def run(params = ARGV)
# 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
Expand Down
2 changes: 1 addition & 1 deletion lib/chef/provider/service.rb
Expand Up @@ -150,7 +150,7 @@ def restart_service
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
Expand Down
100 changes: 99 additions & 1 deletion lib/chef/provider/service/windows.rb
Expand Up @@ -20,6 +20,7 @@

require 'chef/provider/service/simple'
if RUBY_PLATFORM =~ /mswin|mingw32|windows/
require 'chef/win32/error'
require 'win32/service'
end

Expand All @@ -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'
Expand Down Expand Up @@ -67,6 +69,22 @@ def load_current_resource

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"
Expand All @@ -79,7 +97,17 @@ def start_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
Expand Down Expand Up @@ -209,6 +237,76 @@ def action_configure_startup
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
Expand Down
18 changes: 18 additions & 0 deletions lib/chef/resource/windows_service.rb
Expand Up @@ -37,6 +37,8 @@ def initialize(name, run_context=nil)
@resource_name = :windows_service
@allowed_actions.push(:configure_startup)
@startup_type = :automatic
@run_as_user = ""
@run_as_password = ""
end

def startup_type(arg=nil)
Expand All @@ -48,6 +50,22 @@ def startup_type(arg=nil)
: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
97 changes: 97 additions & 0 deletions spec/functional/resource/windows_service_spec.rb
@@ -0,0 +1,97 @@
#
# Author:: Chris Doherty (<cdoherty@chef.io>)
# Copyright:: Copyright (c) 2014 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::Resource::WindowsService, :windows_only, :system_windows_service_gem_only do

include_context "using Win32::Service"

let(:username) { "service_spec_user"}
let(:qualified_username) { ".\\#{username}"}
let(:password) { "1a2b3c4X!&narf"}

let(:user_resource) {
r = Chef::Resource::User.new(username, run_context)
r.username(username)
r.password(password)
r.comment("temp spec user")
r
}

let(:global_service_file_path) {
"#{ENV['WINDIR']}\\temp\\#{File.basename(test_service[:service_file_path])}"
}

let(:service_params) {
test_service.merge( {
run_as_user: qualified_username,
run_as_password: password,
service_name: "spec_service_#{$$}_#{rand(1000)}",
service_display_name: "windows_service test service",
service_description: "Test service for running the windows_service functional spec.",
service_file_path: global_service_file_path,
} )
}

let(:manager) {
Chef::Application::WindowsServiceManager.new(service_params)
}

let(:service_resource) {
r = Chef::Resource::WindowsService.new(service_params[:service_name], run_context)
[:run_as_user, :run_as_password].each { |prop| r.send(prop, service_params[prop]) }
r
}

before {
user_resource.run_action(:create)

# the service executable has to be outside the current user's home
# directory in order for the logon user to execute it.
FileUtils::copy_file(test_service[:service_file_path], global_service_file_path)

# if you don't make the file executable by the service user, you'll get
# the not-very-helpful "service did not respond fast enough" error.

# #mode may break in a post-Windows 8.1 release, and have to be replaced
# with the rights stuff in the file resource.
file = Chef::Resource::File.new(global_service_file_path, run_context)
file.mode("0777")

file.run_action(:create)

manager.run(%w{--action install})
}

after {
user_resource.run_action(:remove)
manager.run(%w{--action uninstall})
File.delete(global_service_file_path)
}

describe "logon as a service" do
it "successfully runs a service as another user" do
service_resource.run_action(:start)
end

it "raises an exception when it can't grant the logon privilege" do
# service_resource.run_action(:start)
end
end
end
60 changes: 3 additions & 57 deletions spec/functional/win32/service_manager_spec.rb
Expand Up @@ -24,7 +24,7 @@
#
# ATTENTION:
# This test creates a windows service for testing purposes and runs it
# as Local System on windows boxes.
# as Local System (or an otherwise specified user) on windows boxes.
# This test will fail if you run the tests inside a Windows VM by
# sharing the code from your host since Local System account by
# default can't see the mounted partitions.
Expand All @@ -35,61 +35,7 @@

describe "Chef::Application::WindowsServiceManager", :windows_only, :system_windows_service_gem_only do

# Some helper methods.

def test_service_exists?
::Win32::Service.exists?("spec-service")
end

def test_service_state
::Win32::Service.status("spec-service").current_state
end

def service_manager
Chef::Application::WindowsServiceManager.new(test_service)
end

def cleanup
# Uninstall if the test service is installed.
if test_service_exists?

# We can only uninstall when the service is stopped.
if test_service_state != "stopped"
::Win32::Service.send("stop", "spec-service")
while test_service_state != "stopped"
sleep 1
end
end

::Win32::Service.delete("spec-service")
end

# Delete the test_service_file if it exists
if File.exists?(test_service_file)
File.delete(test_service_file)
end

end


# Definition for the test-service

let(:test_service) {
{
:service_name => "spec-service",
:service_display_name => "Spec Test Service",
:service_description => "Service for testing Chef::Application::WindowsServiceManager.",
:service_file_path => File.expand_path(File.join(File.dirname(__FILE__), '../../support/platforms/win32/spec_service.rb'))
}
}

# Test service creates a file for us to verify that it is running.
# Since our test service is running as Local System we should look
# for the file it creates under SYSTEM temp directory

let(:test_service_file) {
"#{ENV['SystemDrive']}\\windows\\temp\\spec_service_file"
}
include_context "using Win32::Service"

context "with invalid service definition" do
it "throws an error when initialized with no service definition" do
Expand Down Expand Up @@ -190,7 +136,7 @@ def cleanup

["pause", "resume"].each do |action|
it "#{action} => should raise error" do
expect {service_manager.run(["-a", action])}.to raise_error(::Win32::Service::Error)
expect { service_manager.run(["-a", action]) }.to raise_error(SystemCallError)
end
end

Expand Down

0 comments on commit 8b37ebb

Please sign in to comment.