python: Reformat with Black, except quotes.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
Anders Kaseorg
2021-02-11 23:19:30 -08:00
committed by Tim Abbott
parent 5028c081cb
commit 11741543da
817 changed files with 44952 additions and 24860 deletions

View File

@@ -53,6 +53,7 @@ webhook_unsupported_events_logger = logging.getLogger("zulip.zerver.webhooks.uns
FuncT = TypeVar('FuncT', bound=Callable[..., object])
def cachify(method: FuncT) -> FuncT:
dct: Dict[Tuple[object, ...], object] = {}
@@ -63,10 +64,13 @@ def cachify(method: FuncT) -> FuncT:
result = method(*args)
dct[tup] = result
return result
return cast(FuncT, cache_wrapper) # https://github.com/python/mypy/issues/1927
def update_user_activity(request: HttpRequest, user_profile: UserProfile,
query: Optional[str]) -> None:
def update_user_activity(
request: HttpRequest, user_profile: UserProfile, query: Optional[str]
) -> None:
# update_active_status also pushes to RabbitMQ, and it seems
# redundant to log that here as well.
if request.META["PATH_INFO"] == '/json/users/me/presence':
@@ -79,59 +83,86 @@ def update_user_activity(request: HttpRequest, user_profile: UserProfile,
else:
query = request.META['PATH_INFO']
event = {'query': query,
'user_profile_id': user_profile.id,
'time': datetime_to_timestamp(timezone_now()),
'client_id': request.client.id}
event = {
'query': query,
'user_profile_id': user_profile.id,
'time': datetime_to_timestamp(timezone_now()),
'client_id': request.client.id,
}
queue_json_publish("user_activity", event, lambda event: None)
# Based on django.views.decorators.http.require_http_methods
def require_post(func: ViewFuncT) -> ViewFuncT:
@wraps(func)
def wrapper(request: HttpRequest, *args: object, **kwargs: object) -> HttpResponse:
if request.method != "POST":
err_method = request.method
logging.warning('Method Not Allowed (%s): %s', err_method, request.path,
extra={'status_code': 405, 'request': request})
logging.warning(
'Method Not Allowed (%s): %s',
err_method,
request.path,
extra={'status_code': 405, 'request': request},
)
if request.error_format == 'JSON':
return json_method_not_allowed(["POST"])
else:
return TemplateResponse(request, "404.html", context={'status_code': 405}, status=405)
return TemplateResponse(
request, "404.html", context={'status_code': 405}, status=405
)
return func(request, *args, **kwargs)
return cast(ViewFuncT, wrapper) # https://github.com/python/mypy/issues/1927
def require_realm_owner(func: ViewFuncT) -> ViewFuncT:
@wraps(func)
def wrapper(request: HttpRequest, user_profile: UserProfile, *args: object, **kwargs: object) -> HttpResponse:
def wrapper(
request: HttpRequest, user_profile: UserProfile, *args: object, **kwargs: object
) -> HttpResponse:
if not user_profile.is_realm_owner:
raise OrganizationOwnerRequired()
return func(request, user_profile, *args, **kwargs)
return cast(ViewFuncT, wrapper) # https://github.com/python/mypy/issues/1927
def require_realm_admin(func: ViewFuncT) -> ViewFuncT:
@wraps(func)
def wrapper(request: HttpRequest, user_profile: UserProfile, *args: object, **kwargs: object) -> HttpResponse:
def wrapper(
request: HttpRequest, user_profile: UserProfile, *args: object, **kwargs: object
) -> HttpResponse:
if not user_profile.is_realm_admin:
raise OrganizationAdministratorRequired()
return func(request, user_profile, *args, **kwargs)
return cast(ViewFuncT, wrapper) # https://github.com/python/mypy/issues/1927
def require_organization_member(func: ViewFuncT) -> ViewFuncT:
@wraps(func)
def wrapper(request: HttpRequest, user_profile: UserProfile, *args: object, **kwargs: object) -> HttpResponse:
def wrapper(
request: HttpRequest, user_profile: UserProfile, *args: object, **kwargs: object
) -> HttpResponse:
if user_profile.role > UserProfile.ROLE_MEMBER:
raise OrganizationMemberRequired()
return func(request, user_profile, *args, **kwargs)
return cast(ViewFuncT, wrapper) # https://github.com/python/mypy/issues/1927
def require_billing_access(func: ViewFuncT) -> ViewFuncT:
@wraps(func)
def wrapper(request: HttpRequest, user_profile: UserProfile, *args: object, **kwargs: object) -> HttpResponse:
def wrapper(
request: HttpRequest, user_profile: UserProfile, *args: object, **kwargs: object
) -> HttpResponse:
if not user_profile.has_billing_access:
raise JsonableError(_("Must be a billing administrator or an organization owner"))
return func(request, user_profile, *args, **kwargs)
return cast(ViewFuncT, wrapper) # https://github.com/python/mypy/issues/1927
def get_client_name(request: HttpRequest) -> str:
# If the API request specified a client in the request content,
# that has priority. Otherwise, extract the client from the
@@ -152,11 +183,16 @@ def get_client_name(request: HttpRequest) -> str:
# in logs and figure out the extent of the problem
return "Unspecified"
def process_client(request: HttpRequest, user_profile: UserProfile,
*, is_browser_view: bool=False,
client_name: Optional[str]=None,
skip_update_user_activity: bool=False,
query: Optional[str]=None) -> None:
def process_client(
request: HttpRequest,
user_profile: UserProfile,
*,
is_browser_view: bool = False,
client_name: Optional[str] = None,
skip_update_user_activity: bool = False,
query: Optional[str] = None,
) -> None:
if client_name is None:
client_name = get_client_name(request)
@@ -172,6 +208,7 @@ def process_client(request: HttpRequest, user_profile: UserProfile,
if not skip_update_user_activity and user_profile.is_authenticated:
update_user_activity(request, user_profile, query)
class InvalidZulipServerError(JsonableError):
code = ErrorCode.INVALID_ZULIP_SERVER
data_fields = ['role']
@@ -183,14 +220,20 @@ class InvalidZulipServerError(JsonableError):
def msg_format() -> str:
return "Zulip server auth failure: {role} is not registered"
class InvalidZulipServerKeyError(InvalidZulipServerError):
@staticmethod
def msg_format() -> str:
return "Zulip server auth failure: key does not match role {role}"
def validate_api_key(request: HttpRequest, role: Optional[str],
api_key: str, allow_webhook_access: bool=False,
client_name: Optional[str]=None) -> Union[UserProfile, "RemoteZulipServer"]:
def validate_api_key(
request: HttpRequest,
role: Optional[str],
api_key: str,
allow_webhook_access: bool = False,
client_name: Optional[str] = None,
) -> Union[UserProfile, "RemoteZulipServer"]:
# Remove whitespace to protect users from trivial errors.
api_key = api_key.strip()
if role is not None:
@@ -222,6 +265,7 @@ def validate_api_key(request: HttpRequest, role: Optional[str],
return user_profile
def validate_account_and_subdomain(request: HttpRequest, user_profile: UserProfile) -> None:
if user_profile.realm.deactivated:
raise JsonableError(_("This organization has been deactivated"))
@@ -230,17 +274,23 @@ def validate_account_and_subdomain(request: HttpRequest, user_profile: UserProfi
# Either the subdomain matches, or we're accessing Tornado from
# and to localhost (aka spoofing a request as the user).
if (not user_matches_subdomain(get_subdomain(request), user_profile) and
not (settings.RUNNING_INSIDE_TORNADO and
request.META["SERVER_NAME"] == "127.0.0.1" and
request.META["REMOTE_ADDR"] == "127.0.0.1")):
if not user_matches_subdomain(get_subdomain(request), user_profile) and not (
settings.RUNNING_INSIDE_TORNADO
and request.META["SERVER_NAME"] == "127.0.0.1"
and request.META["REMOTE_ADDR"] == "127.0.0.1"
):
logging.warning(
"User %s (%s) attempted to access API on wrong subdomain (%s)",
user_profile.delivery_email, user_profile.realm.subdomain, get_subdomain(request),
user_profile.delivery_email,
user_profile.realm.subdomain,
get_subdomain(request),
)
raise JsonableError(_("Account is not associated with this subdomain"))
def access_user_by_api_key(request: HttpRequest, api_key: str, email: Optional[str]=None) -> UserProfile:
def access_user_by_api_key(
request: HttpRequest, api_key: str, email: Optional[str] = None
) -> UserProfile:
if not has_api_key_format(api_key):
raise InvalidAPIKeyFormatError()
@@ -258,6 +308,7 @@ def access_user_by_api_key(request: HttpRequest, api_key: str, email: Optional[s
return user_profile
def log_exception_to_webhook_logger(
summary: str,
unsupported_event: bool,
@@ -267,15 +318,17 @@ def log_exception_to_webhook_logger(
else:
webhook_logger.exception(summary, stack_info=True)
def full_webhook_client_name(raw_client_name: Optional[str]=None) -> Optional[str]:
def full_webhook_client_name(raw_client_name: Optional[str] = None) -> Optional[str]:
if raw_client_name is None:
return None
return f"Zulip{raw_client_name}Webhook"
# Use this for webhook views that don't get an email passed in.
def webhook_view(
webhook_client_name: str,
notify_bot_owner_on_invalid_json: bool=True,
webhook_client_name: str,
notify_bot_owner_on_invalid_json: bool = True,
) -> Callable[[Callable[..., HttpResponse]], Callable[..., HttpResponse]]:
# Unfortunately, callback protocols are insufficient for this:
# https://mypy.readthedocs.io/en/stable/protocols.html#callback-protocols
@@ -284,10 +337,16 @@ def webhook_view(
@csrf_exempt
@has_request_variables
@wraps(view_func)
def _wrapped_func_arguments(request: HttpRequest, api_key: str=REQ(),
*args: object, **kwargs: object) -> HttpResponse:
user_profile = validate_api_key(request, None, api_key, allow_webhook_access=True,
client_name=full_webhook_client_name(webhook_client_name))
def _wrapped_func_arguments(
request: HttpRequest, api_key: str = REQ(), *args: object, **kwargs: object
) -> HttpResponse:
user_profile = validate_api_key(
request,
None,
api_key,
allow_webhook_access=True,
client_name=full_webhook_client_name(webhook_client_name),
)
if settings.RATE_LIMITING:
rate_limit_user(request, user_profile, domain='api_by_user')
@@ -299,8 +358,11 @@ def webhook_view(
# cyclic import; correct fix is probably to move
# notify_bot_owner_about_invalid_json to a smaller file.
from zerver.lib.webhooks.common import notify_bot_owner_about_invalid_json
notify_bot_owner_about_invalid_json(user_profile, webhook_client_name)
elif isinstance(err, JsonableError) and not isinstance(err, UnsupportedWebhookEventType):
elif isinstance(err, JsonableError) and not isinstance(
err, UnsupportedWebhookEventType
):
pass
else:
if isinstance(err, UnsupportedWebhookEventType):
@@ -312,20 +374,26 @@ def webhook_view(
raise err
return _wrapped_func_arguments
return _wrapped_view_func
# From Django 2.2, modified to pass the request rather than just the
# user into test_func; this is useful so that we can revalidate the
# subdomain matches the user's realm. It is likely that we could make
# the subdomain validation happen elsewhere and switch to using the
# stock Django version.
def user_passes_test(test_func: Callable[[HttpResponse], bool], login_url: Optional[str]=None,
redirect_field_name: str=REDIRECT_FIELD_NAME) -> Callable[[ViewFuncT], ViewFuncT]:
def user_passes_test(
test_func: Callable[[HttpResponse], bool],
login_url: Optional[str] = None,
redirect_field_name: str = REDIRECT_FIELD_NAME,
) -> Callable[[ViewFuncT], ViewFuncT]:
"""
Decorator for views that checks that the user passes the given test,
redirecting to the log-in page if necessary. The test should be a callable
that takes the user object and returns True if the user passes.
"""
def decorator(view_func: ViewFuncT) -> ViewFuncT:
@wraps(view_func)
def _wrapped_view(request: HttpRequest, *args: object, **kwargs: object) -> HttpResponse:
@@ -337,8 +405,9 @@ def user_passes_test(test_func: Callable[[HttpResponse], bool], login_url: Optio
# use the path as the "next" url.
login_scheme, login_netloc = urllib.parse.urlparse(resolved_login_url)[:2]
current_scheme, current_netloc = urllib.parse.urlparse(path)[:2]
if ((not login_scheme or login_scheme == current_scheme) and
(not login_netloc or login_netloc == current_netloc)):
if (not login_scheme or login_scheme == current_scheme) and (
not login_netloc or login_netloc == current_netloc
):
path = request.get_full_path()
# TODO: Restore testing for this case; it was removed when
@@ -346,11 +415,13 @@ def user_passes_test(test_func: Callable[[HttpResponse], bool], login_url: Optio
if path == "/": # nocoverage
# Don't add ?next=/, to keep our URLs clean
return HttpResponseRedirect(resolved_login_url)
return redirect_to_login(
path, resolved_login_url, redirect_field_name)
return redirect_to_login(path, resolved_login_url, redirect_field_name)
return cast(ViewFuncT, _wrapped_view) # https://github.com/python/mypy/issues/1927
return decorator
def logged_in_and_active(request: HttpRequest) -> bool:
if not request.user.is_authenticated:
return False
@@ -360,11 +431,13 @@ def logged_in_and_active(request: HttpRequest) -> bool:
return False
return user_matches_subdomain(get_subdomain(request), request.user)
def do_two_factor_login(request: HttpRequest, user_profile: UserProfile) -> None:
device = default_device(user_profile)
if device:
django_otp.login(request, device)
def do_login(request: HttpRequest, user_profile: UserProfile) -> None:
"""Creates a session, logging in the user, using the Django method,
and also adds helpful data needed by our server logs.
@@ -376,34 +449,40 @@ def do_login(request: HttpRequest, user_profile: UserProfile) -> None:
# Login with two factor authentication as well.
do_two_factor_login(request, user_profile)
def log_view_func(view_func: ViewFuncT) -> ViewFuncT:
@wraps(view_func)
def _wrapped_view_func(request: HttpRequest, *args: object, **kwargs: object) -> HttpResponse:
request._query = view_func.__name__
return view_func(request, *args, **kwargs)
return cast(ViewFuncT, _wrapped_view_func) # https://github.com/python/mypy/issues/1927
def add_logging_data(view_func: ViewFuncT) -> ViewFuncT:
@wraps(view_func)
def _wrapped_view_func(request: HttpRequest, *args: object, **kwargs: object) -> HttpResponse:
process_client(request, request.user, is_browser_view=True,
query=view_func.__name__)
process_client(request, request.user, is_browser_view=True, query=view_func.__name__)
return rate_limit()(view_func)(request, *args, **kwargs)
return cast(ViewFuncT, _wrapped_view_func) # https://github.com/python/mypy/issues/1927
def human_users_only(view_func: ViewFuncT) -> ViewFuncT:
@wraps(view_func)
def _wrapped_view_func(request: HttpRequest, *args: object, **kwargs: object) -> HttpResponse:
if request.user.is_bot:
return json_error(_("This endpoint does not accept bot requests."))
return view_func(request, *args, **kwargs)
return cast(ViewFuncT, _wrapped_view_func) # https://github.com/python/mypy/issues/1927
# Based on Django 1.8's @login_required
def zulip_login_required(
function: Optional[ViewFuncT]=None,
redirect_field_name: str=REDIRECT_FIELD_NAME,
login_url: str=settings.HOME_NOT_LOGGED_IN,
function: Optional[ViewFuncT] = None,
redirect_field_name: str = REDIRECT_FIELD_NAME,
login_url: str = settings.HOME_NOT_LOGGED_IN,
) -> Union[Callable[[ViewFuncT], ViewFuncT], ViewFuncT]:
actual_decorator = lambda function: user_passes_test(
logged_in_and_active,
@@ -411,7 +490,8 @@ def zulip_login_required(
redirect_field_name=redirect_field_name,
)(
zulip_otp_required(
redirect_field_name=redirect_field_name, login_url=login_url,
redirect_field_name=redirect_field_name,
login_url=login_url,
)(add_logging_data(function))
)
@@ -419,10 +499,11 @@ def zulip_login_required(
return actual_decorator(function)
return actual_decorator # nocoverage # We don't use this without a function
def web_public_view(
view_func: ViewFuncT,
redirect_field_name: str=REDIRECT_FIELD_NAME,
login_url: str=settings.HOME_NOT_LOGGED_IN,
view_func: ViewFuncT,
redirect_field_name: str = REDIRECT_FIELD_NAME,
login_url: str = settings.HOME_NOT_LOGGED_IN,
) -> Union[Callable[[ViewFuncT], ViewFuncT], ViewFuncT]:
"""
This wrapper adds client info for unauthenticated users but
@@ -437,10 +518,12 @@ def web_public_view(
return zulip_login_required(view_func, redirect_field_name, login_url) # nocoverage
actual_decorator = lambda view_func: zulip_otp_required(
redirect_field_name=redirect_field_name, login_url=login_url)(add_logging_data(view_func))
redirect_field_name=redirect_field_name, login_url=login_url
)(add_logging_data(view_func))
return actual_decorator(view_func)
def require_server_admin(view_func: ViewFuncT) -> ViewFuncT:
@zulip_login_required
@wraps(view_func)
@@ -449,50 +532,66 @@ def require_server_admin(view_func: ViewFuncT) -> ViewFuncT:
return HttpResponseRedirect(settings.HOME_NOT_LOGGED_IN)
return add_logging_data(view_func)(request, *args, **kwargs)
return cast(ViewFuncT, _wrapped_view_func) # https://github.com/python/mypy/issues/1927
def require_server_admin_api(view_func: ViewFuncT) -> ViewFuncT:
@zulip_login_required
@wraps(view_func)
def _wrapped_view_func(request: HttpRequest, user_profile: UserProfile, *args: object,
**kwargs: object) -> HttpResponse:
def _wrapped_view_func(
request: HttpRequest, user_profile: UserProfile, *args: object, **kwargs: object
) -> HttpResponse:
if not user_profile.is_staff:
raise JsonableError(_("Must be an server administrator"))
return view_func(request, user_profile, *args, **kwargs)
return cast(ViewFuncT, _wrapped_view_func) # https://github.com/python/mypy/issues/1927
def require_non_guest_user(view_func: ViewFuncT) -> ViewFuncT:
@wraps(view_func)
def _wrapped_view_func(request: HttpRequest, user_profile: UserProfile, *args: object,
**kwargs: object) -> HttpResponse:
def _wrapped_view_func(
request: HttpRequest, user_profile: UserProfile, *args: object, **kwargs: object
) -> HttpResponse:
if user_profile.is_guest:
raise JsonableError(_("Not allowed for guest users"))
return view_func(request, user_profile, *args, **kwargs)
return cast(ViewFuncT, _wrapped_view_func) # https://github.com/python/mypy/issues/1927
def require_member_or_admin(view_func: ViewFuncT) -> ViewFuncT:
@wraps(view_func)
def _wrapped_view_func(request: HttpRequest, user_profile: UserProfile, *args: object,
**kwargs: object) -> HttpResponse:
def _wrapped_view_func(
request: HttpRequest, user_profile: UserProfile, *args: object, **kwargs: object
) -> HttpResponse:
if user_profile.is_guest:
raise JsonableError(_("Not allowed for guest users"))
if user_profile.is_bot:
return json_error(_("This endpoint does not accept bot requests."))
return view_func(request, user_profile, *args, **kwargs)
return cast(ViewFuncT, _wrapped_view_func) # https://github.com/python/mypy/issues/1927
def require_user_group_edit_permission(view_func: ViewFuncT) -> ViewFuncT:
@require_member_or_admin
@wraps(view_func)
def _wrapped_view_func(request: HttpRequest, user_profile: UserProfile,
*args: object, **kwargs: object) -> HttpResponse:
def _wrapped_view_func(
request: HttpRequest, user_profile: UserProfile, *args: object, **kwargs: object
) -> HttpResponse:
realm = user_profile.realm
if realm.user_group_edit_policy != Realm.USER_GROUP_EDIT_POLICY_MEMBERS and \
not user_profile.is_realm_admin:
if (
realm.user_group_edit_policy != Realm.USER_GROUP_EDIT_POLICY_MEMBERS
and not user_profile.is_realm_admin
):
raise OrganizationAdministratorRequired()
return view_func(request, user_profile, *args, **kwargs)
return cast(ViewFuncT, _wrapped_view_func) # https://github.com/python/mypy/issues/1927
# This API endpoint is used only for the mobile apps. It is part of a
# workaround for the fact that React Native doesn't support setting
# HTTP basic authentication headers.
@@ -503,18 +602,21 @@ def authenticated_uploads_api_view(
@csrf_exempt
@has_request_variables
@wraps(view_func)
def _wrapped_func_arguments(request: HttpRequest,
api_key: str=REQ(),
*args: object, **kwargs: object) -> HttpResponse:
def _wrapped_func_arguments(
request: HttpRequest, api_key: str = REQ(), *args: object, **kwargs: object
) -> HttpResponse:
user_profile = validate_api_key(request, None, api_key, False)
if not skip_rate_limiting:
limited_func = rate_limit()(view_func)
else:
limited_func = view_func
return limited_func(request, user_profile, *args, **kwargs)
return _wrapped_func_arguments
return _wrapped_view_func
# A more REST-y authentication decorator, using, in particular, HTTP basic
# authentication.
#
@@ -532,7 +634,9 @@ def authenticated_rest_api_view(
def _wrapped_view_func(view_func: Callable[..., HttpResponse]) -> Callable[..., HttpResponse]:
@csrf_exempt
@wraps(view_func)
def _wrapped_func_arguments(request: HttpRequest, *args: object, **kwargs: object) -> HttpResponse:
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
@@ -550,9 +654,13 @@ def authenticated_rest_api_view(
# 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))
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:
@@ -565,7 +673,9 @@ def authenticated_rest_api_view(
except Exception as err:
if not webhook_client_name:
raise err
if isinstance(err, JsonableError) and not isinstance(err, UnsupportedWebhookEventType): # nocoverage
if isinstance(err, JsonableError) and not isinstance(
err, UnsupportedWebhookEventType
): # nocoverage
raise err
if isinstance(err, UnsupportedWebhookEventType):
@@ -575,9 +685,12 @@ def authenticated_rest_api_view(
unsupported_event=isinstance(err, UnsupportedWebhookEventType),
)
raise err
return _wrapped_func_arguments
return _wrapped_view_func
def process_as_post(view_func: ViewFuncT) -> ViewFuncT:
@wraps(view_func)
def _wrapped_view_func(request: HttpRequest, *args: object, **kwargs: object) -> HttpResponse:
@@ -608,6 +721,7 @@ def process_as_post(view_func: ViewFuncT) -> ViewFuncT:
return cast(ViewFuncT, _wrapped_view_func) # https://github.com/python/mypy/issues/1927
def authenticate_log_and_execute_json(
request: HttpRequest,
view_func: ViewFuncT,
@@ -625,9 +739,13 @@ def authenticate_log_and_execute_json(
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__)
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
@@ -636,10 +754,10 @@ def authenticate_log_and_execute_json(
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__)
process_client(request, user_profile, is_browser_view=True, query=view_func.__name__)
return limited_view_func(request, user_profile, *args, **kwargs)
# Checks if the user is logged in. If not, return an error (the
# @login_required behavior of redirecting to a login page doesn't make
# sense for json views)
@@ -649,8 +767,7 @@ def authenticated_json_view(
allow_unauthenticated: bool = False,
) -> Callable[..., HttpResponse]:
@wraps(view_func)
def _wrapped_view_func(request: HttpRequest,
*args: object, **kwargs: object) -> HttpResponse:
def _wrapped_view_func(request: HttpRequest, *args: object, **kwargs: object) -> HttpResponse:
return authenticate_log_and_execute_json(
request,
view_func,
@@ -659,25 +776,32 @@ def authenticated_json_view(
allow_unauthenticated=allow_unauthenticated,
**kwargs,
)
return _wrapped_view_func
def is_local_addr(addr: str) -> bool:
return addr in ('127.0.0.1', '::1')
# These views are used by the main Django server to notify the Tornado server
# of events. We protect them from the outside world by checking a shared
# secret, and also the originating IP (for now).
def authenticate_notify(request: HttpRequest) -> bool:
return (is_local_addr(request.META['REMOTE_ADDR']) and
request.POST.get('secret') == settings.SHARED_SECRET)
return (
is_local_addr(request.META['REMOTE_ADDR'])
and request.POST.get('secret') == settings.SHARED_SECRET
)
def client_is_exempt_from_rate_limiting(request: HttpRequest) -> bool:
# Don't rate limit requests from Django that come from our own servers,
# and don't rate-limit dev instances
return ((request.client and request.client.name.lower() == 'internal') and
(is_local_addr(request.META['REMOTE_ADDR']) or
settings.DEBUG_RATE_LIMITING))
return (request.client and request.client.name.lower() == 'internal') and (
is_local_addr(request.META['REMOTE_ADDR']) or settings.DEBUG_RATE_LIMITING
)
def internal_notify_view(is_tornado_view: bool) -> Callable[[ViewFuncT], ViewFuncT]:
# The typing here could be improved by using the Extended Callable types:
@@ -685,11 +809,14 @@ def internal_notify_view(is_tornado_view: bool) -> Callable[[ViewFuncT], ViewFun
"""Used for situations where something running on the Zulip server
needs to make a request to the (other) Django/Tornado processes running on
the server."""
def _wrapped_view_func(view_func: ViewFuncT) -> ViewFuncT:
@csrf_exempt
@require_post
@wraps(view_func)
def _wrapped_func_arguments(request: HttpRequest, *args: object, **kwargs: object) -> HttpResponse:
def _wrapped_func_arguments(
request: HttpRequest, *args: object, **kwargs: object
) -> HttpResponse:
if not authenticate_notify(request):
return json_error(_('Access denied'), status=403)
is_tornado_request = hasattr(request, '_tornado_handler')
@@ -701,26 +828,34 @@ def internal_notify_view(is_tornado_view: bool) -> Callable[[ViewFuncT], ViewFun
raise RuntimeError('Django notify view called with Tornado handler')
request._requestor_for_logs = "internal"
return view_func(request, *args, **kwargs)
return _wrapped_func_arguments
return _wrapped_view_func
def to_utc_datetime(timestamp: str) -> datetime.datetime:
return timestamp_to_datetime(float(timestamp))
def statsd_increment(counter: str, val: int=1) -> Callable[[FuncT], FuncT]:
def statsd_increment(counter: str, val: int = 1) -> Callable[[FuncT], FuncT]:
"""Increments a statsd counter on completion of the
decorated function.
Pass the name of the counter to this decorator-returning function."""
def wrapper(func: FuncT) -> FuncT:
@wraps(func)
def wrapped_func(*args: object, **kwargs: object) -> object:
ret = func(*args, **kwargs)
statsd.incr(counter, val)
return ret
return cast(FuncT, wrapped_func) # https://github.com/python/mypy/issues/1927
return wrapper
def rate_limit_user(request: HttpRequest, user: UserProfile, domain: str) -> None:
"""Returns whether or not a user was rate limited. Will raise a RateLimited exception
if the user has been rate limited, otherwise returns and modifies request to contain
@@ -728,11 +863,13 @@ def rate_limit_user(request: HttpRequest, user: UserProfile, domain: str) -> Non
RateLimitedUser(user, domain=domain).rate_limit_request(request)
def rate_limit(domain: str='api_by_user') -> Callable[[ViewFuncT], ViewFuncT]:
def rate_limit(domain: str = 'api_by_user') -> Callable[[ViewFuncT], ViewFuncT]:
"""Rate-limits a view. Takes an optional 'domain' param if you wish to
rate limit different types of API calls independently.
Returns a decorator"""
def wrapper(func: ViewFuncT) -> ViewFuncT:
@wraps(func)
def wrapped_func(request: HttpRequest, *args: object, **kwargs: object) -> HttpResponse:
@@ -748,8 +885,9 @@ def rate_limit(domain: str='api_by_user') -> Callable[[ViewFuncT], ViewFuncT]:
user = request.user
if isinstance(user, AnonymousUser) or (settings.ZILENCER_ENABLED and
isinstance(user, RemoteZulipServer)):
if isinstance(user, AnonymousUser) or (
settings.ZILENCER_ENABLED and isinstance(user, RemoteZulipServer)
):
# We can only rate-limit logged-in users for now.
# We also only support rate-limiting authenticated
# views right now.
@@ -760,20 +898,25 @@ def rate_limit(domain: str='api_by_user') -> Callable[[ViewFuncT], ViewFuncT]:
rate_limit_user(request, user, domain)
return func(request, *args, **kwargs)
return cast(ViewFuncT, wrapped_func) # https://github.com/python/mypy/issues/1927
return wrapper
def return_success_on_head_request(view_func: ViewFuncT) -> ViewFuncT:
@wraps(view_func)
def _wrapped_view_func(request: HttpRequest, *args: object, **kwargs: object) -> HttpResponse:
if request.method == 'HEAD':
return json_success()
return view_func(request, *args, **kwargs)
return cast(ViewFuncT, _wrapped_view_func) # https://github.com/python/mypy/issues/1927
def zulip_otp_required(
redirect_field_name: str='next',
login_url: str=settings.HOME_NOT_LOGGED_IN,
redirect_field_name: str = 'next',
login_url: str = settings.HOME_NOT_LOGGED_IN,
) -> Callable[[ViewFuncT], ViewFuncT]:
"""
The reason we need to create this function is that the stock
@@ -816,18 +959,20 @@ def zulip_otp_required(
# fails the test (and we should redirect to the 2FA view).
return False
decorator = django_user_passes_test(test,
login_url=login_url,
redirect_field_name=redirect_field_name)
decorator = django_user_passes_test(
test, login_url=login_url, redirect_field_name=redirect_field_name
)
return decorator
def add_google_analytics_context(context: Dict[str, object]) -> None:
if settings.GOOGLE_ANALYTICS_ID is not None: # nocoverage
page_params = context.setdefault("page_params", {})
assert isinstance(page_params, dict)
page_params["google_analytics_id"] = settings.GOOGLE_ANALYTICS_ID
def add_google_analytics(view_func: ViewFuncT) -> ViewFuncT:
@wraps(view_func)
def _wrapped_view_func(request: HttpRequest, *args: object, **kwargs: object) -> HttpResponse:
@@ -839,4 +984,5 @@ def add_google_analytics(view_func: ViewFuncT) -> ViewFuncT:
elif response.status_code == 200: # nocoverage
raise TypeError("add_google_analytics requires a TemplateResponse")
return response
return cast(ViewFuncT, _wrapped_view_func) # https://github.com/python/mypy/issues/1927