mirror of https://github.com/zulip/zulip.git
billing: Sign and verify the seat count during upgrade.
This commit is contained in:
parent
d75054fb15
commit
82fc82b7e2
|
@ -3178,6 +3178,11 @@ nav ul li.active::after {
|
|||
-webkit-box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
#error-message-box {
|
||||
margin-top: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes box-shadow-pulse {
|
||||
|
|
|
@ -17,10 +17,17 @@
|
|||
|
||||
<div class="page-content">
|
||||
<div class="main">
|
||||
{% if error_message %}
|
||||
<div class="alert alert-danger" id="error-message-box">
|
||||
{{ error_message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<h1>{% trans %}Upgrade to {{ plan }}{% endtrans %}</h1>
|
||||
<form method="post">
|
||||
{{ csrf_input }}
|
||||
<input type="hidden" name="seat_count" value="{{ seat_count }}">
|
||||
<input type="hidden" name="signed_seat_count" value="{{ signed_seat_count }}">
|
||||
<input type="hidden" name="salt" value="{{ salt }}">
|
||||
<div class="payment-schedule">
|
||||
<h3>{{ _("Payment schedule") }}</h3>
|
||||
<label>
|
||||
|
|
|
@ -44,6 +44,8 @@ class StripeTest(ZulipTestCase):
|
|||
self.stripe_plan_id = 'plan_D7Nh2BtpTvIzYp'
|
||||
self.subscription_created = 1529990751
|
||||
self.quantity = 8
|
||||
|
||||
self.signed_seat_count, self.salt = sign_string(str(self.quantity))
|
||||
Plan.objects.create(nickname=Plan.CLOUD_ANNUAL, stripe_plan_id=self.stripe_plan_id)
|
||||
|
||||
@mock.patch("zilencer.lib.stripe.STRIPE_PUBLISHABLE_KEY", "stripe_publishable_key")
|
||||
|
@ -96,7 +98,9 @@ class StripeTest(ZulipTestCase):
|
|||
# Click "Make payment" in Stripe Checkout
|
||||
response = self.client_post("/upgrade/", {
|
||||
'stripeToken': self.token,
|
||||
'seat_count': self.quantity,
|
||||
# TODO: get these values from the response
|
||||
'signed_seat_count': self.signed_seat_count,
|
||||
'salt': self.salt,
|
||||
'plan': Plan.CLOUD_ANNUAL})
|
||||
# Check that we created a customer and subscription in stripe
|
||||
mock_create_customer.assert_called_once_with(
|
||||
|
@ -148,7 +152,8 @@ class StripeTest(ZulipTestCase):
|
|||
self.assertEqual('/upgrade/', response.url)
|
||||
# Check that non-admins can sign up and pay
|
||||
self.client_post("/upgrade/", {'stripeToken': self.token,
|
||||
'seat_count': self.quantity,
|
||||
'signed_seat_count': self.signed_seat_count,
|
||||
'salt': self.salt,
|
||||
'plan': Plan.CLOUD_ANNUAL})
|
||||
# Check that the non-admin hamlet can still access /billing
|
||||
response = self.client_get("/billing/")
|
||||
|
@ -173,7 +178,8 @@ class StripeTest(ZulipTestCase):
|
|||
# Change the seat count while the user is going through the upgrade flow
|
||||
with mock.patch('zilencer.lib.stripe.get_seat_count', return_value=new_seat_count):
|
||||
self.client_post("/upgrade/", {'stripeToken': self.token,
|
||||
'seat_count': self.quantity,
|
||||
'signed_seat_count': self.signed_seat_count,
|
||||
'salt': self.salt,
|
||||
'plan': Plan.CLOUD_ANNUAL})
|
||||
# Check that the subscription call used the old quantity, not new_seat_count
|
||||
mock_create_subscription.assert_called_once_with(
|
||||
|
@ -200,6 +206,18 @@ class StripeTest(ZulipTestCase):
|
|||
event_type=RealmAuditLog.PLAN_QUANTITY_UPDATED).values_list('extra_data', flat=True).first()),
|
||||
{'quantity': new_seat_count})
|
||||
|
||||
@mock.patch("zilencer.lib.stripe.STRIPE_PUBLISHABLE_KEY", "stripe_publishable_key")
|
||||
@mock.patch("zilencer.views.STRIPE_PUBLISHABLE_KEY", "stripe_publishable_key")
|
||||
def test_upgrade_with_tampered_seat_count(self) -> None:
|
||||
self.login(self.user.email)
|
||||
result = self.client_post("/upgrade/", {
|
||||
'stripeToken': self.token,
|
||||
'signed_seat_count': "randomsalt",
|
||||
'salt': self.salt,
|
||||
'plan': Plan.CLOUD_ANNUAL
|
||||
})
|
||||
self.assert_in_success_response(["Something went wrong. Please contact"], result)
|
||||
|
||||
@mock.patch("zilencer.lib.stripe.STRIPE_PUBLISHABLE_KEY", "stripe_publishable_key")
|
||||
@mock.patch("zilencer.views.STRIPE_PUBLISHABLE_KEY", "stripe_publishable_key")
|
||||
@mock.patch("stripe.Customer.retrieve", side_effect=mock_retrieve_customer)
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
from typing import Any, Dict, Optional, Union, cast
|
||||
import logging
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import validate_email, URLValidator
|
||||
from django.core import signing
|
||||
from django.db import IntegrityError
|
||||
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
|
||||
from django.utils import timezone
|
||||
|
@ -26,10 +28,12 @@ from zerver.views.push_notifications import validate_token
|
|||
from zilencer.lib.stripe import STRIPE_PUBLISHABLE_KEY, StripeError, \
|
||||
do_create_customer_with_payment_source, do_subscribe_customer_to_plan, \
|
||||
get_stripe_customer, get_upcoming_invoice, payment_source, \
|
||||
get_seat_count, extract_current_subscription
|
||||
get_seat_count, extract_current_subscription, sign_string, unsign_string
|
||||
from zilencer.models import RemotePushDeviceToken, RemoteZulipServer, \
|
||||
Customer, Plan
|
||||
|
||||
billing_logger = logging.getLogger('zilencer.stripe')
|
||||
|
||||
def validate_entity(entity: Union[UserProfile, RemoteZulipServer]) -> None:
|
||||
if not isinstance(entity, RemoteZulipServer):
|
||||
raise JsonableError(err_("Must validate with valid Zulip server API key"))
|
||||
|
@ -158,15 +162,21 @@ def remote_server_notify_push(request: HttpRequest, entity: Union[UserProfile, R
|
|||
@zulip_login_required
|
||||
def initial_upgrade(request: HttpRequest) -> HttpResponse:
|
||||
user = request.user
|
||||
error_message = ""
|
||||
|
||||
if Customer.objects.filter(realm=user.realm).exists():
|
||||
return HttpResponseRedirect(reverse('zilencer.views.billing_home'))
|
||||
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
seat_count = int(unsign_string(request.POST['signed_seat_count'], request.POST['salt']))
|
||||
except signing.BadSignature:
|
||||
billing_logger.warning("Tampered seat count during realm upgrade. user: %s, realm: %s (%s)."
|
||||
% (user.id, user.realm.id, user.realm.string_id))
|
||||
error_message = "Something went wrong. Please contact support@zulipchat.com"
|
||||
|
||||
if not error_message:
|
||||
stripe_customer = do_create_customer_with_payment_source(user, request.POST['stripeToken'])
|
||||
# TODO: the current way this is done is subject to tampering by the user.
|
||||
seat_count = int(request.POST['seat_count'])
|
||||
if seat_count < 1:
|
||||
raise AssertionError('seat_count is less than 1')
|
||||
do_subscribe_customer_to_plan(
|
||||
stripe_customer=stripe_customer,
|
||||
stripe_plan_id=Plan.objects.get(nickname=request.POST['plan']).stripe_plan_id,
|
||||
|
@ -177,13 +187,18 @@ def initial_upgrade(request: HttpRequest) -> HttpResponse:
|
|||
# TODO: check for errors and raise/send to frontend
|
||||
return HttpResponseRedirect(reverse('zilencer.views.billing_home'))
|
||||
|
||||
seat_count = get_seat_count(user.realm)
|
||||
signed_seat_count, salt = sign_string(str(seat_count))
|
||||
context = {
|
||||
'publishable_key': STRIPE_PUBLISHABLE_KEY,
|
||||
'email': user.email,
|
||||
'seat_count': get_seat_count(user.realm),
|
||||
'seat_count': seat_count,
|
||||
'signed_seat_count': signed_seat_count,
|
||||
'salt': salt,
|
||||
'plan': "Zulip Premium",
|
||||
'nickname_monthly': Plan.CLOUD_MONTHLY,
|
||||
'nickname_annual': Plan.CLOUD_ANNUAL,
|
||||
'error_message': error_message,
|
||||
} # type: Dict[str, Any]
|
||||
return render(request, 'zilencer/upgrade.html', context=context)
|
||||
|
||||
|
|
Loading…
Reference in New Issue