Example #1
0
def report_urn_ietf_params_xml_ns_caldav_free_busy_query(
        self, request, freebusy):  #@UnusedVariable
    """
    Generate a free-busy REPORT.
    (CalDAV-access-09, section 7.8)
    """
    if not self.isCollection():
        log.error(
            "freebusy report is only allowed on collection resources %s" %
            (self, ))
        raise HTTPError(
            StatusResponse(responsecode.FORBIDDEN,
                           "Not a calendar collection"))

    if freebusy.qname() != (caldavxml.caldav_namespace, "free-busy-query"):
        raise ValueError(
            "{CalDAV:}free-busy-query expected as root element, not %s." %
            (freebusy.sname(), ))

    timerange = freebusy.timerange
    if not timerange.valid():
        raise HTTPError(
            StatusResponse(responsecode.BAD_REQUEST,
                           "Invalid time-range specified"))

    # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
    fbinfo = ([], [], [])

    matchcount = [0]

    accepted_type = bestAcceptType(request.headers.getHeader("accept"),
                                   Component.allowedTypes())
    if accepted_type is None:
        raise HTTPError(
            StatusResponse(responsecode.NOT_ACCEPTABLE,
                           "Cannot generate requested data type"))

    def generateFreeBusyInfo(calresource, uri):  #@UnusedVariable
        """
        Run a free busy report on the specified calendar collection
        accumulating the free busy info for later processing.
        @param calresource: the L{CalDAVResource} for a calendar collection.
        @param uri: the uri for the calendar collecton resource.
        """
        def _gotResult(result):
            matchcount[0] = result
            return True

        d = report_common.generateFreeBusyInfo(request, calresource, fbinfo,
                                               timerange, matchcount[0])
        d.addCallback(_gotResult)
        return d

    # Run report taking depth into account
    try:
        depth = request.headers.getHeader("depth", "0")
        yield report_common.applyToCalendarCollections(
            self, request, request.uri, depth, generateFreeBusyInfo,
            (caldavxml.ReadFreeBusy(), ))
    except NumberOfMatchesWithinLimits:
        log.error("Too many matching components in free-busy report")
        raise HTTPError(
            ErrorResponse(responsecode.FORBIDDEN,
                          davxml.NumberOfMatchesWithinLimits(),
                          "Too many components"))
    except TimeRangeLowerLimit, e:
        raise HTTPError(
            ErrorResponse(
                responsecode.FORBIDDEN, caldavxml.MinDateTime(),
                "Time-range value too far in the past. Must be on or after %s."
                % (str(e.limit), )))
Example #2
0
        reports = self.supportedReports()
        test = lookupElement((namespace, name))
        if not test:
            raise AttributeError()
        test = davxml.Report(test())
        if test not in reports:
            raise AttributeError()
    except AttributeError:
        #
        # Requested report is not supported.
        #
        log.error("Unsupported REPORT {name} for resource {resource} (no method {method})",
                  name=encodeXMLName(namespace, name), resource=self, method=method_name)

        raise HTTPError(ErrorResponse(
            responsecode.FORBIDDEN,
            davxml.SupportedReport(),
            "Report not supported on this resource",
        ))

    #
    # Check authentication and access controls
    #
    privileges = (davxml.Read(),)
    if method_name == "report_urn_ietf_params_xml_ns_caldav_free_busy_query":
        privileges = (caldavxml.ReadFreeBusy(),)
    yield self.authorize(request, privileges)

    result = (yield method(request, doc.root_element))
    returnValue(result)
Example #3
0
def report_urn_ietf_params_xml_ns_caldav_free_busy_query(
        self, request, freebusy):
    """
    Generate a free-busy REPORT.
    (CalDAV-access-09, section 7.8)
    """
    if not self.isCollection():
        log.error(
            "freebusy report is only allowed on collection resources {s!r}",
            s=self)
        raise HTTPError(
            StatusResponse(responsecode.FORBIDDEN,
                           "Not a calendar collection"))

    if freebusy.qname() != (caldavxml.caldav_namespace, "free-busy-query"):
        raise ValueError(
            "{CalDAV:}free-busy-query expected as root element, not %s." %
            (freebusy.sname(), ))

    timerange = freebusy.timerange
    if not timerange.valid():
        raise HTTPError(
            StatusResponse(responsecode.BAD_REQUEST,
                           "Invalid time-range specified"))

    fbset = []

    accepted_type = bestAcceptType(request.headers.getHeader("accept"),
                                   Component.allowedTypes())
    if accepted_type is None:
        raise HTTPError(
            StatusResponse(responsecode.NOT_ACCEPTABLE,
                           "Cannot generate requested data type"))

    def getCalendarList(calresource, uri):  # @UnusedVariable
        """
        Store the calendars that match the query in L{fbset} which will then be used with the
        freebusy query.

        @param calresource: the L{CalDAVResource} for a calendar collection.
        @param uri: the uri for the calendar collection resource.
        """

        fbset.append(calresource._newStoreObject)
        return succeed(True)

    # Run report taking depth into account
    depth = request.headers.getHeader("depth", "0")
    yield report_common.applyToCalendarCollections(
        self, request, request.uri, depth, getCalendarList,
        (caldavxml.ReadFreeBusy(), ))

    # Do the actual freebusy query against the set of matched calendars
    principal = yield self.resourceOwnerPrincipal(request)
    organizer = recipient = LocalCalendarUser(
        principal.canonicalCalendarUserAddress(), principal.record)
    timerange = Period(timerange.start, timerange.end)
    try:
        fbresult = yield FreebusyQuery(
            organizer=organizer, recipient=recipient,
            timerange=timerange).generateAttendeeFreeBusyResponse(fbset=fbset,
                                                                  method=None)
    except NumberOfMatchesWithinLimits:
        log.error("Too many matching components in free-busy report")
        raise HTTPError(
            ErrorResponse(responsecode.FORBIDDEN,
                          davxml.NumberOfMatchesWithinLimits(),
                          "Too many components"))
    except TimeRangeLowerLimit, e:
        raise HTTPError(
            ErrorResponse(
                responsecode.FORBIDDEN, caldavxml.MinDateTime(),
                "Time-range value too far in the past. Must be on or after %s."
                % (str(e.limit), )))
def generateFreeBusyInfo(
    request,
    calresource,
    fbinfo,
    timerange,
    matchtotal,
    excludeuid=None,
    organizer=None,
    organizerPrincipal=None,
    same_calendar_user=False,
    servertoserver=False,
    event_details=None,
):
    """
    Run a free busy report on the specified calendar collection
    accumulating the free busy info for later processing.
    @param request:     the L{IRequest} for the current request.
    @param calresource: the L{CalDAVResource} for a calendar collection.
    @param fbinfo:      the array of busy periods to update.
    @param timerange:   the L{TimeRange} for the query.
    @param matchtotal:  the running total for the number of matches.
    @param excludeuid:  a C{str} containing a UID value to exclude any
        components with that UID from contributing to free-busy.
    @param organizer:   a C{str} containing the value of the ORGANIZER property
        in the VFREEBUSY request.  This is used in conjunction with the UID
        value to process exclusions.
    @param same_calendar_user: a C{bool} indicating whether the calendar user
        requesting the free-busy information is the same as the calendar user
        being targeted.
    @param servertoserver: a C{bool} indicating whether we are doing a local or
        remote lookup request.
    @param event_details: a C{list} into which to store extended VEVENT details if not C{None}
    """

    # First check the privilege on this collection
    # TODO: for server-to-server we bypass this right now as we have no way to authorize external users.
    if not servertoserver:
        try:
            yield calresource.checkPrivileges(request,
                                              (caldavxml.ReadFreeBusy(), ),
                                              principal=organizerPrincipal)
        except AccessDeniedError:
            returnValue(matchtotal)

    # May need organizer principal
    organizer_principal = (yield calresource.principalForCalendarUserAddress(
        organizer)) if organizer else None
    organizer_uid = organizer_principal.principalUID(
    ) if organizer_principal else ""

    # Free busy is per-user
    userPrincipal = (yield calresource.resourceOwnerPrincipal(request))
    if userPrincipal:
        useruid = userPrincipal.principalUID()
    else:
        useruid = ""

    # Get the timezone property from the collection.
    has_prop = (yield calresource.hasProperty(CalendarTimeZone(), request))
    if has_prop:
        tz = (yield calresource.readProperty(CalendarTimeZone(), request))
    else:
        tz = None

    # Look for possible extended free busy information
    rich_options = {
        "organizer": False,
        "delegate": False,
        "resource": False,
    }
    do_event_details = False
    if event_details is not None and organizer_principal is not None and userPrincipal is not None:

        # Check if organizer is attendee
        if organizer_principal == userPrincipal:
            do_event_details = True
            rich_options["organizer"] = True

        # Check if organizer is a delegate of attendee
        proxy = (yield organizer_principal.isProxyFor(userPrincipal))
        if config.Scheduling.Options.DelegeteRichFreeBusy and proxy:
            do_event_details = True
            rich_options["delegate"] = True

        # Check if attendee is room or resource
        if config.Scheduling.Options.RoomResourceRichFreeBusy and userPrincipal.getCUType(
        ) in (
                "RESOURCE",
                "ROOM",
        ):
            do_event_details = True
            rich_options["resource"] = True

    # Try cache
    resources = (yield FBCacheEntry.getCacheEntry(
        calresource, useruid,
        timerange)) if config.EnableFreeBusyCache else None

    if resources is None:

        caching = False
        if config.EnableFreeBusyCache:
            # Log extended item
            if not hasattr(request, "extendedLogItems"):
                request.extendedLogItems = {}
            request.extendedLogItems[
                "fb-uncached"] = request.extendedLogItems.get(
                    "fb-uncached", 0) + 1

            # We want to cache a large range of time based on the current date
            cache_start = normalizeToUTC(DateTime.getToday() + Duration(
                days=0 - config.FreeBusyCacheDaysBack))
            cache_end = normalizeToUTC(DateTime.getToday() + Duration(
                days=config.FreeBusyCacheDaysForward))

            # If the requested timerange would fit in our allowed cache range, trigger the cache creation
            if compareDateTime(timerange.start,
                               cache_start) >= 0 and compareDateTime(
                                   timerange.end, cache_end) <= 0:
                cache_timerange = TimeRange(start=cache_start.getText(),
                                            end=cache_end.getText())
                caching = True

        #
        # What we do is a fake calendar-query for VEVENT/VFREEBUSYs in the specified time-range.
        # We then take those results and merge them into one VFREEBUSY component
        # with appropriate FREEBUSY properties, and return that single item as iCal data.
        #

        # Create fake filter element to match time-range
        filter = caldavxml.Filter(
            caldavxml.ComponentFilter(
                caldavxml.ComponentFilter(
                    cache_timerange if caching else timerange,
                    name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"),
                ),
                name="VCALENDAR",
            ))
        filter = Filter(filter)
        tzinfo = filter.settimezone(tz)

        try:
            resources = yield calresource.search(filter,
                                                 useruid=useruid,
                                                 fbtype=True)
            if caching:
                yield FBCacheEntry.makeCacheEntry(calresource, useruid,
                                                  cache_timerange, resources)
        except IndexedSearchException:
            raise HTTPError(
                StatusResponse(responsecode.INTERNAL_SERVER_ERROR,
                               "Failed freebusy query"))

    else:
        # Log extended item
        if not hasattr(request, "extendedLogItems"):
            request.extendedLogItems = {}
        request.extendedLogItems["fb-cached"] = request.extendedLogItems.get(
            "fb-cached", 0) + 1

        # Determine appropriate timezone (UTC is the default)
        tzinfo = tz.gettimezone() if tz is not None else Timezone(utc=True)

    # We care about separate instances for VEVENTs only
    aggregated_resources = {}
    for name, uid, type, test_organizer, float, start, end, fbtype, transp in resources:
        if transp == 'T' and fbtype != '?':
            fbtype = 'F'
        aggregated_resources.setdefault((
            name,
            uid,
            type,
            test_organizer,
        ), []).append((
            float,
            start,
            end,
            fbtype,
        ))

    for key in aggregated_resources.iterkeys():

        name, uid, type, test_organizer = key

        # Short-cut - if an fbtype exists we can use that
        if type == "VEVENT" and aggregated_resources[key][0][3] != '?':

            matchedResource = False

            # Look at each instance
            for float, start, end, fbtype in aggregated_resources[key]:
                # Ignore free time or unknown
                if fbtype in ('F', '?'):
                    continue

                # Ignore ones of this UID
                if excludeuid:
                    # See if we have a UID match
                    if (excludeuid == uid):
                        test_principal = (
                            yield calresource.principalForCalendarUserAddress(
                                test_organizer)) if test_organizer else None
                        test_uid = test_principal.principalUID(
                        ) if test_principal else ""

                        # Check that ORGANIZER's match (security requirement)
                        if (organizer is None) or (organizer_uid == test_uid):
                            continue
                        # Check for no ORGANIZER and check by same calendar user
                        elif (test_uid == "") and same_calendar_user:
                            continue

                # Apply a timezone to any floating times
                fbstart = parseSQLTimestampToPyCalendar(start)
                if float == 'Y':
                    fbstart.setTimezone(tzinfo)
                else:
                    fbstart.setTimezone(Timezone(utc=True))
                fbend = parseSQLTimestampToPyCalendar(end)
                if float == 'Y':
                    fbend.setTimezone(tzinfo)
                else:
                    fbend.setTimezone(Timezone(utc=True))

                # Clip instance to time range
                clipped = clipPeriod(Period(fbstart, duration=fbend - fbstart),
                                     Period(timerange.start, timerange.end))

                # Double check for overlap
                if clipped:
                    matchedResource = True
                    fbinfo[fbtype_index_mapper.get(fbtype, 0)].append(clipped)

            if matchedResource:
                # Check size of results is within limit
                matchtotal += 1
                if matchtotal > max_number_of_matches:
                    raise NumberOfMatchesWithinLimits(max_number_of_matches)

                # Add extended details
                if do_event_details:
                    child = (yield
                             request.locateChildResource(calresource, name))
                    calendar = (yield child.iCalendarForUser(request))
                    _addEventDetails(calendar, event_details, rich_options,
                                     timerange, tzinfo)

        else:
            child = (yield request.locateChildResource(calresource, name))
            calendar = (yield child.iCalendarForUser(request))

            # The calendar may come back as None if the resource is being changed, or was deleted
            # between our initial index query and getting here. For now we will ignore this error, but in
            # the longer term we need to implement some form of locking, perhaps.
            if calendar is None:
                log.error(
                    "Calendar %s is missing from calendar collection %r" %
                    (name, calresource))
                continue

            # Ignore ones of this UID
            if excludeuid:
                # See if we have a UID match
                if (excludeuid == uid):
                    test_organizer = calendar.getOrganizer()
                    test_principal = (
                        yield calresource.principalForCalendarUserAddress(
                            test_organizer)) if test_organizer else None
                    test_uid = test_principal.principalUID(
                    ) if test_principal else ""

                    # Check that ORGANIZER's match (security requirement)
                    if (organizer is None) or (organizer_uid == test_uid):
                        continue
                    # Check for no ORGANIZER and check by same calendar user
                    elif (test_organizer is None) and same_calendar_user:
                        continue

            if filter.match(calendar, None):
                # Check size of results is within limit
                matchtotal += 1
                if matchtotal > max_number_of_matches:
                    raise NumberOfMatchesWithinLimits(max_number_of_matches)

                if calendar.mainType() == "VEVENT":
                    processEventFreeBusy(calendar, fbinfo, timerange, tzinfo)
                elif calendar.mainType() == "VFREEBUSY":
                    processFreeBusyFreeBusy(calendar, fbinfo, timerange)
                elif calendar.mainType() == "VAVAILABILITY":
                    processAvailabilityFreeBusy(calendar, fbinfo, timerange)
                else:
                    assert "Free-busy query returned unwanted component: %s in %r", (
                        name,
                        calresource,
                    )

                # Add extended details
                if calendar.mainType() == "VEVENT" and do_event_details:
                    child = (yield
                             request.locateChildResource(calresource, name))
                    calendar = (yield child.iCalendarForUser(request))
                    _addEventDetails(calendar, event_details, rich_options,
                                     timerange, tzinfo)

    returnValue(matchtotal)
Example #5
0
    def shareeAccessControlList(self, request, *args, **kwargs):
        """
        Return WebDAV ACLs appropriate for the current user accessing the
        shared collection.  For an "invite" share we take the privilege granted
        to the sharee in the invite and map that to WebDAV ACLs.  For a
        "direct" share, if it is a wiki collection we map the wiki privileges
        into WebDAV ACLs, otherwise we use whatever privileges exist on the
        underlying shared collection.

        @param request: the request used to locate the owner resource.
        @type request: L{txweb2.iweb.IRequest}

        @param args: The arguments for
            L{txweb2.dav.idav.IDAVResource.accessControlList}

        @param kwargs: The keyword arguments for
            L{txweb2.dav.idav.IDAVResource.accessControlList}, plus
            keyword-only arguments.

        @return: the appropriate WebDAV ACL for the sharee
        @rtype: L{davxml.ACL}
        """

        assert self._isShareeResource, "Only call this for a sharee resource"
        assert self.isCalendarCollection() or self.isAddressBookCollection(
        ), "Only call this for a address book or calendar resource"

        sharee = yield self.principalForUID(
            self._newStoreObject.viewerHome().uid())
        access = yield self._checkAccessControl()

        if access == "original" and not self._newStoreObject.ownerHome(
        ).external():
            original = (yield request.locateResource(self._share_url))
            result = (yield original.accessControlList(request, *args,
                                                       **kwargs))
            returnValue(result)

        # Direct shares use underlying privileges of shared collection
        userprivs = []
        if access in (
                "read-only",
                "read-write",
        ):
            userprivs.append(element.Privilege(element.Read()))
            userprivs.append(element.Privilege(element.ReadACL()))
            userprivs.append(
                element.Privilege(element.ReadCurrentUserPrivilegeSet()))
        if access in ("read-only", ):
            userprivs.append(element.Privilege(element.WriteProperties()))
        if access in ("read-write", ):
            userprivs.append(element.Privilege(element.Write()))
        proxyprivs = list(userprivs)
        try:
            proxyprivs.remove(element.Privilege(element.ReadACL()))
        except ValueError:
            # If wiki says no-access then ReadACL won't be in the list
            pass

        aces = (
            # Inheritable specific access for the resource's associated principal.
            element.ACE(
                element.Principal(element.HRef(sharee.principalURL())),
                element.Grant(*userprivs),
                element.Protected(),
                TwistedACLInheritable(),
            ), )

        if self.isCalendarCollection():
            aces += (
                # Inheritable CALDAV:read-free-busy access for authenticated users.
                element.ACE(
                    element.Principal(element.Authenticated()),
                    element.Grant(element.Privilege(caldavxml.ReadFreeBusy())),
                    TwistedACLInheritable(),
                ), )

        # Give read access to config.ReadPrincipals
        aces += config.ReadACEs

        # Give all access to config.AdminPrincipals
        aces += config.AdminACEs

        if self.isCalendarCollection() and config.EnableProxyPrincipals:
            aces += (
                # DAV:read/DAV:read-current-user-privilege-set access for this principal's calendar-proxy-read users.
                element.ACE(
                    element.Principal(
                        element.HRef(
                            joinURL(sharee.principalURL(),
                                    "calendar-proxy-read/"))),
                    element.Grant(
                        element.Privilege(element.Read()),
                        element.Privilege(
                            element.ReadCurrentUserPrivilegeSet()),
                        element.Privilege(element.WriteProperties()),
                    ),
                    element.Protected(),
                    TwistedACLInheritable(),
                ),
                # DAV:read/DAV:read-current-user-privilege-set/DAV:write access for this principal's calendar-proxy-write users.
                element.ACE(
                    element.Principal(
                        element.HRef(
                            joinURL(sharee.principalURL(),
                                    "calendar-proxy-write/"))),
                    element.Grant(*proxyprivs),
                    element.Protected(),
                    TwistedACLInheritable(),
                ),
            )

        returnValue(element.ACL(*aces))