OwlCyberSecurity - MANAGER
Edit File: extensions.py
# -*- test-case-name: twistedcaldav.test.test_extensions -*- ## # Copyright (c) 2005-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 """ Extensions to web2.dav """ __all__ = [ "DAVResource", "DAVResourceWithChildrenMixin", "DAVPrincipalResource", "DAVFile", "ReadOnlyWritePropertiesResourceMixIn", "ReadOnlyResourceMixIn", "CachingPropertyStore", ] import urllib import time from itertools import cycle from twisted.internet.defer import succeed, maybeDeferred from twisted.internet.defer import inlineCallbacks, returnValue from twisted.web.template import Element, XMLFile, renderer, tags, flattenString from twisted.python.modules import getModule from txweb2 import responsecode, server from txweb2.http import HTTPError, Response, RedirectResponse from txweb2.http import StatusResponse from txweb2.http_headers import MimeType from txweb2.stream import FileStream from txweb2.static import MetaDataMixin, StaticRenderMixin from txdav.xml import element from txdav.xml.base import encodeXMLName from txdav.xml.element import dav_namespace from txweb2.dav.http import MultiStatusResponse from txweb2.dav.static import DAVFile as SuperDAVFile from txweb2.dav.resource import DAVResource as SuperDAVResource from txweb2.dav.resource import ( DAVPrincipalResource as SuperDAVPrincipalResource ) from twisted.internet.defer import gatherResults from txweb2.dav.method import prop_common from twext.python.log import Logger from twistedcaldav import customxml from twistedcaldav.customxml import calendarserver_namespace from twistedcaldav.method.report import http_REPORT from twistedcaldav.config import config from txdav.who.directory import CalendarDirectoryRecordMixin from twext.who.expression import Operand, MatchType, MatchFlags thisModule = getModule(__name__) log = Logger() class WebDAVServerInfoMixIn(object): def renderHTTP(self, request): def _addServerInfoURL(request, response): # server-info-token processing if config.EnableServerInfo and ( request.headers.hasHeader("server-info-token") and request.headers.getRawHeaders("server-info-token")[0] != config.ServerInfoToken or not request.headers.hasHeader("server-info-token") and request.method == "OPTIONS" ): uri = request.unparseURL(path="/server-info") response.headers.setRawHeaders("server-info-url", (uri,)) return response request.addResponseFilter(_addServerInfoURL, onlyOnce=True) return super(WebDAVServerInfoMixIn, self).renderHTTP(request) class DirectoryPrincipalPropertySearchMixIn(object): def _get_user_and_domain_from_request (self, request): auth_header = request.headers.getRawHeaders('Authorization', [None])[0] if auth_header and auth_header.startswith('Basic '): import base64 decoded = base64.b64decode(auth_header[6:]).decode('utf-8') username = decoded.split(':', 1)[0] user, domain = username.split('@') if '@' in username else (username, None) return user, domain return None, None def _filter_records_by_domain(self, records, auth_domain): filtered_records = [] for record in records: if ( hasattr(record, 'recordType') and hasattr(record, 'service') and hasattr(record.service, 'recordType') and hasattr(record.service.recordType, 'user') and record.recordType == record.service.recordType.user ): shortName = getattr(record, 'shortName', None) or getattr(record, 'uid', None) email = None if hasattr(record, 'emailAddresses') and record.emailAddresses: # Use the first email address if available email = list(record.emailAddresses)[0] elif shortName and '@' in str(shortName): email = str(shortName) elif getattr(record, 'uid', None) and '@' in str(record.uid): email = str(record.uid) if email: record_domain = email.split('@')[-1] log.debug("Record email: {email}, domain: {record_domain}", email=email, record_domain=record_domain) # Only include records from the same domain as the authenticated user if auth_domain and record_domain == auth_domain: filtered_records.append(record) else: log.debug("Record does not have an email address: {record}", record=record) else: log.debug("Record is not a user type: {record}", record=record) return filtered_records @inlineCallbacks def report_DAV__principal_property_search( self, request, principal_property_search ): """ Generate a principal-property-search REPORT. (RFC 3744, section 9.4) Overrides twisted implementation, targeting only directory-enabled searching. """ # Verify root element if not isinstance(principal_property_search, element.PrincipalPropertySearch): msg = "%s expected as root element, not %s." % (element.PrincipalPropertySearch.sname(), principal_property_search.sname()) log.warn(msg) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg)) # Should we AND (the default) or OR (if test="anyof")? testMode = principal_property_search.attributes.get("test", "allof") if testMode not in ("allof", "anyof"): msg = "Bad XML: unknown value for test attribute: %s" % (testMode,) log.warn(msg) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg)) operand = Operand.AND if testMode == "allof" else Operand.OR # Are we narrowing results down to a single CUTYPE? cuType = principal_property_search.attributes.get("type", None) if cuType not in ("INDIVIDUAL", "GROUP", "RESOURCE", "ROOM", None): msg = "Bad XML: unknown value for type attribute: %s" % (cuType,) log.warn(msg) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg)) # Only handle Depth: 0 depth = request.headers.getHeader("depth", "0") if depth != "0": log.error("Error in principal-property-search REPORT, Depth set to {depth}", depth=depth) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Depth %s not allowed" % (depth,))) # Get any limit value from xml clientLimit = None # Get a single DAV:prop element from the REPORT request body propertiesForResource = None propElement = None propertySearches = [] applyTo = False for child in principal_property_search.children: if child.qname() == (dav_namespace, "prop"): propertiesForResource = prop_common.propertyListForResource propElement = child elif child.qname() == ( dav_namespace, "apply-to-principal-collection-set" ): applyTo = True elif child.qname() == (dav_namespace, "property-search"): props = child.childOfType(element.PropertyContainer) props.removeWhitespaceNodes() match = child.childOfType(element.Match) caseless = match.attributes.get("caseless", "yes") if caseless not in ("yes", "no"): msg = "Bad XML: unknown value for caseless attribute: %s" % (caseless,) log.warn(msg) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg)) caseless = (caseless == "yes") matchType = match.attributes.get("match-type", u"contains").encode("utf-8") if matchType not in ("starts-with", "contains", "equals"): msg = "Bad XML: unknown value for match-type attribute: %s" % (matchType,) log.warn(msg) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg)) # Convert to twext.who.expression form matchType = { "starts-with": MatchType.startsWith, "contains": MatchType.contains, "equals": MatchType.equals }.get(matchType) matchFlags = MatchFlags.caseInsensitive if caseless else MatchFlags.none # Ignore any query strings under three letters matchText = match.toString() # gives us unicode if len(matchText) >= 3: propertySearches.append((props.children, matchText, matchFlags, matchType)) elif child.qname() == (calendarserver_namespace, "limit"): try: nresults = child.childOfType(customxml.NResults) clientLimit = int(str(nresults)) except (TypeError, ValueError,): msg = "Bad XML: unknown value for <limit> element" log.warn(msg) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg)) # Run report resultsWereLimited = None resources = [] if applyTo or not hasattr(self, "directory"): for principalCollection in self.principalCollections(): uri = principalCollection.principalCollectionURL() resource = (yield request.locateResource(uri)) if resource: resources.append((resource, uri)) else: resources.append((self, request.uri)) # We need to access a directory service principalCollection = resources[0][0] if not hasattr(principalCollection, "directory"): # Use Twisted's implementation instead in this case result = (yield super(DirectoryPrincipalPropertySearchMixIn, self).report_DAV__principal_property_search(request, principal_property_search)) returnValue(result) dir = principalCollection.directory # See if we can take advantage of the directory fields = [] nonDirectorySearches = [] for props, match, matchFlags, matchType in propertySearches: nonDirectoryProps = [] for prop in props: try: fieldName, match = principalCollection.propertyToField( prop, match) except ValueError, e: raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, str(e))) if fieldName: fields.append((fieldName, match, matchFlags, matchType)) else: nonDirectoryProps.append(prop) if nonDirectoryProps: nonDirectorySearches.append(( nonDirectoryProps, match, matchFlags, matchType )) matchingResources = [] matchcount = 0 # nonDirectorySearches are ignored if fields: if cuType: recordType = CalendarDirectoryRecordMixin.fromCUType(cuType) else: recordType = None records = ( yield dir.recordsMatchingFields( fields, operand=operand, recordType=recordType, limitResults=clientLimit, timeoutSeconds=config.PrincipalSearchReportTimeout ) ) username, auth_domain = self._get_user_and_domain_from_request(request) # We only want user records that match the exact same domain as the authenticated user filtered_records = self._filter_records_by_domain(records, auth_domain) for record in filtered_records: resource = yield principalCollection.principalForRecord(record) if resource: matchingResources.append(resource) # We've determined this is a matching resource matchcount += 1 if clientLimit is not None and matchcount >= clientLimit: resultsWereLimited = ("client", matchcount) break if matchcount >= config.MaxPrincipalSearchReportResults: resultsWereLimited = ("server", matchcount) break # Generate the response responses = [] for resource in matchingResources: url = resource.url() yield prop_common.responseForHref( request, responses, element.HRef.fromString(url), resource, propertiesForResource, propElement ) if resultsWereLimited is not None: if resultsWereLimited[0] == "server": log.debug("Too many matching resources in principal-property-search report") responses.append(element.StatusResponse( element.HRef.fromString(request.uri), element.Status.fromResponseCode( responsecode.INSUFFICIENT_STORAGE_SPACE ), element.Error(element.NumberOfMatchesWithinLimits()), element.ResponseDescription( "Results limited by %s at %d" % resultsWereLimited ), )) returnValue(MultiStatusResponse(responses)) @inlineCallbacks def report_http___calendarserver_org_ns__calendarserver_principal_search( self, request, calendarserver_principal_search ): """ Generate a calendarserver-principal-search REPORT. @param request: Request object @param calendarserver_principal_search: CalendarServerPrincipalSearch object """ # Verify root element if not isinstance(calendarserver_principal_search, customxml.CalendarServerPrincipalSearch): msg = "%s expected as root element, not %s." % (customxml.CalendarServerPrincipalSearch.sname(), calendarserver_principal_search.sname()) log.warn(msg) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg)) # Only handle Depth: 0 depth = request.headers.getHeader("depth", "0") if depth != "0": log.error("Error in calendarserver-principal-search REPORT, Depth set to {depth}", depth=depth) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Depth %s not allowed" % (depth,))) tokens, context, applyTo, clientLimit, propElement = extractCalendarServerPrincipalSearchData(calendarserver_principal_search) if not validateTokens(tokens): raise HTTPError(StatusResponse( responsecode.FORBIDDEN, "Insufficient search token length" )) # Run report resultsWereLimited = None resources = [] if applyTo or not hasattr(self, "directory"): for principalCollection in self.principalCollections(): uri = principalCollection.principalCollectionURL() resource = (yield request.locateResource(uri)) if resource: resources.append((resource, uri)) else: resources.append((self, request.uri)) # We need to access a directory service principalCollection = resources[0][0] dir = principalCollection.directory matchingResources = [] matchcount = 0 limitResults = config.MaxPrincipalSearchReportResults if clientLimit is not None: limitResults = min(clientLimit, limitResults) records = (yield dir.recordsMatchingTokens( tokens, context=context, limitResults=limitResults, timeoutSeconds=config.PrincipalSearchReportTimeout )) username, auth_domain = self._get_user_and_domain_from_request(request) # We only want user records that match the exact same domain as the authenticated user filtered_records = self._filter_records_by_domain(records, auth_domain) for record in filtered_records: resource = yield principalCollection.principalForRecord(record) if resource: matchingResources.append(resource) # We've determined this is a matching resource matchcount += 1 if clientLimit is not None and matchcount >= clientLimit: resultsWereLimited = ("client", matchcount) break if matchcount >= config.MaxPrincipalSearchReportResults: resultsWereLimited = ("server", matchcount) break # Generate the response responses = [] for resource in matchingResources: url = resource.url() yield prop_common.responseForHref( request, responses, element.HRef.fromString(url), resource, prop_common.propertyListForResource, propElement ) if resultsWereLimited is not None: if resultsWereLimited[0] == "server": log.debug("Too many matching resources in calendarserver-principal-search report") responses.append(element.StatusResponse( element.HRef.fromString(request.uri), element.Status.fromResponseCode( responsecode.INSUFFICIENT_STORAGE_SPACE ), element.Error(element.NumberOfMatchesWithinLimits()), element.ResponseDescription( "Results limited by %s at %d" % resultsWereLimited ), )) returnValue(MultiStatusResponse(responses)) class DirectoryElement(Element): """ A L{DirectoryElement} is an L{Element} for rendering the contents of a L{DirectoryRenderingMixIn} resource as HTML. """ loader = XMLFile( thisModule.filePath.sibling("directory-listing.html") ) def __init__(self, resource): """ @param resource: the L{DirectoryRenderingMixIn} resource being listed. """ super(DirectoryElement, self).__init__() self.resource = resource @renderer def resourceDetail(self, request, tag): """ Renderer which returns a distinct element for this resource's data. Subclasses should override. """ return '' @renderer def children(self, request, tag): """ Renderer which yields all child object tags as table rows. """ whenChildren = ( maybeDeferred(self.resource.listChildren) .addCallback(sorted) .addCallback( lambda names: gatherResults( [maybeDeferred(self.resource.getChild, x) for x in names] ) .addCallback(lambda children: zip(children, names)) ) ) @whenChildren.addCallback def gotChildren(children): for even, [child, name] in zip(cycle(["odd", "even"]), children): [url, name, size, lastModified, contentType] = map( str, self.resource.getChildDirectoryEntry( child, name, request) ) yield tag.clone().fillSlots( url=url, name=name, size=str(size), lastModified=lastModified, even=even, type=contentType, ) return whenChildren @renderer def main(self, request, tag): """ Main renderer; fills slots for title, etc. """ return tag.fillSlots(name=request.path) @renderer def properties(self, request, tag): """ Renderer which yields all properties as table row tags. """ whenPropertiesListed = self.resource.listProperties(request) @whenPropertiesListed.addCallback def gotProperties(qnames): accessDeniedValue = object() def gotError(f, name): f.trap(HTTPError) code = f.value.response.code if code == responsecode.NOT_FOUND: log.error( "Property {name} was returned by listProperties() " "but does not exist for resource {rsrc!r}.", name=name, rsrc=self.resource ) return (name, None) if code == responsecode.UNAUTHORIZED: return (name, accessDeniedValue) return f whenAllProperties = gatherResults([ maybeDeferred(self.resource.readProperty, qn, request) .addCallback(lambda p, iqn=qn: (p.sname(), p.toxml()) if p is not None else (encodeXMLName(*iqn), None)) .addErrback(gotError, encodeXMLName(*qn)) for qn in sorted(qnames) ]) @whenAllProperties.addCallback def gotValues(items): for even, [name, value] in zip(cycle(["odd", "even"]), items): if value is None: value = tags.i("(no value)") elif value is accessDeniedValue: value = tags.i("(access forbidden)") yield tag.clone().fillSlots( even=even, name=name, value=value, ) return whenAllProperties return whenPropertiesListed class DirectoryRenderingMixIn(object): def renderDirectory(self, request): """ Render a directory listing. """ def gotBody(output): mime_params = {"charset": "utf-8"} response = Response(200, {}, output) response.headers.setHeader( "content-type", MimeType("text", "html", mime_params) ) return response return flattenString(request, self.htmlElement()).addCallback(gotBody) def htmlElement(self): """ Create a L{DirectoryElement} or appropriate subclass for rendering this resource. """ return DirectoryElement(self) def getChildDirectoryEntry(self, child, name, request): def orNone(value, default="?", f=None): if value is None: return default elif f is not None: return f(value) else: return value url = urllib.quote(name, '/') if isinstance(child, DAVResource) and child.isCollection(): url += "/" name += "/" if isinstance(child, MetaDataMixin): size = child.contentLength() lastModified = child.lastModified() rtypes = [] fullrtype = child.resourceType() if hasattr(child, "resourceType") else None if fullrtype is not None: for rtype in fullrtype.children: rtypes.append(rtype.name) if rtypes: rtypes = "(%s)" % (", ".join(rtypes),) if child.isCollection() if hasattr(child, "isCollection") else False: contentType = rtypes else: mimeType = child.contentType() if mimeType is None: print('BAD contentType() IMPLEMENTATION', child) contentType = 'application/octet-stream' else: contentType = "%s/%s" % (mimeType.mediaType, mimeType.mediaSubtype) if rtypes: contentType += " %s" % (rtypes,) else: size = None lastModified = None contentType = None if hasattr(child, "resourceType"): rtypes = [] fullrtype = child.resourceType() for rtype in fullrtype.children: rtypes.append(rtype.name) if rtypes: contentType = "(%s)" % (", ".join(rtypes),) return ( url, name, orNone(size), orNone( lastModified, default="", f=lambda t: time.strftime("%Y-%b-%d %H:%M", time.localtime(t)) ), contentType, ) class DAVResource ( WebDAVServerInfoMixIn, DirectoryPrincipalPropertySearchMixIn, SuperDAVResource, DirectoryRenderingMixIn, StaticRenderMixin ): """ Extended L{txweb2.dav.resource.DAVResource} implementation. Note we add StaticRenderMixin as a base class because we need all the etag etc behavior that is currently in static.py but is actually applicable to any type of resource. """ log = Logger() http_REPORT = http_REPORT def davComplianceClasses(self): return ("1", "access-control") # Add "2" when we have locking def render(self, request): if not self.exists(): return responsecode.NOT_FOUND if self.isCollection(): return self.renderDirectory(request) return super(DAVResource, self).render(request) def resourceType(self): # Allow live property to be overridden by dead property if self.deadProperties().contains((dav_namespace, "resourcetype")): return self.deadProperties().get((dav_namespace, "resourcetype")) return element.ResourceType(element.Collection()) if self.isCollection() else element.ResourceType() def contentType(self): return MimeType("httpd", "unix-directory") if self.isCollection() else None class DAVResourceWithChildrenMixin (object): """ Bits needed from txweb2.static """ def __init__(self, principalCollections=None): self.putChildren = {} super(DAVResourceWithChildrenMixin, self).__init__(principalCollections=principalCollections) def putChild(self, name, child): """ Register a child with the given name with this resource. @param name: the name of the child (a URI path segment) @param child: the child to register """ self.putChildren[name] = child def getChild(self, name): """ Look up a child resource. First check C{self.putChildren}, then call C{self.makeChild} if no pre-existing children were found. @return: the child of this resource with the given name. """ if name == "": return self result = self.putChildren.get(name, None) if not result: result = self.makeChild(name) return result def makeChild(self, name): """ Called by L{DAVResourceWithChildrenMixin.getChild} to dynamically create children that have not been pre-created with C{putChild}. """ return None def listChildren(self): """ @return: a sequence of the names of all known children of this resource. """ return self.putChildren.keys() def countChildren(self): """ @return: the number of all known children of this resource. """ return len(self.putChildren.keys()) def locateChild(self, req, segments): """ See L{IResource.locateChild}. """ thisSegment = segments[0] moreSegments = segments[1:] return maybeDeferred(self.getChild, thisSegment).addCallback( lambda it: (it, moreSegments) ) class DAVResourceWithoutChildrenMixin (object): """ Bits needed from txweb2.static """ def __init__(self, principalCollections=None): self.putChildren = {} super(DAVResourceWithChildrenMixin, self).__init__(principalCollections=principalCollections) def findChildren( self, depth, request, callback, privileges=None, inherited_aces=None ): return succeed(None) def locateChild(self, request, segments): return self, server.StopTraversal class DAVPrincipalResource ( WebDAVServerInfoMixIn, DirectoryPrincipalPropertySearchMixIn, SuperDAVPrincipalResource, DirectoryRenderingMixIn ): """ Extended L{txweb2.dav.static.DAVFile} implementation. """ log = Logger() def liveProperties(self): return super(DAVPrincipalResource, self).liveProperties() + ( (calendarserver_namespace, "expanded-group-member-set"), (calendarserver_namespace, "expanded-group-membership"), (calendarserver_namespace, "record-type"), ) http_REPORT = http_REPORT def render(self, request): if not self.exists(): return responsecode.NOT_FOUND if self.isCollection(): return self.renderDirectory(request) return super(DAVResource, self).render(request) @inlineCallbacks def readProperty(self, property, request): if type(property) is tuple: qname = property else: qname = property.qname() namespace, name = qname if namespace == dav_namespace: if name == "resourcetype": rtype = self.resourceType() returnValue(rtype) elif namespace == calendarserver_namespace: if name == "expanded-group-member-set": principals = (yield self.expandedGroupMembers()) returnValue(customxml.ExpandedGroupMemberSet( *[element.HRef(p.principalURL()) for p in principals] )) elif name == "expanded-group-membership": principals = (yield self.expandedGroupMemberships()) returnValue(customxml.ExpandedGroupMembership( *[element.HRef(p.principalURL()) for p in principals] )) elif name == "record-type": if hasattr(self, "record"): returnValue( customxml.RecordType( self.record.service.recordTypeToOldName( self.record.recordType ) ) ) else: raise HTTPError(StatusResponse( responsecode.NOT_FOUND, "Property %s does not exist." % (qname,) )) result = (yield super(DAVPrincipalResource, self).readProperty(property, request)) returnValue(result) def groupMembers(self): return succeed(()) def expandedGroupMembers(self): return succeed(()) def groupMemberships(self): return succeed(()) def expandedGroupMemberships(self): return succeed(()) def resourceType(self): # Allow live property to be overridden by dead property if self.deadProperties().contains((dav_namespace, "resourcetype")): return self.deadProperties().get((dav_namespace, "resourcetype")) if self.isCollection(): return element.ResourceType(element.Principal(), element.Collection()) else: return element.ResourceType(element.Principal()) class DAVFile (WebDAVServerInfoMixIn, SuperDAVFile, DirectoryRenderingMixIn): """ Extended L{txweb2.dav.static.DAVFile} implementation. """ log = Logger() def resourceType(self): # Allow live property to be overridden by dead property if self.deadProperties().contains((dav_namespace, "resourcetype")): return self.deadProperties().get((dav_namespace, "resourcetype")) if self.isCollection(): return element.ResourceType.collection # @UndefinedVariable return element.ResourceType.empty # @UndefinedVariable def render(self, request): if not self.fp.exists(): return responsecode.NOT_FOUND if self.fp.isdir(): if request.path[-1] != "/": # Redirect to include trailing '/' in URI return RedirectResponse(request.unparseURL(path=urllib.quote(urllib.unquote(request.path), safe=':/') + '/')) else: ifp = self.fp.childSearchPreauth(*self.indexNames) if ifp: # Render from the index file return self.createSimilarFile(ifp.path).render(request) return self.renderDirectory(request) try: f = self.fp.open() except IOError, e: import errno if e[0] == errno.EACCES: return responsecode.FORBIDDEN elif e[0] == errno.ENOENT: return responsecode.NOT_FOUND else: raise response = Response() response.stream = FileStream(f, 0, self.fp.getsize()) for (header, value) in ( ("content-type", self.contentType()), ("content-encoding", self.contentEncoding()), ): if value is not None: response.headers.setHeader(header, value) return response class ReadOnlyWritePropertiesResourceMixIn (object): """ Read only that will allow writing of properties resource. """ readOnlyResponse = StatusResponse( responsecode.FORBIDDEN, "Resource is read only." ) def _forbidden(self, request): return self.readOnlyResponse http_DELETE = _forbidden http_MOVE = _forbidden http_PUT = _forbidden class ReadOnlyResourceMixIn (ReadOnlyWritePropertiesResourceMixIn): """ Read only resource. """ http_PROPPATCH = ReadOnlyWritePropertiesResourceMixIn._forbidden def writeProperty(self, property, request): raise HTTPError(self.readOnlyResponse) def accessControlList( self, request, inheritance=True, expanding=False, inherited_aces=None ): # Permissions here are fixed, and are not subject to # inheritance rules, etc. return self.defaultAccessControlList() class PropertyNotFoundError (HTTPError): def __init__(self, qname): HTTPError.__init__( self, StatusResponse( responsecode.NOT_FOUND, "No such property: %s" % encodeXMLName(*qname) ) ) class CachingPropertyStore (object): """ DAV property store using a dict in memory on top of another property store implementation. """ log = Logger() def __init__(self, propertyStore): self.propertyStore = propertyStore self.resource = propertyStore.resource def get(self, qname, uid=None): # self.log.debug("Get: {p}, {n}", p=self.resource.fp.path, n=qname) cache = self._cache() cachedQname = qname + (uid,) if cachedQname in cache: property = cache.get(cachedQname, None) if property is None: self.log.debug("Cache miss: {s!r}, {p}, {n}", s=self, p=self.resource.fp.path, n=qname) try: property = self.propertyStore.get(qname, uid) except HTTPError: del cache[cachedQname] raise PropertyNotFoundError(qname) cache[cachedQname] = property return property else: raise PropertyNotFoundError(qname) def set(self, property, uid=None): # self.log.debug("Set: {p}, {prop!r}", p=self.resource.fp.path, prop=property) cache = self._cache() cachedQname = property.qname() + (uid,) cache[cachedQname] = None self.propertyStore.set(property, uid) cache[cachedQname] = property def contains(self, qname, uid=None): # self.log.debug("Contains: {p}, {n}", p=self.resource.fp.path, n=qname) cachedQname = qname + (uid,) try: cache = self._cache() except HTTPError, e: if e.response.code == responsecode.NOT_FOUND: return False else: raise if cachedQname in cache: # self.log.debug("Contains cache hit: {s!r}, {p}, {n}", s=self, p=self.resource.fp.path, n=qname) return True else: return False def delete(self, qname, uid=None): # self.log.debug("Delete: {p}, {n}", p=self.resource.fp.path, n=qname) cachedQname = qname + (uid,) if self._data is not None and cachedQname in self._data: del self._data[cachedQname] self.propertyStore.delete(qname, uid) def list(self, uid=None, filterByUID=True): # self.log.debug("List: {p}", p=self.resource.fp.path) keys = self._cache().iterkeys() if filterByUID: return [ (namespace, name) for namespace, name, propuid in keys if propuid == uid ] else: return keys def _cache(self): if not hasattr(self, "_data"): # self.log.debug("Cache init: {p}", p=self.resource.fp.path) self._data = dict( (name, None) for name in self.propertyStore.list(filterByUID=False) ) return self._data def extractCalendarServerPrincipalSearchData(doc): """ Extract relevant info from a CalendarServerPrincipalSearch document @param doc: CalendarServerPrincipalSearch object to extract info from @return: A tuple containing: the list of tokens the context string the applyTo boolean the clientLimit integer the propElement containing the properties to return """ context = doc.attributes.get("context", None) applyTo = False tokens = [] clientLimit = None for child in doc.children: if child.qname() == (dav_namespace, "prop"): propElement = child elif child.qname() == ( dav_namespace, "apply-to-principal-collection-set" ): applyTo = True elif child.qname() == (calendarserver_namespace, "search-token"): tokenValue = child.toString().strip() if tokenValue: tokens.append(tokenValue) elif child.qname() == (calendarserver_namespace, "limit"): try: nresults = child.childOfType(customxml.NResults) clientLimit = int(str(nresults)) except (TypeError, ValueError,): msg = "Bad XML: unknown value for <limit> element" log.warn(msg) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg)) return tokens, context, applyTo, clientLimit, propElement def validateTokens(tokens): """ Make sure there is at least one token longer than one character @param tokens: the tokens to inspect @type tokens: iterable of utf-8 encoded strings @return: True if tokens are valid, False otherwise @rtype: boolean """ for token in tokens: if len(token) > 1: return True return False