diff --git a/zerver/tests/test_openapi.py b/zerver/tests/test_openapi.py index 51a1ddcc17..94acc0ca21 100644 --- a/zerver/tests/test_openapi.py +++ b/zerver/tests/test_openapi.py @@ -30,6 +30,7 @@ from zerver.openapi.openapi import ( validate_schema, ) from zerver.tornado.views import get_events, get_events_backend +from zilencer.auth import remote_server_dispatch TEST_ENDPOINT = "/messages/{message_id}" TEST_METHOD = "patch" @@ -260,6 +261,18 @@ class OpenAPIArgumentsTest(ZulipTestCase): # Zulip outgoing webhook payload "/zulip-outgoing-webhook", "/jwt/fetch_api_key", + #### Bouncer endpoints + # Higher priority to document + "/remotes/push/e2ee/register", + "/remotes/push/e2ee/notify", + # Lower priority to document + "/remotes/server/register", + "/remotes/server/register/transfer", + "/remotes/server/register/verify_challenge", + "/remotes/server/deactivate", + "/remotes/server/analytics", + "/remotes/server/analytics/status", + "/remotes/server/billing", } # Endpoints in the API documentation that don't use rest_dispatch @@ -530,8 +543,8 @@ so maybe we shouldn't include it in pending_endpoints. with the arguments declared in our API documentation for every API endpoint in Zulip. - First, we import the fancy-Django version of zproject/urls.py - by doing this, each typed_endpoint wrapper around each + First, we import the fancy-Django version of zproject/urls.py and + zilencer/urls.py. By doing this, each typed_endpoint wrapper around each imported view function gets called to generate the wrapped view function and thus filling the global arguments_map variable. Basically, we're exploiting code execution during import. @@ -546,15 +559,21 @@ so maybe we shouldn't include it in pending_endpoints. in code. """ + from zilencer import urls as zilencer_urlconf from zproject import urls as urlconf # We loop through all the API patterns, looking in particular - # for those using the rest_dispatch decorator; we then parse - # its mapping of (HTTP_METHOD -> FUNCTION). - for p in urlconf.v1_api_and_json_patterns + urlconf.v1_api_mobile_patterns: + # for those using the rest_dispatch or remote_server_dispatch decorator; + # we then parse its mapping of (HTTP_METHOD -> FUNCTION). + for p in ( + urlconf.v1_api_and_json_patterns + + urlconf.v1_api_mobile_patterns + + zilencer_urlconf.v1_api_bouncer_patterns + ): methods_endpoints: dict[str, Any] = {} - if p.callback is not rest_dispatch: - # Endpoints not using rest_dispatch don't have extra data. + if p.callback not in [rest_dispatch, remote_server_dispatch]: + # Endpoints not using rest_dispatch or remote_server_dispatch + # don't have extra data. if str(p.pattern) in self.documented_post_only_endpoints: methods_endpoints = dict(POST=p.callback) else: diff --git a/zilencer/auth.py b/zilencer/auth.py index 4800039ee7..24e0239cfe 100644 --- a/zilencer/auth.py +++ b/zilencer/auth.py @@ -172,6 +172,7 @@ def remote_server_dispatch(request: HttpRequest, /, **kwargs: Any) -> HttpRespon def remote_server_path( route: str, - **handlers: Callable[Concatenate[HttpRequest, RemoteZulipServer, ParamT], HttpResponse], + **handlers: Callable[Concatenate[HttpRequest, RemoteZulipServer, ParamT], HttpResponse] + | tuple[Callable[Concatenate[HttpRequest, RemoteZulipServer, ParamT], HttpResponse], set[str]], ) -> URLPattern: return path(route, remote_server_dispatch, handlers) diff --git a/zilencer/urls.py b/zilencer/urls.py index 34f26bff87..288f978d13 100644 --- a/zilencer/urls.py +++ b/zilencer/urls.py @@ -24,16 +24,31 @@ from zilencer.views import ( i18n_urlpatterns: Any = [] # Zilencer views following the REST API style +# The endpoints marked "intentionally_undocumented" are part of the older system +# for sending non-E2EE push notifications, and will be removed in the future. push_bouncer_patterns = [ - remote_server_path("remotes/push/register", POST=register_remote_push_device), + remote_server_path( + "remotes/push/register", POST=(register_remote_push_device, {"intentionally_undocumented"}) + ), remote_server_path( "remotes/push/e2ee/register", POST=register_remote_push_device_for_e2ee_push_notification ), - remote_server_path("remotes/push/unregister", POST=unregister_remote_push_device), - remote_server_path("remotes/push/unregister/all", POST=unregister_all_remote_push_devices), - remote_server_path("remotes/push/notify", POST=remote_server_notify_push), + remote_server_path( + "remotes/push/unregister", + POST=(unregister_remote_push_device, {"intentionally_undocumented"}), + ), + remote_server_path( + "remotes/push/unregister/all", + POST=(unregister_all_remote_push_devices, {"intentionally_undocumented"}), + ), + remote_server_path( + "remotes/push/notify", POST=(remote_server_notify_push, {"intentionally_undocumented"}) + ), remote_server_path("remotes/push/e2ee/notify", POST=remote_server_send_e2ee_push_notification), - remote_server_path("remotes/push/test_notification", POST=remote_server_send_test_notification), + remote_server_path( + "remotes/push/test_notification", + POST=(remote_server_send_test_notification, {"intentionally_undocumented"}), + ), # Push signup doesn't use the REST API, since there's no auth. path("remotes/server/register", register_remote_server), path("remotes/server/register/transfer", transfer_remote_server_registration), @@ -49,7 +64,8 @@ push_bouncer_patterns = [ billing_patterns = [remote_server_path("remotes/server/billing", POST=remote_realm_billing_entry)] +v1_api_bouncer_patterns = push_bouncer_patterns + billing_patterns + urlpatterns = [ - path("api/v1/", include(push_bouncer_patterns)), - path("api/v1/", include(billing_patterns)), + path("api/v1/", include(v1_api_bouncer_patterns)), ]