mirror of
https://github.com/munki/munki.git
synced 2026-01-02 04:30:11 -06:00
This change is still a good future goal, but is causing problems that are too difficult to work around right now and is delaying the vital release of Munki 5.1 for Big Sur compatibility.
This reverts commit 3bb91cabca.
444 lines
16 KiB
Python
Executable File
444 lines
16 KiB
Python
Executable File
#!/usr/local/munki/python
|
|
# encoding: utf-8
|
|
#
|
|
# Copyright 2017-2020 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.
|
|
|
|
"""
|
|
from __future__ import absolute_import, print_function
|
|
|
|
import os
|
|
import sys
|
|
import time
|
|
import stat
|
|
import logging
|
|
import logging.handlers
|
|
try:
|
|
import SocketServer
|
|
except ImportError:
|
|
import socketserver as SocketServer
|
|
import socket
|
|
import struct
|
|
|
|
from munkilib import authrestart
|
|
from munkilib import launchd
|
|
from munkilib import prefs
|
|
from munkilib.wrappers import readPlistFromString, PlistError, unicode_or_str
|
|
|
|
|
|
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'] == 'delayed_authrestart':
|
|
# set up a delayed authrestart. Defaults to waiting indefinitely,
|
|
# so the next "normal" restart becomes an authrestart.
|
|
self.log.info('Delayed restart request from uid %s', self.uid)
|
|
if self.uid == 0:
|
|
self.log.info('Stored username for authrestart: %s',
|
|
self.server.stored_username)
|
|
delayminutes = self.request.get('delayminutes', -1)
|
|
authrestart.perform_auth_restart(
|
|
password=self.server.stored_password,
|
|
username=self.server.stored_username,
|
|
delayminutes=delayminutes)
|
|
return 'DONE'
|
|
else:
|
|
self.log.info('Delayed restart request denied')
|
|
raise FDEUtilError(
|
|
'Delayed 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 = b'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 = readPlistFromString(plist_string)
|
|
except PlistError as err:
|
|
self.log.error('Malformed request')
|
|
self.request.send(u'ERROR:Malformed request\n'.encode('UTF-8'))
|
|
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')
|
|
msg = (''.join(
|
|
['ERROR:%s\n' % e for e in errors])).encode('UTF-8')
|
|
self.request.send(msg)
|
|
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' % unicode_or_str(result)).encode('UTF-8'))
|
|
except FDEUtilError as err:
|
|
self.request.send(
|
|
(u'ERROR:%s\n' % unicode_or_str(err)).encode('UTF-8'))
|
|
except Exception as err:
|
|
raise
|
|
#self.log.error(u'Run failed: %s' % unicode_or_str(err))
|
|
#self.request.send(
|
|
# (u'ERROR:%s\n' % unicode_or_str(err)).encode('UTF-8'))
|
|
|
|
except Exception as err:
|
|
self.log.error('Caught exception: %s' % repr(err))
|
|
self.request.send(
|
|
(u'ERROR:Caught exception: %s' % repr(err)).encode('UTF-8'))
|
|
raise
|
|
|
|
|
|
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.
|
|
# pylint: disable=super-init-not-called
|
|
self.socket = socket.fromfd(
|
|
socket_fd, socket.AF_UNIX, socket.SOCK_STREAM)
|
|
self.socket.listen(self.request_queue_size)
|
|
# now do the base class's init
|
|
# pylint: disable=non-parent-init-called
|
|
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('%s must be run as root.' % APPNAME, file=sys.stderr)
|
|
# 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('%s must be owned by root.' % exepath, file=sys.stderr)
|
|
path_ok = False
|
|
if info.st_gid not in (wheel_gid, admin_gid):
|
|
print('%s must have group wheel or admin.' % exepath,
|
|
file=sys.stderr)
|
|
path_ok = False
|
|
if info.st_mode & stat.S_IWOTH:
|
|
print('%s mustn\'t be world writeable.' % exepath, file=sys.stderr)
|
|
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'.encode("UTF-8"))
|
|
if not socket_fd:
|
|
print('No socket provided to us by launchd', file=sys.stderr)
|
|
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())
|