OwlCyberSecurity - MANAGER
Edit File: config.py
#!/usr/bin/env python ## # Copyright (c) 2010-2017 Apple Inc. All rights reserved. # # 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. ## from __future__ import print_function # Suppress warning that occurs on Linux import sys if sys.platform.startswith("linux"): from Crypto.pct_warnings import PowmInsecureWarning import warnings warnings.simplefilter("ignore", PowmInsecureWarning) """ This tool gets and sets Calendar Server configuration keys """ from getopt import getopt, GetoptError from itertools import chain import os import plistlib import signal import subprocess import xml from plistlib import readPlistFromString, writePlistToString from twistedcaldav.config import config, ConfigDict, ConfigurationError, mergeData from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE from twisted.python.filepath import FilePath WRITABLE_CONFIG_KEYS = [ "AccountingCategories", "Authentication.Basic.AllowedOverWireUnencrypted", "Authentication.Basic.Enabled", "Authentication.Digest.AllowedOverWireUnencrypted", "Authentication.Digest.Enabled", "Authentication.Kerberos.AllowedOverWireUnencrypted", "Authentication.Kerberos.Enabled", "Authentication.Wiki.Enabled", "BehindTLSProxy", "DefaultLogLevel", "DirectoryAddressBook.params.queryPeopleRecords", "DirectoryAddressBook.params.queryUserRecords", "EnableCalDAV", "EnableCardDAV", "EnableSACLs", "EnableSearchAddressBook", "EnableSSL", "HTTPPort", "LogLevels", "Notifications.Services.APNS.Enabled", "RedirectHTTPToHTTPS", "Scheduling.iMIP.Enabled", "Scheduling.iMIP.Receiving.Port", "Scheduling.iMIP.Receiving.Server", "Scheduling.iMIP.Receiving.Type", "Scheduling.iMIP.Receiving.Username", "Scheduling.iMIP.Receiving.UseSSL", "Scheduling.iMIP.Sending.Address", "Scheduling.iMIP.Sending.Port", "Scheduling.iMIP.Sending.Server", "Scheduling.iMIP.Sending.Username", "Scheduling.iMIP.Sending.UseSSL", "ServerHostName", "SSLAuthorityChain", "SSLCertificate", "SSLPort", "SSLPrivateKey", "SSLKeychainIdentity", ] READONLY_CONFIG_KEYS = [ "ServerRoot", ] ACCOUNTING_CATEGORIES = { "http": ("HTTP",), "itip": ("iTIP", "iTIP-VFREEBUSY",), "implicit": ("Implicit Errors",), "autoscheduling": ("AutoScheduling",), "ischedule": ("iSchedule",), "migration": ("migration",), } LOGGING_CATEGORIES = { "directory": ("twext^who", "txdav^who",), "imip": ("txdav^caldav^datastore^scheduling^imip",), } def usage(e=None): if e: print(e) print("") name = os.path.basename(sys.argv[0]) print("usage: %s [options] config_key" % (name,)) print("") print("Print the value of the given config key.") print("options:") print(" -a --accounting: Specify accounting categories (combinations of http, itip, implicit, autoscheduling, ischedule, migration, or off)") print(" -h --help: print this help and exit") print(" -f --config: Specify caldavd.plist configuration path") print(" -l --logging: Specify logging categories (combinations of directory, imip, or default)") print(" -r --restart: Restart the calendar service") print(" --start: Start the calendar service and agent") print(" --stop: Stop the calendar service and agent") print(" -w --writeconfig: Specify caldavd.plist configuration path for writing") if e: sys.exit(64) else: sys.exit(0) def runAsRootCheck(): """ If we're running in Server.app context and are not running as root, exit. """ if os.path.abspath(__file__).startswith("/Applications/Server.app/"): if os.getuid() != 0: print("Must be run as root") sys.exit(1) def main(): runAsRootCheck() try: (optargs, args) = getopt( sys.argv[1:], "hf:rw:a:l:", [ "help", "config=", "writeconfig=", "restart", "start", "stop", "accounting=", "logging=", ], ) except GetoptError, e: usage(e) configFileName = DEFAULT_CONFIG_FILE writeConfigFileName = "" accountingCategories = None loggingCategories = None doStop = False doStart = False doRestart = False for opt, arg in optargs: if opt in ("-h", "--help"): usage() elif opt in ("-f", "--config"): configFileName = arg elif opt in ("-w", "--writeconfig"): writeConfigFileName = arg elif opt in ("-a", "--accounting"): accountingCategories = arg.split(",") elif opt in ("-l", "--logging"): loggingCategories = arg.split(",") if opt == "--stop": doStop = True if opt == "--start": doStart = True if opt in ("-r", "--restart"): doRestart = True try: config.load(configFileName) except ConfigurationError, e: sys.stdout.write("%s\n" % (e,)) sys.exit(1) if not writeConfigFileName: # If --writeconfig was not passed, use WritableConfigFile from # main plist. If that's an empty string, writes will happen to # the main file. writeConfigFileName = config.WritableConfigFile if not writeConfigFileName: writeConfigFileName = configFileName if doStop: setServiceState("org.calendarserver.agent", "disable") setServiceState("org.calendarserver.calendarserver", "disable") setEnabled(False) if doStart: setServiceState("org.calendarserver.agent", "enable") setServiceState("org.calendarserver.calendarserver", "enable") setEnabled(True) if doStart or doStop: setReverseProxies() sys.exit(0) if doRestart: restartService(config.PIDFile) sys.exit(0) writable = WritableConfig(config, writeConfigFileName) writable.read() # Convert logging categories to actual config key changes if loggingCategories is not None: args = ["LogLevels="] if loggingCategories != ["default"]: for cat in loggingCategories: if cat in LOGGING_CATEGORIES: for moduleName in LOGGING_CATEGORIES[cat]: args.append("LogLevels.{}=debug".format(moduleName)) # Convert accounting categories to actual config key changes if accountingCategories is not None: args = ["AccountingCategories="] if accountingCategories != ["off"]: for cat in accountingCategories: if cat in ACCOUNTING_CATEGORIES: for key in ACCOUNTING_CATEGORIES[cat]: args.append("AccountingCategories.{}=True".format(key)) processArgs(writable, args) def setServiceState(service, state): """ Invoke serverctl to enable/disable a service """ SERVERCTL = "/Applications/Server.app/Contents/ServerRoot/usr/sbin/serverctl" child = subprocess.Popen( args=[SERVERCTL, state, "service={}".format(service)], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) _ignore_output, error = child.communicate() if child.returncode: sys.stdout.write( "Error from serverctl: %d, %s" % (child.returncode, error) ) def setEnabled(enabled): command = { "command": "writeConfig", "Values": { "EnableCalDAV": enabled, "EnableCardDAV": enabled, }, } runner = Runner([command], quiet=True) runner.run() def setReverseProxies(): """ Invoke calendarserver_reverse_proxies """ SERVERCTL = "/Applications/Server.app/Contents/ServerRoot/usr/libexec/calendarserver_reverse_proxies" child = subprocess.Popen( args=[SERVERCTL, ], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) child.communicate() def processArgs(writable, args, restart=True): """ Perform the read/write operations requested in the command line args. If there are no args, stdin is read, and plist-formatted commands are processed from there. @param writable: the WritableConfig @param args: a list of utf-8 encoded strings @param restart: whether to restart the calendar server after making a config change. """ if args: for configKey in args: # args come in as utf-8 encoded strings configKey = configKey.decode("utf-8") if "=" in configKey: # This is an assignment configKey, stringValue = configKey.split("=") if stringValue: value = writable.convertToValue(stringValue) valueDict = setKeyPath({}, configKey, value) writable.set(valueDict) else: writable.delete(configKey) else: # This is a read c = config for subKey in configKey.split("."): c = c.get(subKey, None) if c is None: sys.stderr.write("No such config key: %s\n" % configKey) break sys.stdout.write("%s=%s\n" % (configKey.encode("utf-8"), c)) writable.save(restart=restart) else: # Read plist commands from stdin rawInput = sys.stdin.read() try: plist = readPlistFromString(rawInput) # Note: values in plist will already be unicode except xml.parsers.expat.ExpatError, e: # @UndefinedVariable respondWithError(str(e)) return # If the plist is an array, each element of the array is a separate # command dictionary. if isinstance(plist, list): commands = plist else: commands = [plist] runner = Runner(commands) runner.run() class Runner(object): """ A class which carries out commands, which are plist strings containing dictionaries with a "command" key, plus command-specific data. """ def __init__(self, commands, quiet=False): """ @param commands: the commands to run @type commands: list of plist strings """ self.commands = commands self.quiet = quiet def validate(self): """ Validate all the commands by making sure this class implements all the command keys. @return: True if all commands are valid, False otherwise """ # Make sure commands are valid for command in self.commands: if 'command' not in command: respondWithError("'command' missing from plist") return False commandName = command['command'] methodName = "command_%s" % (commandName,) if not hasattr(self, methodName): respondWithError("Unknown command '%s'" % (commandName,)) return False return True def run(self): """ Find the appropriate method for each command and call them. """ try: for command in self.commands: commandName = command['command'] methodName = "command_%s" % (commandName,) if hasattr(self, methodName): getattr(self, methodName)(command) else: respondWithError("Unknown command '%s'" % (commandName,)) except Exception, e: respondWithError("Command failed: '%s'" % (str(e),)) raise def command_readConfig(self, command): """ Return current configuration @param command: the dictionary parsed from the plist read from stdin @type command: C{dict} """ result = {} for keyPath in chain(WRITABLE_CONFIG_KEYS, READONLY_CONFIG_KEYS): value = getKeyPath(config, keyPath) if value is not None: # Note: config contains utf-8 encoded strings, but plistlib # wants unicode, so decode here: if isinstance(value, str): value = value.decode("utf-8") setKeyPath(result, keyPath, value) respond(command, result) def command_writeConfig(self, command): """ Write config to secondary, writable plist @param command: the dictionary parsed from the plist read from stdin @type command: C{dict} """ writable = WritableConfig(config, config.WritableConfigFile) writable.read() valuesToWrite = command.get("Values", {}) # Note: values are unicode if they contain non-ascii for keyPath, value in flattenDictionary(valuesToWrite): if keyPath in WRITABLE_CONFIG_KEYS: writable.set(setKeyPath(ConfigDict(), keyPath, value)) try: writable.save(restart=False) except Exception, e: respond(command, {"error": str(e)}) else: config.reload() if not self.quiet: self.command_readConfig(command) def setKeyPath(parent, keyPath, value): """ Allows the setting of arbitrary nested dictionary keys via a single dot-separated string. For example, setKeyPath(parent, "foo.bar.baz", "xyzzy") would create any intermediate missing directories (or whatever class parent is, such as ConfigDict) so that the following structure results: parent = { "foo" : { "bar" : { "baz" : "xyzzy } } } @param parent: the object to modify @type parent: any dict-like object @param keyPath: a dot-delimited string specifying the path of keys to traverse @type keyPath: C{str} @param value: the value to set @type value: c{object} @return: parent """ original = parent # Embedded ^ should be replaced by . # That way, we can still use . as key path separator even though sometimes # a key part wants to have a . in it (such as a LogLevels module). # For example: LogLevels.twext^who, will get converted to a "LogLevels" # dict containing a the key "twext.who" parts = [p.replace("^", ".") for p in keyPath.split(".")] for part in parts[:-1]: child = parent.get(part, None) if child is None: parent[part] = child = parent.__class__() parent = child parent[parts[-1]] = value return original def getKeyPath(parent, keyPath): """ Allows the getting of arbitrary nested dictionary keys via a single dot-separated string. For example, getKeyPath(parent, "foo.bar.baz") would fetch parent["foo"]["bar"]["baz"]. If any of the keys don't exist, None is returned instead. @param parent: the object to traverse @type parent: any dict-like object @param keyPath: a dot-delimited string specifying the path of keys to traverse @type keyPath: C{str} @return: the value at keyPath """ parts = keyPath.split(".") for part in parts[:-1]: child = parent.get(part, None) if child is None: return None parent = child return parent.get(parts[-1], None) def flattenDictionary(dictionary, current=""): """ Returns a generator of (keyPath, value) tuples for the given dictionary, where each keyPath is a dot-separated string representing the complete path to a nested key. @param dictionary: the dict object to traverse @type dictionary: C{dict} @param current: do not use; used internally for recursion @type current: C{str} @return: generator of (keyPath, value) tuples """ for key, value in dictionary.iteritems(): if isinstance(value, dict): for result in flattenDictionary(value, current + key + "."): yield result else: yield (current + key, value) def restartService(pidFilename): """ Given the path to a PID file, sends a HUP signal to the contained pid in order to cause calendar server to restart. @param pidFilename: an absolute path to a PID file @type pidFilename: C{str} """ if os.path.exists(pidFilename): with open(pidFilename, "r") as pidFile: pid = pidFile.read().strip() try: pid = int(pid) except ValueError: return try: os.kill(pid, signal.SIGHUP) except OSError: pass class WritableConfig(object): """ A wrapper around a Config object which allows writing of values. The idea is a deployment could have a master plist which doesn't change, and have it include a plist file which does. This class facilitates writing to that included plist. """ def __init__(self, wrappedConfig, fileName): """ @param wrappedConfig: the Config object to read from @type wrappedConfig: C{Config} @param fileName: the full path to the modifiable plist @type fileName: C{str} """ self.config = wrappedConfig self.fileName = fileName self.changes = None self.currentConfigSubset = ConfigDict() self.dirty = False def set(self, data): """ Merges data into a ConfigDict of changes intended to be saved to disk when save( ) is called. @param data: a dict containing new values @type data: C{dict} """ if not isinstance(data, ConfigDict): data = ConfigDict(mapping=data) mergeData(self.currentConfigSubset, data) self.dirty = True def delete(self, key): """ Deletes the specified key @param key: the key to delete @type key: C{str} """ try: del self.currentConfigSubset[key] self.dirty = True except: pass def read(self): """ Reads in the data contained in the writable plist file. @return: C{ConfigDict} """ if os.path.exists(self.fileName): self.currentConfigSubset = ConfigDict(mapping=plistlib.readPlist(self.fileName)) else: self.currentConfigSubset = ConfigDict() def toString(self): return plistlib.writePlistToString(self.currentConfigSubset) def save(self, restart=False): """ Writes any outstanding changes to the writable plist file. Optionally restart calendar server. @param restart: whether to restart the calendar server. @type restart: C{bool} """ if self.dirty: content = writePlistToString(self.currentConfigSubset) fp = FilePath(self.fileName) fp.setContent(content) self.dirty = False if restart: restartService(self.config.PIDFile) @classmethod def convertToValue(cls, string): """ Inspect string and convert the value into an appropriate Python data type TODO: change this to look at actual types defined within stdconfig """ if "." in string: try: value = float(string) except ValueError: value = string else: try: value = int(string) except ValueError: if string == "True": value = True elif string == "False": value = False else: value = string return value def respond(command, result): sys.stdout.write(writePlistToString({'command': command['command'], 'result': result})) def respondWithError(msg, status=1): sys.stdout.write(writePlistToString({'error': msg, }))