mirror of
https://github.com/munki/munki.git
synced 2025-12-30 11:09:57 -06:00
411 lines
15 KiB
Python
Executable File
411 lines
15 KiB
Python
Executable File
#!/usr/bin/python
|
|
# encoding: utf-8
|
|
#
|
|
# Copyright 2017-2018 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
|
|
#
|
|
# https://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.
|
|
"""
|
|
authrestartd
|
|
|
|
Created by Greg Neagle on 2017-04-15.
|
|
|
|
Much code based on and adapted from autopkgserver by Per Olofsson
|
|
|
|
A helper tool for FileVault authorized restarts. Allows non-privileged users
|
|
to get certain info about FileVault, and to store a password for authrestart.
|
|
Root user can trigger an authrestart, possibly using the password stored
|
|
earlier by a non-privileged user.
|
|
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import time
|
|
import stat
|
|
import logging
|
|
import logging.handlers
|
|
import SocketServer
|
|
import socket
|
|
import plistlib
|
|
import struct
|
|
|
|
from munkilib import authrestart
|
|
from munkilib import launchd
|
|
from munkilib import prefs
|
|
|
|
|
|
APPNAME = 'authrestartd'
|
|
VERSION = '0.2'
|
|
|
|
|
|
class FDEUtilError(Exception):
|
|
'''Exception to raise if there is any error in FDEUtil'''
|
|
pass
|
|
|
|
|
|
class FDEUtil(object):
|
|
'''Class for working with fdesetup'''
|
|
def __init__(self, server, request, uid):
|
|
'''Arguments:
|
|
request A request in plist format.
|
|
uid The uid of the requestor
|
|
'''
|
|
self.server = server
|
|
self.log = server.log
|
|
self.request = request
|
|
self.uid = uid
|
|
|
|
def verify_request(self):
|
|
'''Make sure copy request has everything we need'''
|
|
self.log.debug('Verifying request')
|
|
for key in ['task']:
|
|
if not key in self.request:
|
|
raise FDEUtilError('No %s in request' % key)
|
|
|
|
def store_password(self, password, username=None):
|
|
'''Stores a password for later use for authrestart'''
|
|
self.server.stored_password = password
|
|
if username:
|
|
self.server.stored_username = username
|
|
|
|
def handle(self):
|
|
'''Handle our request'''
|
|
self.verify_request()
|
|
|
|
if self.request['task'] == 'restart':
|
|
# Attempt to perform an authrestart, falling back to a regular
|
|
# restart
|
|
self.log.info('Restart request from uid %s', self.uid)
|
|
if self.uid == 0:
|
|
self.log.info('Stored username for authrestart: %s',
|
|
self.server.stored_username)
|
|
authrestart.do_authorized_or_normal_restart(
|
|
password=self.server.stored_password,
|
|
username=self.server.stored_username)
|
|
return 'RESTARTING'
|
|
else:
|
|
self.log.info('Restart request denied')
|
|
raise FDEUtilError('Restart may only be triggered by root')
|
|
|
|
if self.request['task'] == 'store_password':
|
|
# store a password for later fdesetup authrestart
|
|
self.log.info('Store password request')
|
|
password = self.request.get('password')
|
|
if not password:
|
|
self.log.info('No password in request')
|
|
raise FDEUtilError('No password provided')
|
|
username = self.request.get('username')
|
|
self.log.info('Request username: %s', username)
|
|
# don't store the password if the user isn't enabled for FileVault
|
|
if (username and
|
|
not authrestart.can_attempt_auth_restart_for(username)):
|
|
self.log.info('User %s can\'t do auth restart', username)
|
|
raise FDEUtilError(
|
|
'User %s can\'t do FileVault authrestart' % username)
|
|
self.store_password(password, username=username)
|
|
self.log.info('Password stored.')
|
|
return 'DONE'
|
|
|
|
if self.request['task'] == 'verify_can_attempt_auth_restart':
|
|
# Check if we have all the required bits to attempt an auth
|
|
# restart.
|
|
self.log.info('Verify ready for auth restart')
|
|
if authrestart.can_attempt_auth_restart(
|
|
have_password=bool(self.server.stored_password)):
|
|
self.log.info('Ready for auth restart attempt')
|
|
return 'READY'
|
|
else:
|
|
self.log.info('Not ready for auth restart attempt')
|
|
raise FDEUtilError('Not ready for auth restart attempt')
|
|
|
|
if self.request['task'] == 'verify_recovery_key_present':
|
|
# Check if a plist containing a recovery key or password is
|
|
# present.
|
|
self.log.info('Verify recovery key request')
|
|
if authrestart.get_auth_restart_key(quiet=True) == '':
|
|
self.log.info('No valid recovery key plist')
|
|
raise FDEUtilError('No recovery key plist')
|
|
self.log.info('Valid recovery key plist found')
|
|
return 'PRESENT'
|
|
|
|
if self.request['task'] == 'verify_user':
|
|
# Check to see if we can perform an authrestart for this user.
|
|
# FileVault must be active, the hardware must support authrestart,
|
|
# and the user must be enabled for FileVault.
|
|
username = self.request.get('username')
|
|
if not username:
|
|
self.log.info('Verify user request with no username')
|
|
raise FDEUtilError('No username provided')
|
|
self.log.info('Verify user request for %s', username)
|
|
if not authrestart.can_attempt_auth_restart_for(username):
|
|
self.log.info(
|
|
'User %s can\'t do FileVault authrestart' % username)
|
|
raise FDEUtilError('User not authorized for FileVault')
|
|
self.log.info('User %s ok for auth restart', username)
|
|
return 'USER VERIFIED'
|
|
|
|
if self.request['task'] == 'verify_filevault':
|
|
# check if FileVault is active
|
|
self.log.info('Verify FileVault request')
|
|
if not authrestart.filevault_is_active():
|
|
self.log.info('FileVault is not active.')
|
|
raise FDEUtilError('FileVault is not active')
|
|
self.log.info('FileVault is active.')
|
|
return 'FILEVAULT ON'
|
|
|
|
# the task is not one we know how to handle
|
|
self.log.info('Unknown task request: %s', self.request['task'])
|
|
raise FDEUtilError('Unknown task')
|
|
|
|
|
|
class RunHandler(SocketServer.StreamRequestHandler):
|
|
'''Handler for restarthelper run requests'''
|
|
|
|
def verify_request_syntax(self, plist):
|
|
'''Verify the basic syntax of request plist.'''
|
|
# pylint: disable=no-self-use
|
|
# Keep a list of error messages.
|
|
errors = list()
|
|
|
|
# Root should be a dictionary.
|
|
if not isinstance(plist, dict):
|
|
errors.append('Request root is not a dictionary')
|
|
# Bail out early if it's not.
|
|
return False, errors
|
|
|
|
syntax_ok = True
|
|
# TO-DO: Actual verification!
|
|
return syntax_ok, errors
|
|
|
|
def getpeerid(self):
|
|
'''
|
|
Get peer credentials on a UNIX domain socket.
|
|
Returns uid, gids.
|
|
'''
|
|
|
|
# /usr/include/sys/ucred.h
|
|
#
|
|
# struct xucred {
|
|
# u_int cr_version; /* structure layout version */
|
|
# uid_t cr_uid; /* effective user id */
|
|
# short cr_ngroups; /* number of advisory groups */
|
|
# gid_t cr_groups[NGROUPS]; /* advisory group list */
|
|
# };
|
|
|
|
# pylint: disable=invalid-name
|
|
LOCAL_PEERCRED = 0x001
|
|
XUCRED_VERSION = 0
|
|
NGROUPS = 16
|
|
# pylint: enable=invalid-name
|
|
cr_version = 0
|
|
cr_uid = 1
|
|
cr_ngroups = 2
|
|
cr_groups = 3
|
|
|
|
xucred_fmt = 'IIh%dI' % NGROUPS
|
|
res = struct.unpack(
|
|
xucred_fmt,
|
|
self.request.getsockopt(
|
|
0, LOCAL_PEERCRED, struct.calcsize(xucred_fmt)))
|
|
|
|
if res[cr_version] != XUCRED_VERSION:
|
|
raise OSError('Incompatible struct xucred version')
|
|
|
|
return res[cr_uid], res[cr_groups:cr_groups + res[cr_ngroups]]
|
|
|
|
def handle(self):
|
|
'''Handle an incoming run request.'''
|
|
|
|
try:
|
|
# Log through server parent.
|
|
self.log = self.server.log
|
|
self.log.debug('Handling request')
|
|
|
|
# Get uid and primary gid of connecting peer.
|
|
uid, gids = self.getpeerid()
|
|
gid = gids[0]
|
|
self.log.debug(
|
|
'Got request from uid %d gid %d' % (uid, gid))
|
|
|
|
# Receive a plist.
|
|
plist_string = self.request.recv(8192)
|
|
|
|
# Try to parse it.
|
|
try:
|
|
plist = plistlib.readPlistFromString(plist_string)
|
|
except BaseException as err:
|
|
self.log.error('Malformed request')
|
|
self.request.send('ERROR:Malformed request\n')
|
|
return
|
|
self.log.debug('Parsed request plist')
|
|
|
|
# Verify the plist syntax.
|
|
syntax_ok, errors = self.verify_request_syntax(plist)
|
|
if not syntax_ok:
|
|
self.log.error('Plist syntax error')
|
|
self.request.send(''.join(['ERROR:%s\n' % e for e in errors]))
|
|
return
|
|
|
|
self.log.info(
|
|
'Dispatching worker to process request for user %d' % (uid))
|
|
try:
|
|
fdeutil = FDEUtil(self.server, plist, uid)
|
|
result = fdeutil.handle()
|
|
self.request.send(u'OK:%s\n' % result)
|
|
except FDEUtilError as err:
|
|
self.request.send(u'ERROR:%s\n' % unicode(err))
|
|
except BaseException as err:
|
|
self.log.error('Run failed: %s' % unicode(err))
|
|
self.request.send(u'ERROR:%s\n' % unicode(err))
|
|
|
|
except BaseException as err:
|
|
self.log.error('Caught exception: %s' % repr(err))
|
|
self.request.send('ERROR:Caught exception: %s' % repr(err))
|
|
return
|
|
|
|
|
|
class RestartHelperDaemonError(Exception):
|
|
'''Exception to raise for RestartHelperDaemon errors'''
|
|
pass
|
|
|
|
|
|
class RestartHelperDaemon(SocketServer.UnixStreamServer):
|
|
'''Daemon that runs as root,
|
|
receiving requests for fdesetup tasks including authrestart.'''
|
|
|
|
allow_reuse_address = True
|
|
request_queue_size = 10
|
|
timeout = 10
|
|
|
|
def __init__(self, socket_fd, RequestHandlerClass):
|
|
# Avoid initialization of UnixStreamServer as we need to open the
|
|
# socket from a file descriptor instead of creating our own.
|
|
self.socket = socket.fromfd(
|
|
socket_fd, socket.AF_UNIX, socket.SOCK_STREAM)
|
|
self.socket.listen(self.request_queue_size)
|
|
SocketServer.BaseServer.__init__(
|
|
self, self.socket.getsockname(), RequestHandlerClass)
|
|
self.timed_out = False
|
|
self.stored_password = None
|
|
self.stored_username = None
|
|
self.log = logging.getLogger(APPNAME)
|
|
|
|
def setup_logging(self):
|
|
'''Configure logging'''
|
|
try:
|
|
self.log.setLevel(logging.DEBUG)
|
|
|
|
log_console = logging.StreamHandler()
|
|
log_console.setLevel(logging.DEBUG)
|
|
# store the log in the same directory as ManagedSoftwareUpdate.log
|
|
logfilepath = os.path.join(
|
|
os.path.dirname(prefs.pref('LogFile')), APPNAME + '.log')
|
|
log_file = logging.handlers.RotatingFileHandler(
|
|
logfilepath, 'a', 100000, 9, 'utf-8')
|
|
log_file.setLevel(logging.DEBUG)
|
|
|
|
console_formatter = logging.Formatter('%(message)s')
|
|
file_formatter = logging.Formatter(
|
|
'%(asctime)s %(module)s[%(process)d]: '
|
|
'%(message)s (%(funcName)s)')
|
|
|
|
log_console.setFormatter(console_formatter)
|
|
log_file.setFormatter(file_formatter)
|
|
|
|
self.log.addHandler(log_console)
|
|
self.log.addHandler(log_file)
|
|
except (OSError, IOError) as err:
|
|
raise RestartHelperDaemonError(
|
|
'Can\'t open log: %s' % (err.strerror))
|
|
|
|
def handle_timeout(self):
|
|
self.timed_out = True
|
|
|
|
|
|
def main():
|
|
'''Start our daemon, connect to socket and process requests'''
|
|
# Make sure we're launched as root
|
|
if os.geteuid() != 0:
|
|
print >>sys.stderr, '%s must be run as root.' % APPNAME
|
|
# Sleep to avoid respawn.
|
|
time.sleep(10)
|
|
return 1
|
|
|
|
# Make sure that the executable and all containing directories are owned
|
|
# by root:wheel or root:admin, and not writeable by other users.
|
|
root_uid = 0
|
|
wheel_gid = 0
|
|
admin_gid = 80
|
|
|
|
exepath = os.path.realpath(os.path.abspath(__file__))
|
|
path_ok = True
|
|
while True:
|
|
info = os.stat(exepath)
|
|
if info.st_uid != root_uid:
|
|
print >> sys.stderr, '%s must be owned by root.' % exepath
|
|
path_ok = False
|
|
if info.st_gid not in (wheel_gid, admin_gid):
|
|
print >> sys.stderr, '%s must have group wheel or admin.' % exepath
|
|
path_ok = False
|
|
if info.st_mode & stat.S_IWOTH:
|
|
print >> sys.stderr, '%s mustn\'t be world writeable.' % exepath
|
|
path_ok = False
|
|
exepath = os.path.dirname(exepath)
|
|
if exepath == '/':
|
|
break
|
|
|
|
if not path_ok:
|
|
# Sleep to avoid immediate respawn.
|
|
time.sleep(10)
|
|
return 1
|
|
|
|
# Keep track of time for launchd.
|
|
start_time = time.time()
|
|
|
|
# Get socket file descriptors from launchd.
|
|
socket_fd = launchd.get_socket_fd('authrestartd')
|
|
if not socket_fd:
|
|
print >> sys.stderr, 'No socket provided to us by launchd'
|
|
time.sleep(10)
|
|
return 1
|
|
|
|
# Create the daemon object.
|
|
daemon = RestartHelperDaemon(socket_fd, RunHandler)
|
|
daemon.setup_logging()
|
|
|
|
daemon.log.info('%s v%s starting', APPNAME, VERSION)
|
|
|
|
# Process incoming requests
|
|
while True:
|
|
daemon.handle_request()
|
|
if not daemon.timed_out or daemon.stored_password:
|
|
continue
|
|
# we timed out and we don't have a stored password, so we can exit
|
|
# Keep running for at least 10 seconds make launchd happy.
|
|
run_time = time.time() - start_time
|
|
daemon.log.info("run time: %fs", run_time)
|
|
if run_time < 10.0:
|
|
# Only sleep for a short while in case new requests pop up.
|
|
sleep_time = min(1.0, 10.0 - run_time)
|
|
daemon.log.debug(
|
|
"sleeping for %f seconds to make launchd happy",
|
|
sleep_time)
|
|
time.sleep(sleep_time)
|
|
else:
|
|
daemon.log.info('Nothing to do: exiting')
|
|
break
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|