billing: Enforce manual license management for guest role changes.

Adds a check for changing an existing guest user's role before
calling do_update_user in the case that a realm has a current
paid plan with manual license management.
This commit is contained in:
Lauryn Menard 2024-09-09 21:00:11 +02:00 committed by Tim Abbott
parent 7861c1ba63
commit 4bd4534450
4 changed files with 72 additions and 2 deletions

View File

@ -4,7 +4,7 @@ from django.utils.translation import gettext as _
from corporate.lib.stripe import LicenseLimitError, get_latest_seat_count, get_seat_count from corporate.lib.stripe import LicenseLimitError, get_latest_seat_count, get_seat_count
from corporate.models import CustomerPlan, get_current_plan_by_realm from corporate.models import CustomerPlan, get_current_plan_by_realm
from zerver.actions.create_user import send_group_direct_message_to_admins from zerver.actions.create_user import send_group_direct_message_to_admins
from zerver.lib.exceptions import InvitationError from zerver.lib.exceptions import InvitationError, JsonableError
from zerver.models import Realm, UserProfile from zerver.models import Realm, UserProfile
from zerver.models.users import get_system_bot from zerver.models.users import get_system_bot
@ -119,3 +119,17 @@ def check_spare_licenses_available_for_inviting_new_users(
"Your organization does not have enough Zulip licenses. Invitations were not sent." "Your organization does not have enough Zulip licenses. Invitations were not sent."
) )
raise InvitationError(message, [], sent_invitations=False, license_limit_reached=True) raise InvitationError(message, [], sent_invitations=False, license_limit_reached=True)
def check_spare_license_available_for_changing_guest_user_role(realm: Realm) -> None:
plan = get_plan_if_manual_license_management_enforced(realm)
if plan is None:
return
try:
check_spare_licenses_available(realm, plan, extra_non_guests_count=1)
except LicenseLimitError:
error_message = _(
"Your organization does not have enough Zulip licenses to change a guest user's role."
)
raise JsonableError(error_message)

View File

@ -1,6 +1,7 @@
from argparse import ArgumentParser from argparse import ArgumentParser
from typing import Any from typing import Any
from django.conf import settings
from django.core.management.base import CommandError from django.core.management.base import CommandError
from typing_extensions import override from typing_extensions import override
@ -10,6 +11,7 @@ from zerver.actions.users import (
do_change_is_billing_admin, do_change_is_billing_admin,
do_change_user_role, do_change_user_role,
) )
from zerver.lib.exceptions import JsonableError
from zerver.lib.management import ZulipBaseCommand from zerver.lib.management import ZulipBaseCommand
from zerver.models import UserProfile from zerver.models import UserProfile
@ -52,7 +54,7 @@ ONLY perform this on customer request from an authorized person.
def handle(self, *args: Any, **options: Any) -> None: def handle(self, *args: Any, **options: Any) -> None:
email = options["email"] email = options["email"]
realm = self.get_realm(options) realm = self.get_realm(options)
assert realm is not None
user = self.get_user(email, realm) user = self.get_user(email, realm)
user_role_map = { user_role_map = {
@ -71,6 +73,17 @@ ONLY perform this on customer request from an authorized person.
) )
if new_role == user.role: if new_role == user.role:
raise CommandError("User already has this role.") raise CommandError("User already has this role.")
if settings.BILLING_ENABLED and user.is_guest:
from corporate.lib.registration import (
check_spare_license_available_for_changing_guest_user_role,
)
try:
check_spare_license_available_for_changing_guest_user_role(realm)
except JsonableError:
raise CommandError(
"This realm does not have enough licenses to change a guest user's role."
)
old_role_name = UserProfile.ROLE_ID_TO_NAME_MAP[user.role] old_role_name = UserProfile.ROLE_ID_TO_NAME_MAP[user.role]
do_change_user_role(user, new_role, acting_user=None) do_change_user_role(user, new_role, acting_user=None)
new_role_name = UserProfile.ROLE_ID_TO_NAME_MAP[user.role] new_role_name = UserProfile.ROLE_ID_TO_NAME_MAP[user.role]

View File

@ -13,6 +13,7 @@ from django.test import override_settings
from django.utils.timezone import now as timezone_now from django.utils.timezone import now as timezone_now
from confirmation.models import Confirmation from confirmation.models import Confirmation
from corporate.lib.stripe import get_latest_seat_count
from zerver.actions.create_user import do_create_user, do_reactivate_user from zerver.actions.create_user import do_create_user, do_reactivate_user
from zerver.actions.invites import do_create_multiuse_invite_link, do_invite_users from zerver.actions.invites import do_create_multiuse_invite_link, do_invite_users
from zerver.actions.message_send import RecipientInfoResult, get_recipient_info from zerver.actions.message_send import RecipientInfoResult, get_recipient_info
@ -206,6 +207,40 @@ class PermissionTest(ZulipTestCase):
result = self.client_patch(f"/json/users/{invalid_user_id}", {}) result = self.client_patch(f"/json/users/{invalid_user_id}", {})
self.assert_json_error(result, "No such user") self.assert_json_error(result, "No such user")
def test_change_guest_user_role_with_manual_license_plan(self) -> None:
desdemona = self.example_user("desdemona")
polonius = self.example_user("polonius")
self.login("desdemona")
_, ledger = self.subscribe_realm_to_monthly_plan_on_manual_license_management(
desdemona.realm, 5, 5
)
assert polonius.is_guest
req = dict(role=UserProfile.ROLE_MEMBER)
with self.settings(BILLING_ENABLED=True):
result = self.client_patch(f"/json/users/{polonius.id}", req)
self.assert_json_error(
result,
"Your organization does not have enough Zulip licenses to change a guest user's role.",
)
ledger.licenses = get_latest_seat_count(desdemona.realm) + 1
ledger.save(update_fields=["licenses"])
with self.settings(BILLING_ENABLED=True):
result = self.client_patch(f"/json/users/{polonius.id}", req)
self.assert_json_error(
result,
"Your organization does not have enough Zulip licenses to change a guest user's role.",
)
ledger.licenses_at_next_renewal = get_latest_seat_count(desdemona.realm) + 1
ledger.save(update_fields=["licenses_at_next_renewal"])
with self.settings(BILLING_ENABLED=True):
result = self.client_patch(f"/json/users/{polonius.id}", req)
self.assert_json_success(result)
polonius.refresh_from_db()
assert polonius.role == UserProfile.ROLE_MEMBER
def test_owner_api(self) -> None: def test_owner_api(self) -> None:
self.login("iago") self.login("iago")

View File

@ -224,6 +224,14 @@ def update_user_backend(
raise JsonableError( raise JsonableError(
_("The owner permission cannot be removed from the only organization owner.") _("The owner permission cannot be removed from the only organization owner.")
) )
if settings.BILLING_ENABLED and target.is_guest:
from corporate.lib.registration import (
check_spare_license_available_for_changing_guest_user_role,
)
check_spare_license_available_for_changing_guest_user_role(user_profile.realm)
do_change_user_role(target, role, acting_user=user_profile) do_change_user_role(target, role, acting_user=user_profile)
if full_name is not None and target.full_name != full_name and full_name.strip() != "": if full_name is not None and target.full_name != full_name and full_name.strip() != "":