diff --git a/tools/lib/capitalization.py b/tools/lib/capitalization.py index 1f909e10aa..f228e24524 100644 --- a/tools/lib/capitalization.py +++ b/tools/lib/capitalization.py @@ -49,6 +49,7 @@ IGNORED_PHRASES = [ r"Zulip Security", r"Zulip Cloud", r"Zulip Cloud Standard", + r"Zulip Cloud Plus", r"BigBlueButton", # Code things r"\.zuliprc", diff --git a/web/src/admin.js b/web/src/admin.js index e7190b37dd..b3b7e911ca 100644 --- a/web/src/admin.js +++ b/web/src/admin.js @@ -1,4 +1,5 @@ import $ from "jquery"; +import tippy from "tippy.js"; import render_admin_tab from "../templates/settings/admin_tab.hbs"; import render_settings_organization_settings_tip from "../templates/settings/organization_settings_tip.hbs"; @@ -254,6 +255,23 @@ export function build_page() { $("#id_realm_bot_creation_policy").val(page_params.realm_bot_creation_policy); $("#id_realm_digest_weekday").val(options.realm_digest_weekday); + + const is_plan_plus = page_params.realm_plan_type === 10; + const is_plan_self_hosted = page_params.realm_plan_type === 1; + if ( + page_params.development_environment && + page_params.is_admin && + !(is_plan_plus || is_plan_self_hosted) + ) { + $("#realm_can_access_all_users_group_widget").prop("disabled", true); + const opts = { + content: $t({ + defaultMessage: "This feature is available on Zulip Cloud Plus. Upgrade to access.", + }), + }; + + tippy($("#realm_can_access_all_users_group_widget_container")[0], opts); + } } export function launch(section) { diff --git a/zerver/actions/realm_settings.py b/zerver/actions/realm_settings.py index 31eb1bdfa5..06e568e6ff 100644 --- a/zerver/actions/realm_settings.py +++ b/zerver/actions/realm_settings.py @@ -32,6 +32,7 @@ from zerver.models import ( ScheduledEmail, Stream, Subscription, + SystemGroups, UserGroup, UserProfile, active_user_ids, @@ -147,7 +148,7 @@ def do_set_push_notifications_enabled_end_timestamp( send_event(realm, event, active_user_ids(realm.id)) -@transaction.atomic(durable=True) +@transaction.atomic(savepoint=False) def do_change_realm_permission_group_setting( realm: Realm, setting_name: str, user_group: UserGroup, *, acting_user: Optional[UserProfile] ) -> None: @@ -520,6 +521,21 @@ def do_change_realm_plan_type( # We do not allow public access on limited plans. do_set_realm_property(realm, "enable_spectator_access", False, acting_user=acting_user) + if old_value in [Realm.PLAN_TYPE_PLUS, Realm.PLAN_TYPE_SELF_HOSTED] and plan_type not in [ + Realm.PLAN_TYPE_PLUS, + Realm.PLAN_TYPE_SELF_HOSTED, + ]: + # If downgrading to a plan that no longer has access to change + # can_access_all_users_group, set it back to the default + # value. + everyone_system_group = UserGroup.objects.get( + name=SystemGroups.EVERYONE, realm=realm, is_system_group=True + ) + if realm.can_access_all_users_group_id != everyone_system_group.id: + do_change_realm_permission_group_setting( + realm, "can_access_all_users_group", everyone_system_group, acting_user=acting_user + ) + realm.plan_type = plan_type realm.save(update_fields=["plan_type"]) RealmAuditLog.objects.create( diff --git a/zerver/models.py b/zerver/models.py index 05ff6c20cb..9b3126bdc4 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -685,6 +685,7 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub ) UPGRADE_TEXT_STANDARD = gettext_lazy("Available on Zulip Cloud Standard. Upgrade to access.") + UPGRADE_TEXT_PLUS = gettext_lazy("Available on Zulip Cloud Plus. Upgrade to access.") # plan_type controls various features around resource/feature # limitations for a Zulip organization on multi-tenant installations # like Zulip Cloud. @@ -1047,6 +1048,10 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub if self.plan_type == Realm.PLAN_TYPE_LIMITED: raise JsonableError(str(self.UPGRADE_TEXT_STANDARD)) + def can_enable_restricted_user_access_for_guests(self) -> None: + if self.plan_type not in [Realm.PLAN_TYPE_PLUS, Realm.PLAN_TYPE_SELF_HOSTED]: + raise JsonableError(str(self.UPGRADE_TEXT_PLUS)) + @property def subdomain(self) -> str: return self.string_id diff --git a/zerver/tests/test_events.py b/zerver/tests/test_events.py index 0a5b771b16..c26f11b83b 100644 --- a/zerver/tests/test_events.py +++ b/zerver/tests/test_events.py @@ -2628,6 +2628,10 @@ class NormalActionsTest(BaseAction): def test_realm_update_plan_type(self) -> None: realm = self.user_profile.realm + members_group = UserGroup.objects.get(name=SystemGroups.MEMBERS, realm=realm) + do_change_realm_permission_group_setting( + realm, "can_access_all_users_group", members_group, acting_user=None + ) state_data = fetch_initial_state_data(self.user_profile) self.assertEqual(state_data["realm_plan_type"], Realm.PLAN_TYPE_SELF_HOSTED) @@ -2637,10 +2641,11 @@ class NormalActionsTest(BaseAction): lambda: do_change_realm_plan_type( realm, Realm.PLAN_TYPE_LIMITED, acting_user=self.user_profile ), - num_events=2, + num_events=3, ) check_realm_update("events[0]", events[0], "enable_spectator_access") - check_realm_update("events[1]", events[1], "plan_type") + check_realm_update_dict("events[1]", events[1]) + check_realm_update("events[2]", events[2], "plan_type") state_data = fetch_initial_state_data(self.user_profile) self.assertEqual(state_data["realm_plan_type"], Realm.PLAN_TYPE_LIMITED) diff --git a/zerver/tests/test_realm.py b/zerver/tests/test_realm.py index 82f132900a..420bb92de4 100644 --- a/zerver/tests/test_realm.py +++ b/zerver/tests/test_realm.py @@ -21,6 +21,7 @@ from zerver.actions.message_send import ( from zerver.actions.realm_settings import ( do_add_deactivated_redirect, do_change_realm_org_type, + do_change_realm_permission_group_setting, do_change_realm_plan_type, do_deactivate_realm, do_delete_all_realm_attachments, @@ -798,6 +799,12 @@ class RealmTest(ZulipTestCase): self.assertEqual(realm.message_visibility_limit, None) self.assertEqual(realm.upload_quota_gb, None) + members_system_group = UserGroup.objects.get(name=SystemGroups.MEMBERS, realm=realm) + do_change_realm_permission_group_setting( + realm, "can_access_all_users_group", members_system_group, acting_user=None + ) + self.assertEqual(realm.can_access_all_users_group_id, members_system_group.id) + do_change_realm_plan_type(realm, Realm.PLAN_TYPE_STANDARD, acting_user=iago) realm = get_realm("zulip") realm_audit_log = RealmAuditLog.objects.filter( @@ -814,6 +821,8 @@ class RealmTest(ZulipTestCase): self.assertEqual(realm.max_invites, Realm.INVITES_STANDARD_REALM_DAILY_MAX) self.assertEqual(realm.message_visibility_limit, None) self.assertEqual(realm.upload_quota_gb, Realm.UPLOAD_QUOTA_STANDARD) + everyone_system_group = UserGroup.objects.get(name=SystemGroups.EVERYONE, realm=realm) + self.assertEqual(realm.can_access_all_users_group_id, everyone_system_group.id) do_set_realm_property(realm, "enable_spectator_access", True, acting_user=None) do_change_realm_plan_type(realm, Realm.PLAN_TYPE_LIMITED, acting_user=iago) @@ -839,6 +848,17 @@ class RealmTest(ZulipTestCase): self.assertEqual(realm.message_visibility_limit, None) self.assertEqual(realm.upload_quota_gb, Realm.UPLOAD_QUOTA_STANDARD) + do_change_realm_permission_group_setting( + realm, "can_access_all_users_group", members_system_group, acting_user=None + ) + do_change_realm_plan_type(realm, Realm.PLAN_TYPE_STANDARD, acting_user=iago) + realm = get_realm("zulip") + self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_STANDARD) + self.assertEqual(realm.max_invites, Realm.INVITES_STANDARD_REALM_DAILY_MAX) + self.assertEqual(realm.message_visibility_limit, None) + self.assertEqual(realm.upload_quota_gb, Realm.UPLOAD_QUOTA_STANDARD) + self.assertEqual(realm.can_access_all_users_group_id, everyone_system_group.id) + do_change_realm_plan_type(realm, Realm.PLAN_TYPE_SELF_HOSTED, acting_user=iago) self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_SELF_HOSTED) self.assertEqual(realm.max_invites, settings.INVITES_DEFAULT_REALM_DAILY_MAX) @@ -1601,6 +1621,21 @@ class RealmAPITest(ZulipTestCase): result = self.client_patch("/json/realm", req) self.assert_json_error(result, "Available on Zulip Cloud Standard. Upgrade to access.") + def test_changing_can_access_all_users_group_based_on_plan_type(self) -> None: + realm = get_realm("zulip") + do_change_realm_plan_type(realm, Realm.PLAN_TYPE_LIMITED, acting_user=None) + self.login("iago") + + members_group = UserGroup.objects.get(name="role:members", realm=realm) + req = {"can_access_all_users_group": orjson.dumps(members_group.id).decode()} + result = self.client_patch("/json/realm", req) + self.assert_json_error(result, "Available on Zulip Cloud Plus. Upgrade to access.") + + do_change_realm_plan_type(realm, Realm.PLAN_TYPE_STANDARD, acting_user=None) + req = {"can_access_all_users_group": orjson.dumps(members_group.id).decode()} + result = self.client_patch("/json/realm", req) + self.assert_json_error(result, "Available on Zulip Cloud Plus. Upgrade to access.") + class ScrubRealmTest(ZulipTestCase): def test_do_delete_all_realm_attachments(self) -> None: diff --git a/zerver/views/realm.py b/zerver/views/realm.py index 6bdca491a3..1dbf127805 100644 --- a/zerver/views/realm.py +++ b/zerver/views/realm.py @@ -239,6 +239,8 @@ def update_realm( # Remove this when the feature is ready for production. assert settings.DEVELOPMENT + realm.can_enable_restricted_user_access_for_guests() + data: Dict[str, Any] = {} message_content_delete_limit_seconds: Optional[int] = None