mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 14:03:30 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			225 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			225 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
import logging
 | 
						|
from collections.abc import Callable
 | 
						|
from functools import wraps
 | 
						|
from typing import Concatenate
 | 
						|
 | 
						|
from django.http import HttpRequest, HttpResponse, HttpResponseBase
 | 
						|
from django.urls import path
 | 
						|
from django.urls.resolvers import URLPattern
 | 
						|
from django.utils.cache import add_never_cache_headers
 | 
						|
from django.views.decorators.csrf import csrf_exempt, csrf_protect
 | 
						|
from typing_extensions import ParamSpec
 | 
						|
 | 
						|
from zerver.decorator import (
 | 
						|
    authenticated_json_view,
 | 
						|
    authenticated_rest_api_view,
 | 
						|
    authenticated_uploads_api_view,
 | 
						|
    process_as_post,
 | 
						|
    public_json_view,
 | 
						|
)
 | 
						|
from zerver.lib.exceptions import MissingAuthenticationError
 | 
						|
from zerver.lib.request import RequestNotes
 | 
						|
from zerver.lib.response import json_method_not_allowed
 | 
						|
from zerver.lib.sessions import narrow_request_user
 | 
						|
 | 
						|
ParamT = ParamSpec("ParamT")
 | 
						|
METHODS = ("GET", "HEAD", "POST", "PUT", "DELETE", "PATCH")
 | 
						|
 | 
						|
 | 
						|
def default_never_cache_responses(
 | 
						|
    view_func: Callable[Concatenate[HttpRequest, ParamT], HttpResponse],
 | 
						|
) -> Callable[Concatenate[HttpRequest, ParamT], HttpResponse]:
 | 
						|
    """Patched version of the standard Django never_cache_responses
 | 
						|
    decorator that adds headers to a response so that it will never be
 | 
						|
    cached, unless the view code has already set a Cache-Control
 | 
						|
    header.
 | 
						|
    """
 | 
						|
 | 
						|
    @wraps(view_func)
 | 
						|
    def _wrapped_view_func(
 | 
						|
        request: HttpRequest, /, *args: ParamT.args, **kwargs: ParamT.kwargs
 | 
						|
    ) -> HttpResponse:
 | 
						|
        response = view_func(request, *args, **kwargs)
 | 
						|
        if response.has_header("Cache-Control"):
 | 
						|
            return response
 | 
						|
 | 
						|
        add_never_cache_headers(response)
 | 
						|
        return response
 | 
						|
 | 
						|
    return _wrapped_view_func
 | 
						|
 | 
						|
 | 
						|
def get_target_view_function_or_response(
 | 
						|
    request: HttpRequest, rest_dispatch_kwargs: dict[str, object]
 | 
						|
) -> tuple[Callable[..., HttpResponse], set[str]] | HttpResponse:
 | 
						|
    """Helper for REST API request dispatch. The rest_dispatch_kwargs
 | 
						|
    parameter is expected to be a dictionary mapping HTTP methods to
 | 
						|
    a mix of view functions and (view_function, {view_flags}) tuples.
 | 
						|
 | 
						|
    * Returns an error HttpResponse for unsupported HTTP methods.
 | 
						|
 | 
						|
    * Otherwise, returns a tuple containing the view function
 | 
						|
      corresponding to the request's HTTP method, as well as the
 | 
						|
      appropriate set of view flags.
 | 
						|
 | 
						|
    HACK: Mutates the passed rest_dispatch_kwargs, removing the HTTP
 | 
						|
    method details but leaving any other parameters for the caller to
 | 
						|
    pass directly to the view function. We should see if we can remove
 | 
						|
    this feature; it's not clear it's actually used.
 | 
						|
 | 
						|
    """
 | 
						|
    supported_methods: dict[str, object] = {}
 | 
						|
    request_notes = RequestNotes.get_notes(request)
 | 
						|
    if request_notes.saved_response is not None:
 | 
						|
        # For completing long-polled Tornado requests, we skip the
 | 
						|
        # view function logic and just return the response.
 | 
						|
        return request_notes.saved_response
 | 
						|
 | 
						|
    # The list() duplicates rest_dispatch_kwargs, since this loop
 | 
						|
    # mutates the original.
 | 
						|
    for arg in list(rest_dispatch_kwargs):
 | 
						|
        if arg in METHODS:
 | 
						|
            supported_methods[arg] = rest_dispatch_kwargs[arg]
 | 
						|
            del rest_dispatch_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:
 | 
						|
        logging.warning(
 | 
						|
            "Overriding HTTP method via 'method' parameter: original=%s, override=%s, client=%s",
 | 
						|
            request.method,
 | 
						|
            request.POST["method"],
 | 
						|
            request_notes.client_name,
 | 
						|
        )
 | 
						|
        method_to_use = request.POST["method"]
 | 
						|
 | 
						|
    if method_to_use in supported_methods:
 | 
						|
        entry = supported_methods[method_to_use]
 | 
						|
        if isinstance(entry, tuple):
 | 
						|
            handler, view_flags = entry
 | 
						|
            assert callable(handler)
 | 
						|
            assert isinstance(view_flags, set)
 | 
						|
            return handler, view_flags
 | 
						|
        assert callable(entry)
 | 
						|
        return entry, set()
 | 
						|
 | 
						|
    return json_method_not_allowed(list(supported_methods.keys()))
 | 
						|
 | 
						|
 | 
						|
@default_never_cache_responses
 | 
						|
@csrf_exempt
 | 
						|
def rest_dispatch(request: HttpRequest, /, **kwargs: object) -> HttpResponse:
 | 
						|
    """Dispatch to a REST API endpoint.
 | 
						|
 | 
						|
    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
 | 
						|
 | 
						|
    Unauthenticated requests may use this endpoint only with the
 | 
						|
    allow_anonymous_user_web view flag.
 | 
						|
 | 
						|
    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.
 | 
						|
 | 
						|
    """
 | 
						|
    result = get_target_view_function_or_response(request, kwargs)
 | 
						|
    if isinstance(result, HttpResponse):
 | 
						|
        return result
 | 
						|
    target_function, view_flags = result
 | 
						|
    request_notes = RequestNotes.get_notes(request)
 | 
						|
 | 
						|
    # Set request_notes.query for update_activity_user(), which is called
 | 
						|
    # by some of the later wrappers.
 | 
						|
    request_notes.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 miscategorize a request.
 | 
						|
 | 
						|
    def check_is_authenticated(request: HttpRequest) -> bool:
 | 
						|
        if "narrow_user_session_cache" not in view_flags:
 | 
						|
            return request.user.is_authenticated
 | 
						|
        return narrow_request_user(request).is_authenticated
 | 
						|
 | 
						|
    # 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 "Authorization" in request.headers:
 | 
						|
        # 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 the Authorization header 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 check_is_authenticated(request):
 | 
						|
        # 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.path.startswith("/api") and "Authorization" in request.headers:
 | 
						|
        # Wrap function with decorator to authenticate the user before
 | 
						|
        # proceeding
 | 
						|
        target_function = authenticated_rest_api_view(
 | 
						|
            allow_webhook_access="allow_incoming_webhooks" in view_flags,
 | 
						|
        )(target_function)
 | 
						|
    elif (
 | 
						|
        request.path.startswith(("/json", "/avatar", "/user_uploads", "/thumbnail"))
 | 
						|
        and "allow_anonymous_user_web" in view_flags
 | 
						|
    ):
 | 
						|
        # For endpoints that support anonymous web access, we do that.
 | 
						|
        # TODO: Allow /api calls when this is stable enough.
 | 
						|
        target_function = csrf_protect(public_json_view(target_function))
 | 
						|
    else:
 | 
						|
        # Otherwise, throw an authentication error; our middleware
 | 
						|
        # will generate the appropriate HTTP response.
 | 
						|
        raise MissingAuthenticationError
 | 
						|
 | 
						|
    if request.method in ["DELETE", "PATCH", "PUT"]:
 | 
						|
        # process_as_post needs to be the outer decorator, because
 | 
						|
        # otherwise we might access and thus cache a value for
 | 
						|
        # request.POST.
 | 
						|
        target_function = process_as_post(target_function)
 | 
						|
 | 
						|
    return target_function(request, **kwargs)
 | 
						|
 | 
						|
 | 
						|
def rest_path(
 | 
						|
    route: str,
 | 
						|
    **handlers: Callable[..., HttpResponseBase] | tuple[Callable[..., HttpResponseBase], set[str]],
 | 
						|
) -> URLPattern:
 | 
						|
    return path(route, rest_dispatch, handlers)
 |