Exemple #1
0
        def _wrapped_func_arguments(request, *args, **kwargs):
            # type: (HttpRequest, *Any, **Any) -> HttpResponse
            # First try block attempts to get the credentials we need to do authentication
            try:
                # Grab the base64-encoded authentication string, decode it, and split it into
                # the email and API key
                auth_type, credentials = request.META['HTTP_AUTHORIZATION'].split()
                # case insensitive per RFC 1945
                if auth_type.lower() != "basic":
                    return json_error(_("This endpoint requires HTTP basic authentication."))
                role, api_key = base64.b64decode(force_bytes(credentials)).decode('utf-8').split(":")
            except ValueError:
                return json_unauthorized(_("Invalid authorization header for basic auth"))
            except KeyError:
                return json_unauthorized("Missing authorization header for basic auth")

            # Now we try to do authentication or die
            try:
                # profile is a Union[UserProfile, RemoteZulipServer]
                profile = validate_api_key(request, role, api_key, is_webhook)
            except JsonableError as e:
                return json_unauthorized(e.error)
            request.user = profile
            if is_remote_server(role):
                assert isinstance(profile, RemoteZulipServer)  # type: ignore # https://github.com/python/mypy/issues/2957
                request._email = "zulip-server:" + role
                profile.rate_limits = ""
                process_client(request, profile, remote_server_request=True)
            else:
                assert isinstance(profile, UserProfile)  # type: ignore # https://github.com/python/mypy/issues/2957
                request._email = profile.email
                process_client(request, profile)
            # Apply rate limiting
            return rate_limit()(view_func)(request, profile, *args, **kwargs)
Exemple #2
0
        def _wrapped_func_arguments(request, *args, **kwargs):
            # type: (HttpRequest, *Any, **Any) -> HttpResponse
            # First try block attempts to get the credentials we need to do authentication
            try:
                # Grab the base64-encoded authentication string, decode it, and split it into
                # the email and API key
                auth_type, credentials = request.META['HTTP_AUTHORIZATION'].split()
                # case insensitive per RFC 1945
                if auth_type.lower() != "basic":
                    return json_error(_("Only Basic authentication is supported."))
                role, api_key = base64.b64decode(force_bytes(credentials)).decode('utf-8').split(":")
            except ValueError:
                json_error(_("Invalid authorization header for basic auth"))
            except KeyError:
                return json_unauthorized("Missing authorization header for basic auth")

            # Now we try to do authentication or die
            try:
                # Could be a UserProfile or a Deployment
                profile = validate_api_key(role, api_key, is_webhook)
            except JsonableError as e:
                return json_unauthorized(e.error)
            request.user = profile
            process_client(request, profile)
            if isinstance(profile, UserProfile):
                request._email = profile.email
            else:
                assert isinstance(profile, Deployment) # type: ignore # https://github.com/python/mypy/issues/1720#issuecomment-228596830
                request._email = "deployment:" + role
                profile.rate_limits = ""
            # Apply rate limiting
            return rate_limit()(view_func)(request, profile, *args, **kwargs)
Exemple #3
0
        def _wrapped_func_arguments(request, *args, **kwargs):
            # type: (HttpRequest, *Any, **Any) -> HttpResponse
            # First try block attempts to get the credentials we need to do authentication
            try:
                # Grab the base64-encoded authentication string, decode it, and split it into
                # the email and API key
                auth_type, credentials = request.META[
                    'HTTP_AUTHORIZATION'].split()
                # case insensitive per RFC 1945
                if auth_type.lower() != "basic":
                    return json_error(
                        _("This endpoint requires HTTP basic authentication."))
                role, api_key = base64.b64decode(
                    force_bytes(credentials)).decode('utf-8').split(":")
            except ValueError:
                return json_unauthorized(
                    _("Invalid authorization header for basic auth"))
            except KeyError:
                return json_unauthorized(
                    "Missing authorization header for basic auth")

            # Now we try to do authentication or die
            try:
                # profile is a Union[UserProfile, RemoteZulipServer]
                profile = validate_api_key(request, role, api_key, is_webhook)
            except JsonableError as e:
                return json_unauthorized(e.msg)
            # Apply rate limiting
            return rate_limit()(view_func)(request, profile, *args, **kwargs)
Exemple #4
0
    def process_exception(self, request: HttpRequest,
                          exception: Exception) -> Optional[HttpResponse]:
        if isinstance(exception, MissingAuthenticationError):
            if "text/html" in request.headers.get("Accept", ""):
                # If this looks like a request from a top-level page in a
                # browser, send the user to the login page.
                #
                # TODO: The next part is a bit questionable; it will
                # execute the likely intent for intentionally visiting
                # an API endpoint without authentication in a browser,
                # but that's an unlikely to be done intentionally often.
                return HttpResponseRedirect(
                    f"{settings.HOME_NOT_LOGGED_IN}?{urlencode({'next': request.path})}"
                )
            if request.path.startswith("/api"):
                # For API routes, ask for HTTP basic auth (email:apiKey).
                return json_unauthorized()
            else:
                # For /json routes, ask for session authentication.
                return json_unauthorized(www_authenticate="session")

        if isinstance(exception, JsonableError):
            return json_response_from_error(exception)
        if RequestNotes.get_notes(
                request).error_format == "JSON" and not settings.TEST_SUITE:
            capture_exception(exception)
            json_error_logger = logging.getLogger(
                "zerver.middleware.json_error_handler")
            json_error_logger.error(traceback.format_exc(),
                                    extra=dict(request=request))
            return json_response(res_type="error",
                                 msg=_("Internal server error"),
                                 status=500)
        return None
Exemple #5
0
    def process_exception(self, request: HttpRequest,
                          exception: Exception) -> Optional[HttpResponse]:
        if isinstance(exception, MissingAuthenticationError):
            if 'text/html' in request.META.get('HTTP_ACCEPT', ''):
                # If this looks like a request from a top-level page in a
                # browser, send the user to the login page.
                #
                # TODO: The next part is a bit questionable; it will
                # execute the likely intent for intentionally visiting
                # an API endpoint without authentication in a browser,
                # but that's an unlikely to be done intentionally often.
                return HttpResponseRedirect(
                    f'{settings.HOME_NOT_LOGGED_IN}?next={request.path}')
            if request.path.startswith("/api"):
                # For API routes, ask for HTTP basic auth (email:apiKey).
                return json_unauthorized()
            else:
                # For /json routes, ask for session authentication.
                return json_unauthorized(www_authenticate='session')

        if isinstance(exception, JsonableError):
            return json_response_from_error(exception)
        if request.error_format == "JSON":
            capture_exception(exception)
            json_error_logger = logging.getLogger(
                "zerver.middleware.json_error_handler")
            json_error_logger.error(traceback.format_exc(),
                                    extra=dict(request=request))
            return json_error(_("Internal server error"), status=500)
        return None
Exemple #6
0
        def _wrapped_func_arguments(request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
            # First try block attempts to get the credentials we need to do authentication
            try:
                # Grab the base64-encoded authentication string, decode it, and split it into
                # the email and API key
                auth_type, credentials = request.META['HTTP_AUTHORIZATION'].split()
                # case insensitive per RFC 1945
                if auth_type.lower() != "basic":
                    return json_error(_("This endpoint requires HTTP basic authentication."))
                role, api_key = base64.b64decode(credentials).decode('utf-8').split(":")
            except ValueError:
                return json_unauthorized(_("Invalid authorization header for basic auth"))
            except KeyError:
                return json_unauthorized(_("Missing authorization header for basic auth"))

            # Now we try to do authentication or die
            try:
                # profile is a Union[UserProfile, RemoteZulipServer]
                profile = validate_api_key(request, role, api_key,
                                           is_webhook=is_webhook or webhook_client_name is not None,
                                           client_name=full_webhook_client_name(webhook_client_name))
            except JsonableError as e:
                return json_unauthorized(e.msg)
            try:
                # Apply rate limiting
                return rate_limit()(view_func)(request, profile, *args, **kwargs)
            except Exception as err:
                if is_webhook or webhook_client_name is not None:
                    request_body = request.POST.get('payload')
                    if request_body is not None:
                        log_exception_to_webhook_logger(request, profile,
                                                        request_body=request_body)
                raise err
Exemple #7
0
        def _wrapped_func_arguments(request, *args, **kwargs):
            # type: (HttpRequest, *Any, **Any) -> HttpResponse
            # First try block attempts to get the credentials we need to do authentication
            try:
                # Grab the base64-encoded authentication string, decode it, and split it into
                # the email and API key
                auth_type, credentials = request.META['HTTP_AUTHORIZATION'].split()
                # case insensitive per RFC 1945
                if auth_type.lower() != "basic":
                    return json_error(_("This endpoint requires HTTP basic authentication."))
                role, api_key = base64.b64decode(force_bytes(credentials)).decode('utf-8').split(":")
            except ValueError:
                return json_unauthorized(_("Invalid authorization header for basic auth"))
            except KeyError:
                return json_unauthorized("Missing authorization header for basic auth")

            # Now we try to do authentication or die
            try:
                # profile is a Union[UserProfile, RemoteZulipServer]
                profile = validate_api_key(request, role, api_key, is_webhook)
            except JsonableError as e:
                return json_unauthorized(e.error)
            request.user = profile
            if is_remote_server(role):
                assert isinstance(profile, RemoteZulipServer)  # type: ignore # https://github.com/python/mypy/issues/2957
                request._email = "zulip-server:" + role
                profile.rate_limits = ""
                process_client(request, profile, remote_server_request=True)
            else:
                assert isinstance(profile, UserProfile)  # type: ignore # https://github.com/python/mypy/issues/2957
                request._email = profile.email
                process_client(request, profile)
            # Apply rate limiting
            return rate_limit()(view_func)(request, profile, *args, **kwargs)
Exemple #8
0
    def _wrapped_view_func(request, *args, **kwargs):
        # First try block attempts to get the credentials we need to do authentication
        try:
            # Grab the base64-encoded authentication string, decode it, and split it into
            # the email and API key
            auth_type, encoded_value = request.META['HTTP_AUTHORIZATION'].split()
            # case insensitive per RFC 1945
            if auth_type.lower() != "basic":
                return json_error("Only Basic authentication is supported.")
            role, api_key = base64.b64decode(encoded_value).split(":")
        except ValueError:
            return json_error("Invalid authorization header for basic auth")
        except KeyError:
            return json_unauthorized("Missing authorization header for basic auth")

        # Now we try to do authentication or die
        try:
            # Could be a UserProfile or a Deployment
            profile = validate_api_key(role, api_key)
        except JsonableError as e:
            return json_unauthorized(e.error)
        request.user = profile
        process_client(request, profile)
        if isinstance(profile, UserProfile):
            request._email = profile.email
        else:
            request._email = "deployment:" + role
            profile.rate_limits = ""
        # Apply rate limiting
        return rate_limit()(view_func)(request, profile, *args, **kwargs)
Exemple #9
0
        def _wrapped_func_arguments(request: HttpRequest, *args: object,
                                    **kwargs: object) -> HttpResponse:
            # First try block attempts to get the credentials we need to do authentication
            try:
                # Grab the base64-encoded authentication string, decode it, and split it into
                # the email and API key
                auth_type, credentials = request.META[
                    "HTTP_AUTHORIZATION"].split()
                # case insensitive per RFC 1945
                if auth_type.lower() != "basic":
                    return json_error(
                        _("This endpoint requires HTTP basic authentication."))
                role, api_key = base64.b64decode(credentials).decode(
                    "utf-8").split(":")
            except ValueError:
                return json_unauthorized(
                    _("Invalid authorization header for basic auth"))
            except KeyError:
                return json_unauthorized(
                    _("Missing authorization header for basic auth"))

            # Now we try to do authentication or die
            try:
                # profile is a Union[UserProfile, RemoteZulipServer]
                profile = validate_api_key(
                    request,
                    role,
                    api_key,
                    allow_webhook_access=allow_webhook_access,
                    client_name=full_webhook_client_name(webhook_client_name),
                )
            except JsonableError as e:
                return json_unauthorized(e.msg)
            try:
                if not skip_rate_limiting:
                    # Apply rate limiting
                    target_view_func = rate_limit()(view_func)
                else:
                    target_view_func = view_func
                return target_view_func(request, profile, *args, **kwargs)
            except Exception as err:
                if not webhook_client_name:
                    raise err
                if isinstance(err, JsonableError) and not isinstance(
                        err, UnsupportedWebhookEventType):  # nocoverage
                    raise err

                if isinstance(err, UnsupportedWebhookEventType):
                    err.webhook_name = webhook_client_name
                log_exception_to_webhook_logger(
                    summary=str(err),
                    unsupported_event=isinstance(err,
                                                 UnsupportedWebhookEventType),
                )
                raise err
Exemple #10
0
        def _wrapped_func_arguments(request: HttpRequest, *args: object,
                                    **kwargs: object) -> HttpResponse:
            # First try block attempts to get the credentials we need to do authentication
            try:
                # Grab the base64-encoded authentication string, decode it, and split it into
                # the email and API key
                auth_type, credentials = request.headers[
                    "Authorization"].split()
                # case insensitive per RFC 1945
                if auth_type.lower() != "basic":
                    raise JsonableError(
                        _("This endpoint requires HTTP basic authentication."))
                role, api_key = base64.b64decode(credentials).decode().split(
                    ":")
                if beanstalk_email_decode:
                    # Beanstalk's web hook UI rejects URL with a @ in the username section
                    # So we ask the user to replace them with %40
                    role = role.replace("%40", "@")
            except ValueError:
                return json_unauthorized(
                    _("Invalid authorization header for basic auth"))
            except KeyError:
                return json_unauthorized(
                    _("Missing authorization header for basic auth"))

            # Now we try to do authentication or die
            try:
                # profile is a Union[UserProfile, RemoteZulipServer]
                profile = validate_api_key(
                    request,
                    role,
                    api_key,
                    allow_webhook_access=allow_webhook_access,
                    client_name=full_webhook_client_name(webhook_client_name),
                )
            except JsonableError as e:
                return json_unauthorized(e.msg)
            try:
                if not skip_rate_limiting:
                    # Apply rate limiting
                    target_view_func = rate_limit()(view_func)
                else:
                    target_view_func = view_func
                return target_view_func(request, profile, *args, **kwargs)
            except Exception as err:
                if not webhook_client_name:
                    raise err
                if isinstance(err, JsonableError) and not isinstance(
                        err, WebhookError):  # nocoverage
                    raise err

                if isinstance(err, WebhookError):
                    err.webhook_name = webhook_client_name
                log_exception_to_webhook_logger(err)
                raise err
Exemple #11
0
def authenticate_log_and_execute_json(
    request: HttpRequest,
    view_func: ViewFuncT,
    *args: object,
    skip_rate_limiting: bool = False,
    allow_unauthenticated: bool = False,
    **kwargs: object,
) -> HttpResponse:
    if not skip_rate_limiting:
        limited_view_func = rate_limit()(view_func)
    else:
        limited_view_func = view_func

    if not request.user.is_authenticated:
        if not allow_unauthenticated:
            return json_unauthorized()

        process_client(request, request.user, is_browser_view=True,
                       skip_update_user_activity=True,
                       query=view_func.__name__)
        return limited_view_func(request, request.user, *args, **kwargs)

    user_profile = request.user
    validate_account_and_subdomain(request, user_profile)

    if user_profile.is_incoming_webhook:
        raise JsonableError(_("Webhook bots can only access webhooks"))

    process_client(request, user_profile, is_browser_view=True,
                   query=view_func.__name__)
    return limited_view_func(request, user_profile, *args, **kwargs)
Exemple #12
0
        def _wrapped_func_arguments(request, *args, **kwargs):
            # type: (HttpRequest, *Any, **Any) -> HttpResponse
            # First try block attempts to get the credentials we need to do authentication
            try:
                # Grab the base64-encoded authentication string, decode it, and split it into
                # the email and API key
                auth_type, credentials = request.META[
                    'HTTP_AUTHORIZATION'].split()
                # case insensitive per RFC 1945
                if auth_type.lower() != "basic":
                    return json_error(
                        _("Only Basic authentication is supported."))
                role, api_key = base64.b64decode(
                    force_bytes(credentials)).decode('utf-8').split(":")
            except ValueError:
                json_error(_("Invalid authorization header for basic auth"))
            except KeyError:
                return json_unauthorized(
                    "Missing authorization header for basic auth")

            # Now we try to do authentication or die
            try:
                # Could be a UserProfile or a Deployment
                profile = validate_api_key(role, api_key, is_webhook)
            except JsonableError as e:
                return json_unauthorized(e.error)
            request.user = profile
            process_client(request, profile)
            if isinstance(profile, UserProfile):
                request._email = profile.email
            else:
                assert isinstance(
                    profile, Deployment
                )  # type: ignore # https://github.com/python/mypy/issues/1720#issuecomment-228596830
                request._email = "deployment:" + role
                profile.rate_limits = ""
            # Apply rate limiting
            return rate_limit()(view_func)(request, profile, *args, **kwargs)
Exemple #13
0
        def _wrapped_func_arguments(request, *args, **kwargs):
            # type: (HttpRequest, *Any, **Any) -> HttpResponse
            # First try block attempts to get the credentials we need to do authentication
            try:
                # Grab the base64-encoded authentication string, decode it, and split it into
                # the email and API key
                auth_type, credentials = request.META['HTTP_AUTHORIZATION'].split()
                # case insensitive per RFC 1945
                if auth_type.lower() != "basic":
                    return json_error(_("This endpoint requires HTTP basic authentication."))
                role, api_key = base64.b64decode(force_bytes(credentials)).decode('utf-8').split(":")
            except ValueError:
                return json_unauthorized(_("Invalid authorization header for basic auth"))
            except KeyError:
                return json_unauthorized("Missing authorization header for basic auth")

            # Now we try to do authentication or die
            try:
                # profile is a Union[UserProfile, RemoteZulipServer]
                profile = validate_api_key(request, role, api_key, is_webhook)
            except JsonableError as e:
                return json_unauthorized(e.msg)
            # Apply rate limiting
            return rate_limit()(view_func)(request, profile, *args, **kwargs)
Exemple #14
0
def rest_dispatch(request, globals_list, **kwargs):
    # type: (HttpRequest, Dict[str, Any], **Any) -> HttpResponse
    """Dispatch to a REST API endpoint.

    This calls the function named in kwargs[request.method], if that request
    method is supported, and after wrapping that function to:

        * protect against CSRF (if the user is already authenticated through
          a Django session)
        * authenticate via an API key (otherwise)
        * coerce PUT/PATCH/DELETE into having POST-like semantics for
          retrieving variables

    Any keyword args that are *not* HTTP methods are passed through to the
    target function.

    Note that we search views.py globals for the function to call, so never
    make a urls.py pattern put user input into a variable called GET, POST,
    etc.
    """
    supported_methods = {} # type: Dict[str, Any]
    # duplicate kwargs so we can mutate the original as we go
    for arg in list(kwargs):
        if arg in METHODS:
            supported_methods[arg] = kwargs[arg]
            del kwargs[arg]

    if request.method == 'OPTIONS':
        response = HttpResponse(status=204) # No content
        response['Allow'] = ', '.join(supported_methods.keys())
        response['Content-Length'] = "0"
        return response

    # Override requested method if magic method=??? parameter exists
    method_to_use = request.method
    if request.POST and 'method' in request.POST:
        method_to_use = request.POST['method']
    if method_to_use == "SOCKET" and "zulip.emulated_method" in request.META:
        method_to_use = request.META["zulip.emulated_method"]

    if method_to_use in supported_methods:
        target_function = globals_list[supported_methods[method_to_use]]

        # Set request._query for update_activity_user(), which is called
        # by some of the later wrappers.
        request._query = target_function.__name__

        # We want to support authentication by both cookies (web client)
        # and API keys (API clients). In the former case, we want to
        # do a check to ensure that CSRF etc is honored, but in the latter
        # we can skip all of that.
        #
        # Security implications of this portion of the code are minimal,
        # as we should worst-case fail closed if we miscategorise a request.
        if not request.path.startswith("/api") and request.user.is_authenticated():
            # Authenticated via sessions framework, only CSRF check needed
            target_function = csrf_protect(authenticated_json_view(target_function))
        elif request.META.get('HTTP_AUTHORIZATION', None):
            # Wrap function with decorator to authenticate the user before
            # proceeding
            target_function = authenticated_rest_api_view(target_function)
        else:
            if 'text/html' in request.META.get('HTTP_ACCEPT', ''):
                # If this looks like a request from a top-level page in a
                # browser, send the user to the login page
                return HttpResponseRedirect('%s/?next=%s' % (settings.HOME_NOT_LOGGED_IN, request.path))
            else:
                return json_unauthorized(_("Not logged in: API authentication or user session required"))

        if request.method not in ["GET", "POST"]:
            # process_as_post needs to be the outer decorator, because
            # otherwise we might access and thus cache a value for
            # request.REQUEST.
            target_function = process_as_post(target_function)

        return target_function(request, **kwargs)

    return json_method_not_allowed(list(supported_methods.keys()))
Exemple #15
0
def rest_dispatch(request: HttpRequest, **kwargs: Any) -> HttpResponse:
    """Dispatch to a REST API endpoint.

    Unauthenticated endpoints should not use this, as authentication is verified
    in the following ways:
        * for paths beginning with /api, HTTP Basic auth
        * for paths beginning with /json (used by the web client), the session token

    This calls the function named in kwargs[request.method], if that request
    method is supported, and after wrapping that function to:

        * protect against CSRF (if the user is already authenticated through
          a Django session)
        * authenticate via an API key (otherwise)
        * coerce PUT/PATCH/DELETE into having POST-like semantics for
          retrieving variables

    Any keyword args that are *not* HTTP methods are passed through to the
    target function.

    Never make a urls.py pattern put user input into a variable called GET, POST,
    etc, as that is where we route HTTP verbs to target functions.
    """
    supported_methods = {}  # type: Dict[str, Any]

    # duplicate kwargs so we can mutate the original as we go
    for arg in list(kwargs):
        if arg in METHODS:
            supported_methods[arg] = kwargs[arg]
            del kwargs[arg]

    if request.method == 'OPTIONS':
        response = HttpResponse(status=204)  # No content
        response['Allow'] = ', '.join(sorted(supported_methods.keys()))
        response['Content-Length'] = "0"
        return response

    # Override requested method if magic method=??? parameter exists
    method_to_use = request.method
    if request.POST and 'method' in request.POST:
        method_to_use = request.POST['method']
    if method_to_use == "SOCKET" and "zulip.emulated_method" in request.META:
        method_to_use = request.META["zulip.emulated_method"]

    if method_to_use in supported_methods:
        entry = supported_methods[method_to_use]
        if isinstance(entry, tuple):
            target_function, view_flags = entry
            target_function = import_string(target_function)
        else:
            target_function = import_string(supported_methods[method_to_use])
            view_flags = set()

        # Set request._query for update_activity_user(), which is called
        # by some of the later wrappers.
        request._query = target_function.__name__

        # We want to support authentication by both cookies (web client)
        # and API keys (API clients). In the former case, we want to
        # do a check to ensure that CSRF etc is honored, but in the latter
        # we can skip all of that.
        #
        # Security implications of this portion of the code are minimal,
        # as we should worst-case fail closed if we miscategorise a request.

        # for some special views (e.g. serving a file that has been
        # uploaded), we support using the same url for web and API clients.
        if ('override_api_url_scheme' in view_flags and
                request.META.get('HTTP_AUTHORIZATION', None) is not None):
            # This request  API based authentication.
            target_function = authenticated_rest_api_view()(target_function)
        # /json views (web client) validate with a session token (cookie)
        elif not request.path.startswith("/api") and request.user.is_authenticated:
            # Authenticated via sessions framework, only CSRF check needed
            target_function = csrf_protect(authenticated_json_view(target_function))

        # most clients (mobile, bots, etc) use HTTP Basic Auth and REST calls, where instead of
        # username:password, we use email:apiKey
        elif request.META.get('HTTP_AUTHORIZATION', None):
            # Wrap function with decorator to authenticate the user before
            # proceeding
            view_kwargs = {}
            if 'allow_incoming_webhooks' in view_flags:
                view_kwargs['is_webhook'] = True
            target_function = authenticated_rest_api_view(**view_kwargs)(target_function)
        # Pick a way to tell user they're not authed based on how the request was made
        else:
            # If this looks like a request from a top-level page in a
            # browser, send the user to the login page
            if 'text/html' in request.META.get('HTTP_ACCEPT', ''):
                # TODO: It seems like the `?next=` part is unlikely to be helpful
                return HttpResponseRedirect('%s/?next=%s' % (settings.HOME_NOT_LOGGED_IN, request.path))
            # Ask for basic auth (email:apiKey)
            elif request.path.startswith("/api"):
                return json_unauthorized(_("Not logged in: API authentication or user session required"))
            # Session cookie expired, notify the client
            else:
                return json_unauthorized(_("Not logged in: API authentication or user session required"),
                                         www_authenticate='session')

        if request.method not in ["GET", "POST"]:
            # process_as_post needs to be the outer decorator, because
            # otherwise we might access and thus cache a value for
            # request.REQUEST.
            target_function = process_as_post(target_function)

        return target_function(request, **kwargs)

    return json_method_not_allowed(list(supported_methods.keys()))
Exemple #16
0
def rest_dispatch(request, globals_list, **kwargs):
    """Dispatch to a REST API endpoint.

    This calls the function named in kwargs[request.method], if that request
    method is supported, and after wrapping that function to:

        * protect against CSRF (if the user is already authenticated through
          a Django session)
        * authenticate via an API key (otherwise)
        * coerce PUT/PATCH/DELETE into having POST-like semantics for
          retrieving variables

    Any keyword args that are *not* HTTP methods are passed through to the
    target function.

    Note that we search views.py globals for the function to call, so never
    make a urls.py pattern put user input into a variable called GET, POST,
    etc.
    """
    supported_methods = {}
    # duplicate kwargs so we can mutate the original as we go
    for arg in list(kwargs):
        if arg in METHODS:
            supported_methods[arg] = kwargs[arg]
            del kwargs[arg]

    if request.method == 'OPTIONS':
        response = HttpResponse(status=204)  # No content
        response['Allow'] = ', '.join(supported_methods.keys())
        response['Content-Length'] = "0"
        return response

    # Override requested method if magic method=??? parameter exists
    method_to_use = request.method
    if request.POST and 'method' in request.POST:
        method_to_use = request.POST['method']

    if method_to_use in supported_methods.keys():
        target_function = globals_list[supported_methods[method_to_use]]

        # Set request._query for update_activity_user(), which is called
        # by some of the later wrappers.
        request._query = target_function.__name__

        # We want to support authentication by both cookies (web client)
        # and API keys (API clients). In the former case, we want to
        # do a check to ensure that CSRF etc is honored, but in the latter
        # we can skip all of that.
        #
        # Security implications of this portion of the code are minimal,
        # as we should worst-case fail closed if we miscategorise a request.
        if not request.path.startswith(
                "/api") and request.user.is_authenticated():
            # Authenticated via sessions framework, only CSRF check needed
            target_function = csrf_protect(
                authenticated_json_view(target_function))
        elif request.META.get('HTTP_AUTHORIZATION', None):
            # Wrap function with decorator to authenticate the user before
            # proceeding
            target_function = authenticated_rest_api_view(target_function)
        else:
            if 'text/html' in request.META.get('HTTP_ACCEPT', ''):
                # If this looks like a request from a top-level page in a
                # browser, send the user to the login page
                return HttpResponseRedirect(
                    '%s/?next=%s' %
                    (settings.HOME_NOT_LOGGED_IN, request.path))
            else:
                return json_unauthorized(
                    "Not logged in: API authentication or user session required"
                )

        if request.method not in ["GET", "POST"]:
            # process_as_post needs to be the outer decorator, because
            # otherwise we might access and thus cache a value for
            # request.REQUEST.
            target_function = process_as_post(target_function)

        return target_function(request, **kwargs)

    return json_method_not_allowed(supported_methods.keys())
Exemple #17
0
def rest_dispatch(request, **kwargs):
    # type: (HttpRequest, **Any) -> HttpResponse
    """Dispatch to a REST API endpoint.

    Unauthenticated endpoints should not use this, as authentication is verified
    in the following ways:
        * for paths beginning with /api, HTTP Basic auth
        * for paths beginning with /json (used by the web client), the session token

    This calls the function named in kwargs[request.method], if that request
    method is supported, and after wrapping that function to:

        * protect against CSRF (if the user is already authenticated through
          a Django session)
        * authenticate via an API key (otherwise)
        * coerce PUT/PATCH/DELETE into having POST-like semantics for
          retrieving variables

    Any keyword args that are *not* HTTP methods are passed through to the
    target function.

    Never make a urls.py pattern put user input into a variable called GET, POST,
    etc, as that is where we route HTTP verbs to target functions.
    """
    supported_methods = {}  # type: Dict[str, Any]

    # duplicate kwargs so we can mutate the original as we go
    for arg in list(kwargs):
        if arg in METHODS:
            supported_methods[arg] = kwargs[arg]
            del kwargs[arg]

    if request.method == 'OPTIONS':
        response = HttpResponse(status=204)  # No content
        response['Allow'] = ', '.join(sorted(supported_methods.keys()))
        response['Content-Length'] = "0"
        return response

    # Override requested method if magic method=??? parameter exists
    method_to_use = request.method
    if request.POST and 'method' in request.POST:
        method_to_use = request.POST['method']
    if method_to_use == "SOCKET" and "zulip.emulated_method" in request.META:
        method_to_use = request.META["zulip.emulated_method"]

    if method_to_use in supported_methods:
        entry = supported_methods[method_to_use]
        if isinstance(entry, tuple):
            target_function, view_flags = entry
            target_function = import_string(target_function)
        else:
            target_function = import_string(supported_methods[method_to_use])
            view_flags = set()

        # Set request._query for update_activity_user(), which is called
        # by some of the later wrappers.
        request._query = target_function.__name__

        # We want to support authentication by both cookies (web client)
        # and API keys (API clients). In the former case, we want to
        # do a check to ensure that CSRF etc is honored, but in the latter
        # we can skip all of that.
        #
        # Security implications of this portion of the code are minimal,
        # as we should worst-case fail closed if we miscategorise a request.

        # for some special views (e.g. serving a file that has been
        # uploaded), we support using the same url for web and API clients.
        if ('override_api_url_scheme' in view_flags
                and request.META.get('HTTP_AUTHORIZATION', None) is not None):
            # This request  API based authentication.
            target_function = authenticated_rest_api_view()(target_function)
        # /json views (web client) validate with a session token (cookie)
        elif not request.path.startswith(
                "/api") and request.user.is_authenticated:
            # Authenticated via sessions framework, only CSRF check needed
            target_function = csrf_protect(
                authenticated_json_view(target_function))

        # most clients (mobile, bots, etc) use HTTP Basic Auth and REST calls, where instead of
        # username:password, we use email:apiKey
        elif request.META.get('HTTP_AUTHORIZATION', None):
            # Wrap function with decorator to authenticate the user before
            # proceeding
            view_kwargs = {}
            if 'allow_incoming_webhooks' in view_flags:
                view_kwargs['is_webhook'] = True
            target_function = authenticated_rest_api_view(
                **view_kwargs)(target_function)
        # Pick a way to tell user they're not authed based on how the request was made
        else:
            # If this looks like a request from a top-level page in a
            # browser, send the user to the login page
            if 'text/html' in request.META.get('HTTP_ACCEPT', ''):
                # TODO: It seems like the `?next=` part is unlikely to be helpful
                return HttpResponseRedirect(
                    '%s/?next=%s' %
                    (settings.HOME_NOT_LOGGED_IN, request.path))
            # Ask for basic auth (email:apiKey)
            elif request.path.startswith("/api"):
                return json_unauthorized(
                    _("Not logged in: API authentication or user session required"
                      ))
            # Session cookie expired, notify the client
            else:
                return json_unauthorized(_(
                    "Not logged in: API authentication or user session required"
                ),
                                         www_authenticate='session')

        if request.method not in ["GET", "POST"]:
            # process_as_post needs to be the outer decorator, because
            # otherwise we might access and thus cache a value for
            # request.REQUEST.
            target_function = process_as_post(target_function)

        return target_function(request, **kwargs)

    return json_method_not_allowed(list(supported_methods.keys()))
Exemple #18
0
def rest_dispatch(request: HttpRequest, **kwargs: Any) -> HttpResponse:
    """Dispatch to a REST API endpoint.

    Unauthenticated endpoints should not use this, as authentication is verified
    in the following ways:
        * for paths beginning with /api, HTTP Basic auth
        * for paths beginning with /json (used by the web client), the session token

    This calls the function named in kwargs[request.method], if that request
    method is supported, and after wrapping that function to:

        * protect against CSRF (if the user is already authenticated through
          a Django session)
        * authenticate via an API key (otherwise)
        * coerce PUT/PATCH/DELETE into having POST-like semantics for
          retrieving variables

    Any keyword args that are *not* HTTP methods are passed through to the
    target function.

    Never make a urls.py pattern put user input into a variable called GET, POST,
    etc, as that is where we route HTTP verbs to target functions.
    """
    supported_methods: Dict[str, Any] = {}

    if hasattr(request, "saved_response"):
        # For completing long-polled Tornado requests, we skip the
        # view function logic and just return the response.
        return request.saved_response

    # duplicate kwargs so we can mutate the original as we go
    for arg in list(kwargs):
        if arg in METHODS:
            supported_methods[arg] = kwargs[arg]
            del kwargs[arg]

    if 'GET' in supported_methods:
        supported_methods.setdefault('HEAD', supported_methods['GET'])

    if request.method == 'OPTIONS':
        response = HttpResponse(status=204)  # No content
        response['Allow'] = ', '.join(sorted(supported_methods.keys()))
        return response

    # Override requested method if magic method=??? parameter exists
    method_to_use = request.method
    if request.POST and 'method' in request.POST:
        method_to_use = request.POST['method']

    if method_to_use in supported_methods:
        entry = supported_methods[method_to_use]
        if isinstance(entry, tuple):
            target_function, view_flags = entry
            target_function = import_string(target_function)
        else:
            target_function = import_string(supported_methods[method_to_use])
            view_flags = set()

        # Set request._query for update_activity_user(), which is called
        # by some of the later wrappers.
        request._query = target_function.__name__

        # We want to support authentication by both cookies (web client)
        # and API keys (API clients). In the former case, we want to
        # do a check to ensure that CSRF etc is honored, but in the latter
        # we can skip all of that.
        #
        # Security implications of this portion of the code are minimal,
        # as we should worst-case fail closed if we miscategorise a request.

        # for some special views (e.g. serving a file that has been
        # uploaded), we support using the same url for web and API clients.
        if ('override_api_url_scheme' in view_flags and
                request.META.get('HTTP_AUTHORIZATION', None) is not None):
            # This request uses standard API based authentication.
            # For override_api_url_scheme views, we skip our normal
            # rate limiting, because there are good reasons clients
            # might need to (e.g.) request a large number of uploaded
            # files or avatars in quick succession.
            target_function = authenticated_rest_api_view(skip_rate_limiting=True)(target_function)
        elif ('override_api_url_scheme' in view_flags and
              request.GET.get('api_key') is not None):
            # This request uses legacy API authentication.  We
            # unfortunately need that in the React Native mobile apps,
            # because there's no way to set HTTP_AUTHORIZATION in
            # React Native.  See last block for rate limiting notes.
            target_function = authenticated_uploads_api_view(skip_rate_limiting=True)(target_function)
        # /json views (web client) validate with a session token (cookie)
        elif not request.path.startswith("/api") and request.user.is_authenticated:
            # Authenticated via sessions framework, only CSRF check needed
            auth_kwargs = {}
            if 'override_api_url_scheme' in view_flags:
                auth_kwargs["skip_rate_limiting"] = True
            target_function = csrf_protect(authenticated_json_view(target_function, **auth_kwargs))

        # most clients (mobile, bots, etc) use HTTP Basic Auth and REST calls, where instead of
        # username:password, we use email:apiKey
        elif request.META.get('HTTP_AUTHORIZATION', None):
            # Wrap function with decorator to authenticate the user before
            # proceeding
            view_kwargs = {}
            if 'allow_incoming_webhooks' in view_flags:
                view_kwargs['is_webhook'] = True
            target_function = authenticated_rest_api_view(**view_kwargs)(target_function)  # type: ignore[arg-type] # likely mypy bug
        # Pick a way to tell user they're not authed based on how the request was made
        else:
            # If this looks like a request from a top-level page in a
            # browser, send the user to the login page
            if 'text/html' in request.META.get('HTTP_ACCEPT', ''):
                # TODO: It seems like the `?next=` part is unlikely to be helpful
                return HttpResponseRedirect(f'{settings.HOME_NOT_LOGGED_IN}?next={request.path}')
            # Ask for basic auth (email:apiKey)
            elif request.path.startswith("/api"):
                return json_unauthorized()
            # Logged out user accessing an endpoint with anonymous user access on JSON; proceed.
            elif request.path.startswith("/json") and 'allow_anonymous_user_web' in view_flags:
                auth_kwargs = dict(allow_unauthenticated=True)
                target_function = csrf_protect(authenticated_json_view(
                    target_function, **auth_kwargs))
            # Session cookie expired, notify the client
            else:
                return json_unauthorized(www_authenticate='session')

        if request.method not in ["GET", "POST"]:
            # process_as_post needs to be the outer decorator, because
            # otherwise we might access and thus cache a value for
            # request.REQUEST.
            target_function = process_as_post(target_function)

        return target_function(request, **kwargs)

    return json_method_not_allowed(list(supported_methods.keys()))