mirror of
https://github.com/zulip/zulip.git
synced 2025-10-23 04:52:12 +00:00
The way the flow goes now is this: 1. The user initiaties login via "Billing" in the gear menu. 2. That takes them to `/self-hosted-billing/` (possibly with a `next_page` param if we use that for some gear menu options). 3. The server queries the bouncer to give the user a link with a signed access token. 4. The user is redirected to that link (on `selfhosting.zulipchat.com`). Now we have two cases, either the user is logging in for the first time and already did in the past. If this is the first time, we have: 5. The user is asked to fill in their email in a form that's shown, pre-filled with the value provided inside the signed access token. They POST this to the next endpoint. 6. The next endpoint sends a confirmation email to that address and asks the user to go check their email. 7. The user clicks the link in their email is taken to the from_confirmation endpoint. 8. Their initial RemoteBillingUser is created, a new signed link like in (3) is generated and they're transparently taken back to (4), where now that they have a RemoteBillingUser, they're handled just like a user who already logged in before: If the user already logged in before, they go straight here: 9. "Confirm login" page - they're shown their information (email and full_name), can update their full name in the form if they want. They also accept ToS here if necessary. They POST this form back to the endpoint and finally have a logged in session. 10. They're redirected to billing (or `next_page`) now that they have access.
165 lines
5.2 KiB
Python
165 lines
5.2 KiB
Python
import logging
|
|
from typing import Literal, Optional, Tuple, TypedDict, Union, cast
|
|
|
|
from django.http import HttpRequest
|
|
from django.utils.timezone import now as timezone_now
|
|
from django.utils.translation import gettext as _
|
|
|
|
from zerver.lib.exceptions import JsonableError, RemoteBillingAuthenticationError
|
|
from zerver.lib.timestamp import datetime_to_timestamp
|
|
from zilencer.models import RemoteRealm, RemoteServerBillingUser, RemoteZulipServer
|
|
|
|
billing_logger = logging.getLogger("corporate.stripe")
|
|
|
|
# The sessions are relatively short-lived, so that we can avoid issues
|
|
# with users who have their privileges revoked on the remote server
|
|
# maintaining access to the billing page for too long.
|
|
REMOTE_BILLING_SESSION_VALIDITY_SECONDS = 2 * 60 * 60
|
|
|
|
|
|
class RemoteBillingUserDict(TypedDict):
|
|
user_uuid: str
|
|
user_email: str
|
|
user_full_name: str
|
|
|
|
|
|
class RemoteBillingIdentityDict(TypedDict):
|
|
user: RemoteBillingUserDict
|
|
remote_server_uuid: str
|
|
remote_realm_uuid: str
|
|
|
|
remote_billing_user_id: Optional[int]
|
|
authenticated_at: int
|
|
uri_scheme: Literal["http://", "https://"]
|
|
|
|
next_page: Optional[str]
|
|
|
|
|
|
class LegacyServerIdentityDict(TypedDict):
|
|
# Currently this has only one field. We can extend this
|
|
# to add more information as appropriate.
|
|
remote_server_uuid: str
|
|
|
|
remote_billing_user_id: Optional[int]
|
|
authenticated_at: int
|
|
|
|
|
|
class RemoteBillingIdentityExpiredError(Exception):
|
|
def __init__(
|
|
self,
|
|
*,
|
|
realm_uuid: Optional[str] = None,
|
|
server_uuid: Optional[str] = None,
|
|
uri_scheme: Optional[Literal["http://", "https://"]] = None,
|
|
) -> None:
|
|
self.realm_uuid = realm_uuid
|
|
self.server_uuid = server_uuid
|
|
self.uri_scheme = uri_scheme
|
|
|
|
|
|
def get_identity_dict_from_session(
|
|
request: HttpRequest,
|
|
*,
|
|
realm_uuid: Optional[str],
|
|
server_uuid: Optional[str],
|
|
) -> Optional[Union[RemoteBillingIdentityDict, LegacyServerIdentityDict]]:
|
|
if not (realm_uuid or server_uuid):
|
|
return None
|
|
|
|
identity_dicts = request.session.get("remote_billing_identities")
|
|
if identity_dicts is None:
|
|
return None
|
|
|
|
if realm_uuid is not None:
|
|
result = identity_dicts.get(f"remote_realm:{realm_uuid}")
|
|
else:
|
|
assert server_uuid is not None
|
|
result = identity_dicts.get(f"remote_server:{server_uuid}")
|
|
|
|
if result is None:
|
|
return None
|
|
if (
|
|
datetime_to_timestamp(timezone_now()) - result["authenticated_at"]
|
|
> REMOTE_BILLING_SESSION_VALIDITY_SECONDS
|
|
):
|
|
# In this case we raise, because callers want to catch this as an explicitly
|
|
# different scenario from the user not being authenticated, to handle it nicely
|
|
# by redirecting them to their login page.
|
|
raise RemoteBillingIdentityExpiredError(
|
|
realm_uuid=result.get("remote_realm_uuid"),
|
|
server_uuid=result.get("remote_server_uuid"),
|
|
uri_scheme=result.get("uri_scheme"),
|
|
)
|
|
|
|
return result
|
|
|
|
|
|
def get_remote_realm_from_session(
|
|
request: HttpRequest,
|
|
realm_uuid: Optional[str],
|
|
) -> RemoteRealm:
|
|
# Cannot use isinstance with TypeDicts, to make mypy know
|
|
# which of the TypedDicts in the Union this is - so just cast it.
|
|
identity_dict = cast(
|
|
Optional[RemoteBillingIdentityDict],
|
|
get_identity_dict_from_session(request, realm_uuid=realm_uuid, server_uuid=None),
|
|
)
|
|
|
|
if identity_dict is None:
|
|
raise RemoteBillingAuthenticationError
|
|
|
|
remote_server_uuid = identity_dict["remote_server_uuid"]
|
|
remote_realm_uuid = identity_dict["remote_realm_uuid"]
|
|
|
|
try:
|
|
remote_realm = RemoteRealm.objects.get(
|
|
uuid=remote_realm_uuid, server__uuid=remote_server_uuid
|
|
)
|
|
except RemoteRealm.DoesNotExist:
|
|
raise AssertionError(
|
|
"The remote realm is missing despite being in the RemoteBillingIdentityDict"
|
|
)
|
|
|
|
if (
|
|
remote_realm.registration_deactivated
|
|
or remote_realm.realm_deactivated
|
|
or remote_realm.server.deactivated
|
|
):
|
|
raise JsonableError(_("Registration is deactivated"))
|
|
|
|
return remote_realm
|
|
|
|
|
|
def get_remote_server_and_user_from_session(
|
|
request: HttpRequest,
|
|
server_uuid: str,
|
|
) -> Tuple[RemoteZulipServer, Optional[RemoteServerBillingUser]]:
|
|
identity_dict: Optional[LegacyServerIdentityDict] = get_identity_dict_from_session(
|
|
request, realm_uuid=None, server_uuid=server_uuid
|
|
)
|
|
|
|
if identity_dict is None:
|
|
raise RemoteBillingAuthenticationError
|
|
|
|
remote_server_uuid = identity_dict["remote_server_uuid"]
|
|
try:
|
|
remote_server = RemoteZulipServer.objects.get(uuid=remote_server_uuid)
|
|
except RemoteZulipServer.DoesNotExist:
|
|
raise JsonableError(_("Invalid remote server."))
|
|
|
|
if remote_server.deactivated:
|
|
raise JsonableError(_("Registration is deactivated"))
|
|
|
|
remote_billing_user_id = identity_dict.get("remote_billing_user_id")
|
|
if remote_billing_user_id is None:
|
|
return remote_server, None
|
|
|
|
try:
|
|
remote_billing_user = RemoteServerBillingUser.objects.get(
|
|
id=remote_billing_user_id, remote_server=remote_server
|
|
)
|
|
except RemoteServerBillingUser.DoesNotExist:
|
|
remote_billing_user = None
|
|
|
|
return remote_server, remote_billing_user
|