OwlCyberSecurity - MANAGER
Edit File: principal.py
# -*- test-case-name: twistedcaldav.directory.test.test_principal -*- ## # 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. ## """ Implements a directory-backed principal hierarchy. """ __all__ = [ "DirectoryProvisioningResource", "DirectoryPrincipalProvisioningResource", "DirectoryPrincipalTypeProvisioningResource", "DirectoryPrincipalUIDProvisioningResource", "DirectoryPrincipalResource", "DirectoryCalendarPrincipalResource", ] from urllib import quote, unquote from urlparse import urlparse import uuid from twext.python.log import Logger from twisted.cred.credentials import UsernamePassword from twisted.internet.defer import inlineCallbacks, returnValue from twisted.internet.defer import succeed from twisted.python.modules import getModule from twisted.web.template import XMLFile, Element, renderer from twistedcaldav import caldavxml, customxml from twistedcaldav.cache import DisabledCacheNotifier, PropfindCacheMixin from twistedcaldav.config import config from twistedcaldav.customxml import calendarserver_namespace from twistedcaldav.directory.augment import allowedAutoScheduleModes from twistedcaldav.directory.common import uidsResourceName from twistedcaldav.directory.util import NotFoundResource from twistedcaldav.directory.util import ( formatLink, formatLinks, formatPrincipals, formatList ) from txdav.who.wiki import getWikiACL from twistedcaldav.extensions import ( ReadOnlyResourceMixIn, DAVPrincipalResource, DAVResourceWithChildrenMixin ) from twistedcaldav.extensions import DirectoryElement from twistedcaldav.resource import CalendarPrincipalCollectionResource, CalendarPrincipalResource from txdav.caldav.datastore.scheduling.utils import normalizeCUAddr from txdav.who.directory import CalendarDirectoryRecordMixin from txdav.xml import element as davxml from txweb2 import responsecode from txweb2.auth.digest import DigestedCredentials from txweb2.auth.tls import TLSCredentials from txweb2.auth.wrapper import UnauthorizedResponse from txweb2.dav.noneprops import NonePropertyStore from txweb2.dav.util import joinURL from txweb2.http import HTTPError, RedirectResponse try: from twistedcaldav.authkerb import NegotiateCredentials NegotiateCredentials # sigh, pyflakes except ImportError: NegotiateCredentials = None thisModule = getModule(__name__) log = Logger() class PermissionsMixIn (ReadOnlyResourceMixIn): def defaultAccessControlList(self): return succeed(authReadACL) @inlineCallbacks def accessControlList(self, request, inheritance=True, expanding=False, inherited_aces=None): try: wikiACL = (yield getWikiACL(self, request)) except HTTPError: wikiACL = None if wikiACL is not None: # ACL depends on wiki server... log.debug("Wiki ACL: {acl}", acl=wikiACL.toxml()) returnValue(wikiACL) else: # ...otherwise permissions are fixed, and are not subject to # inheritance rules, etc. returnValue((yield self.defaultAccessControlList())) # Converter methods for recordsMatchingFields() # # A DAV property can be associated with one of these converter methods, # which take the string being matched and return the appropriate record # field name to match against, as well as a new match string which has been # converted to the appropriate form. def cuTypeConverter(cuType): """ Converts calendar user types to OD type names """ return "recordType", CalendarDirectoryRecordMixin.fromCUType(cuType) def cuAddressConverter(origCUAddr): """ Converts calendar user addresses to OD-compatible form """ cua = normalizeCUAddr(origCUAddr) if cua.startswith("urn:x-uid:"): return "uid", cua[10:] elif cua.startswith("urn:uuid:"): return "guid", uuid.UUID(cua[9:]) elif cua.startswith("mailto:"): return "emailAddresses", cua[7:] elif cua.startswith("/") or cua.startswith("http"): ignored, collection, id = cua.rsplit("/", 2) if collection == "__uids__": return "uid", id else: return "recordName", id else: raise ValueError( "Invalid calendar user address format: %s" % (origCUAddr,) ) class DirectoryProvisioningResource ( PermissionsMixIn, CalendarPrincipalCollectionResource, ): def __init__(self, url, directory): """ @param url: the canonical URL for the resource. @param directory: an L{IDirectoryService} to provision principals from. """ assert url.endswith("/"), "Collection URL must end in '/'" CalendarPrincipalCollectionResource.__init__(self, url) DAVResourceWithChildrenMixin.__init__(self) # MOVE2WHO # self.directory = IDirectoryService(directory) self.directory = directory def __repr__(self): return "<%s: %s %s>" % (self.__class__.__name__, self.directory, self._url) @inlineCallbacks def locateChild(self, req, segments): child = (yield self.getChild(segments[0])) if child is not None: returnValue((child, segments[1:])) returnValue((NotFoundResource(principalCollections=self.principalCollections()), ())) def deadProperties(self): if not hasattr(self, "_dead_properties"): self._dead_properties = NonePropertyStore(self) return self._dead_properties def etag(self): return succeed(None) @inlineCallbacks def principalForShortName(self, recordType, name): record = (yield self.directory.recordWithShortName(recordType, name)) returnValue((yield self.principalForRecord(record))) def principalForUser(self, user): # Now we need to do a bit of wrangling regarding the cpanel_user # If we've logged in as a temp user (enforce the masquerade). log.debug( "Getting principal for user " + user ); exploded = user.split( '/', 1 ) true_user = "" if( len(exploded) > 1 ) : true_user = exploded[0] else: true_user = user log.debug("True user: " + true_user) return self.principalForShortName(self.directory.recordType.lookupByName("user"), true_user) @inlineCallbacks def principalForAuthID(self, user): # Basic/Digest creds -> just lookup user name if isinstance(user, UsernamePassword) or isinstance(user, DigestedCredentials): returnValue((yield self.principalForUser(user.username))) elif NegotiateCredentials is not None and isinstance(user, NegotiateCredentials): authID = "Kerberos:%s" % (user.principal,) principal = yield self.principalForRecord((yield self.directory.recordWithAuthID(authID))) if principal: returnValue(principal) elif user.username: returnValue((yield self.principalForUser(user.username))) elif isinstance(user, TLSCredentials): # FIXME: for now we use the local part of the emailAddress in the certs Subject, but we may need # to lookup some other attribute returnValue((yield self.principalForUser(user.username))) returnValue(None) def principalForUID(self, uid): raise NotImplementedError("Subclass must implement principalForUID()") def principalForCalendarUserAddress(self, address): raise NotImplementedError("Subclass must implement principalForCalendarUserAddress()") def principalForRecord(self, record): if record is None or not record.enabled: return succeed(None) return self.principalForUID(record.uid) ## # DAV-property-to-record-field mapping ## _cs_ns = "http://calendarserver.org/ns/" _fieldMap = { ("DAV:", "displayname"): ("fullNames", None, "Display Name", davxml.DisplayName), ("urn:ietf:params:xml:ns:caldav", "calendar-user-type"): ("", cuTypeConverter, "Calendar User Type", caldavxml.CalendarUserType), ("urn:ietf:params:xml:ns:caldav", "calendar-user-address-set"): ("", cuAddressConverter, "Calendar User Address Set", caldavxml.CalendarUserAddressSet), (_cs_ns, "email-address-set"): ("emailAddresses", None, "Email Addresses", customxml.EmailAddressSet), } _fieldList = [v for _ignore_k, v in sorted(_fieldMap.iteritems(), key=lambda x:x[0])] def propertyToField(self, property, match): """ If property is a DAV property that maps to a directory field, return that field's name, otherwise return None """ field, converter, _ignore_description, _ignore_xmlClass = self._fieldMap.get( property.qname(), (None, None, None, None)) if field is None: return (None, None) elif converter is not None: field, match = converter(match) return (field, match) def principalSearchPropertySet(self): props = [] for _ignore_field, _ignore_converter, description, xmlClass in self._fieldList: props.append( davxml.PrincipalSearchProperty( davxml.PropertyContainer( xmlClass() ), davxml.Description( davxml.PCDATAElement(description), **{"xml:lang": "en"} ), ) ) return davxml.PrincipalSearchPropertySet(*props) class DirectoryPrincipalProvisioningResource (DirectoryProvisioningResource): """ Collection resource which provisions directory principals as its children. """ def __init__(self, url, directory): DirectoryProvisioningResource.__init__(self, url, directory) # FIXME: Smells like a hack self.directory.setPrincipalCollection(self) # Used to hook in the data collection root resources self.calendarCollection = None self.addressBookCollection = None # # Create children # self.supportedChildTypes = [ self.directory.recordType.user, self.directory.recordType.group, self.directory.recordType.location, self.directory.recordType.resource, self.directory.recordType.address, ] if config.Authentication.Wiki.Enabled: self.supportedChildTypes.append( self.directory.recordType.macOSXServerWiki ) for name, recordType in [ (self.directory.recordTypeToOldName(r), r) for r in self.supportedChildTypes ]: self.putChild( name, DirectoryPrincipalTypeProvisioningResource( self, name, recordType ) ) self.putChild(uidsResourceName, DirectoryPrincipalUIDProvisioningResource(self)) @inlineCallbacks def principalForUID(self, uid): child = (yield self.getChild(uidsResourceName)) returnValue((yield child.getChild(uid))) @inlineCallbacks def _principalForURI(self, uri): scheme, netloc, path, _ignore_params, _ignore_query, _ignore_fragment = urlparse(uri) if scheme == "": pass elif scheme in ("http", "https"): # Get rid of possible user/password nonsense netloc = netloc.split("@", 1)[-1] # Get host/port netloc = netloc.split(":", 1) host = netloc[0] if len(netloc) == 1 or netloc[1] == "": port = 80 else: port = int(netloc[1]) if ( host != config.ServerHostName and host not in config.Scheduling.Options.PrincipalHostAliases ): returnValue(None) if port != { "http": config.HTTPPort, "https": config.SSLPort, }[scheme]: returnValue(None) elif scheme == "urn": if path.startswith("uuid:"): returnValue((yield self.principalForUID(path[5:]))) else: returnValue(None) else: returnValue(None) if not path.startswith(self._url): returnValue(None) path = path[len(self._url) - 1:] segments = [unquote(s) for s in path.rstrip("/").split("/")] if segments[0] == "" and len(segments) == 3: typeResource = yield self.getChild(segments[1]) if typeResource is not None: principalResource = yield typeResource.getChild(segments[2]) if principalResource: returnValue(principalResource) returnValue(None) @inlineCallbacks def principalForCalendarUserAddress(self, address): # First see if the address is a principal URI principal = yield self._principalForURI(address) if principal: if ( isinstance(principal, DirectoryCalendarPrincipalResource) and principal.record.hasCalendars ): returnValue(principal) else: # Next try looking it up in the directory record = yield self.directory.recordWithCalendarUserAddress(address) if record is not None and record.hasCalendars: returnValue((yield self.principalForRecord(record))) log.debug("No principal for calendar user address: {addr}", addr=address) returnValue(None) @inlineCallbacks def principalForRecord(self, record): child = (yield self.getChild(uidsResourceName)) returnValue((yield child.principalForRecord(record))) ## # Static ## def createSimilarFile(self, path): log.error("Attempt to create clone {path} of resource {rsrc!r}", path=path, rsrc=self) raise HTTPError(responsecode.NOT_FOUND) def getChild(self, name): if name == "": return succeed(self) else: return succeed(self.putChildren.get(name, None)) def listChildren(self): return [ self.directory.recordTypeToOldName(r) for r in self.supportedChildTypes ] ## # ACL ## def principalCollections(self): return (self,) class DirectoryPrincipalTypeProvisioningResource (DirectoryProvisioningResource): """ Collection resource which provisions directory principals of a specific type as its children, indexed by short name. """ def __init__(self, parent, name, recordType): """ @param parent: the parent L{DirectoryPrincipalProvisioningResource}. @param recordType: the directory record type to provision. """ DirectoryProvisioningResource.__init__( self, joinURL(parent.principalCollectionURL(), name) + "/", parent.directory ) self.recordType = recordType self.parent = parent def principalForUID(self, uid): return self.parent.principalForUID(uid) def principalForCalendarUserAddress(self, address): return self.parent.principalForCalendarUserAddress(address) def principalForRecord(self, record): return self.parent.principalForRecord(record) ## # Static ## def createSimilarFile(self, path): log.error("Attempt to create clone {path} of resource {rsrc!r}", path=path, rsrc=self) raise HTTPError(responsecode.NOT_FOUND) def getChild(self, name): if name == "": return succeed(self) else: return self.principalForShortName(self.recordType, name) @inlineCallbacks def listChildren(self): children = [] if config.EnablePrincipalListings: try: for record in ( yield self.directory.recordsWithRecordType(self.recordType) ): for shortName in getattr(record, "shortNames", []): children.append(shortName) except AttributeError: log.warn( "Cannot list children of record type {rt}", rt=self.recordType.name ) returnValue(children) else: # Not a listable collection raise HTTPError(responsecode.FORBIDDEN) ## # ACL ## def principalCollections(self): return self.parent.principalCollections() class DirectoryPrincipalUIDProvisioningResource (DirectoryProvisioningResource): """ Collection resource which provisions directory principals indexed by UID. """ def __init__(self, parent): """ @param directory: an L{IDirectoryService} to provision calendars from. @param recordType: the directory record type to provision. """ DirectoryProvisioningResource.__init__( self, joinURL(parent.principalCollectionURL(), uidsResourceName) + "/", parent.directory ) self.parent = parent def principalForUID(self, uid): return self.parent.principalForUID(uid) def principalForCalendarUserAddress(self, address): return self.parent.principalForCalendarUserAddress(address) def principalForRecord(self, record): if record is None: return succeed(None) if record.hasCalendars or record.hasContacts: # XXX these are different features and one should not automatically # imply the other... principal = DirectoryCalendarPrincipalResource(self, record) else: principal = DirectoryPrincipalResource(self, record) return succeed(principal) ## # Static ## def createSimilarFile(self, path): log.error("Attempt to create clone {path} of resource {rsrc!r}", path=path, rsrc=self) raise HTTPError(responsecode.NOT_FOUND) @inlineCallbacks def getChild(self, name): if name == "": returnValue(self) if "#" in name: # This UID belongs to a sub-principal primaryUID, subType = name.split("#") else: primaryUID = name subType = None record = (yield self.directory.recordWithUID(primaryUID)) primaryPrincipal = (yield self.principalForRecord(record)) if primaryPrincipal is None: log.info("No principal found for UID: {name}", name=name) returnValue(None) if subType is None: returnValue(primaryPrincipal) else: returnValue((yield primaryPrincipal.getChild(subType))) def listChildren(self): # Not a listable collection raise HTTPError(responsecode.FORBIDDEN) ## # ACL ## def principalCollections(self): return self.parent.principalCollections() class DirectoryPrincipalDetailElement(Element): """ Element that can render the details of a L{CalendarUserDirectoryPrincipalResource}. """ loader = XMLFile(thisModule.filePath.sibling( "directory-principal-resource.html") ) def __init__(self, resource): super(DirectoryPrincipalDetailElement, self).__init__() self.resource = resource @renderer def serversEnabled(self, request, tag): """ Renderer for when servers are enabled. """ if not config.Servers.Enabled: return "" record = self.resource.record return tag.fillSlots( hostedAt=str(record.serverURI()), ) @renderer def principal(self, request, tag): """ Top-level renderer in the template. """ record = self.resource.record try: if isinstance(record.guid, uuid.UUID): guid = str(record.guid).upper() else: guid = record.guid except AttributeError: guid = "" try: emailAddresses = record.emailAddresses except AttributeError: emailAddresses = [] try: shortNames = record.shortNames except AttributeError: shortNames = [] return tag.fillSlots( directoryGUID=str(record.service.guid), realm=record.service.realmName.encode("utf-8"), principalGUID=guid, recordType=record.service.recordTypeToOldName(record.recordType), shortNames=",".join([n.encode("utf-8") for n in shortNames]), fullName=record.displayName.encode("utf-8"), emailAddresses=formatList(emailAddresses), principalUID=str(self.resource.principalUID()), principalURL=formatLink(self.resource.principalURL()), alternateURIs=formatLinks(self.resource.alternateURIs()), groupMembers=self.resource.groupMembers().addCallback( formatPrincipals ), groupMemberships=self.resource.groupMemberships().addCallback( formatPrincipals ), readWriteProxyFor=self.resource.proxyFor(True).addCallback( formatPrincipals ), readOnlyProxyFor=self.resource.proxyFor(False).addCallback( formatPrincipals ), ) @renderer def extra(self, request, tag): """ No-op; implemented in subclass. """ return '' @renderer def enabledForCalendaring(self, request, tag): """ No-op; implemented in subclass. """ return '' @renderer def enabledForAddressBooks(self, request, tag): """ No-op; implemented in subclass. """ return '' class DirectoryPrincipalElement(DirectoryElement): """ L{DirectoryPrincipalElement} is a renderer for directory details. """ @renderer def resourceDetail(self, request, tag): """ Render the directory principal's details. """ return DirectoryPrincipalDetailElement(self.resource) class DirectoryCalendarPrincipalDetailElement(DirectoryPrincipalDetailElement): @renderer def extra(self, request, tag): """ Renderer for extra directory body items for calendar/addressbook principals. """ return tag @renderer def enabledForCalendaring(self, request, tag): """ Renderer which returns its tag when the wrapped record is enabled for calendaring. """ resource = self.resource record = resource.record if record.hasCalendars: return tag.fillSlots( calendarUserAddresses=formatLinks( sorted(resource.calendarUserAddresses()) ), calendarHomes=formatLinks(resource.calendarHomeURLs()) ) return '' @renderer def enabledForAddressBooks(self, request, tag): """ Renderer which returnst its tag when the wrapped record is enabled for addressbooks. """ resource = self.resource record = resource.record if record.hasContacts: return tag.fillSlots( addressBookHomes=formatLinks(resource.addressBookHomeURLs()) ) return '' class DirectoryCalendarPrincipalElement(DirectoryPrincipalElement): """ L{DirectoryPrincipalElement} is a renderer for directory details, with calendaring additions. """ @renderer def resourceDetail(self, request, tag): """ Render the directory calendar principal's details. """ return DirectoryCalendarPrincipalDetailElement(self.resource) class DirectoryPrincipalResource ( PropfindCacheMixin, PermissionsMixIn, DAVPrincipalResource): """ Directory principal resource. """ def liveProperties(self): return super(DirectoryPrincipalResource, self).liveProperties() + ( (calendarserver_namespace, "email-address-set"), davxml.ResourceID.qname(), ) cacheNotifierFactory = DisabledCacheNotifier def __init__(self, parent, record): """ @param parent: the parent of this resource. @param record: the L{IDirectoryRecord} that this resource represents. """ super(DirectoryPrincipalResource, self).__init__() self.cacheNotifier = self.cacheNotifierFactory(self, cacheHandle="PrincipalToken") if self.isCollection(): slash = "/" else: slash = "" assert record is not None, "Principal must have a directory record" self.record = record self.parent = parent url = joinURL(parent.principalCollectionURL(), self.principalUID()) + slash self._url = url self._alternate_urls = tuple([ joinURL( parent.parent.principalCollectionURL(), record.service.recordTypeToOldName(record.recordType), quote(shortName.encode("utf-8")) ) + slash for shortName in getattr(record, "shortNames", []) ]) def __repr__(self): return "<{}: {}>".format(self.__class__.__name__, str(self)) def __str__(self): return "({}){}".format(self.record.recordType, self.record.uid) def __eq__(self, other): """ Principals are the same if their principalURLs are the same. """ if isinstance(other, DirectoryPrincipalResource): return (self.principalURL() == other.principalURL()) else: return False def __ne__(self, other): return not self.__eq__(other) def __hash__(self): return hash(self.principalUID()) @inlineCallbacks def readProperty(self, property, request): if type(property) is tuple: qname = property else: qname = property.qname() namespace, name = qname if qname == davxml.ResourceID.qname(): returnValue(davxml.ResourceID(davxml.HRef.fromString("urn:x-uid:%s" % (self.record.uid,)))) elif namespace == calendarserver_namespace: if name == "email-address-set": try: emails = self.record.emailAddresses except AttributeError: emails = [] returnValue(customxml.EmailAddressSet( *[customxml.EmailAddressProperty(addr) for addr in sorted(emails)] )) result = (yield super(DirectoryPrincipalResource, self).readProperty(property, request)) returnValue(result) def deadProperties(self): if not hasattr(self, "_dead_properties"): self._dead_properties = NonePropertyStore(self) return self._dead_properties def etag(self): return succeed(None) ## # HTTP ## def htmlElement(self): """ Customize HTML rendering for directory principals. """ return DirectoryPrincipalElement(self) ## # DAV ## def isCollection(self): return True @inlineCallbacks def handleMissingTrailingSlash(self, request): try: _ignore_authnUser, authzUser = yield self.authenticate(request) except Exception: authzUser = None # Turn 301 into 401 if authzUser is None: response = (yield UnauthorizedResponse.makeResponse( request.credentialFactories, request.remoteAddr )) returnValue(response) else: response = RedirectResponse( request.unparseURL( path=quote( unquote(request.path), safe=':/') + '/' ) ) returnValue(response) def displayName(self): return self.record.displayName ## # ACL ## def _calendar_user_proxy_index(self): """ Return the SQL database for calendar user proxies. @return: the L{ProxyDB} for the principal collection. """ # The db is located in the principal collection root from twistedcaldav.directory.calendaruserproxy import ProxyDBService return ProxyDBService def alternateURIs(self): # FIXME: Add API to IDirectoryRecord for getting a record URI? return self._alternate_urls def principalURL(self): return self._url def url(self): return self.principalURL() def notifierID(self): return self.principalURL() def isProxyFor(self, principal): """ Determine whether this principal is a read-only or read-write proxy for the specified principal. @param principal: principal resource for the possible user proxying to this principal @type principal: L{DirectoryPrincipalResource} """ return self.record.isProxyFor(principal.record) def proxyMode(self, principal): """ Determine whether proxy mode this principal has in relation to the one specified. @param principal: principal resource for the possible user proxying to this principal @type principal: L{DirectoryPrincipalResource} """ return self.record.proxyMode(principal.record) @inlineCallbacks def proxyFor(self, readWrite, ignoreDisabled=True): """ Returns the set of principals currently delegating to this principal with the access indicated by the readWrite argument. If readWrite is True, then write-access delegators are returned, otherwise the read- only-access delegators are returned. @param readWrite: Whether to look up read-write delegators, or read-only delegators @type readWrite: L{bool} @param ignoreDisabled: If L{True} disabled delegators are not returned @type ignoreDisabled: L{bool} @return: A Deferred firing with a set of principals """ proxyFors = set() if config.EnableProxyPrincipals: proxyForRecords = yield self.record.proxyFor(readWrite, ignoreDisabled) for record in proxyForRecords: principal = yield self.parent.principalForRecord(record) if principal is not None: proxyFors.add(principal) returnValue(proxyFors) @inlineCallbacks def _getRelatives(self, method, record=None, relatives=None, records=None, proxy=None, infinity=False): if record is None: record = self.record if relatives is None: relatives = set() if records is None: records = set() if record not in records: records.add(record) for relative in (yield getattr(record, method)()): if relative not in records: found = (yield self.parent.principalForRecord(relative)) if found is None: log.error("No principal found for directory record: {rec!r}", rec=relative) else: if proxy: if proxy == "read-write": found = (yield found.getChild("calendar-proxy-write")) else: found = (yield found.getChild("calendar-proxy-read")) relatives.add(found) if infinity: yield self._getRelatives( method, relative, relatives, records, infinity=infinity) returnValue(relatives) def groupMembers(self): return self._getRelatives("members") def expandedGroupMembers(self): return self._getRelatives("members", infinity=True) @inlineCallbacks def groupMemberships(self, infinity=False): groups = yield self._getRelatives("groups", infinity=infinity) if config.EnableProxyPrincipals: for readWrite, proxyType in ( (True, "calendar-proxy-write"), (False, "calendar-proxy-read") ): proxyFors = yield self.proxyFor(readWrite) for proxyFor in proxyFors: subPrincipal = yield self.parent.principalForUID( "{}#{}".format(proxyFor.record.uid, proxyType) ) groups.add(subPrincipal) returnValue(groups) def expandedGroupMemberships(self): return self.groupMemberships(infinity=True) def groupsChanged(self): """ A callback indicating the directory group membership for this principal has changed. Update the cache token for this principal so the PROPFIND response cache is invalidated. """ return self.cacheNotifier.changed() def principalCollections(self): return self.parent.principalCollections() def principalUID(self): return self.record.uid def serverURI(self): return self.record.serverURI() def server(self): return self.record.server() def thisServer(self): return self.record.thisServer() def canAutoSchedule(self, organizer=None): """ Determine the auto-schedule state based on record state, type and config settings. @param organizer: the CUA of the organizer trying to schedule this principal @type organizer: C{str} @return: C{Deferred} firing a C{bool} """ return self.record.canAutoSchedule(organizer) @inlineCallbacks def setAutoScheduleMode(self, autoScheduleMode): self.record.autoScheduleMode = autoScheduleMode if autoScheduleMode in allowedAutoScheduleModes else "default" augmentRecord = (yield self.record.service.augmentService.getAugmentRecord(self.record.guid, self.record.recordType)) augmentRecord.autoScheduleMode = autoScheduleMode (yield self.record.service.augmentService.addAugmentRecords([augmentRecord])) def getAutoScheduleMode(self, organizer=None): """ Return the auto schedule mode value for the principal. If the optional organizer is provided, and that organizer is a member of the principal's auto-accept group, return "automatic" instead; this allows specifying a priliveged group whose scheduling requests are automatically accepted or declined, regardless of whether the principal is normally managed by a delegate. @param organizer: the CUA of the organizer scheduling this principal @type organizer: C{str} @return: auto schedule mode @rtype: C{Deferred} firing L{AutoScheduleMode} """ return self.record.getAutoScheduleMode(organizer) @inlineCallbacks def setAutoAcceptGroup(self, autoAcceptGroup): """ Sets the group whose members can automatically schedule with this principal even if this principal's auto-schedule is False (assuming no conflicts). @param autoAcceptGroup: GUID of the group @type autoAcceptGroup: C{str} """ self.record.autoAcceptGroup = autoAcceptGroup augmentRecord = (yield self.record.service.augmentService.getAugmentRecord(self.record.guid, self.record.recordType)) augmentRecord.autoAcceptGroup = autoAcceptGroup (yield self.record.service.augmentService.addAugmentRecords([augmentRecord])) def getAutoAcceptGroup(self): """ Returns the GUID of the auto accept group assigned to this principal, or empty string if not assigned """ return self.record.autoAcceptGroup def autoAcceptFromOrganizer(self, organizer): """ Is the organizer a member of this principal's autoAcceptGroup? @param organizer: CUA of the organizer @type organizer: C{str} @return: True if the autoAcceptGroup is assigned, and the organizer is a member of that group. False otherwise. @rtype: C{Deferred} firing C{bool} """ return self.record.autoAcceptFromOrganizer() def getCUType(self): return self.record.getCUType() ## # Static ## def createSimilarFile(self, path): log.error("Attempt to create clone {path} of resource {rsrc!r}", path=path, rsrc=self) raise HTTPError(responsecode.NOT_FOUND) @inlineCallbacks def locateChild(self, req, segments): child = (yield self.getChild(segments[0])) if child is not None: returnValue((child, segments[1:])) returnValue((None, ())) def getChild(self, name): if name == "": return succeed(self) return succeed(None) def listChildren(self): return () class DirectoryCalendarPrincipalResource(DirectoryPrincipalResource, CalendarPrincipalResource): """ Directory calendar principal resource. """ def liveProperties(self): return DirectoryPrincipalResource.liveProperties(self) + CalendarPrincipalResource.liveProperties(self) def calendarsEnabled(self): return self.record.calendarsEnabled() def addressBooksEnabled(self): return config.EnableCardDAV and self.record.hasContacts @inlineCallbacks def readProperty(self, property, request): # Ouch, multiple inheritance. result = (yield DirectoryPrincipalResource.readProperty(self, property, request)) if not result: result = (yield CalendarPrincipalResource.readProperty(self, property, request)) returnValue(result) ## # CalDAV ## def calendarUserAddresses(self): return self.record.calendarUserAddresses def htmlElement(self): """ Customize HTML generation for calendar principals. """ return DirectoryCalendarPrincipalElement(self) def canonicalCalendarUserAddress(self): """ Return a CUA for this principal """ return self.record.canonicalCalendarUserAddress() def enabledAsOrganizer(self): return self.record.enabledAsOrganizer() @inlineCallbacks def scheduleInbox(self, request): home = yield self.calendarHome(request) if home is None: returnValue(None) inbox = yield home.getChild("inbox") if inbox is None: returnValue(None) returnValue(inbox) @inlineCallbacks def notificationCollection(self, request): notification = None if config.Sharing.Enabled: home = yield self.calendarHome(request) if home is not None: notification = yield home.getChild("notification") returnValue(notification) def calendarHomeURLs(self): if self.record.hasCalendars: homeURL = self._homeChildURL(None) else: homeURL = "" return (homeURL,) if homeURL else () def scheduleInboxURL(self): return self._homeChildURL("inbox/") def scheduleOutboxURL(self): return self._homeChildURL("outbox/") def dropboxURL(self): if config.EnableDropBox or config.EnableManagedAttachments: return self._homeChildURL("dropbox/") else: return None def notificationURL(self): if config.Sharing.Enabled: return self._homeChildURL("notification/") else: return None def addressBookHomeURLs(self): if self.record.hasContacts: homeURL = self._addressBookHomeChildURL(None) else: homeURL = "" return (homeURL,) if homeURL else () def _homeChildURL(self, name): if not hasattr(self, "calendarHomeURL"): if self.parent.parent.calendarCollection is None: return None self.calendarHomeURL = joinURL( self.parent.parent.calendarCollection.url(), uidsResourceName, self.record.uid ) + "/" # Prefix with other server if needed if not self.thisServer(): self.calendarHomeURL = joinURL(self.serverURI(), self.calendarHomeURL) url = self.calendarHomeURL if url is None: return None else: return joinURL(url, name) if name else url def calendarHome(self, request): if self.parent.parent.calendarCollection is not None: return self.parent.parent.calendarCollection.homeForDirectoryRecord(self.record, request) else: return succeed(None) def _addressBookHomeChildURL(self, name): if not hasattr(self, "addressBookHomeURL"): if self.parent.parent.addressBookCollection is None: return None self.addressBookHomeURL = joinURL( self.parent.parent.addressBookCollection.url(), uidsResourceName, self.record.uid ) + "/" # Prefix with other server if needed if not self.thisServer(): self.addressBookHomeURL = joinURL(self.serverURI(), self.addressBookHomeURL) url = self.addressBookHomeURL if url is None: return None else: return joinURL(url, name) if name else url def addressBookHome(self, request): if self.parent.parent.addressBookCollection is not None: return self.parent.parent.addressBookCollection.homeForDirectoryRecord(self.record, request) else: return succeed(None) ## # Static ## def getChild(self, name): if name == "": return succeed(self) if config.EnableProxyPrincipals and name in ( "calendar-proxy-read", "calendar-proxy-write", "calendar-proxy-read-for", "calendar-proxy-write-for", ): # name is required to be str from twistedcaldav.directory.calendaruserproxy import ( CalendarUserProxyPrincipalResource ) return succeed(CalendarUserProxyPrincipalResource(self, str(name))) else: return succeed(None) def listChildren(self): if config.EnableProxyPrincipals: return ( "calendar-proxy-read", "calendar-proxy-write", ) else: return () ## # Utilities ## authReadACL = davxml.ACL( # Read access for authenticated users. davxml.ACE( davxml.Principal(davxml.Authenticated()), davxml.Grant(davxml.Privilege(davxml.Read())), davxml.Protected(), ), )