diff --git a/zerver/lib/users.py b/zerver/lib/users.py index f7c6d4c73b..2d26bd86e2 100644 --- a/zerver/lib/users.py +++ b/zerver/lib/users.py @@ -346,13 +346,7 @@ def compute_show_invites_and_add_streams(user_profile: Optional[UserProfile]) -> if user_profile.is_guest: return False, False - if user_profile.is_realm_admin: - return True, True - - if user_profile.realm.invite_to_realm_policy == Realm.INVITE_TO_REALM_ADMINS_ONLY: - return False, True - - return True, True + return user_profile.can_invite_others_to_realm(), True def format_user_row( diff --git a/zerver/models.py b/zerver/models.py index b09ea99708..7229e72a59 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -224,17 +224,7 @@ class Realm(models.Model): # See RealmDomain for the domains that apply for a given organization. emails_restricted_to_domains: bool = models.BooleanField(default=False) - INVITE_TO_REALM_MEMBERS_ONLY = 1 - INVITE_TO_REALM_ADMINS_ONLY = 2 - INVITE_TO_REALM_POLICY_TYPES = [ - INVITE_TO_REALM_MEMBERS_ONLY, - INVITE_TO_REALM_ADMINS_ONLY, - ] - invite_required: bool = models.BooleanField(default=True) - invite_to_realm_policy: int = models.PositiveSmallIntegerField( - default=INVITE_TO_REALM_MEMBERS_ONLY - ) _max_invites: Optional[int] = models.IntegerField(null=True, db_column="max_invites") disallow_disposable_email_addresses: bool = models.BooleanField(default=True) @@ -276,6 +266,9 @@ class Realm(models.Model): # Who in the organization is allowed to create streams. create_stream_policy: int = models.PositiveSmallIntegerField(default=POLICY_MEMBERS_ONLY) + # Who in the organization is allowed to invite other users to organization. + invite_to_realm_policy: int = models.PositiveSmallIntegerField(default=POLICY_MEMBERS_ONLY) + # Who in the organization is allowed to invite other users to streams. invite_to_stream_policy: int = models.PositiveSmallIntegerField(default=POLICY_MEMBERS_ONLY) diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index 5ffb1ed10c..b5f8eaba10 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -3025,6 +3025,8 @@ paths: * 1 = Members only * 2 = Administrators only + * 3 = Full members only + * 4 = Moderators only **Changes**: New in Zulip 4.0 (feature level 50) replacing the previous `invite_by_admins_only` boolean. @@ -7777,6 +7779,8 @@ paths: * 1 = Members only * 2 = Administrators only + * 3 = Full members only + * 4 = Moderators only **Changes**: New in Zulip 4.0 (feature level 50) replacing the previous `realm_invite_by_admins_only` boolean. diff --git a/zerver/tests/test_events.py b/zerver/tests/test_events.py index 68e4ca6807..b513db5645 100644 --- a/zerver/tests/test_events.py +++ b/zerver/tests/test_events.py @@ -1872,7 +1872,7 @@ class RealmPropertyActionTest(BaseAction): ], default_code_block_language=["python", "javascript"], message_content_delete_limit_seconds=[1000, 1100, 1200], - invite_to_realm_policy=[2, 1], + invite_to_realm_policy=[4, 3, 2, 1], ) vals = test_values.get(name) diff --git a/zerver/tests/test_home.py b/zerver/tests/test_home.py index 285d7fd946..d4f0e8139d 100644 --- a/zerver/tests/test_home.py +++ b/zerver/tests/test_home.py @@ -670,7 +670,7 @@ class HomeTest(ZulipTestCase): user_profile = self.example_user("hamlet") realm = user_profile.realm - realm.invite_to_realm_policy = Realm.INVITE_TO_REALM_ADMINS_ONLY + realm.invite_to_realm_policy = Realm.POLICY_ADMINS_ONLY realm.save() self.login_user(user_profile) @@ -689,14 +689,12 @@ class HomeTest(ZulipTestCase): user_profile = self.example_user("polonius") realm = user_profile.realm - realm.invite_to_realm_policy = Realm.INVITE_TO_REALM_MEMBERS_ONLY + realm.invite_to_realm_policy = Realm.POLICY_MEMBERS_ONLY realm.save() self.login_user(user_profile) self.assertFalse(user_profile.is_realm_admin) - self.assertEqual( - get_realm("zulip").invite_to_realm_policy, Realm.INVITE_TO_REALM_MEMBERS_ONLY - ) + self.assertEqual(get_realm("zulip").invite_to_realm_policy, Realm.POLICY_MEMBERS_ONLY) result = self._get_home_page() html = result.content.decode("utf-8") self.assertNotIn("Invite more users", html) @@ -1080,7 +1078,7 @@ class HomeTest(ZulipTestCase): user = self.example_user("iago") realm = user.realm - realm.invite_to_realm_policy = Realm.INVITE_TO_REALM_ADMINS_ONLY + realm.invite_to_realm_policy = Realm.POLICY_ADMINS_ONLY realm.save() show_invites, show_add_streams = compute_show_invites_and_add_streams(user) @@ -1091,7 +1089,7 @@ class HomeTest(ZulipTestCase): user = self.example_user("hamlet") realm = user.realm - realm.invite_to_realm_policy = Realm.INVITE_TO_REALM_ADMINS_ONLY + realm.invite_to_realm_policy = Realm.POLICY_ADMINS_ONLY realm.save() show_invites, show_add_streams = compute_show_invites_and_add_streams(user) diff --git a/zerver/tests/test_realm.py b/zerver/tests/test_realm.py index 453b6d6597..035474c9e2 100644 --- a/zerver/tests/test_realm.py +++ b/zerver/tests/test_realm.py @@ -554,7 +554,7 @@ class RealmTest(ZulipTestCase): def test_change_invite_to_realm_policy(self) -> None: # We need an admin user. self.login("iago") - req = dict(invite_to_realm_policy=orjson.dumps(Realm.INVITE_TO_REALM_ADMINS_ONLY).decode()) + req = dict(invite_to_realm_policy=orjson.dumps(Realm.POLICY_ADMINS_ONLY).decode()) result = self.client_patch("/json/realm", req) self.assert_json_success(result) @@ -888,8 +888,10 @@ class RealmAPITest(ZulipTestCase): ], message_content_delete_limit_seconds=[1000, 1100, 1200], invite_to_realm_policy=[ - Realm.INVITE_TO_REALM_ADMINS_ONLY, - Realm.INVITE_TO_REALM_MEMBERS_ONLY, + Realm.POLICY_ADMINS_ONLY, + Realm.POLICY_MEMBERS_ONLY, + Realm.POLICY_FULL_MEMBERS_ONLY, + Realm.POLICY_MODERATORS_ONLY, ], ) diff --git a/zerver/tests/test_signup.py b/zerver/tests/test_signup.py index bf5786ab31..0203c852d6 100644 --- a/zerver/tests/test_signup.py +++ b/zerver/tests/test_signup.py @@ -1240,24 +1240,94 @@ class InviteUserTest(InviteUserBase): self.check_has_permission_policies(othello, "invite_to_realm_policy", validation_func) - def test_require_realm_admin(self) -> None: + def test_invite_others_to_realm_setting(self) -> None: """ The invite_to_realm_policy realm setting works properly. """ realm = get_realm("zulip") - realm.invite_to_realm_policy = Realm.INVITE_TO_REALM_ADMINS_ONLY - realm.save() + do_set_realm_property( + realm, "invite_to_realm_policy", Realm.POLICY_ADMINS_ONLY, acting_user=None + ) - self.login("hamlet") + self.login("shiva") email = "alice-test@zulip.com" email2 = "bob-test@zulip.com" invitee = f"Alice Test <{email}>, {email2}" self.assert_json_error( - self.invite(invitee, ["Denmark"]), "Must be an organization administrator" + self.invite(invitee, ["Denmark"]), + "Only administrators can invite others to this organization.", ) # Now verify an administrator can do it self.login("iago") + self.assert_json_success(self.invite(invitee, ["Denmark"])) + self.assertTrue(find_key_by_email(email)) + self.assertTrue(find_key_by_email(email2)) + + self.check_sent_emails([email, email2]) + + from django.core import mail + + mail.outbox = [] + + do_set_realm_property( + realm, "invite_to_realm_policy", Realm.POLICY_MODERATORS_ONLY, acting_user=None + ) + self.login("hamlet") + email = "carol-test@zulip.com" + email2 = "earl-test@zulip.com" + invitee = f"Carol Test <{email}>, {email2}" + self.assert_json_error( + self.invite(invitee, ["Denmark"]), + "Only administrators and moderators can invite others to this organization.", + ) + + self.login("shiva") + self.assert_json_success(self.invite(invitee, ["Denmark"])) + self.assertTrue(find_key_by_email(email)) + self.assertTrue(find_key_by_email(email2)) + self.check_sent_emails([email, email2]) + + mail.outbox = [] + + do_set_realm_property( + realm, "invite_to_realm_policy", Realm.POLICY_MEMBERS_ONLY, acting_user=None + ) + + self.login("polonius") + email = "dave-test@zulip.com" + email2 = "mark-test@zulip.com" + invitee = f"Dave Test <{email}>, {email2}" + self.assert_json_error(self.invite(invitee, ["Denmark"]), "Not allowed for guest users") + + self.login("hamlet") + self.assert_json_success(self.invite(invitee, ["Denmark"])) + self.assertTrue(find_key_by_email(email)) + self.assertTrue(find_key_by_email(email2)) + self.check_sent_emails([email, email2]) + + mail.outbox = [] + + do_set_realm_property( + realm, "invite_to_realm_policy", Realm.POLICY_FULL_MEMBERS_ONLY, acting_user=None + ) + do_set_realm_property(realm, "waiting_period_threshold", 1000, acting_user=None) + + hamlet = self.example_user("hamlet") + hamlet.date_joined = timezone_now() - datetime.timedelta( + days=(realm.waiting_period_threshold - 1) + ) + + email = "issac-test@zulip.com" + email2 = "steven-test@zulip.com" + invitee = f"Issac Test <{email}>, {email2}" + self.assert_json_error( + self.invite(invitee, ["Denmark"]), + "Your account is too new to invite others to this organization.", + ) + + do_set_realm_property(realm, "waiting_period_threshold", 0, acting_user=None) + self.assert_json_success(self.invite(invitee, ["Denmark"])) self.assertTrue(find_key_by_email(email)) self.assertTrue(find_key_by_email(email2)) @@ -2441,8 +2511,8 @@ class MultiuseInviteTest(ZulipTestCase): def test_only_admin_can_create_multiuse_link_api_call(self) -> None: self.login("iago") # Only admins should be able to create multiuse invites even if - # invite_to_realm_policy is set to Realm.INVITE_TO_REALM_MEMBERS_ONLY. - self.realm.invite_to_realm_policy = Realm.INVITE_TO_REALM_MEMBERS_ONLY + # invite_to_realm_policy is set to Realm.POLICY_MEMBERS_ONLY. + self.realm.invite_to_realm_policy = Realm.POLICY_MEMBERS_ONLY self.realm.save() result = self.client_post("/json/invites/multiuse") diff --git a/zerver/views/invite.py b/zerver/views/invite.py index 56928b4cce..8bd27b0e7a 100644 --- a/zerver/views/invite.py +++ b/zerver/views/invite.py @@ -13,7 +13,7 @@ from zerver.lib.actions import ( do_revoke_multi_use_invite, do_revoke_user_invite, ) -from zerver.lib.exceptions import OrganizationAdministratorRequired, OrganizationOwnerRequired +from zerver.lib.exceptions import OrganizationOwnerRequired from zerver.lib.request import REQ, JsonableError, has_request_variables from zerver.lib.response import json_error, json_success from zerver.lib.streams import access_stream_by_id @@ -39,11 +39,16 @@ def invite_users_backend( stream_ids: List[int] = REQ(validator=check_list(check_int)), ) -> HttpResponse: - if ( - user_profile.realm.invite_to_realm_policy == Realm.INVITE_TO_REALM_ADMINS_ONLY - and not user_profile.is_realm_admin - ): - raise OrganizationAdministratorRequired() + if not user_profile.can_invite_others_to_realm(): + if user_profile.realm.invite_to_realm_policy == Realm.POLICY_ADMINS_ONLY: + return json_error(_("Only administrators can invite others to this organization.")) + if user_profile.realm.invite_to_realm_policy == Realm.POLICY_MODERATORS_ONLY: + return json_error( + _("Only administrators and moderators can invite others to this organization.") + ) + if user_profile.realm.invite_to_realm_policy == Realm.POLICY_FULL_MEMBERS_ONLY: + return json_error(_("Your account is too new to invite others to this organization.")) + # Guest case will be handled by require_member_or_admin decorator. if invite_as not in PreregistrationUser.INVITE_AS.values(): return json_error(_("Must be invited as an valid type of user")) check_if_owner_required(invite_as, user_profile) diff --git a/zerver/views/realm.py b/zerver/views/realm.py index 8f90e0fd96..e0ef063642 100644 --- a/zerver/views/realm.py +++ b/zerver/views/realm.py @@ -47,7 +47,7 @@ def update_realm( disallow_disposable_email_addresses: Optional[bool] = REQ(validator=check_bool, default=None), invite_required: Optional[bool] = REQ(validator=check_bool, default=None), invite_to_realm_policy: Optional[int] = REQ( - validator=check_int_in(Realm.INVITE_TO_REALM_POLICY_TYPES), default=None + validator=check_int_in(Realm.COMMON_POLICY_TYPES), default=None ), name_changes_disabled: Optional[bool] = REQ(validator=check_bool, default=None), email_changes_disabled: Optional[bool] = REQ(validator=check_bool, default=None),