OwlCyberSecurity - MANAGER
Edit File: principals.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 operator import os import uuid from calendarserver.tools.cmdline import utilityMain, WorkerService from calendarserver.tools.util import ( recordForPrincipalID, prettyRecord, action_addProxy, action_removeProxy ) from twext.who.directory import DirectoryRecord from twext.who.idirectory import RecordType, InvalidDirectoryRecordError from twisted.internet import reactor from twisted.internet.defer import inlineCallbacks, returnValue, succeed from twistedcaldav.config import config from twistedcaldav.cache import MemcacheChangeNotifier from txdav.who.delegates import CachingDelegates from txdav.who.idirectory import AutoScheduleMode from txdav.who.groups import GroupCacherPollingWork allowedAutoScheduleModes = { "default": None, "none": AutoScheduleMode.none, "accept-always": AutoScheduleMode.accept, "decline-always": AutoScheduleMode.decline, "accept-if-free": AutoScheduleMode.acceptIfFree, "decline-if-busy": AutoScheduleMode.declineIfBusy, "automatic": AutoScheduleMode.acceptIfFreeDeclineIfBusy, } def usage(e=None): if e: print(e) print("") name = os.path.basename(sys.argv[0]) print("usage: %s [options] action_flags principal [principal ...]" % (name,)) print(" %s [options] --list-principal-types" % (name,)) print(" %s [options] --list-principals type" % (name,)) print("") print(" Performs the given actions against the giving principals.") print("") print(" Principals are identified by one of the following:") print(" Type and shortname (eg.: users:wsanchez)") # print(" A principal path (eg.: /principals/users/wsanchez/)") print(" A GUID (eg.: E415DBA7-40B5-49F5-A7CC-ACC81E4DEC79)") print("") print("options:") print(" -h --help: print this help and exit") print(" -f --config <path>: Specify caldavd.plist configuration path") print(" -v --verbose: print debugging information") print("") print("actions:") print(" --context <search-context>: {user|group|location|resource|attendee}; must be used in conjunction with --search") print(" --search <search-tokens>: search using one or more tokens") print(" --list-principal-types: list all of the known principal types") print(" --list-principals type: list all principals of the given type") print(" --list-read-proxies: list proxies with read-only access") print(" --list-write-proxies: list proxies with read-write access") print(" --list-proxies: list all proxies") print(" --list-proxy-for: principals this principal is a proxy for") print(" --add-read-proxy=principal: add a read-only proxy") print(" --add-write-proxy=principal: add a read-write proxy") print(" --remove-proxy=principal: remove a proxy") print(" --set-auto-schedule-mode={default|none|accept-always|decline-always|accept-if-free|decline-if-busy|automatic}: set auto-schedule mode") print(" --get-auto-schedule-mode: read auto-schedule mode") print(" --set-auto-accept-group=principal: set auto-accept-group") print(" --get-auto-accept-group: read auto-accept-group") print(" --add {locations|resources|addresses} full-name record-name UID: add a principal") print(" --remove: remove a principal") print(" --set-name=name: set the name of a principal") print(" --get-name: get the name of a principal") print(" --set-geo=url: set the geo: url for an address (e.g. geo:37.331741,-122.030333)") print(" --get-geo: get the geo: url for an address") print(" --set-street-address=streetaddress: set the street address string for an address") print(" --get-street-address: get the street address string for an address") print(" --set-address=guid: associate principal with an address (by guid)") print(" --get-address: get the associated address's guid") print(" --refresh-groups: schedule a group membership refresh") print(" --print-group-info <group principals>: prints group delegation and membership") if e: sys.exit(64) else: sys.exit(0) class PrincipalService(WorkerService): """ Executes principals-related functions in a context which has access to the store """ function = None params = [] @inlineCallbacks def doWork(self): """ Calls the function that's been assigned to "function" and passes the root resource, directory, store, and whatever has been assigned to "params". """ if ( config.EnableResponseCache and config.Memcached.Pools.Default.ClientEnabled ): # These class attributes need to be setup with our memcache\ # notifier CachingDelegates.cacheNotifier = MemcacheChangeNotifier(None, cacheHandle="PrincipalToken") if self.function is not None: yield self.function(self.store, *self.params) def main(): try: (optargs, args) = getopt( sys.argv[1:], "a:hf:P:v", [ "help", "config=", "add=", "remove", "context=", "search", "list-principal-types", "list-principals=", # Proxies "list-read-proxies", "list-write-proxies", "list-proxies", "list-proxy-for", "add-read-proxy=", "add-write-proxy=", "remove-proxy=", # Groups "list-group-members", "add-group-member=", "remove-group-member=", "print-group-info", "refresh-groups", # Scheduling "set-auto-schedule-mode=", "get-auto-schedule-mode", "set-auto-accept-group=", "get-auto-accept-group", # Principal details "set-name=", "get-name", "set-geo=", "get-geo", "set-address=", "get-address", "set-street-address=", "get-street-address", "verbose", ], ) except GetoptError, e: usage(e) # # Get configuration # configFileName = None addType = None listPrincipalTypes = False listPrincipals = None searchContext = None searchTokens = None printGroupInfo = False scheduleGroupRefresh = False principalActions = [] verbose = False for opt, arg in optargs: # Args come in as encoded bytes arg = arg.decode("utf-8") if opt in ("-h", "--help"): usage() elif opt in ("-v", "--verbose"): verbose = True elif opt in ("-f", "--config"): configFileName = arg elif opt in ("-a", "--add"): addType = arg elif opt in ("-r", "--remove"): principalActions.append((action_removePrincipal,)) elif opt in ("", "--list-principal-types"): listPrincipalTypes = True elif opt in ("", "--list-principals"): listPrincipals = arg elif opt in ("", "--context"): searchContext = arg elif opt in ("", "--search"): searchTokens = args elif opt in ("", "--list-read-proxies"): principalActions.append((action_listProxies, "read")) elif opt in ("", "--list-write-proxies"): principalActions.append((action_listProxies, "write")) elif opt in ("-L", "--list-proxies"): principalActions.append((action_listProxies, "read", "write")) elif opt in ("--list-proxy-for"): principalActions.append((action_listProxyFor, "read", "write")) elif opt in ("--add-read-proxy", "--add-write-proxy"): if "read" in opt: proxyType = "read" elif "write" in opt: proxyType = "write" else: raise AssertionError("Unknown proxy type") principalActions.append((action_addProxy, proxyType, arg)) elif opt in ("", "--remove-proxy"): principalActions.append((action_removeProxy, arg)) elif opt in ("", "--list-group-members"): principalActions.append((action_listGroupMembers,)) elif opt in ("--add-group-member"): principalActions.append((action_addGroupMember, arg)) elif opt in ("", "--remove-group-member"): principalActions.append((action_removeGroupMember, arg)) elif opt in ("", "--print-group-info"): printGroupInfo = True elif opt in ("", "--refresh-groups"): scheduleGroupRefresh = True elif opt in ("", "--set-auto-schedule-mode"): try: if arg not in allowedAutoScheduleModes: raise ValueError("Unknown auto-schedule mode: {mode}".format( mode=arg)) autoScheduleMode = allowedAutoScheduleModes[arg] except ValueError, e: abort(e) principalActions.append((action_setAutoScheduleMode, autoScheduleMode)) elif opt in ("", "--get-auto-schedule-mode"): principalActions.append((action_getAutoScheduleMode,)) elif opt in ("", "--set-auto-accept-group"): principalActions.append((action_setAutoAcceptGroup, arg)) elif opt in ("", "--get-auto-accept-group"): principalActions.append((action_getAutoAcceptGroup,)) elif opt in ("", "--set-name"): principalActions.append((action_setValue, u"fullNames", [arg])) elif opt in ("", "--get-name"): principalActions.append((action_getValue, u"fullNames")) elif opt in ("", "--set-geo"): principalActions.append((action_setValue, u"geographicLocation", arg)) elif opt in ("", "--get-geo"): principalActions.append((action_getValue, u"geographicLocation")) elif opt in ("", "--set-street-address"): principalActions.append((action_setValue, u"streetAddress", arg)) elif opt in ("", "--get-street-address"): principalActions.append((action_getValue, u"streetAddress")) elif opt in ("", "--set-address"): principalActions.append((action_setValue, u"associatedAddress", arg)) elif opt in ("", "--get-address"): principalActions.append((action_getValue, u"associatedAddress")) else: raise NotImplementedError(opt) # # List principals # if listPrincipalTypes: if args: usage("Too many arguments") function = runListPrincipalTypes params = () elif printGroupInfo: function = printGroupCacherInfo params = (args,) elif scheduleGroupRefresh: function = scheduleGroupRefreshJob params = () elif addType: try: addType = matchStrings( addType, [ "locations", "resources", "addresses", "users", "groups" ] ) except ValueError, e: print(e) return try: fullName, shortName, uid = parseCreationArgs(args) except ValueError, e: print(e) return if fullName is not None: fullNames = [fullName] else: fullNames = () if shortName is not None: shortNames = [shortName] else: shortNames = () function = runAddPrincipal params = (addType, uid, shortNames, fullNames) elif listPrincipals: try: listPrincipals = matchStrings( listPrincipals, ["users", "groups", "locations", "resources", "addresses"] ) except ValueError, e: print(e) return if args: usage("Too many arguments") function = runListPrincipals params = (listPrincipals,) elif searchTokens: function = runSearch searchTokens = [t.decode("utf-8") for t in searchTokens] params = (searchTokens, searchContext) else: if not args: usage("No principals specified.") unicodeArgs = [a.decode("utf-8") for a in args] function = runPrincipalActions params = (unicodeArgs, principalActions) PrincipalService.function = function PrincipalService.params = params utilityMain(configFileName, PrincipalService, verbose=verbose) def runListPrincipalTypes(service, store): directory = store.directoryService() for recordType in directory.recordTypes(): print(directory.recordTypeToOldName(recordType)) return succeed(None) @inlineCallbacks def runListPrincipals(service, store, listPrincipals): directory = store.directoryService() recordType = directory.oldNameToRecordType(listPrincipals) try: records = list((yield directory.recordsWithRecordType(recordType))) if records: printRecordList(records) else: print("No records of type %s" % (listPrincipals,)) except InvalidDirectoryRecordError, e: usage(e) returnValue(None) @inlineCallbacks def runPrincipalActions(service, store, principalIDs, actions): directory = store.directoryService() for principalID in principalIDs: # Resolve the given principal IDs to records try: record = yield recordForPrincipalID(directory, principalID) except ValueError: record = None if record is None: sys.stderr.write("Invalid principal ID: %s\n" % (principalID,)) continue # Performs requested actions for action in actions: (yield action[0](store, record, *action[1:])) print("") @inlineCallbacks def runSearch(service, store, tokens, context=None): directory = store.directoryService() records = list( ( yield directory.recordsMatchingTokens( tokens, context=context ) ) ) if records: records.sort(key=operator.attrgetter('fullNames')) print("{n} matches found:".format(n=len(records))) for record in records: print( "\n{d} ({rt})".format( d=record.displayName, rt=record.recordType.name ) ) print(" UID: {u}".format(u=record.uid,)) try: print( " Record name{plural}: {names}".format( plural=("s" if len(record.shortNames) > 1 else ""), names=(", ".join(record.shortNames)) ) ) except AttributeError: pass try: if record.emailAddresses: print( " Email{plural}: {emails}".format( plural=("s" if len(record.emailAddresses) > 1 else ""), emails=(", ".join(record.emailAddresses)) ) ) except AttributeError: pass else: print("No matches found") print("") @inlineCallbacks def runAddPrincipal(service, store, addType, uid, shortNames, fullNames): directory = store.directoryService() recordType = directory.oldNameToRecordType(addType) # See if that UID is in use record = yield directory.recordWithUID(uid) if record is not None: print("UID already in use: {uid}".format(uid=uid)) returnValue(None) # See if the shortnames are in use for shortName in shortNames: record = yield directory.recordWithShortName(recordType, shortName) if record is not None: print("Record name already in use: {name}".format(name=shortName)) returnValue(None) fields = { directory.fieldName.recordType: recordType, directory.fieldName.uid: uid, directory.fieldName.shortNames: shortNames, directory.fieldName.fullNames: fullNames, directory.fieldName.hasCalendars: True, directory.fieldName.hasContacts: True, } record = DirectoryRecord(directory, fields) yield record.service.updateRecords([record], create=True) print("Added '{name}'".format(name=fullNames[0])) @inlineCallbacks def action_removePrincipal(store, record): directory = store.directoryService() fullName = record.displayName shortNames = ",".join(record.shortNames) yield directory.removeRecords([record.uid]) print( "Removed '{full}' {shorts} {uid}".format( full=fullName, shorts=shortNames, uid=record.uid ) ) @inlineCallbacks def action_listProxies(store, record, *proxyTypes): directory = store.directoryService() for proxyType in proxyTypes: groupRecordType = { "read": directory.recordType.readDelegateGroup, "write": directory.recordType.writeDelegateGroup, }.get(proxyType) pseudoGroup = yield directory.recordWithShortName( groupRecordType, record.uid ) proxies = yield pseudoGroup.members() if proxies: print("%s proxies for %s:" % ( {"read": "Read-only", "write": "Read/write"}[proxyType], prettyRecord(record) )) printRecordList(proxies) print("") else: print("No %s proxies for %s" % (proxyType, prettyRecord(record))) @inlineCallbacks def action_listProxyFor(store, record, *proxyTypes): directory = store.directoryService() if record.recordType != directory.recordType.user: print("You must pass a user principal to this command") returnValue(None) for proxyType in proxyTypes: groupRecordType = { "read": directory.recordType.readDelegatorGroup, "write": directory.recordType.writeDelegatorGroup, }.get(proxyType) pseudoGroup = yield directory.recordWithShortName( groupRecordType, record.uid ) proxies = yield pseudoGroup.members() if proxies: print("%s is a %s proxy for:" % ( prettyRecord(record), {"read": "Read-only", "write": "Read/write"}[proxyType] )) printRecordList(proxies) print("") else: print( "{r} is not a {t} proxy for anyone".format( r=prettyRecord(record), t={"read": "Read-only", "write": "Read/write"}[proxyType] ) ) @inlineCallbacks def action_listGroupMembers(store, record): members = yield record.members() if members: print("Group members for %s:\n" % ( prettyRecord(record) )) printRecordList(members) print("") else: print("No group members for %s" % (prettyRecord(record),)) @inlineCallbacks def action_addGroupMember(store, record, *memberIDs): directory = store.directoryService() existingMembers = yield record.members() existingMemberUIDs = set([member.uid for member in existingMembers]) add = set() for memberID in memberIDs: memberRecord = yield recordForPrincipalID(directory, memberID) if memberRecord is None: print("Invalid member ID: %s" % (memberID,)) elif memberRecord.uid in existingMemberUIDs: print("Existing member ID: %s" % (memberID,)) else: add.add(memberRecord) if add: yield record.addMembers(add) for memberRecord in add: print( "Added {member} for {record}".format( member=prettyRecord(memberRecord), record=prettyRecord(record) ) ) yield record.service.updateRecords([record], create=False) @inlineCallbacks def action_removeGroupMember(store, record, *memberIDs): directory = store.directoryService() existingMembers = yield record.members() existingMemberUIDs = set([member.uid for member in existingMembers]) remove = set() for memberID in memberIDs: memberRecord = yield recordForPrincipalID(directory, memberID) if memberRecord is None: print("Invalid member ID: %s" % (memberID,)) elif memberRecord.uid not in existingMemberUIDs: print("Missing member ID: %s" % (memberID,)) else: remove.add(memberRecord) if remove: yield record.removeMembers(remove) for memberRecord in remove: print( "Removed {member} for {record}".format( member=prettyRecord(memberRecord), record=prettyRecord(record) ) ) yield record.service.updateRecords([record], create=False) @inlineCallbacks def printGroupCacherInfo(service, store, principalIDs): """ Print all groups that have been delegated to, their cached members, and who delegated to those groups. """ directory = store.directoryService() txn = store.newTransaction() if not principalIDs: groupUIDs = yield txn.allGroupDelegates() else: groupUIDs = [] for principalID in principalIDs: record = yield recordForPrincipalID(directory, principalID) if record: groupUIDs.append(record.uid) for groupUID in groupUIDs: group = yield txn.groupByUID(groupUID) print("Group: \"{name}\" ({uid})".format(name=group.name, uid=group.groupUID)) for txt, readWrite in (("read-only", False), ("read-write", True)): delegatorUIDs = yield txn.delegatorsToGroup(group.groupID, readWrite) for delegatorUID in delegatorUIDs: delegator = yield directory.recordWithUID(delegatorUID) print( "...has {rw} access to {rec}".format( rw=txt, rec=prettyRecord(delegator) ) ) print("Group members:") memberUIDs = yield txn.groupMemberUIDs(group.groupID) for memberUID in memberUIDs: record = yield directory.recordWithUID(memberUID) print(prettyRecord(record)) print("Last cached: {} GMT".format(group.modified)) print() yield txn.commit() @inlineCallbacks def scheduleGroupRefreshJob(service, store): """ Schedule GroupCacherPollingWork """ txn = store.newTransaction() print("Scheduling a group refresh") yield GroupCacherPollingWork.reschedule(txn, 0, force=True) yield txn.commit() def action_getAutoScheduleMode(store, record): print( "Auto-schedule mode for {record} is {mode}".format( record=prettyRecord(record), mode=( record.autoScheduleMode.description if record.autoScheduleMode else "Default" ) ) ) @inlineCallbacks def action_setAutoScheduleMode(store, record, autoScheduleMode): if record.recordType == RecordType.group: print( "Setting auto-schedule-mode for {record} is not allowed.".format( record=prettyRecord(record) ) ) elif ( record.recordType == RecordType.user and not config.Scheduling.Options.AutoSchedule.AllowUsers ): print( "Setting auto-schedule-mode for {record} is not allowed.".format( record=prettyRecord(record) ) ) else: print( "Setting auto-schedule-mode to {mode} for {record}".format( mode=("default" if autoScheduleMode is None else autoScheduleMode.description), record=prettyRecord(record), ) ) yield record.setAutoScheduleMode(autoScheduleMode) @inlineCallbacks def action_setAutoAcceptGroup(store, record, autoAcceptGroup): if record.recordType == RecordType.group: print( "Setting auto-accept-group for {record} is not allowed.".format( record=prettyRecord(record) ) ) elif ( record.recordType == RecordType.user and not config.Scheduling.Options.AutoSchedule.AllowUsers ): print( "Setting auto-accept-group for {record} is not allowed.".format( record=prettyRecord(record) ) ) else: groupRecord = yield recordForPrincipalID(record.service, autoAcceptGroup) if groupRecord is None or groupRecord.recordType != RecordType.group: print("Invalid principal ID: {id}".format(id=autoAcceptGroup)) else: print("Setting auto-accept-group to {group} for {record}".format( group=prettyRecord(groupRecord), record=prettyRecord(record), )) # Get original fields newFields = record.fields.copy() # Set new values newFields[record.service.fieldName.autoAcceptGroup] = groupRecord.uid updatedRecord = DirectoryRecord(record.service, newFields) yield record.service.updateRecords([updatedRecord], create=False) @inlineCallbacks def action_getAutoAcceptGroup(store, record): if record.autoAcceptGroup: groupRecord = yield record.service.recordWithUID( record.autoAcceptGroup ) if groupRecord is not None: print( "Auto-accept-group for {record} is {group}".format( record=prettyRecord(record), group=prettyRecord(groupRecord), ) ) else: print( "Invalid auto-accept-group assigned: {uid}".format( uid=record.autoAcceptGroup ) ) else: print( "No auto-accept-group assigned to {record}".format( record=prettyRecord(record) ) ) @inlineCallbacks def action_setValue(store, record, name, value): displayValue = value if isinstance(value, list): displayValue = u", ".join(value) print( "Setting {name} to {value} for {record}".format( name=name, value=displayValue, record=prettyRecord(record), ) ) # Get original fields newFields = record.fields.copy() # Set new value newFields[record.service.fieldName.lookupByName(name)] = value updatedRecord = DirectoryRecord(record.service, newFields) yield record.service.updateRecords([updatedRecord], create=False) def action_getValue(store, record, name): try: value = record.fields[record.service.fieldName.lookupByName(name)] if isinstance(value, list): value = u", ".join(value) print( "{name} for {record} is {value}".format( name=name, record=prettyRecord(record), value=value ) ) except KeyError: print( "{name} is not set for {record}".format( name=name, record=prettyRecord(record), ) ) def abort(msg, status=1): sys.stdout.write("%s\n" % (msg,)) try: reactor.stop() except RuntimeError: pass sys.exit(status) def parseCreationArgs(args): """ Look at the command line arguments for --add; the first arg is required and is the full name. If only that one arg is provided, generate a UUID and use it for record name and uid. If two args are provided, use the second arg as the record name and generate a UUID for the uid. If three args are provided, the second arg is the record name and the third arg is the uid. """ numArgs = len(args) if numArgs == 0: print( "When adding a principal, you must provide the full-name" ) sys.exit(64) fullName = args[0].decode("utf-8") if numArgs == 1: shortName = uid = unicode(uuid.uuid4()).upper() elif numArgs == 2: shortName = args[1].decode("utf-8") uid = unicode(uuid.uuid4()).upper() else: shortName = args[1].decode("utf-8") uid = args[2].decode("utf-8") return fullName, shortName, uid def isUUID(value): try: uuid.UUID(value) return True except: return False def matchStrings(value, validValues): for validValue in validValues: if validValue.startswith(value): return validValue raise ValueError("'%s' is not a recognized value" % (value,)) def printRecordList(records): results = [] for record in records: try: shortNames = record.shortNames except AttributeError: shortNames = [] results.append( (record.displayName, record.recordType.name, record.uid, shortNames) ) results.sort() format = "%-22s %-10s %-20s %s" print(format % ("Full name", "Type", "UID", "Short names")) print(format % ("---------", "----", "---", "-----------")) for fullName, recordType, uid, shortNames in results: print(format % (fullName, recordType, uid, u", ".join(shortNames))) if __name__ == "__main__": main()