mirror of
https://github.com/zulip/zulip.git
synced 2025-11-02 21:13:36 +00:00
python: Reformat with Black, except quotes.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
committed by
Tim Abbott
parent
5028c081cb
commit
11741543da
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user