realm: Add uuid and associated secret columns.

Thisi and the following commit follow the approach used in
3e2ad84bbe.

First migration requires a server restart - after that any new realms
will be created with the columns set.

The following migrations are in the next commit:

Second migration does a backfill for older realms and can run in the
background while the server is operating normally.

Third migration enforces null=False now that all realms have the columns
set.
This commit is contained in:
Mateusz Mandera 2023-10-12 20:10:07 +02:00 committed by Tim Abbott
parent 23f53752b2
commit 3afe585922
3 changed files with 61 additions and 12 deletions

View File

@ -0,0 +1,22 @@
# Generated by Django 4.2.5 on 2023-10-06 09:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("zerver", "0478_realm_enable_guest_user_indicator"),
]
operations = [
migrations.AddField(
model_name="realm",
name="uuid",
field=models.UUIDField(null=True, unique=True),
),
migrations.AddField(
model_name="realm",
name="uuid_owner_secret",
field=models.TextField(null=True),
),
]

View File

@ -275,6 +275,14 @@ class RealmAuthenticationMethod(models.Model):
unique_together = ("realm", "name") unique_together = ("realm", "name")
def generate_realm_uuid_owner_secret() -> str:
token = generate_api_key()
# We include a prefix to facilitate scanning for accidental
# disclosure of secrets e.g. in Github commit pushes.
return f"zuliprealm_{token}"
class Realm(models.Model): # type: ignore[django-manager-missing] # django-stubs cannot resolve the custom CTEManager yet https://github.com/typeddjango/django-stubs/issues/1023 class Realm(models.Model): # type: ignore[django-manager-missing] # django-stubs cannot resolve the custom CTEManager yet https://github.com/typeddjango/django-stubs/issues/1023
MAX_REALM_NAME_LENGTH = 40 MAX_REALM_NAME_LENGTH = 40
MAX_REALM_DESCRIPTION_LENGTH = 1000 MAX_REALM_DESCRIPTION_LENGTH = 1000
@ -295,6 +303,11 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub
# at `foo.example.com`. # at `foo.example.com`.
string_id = models.CharField(max_length=MAX_REALM_SUBDOMAIN_LENGTH, unique=True) string_id = models.CharField(max_length=MAX_REALM_SUBDOMAIN_LENGTH, unique=True)
# uuid and a secret for the sake of per-realm authentication with the push notification
# bouncer.
uuid = models.UUIDField(default=uuid4, unique=True)
uuid_owner_secret = models.TextField(default=generate_realm_uuid_owner_secret)
date_created = models.DateTimeField(default=timezone_now) date_created = models.DateTimeField(default=timezone_now)
demo_organization_scheduled_deletion_date = models.DateTimeField(default=None, null=True) demo_organization_scheduled_deletion_date = models.DateTimeField(default=None, null=True)
deactivated = models.BooleanField(default=False) deactivated = models.BooleanField(default=False)

View File

@ -1,6 +1,7 @@
import datetime import datetime
import os import os
import shutil import shutil
import uuid
from collections import defaultdict from collections import defaultdict
from typing import Any, Callable, Dict, FrozenSet, Iterable, List, Optional, Set, Tuple from typing import Any, Callable, Dict, FrozenSet, Iterable, List, Optional, Set, Tuple
from unittest.mock import patch from unittest.mock import patch
@ -329,6 +330,13 @@ class RealmImportExportTest(ExportFile):
consent_message_id=consent_message_id, consent_message_id=consent_message_id,
public_only=public_only, public_only=public_only,
) )
# This is a unique field and thus the cycle of export->import
# within the same server (which is what happens in our tests)
# will cause a conflict - so rotate it.
realm.uuid = uuid.uuid4()
realm.save()
export_usermessages_batch( export_usermessages_batch(
input_path=os.path.join(output_dir, "messages-000001.json.partial"), input_path=os.path.join(output_dir, "messages-000001.json.partial"),
output_path=os.path.join(output_dir, "messages-000001.json"), output_path=os.path.join(output_dir, "messages-000001.json"),
@ -1538,25 +1546,31 @@ class RealmImportExportTest(ExportFile):
self.export_realm(realm) self.export_realm(realm)
with self.settings(BILLING_ENABLED=True), self.assertLogs(level="INFO"): with self.settings(BILLING_ENABLED=True), self.assertLogs(level="INFO"):
realm = do_import_realm(get_output_dir(), "test-zulip-1") imported_realm = do_import_realm(get_output_dir(), "test-zulip-1")
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_LIMITED) self.assertEqual(imported_realm.plan_type, Realm.PLAN_TYPE_LIMITED)
self.assertEqual(realm.max_invites, 100) self.assertEqual(imported_realm.max_invites, 100)
self.assertEqual(realm.upload_quota_gb, 5) self.assertEqual(imported_realm.upload_quota_gb, 5)
self.assertEqual(realm.message_visibility_limit, 10000) self.assertEqual(imported_realm.message_visibility_limit, 10000)
self.assertTrue( self.assertTrue(
RealmAuditLog.objects.filter( RealmAuditLog.objects.filter(
realm=realm, event_type=RealmAuditLog.REALM_PLAN_TYPE_CHANGED realm=imported_realm, event_type=RealmAuditLog.REALM_PLAN_TYPE_CHANGED
).exists() ).exists()
) )
# Importing the same export data twice would cause conflict on unique fields,
# so instead re-export the original realm via self.export_realm, which handles
# this issue.
self.export_realm(realm)
with self.settings(BILLING_ENABLED=False), self.assertLogs(level="INFO"): with self.settings(BILLING_ENABLED=False), self.assertLogs(level="INFO"):
realm = do_import_realm(get_output_dir(), "test-zulip-2") imported_realm = do_import_realm(get_output_dir(), "test-zulip-2")
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_SELF_HOSTED) self.assertEqual(imported_realm.plan_type, Realm.PLAN_TYPE_SELF_HOSTED)
self.assertEqual(realm.max_invites, 100) self.assertEqual(imported_realm.max_invites, 100)
self.assertEqual(realm.upload_quota_gb, None) self.assertEqual(imported_realm.upload_quota_gb, None)
self.assertEqual(realm.message_visibility_limit, None) self.assertEqual(imported_realm.message_visibility_limit, None)
self.assertTrue( self.assertTrue(
RealmAuditLog.objects.filter( RealmAuditLog.objects.filter(
realm=realm, event_type=RealmAuditLog.REALM_PLAN_TYPE_CHANGED realm=imported_realm, event_type=RealmAuditLog.REALM_PLAN_TYPE_CHANGED
).exists() ).exists()
) )