mirror of https://github.com/zulip/zulip.git
rest: Extract remote_server_path from rest_path.
This allows us to separate the zilencer paths from other JSON paths, with explicit type annotation expecting `RemoteZulipServer` as the second parameter of the handler using authenticated_remote_server_view. The test case is also updated to remove a test for a situation that no longer occurs anymore, since we don't perform subdomain checks on remote servers. Signed-off-by: Zixuan James Li <p359101898@gmail.com>
This commit is contained in:
parent
dd2fd8edda
commit
5c49e4ba06
|
@ -62,9 +62,6 @@ from zerver.lib.users import is_2fa_verified
|
||||||
from zerver.lib.utils import has_api_key_format, statsd
|
from zerver.lib.utils import has_api_key_format, statsd
|
||||||
from zerver.models import UserProfile, get_client, get_user_profile_by_api_key
|
from zerver.models import UserProfile, get_client, get_user_profile_by_api_key
|
||||||
|
|
||||||
if settings.ZILENCER_ENABLED:
|
|
||||||
from zilencer.models import RemoteZulipServer
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from django.http.request import _ImmutableQueryDict
|
from django.http.request import _ImmutableQueryDict
|
||||||
|
|
||||||
|
@ -242,18 +239,12 @@ def validate_api_key(
|
||||||
api_key: str,
|
api_key: str,
|
||||||
allow_webhook_access: bool = False,
|
allow_webhook_access: bool = False,
|
||||||
client_name: Optional[str] = None,
|
client_name: Optional[str] = None,
|
||||||
) -> Union[UserProfile, "RemoteZulipServer"]:
|
) -> UserProfile:
|
||||||
# Remove whitespace to protect users from trivial errors.
|
# Remove whitespace to protect users from trivial errors.
|
||||||
api_key = api_key.strip()
|
api_key = api_key.strip()
|
||||||
if role is not None:
|
if role is not None:
|
||||||
role = role.strip()
|
role = role.strip()
|
||||||
|
|
||||||
# If `role` doesn't look like an email, it might be a uuid.
|
|
||||||
if settings.ZILENCER_ENABLED and role is not None and "@" not in role:
|
|
||||||
from zilencer.auth import validate_remote_server
|
|
||||||
|
|
||||||
return validate_remote_server(request, role, api_key)
|
|
||||||
|
|
||||||
user_profile = access_user_by_api_key(request, api_key, email=role)
|
user_profile = access_user_by_api_key(request, api_key, email=role)
|
||||||
if user_profile.is_incoming_webhook and not allow_webhook_access:
|
if user_profile.is_incoming_webhook and not allow_webhook_access:
|
||||||
raise JsonableError(_("This API is not available to incoming webhook bots."))
|
raise JsonableError(_("This API is not available to incoming webhook bots."))
|
||||||
|
@ -749,7 +740,6 @@ def authenticated_rest_api_view(
|
||||||
|
|
||||||
# Now we try to do authentication or die
|
# Now we try to do authentication or die
|
||||||
try:
|
try:
|
||||||
# profile is a Union[UserProfile, RemoteZulipServer]
|
|
||||||
profile = validate_api_key(
|
profile = validate_api_key(
|
||||||
request,
|
request,
|
||||||
role,
|
role,
|
||||||
|
|
|
@ -2015,6 +2015,11 @@ class RestAPITest(ZulipTestCase):
|
||||||
self.assertEqual(result.status_code, 405)
|
self.assertEqual(result.status_code, 405)
|
||||||
self.assert_in_response("Method Not Allowed", result)
|
self.assert_in_response("Method Not Allowed", result)
|
||||||
|
|
||||||
|
with self.settings(ZILENCER_ENABLED=True):
|
||||||
|
result = self.client_patch("/json/remotes/push/register")
|
||||||
|
self.assertEqual(result.status_code, 405)
|
||||||
|
self.assert_in_response("Method Not Allowed", result)
|
||||||
|
|
||||||
def test_options_method(self) -> None:
|
def test_options_method(self) -> None:
|
||||||
self.login("hamlet")
|
self.login("hamlet")
|
||||||
result = self.client_options("/json/users")
|
result = self.client_options("/json/users")
|
||||||
|
|
|
@ -205,22 +205,6 @@ class PushBouncerNotificationTest(BouncerTestCase):
|
||||||
|
|
||||||
hamlet = self.example_user("hamlet")
|
hamlet = self.example_user("hamlet")
|
||||||
|
|
||||||
with self.assertLogs(level="WARNING") as warn_log:
|
|
||||||
result = self.api_post(
|
|
||||||
hamlet,
|
|
||||||
endpoint,
|
|
||||||
dict(user_id=user_id, token_kin=token_kind, token=token),
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
warn_log.output,
|
|
||||||
[
|
|
||||||
"WARNING:root:User hamlet@zulip.com (zulip) attempted to access API on wrong subdomain ()"
|
|
||||||
],
|
|
||||||
)
|
|
||||||
self.assert_json_error(
|
|
||||||
result, "Account is not associated with this subdomain", status_code=401
|
|
||||||
)
|
|
||||||
|
|
||||||
# We need the root ('') subdomain to be in use for this next
|
# We need the root ('') subdomain to be in use for this next
|
||||||
# test, since the push bouncer API is only available there:
|
# test, since the push bouncer API is only available there:
|
||||||
realm = get_realm("zulip")
|
realm = get_realm("zulip")
|
||||||
|
|
|
@ -1,14 +1,29 @@
|
||||||
from django.http import HttpRequest
|
from functools import wraps
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
from django.urls import path
|
||||||
|
from django.urls.resolvers import URLPattern
|
||||||
from django.utils.crypto import constant_time_compare
|
from django.utils.crypto import constant_time_compare
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
from typing_extensions import Concatenate, ParamSpec
|
||||||
|
|
||||||
from zerver.decorator import process_client
|
from zerver.decorator import get_basic_credentials, process_client
|
||||||
from zerver.lib.exceptions import ErrorCode, JsonableError, RemoteServerDeactivatedError
|
from zerver.lib.exceptions import (
|
||||||
|
ErrorCode,
|
||||||
|
JsonableError,
|
||||||
|
RemoteServerDeactivatedError,
|
||||||
|
UnauthorizedError,
|
||||||
|
)
|
||||||
|
from zerver.lib.rate_limiter import rate_limit
|
||||||
from zerver.lib.request import RequestNotes
|
from zerver.lib.request import RequestNotes
|
||||||
|
from zerver.lib.rest import get_target_view_function_or_response
|
||||||
from zerver.lib.subdomains import get_subdomain
|
from zerver.lib.subdomains import get_subdomain
|
||||||
from zerver.models import Realm
|
from zerver.models import Realm
|
||||||
from zilencer.models import RemoteZulipServer, get_remote_server_by_uuid
|
from zilencer.models import RemoteZulipServer, get_remote_server_by_uuid
|
||||||
|
|
||||||
|
ParamT = ParamSpec("ParamT")
|
||||||
|
|
||||||
|
|
||||||
class InvalidZulipServerError(JsonableError):
|
class InvalidZulipServerError(JsonableError):
|
||||||
code = ErrorCode.INVALID_ZULIP_SERVER
|
code = ErrorCode.INVALID_ZULIP_SERVER
|
||||||
|
@ -48,3 +63,39 @@ def validate_remote_server(
|
||||||
RequestNotes.get_notes(request).remote_server = remote_server
|
RequestNotes.get_notes(request).remote_server = remote_server
|
||||||
process_client(request)
|
process_client(request)
|
||||||
return remote_server
|
return remote_server
|
||||||
|
|
||||||
|
|
||||||
|
def authenticated_remote_server_view(
|
||||||
|
view_func: Callable[Concatenate[HttpRequest, RemoteZulipServer, ParamT], HttpResponse]
|
||||||
|
) -> Callable[Concatenate[HttpRequest, ParamT], HttpResponse]:
|
||||||
|
@wraps(view_func)
|
||||||
|
def _wrapped_view_func(
|
||||||
|
request: HttpRequest, /, *args: ParamT.args, **kwargs: ParamT.kwargs
|
||||||
|
) -> HttpResponse:
|
||||||
|
role, api_key = get_basic_credentials(request)
|
||||||
|
if "@" in role:
|
||||||
|
raise JsonableError(_("Must validate with valid Zulip server API key"))
|
||||||
|
try:
|
||||||
|
remote_server = validate_remote_server(request, role, api_key)
|
||||||
|
except JsonableError as e:
|
||||||
|
raise UnauthorizedError(e.msg)
|
||||||
|
|
||||||
|
rate_limit(request)
|
||||||
|
return view_func(request, remote_server, *args, **kwargs)
|
||||||
|
|
||||||
|
return _wrapped_view_func
|
||||||
|
|
||||||
|
|
||||||
|
def remote_server_dispatch(request: HttpRequest, **kwargs: Any) -> HttpResponse:
|
||||||
|
result = get_target_view_function_or_response(request, kwargs)
|
||||||
|
if isinstance(result, HttpResponse):
|
||||||
|
return result
|
||||||
|
target_function, view_flags = result
|
||||||
|
return authenticated_remote_server_view(target_function)(request, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def remote_server_path(
|
||||||
|
route: str,
|
||||||
|
**handlers: Callable[Concatenate[HttpRequest, RemoteZulipServer, ParamT], HttpResponse],
|
||||||
|
) -> URLPattern:
|
||||||
|
return path(route, remote_server_dispatch, handlers)
|
||||||
|
|
|
@ -3,7 +3,7 @@ from typing import Any
|
||||||
from django.conf.urls import include
|
from django.conf.urls import include
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from zerver.lib.rest import rest_path
|
from zilencer.auth import remote_server_path
|
||||||
from zilencer.views import (
|
from zilencer.views import (
|
||||||
deactivate_remote_server,
|
deactivate_remote_server,
|
||||||
register_remote_push_device,
|
register_remote_push_device,
|
||||||
|
@ -19,16 +19,16 @@ i18n_urlpatterns: Any = []
|
||||||
|
|
||||||
# Zilencer views following the REST API style
|
# Zilencer views following the REST API style
|
||||||
v1_api_and_json_patterns = [
|
v1_api_and_json_patterns = [
|
||||||
rest_path("remotes/push/register", POST=register_remote_push_device),
|
remote_server_path("remotes/push/register", POST=register_remote_push_device),
|
||||||
rest_path("remotes/push/unregister", POST=unregister_remote_push_device),
|
remote_server_path("remotes/push/unregister", POST=unregister_remote_push_device),
|
||||||
rest_path("remotes/push/unregister/all", POST=unregister_all_remote_push_devices),
|
remote_server_path("remotes/push/unregister/all", POST=unregister_all_remote_push_devices),
|
||||||
rest_path("remotes/push/notify", POST=remote_server_notify_push),
|
remote_server_path("remotes/push/notify", POST=remote_server_notify_push),
|
||||||
# Push signup doesn't use the REST API, since there's no auth.
|
# Push signup doesn't use the REST API, since there's no auth.
|
||||||
path("remotes/server/register", register_remote_server),
|
path("remotes/server/register", register_remote_server),
|
||||||
rest_path("remotes/server/deactivate", POST=deactivate_remote_server),
|
remote_server_path("remotes/server/deactivate", POST=deactivate_remote_server),
|
||||||
# For receiving table data used in analytics and billing
|
# For receiving table data used in analytics and billing
|
||||||
rest_path("remotes/server/analytics", POST=remote_server_post_analytics),
|
remote_server_path("remotes/server/analytics", POST=remote_server_post_analytics),
|
||||||
rest_path("remotes/server/analytics/status", GET=remote_server_check_analytics),
|
remote_server_path("remotes/server/analytics/status", GET=remote_server_check_analytics),
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
Loading…
Reference in New Issue