Better implementation of r863; move verifyFileOnlyWritableByMunkiAndRoot() and runExternalScript() to new munkilib utils module that is 100% free of ObjC-dependant Python.

git-svn-id: http://munki.googlecode.com/svn/trunk@865 a4e17f2e-e282-11dd-95e1-755cbddbdd66
This commit is contained in:
Justin McWilliams
2010-10-26 16:32:48 +00:00
parent 963c5c319b
commit 7afec5b605
3 changed files with 174 additions and 126 deletions

View File

@@ -30,38 +30,27 @@ import traceback
# Do not place any imports with ObjC bindings above this!
try:
from Foundation import NSDate
from munkilib import munkicommon
from munkilib import updatecheck
from munkilib import installer
from munkilib import munkistatus
from munkilib import appleupdates
from munkilib import FoundationPlist
OBJ_C_OK = True
except:
# this version of Python is missing the ObjC bindings.
class munkicommon: # mock munkicommon so runExternalScript can run.
@classmethod
def display_warning(cls, msg):
print msg
@classmethod
def display_info(cls, msg):
print msg
@classmethod
def log(cls, msg):
print msg
OBJ_C_OK = False
# Python is missing ObjC bindings. Run external report script.
from munkilib import utils
print >> sys.stderr, 'Python is missing ObjC bindings.'
scriptdir = os.path.realpath(os.path.dirname(sys.argv[0]))
script = os.path.join(scriptdir, 'report_broken_client')
try:
utils.runExternalScript(script)
except utils.ScriptNotFoundError:
pass # script is not required, so pass
except utils.RunExternalScriptError, e:
print >> sys.stderr, str(e)
sys.exit(1)
class Error(Exception):
"""Class for domain specific exceptions."""
class VerifyFilePermissionsError(Error):
"""There was an error verifying file permissions."""
class InsecureFilePermissionsError(VerifyFilePermissionsError):
"""The permissions of the specified file are insecure."""
from munkilib import munkicommon
from munkilib import updatecheck
from munkilib import installer
from munkilib import munkistatus
from munkilib import appleupdates
from munkilib import FoundationPlist
from munkilib import utils
def getIdleSeconds():
@@ -113,53 +102,13 @@ def clearLastNotifiedDate():
pass
def verifyFileOnlyWritableByMunkiAndRoot(file_path):
"""
Check the permissions on a given file path; fail if owner or group
does not match the munki process (default: root/admin) or the group is not
'wheel', or if other users are able to write to the file. This prevents
escalated execution of arbitrary code.
Args:
file_path: str path of file to verify permissions on.
Raises:
VerifyFilePermissionsError: there was an error verifying file permissions.
InsecureFilePermissionsError: file permissions were found to be insecure.
"""
try:
file_stat = os.stat(file_path)
except OSError, e:
raise VerifyFilePermissionsError(
'%s does not exist. \n %s' % (file_path, str(e)))
try:
admin_gid = grp.getgrnam('admin').gr_gid
wheel_gid = grp.getgrnam('wheel').gr_gid
user_gid = os.getegid()
# verify the munki process uid matches the file owner uid.
if os.geteuid() != file_stat.st_uid:
raise InsecureFilePermissionsError(
'owner does not match munki process!')
# verify the munki process gid matches the file owner gid, or the file
# owner gid is wheel or admin gid.
elif file_stat.st_gid not in [admin_gid, wheel_gid, user_gid]:
raise InsecureFilePermissionsError(
'group does not match munki process!')
# verify other users cannot write to the file.
elif file_stat.st_mode & stat.S_IWOTH != 0:
raise InsecureFilePermissionsError('world writable!')
except InsecureFilePermissionsError, e:
raise InsecureFilePermissionsError(
'%s is not secure! %s' % (file_path, e.args[0]))
def createDirsIfNeeded(dirlist):
"""Create any missing directories needed by the munki tools.
Args:
dirlist: a sequence of directories.
Returns:
Boolean. True if all directories existed or were created,
Boolean. True if all directories existed or were created,
False otherwise.
"""
for directory in dirlist:
@@ -345,16 +294,16 @@ def notifyUserOfUpdates():
# when there is no Managed Software Update process running
cmd = ['/usr/bin/killall', 'Managed Software Update']
proc = subprocess.Popen(cmd, shell=False, stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
(unused_output, unused_err) = proc.communicate()
# notify user of available updates using LaunchAgent to start
# Managed Software Update.app in the user context.
launchfile = '/var/run/com.googlecode.munki.ManagedSoftwareUpdate'
cmd = ['/usr/bin/touch', launchfile]
unused_retcode = subprocess.call(cmd)
time.sleep(0.1)
if os.path.exists(launchfile):
os.unlink(launchfile)
@@ -362,45 +311,6 @@ def notifyUserOfUpdates():
return user_was_notified
def runExternalScript(script, runtype='custom'):
"""Run an script (e.g. preflight/postflight) and return its exit status.
Args:
script: string path to the script to execute.
runtype: string mode managedsoftwareupdate was executed with.
For more info see
http://code.google.com/p/munki/wiki/PreflightAndPostflightScripts
Returns:
Integer exit status from script.
"""
if os.path.exists(script):
try:
verifyFileOnlyWritableByMunkiAndRoot(script)
except VerifyFilePermissionsError, e:
msg = ('Skipping execution due to failed file permissions '
'verification: %s\n%s' % (script, str(e)))
munkicommon.display_warning(msg)
return 0 # script does not get run
if os.access(script, os.X_OK):
munkicommon.log('Running %s with runtype: %s...' %
(script, runtype))
cmd = [script, runtype]
proc = subprocess.Popen(cmd, shell=False,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
(output, err) = proc.communicate()
if output:
munkicommon.display_info(output)
if err:
munkicommon.display_info(output)
return proc.returncode
else:
munkicommon.display_warning('%s not executable' % script)
return 0
def main():
"""Main"""
# check to see if we're root
@@ -411,13 +321,6 @@ def main():
# save this for later
scriptdir = os.path.realpath(os.path.dirname(sys.argv[0]))
# Do not place any ObjC or munkilib.* calls above this line.
if not OBJ_C_OK:
print >> sys.stderr, 'Python is missing ObjC bindings.'
script = os.path.join(scriptdir, 'report_broken_client')
runExternalScript(script)
sys.exit(1)
p = optparse.OptionParser()
p.set_usage("""Usage: %prog [options]""")
p.add_option('--auto', '-a', action='store_true',
@@ -548,7 +451,16 @@ def main():
# run the preflight script if it exists
preflightscript = os.path.join(scriptdir, 'preflight')
result = runExternalScript(preflightscript, runtype)
try:
result, output = utils.runExternalScript(preflightscript, runtype)
munkicommon.display_info(output)
except utils.ScriptNotFoundError:
result = 0
pass # script is not required, so pass
except utils.RunExternalScriptError, e:
result = 0
munkicommon.display_warning(msg)
if result:
# non-zero return code means don't run
munkicommon.display_info('managedsoftwareupdate run aborted by'
@@ -598,7 +510,7 @@ def main():
print >> sys.stderr, \
'Another instance of %s is running. Exiting.' % myname
exit(0)
applesoftwareupdatesonly = munkicommon.pref('AppleSoftwareUpdatesOnly')
if not options.installonly and not applesoftwareupdatesonly:
# check to see if we can talk to the manifest server
@@ -631,7 +543,7 @@ def main():
print 'Managed Software Update Tool'
print 'Copyright 2010 The Munki Project'
print 'http://code.google.com/p/munki\n'
if applesoftwareupdatesonly and options.verbose:
print ('NOTE: managedsoftwareupdate is configured to process Apple '
'Software Updates only.')
@@ -648,7 +560,7 @@ def main():
if updatecheckresult is not None:
recordUpdateCheckResult(updatecheckresult)
updatesavailable = munkiUpdatesAvailable()
if (not updatesavailable and not options.installonly and
not munkicommon.stopRequested()):
@@ -746,7 +658,13 @@ def main():
# run the postflight script if it exists
postflightscript = os.path.join(scriptdir, 'postflight')
result = runExternalScript(postflightscript, runtype)
try:
unused_r, output = utils.runExternalScript(postflightscript, runtype)
munkicommon.display_info(output)
except utils.ScriptNotFoundError:
pass # script is not required, so pass
except utils.RunExternalScriptError, e:
munkicommon.display_warning(msg)
# we ignore the result of the postflight
if munkicommon.tmpdir:

View File

@@ -1 +1,3 @@
# this is needed to make Python recognize the directory as a module package.
# this is needed to make Python recognize the directory as a module package.
#
# Warning: do NOT put any Python imports here that require ObjC.

View File

@@ -0,0 +1,128 @@
#!/usr/bin/python
# encoding: utf-8
#
# Copyright 2009-2010 Greg Neagle.
#
# 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.
"""
utils
Created by Justin McWilliams on 2010-10-26.
Common utility functions used throughout Munki.
Note: this module should be 100% free of ObjC-dependant Python imports.
"""
import grp
import os
import subprocess
import stat
class Error(Exception):
"""Class for domain specific exceptions."""
class RunExternalScriptError(Error):
"""There was an error running the script."""
class ScriptNotFoundError(RunExternalScriptError):
"""The script was not found at the given path."""
class VerifyFilePermissionsError(Error):
"""There was an error verifying file permissions."""
class InsecureFilePermissionsError(VerifyFilePermissionsError):
"""The permissions of the specified file are insecure."""
def verifyFileOnlyWritableByMunkiAndRoot(file_path):
"""
Check the permissions on a given file path; fail if owner or group
does not match the munki process (default: root/admin) or the group is not
'wheel', or if other users are able to write to the file. This prevents
escalated execution of arbitrary code.
Args:
file_path: str path of file to verify permissions on.
Raises:
VerifyFilePermissionsError: there was an error verifying file permissions.
InsecureFilePermissionsError: file permissions were found to be insecure.
"""
try:
file_stat = os.stat(file_path)
except OSError, e:
raise VerifyFilePermissionsError(
'%s does not exist. \n %s' % (file_path, str(e)))
try:
admin_gid = grp.getgrnam('admin').gr_gid
wheel_gid = grp.getgrnam('wheel').gr_gid
user_gid = os.getegid()
# verify the munki process uid matches the file owner uid.
if os.geteuid() != file_stat.st_uid:
raise InsecureFilePermissionsError(
'owner does not match munki process!')
# verify the munki process gid matches the file owner gid, or the file
# owner gid is wheel or admin gid.
elif file_stat.st_gid not in [admin_gid, wheel_gid, user_gid]:
raise InsecureFilePermissionsError(
'group does not match munki process!')
# verify other users cannot write to the file.
elif file_stat.st_mode & stat.S_IWOTH != 0:
raise InsecureFilePermissionsError('world writable!')
except InsecureFilePermissionsError, e:
raise InsecureFilePermissionsError(
'%s is not secure! %s' % (file_path, e.args[0]))
def runExternalScript(script, *args):
"""Run a script (e.g. preflight/postflight) and return its exit status.
Args:
script: string path to the script to execute.
args: args to pass to the script.
Returns:
Tuple. (integer exit status from script, str output).
Raises:
ScriptNotFoundError: the script was not found at the given path.
RunExternalScriptError: there was an error running the script.
"""
if not os.path.exists(script):
raise ScriptNotFoundError('script does not exist: %s' % script)
try:
verifyFileOnlyWritableByMunkiAndRoot(script)
except VerifyFilePermissionsError, e:
msg = ('Skipping execution due to failed file permissions '
'verification: %s\n%s' % (script, str(e)))
raise RunExternalScriptError(msg)
if os.access(script, os.X_OK):
cmd = [script]
if args:
cmd.extend(args)
proc = subprocess.Popen(cmd, shell=False,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
(output, err) = proc.communicate()
return proc.returncode, output
else:
raise RunExternalScriptError('%s not executable' % script)