Example #1
0
    def __init__(self, path, *args, **kwargs):
        super(RootResource, self).__init__(path, *args, **kwargs)

        if config.EnableSACLs:
            self.useSacls = True

        self.contentFilters = []

        if (
            config.EnableResponseCache and
            config.Memcached.Pools.Default.ClientEnabled
        ):
            self.responseCache = MemcacheResponseCache(self.fp)

            # These class attributes need to be setup with our memcache\
            # notifier
            DirectoryPrincipalResource.cacheNotifierFactory = (
                MemcacheChangeNotifier
            )
        else:
            self.responseCache = DisabledCache()

        if config.ResponseCompression:
            from txweb2.filter import gzip
            self.contentFilters.append((gzip.gzipfilter, True))
Example #2
0
    def __init__(self, path, *args, **kwargs):
        super(RootResource, self).__init__(path, *args, **kwargs)

        if config.EnableSACLs:
            self.useSacls = True

        self.contentFilters = []

        if (
            config.EnableResponseCache and
            config.Memcached.Pools.Default.ClientEnabled
        ):
            self.responseCache = MemcacheResponseCache(self.fp)

            # These class attributes need to be setup with our memcache\
            # notifier
            DirectoryPrincipalResource.cacheNotifierFactory = (
                MemcacheChangeNotifier
            )
        else:
            self.responseCache = DisabledCache()

        if config.ResponseCompression:
            from txweb2.filter import gzip
            self.contentFilters.append((gzip.gzipfilter, True))
Example #3
0
class RootResource(
    ReadOnlyResourceMixIn, DirectoryPrincipalPropertySearchMixIn,
    CalDAVComplianceMixIn, DAVFile
):
    """
    A special root resource that contains support checking SACLs
    as well as adding responseFilters.
    """

    useSacls = False

    # Mapping of top-level resource paths to SACLs.  If a request path
    # starts with any of these, then the list of SACLs are checked.  If the
    # request path does not start with any of these, then no SACLs are checked.
    saclMap = {
        "addressbooks": ("addressbook",),
        "calendars": ("calendar",),
        "directory": ("addressbook",),
        "principals": ("addressbook", "calendar"),
        "webcal": ("calendar",),
    }

    # If a top-level resource path starts with any of these, an unauthenticated
    # request is redirected to the auth url (config.WebCalendarAuthPath)
    authServiceMap = {
        "webcal": True,
    }

    def __init__(self, path, *args, **kwargs):
        super(RootResource, self).__init__(path, *args, **kwargs)

        if config.EnableSACLs:
            self.useSacls = True

        self.contentFilters = []

        if (
            config.EnableResponseCache and
            config.Memcached.Pools.Default.ClientEnabled
        ):
            self.responseCache = MemcacheResponseCache(self.fp)

            # These class attributes need to be setup with our memcache\
            # notifier
            DirectoryPrincipalResource.cacheNotifierFactory = MemcacheChangeNotifier
            CachingDelegates.cacheNotifier = MemcacheChangeNotifier(None, cacheHandle="PrincipalToken")
        else:
            self.responseCache = DisabledCache()

        if config.ResponseCompression:
            from txweb2.filter import gzip
            self.contentFilters.append((gzip.gzipfilter, True))

    def deadProperties(self):
        if not hasattr(self, "_dead_properties"):
            # Get the property store from super
            deadProperties = (
                namedClass(config.RootResourcePropStoreClass)(self)
            )

            # Wrap the property store in a memory store
            if isinstance(deadProperties, xattrPropertyStore):
                deadProperties = CachingPropertyStore(deadProperties)

            self._dead_properties = deadProperties

        return self._dead_properties

    def defaultAccessControlList(self):
        return succeed(config.RootResourceACL)

    @inlineCallbacks
    def checkSACL(self, request):
        """
        Check SACLs against the current request
        """

        topLevel = request.path.strip("/").split("/")[0]
        saclServices = self.saclMap.get(topLevel, None)
        if not saclServices:
            returnValue(True)

        try:
            authnUser, authzUser = yield self.authenticate(request)
        except Exception:
            response = (yield UnauthorizedResponse.makeResponse(
                request.credentialFactories,
                request.remoteAddr
            ))
            raise HTTPError(response)

        # SACLs are enabled in the plist, but there may not actually
        # be a SACL group assigned to this service.  Let's see if
        # unauthenticated users are allowed by calling CheckSACL
        # with an empty string.
        if authzUser is None:
            for saclService in saclServices:
                if checkSACL("", saclService):
                    # No group actually exists for this SACL, so allow
                    # unauthenticated access
                    returnValue(True)
            # There is a SACL group for at least one of the SACLs, so no
            # unauthenticated access
            response = (yield UnauthorizedResponse.makeResponse(
                request.credentialFactories,
                request.remoteAddr
            ))
            log.info("Unauthenticated user denied by SACLs")
            raise HTTPError(response)

        # Cache the authentication details
        request.authnUser = authnUser
        request.authzUser = authzUser

        # Figure out the "username" from the davxml.Principal object
        username = authzUser.record.shortNames[0]

        access = False
        for saclService in saclServices:
            if checkSACL(username, saclService):
                # Access is allowed
                access = True
                break

        # Mark SACLs as having been checked so we can avoid doing it
        # multiple times
        request.checkedSACL = True

        if access:
            returnValue(True)

        log.warn(
            "User {user!r} is not enabled with the {sacl!r} SACL(s)",
            user=username, sacl=saclServices
        )
        raise HTTPError(responsecode.FORBIDDEN)

    @inlineCallbacks
    def locateChild(self, request, segments):

        for filter in self.contentFilters:
            request.addResponseFilter(filter[0], atEnd=filter[1])

        # Examine cookies for wiki auth token; if there, ask the paired wiki
        # server for the corresponding record name.  If that maps to a
        # principal, assign that to authnuser.

        # Also, certain non-browser clients send along the wiki auth token
        # sometimes, so we now also look for the presence of x-requested-with
        # header that the webclient sends.  However, in the case of a GET on
        # /webcal that header won't be sent so therefore we allow wiki auth
        # for any path in the authServiceMap even if that header is missing.
        allowWikiAuth = False
        topLevel = request.path.strip("/").split("/")[0]
        if self.authServiceMap.get(topLevel, False):
            allowWikiAuth = True

        if not hasattr(request, "checkedWiki"):
            # Only do this once per request
            request.checkedWiki = True

            wikiConfig = config.Authentication.Wiki
            cookies = request.headers.getHeader("cookie")
            requestedWith = request.headers.hasHeader("x-requested-with")
            if (
                wikiConfig["Enabled"] and
                (requestedWith or allowWikiAuth) and
                cookies is not None
            ):
                for cookie in cookies:
                    if cookie.name == wikiConfig["Cookie"]:
                        token = cookie.value
                        break
                else:
                    token = None

                if token is not None and token != "unauthenticated":
                    log.debug(
                        "Wiki sessionID cookie value: {token}", token=token
                    )

                    try:
                        uid = yield uidForAuthToken(token, wikiConfig["EndpointDescriptor"])
                        if uid == "unauthenticated":
                            uid = None

                    except WebError as w:
                        uid = None
                        # FORBIDDEN status means it's an unknown token
                        if int(w.status) == responsecode.NOT_FOUND:
                            log.debug(
                                "Unknown wiki token: {token}", token=token
                            )
                        else:
                            log.error(
                                "Failed to look up wiki token {token}: {msg}",
                                token=token,
                                msg=w.message
                            )

                    except Exception as e:
                        log.error(
                            "Failed to look up wiki token: {error}",
                            error=e
                        )
                        uid = None

                    if uid is not None:
                        log.debug(
                            "Wiki lookup returned uid: {uid}", uid=uid
                        )
                        principal = yield self.principalForUID(request, uid)

                        if principal:
                            log.debug(
                                "Wiki-authenticated principal {uid} "
                                "being assigned to authnUser and authzUser",
                                uid=uid
                            )
                            request.authzUser = request.authnUser = principal

        if not hasattr(request, "authzUser") and config.WebCalendarAuthPath:
            topLevel = request.path.strip("/").split("/")[0]
            if self.authServiceMap.get(topLevel, False):
                # We've not been authenticated and the auth service is enabled
                # for this resource, so redirect.

                # Use config.ServerHostName if no x-forwarded-host header,
                # otherwise use the final hostname in x-forwarded-host.
                host = request.headers.getRawHeaders(
                    "x-forwarded-host",
                    [config.ServerHostName]
                )[-1].split(",")[-1].strip()
                port = 443 if (config.EnableSSL or config.BehindTLSProxy) else 80
                scheme = "https" if config.EnableSSL else "http"

                response = RedirectResponse(
                    request.unparseURL(
                        host=host,
                        port=port,
                        scheme=scheme,
                        path=config.WebCalendarAuthPath,
                        querystring="redirect={}://{}{}".format(
                            scheme,
                            host,
                            request.path
                        )
                    ),
                    temporary=True
                )
                raise HTTPError(response)

        # We don't want the /inbox resource to pay attention to SACLs because
        # we just want it to use the hard-coded ACL for the imip reply user.
        # The /timezones resource is used by the wiki web calendar, so open
        # up that resource.
        if segments[0] in ("inbox", "timezones"):
            request.checkedSACL = True

        elif (
            (
                len(segments) > 2 and
                segments[0] in ("calendars", "principals") and
                (
                    segments[1] == "wikis" or
                    (
                        segments[1] == "__uids__" and
                        segments[2].startswith(WikiDirectoryService.uidPrefix)
                    )
                )
            )
        ):
            # This is a wiki-related calendar resource. SACLs are not checked.
            request.checkedSACL = True

            # The authzuser value is set to that of the wiki principal if
            # not already set.
            if not hasattr(request, "authzUser") and segments[2]:
                wikiUid = None
                if segments[1] == "wikis":
                    wikiUid = "{}{}".format(WikiDirectoryService.uidPrefix, segments[2])
                else:
                    wikiUid = segments[2]
                if wikiUid:
                    log.debug(
                        "Wiki principal {name} being assigned to authzUser",
                        name=wikiUid
                    )
                    request.authzUser = yield self.principalForUID(request, wikiUid)

        elif (
            self.useSacls and
            not hasattr(request, "checkedSACL")
        ):
            yield self.checkSACL(request)

        if config.RejectClients:
            #
            # Filter out unsupported clients
            #
            agent = request.headers.getHeader("user-agent")
            if agent is not None:
                for reject in config.RejectClients:
                    if reject.search(agent) is not None:
                        log.info("Rejecting user-agent: {agent}", agent=agent)
                        raise HTTPError(StatusResponse(
                            responsecode.FORBIDDEN,
                            "Your client software ({}) is not allowed to "
                            "access this service."
                            .format(agent)
                        ))

        if not hasattr(request, "authnUser"):
            try:
                authnUser, authzUser = yield self.authenticate(request)
                request.authnUser = authnUser
                request.authzUser = authzUser
            except (UnauthorizedLogin, LoginFailed):
                response = yield UnauthorizedResponse.makeResponse(
                    request.credentialFactories,
                    request.remoteAddr
                )
                raise HTTPError(response)

        if (
            config.EnableResponseCache and
            request.method == "PROPFIND" and
            not getattr(request, "notInCache", False) and
            len(segments) > 1
        ):

            try:
                if not getattr(request, "checkingCache", False):
                    request.checkingCache = True
                    response = yield self.responseCache.getResponseForRequest(
                        request
                    )
                    if response is None:
                        request.notInCache = True
                        raise KeyError("Not found in cache.")

                    returnValue((_CachedResponseResource(response), []))
            except KeyError:
                pass

        child = yield super(RootResource, self).locateChild(
            request, segments
        )
        returnValue(child)

    @inlineCallbacks
    def principalForUID(self, request, uid):
        principal = None
        directory = request.site.resource.getDirectory()
        record = yield directory.recordWithUID(uid)
        if record is not None:
            username = record.shortNames[0]
            log.debug(
                "Wiki user record for user {user}: {record}",
                user=username, record=record
            )
            for collection in self.principalCollections():
                principal = yield collection.principalForRecord(record)
                if principal is not None:
                    break

        returnValue(principal)

    def http_COPY(self, request):
        return responsecode.FORBIDDEN

    def http_MOVE(self, request):
        return responsecode.FORBIDDEN

    def http_DELETE(self, request):
        return responsecode.FORBIDDEN
Example #4
0
class RootResource(
    ReadOnlyResourceMixIn, DirectoryPrincipalPropertySearchMixIn,
    CalDAVComplianceMixIn, DAVFile
):
    """
    A special root resource that contains support checking SACLs
    as well as adding responseFilters.
    """

    useSacls = False

    # Mapping of top-level resource paths to SACLs.  If a request path
    # starts with any of these, then the list of SACLs are checked.  If the
    # request path does not start with any of these, then no SACLs are checked.
    saclMap = {
        "addressbooks": ("addressbook",),
        "calendars": ("calendar",),
        "directory": ("addressbook",),
        "principals": ("addressbook", "calendar"),
        "webcal": ("calendar",),
    }

    # If a top-level resource path starts with any of these, an unauthenticated
    # request is redirected to the auth url (config.WebCalendarAuthPath)
    authServiceMap = {
        "webcal": True,
    }

    def __init__(self, path, *args, **kwargs):
        super(RootResource, self).__init__(path, *args, **kwargs)

        if config.EnableSACLs:
            self.useSacls = True

        self.contentFilters = []

        if (
            config.EnableResponseCache and
            config.Memcached.Pools.Default.ClientEnabled
        ):
            self.responseCache = MemcacheResponseCache(self.fp)

            # These class attributes need to be setup with our memcache\
            # notifier
            DirectoryPrincipalResource.cacheNotifierFactory = (
                MemcacheChangeNotifier
            )
        else:
            self.responseCache = DisabledCache()

        if config.ResponseCompression:
            from txweb2.filter import gzip
            self.contentFilters.append((gzip.gzipfilter, True))


    def deadProperties(self):
        if not hasattr(self, "_dead_properties"):
            # Get the property store from super
            deadProperties = (
                namedClass(config.RootResourcePropStoreClass)(self)
            )

            # Wrap the property store in a memory store
            if isinstance(deadProperties, xattrPropertyStore):
                deadProperties = CachingPropertyStore(deadProperties)

            self._dead_properties = deadProperties

        return self._dead_properties


    def defaultAccessControlList(self):
        return succeed(config.RootResourceACL)


    @inlineCallbacks
    def checkSACL(self, request):
        """
        Check SACLs against the current request
        """

        topLevel = request.path.strip("/").split("/")[0]
        saclServices = self.saclMap.get(topLevel, None)
        if not saclServices:
            returnValue(True)

        try:
            authnUser, authzUser = yield self.authenticate(request)
        except Exception:
            response = (yield UnauthorizedResponse.makeResponse(
                request.credentialFactories,
                request.remoteAddr
            ))
            raise HTTPError(response)

        # SACLs are enabled in the plist, but there may not actually
        # be a SACL group assigned to this service.  Let's see if
        # unauthenticated users are allowed by calling CheckSACL
        # with an empty string.
        if authzUser is None:
            for saclService in saclServices:
                if checkSACL("", saclService):
                    # No group actually exists for this SACL, so allow
                    # unauthenticated access
                    returnValue(True)
            # There is a SACL group for at least one of the SACLs, so no
            # unauthenticated access
            response = (yield UnauthorizedResponse.makeResponse(
                request.credentialFactories,
                request.remoteAddr
            ))
            log.info("Unauthenticated user denied by SACLs")
            raise HTTPError(response)

        # Cache the authentication details
        request.authnUser = authnUser
        request.authzUser = authzUser

        # Figure out the "username" from the davxml.Principal object
        username = authzUser.record.shortNames[0]

        access = False
        for saclService in saclServices:
            if checkSACL(username, saclService):
                # Access is allowed
                access = True
                break

        # Mark SACLs as having been checked so we can avoid doing it
        # multiple times
        request.checkedSACL = True

        if access:
            returnValue(True)

        log.warn(
            "User {user!r} is not enabled with the {sacl!r} SACL(s)",
            user=username, sacl=saclServices
        )
        raise HTTPError(responsecode.FORBIDDEN)


    @inlineCallbacks
    def locateChild(self, request, segments):

        for filter in self.contentFilters:
            request.addResponseFilter(filter[0], atEnd=filter[1])

        # Examine cookies for wiki auth token; if there, ask the paired wiki
        # server for the corresponding record name.  If that maps to a
        # principal, assign that to authnuser.

        # Also, certain non-browser clients send along the wiki auth token
        # sometimes, so we now also look for the presence of x-requested-with
        # header that the webclient sends.  However, in the case of a GET on
        # /webcal that header won't be sent so therefore we allow wiki auth
        # for any path in the authServiceMap even if that header is missing.
        allowWikiAuth = False
        topLevel = request.path.strip("/").split("/")[0]
        if self.authServiceMap.get(topLevel, False):
            allowWikiAuth = True

        if not hasattr(request, "checkedWiki"):
            # Only do this once per request
            request.checkedWiki = True

            wikiConfig = config.Authentication.Wiki
            cookies = request.headers.getHeader("cookie")
            requestedWith = request.headers.hasHeader("x-requested-with")
            if (
                wikiConfig["Enabled"] and
                (requestedWith or allowWikiAuth) and
                cookies is not None
            ):
                for cookie in cookies:
                    if cookie.name == wikiConfig["Cookie"]:
                        token = cookie.value
                        break
                else:
                    token = None

                if token is not None and token != "unauthenticated":
                    log.debug(
                        "Wiki sessionID cookie value: {token}", token=token
                    )

                    try:
                        uid = yield uidForAuthToken(token)
                        if uid == "unauthenticated":
                            uid = None

                    except WebError as w:
                        uid = None
                        # FORBIDDEN status means it's an unknown token
                        if int(w.status) == responsecode.NOT_FOUND:
                            log.debug(
                                "Unknown wiki token: {token}", token=token
                            )
                        else:
                            log.error(
                                "Failed to look up wiki token {token}: "
                                "{message}",
                                token=token, message=w.message
                            )

                    except Exception as e:
                        log.error(
                            "Failed to look up wiki token: {error}",
                            error=e
                        )
                        uid = None

                    if uid is not None:
                        log.debug(
                            "Wiki lookup returned uid: {uid}", uid=uid
                        )
                        principal = yield self.principalForUID(request, uid)

                        if principal:
                            log.debug(
                                "Wiki-authenticated principal {uid} "
                                "being assigned to authnUser and authzUser",
                                uid=uid
                            )
                            request.authzUser = request.authnUser = principal

        if not hasattr(request, "authzUser") and config.WebCalendarAuthPath:
            topLevel = request.path.strip("/").split("/")[0]
            if self.authServiceMap.get(topLevel, False):
                # We've not been authenticated and the auth service is enabled
                # for this resource, so redirect.

                # Use config.ServerHostName if no x-forwarded-host header,
                # otherwise use the final hostname in x-forwarded-host.
                host = request.headers.getRawHeaders(
                    "x-forwarded-host",
                    [config.ServerHostName]
                )[-1].split(",")[-1].strip()
                port = 443 if config.EnableSSL else 80
                scheme = "https" if config.EnableSSL else "http"

                response = RedirectResponse(
                    request.unparseURL(
                        host=host,
                        port=port,
                        scheme=scheme,
                        path=config.WebCalendarAuthPath,
                        querystring="redirect={}://{}{}".format(
                            scheme,
                            host,
                            request.path
                        )
                    ),
                    temporary=True
                )
                raise HTTPError(response)

        # We don't want the /inbox resource to pay attention to SACLs because
        # we just want it to use the hard-coded ACL for the imip reply user.
        # The /timezones resource is used by the wiki web calendar, so open
        # up that resource.
        if segments[0] in ("inbox", "timezones"):
            request.checkedSACL = True

        elif (
            (
                len(segments) > 2 and
                segments[0] in ("calendars", "principals") and
                (
                    segments[1] == "wikis" or
                    (
                        segments[1] == "__uids__" and
                        segments[2].startswith(WikiDirectoryService.uidPrefix)
                    )
                )
            )
        ):
            # This is a wiki-related calendar resource. SACLs are not checked.
            request.checkedSACL = True

            # The authzuser value is set to that of the wiki principal if
            # not already set.
            if not hasattr(request, "authzUser") and segments[2]:
                wikiUid = None
                if segments[1] == "wikis":
                    wikiUid = "{}{}".format(WikiDirectoryService.uidPrefix, segments[2])
                else:
                    wikiUid = segments[2]
                if wikiUid:
                    log.debug(
                        "Wiki principal {name} being assigned to authzUser",
                        name=wikiUid
                    )
                    request.authzUser = yield self.principalForUID(request, wikiUid)

        elif (
            self.useSacls and
            not hasattr(request, "checkedSACL")
        ):
            yield self.checkSACL(request)

        if config.RejectClients:
            #
            # Filter out unsupported clients
            #
            agent = request.headers.getHeader("user-agent")
            if agent is not None:
                for reject in config.RejectClients:
                    if reject.search(agent) is not None:
                        log.info("Rejecting user-agent: {agent}", agent=agent)
                        raise HTTPError(StatusResponse(
                            responsecode.FORBIDDEN,
                            "Your client software ({}) is not allowed to "
                            "access this service."
                            .format(agent)
                        ))

        if not hasattr(request, "authnUser"):
            try:
                authnUser, authzUser = yield self.authenticate(request)
                request.authnUser = authnUser
                request.authzUser = authzUser
            except (UnauthorizedLogin, LoginFailed):
                response = yield UnauthorizedResponse.makeResponse(
                    request.credentialFactories,
                    request.remoteAddr
                )
                raise HTTPError(response)

        if (
            config.EnableResponseCache and
            request.method == "PROPFIND" and
            not getattr(request, "notInCache", False) and
            len(segments) > 1
        ):

            try:
                if not getattr(request, "checkingCache", False):
                    request.checkingCache = True
                    response = yield self.responseCache.getResponseForRequest(
                        request
                    )
                    if response is None:
                        request.notInCache = True
                        raise KeyError("Not found in cache.")

                    returnValue((_CachedResponseResource(response), []))
            except KeyError:
                pass

        child = yield super(RootResource, self).locateChild(
            request, segments
        )
        returnValue(child)


    @inlineCallbacks
    def principalForUID(self, request, uid):
        principal = None
        directory = request.site.resource.getDirectory()
        record = yield directory.recordWithUID(uid)
        if record is not None:
            username = record.shortNames[0]
            log.debug(
                "Wiki user record for user {user}: {record}",
                user=username, record=record
            )
            for collection in self.principalCollections():
                principal = yield collection.principalForRecord(record)
                if principal is not None:
                    break

        returnValue(principal)


    def http_COPY(self, request):
        return responsecode.FORBIDDEN


    def http_MOVE(self, request):
        return responsecode.FORBIDDEN


    def http_DELETE(self, request):
        return responsecode.FORBIDDEN