OwlCyberSecurity - MANAGER
Edit File: gateway.py
#!/usr/bin/env python ## # Copyright (c) 2006-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) from getopt import getopt, GetoptError import os from plistlib import readPlistFromString, writePlistToString import uuid import xml from calendarserver.tools.cmdline import utilityMain from calendarserver.tools.config import ( WRITABLE_CONFIG_KEYS, setKeyPath, getKeyPath, flattenDictionary, WritableConfig ) from calendarserver.tools.purge import ( WorkerService, PurgeOldEventsService, DEFAULT_BATCH_SIZE, DEFAULT_RETAIN_DAYS, PrincipalPurgeWork ) from calendarserver.tools.util import ( recordForPrincipalID, autoDisableMemcached, getProxies, setProxies ) from pycalendar.datetime import DateTime from twext.who.directory import DirectoryRecord from twisted.internet.defer import inlineCallbacks, succeed, returnValue from twistedcaldav.config import config, ConfigDict from txdav.who.idirectory import RecordType as CalRecordType from twext.who.idirectory import FieldName from twisted.python.constants import Names, NamedConstant from txdav.who.delegates import Delegates, RecordType as DelegateRecordType attrMap = { 'GeneratedUID': {'attr': 'uid', }, 'RealName': {'attr': 'fullNames', }, 'RecordName': {'attr': 'shortNames', }, 'AutoScheduleMode': {'attr': 'autoScheduleMode', }, 'AutoAcceptGroup': {'attr': 'autoAcceptGroup', }, # 'Comment': {'extras': True, 'attr': 'comment', }, # 'Description': {'extras': True, 'attr': 'description', }, # 'Type': {'extras': True, 'attr': 'type', }, # For "Locations", i.e. scheduled spaces 'Capacity': {'attr': 'capacity', }, 'Floor': {'attr': 'floor', }, 'AssociatedAddress': {'attr': 'associatedAddress', }, # For "Addresses", i.e. nonscheduled areas containing Locations 'AbbreviatedName': {'attr': 'abbreviatedName', }, 'StreetAddress': {'attr': 'streetAddress', }, 'GeographicLocation': {'attr': 'geographicLocation', }, } def usage(e=None): name = os.path.basename(sys.argv[0]) print("usage: %s [options]" % (name,)) print("") print(" TODO: describe usage") print("") print("options:") print(" -h --help: print this help and exit") print(" -e --error: send stderr to stdout") print(" -f --config <path>: Specify caldavd.plist configuration path") print("") if e: sys.exit(64) else: sys.exit(0) class RunnerService(WorkerService): """ A wrapper around Runner which uses utilityMain to get the store """ commands = None @inlineCallbacks def doWork(self): """ Create/run a Runner to execute the commands """ runner = Runner(self.store, self.commands) if runner.validate(): yield runner.run() def doWorkWithoutStore(self): respondWithError("Database is not available") return succeed(None) def main(): try: (optargs, _ignore_args) = getopt( sys.argv[1:], "hef:", [ "help", "error", "config=", ], ) except GetoptError, e: usage(e) # # Get configuration # configFileName = None debug = False for opt, arg in optargs: if opt in ("-h", "--help"): usage() if opt in ("-e", "--error"): debug = True elif opt in ("-f", "--config"): configFileName = arg else: raise NotImplementedError(opt) # # Read commands from stdin # rawInput = sys.stdin.read() try: plist = readPlistFromString(rawInput) 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] RunnerService.commands = commands utilityMain(configFileName, RunnerService, verbose=debug) class Runner(object): def __init__(self, store, commands, output=None): self.store = store self.dir = store.directoryService() self.commands = commands if output is None: output = sys.stdout self.output = output def validate(self): # Make sure commands are valid for command in self.commands: if 'command' not in command: self.respondWithError("'command' missing from plist") return False commandName = command['command'] methodName = "command_%s" % (commandName,) if not hasattr(self, methodName): self.respondWithError("Unknown command '%s'" % (commandName,)) return False return True @inlineCallbacks def run(self): # This method can be called as the result of an agent request. We # check to see if memcached is there for each call because the server # could have stopped/started since the last time. for pool in config.Memcached.Pools.itervalues(): pool.ClientEnabled = True autoDisableMemcached(config) try: for command in self.commands: commandName = command['command'] methodName = "command_%s" % (commandName,) if hasattr(self, methodName): (yield getattr(self, methodName)(command)) else: self.respondWithError("Unknown command '%s'" % (commandName,)) except Exception, e: self.respondWithError("Command failed: '%s'" % (str(e),)) raise # Locations # deferred def command_getLocationList(self, command): return self.respondWithRecordsOfTypes(self.dir, command, ["locations"]) @inlineCallbacks def _saveRecord(self, typeName, recordType, command, oldFields=None): """ Save a record using the values in the command plist, starting with any fields in the optional oldFields. @param typeName: one of "locations", "resources", "addresses"; used to return the appropriate list of records afterwards. @param recordType: the type of record to save @param command: the command containing values @type command: C{dict} @param oldFields: the optional fields to start with, which will be overridden by values from command @type oldFiles: C{dict} """ if oldFields is None: fields = { self.dir.fieldName.recordType: recordType, self.dir.fieldName.hasCalendars: True, self.dir.fieldName.hasContacts: True, } create = True else: fields = oldFields.copy() create = False for key, info in attrMap.iteritems(): if key in command: attrName = info['attr'] field = self.dir.fieldName.lookupByName(attrName) valueType = self.dir.fieldName.valueType(field) value = command[key] # For backwards compatibility, convert to a list if needed if ( self.dir.fieldName.isMultiValue(field) and not isinstance(value, list) ): value = [value] if valueType == int: value = int(value) elif issubclass(valueType, Names): if value is not None: value = valueType.lookupByName(value) else: if isinstance(value, list): newList = [] for item in value: if isinstance(item, str): newList.append(item.decode("utf-8")) else: newList.append(item) value = newList elif isinstance(value, str): value = value.decode("utf-8") fields[field] = value if FieldName.uid not in fields: # No uid provided, so generate one fields[FieldName.uid] = unicode(uuid.uuid4()).upper() if FieldName.shortNames not in fields: # No short names were provided, so copy from uid fields[FieldName.shortNames] = [fields[FieldName.uid]] record = DirectoryRecord(self.dir, fields) yield self.dir.updateRecords([record], create=create) readProxies = command.get("ReadProxies", None) if readProxies: proxyRecords = [] for proxyUID in readProxies: proxyRecord = yield self.dir.recordWithUID(proxyUID) if proxyRecord is not None: proxyRecords.append(proxyRecord) readProxies = proxyRecords writeProxies = command.get("WriteProxies", None) if writeProxies: proxyRecords = [] for proxyUID in writeProxies: proxyRecord = yield self.dir.recordWithUID(proxyUID) if proxyRecord is not None: proxyRecords.append(proxyRecord) writeProxies = proxyRecords yield setProxies(record, readProxies, writeProxies) yield self.respondWithRecordsOfTypes(self.dir, command, [typeName]) def command_createLocation(self, command): return self._saveRecord("locations", CalRecordType.location, command) def command_createResource(self, command): return self._saveRecord("resources", CalRecordType.resource, command) def command_createAddress(self, command): return self._saveRecord("addresses", CalRecordType.address, command) @inlineCallbacks def command_setLocationAttributes(self, command): uid = command['GeneratedUID'] record = yield self.dir.recordWithUID(uid) yield self._saveRecord( "locations", CalRecordType.location, command, oldFields=record.fields ) @inlineCallbacks def command_setResourceAttributes(self, command): uid = command['GeneratedUID'] record = yield self.dir.recordWithUID(uid) yield self._saveRecord( "resources", CalRecordType.resource, command, oldFields=record.fields ) @inlineCallbacks def command_setAddressAttributes(self, command): uid = command['GeneratedUID'] record = yield self.dir.recordWithUID(uid) yield self._saveRecord( "addresses", CalRecordType.address, command, oldFields=record.fields ) @inlineCallbacks def command_getLocationAttributes(self, command): uid = command['GeneratedUID'] record = yield self.dir.recordWithUID(uid) if record is None: self.respondWithError("Principal not found: %s" % (uid,)) return recordDict = recordToDict(record) # recordDict['AutoSchedule'] = yield principal.getAutoSchedule() try: recordDict['AutoAcceptGroup'] = record.autoAcceptGroup except AttributeError: pass readProxies, writeProxies = yield getProxies(record) recordDict['ReadProxies'] = [r.uid for r in readProxies] recordDict['WriteProxies'] = [r.uid for r in writeProxies] self.respond(command, recordDict) command_getResourceAttributes = command_getLocationAttributes command_getAddressAttributes = command_getLocationAttributes # Resources def command_getResourceList(self, command): return self.respondWithRecordsOfTypes(self.dir, command, ["resources"]) # deferred def command_getLocationAndResourceList(self, command): return self.respondWithRecordsOfTypes(self.dir, command, ["locations", "resources"]) # Addresses def command_getAddressList(self, command): return self.respondWithRecordsOfTypes(self.dir, command, ["addresses"]) @inlineCallbacks def _delete(self, typeName, command): uid = command['GeneratedUID'] yield self.dir.removeRecords([uid]) yield self.respondWithRecordsOfTypes(self.dir, command, [typeName]) @inlineCallbacks def command_deleteLocation(self, command): txn = self.store.newTransaction() uid = command['GeneratedUID'] yield txn.enqueue(PrincipalPurgeWork, uid=uid) yield txn.commit() yield self._delete("locations", command) @inlineCallbacks def command_deleteResource(self, command): txn = self.store.newTransaction() uid = command['GeneratedUID'] yield txn.enqueue(PrincipalPurgeWork, uid=uid) yield txn.commit() yield self._delete("resources", command) def command_deleteAddress(self, command): return self._delete("addresses", command) # Config def command_readConfig(self, command): """ Return current configuration @param command: the dictionary parsed from the plist read from stdin @type command: C{dict} """ config.reload() # config.Memcached.Pools.Default.ClientEnabled = False result = {} for keyPath in WRITABLE_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) self.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: self.respond(command, {"error": str(e)}) else: self.command_readConfig(command) # Proxies def command_listWriteProxies(self, command): return self._listProxies(command, "write") def command_listReadProxies(self, command): return self._listProxies(command, "read") @inlineCallbacks def _listProxies(self, command, proxyType): record = yield recordForPrincipalID(self.dir, command['Principal']) if record is None: self.respondWithError("Principal not found: %s" % (command['Principal'],)) returnValue(None) yield self.respondWithProxies(command, record, proxyType) def command_addReadProxy(self, command): return self._addProxy(command, "read") def command_addWriteProxy(self, command): return self._addProxy(command, "write") @inlineCallbacks def _addProxy(self, command, proxyType): record = yield recordForPrincipalID(self.dir, command['Principal']) if record is None: self.respondWithError("Principal not found: %s" % (command['Principal'],)) returnValue(None) proxyRecord = yield recordForPrincipalID(self.dir, command['Proxy']) if proxyRecord is None: self.respondWithError("Proxy not found: %s" % (command['Proxy'],)) returnValue(None) txn = self.store.newTransaction() yield Delegates.addDelegate(txn, record, proxyRecord, (proxyType == "write")) yield txn.commit() yield self.respondWithProxies(command, record, proxyType) def command_removeReadProxy(self, command): return self._removeProxy(command, "read") def command_removeWriteProxy(self, command): return self._removeProxy(command, "write") @inlineCallbacks def _removeProxy(self, command, proxyType): record = yield recordForPrincipalID(self.dir, command['Principal']) if record is None: self.respondWithError("Principal not found: %s" % (command['Principal'],)) returnValue(None) proxyRecord = yield recordForPrincipalID(self.dir, command['Proxy']) if proxyRecord is None: self.respondWithError("Proxy not found: %s" % (command['Proxy'],)) returnValue(None) txn = self.store.newTransaction() yield Delegates.removeDelegate(txn, record, proxyRecord, (proxyType == "write")) yield txn.commit() yield self.respondWithProxies(command, record, proxyType) @inlineCallbacks def command_purgeOldEvents(self, command): """ Convert RetainDays from the command dictionary into a date, then purge events older than that date. @param command: the dictionary parsed from the plist read from stdin @type command: C{dict} """ retainDays = command.get("RetainDays", DEFAULT_RETAIN_DAYS) cutoff = DateTime.getToday() cutoff.setDateOnly(False) cutoff.offsetDay(-retainDays) eventCount = (yield PurgeOldEventsService.purgeOldEvents(self.store, None, cutoff, DEFAULT_BATCH_SIZE)) self.respond(command, {'EventsRemoved': eventCount, "RetainDays": retainDays}) @inlineCallbacks def respondWithProxies(self, command, record, proxyType): proxies = [] recordType = { "read": DelegateRecordType.readDelegateGroup, "write": DelegateRecordType.writeDelegateGroup, }[proxyType] proxyGroup = yield self.dir.recordWithShortName(recordType, record.uid) for member in (yield proxyGroup.members()): proxies.append(member.uid) self.respond(command, { 'Principal': record.uid, 'Proxies': proxies }) @inlineCallbacks def respondWithRecordsOfTypes(self, directory, command, recordTypes): result = [] for recordType in recordTypes: recordType = directory.oldNameToRecordType(recordType) for record in (yield directory.recordsWithRecordType(recordType)): recordDict = recordToDict(record) result.append(recordDict) self.respond(command, result) def respond(self, command, result): self.output.write(writePlistToString({'command': command['command'], 'result': result})) def respondWithError(self, msg, status=1): self.output.write(writePlistToString({'error': msg, })) def recordToDict(record): recordDict = {} for key, info in attrMap.iteritems(): try: value = record.fields[record.service.fieldName.lookupByName(info['attr'])] if value is None: continue # For backwards compatibility, present fullName/RealName as single # value even though twext.who now has it as multiValue if key == "RealName": value = value[0] if isinstance(value, str): value = value.decode("utf-8") elif isinstance(value, NamedConstant): value = value.name recordDict[key] = value except KeyError: pass return recordDict def respondWithError(msg, status=1): sys.stdout.write(writePlistToString({'error': msg, })) if __name__ == "__main__": main()