mirror of https://github.com/zulip/zulip.git
rest: Implement get_target_view_function_or_response.
As noted in the docstring, this is a temporary helper function that separates routing for paths that support multiple HTTP methods from `rest_dispatch` itself. We will need to replace this helper with class-based views in the future. The helper will also be handy to reduce duplication when splitting up `rest_dispatch` by authentication methods. Signed-off-by: Zixuan James Li <p359101898@gmail.com>
This commit is contained in:
parent
af88417847
commit
dd2fd8edda
|
@ -41,30 +41,24 @@ def default_never_cache_responses(view_func: ViewFuncT) -> ViewFuncT:
|
||||||
return cast(ViewFuncT, _wrapped_view_func) # https://github.com/python/mypy/issues/1927
|
return cast(ViewFuncT, _wrapped_view_func) # https://github.com/python/mypy/issues/1927
|
||||||
|
|
||||||
|
|
||||||
@default_never_cache_responses
|
def get_target_view_function_or_response(
|
||||||
@csrf_exempt
|
request: HttpRequest, rest_dispatch_kwargs: Dict[str, Any]
|
||||||
def rest_dispatch(request: HttpRequest, **kwargs: Any) -> HttpResponse:
|
) -> Union[Tuple[Callable[..., HttpResponse], Set[str]], HttpResponse]:
|
||||||
"""Dispatch to a REST API endpoint.
|
"""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.
|
||||||
|
|
||||||
Unauthenticated endpoints should not use this, as authentication is verified
|
* Returns an error HttpResponse for unsupported HTTP methods.
|
||||||
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
|
* Otherwise, returns a tuple containing the view function
|
||||||
method is supported, and after wrapping that function to:
|
corresponding to the request's HTTP method, as well as the
|
||||||
|
appropriate set of view flags.
|
||||||
|
|
||||||
* protect against CSRF (if the user is already authenticated through
|
HACK: Mutates the passed rest_dispatch_kwargs, removing the HTTP
|
||||||
a Django session)
|
method details but leaving any other parameters for the caller to
|
||||||
* authenticate via an API key (otherwise)
|
pass directly to the view function. We should see if we can remove
|
||||||
* coerce PUT/PATCH/DELETE into having POST-like semantics for
|
this feature; it's not clear it's actually used.
|
||||||
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] = {}
|
supported_methods: Dict[str, Any] = {}
|
||||||
request_notes = RequestNotes.get_notes(request)
|
request_notes = RequestNotes.get_notes(request)
|
||||||
|
@ -73,11 +67,12 @@ def rest_dispatch(request: HttpRequest, **kwargs: Any) -> HttpResponse:
|
||||||
# view function logic and just return the response.
|
# view function logic and just return the response.
|
||||||
return request_notes.saved_response
|
return request_notes.saved_response
|
||||||
|
|
||||||
# duplicate kwargs so we can mutate the original as we go
|
# The list() duplicates rest_dispatch_kwargs, since this loop
|
||||||
for arg in list(kwargs):
|
# mutates the original.
|
||||||
|
for arg in list(rest_dispatch_kwargs):
|
||||||
if arg in METHODS:
|
if arg in METHODS:
|
||||||
supported_methods[arg] = kwargs[arg]
|
supported_methods[arg] = rest_dispatch_kwargs[arg]
|
||||||
del kwargs[arg]
|
del rest_dispatch_kwargs[arg]
|
||||||
|
|
||||||
if "GET" in supported_methods:
|
if "GET" in supported_methods:
|
||||||
supported_methods.setdefault("HEAD", supported_methods["GET"])
|
supported_methods.setdefault("HEAD", supported_methods["GET"])
|
||||||
|
@ -95,10 +90,45 @@ def rest_dispatch(request: HttpRequest, **kwargs: Any) -> HttpResponse:
|
||||||
if method_to_use in supported_methods:
|
if method_to_use in supported_methods:
|
||||||
entry = supported_methods[method_to_use]
|
entry = supported_methods[method_to_use]
|
||||||
if isinstance(entry, tuple):
|
if isinstance(entry, tuple):
|
||||||
target_function, view_flags = entry
|
return entry
|
||||||
else:
|
return supported_methods[method_to_use], set()
|
||||||
target_function = supported_methods[method_to_use]
|
|
||||||
view_flags = set()
|
return json_method_not_allowed(list(supported_methods.keys()))
|
||||||
|
|
||||||
|
|
||||||
|
@default_never_cache_responses
|
||||||
|
@csrf_exempt
|
||||||
|
def rest_dispatch(request: HttpRequest, **kwargs: Any) -> 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
|
# Set request_notes.query for update_activity_user(), which is called
|
||||||
# by some of the later wrappers.
|
# by some of the later wrappers.
|
||||||
|
@ -126,9 +156,7 @@ def rest_dispatch(request: HttpRequest, **kwargs: Any) -> HttpResponse:
|
||||||
# unfortunately need that in the React Native mobile apps,
|
# unfortunately need that in the React Native mobile apps,
|
||||||
# because there's no way to set the Authorization header in
|
# because there's no way to set the Authorization header in
|
||||||
# React Native. See last block for rate limiting notes.
|
# React Native. See last block for rate limiting notes.
|
||||||
target_function = authenticated_uploads_api_view(skip_rate_limiting=True)(
|
target_function = authenticated_uploads_api_view(skip_rate_limiting=True)(target_function)
|
||||||
target_function
|
|
||||||
)
|
|
||||||
# /json views (web client) validate with a session token (cookie)
|
# /json views (web client) validate with a session token (cookie)
|
||||||
elif not request.path.startswith("/api") and request.user.is_authenticated:
|
elif not request.path.startswith("/api") and request.user.is_authenticated:
|
||||||
# Authenticated via sessions framework, only CSRF check needed
|
# Authenticated via sessions framework, only CSRF check needed
|
||||||
|
@ -165,8 +193,6 @@ def rest_dispatch(request: HttpRequest, **kwargs: Any) -> HttpResponse:
|
||||||
|
|
||||||
return target_function(request, **kwargs)
|
return target_function(request, **kwargs)
|
||||||
|
|
||||||
return json_method_not_allowed(list(supported_methods.keys()))
|
|
||||||
|
|
||||||
|
|
||||||
def rest_path(
|
def rest_path(
|
||||||
route: str,
|
route: str,
|
||||||
|
|
Loading…
Reference in New Issue