Files
munki/code/client/authrestartd
David Aguilar 3bb91cabca munki: rename "/usr/local/munki/python" symlink to "munki-python" (#997)
Avoid masking the system /usr/bin/python by calling our symlink
"munki-python" instead of "python".

Closes #996
2020-07-07 13:58:34 -07:00

444 lines
16 KiB
Plaintext
Executable File

#!/usr/local/munki/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())