From cbfbdd73372a865d576a08b7e54e8746e765b5e4 Mon Sep 17 00:00:00 2001 From: Mateusz Mandera Date: Mon, 25 Dec 2023 03:01:58 +0100 Subject: [PATCH] zilencer: Add last_request_datetime to RemoteRealm + RemoteZulipServer. For the RemoteRealm case, we can only set this in endpoints where the remote server sends us the realm_uuid. So we're missing that for the endpoints: - remotes/push/unregister and remotes/push/unregister/all - remotes/push/test_notification This should be added in a follow-up commit. --- zerver/tests/test_push_notifications.py | 46 +++++++++++++++---- zilencer/auth.py | 5 ++ ...terealm_last_request_timestamp_and_more.py | 22 +++++++++ zilencer/models.py | 2 + zilencer/views.py | 9 ++++ 5 files changed, 75 insertions(+), 9 deletions(-) create mode 100644 zilencer/migrations/0057_remoterealm_last_request_timestamp_and_more.py diff --git a/zerver/tests/test_push_notifications.py b/zerver/tests/test_push_notifications.py index d71546fc8e..caef440fc5 100644 --- a/zerver/tests/test_push_notifications.py +++ b/zerver/tests/test_push_notifications.py @@ -606,6 +606,8 @@ class PushBouncerNotificationTest(BouncerTestCase): def test_send_notification_endpoint(self) -> None: hamlet = self.example_user("hamlet") server = self.server + remote_realm = RemoteRealm.objects.get(server=server, uuid=hamlet.realm.uuid) + token = "aaaa" android_tokens = [] uuid_android_tokens = [] @@ -649,6 +651,8 @@ class PushBouncerNotificationTest(BouncerTestCase): }, "gcm_options": {}, } + + time_sent = now() with mock.patch( "zilencer.views.send_android_push_notification", return_value=2 ) as android_push, mock.patch( @@ -656,6 +660,8 @@ class PushBouncerNotificationTest(BouncerTestCase): ) as apple_push, mock.patch( "corporate.lib.stripe.RemoteServerBillingSession.current_count_for_billed_licenses", return_value=10, + ), time_machine.travel( + time_sent, tick=False ), self.assertLogs( "zilencer.views", level="INFO" ) as logger: @@ -711,6 +717,11 @@ class PushBouncerNotificationTest(BouncerTestCase): remote=server, ) + remote_realm.refresh_from_db() + server.refresh_from_db() + self.assertEqual(remote_realm.last_request_datetime, time_sent) + self.assertEqual(server.last_request_datetime, time_sent) + def test_send_notification_endpoint_on_free_plans(self) -> None: hamlet = self.example_user("hamlet") remote_server = self.server @@ -1221,12 +1232,14 @@ class PushBouncerNotificationTest(BouncerTestCase): # normal setup. update_remote_realm_data_for_server(self.server, get_realms_info_for_push_bouncer()) - # Test that we can push more times - result = self.client_post(endpoint, {"token": token, **appid}, subdomain="zulip") - self.assert_json_success(result) + time_sent = now() + with time_machine.travel(time_sent, tick=False): + result = self.client_post(endpoint, {"token": token, **appid}, subdomain="zulip") + self.assert_json_success(result) - result = self.client_post(endpoint, {"token": token, **appid}, subdomain="zulip") - self.assert_json_success(result) + # Test that we can push more times + result = self.client_post(endpoint, {"token": token, **appid}, subdomain="zulip") + self.assert_json_success(result) tokens = list( RemotePushDeviceToken.objects.filter( @@ -1237,9 +1250,16 @@ class PushBouncerNotificationTest(BouncerTestCase): self.assertEqual(tokens[0].kind, kind) # These new registrations have .remote_realm set properly. assert tokens[0].remote_realm is not None - self.assertEqual(tokens[0].remote_realm.uuid, user.realm.uuid) + remote_realm = tokens[0].remote_realm + self.assertEqual(remote_realm.uuid, user.realm.uuid) self.assertEqual(tokens[0].ios_app_id, appid.get("appid")) + # Both RemoteRealm and RemoteZulipServer should have last_request_datetime + # updated. + self.assertEqual(remote_realm.last_request_datetime, time_sent) + server.refresh_from_db() + self.assertEqual(server.last_request_datetime, time_sent) + # User should have tokens for both devices now. tokens = list(RemotePushDeviceToken.objects.filter(user_uuid=user.uuid, server=server)) self.assert_length(tokens, 2) @@ -4749,11 +4769,16 @@ class PushBouncerSignupTest(ZulipTestCase): hostname="example.com", contact_email="server-admin@example.com", ) - result = self.client_post("/api/v1/remotes/server/register", request) + + time_sent = now() + + with time_machine.travel(time_sent, tick=False): + result = self.client_post("/api/v1/remotes/server/register", request) self.assert_json_success(result) server = RemoteZulipServer.objects.get(uuid=zulip_org_id) self.assertEqual(server.hostname, "example.com") self.assertEqual(server.contact_email, "server-admin@example.com") + self.assertEqual(server.last_request_datetime, time_sent) # Update our hostname request = dict( @@ -4762,11 +4787,14 @@ class PushBouncerSignupTest(ZulipTestCase): hostname="zulip.example.com", contact_email="server-admin@example.com", ) - result = self.client_post("/api/v1/remotes/server/register", request) + + with time_machine.travel(time_sent + timedelta(minutes=1), tick=False): + result = self.client_post("/api/v1/remotes/server/register", request) self.assert_json_success(result) server = RemoteZulipServer.objects.get(uuid=zulip_org_id) self.assertEqual(server.hostname, "zulip.example.com") self.assertEqual(server.contact_email, "server-admin@example.com") + self.assertEqual(server.last_request_datetime, time_sent + timedelta(minutes=1)) # Now test rotating our key request = dict( @@ -4784,7 +4812,7 @@ class PushBouncerSignupTest(ZulipTestCase): zulip_org_key = request["new_org_key"] self.assertEqual(server.api_key, zulip_org_key) - # Update our hostname + # Update contact_email request = dict( zulip_org_id=zulip_org_id, zulip_org_key=zulip_org_key, diff --git a/zilencer/auth.py b/zilencer/auth.py index 9136dc986a..e69e90a32b 100644 --- a/zilencer/auth.py +++ b/zilencer/auth.py @@ -8,6 +8,7 @@ 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.timezone import now as timezone_now from django.utils.translation import gettext as _ from django.views.decorators.csrf import csrf_exempt from typing_extensions import Concatenate, ParamSpec, override @@ -120,6 +121,10 @@ def authenticated_remote_server_view( raise UnauthorizedError(e.msg) rate_limit_remote_server(request, remote_server, domain="api_by_remote_server") + + remote_server.last_request_datetime = timezone_now() + remote_server.save(update_fields=["last_request_datetime"]) + return view_func(request, remote_server, *args, **kwargs) return _wrapped_view_func diff --git a/zilencer/migrations/0057_remoterealm_last_request_timestamp_and_more.py b/zilencer/migrations/0057_remoterealm_last_request_timestamp_and_more.py new file mode 100644 index 0000000000..f2a76de285 --- /dev/null +++ b/zilencer/migrations/0057_remoterealm_last_request_timestamp_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.8 on 2023-12-25 00:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("zilencer", "0056_remoterealm_realm_locally_deleted"), + ] + + operations = [ + migrations.AddField( + model_name="remoterealm", + name="last_request_datetime", + field=models.DateTimeField(null=True), + ), + migrations.AddField( + model_name="remotezulipserver", + name="last_request_datetime", + field=models.DateTimeField(null=True), + ), + ] diff --git a/zilencer/models.py b/zilencer/models.py index af0a2eb751..1c6d8091b6 100644 --- a/zilencer/models.py +++ b/zilencer/models.py @@ -47,6 +47,7 @@ class RemoteZulipServer(models.Model): hostname = models.CharField(max_length=HOSTNAME_MAX_LENGTH) contact_email = models.EmailField(blank=True, null=False) last_updated = models.DateTimeField("last updated", auto_now=True) + last_request_datetime = models.DateTimeField(null=True) last_version = models.CharField(max_length=VERSION_MAX_LENGTH, null=True) last_api_feature_level = models.PositiveIntegerField(null=True) @@ -142,6 +143,7 @@ class RemoteRealm(models.Model): # The fields below are analogical to RemoteZulipServer fields. last_updated = models.DateTimeField("last updated", auto_now=True) + last_request_datetime = models.DateTimeField(null=True) # Whether the realm registration has been deactivated. registration_deactivated = models.BooleanField(default=False) diff --git a/zilencer/views.py b/zilencer/views.py index 46f5b9acb9..88fdb5f910 100644 --- a/zilencer/views.py +++ b/zilencer/views.py @@ -144,6 +144,7 @@ def register_remote_server( "hostname": hostname, "contact_email": contact_email, "api_key": zulip_org_key, + "last_request_datetime": timezone_now(), }, ) if created: @@ -163,6 +164,8 @@ def register_remote_server( remote_server.contact_email = contact_email if new_org_key is not None: remote_server.api_key = new_org_key + + remote_server.last_request_datetime = timezone_now() remote_server.save() return json_success(request, data={"created": created}) @@ -207,6 +210,9 @@ def register_remote_push_device( # We want to associate the RemotePushDeviceToken with the RemoteRealm. kwargs["remote_realm_id"] = remote_realm.id + remote_realm.last_request_datetime = timezone_now() + remote_realm.save(update_fields=["last_request_datetime"]) + try: with transaction.atomic(): RemotePushDeviceToken.objects.create( @@ -505,6 +511,9 @@ def remote_server_notify_push( increment=len(android_devices) + len(apple_devices), ) + remote_realm.last_request_datetime = timezone_now() + remote_realm.save(update_fields=["last_request_datetime"]) + # Truncate incoming pushes to 200, due to APNs maximum message # sizes; see handle_remove_push_notification for the version of # this for notifications generated natively on the server. We