mirror of https://github.com/zulip/zulip.git
billing: Use require_billing_access decorator in JSON endpoints.
This commit is contained in:
parent
69e9a8f196
commit
677f34c2b9
|
@ -11,6 +11,7 @@ import json
|
|||
|
||||
from django.core import signing
|
||||
from django.core.management import call_command
|
||||
from django.core.urlresolvers import get_resolver
|
||||
from django.http import HttpResponse
|
||||
from django.utils.timezone import utc as timezone_utc
|
||||
|
||||
|
@ -27,6 +28,7 @@ from corporate.lib.stripe import catch_stripe_errors, \
|
|||
get_next_billing_log_entry, run_billing_processor_one_step, \
|
||||
BillingError, StripeCardError, StripeConnectionError, stripe_get_customer
|
||||
from corporate.models import Customer, Plan, Coupon, BillingProcessor
|
||||
import corporate.urls
|
||||
|
||||
CallableT = TypeVar('CallableT', bound=Callable[..., Any])
|
||||
|
||||
|
@ -561,26 +563,6 @@ class StripeTest(ZulipTestCase):
|
|||
self.assertEqual(ujson.loads(response.content)['error_description'], 'downgrade without subscription')
|
||||
mock_save_customer.assert_not_called()
|
||||
|
||||
def test_downgrade_permissions(self) -> None:
|
||||
self.login(self.example_email('hamlet'))
|
||||
response = self.client_post("/json/billing/downgrade", {})
|
||||
self.assert_json_error_contains(response, "Access denied")
|
||||
# billing admin but not realm admin
|
||||
user = self.example_user('hamlet')
|
||||
user.is_billing_admin = True
|
||||
user.save(update_fields=['is_billing_admin'])
|
||||
with patch('corporate.views.process_downgrade') as mocked1:
|
||||
self.client_post("/json/billing/downgrade", {})
|
||||
mocked1.assert_called()
|
||||
# realm admin but not billing admin
|
||||
user = self.example_user('hamlet')
|
||||
user.is_billing_admin = False
|
||||
user.is_realm_admin = True
|
||||
user.save(update_fields=['is_billing_admin', 'is_realm_admin'])
|
||||
with patch('corporate.views.process_downgrade') as mocked2:
|
||||
self.client_post("/json/billing/downgrade", {})
|
||||
mocked2.assert_called()
|
||||
|
||||
@patch("stripe.Subscription.delete")
|
||||
@patch("stripe.Customer.retrieve", side_effect=mock_customer_with_account_balance(1234))
|
||||
def test_downgrade_credits(self, mock_retrieve_customer: Mock,
|
||||
|
@ -632,31 +614,6 @@ class StripeTest(ZulipTestCase):
|
|||
self.assertFalse(RealmAuditLog.objects.filter(
|
||||
event_type=RealmAuditLog.STRIPE_CARD_CHANGED).exists())
|
||||
|
||||
def test_update_payment_source_permissions(self) -> None:
|
||||
# This can be removed / merged with e.g. test_downgrade_permissions
|
||||
# once we have a decorator that handles billing page permissions
|
||||
self.login(self.example_email('hamlet'))
|
||||
response = self.client_post("/json/billing/sources/change",
|
||||
{'stripe_token': ujson.dumps('token')})
|
||||
self.assert_json_error_contains(response, "Access denied")
|
||||
# billing admin but not realm admin
|
||||
user = self.example_user('hamlet')
|
||||
user.is_billing_admin = True
|
||||
user.save(update_fields=['is_billing_admin'])
|
||||
with patch('corporate.views.do_replace_payment_source') as mocked1:
|
||||
self.client_post("/json/billing/sources/change",
|
||||
{'stripe_token': ujson.dumps('token')})
|
||||
mocked1.assert_called()
|
||||
# realm admin but not billing admin
|
||||
user = self.example_user('hamlet')
|
||||
user.is_billing_admin = False
|
||||
user.is_realm_admin = True
|
||||
user.save(update_fields=['is_billing_admin', 'is_realm_admin'])
|
||||
with patch('corporate.views.do_replace_payment_source') as mocked2:
|
||||
self.client_post("/json/billing/sources/change",
|
||||
{'stripe_token': ujson.dumps('token')})
|
||||
mocked2.assert_called()
|
||||
|
||||
@patch("stripe.Customer.create", side_effect=mock_create_customer)
|
||||
@patch("stripe.Subscription.create", side_effect=mock_create_subscription)
|
||||
@patch("stripe.Customer.retrieve", side_effect=mock_customer_with_subscription)
|
||||
|
@ -739,6 +696,53 @@ class RequiresBillingUpdateTest(ZulipTestCase):
|
|||
do_activate_user(user2)
|
||||
self.assertEqual(4, RealmAuditLog.objects.filter(requires_billing_update=True).count())
|
||||
|
||||
class RequiresBillingAccessTest(ZulipTestCase):
|
||||
def setUp(self) -> None:
|
||||
hamlet = self.example_user("hamlet")
|
||||
hamlet.is_billing_admin = True
|
||||
hamlet.save(update_fields=["is_billing_admin"])
|
||||
|
||||
# mocked_function_name will typically be something imported from
|
||||
# stripe.py. In theory we could have endpoints that need to mock
|
||||
# multiple functions, but we'll cross that bridge when we get there.
|
||||
def _test_endpoint(self, url: str, mocked_function_name: str,
|
||||
request_data: Optional[Dict[str, Any]]={}) -> None:
|
||||
# Normal users do not have access
|
||||
self.login(self.example_email('cordelia'))
|
||||
response = self.client_post(url, request_data)
|
||||
self.assert_json_error_contains(response, "Access denied")
|
||||
|
||||
# Billing admins have access
|
||||
self.login(self.example_email('hamlet'))
|
||||
with patch("corporate.views.{}".format(mocked_function_name)) as mocked1:
|
||||
response = self.client_post(url, request_data)
|
||||
self.assert_json_success(response)
|
||||
mocked1.assert_called()
|
||||
|
||||
# Realm admins have access, even if they are not billing admins
|
||||
self.login(self.example_email('iago'))
|
||||
with patch("corporate.views.{}".format(mocked_function_name)) as mocked2:
|
||||
response = self.client_post(url, request_data)
|
||||
self.assert_json_success(response)
|
||||
mocked2.assert_called()
|
||||
|
||||
def test_json_endpoints(self) -> None:
|
||||
params = [
|
||||
("/json/billing/sources/change", "do_replace_payment_source",
|
||||
{'stripe_token': ujson.dumps('token')}),
|
||||
("/json/billing/downgrade", "process_downgrade", {})
|
||||
] # type: List[Tuple[str, str, Dict[str, Any]]]
|
||||
|
||||
for (url, mocked_function_name, data) in params:
|
||||
self._test_endpoint(url, mocked_function_name, data)
|
||||
|
||||
# Make sure that we are testing all the JSON endpoints
|
||||
# Quite a hack, but probably fine for now
|
||||
string_with_all_endpoints = str(get_resolver('corporate.urls').reverse_dict)
|
||||
json_endpoints = set([word.strip("\"'()[],$") for word in string_with_all_endpoints.split()
|
||||
if 'json' in word])
|
||||
self.assertEqual(len(json_endpoints), len(params))
|
||||
|
||||
class BillingProcessorTest(ZulipTestCase):
|
||||
def add_log_entry(self, realm: Realm=get_realm('zulip'),
|
||||
event_type: str=RealmAuditLog.USER_CREATED,
|
||||
|
|
|
@ -19,7 +19,7 @@ i18n_urlpatterns = [
|
|||
v1_api_and_json_patterns = [
|
||||
url(r'^billing/downgrade$', rest_dispatch,
|
||||
{'POST': 'corporate.views.downgrade'}),
|
||||
url(r'billing/sources/change', rest_dispatch,
|
||||
url(r'^billing/sources/change', rest_dispatch,
|
||||
{'POST': 'corporate.views.replace_payment_source'}),
|
||||
]
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ from django.shortcuts import redirect, render
|
|||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
|
||||
from zerver.decorator import zulip_login_required
|
||||
from zerver.decorator import zulip_login_required, require_billing_access
|
||||
from zerver.lib.request import REQ, has_request_variables
|
||||
from zerver.lib.response import json_error, json_success
|
||||
from zerver.lib.validator import check_string
|
||||
|
@ -144,20 +144,18 @@ def billing_home(request: HttpRequest) -> HttpResponse:
|
|||
|
||||
return render(request, 'corporate/billing.html', context=context)
|
||||
|
||||
@require_billing_access
|
||||
def downgrade(request: HttpRequest, user: UserProfile) -> HttpResponse:
|
||||
if not user.is_realm_admin and not user.is_billing_admin:
|
||||
return json_error(_('Access denied'))
|
||||
try:
|
||||
process_downgrade(user)
|
||||
except BillingError as e:
|
||||
return json_error(e.message, data={'error_description': e.description})
|
||||
return json_success()
|
||||
|
||||
@require_billing_access
|
||||
@has_request_variables
|
||||
def replace_payment_source(request: HttpRequest, user: UserProfile,
|
||||
stripe_token: str=REQ("stripe_token", validator=check_string)) -> HttpResponse:
|
||||
if not user.is_realm_admin and not user.is_billing_admin:
|
||||
return json_error(_("Access denied"))
|
||||
try:
|
||||
do_replace_payment_source(user, stripe_token)
|
||||
except BillingError as e:
|
||||
|
|
|
@ -135,6 +135,14 @@ def require_realm_admin(func: ViewFuncT) -> ViewFuncT:
|
|||
return func(request, user_profile, *args, **kwargs)
|
||||
return wrapper # type: ignore # https://github.com/python/mypy/issues/1927
|
||||
|
||||
def require_billing_access(func: ViewFuncT) -> ViewFuncT:
|
||||
@wraps(func)
|
||||
def wrapper(request: HttpRequest, user_profile: UserProfile, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
if not user_profile.is_realm_admin and not user_profile.is_billing_admin:
|
||||
raise JsonableError(_("Access denied"))
|
||||
return func(request, user_profile, *args, **kwargs)
|
||||
return wrapper # type: ignore # https://github.com/python/mypy/issues/1927
|
||||
|
||||
from zerver.lib.user_agent import parse_user_agent
|
||||
|
||||
def get_client_name(request: HttpRequest, is_browser_view: bool) -> str:
|
||||
|
|
Loading…
Reference in New Issue