remote_billing_page: Show error page for registration mismatch.

When a self-hosted Zulip server does a data export and then import
process into a different hosting environment (i.e. not sharing the
RemoteZulipServer with the original, we'll have various things that
fail where we look up the RemoteRealm by UUID and find it but the
RemoteZulipServer it is associated with is the wrong one.

Right now, we ask user to contact support via an error page but
might develop UI to help user do the migration directly.
This commit is contained in:
Aman Agrawal 2023-12-10 10:59:28 +00:00 committed by Tim Abbott
parent 82db8e7ac5
commit ac8d5a5f0b
7 changed files with 85 additions and 14 deletions

View File

@ -34,6 +34,7 @@ from zerver.lib.exceptions import (
JsonableError, JsonableError,
MissingRemoteRealmError, MissingRemoteRealmError,
RemoteBillingAuthenticationError, RemoteBillingAuthenticationError,
RemoteRealmServerMismatchError,
) )
from zerver.lib.remote_server import RealmDataForAnalytics, UserDataForRemoteBilling from zerver.lib.remote_server import RealmDataForAnalytics, UserDataForRemoteBilling
from zerver.lib.response import json_success from zerver.lib.response import json_success
@ -72,13 +73,21 @@ def remote_realm_billing_entry(
) -> HttpResponse: ) -> HttpResponse:
if not settings.DEVELOPMENT: if not settings.DEVELOPMENT:
return render(request, "404.html", status=404) return render(request, "404.html", status=404)
try: try:
remote_realm = RemoteRealm.objects.get(uuid=realm.uuid, server=remote_server) remote_realm = RemoteRealm.objects.get(uuid=realm.uuid, server=remote_server)
except RemoteRealm.DoesNotExist: except RemoteRealm.DoesNotExist:
# This error will prod the remote server to submit its realm info, which if RemoteRealm.objects.filter(uuid=realm.uuid).exists(): # nocoverage
# should lead to the creation of this missing RemoteRealm registration. billing_logger.warning(
raise MissingRemoteRealmError "%s: Realm %s exists, but not registered to server %s",
request.path,
realm.uuid,
remote_server.id,
)
raise RemoteRealmServerMismatchError
else:
# This error will prod the remote server to submit its realm info, which
# should lead to the creation of this missing RemoteRealm registration.
raise MissingRemoteRealmError
identity_dict = RemoteBillingIdentityDict( identity_dict = RemoteBillingIdentityDict(
user=RemoteBillingUserDict( user=RemoteBillingUserDict(

View File

@ -0,0 +1,31 @@
{% extends "zerver/portico.html" %}
{% block title %}
<title>{{ _("Error") }} | Zulip</title>
{% endblock %}
{% block portico_class_name %}error{% endblock %}
{% block portico_content %}
<div class="error_page">
<div class="container">
<div class="row-fluid">
<img src="{{ static('images/errors/400art.svg') }}" alt=""/>
<div class="errorbox">
<div class="errorcontent">
<h1 class="lead">{{ _("Unexpected Zulip server registration") }}</h1>
<p>
{% trans %}
Your Zulip organization is registered as associated with a
different Zulip server installation.
Please <a href="mailto:{{ support_email }}">contact Zulip support</a>
for assistance in resolving this issue.
{% endtrans %}
</p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -50,6 +50,7 @@ class ErrorCode(Enum):
TOPIC_WILDCARD_MENTION_NOT_ALLOWED = auto() TOPIC_WILDCARD_MENTION_NOT_ALLOWED = auto()
STREAM_WILDCARD_MENTION_NOT_ALLOWED = auto() STREAM_WILDCARD_MENTION_NOT_ALLOWED = auto()
REMOTE_BILLING_UNAUTHENTICATED_USER = auto() REMOTE_BILLING_UNAUTHENTICATED_USER = auto()
REMOTE_REALM_SERVER_MISMATCH_ERROR = auto()
class JsonableError(Exception): class JsonableError(Exception):
@ -592,6 +593,21 @@ class ServerNotReadyError(JsonableError):
http_status_code = 500 http_status_code = 500
class RemoteRealmServerMismatchError(JsonableError): # nocoverage
code = ErrorCode.REMOTE_REALM_SERVER_MISMATCH_ERROR
http_status_code = 403
def __init__(self) -> None:
pass
@staticmethod
@override
def msg_format() -> str:
return _(
"Your organization is registered to a different Zulip server. Please contact Zulip support for assistance in resolving this issue."
)
class MissingRemoteRealmError(JsonableError): # nocoverage class MissingRemoteRealmError(JsonableError): # nocoverage
code: ErrorCode = ErrorCode.MISSING_REMOTE_REALM code: ErrorCode = ErrorCode.MISSING_REMOTE_REALM
http_status_code = 403 http_status_code = 403

View File

@ -11,7 +11,11 @@ from pydantic import UUID4, BaseModel, ConfigDict, Field, Json, field_validator
from analytics.models import InstallationCount, RealmCount from analytics.models import InstallationCount, RealmCount
from version import API_FEATURE_LEVEL, ZULIP_VERSION from version import API_FEATURE_LEVEL, ZULIP_VERSION
from zerver.lib.exceptions import JsonableError, MissingRemoteRealmError from zerver.lib.exceptions import (
JsonableError,
MissingRemoteRealmError,
RemoteRealmServerMismatchError,
)
from zerver.lib.outgoing_http import OutgoingSession from zerver.lib.outgoing_http import OutgoingSession
from zerver.lib.queue import queue_json_publish from zerver.lib.queue import queue_json_publish
from zerver.lib.types import RemoteRealmDictValue from zerver.lib.types import RemoteRealmDictValue
@ -195,6 +199,14 @@ def send_to_push_bouncer(
# The callers requesting this endpoint want the exception to propagate # The callers requesting this endpoint want the exception to propagate
# so they can catch it. # so they can catch it.
raise MissingRemoteRealmError raise MissingRemoteRealmError
elif (
endpoint == "server/billing"
and "code" in result_dict
and result_dict["code"] == "REMOTE_REALM_SERVER_MISMATCH_ERROR"
): # nocoverage
# The callers requesting this endpoint want the exception to propagate
# so they can catch it.
raise RemoteRealmServerMismatchError
else: else:
# But most other errors coming from the push bouncer # But most other errors coming from the push bouncer
# server are client errors (e.g. never-registered token) # server are client errors (e.g. never-registered token)

View File

@ -816,8 +816,8 @@ class PushBouncerNotificationTest(BouncerTestCase):
# is processing the token registration request, it will find a RemoteRealm matching # is processing the token registration request, it will find a RemoteRealm matching
# the realm_uuid in the request, but that RemoteRealm will be registered to a # the realm_uuid in the request, but that RemoteRealm will be registered to a
# different server than the one making the request (self.server). # different server than the one making the request (self.server).
# This will make it log a warning and register the token, but of course without # This will make it log a warning, raise an exception when trying to get
# assigning the token registration to that RemoteRealm. # remote realm via get_remote_realm_helper and thus, not register the token.
second_server = RemoteZulipServer.objects.create( second_server = RemoteZulipServer.objects.create(
uuid=uuid.uuid4(), uuid=uuid.uuid4(),
api_key="magic_secret_api_key2", api_key="magic_secret_api_key2",
@ -835,7 +835,10 @@ class PushBouncerNotificationTest(BouncerTestCase):
result = self.client_post( result = self.client_post(
endpoint, {"token": token, "appid": "org.zulip.Zulip"}, subdomain="zulip" endpoint, {"token": token, "appid": "org.zulip.Zulip"}, subdomain="zulip"
) )
self.assert_json_success(result) self.assert_json_error_contains(
result,
"Your organization is registered to a different Zulip server. Please contact Zulip support",
)
self.assertEqual( self.assertEqual(
warn_log.output, warn_log.output,
[ [
@ -844,10 +847,7 @@ class PushBouncerNotificationTest(BouncerTestCase):
], ],
) )
remote_token = RemotePushDeviceToken.objects.get(token=token) self.assert_length(RemotePushDeviceToken.objects.filter(token=token), 0)
self.assertEqual(remote_token.server, self.server)
self.assertEqual(remote_token.user_uuid, user.uuid)
self.assertEqual(remote_token.remote_realm, None)
@override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com") @override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com")
@responses.activate @responses.activate

View File

@ -11,6 +11,7 @@ from zerver.lib.exceptions import (
JsonableError, JsonableError,
MissingRemoteRealmError, MissingRemoteRealmError,
OrganizationOwnerRequiredError, OrganizationOwnerRequiredError,
RemoteRealmServerMismatchError,
) )
from zerver.lib.push_notifications import ( from zerver.lib.push_notifications import (
InvalidPushDeviceTokenError, InvalidPushDeviceTokenError,
@ -163,6 +164,8 @@ def self_hosting_auth_redirect(
# Upload realm info and re-try. It should work now. # Upload realm info and re-try. It should work now.
send_realms_only_to_push_bouncer() send_realms_only_to_push_bouncer()
result = send_to_push_bouncer("POST", "server/billing", post_data) result = send_to_push_bouncer("POST", "server/billing", post_data)
except RemoteRealmServerMismatchError:
return render(request, "zilencer/remote_realm_server_mismatch_error.html", status=403)
if result["result"] != "success": if result["result"] != "success":
raise JsonableError(_("Error returned by the bouncer: {result}").format(result=result)) raise JsonableError(_("Error returned by the bouncer: {result}").format(result=result))

View File

@ -26,7 +26,7 @@ from analytics.lib.counts import (
from corporate.lib.stripe import RemoteRealmBillingSession, do_deactivate_remote_server from corporate.lib.stripe import RemoteRealmBillingSession, do_deactivate_remote_server
from corporate.models import CustomerPlan, get_current_plan_by_customer from corporate.models import CustomerPlan, get_current_plan_by_customer
from zerver.decorator import require_post from zerver.decorator import require_post
from zerver.lib.exceptions import JsonableError from zerver.lib.exceptions import JsonableError, RemoteRealmServerMismatchError
from zerver.lib.push_notifications import ( from zerver.lib.push_notifications import (
InvalidRemotePushDeviceTokenError, InvalidRemotePushDeviceTokenError,
UserPushIdentityCompat, UserPushIdentityCompat,
@ -375,7 +375,7 @@ def get_remote_realm_helper(
realm_uuid, realm_uuid,
server.id, server.id,
) )
return None raise RemoteRealmServerMismatchError
return remote_realm return remote_realm