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)
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)
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)
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
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
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
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)
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
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
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)
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)
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)
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()))
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()))
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())
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()))
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()))