diff --git a/api_docs/changelog.md b/api_docs/changelog.md index afe7141728..9e02e6d9c1 100644 --- a/api_docs/changelog.md +++ b/api_docs/changelog.md @@ -20,6 +20,15 @@ format used by the Zulip server that they are interacting with. ## Changes in Zulip 10.0 +**Feature level 321** + +* `PATCH /realm`, [`GET /events`](/api/get-events), + [`POST /register`](/api/register-queue): + Added `can_invite_users_group` realm setting which is a + [group-setting value](/api/group-setting-values) describing the set of users + with permission to send email invitations for inviting other users to the + organization. + **Feature level 320** * [`GET /users/me/subscriptions`](/api/get-subscriptions), diff --git a/version.py b/version.py index f8a8060479..897cec574d 100644 --- a/version.py +++ b/version.py @@ -34,9 +34,7 @@ DESKTOP_WARNING_VERSION = "5.9.3" # new level means in api_docs/changelog.md, as well as "**Changes**" # entries in the endpoint's documentation in `zulip.yaml`. -API_FEATURE_LEVEL = ( - 320 # Last bumped for supporting anonymous groups for can_remove_subscribers_group -) +API_FEATURE_LEVEL = 321 # Last bumped for can_invite_users_group # Bump the minor PROVISION_VERSION to indicate that folks should provision # only when going from an old version of the code to a newer version. Bump diff --git a/web/src/server_events_dispatch.js b/web/src/server_events_dispatch.js index c4293026dc..55c1e5bcea 100644 --- a/web/src/server_events_dispatch.js +++ b/web/src/server_events_dispatch.js @@ -233,7 +233,6 @@ export function dispatch_normal_event(event) { disallow_disposable_email_addresses: noop, inline_image_preview: noop, inline_url_embed_preview: noop, - invite_to_realm_policy: noop, invite_required: noop, mandatory_topics: noop, message_content_edit_limit_seconds: noop, @@ -273,12 +272,6 @@ export function dispatch_normal_event(event) { electron_bridge?.send_event("realm_name", event.value); } - if (event.property === "invite_to_realm_policy") { - settings_invites.update_invite_user_panel(); - sidebar_ui.update_invite_user_option(); - gear_menu.rerender(); - } - if (event.property === "enable_spectator_access") { stream_settings_ui.update_stream_privacy_choices( "can_create_web_public_channel_group", @@ -300,7 +293,10 @@ export function dispatch_normal_event(event) { settings_org.sync_realm_settings(key); } - if (key === "create_multiuse_invite_group") { + if ( + key === "create_multiuse_invite_group" || + key === "can_invite_users_group" + ) { settings_invites.update_invite_user_panel(); sidebar_ui.update_invite_user_option(); gear_menu.rerender(); diff --git a/web/src/settings_components.ts b/web/src/settings_components.ts index bbfc465668..961ffb3799 100644 --- a/web/src/settings_components.ts +++ b/web/src/settings_components.ts @@ -238,7 +238,6 @@ export function get_subsection_property_elements($subsection: JQuery): HTMLEleme export const simple_dropdown_realm_settings_schema = realm_schema.pick({ realm_invite_to_stream_policy: true, - realm_invite_to_realm_policy: true, realm_wildcard_mention_policy: true, realm_org_type: true, }); @@ -804,6 +803,7 @@ export function check_realm_settings_property_changed(elem: HTMLElement): boolea case "realm_can_create_private_channel_group": case "realm_can_delete_any_message_group": case "realm_can_delete_own_message_group": + case "realm_can_invite_users_group": case "realm_can_manage_all_groups": case "realm_can_move_messages_between_channels_group": case "realm_can_move_messages_between_topics_group": @@ -1055,6 +1055,7 @@ export function populate_data_for_realm_settings_request( "can_manage_all_groups", "can_delete_any_message_group", "can_delete_own_message_group", + "can_invite_users_group", "can_move_messages_between_channels_group", "can_move_messages_between_topics_group", "create_multiuse_invite_group", @@ -1502,6 +1503,7 @@ export const group_setting_widget_map = new Map settings_save_discard_widget section_name="join-settings" }}
-
- {{> settings_checkbox - setting_name="realm_invite_required" - prefix="id_" - is_checked=realm_invite_required - label=admin_settings_label.realm_invite_required}} - - -
+ + {{> group_setting_value_pill_input + setting_name="realm_can_invite_users_group" + label=(t 'Who can send email invitations to new users')}} {{> group_setting_value_pill_input setting_name="realm_create_multiuse_invite_group" diff --git a/web/tests/dispatch.test.cjs b/web/tests/dispatch.test.cjs index 0202960b7e..58e33a5569 100644 --- a/web/tests/dispatch.test.cjs +++ b/web/tests/dispatch.test.cjs @@ -539,9 +539,6 @@ run_test("realm settings", ({override}) => { event = event_fixtures.realm__update__invite_required; test_realm_boolean(event, "realm_invite_required"); - event = event_fixtures.realm__update__invite_to_realm_policy; - test_realm_integer(event, "realm_invite_to_realm_policy"); - event = event_fixtures.realm__update__want_advertise_in_communities_directory; test_realm_boolean(event, "realm_want_advertise_in_communities_directory"); @@ -605,6 +602,7 @@ run_test("realm settings", ({override}) => { override(realm, "realm_authentication_methods", {Google: {enabled: false, available: true}}); override(realm, "realm_can_add_custom_emoji_group", 1); override(realm, "realm_can_create_public_channel_group", 1); + override(realm, "realm_can_invite_users_group", 1); override(realm, "realm_can_move_messages_between_topics_group", 1); override(realm, "realm_direct_message_permission_group", 1); override(realm, "realm_plan_type", 2); @@ -620,6 +618,7 @@ run_test("realm settings", ({override}) => { }); assert_same(realm.realm_can_add_custom_emoji_group, 3); assert_same(realm.realm_can_create_public_channel_group, 3); + assert_same(realm.realm_can_invite_users_group, 3); assert_same(realm.realm_can_move_messages_between_topics_group, 3); assert_same(realm.realm_direct_message_permission_group, 3); assert_same(realm.realm_plan_type, 3); diff --git a/web/tests/lib/events.cjs b/web/tests/lib/events.cjs index 2b47162242..49db2ec8ba 100644 --- a/web/tests/lib/events.cjs +++ b/web/tests/lib/events.cjs @@ -302,13 +302,6 @@ exports.fixtures = { value: false, }, - realm__update__invite_to_realm_policy: { - type: "realm", - op: "update", - property: "invite_to_realm_policy", - value: 2, - }, - realm__update__invite_to_stream_policy: { type: "realm", op: "update", @@ -371,6 +364,7 @@ exports.fixtures = { }, can_add_custom_emoji_group: 3, can_create_public_channel_group: 3, + can_invite_users_group: 3, can_move_messages_between_topics_group: 3, direct_message_permission_group: 3, plan_type: 3, diff --git a/web/tests/settings_data.test.cjs b/web/tests/settings_data.test.cjs index a271e505a3..c1ab92ebaf 100644 --- a/web/tests/settings_data.test.cjs +++ b/web/tests/settings_data.test.cjs @@ -157,11 +157,6 @@ test_policy( "realm_invite_to_stream_policy", settings_data.user_can_subscribe_other_users, ); -test_policy( - "user_can_invite_others_to_realm", - "realm_invite_to_realm_policy", - settings_data.user_can_invite_users_by_email, -); test_realm_group_settings( "realm_can_add_custom_emoji_group", @@ -178,6 +173,11 @@ test_realm_group_settings( settings_data.user_can_delete_own_message, ); +test_realm_group_settings( + "realm_can_invite_users_group", + settings_data.user_can_invite_users_by_email, +); + test_realm_group_settings( "realm_can_move_messages_between_channels_group", settings_data.user_can_move_messages_between_streams, @@ -210,17 +210,6 @@ run_test("using_dark_theme", ({override}) => { assert.equal(settings_data.using_dark_theme(), false); }); -run_test("user_can_invite_others_to_realm_nobody_case", ({override}) => { - override(current_user, "is_admin", true); - override(current_user, "is_guest", false); - override( - realm, - "realm_invite_to_realm_policy", - settings_config.email_invite_to_realm_policy_values.nobody.code, - ); - assert.equal(settings_data.user_can_invite_users_by_email(), false); -}); - run_test("user_email_not_configured", ({override}) => { const user_email_not_configured = settings_data.user_email_not_configured; diff --git a/web/tests/settings_org.test.cjs b/web/tests/settings_org.test.cjs index fde57155c1..de5bf52f21 100644 --- a/web/tests/settings_org.test.cjs +++ b/web/tests/settings_org.test.cjs @@ -105,7 +105,6 @@ function test_submit_settings_form(override, submit_form) { realm_waiting_period_threshold: 1, realm_default_language: '"es"', realm_invite_to_stream_policy: settings_config.common_policy_values.by_admins_only.code, - realm_invite_to_realm_policy: settings_config.common_policy_values.by_members.code, }); override(global, "setTimeout", (func) => func()); @@ -142,15 +141,9 @@ function test_submit_settings_form(override, submit_form) { $bot_creation_policy_elem.attr("id", "id_realm_bot_creation_policy"); $bot_creation_policy_elem.data = () => "number"; - const $invite_to_realm_policy_elem = $("#id_realm_invite_to_realm_policy"); - $invite_to_realm_policy_elem.val("2"); - $invite_to_realm_policy_elem.attr("id", "id_realm_invite_to_realm_policy"); - $invite_to_realm_policy_elem.data = () => "number"; - let $subsection_elem = $(`#org-${CSS.escape(subsection)}`); $subsection_elem.set_find_results(".prop-element", [ $bot_creation_policy_elem, - $invite_to_realm_policy_elem, $invite_to_stream_policy_elem, ]); @@ -160,7 +153,6 @@ function test_submit_settings_form(override, submit_form) { let expected_value = { bot_creation_policy: 1, - invite_to_realm_policy: 2, invite_to_stream_policy: 1, }; assert.deepEqual(data, expected_value); @@ -317,7 +309,6 @@ function test_sync_realm_settings({override}) { } test_common_policy("invite_to_stream_policy"); - test_common_policy("invite_to_realm_policy"); { /* Test message content edit limit minutes sync */ @@ -373,9 +364,9 @@ function test_sync_realm_settings({override}) { { // Test hiding save-discard buttons on live-updating. - const $property_elem = $("#id_realm_invite_to_realm_policy"); + const $property_elem = $("#id_realm_invite_to_stream_policy"); $property_elem.length = 1; - $property_elem.attr("id", "id_realm_invite_to_realm_policy"); + $property_elem.attr("id", "id_realm_invite_to_stream_policy"); $property_elem.closest = () => $subsection_stub; const save_button_stubs = createSaveButtons("subsection-stub"); @@ -386,13 +377,13 @@ function test_sync_realm_settings({override}) { $property_elem.val(settings_config.common_policy_values.by_admins_only.code); override( realm, - "realm_invite_to_realm_policy", + "realm_invite_to_stream_policy", settings_config.common_policy_values.by_members.code, ); save_button_stubs.$save_button_controls.removeClass("hide"); $subsection_stub.set_find_results(".prop-element", [$property_elem]); - settings_org.sync_realm_settings("invite_to_realm_policy"); + settings_org.sync_realm_settings("invite_to_stream_policy"); assert.equal(save_button_stubs.props.hidden, true); } } diff --git a/zerver/actions/create_realm.py b/zerver/actions/create_realm.py index 13383f9a1d..c977fee6d5 100644 --- a/zerver/actions/create_realm.py +++ b/zerver/actions/create_realm.py @@ -37,7 +37,7 @@ from zerver.models import ( from zerver.models.groups import SystemGroups from zerver.models.presence import PresenceSequence from zerver.models.realm_audit_logs import AuditLogEventType -from zerver.models.realms import CommonPolicyEnum, InviteToRealmPolicyEnum +from zerver.models.realms import CommonPolicyEnum from zproject.backends import all_default_backend_names @@ -115,8 +115,6 @@ def set_realm_permissions_based_on_org_type(realm: Realm) -> None: Realm.ORG_TYPES["education_nonprofit"]["id"], Realm.ORG_TYPES["education"]["id"], ): - # Limit user creation to administrators. - realm.invite_to_realm_policy = InviteToRealmPolicyEnum.ADMINS_ONLY # Don't allow members (students) to manage user groups or # stream subscriptions. realm.invite_to_stream_policy = CommonPolicyEnum.MODERATORS_ONLY @@ -290,6 +288,10 @@ def do_create_realm( Realm.ORG_TYPES["education_nonprofit"]["id"]: SystemGroups.MODERATORS, Realm.ORG_TYPES["education"]["id"]: SystemGroups.MODERATORS, }, + "can_invite_users_group": { + Realm.ORG_TYPES["education_nonprofit"]["id"]: SystemGroups.ADMINISTRATORS, + Realm.ORG_TYPES["education"]["id"]: SystemGroups.ADMINISTRATORS, + }, "can_move_messages_between_channels_group": { Realm.ORG_TYPES["education_nonprofit"]["id"]: SystemGroups.MODERATORS, Realm.ORG_TYPES["education"]["id"]: SystemGroups.MODERATORS, diff --git a/zerver/lib/event_schema.py b/zerver/lib/event_schema.py index 7cf965d2ff..cc0413bd33 100644 --- a/zerver/lib/event_schema.py +++ b/zerver/lib/event_schema.py @@ -1077,6 +1077,7 @@ group_setting_update_data_type = DictType( ("can_create_web_public_channel_group", group_setting_type), ("can_delete_any_message_group", group_setting_type), ("can_delete_own_message_group", group_setting_type), + ("can_invite_users_group", group_setting_type), ("can_manage_all_groups", group_setting_type), ("can_move_messages_between_channels_group", group_setting_type), ("can_move_messages_between_topics_group", group_setting_type), diff --git a/zerver/lib/events.py b/zerver/lib/events.py index d426418ec6..29b19785fe 100644 --- a/zerver/lib/events.py +++ b/zerver/lib/events.py @@ -589,7 +589,7 @@ def fetch_initial_state_data( or state["can_create_web_public_streams"] ) state["can_subscribe_other_users"] = settings_user.can_subscribe_other_users() - state["can_invite_others_to_realm"] = settings_user.can_invite_users_by_email() + state["can_invite_others_to_realm"] = settings_user.can_invite_users_by_email(realm) state["is_admin"] = settings_user.is_realm_admin state["is_owner"] = settings_user.is_realm_owner state["is_moderator"] = settings_user.is_moderator @@ -1302,26 +1302,11 @@ def apply_event( else state["server_jitsi_server_url"] ) - policy_permission_dict = { - "invite_to_stream_policy": "can_subscribe_other_users", - "invite_to_realm_policy": "can_invite_others_to_realm", - } - - # Tricky interaction: Whether we can create streams and can subscribe other users - # can get changed here. - - if field == "realm_waiting_period_threshold": - for policy, permission in policy_permission_dict.items(): - if permission in state: - state[permission] = user_profile.has_permission(policy) - if ( - event["property"] in policy_permission_dict - and policy_permission_dict[event["property"]] in state + event["property"] == "invite_to_stream_policy" + and "can_subscribe_other_users" in state ): - state[policy_permission_dict[event["property"]]] = user_profile.has_permission( - event["property"] - ) + state["can_subscribe_other_users"] = user_profile.has_permission(event["property"]) elif event["op"] == "update_dict": for key, value in event["data"].items(): if key == "max_file_upload_size_mib": @@ -1377,6 +1362,11 @@ def apply_event( or state["can_create_web_public_streams"] ) + if key == "can_invite_users_group" and "can_invite_others_to_realm" in state: + state["can_invite_others_to_realm"] = user_profile.has_permission( + "can_invite_users_group" + ) + if key == "plan_type": # Then there are some extra fields that also need to be set. state["zulip_plan_is_not_limited"] = value != Realm.PLAN_TYPE_LIMITED diff --git a/zerver/migrations/0625_realm_can_invite_users_group.py b/zerver/migrations/0625_realm_can_invite_users_group.py new file mode 100644 index 0000000000..8a9b74d273 --- /dev/null +++ b/zerver/migrations/0625_realm_can_invite_users_group.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.9 on 2024-11-13 18:13 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("zerver", "0624_alter_realmexport_tarball_size_bytes"), + ] + + operations = [ + migrations.AddField( + model_name="realm", + name="can_invite_users_group", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="+", + to="zerver.usergroup", + ), + ), + ] diff --git a/zerver/migrations/0626_set_default_value_for_can_invite_users_group.py b/zerver/migrations/0626_set_default_value_for_can_invite_users_group.py new file mode 100644 index 0000000000..5167013044 --- /dev/null +++ b/zerver/migrations/0626_set_default_value_for_can_invite_users_group.py @@ -0,0 +1,44 @@ +# Generated by Django 4.2.1 on 2023-06-12 10:47 + +from django.db import migrations +from django.db.backends.base.schema import BaseDatabaseSchemaEditor +from django.db.migrations.state import StateApps +from django.db.models import OuterRef + + +def set_default_value_for_can_invite_users_group( + apps: StateApps, schema_editor: BaseDatabaseSchemaEditor +) -> None: + Realm = apps.get_model("zerver", "Realm") + NamedUserGroup = apps.get_model("zerver", "NamedUserGroup") + + invite_to_realm_policy_to_group_name = { + 1: "role:members", + 2: "role:administrators", + 3: "role:fullmembers", + 4: "role:moderators", + 6: "role:nobody", + } + + for id, group_name in invite_to_realm_policy_to_group_name.items(): + Realm.objects.filter(can_invite_users_group=None, invite_to_realm_policy=id).update( + can_invite_users_group=NamedUserGroup.objects.filter( + name=group_name, realm=OuterRef("id"), is_system_group=True + ).values("pk") + ) + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ("zerver", "0625_realm_can_invite_users_group"), + ] + + operations = [ + migrations.RunPython( + set_default_value_for_can_invite_users_group, + elidable=True, + reverse_code=migrations.RunPython.noop, + ) + ] diff --git a/zerver/migrations/0627_alter_realm_can_invite_users_group.py b/zerver/migrations/0627_alter_realm_can_invite_users_group.py new file mode 100644 index 0000000000..35045954e2 --- /dev/null +++ b/zerver/migrations/0627_alter_realm_can_invite_users_group.py @@ -0,0 +1,22 @@ +# Generated by Django 5.0.9 on 2024-11-16 07:59 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("zerver", "0626_set_default_value_for_can_invite_users_group"), + ] + + operations = [ + migrations.AlterField( + model_name="realm", + name="can_invite_users_group", + field=models.ForeignKey( + on_delete=django.db.models.deletion.RESTRICT, + related_name="+", + to="zerver.usergroup", + ), + ), + ] diff --git a/zerver/models/realms.py b/zerver/models/realms.py index 00d52ece3f..9a32181c6f 100644 --- a/zerver/models/realms.py +++ b/zerver/models/realms.py @@ -305,6 +305,11 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub default=InviteToRealmPolicyEnum.MEMBERS_ONLY ) + # UserGroup whose members are allowed to invite other users to organization. + can_invite_users_group = models.ForeignKey( + "UserGroup", on_delete=models.RESTRICT, related_name="+" + ) + # UserGroup whose members are allowed to create invite link. create_multiuse_invite_group = models.ForeignKey( "UserGroup", on_delete=models.RESTRICT, related_name="+" @@ -759,6 +764,15 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub default_group_name=SystemGroups.EVERYONE, id_field_name="can_delete_own_message_group_id", ), + can_invite_users_group=GroupPermissionSetting( + require_system_group=False, + allow_internet_group=False, + allow_owners_group=False, + allow_nobody_group=True, + allow_everyone_group=False, + default_group_name=SystemGroups.MEMBERS, + id_field_name="can_invite_users_group_id", + ), can_manage_all_groups=GroupPermissionSetting( require_system_group=False, allow_internet_group=False, @@ -1198,6 +1212,8 @@ def get_realm_with_settings(realm_id: int) -> Realm: "can_delete_any_message_group__named_user_group", "can_delete_own_message_group", "can_delete_own_message_group__named_user_group", + "can_invite_users_group", + "can_invite_users_group__named_user_group", "can_manage_all_groups", "can_manage_all_groups__named_user_group", "can_move_messages_between_channels_group", diff --git a/zerver/models/users.py b/zerver/models/users.py index 4efe99a9b1..53705fe5f8 100644 --- a/zerver/models/users.py +++ b/zerver/models/users.py @@ -816,10 +816,10 @@ class UserProfile(AbstractBaseUser, PermissionsMixin, UserBaseSettings): from zerver.lib.user_groups import user_has_permission_for_group_setting from zerver.models import Realm - if policy_name not in Realm.REALM_PERMISSION_GROUP_SETTINGS and policy_name not in [ - "invite_to_stream_policy", - "invite_to_realm_policy", - ]: + if ( + policy_name not in Realm.REALM_PERMISSION_GROUP_SETTINGS + and policy_name != "invite_to_stream_policy" + ): raise AssertionError("Invalid policy") if policy_name in Realm.REALM_PERMISSION_GROUP_SETTINGS: @@ -880,8 +880,8 @@ class UserProfile(AbstractBaseUser, PermissionsMixin, UserBaseSettings): def can_subscribe_other_users(self) -> bool: return self.has_permission("invite_to_stream_policy") - def can_invite_users_by_email(self) -> bool: - return self.has_permission("invite_to_realm_policy") + def can_invite_users_by_email(self, realm: Optional["Realm"] = None) -> bool: + return self.has_permission("can_invite_users_group", realm) def can_create_multiuse_invite_to_realm(self) -> bool: return self.has_permission("create_multiuse_invite_group") diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index f9f83dcd3b..2f1f4c3d8d 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -4531,6 +4531,21 @@ paths: setting controlled this permission; `true` corresponded to `Everyone`, and `false` to `Admins`. - $ref: "#/components/schemas/GroupSettingValue" + can_invite_users_group: + allOf: + - description: | + A [group-setting value](/api/group-setting-values) defining the set of + users who have permission to send email invitations for inviting other users + to the organization. + + **Changes**: New in Zulip 10.0 (feature level 321). Previously, this + permission was controlled by the enum `invite_to_realm_policy`. Values + were 1=Members, 2=Admins, 3=Full members, 4=Moderators, 6=Nobody. + + Before Zulip 4.0 (feature level 50), the `invite_by_admins_only` boolean + setting controlled this permission; `true` corresponded to `Admins`, and + `false` to `Members`. + - $ref: "#/components/schemas/GroupSettingValue" can_move_messages_between_channels_group: allOf: - description: | @@ -16362,6 +16377,23 @@ paths: setting controlled this permission; `true` corresponded to `Everyone`, and `false` to `Admins`. - $ref: "#/components/schemas/GroupSettingValue" + realm_can_invite_users_group: + allOf: + - description: | + Present if `realm` is present in `fetch_event_types`. + + A [group-setting value](/api/group-setting-values) defining the set of + users who have permission to send email invitations for inviting other users + to the organization. + + **Changes**: New in Zulip 10.0 (feature level 321). Previously, this + permission was controlled by the enum `invite_to_realm_policy`. Values + were 1=Members, 2=Admins, 3=Full members, 4=Moderators, 6=Nobody. + + Before Zulip 4.0 (feature level 50), the `invite_by_admins_only` boolean + setting controlled this permission; `true` corresponded to `Admins`, and + `false` to `Members`. + - $ref: "#/components/schemas/GroupSettingValue" realm_can_move_messages_between_channels_group: allOf: - description: | diff --git a/zerver/tests/test_event_system.py b/zerver/tests/test_event_system.py index ba11604373..5ecd6c47a2 100644 --- a/zerver/tests/test_event_system.py +++ b/zerver/tests/test_event_system.py @@ -1192,7 +1192,7 @@ class FetchQueriesTest(ZulipTestCase): realm = get_realm_with_settings(realm_id=user.realm_id) with ( - self.assert_database_query_count(42), + self.assert_database_query_count(43), mock.patch("zerver.lib.events.always_want") as want_mock, ): fetch_initial_state_data(user, realm=realm) @@ -1217,7 +1217,7 @@ class FetchQueriesTest(ZulipTestCase): realm_filters=0, realm_linkifiers=0, realm_playgrounds=1, - realm_user=4, + realm_user=5, realm_user_groups=3, realm_user_settings_defaults=1, recent_private_conversations=1, diff --git a/zerver/tests/test_home.py b/zerver/tests/test_home.py index 85b5cdffda..ff411a2d14 100644 --- a/zerver/tests/test_home.py +++ b/zerver/tests/test_home.py @@ -136,6 +136,7 @@ class HomeTest(ZulipTestCase): "realm_can_create_web_public_channel_group", "realm_can_delete_any_message_group", "realm_can_delete_own_message_group", + "realm_can_invite_users_group", "realm_can_manage_all_groups", "realm_can_move_messages_between_channels_group", "realm_can_move_messages_between_topics_group", @@ -271,7 +272,7 @@ class HomeTest(ZulipTestCase): # Verify succeeds once logged-in with ( - self.assert_database_query_count(52), + self.assert_database_query_count(53), patch("zerver.lib.cache.cache_set") as cache_mock, ): result = self._get_home_page(stream="Denmark") @@ -576,7 +577,7 @@ class HomeTest(ZulipTestCase): # Verify number of queries for Realm admin isn't much higher than for normal users. self.login("iago") with ( - self.assert_database_query_count(52), + self.assert_database_query_count(53), patch("zerver.lib.cache.cache_set") as cache_mock, ): result = self._get_home_page() @@ -608,7 +609,7 @@ class HomeTest(ZulipTestCase): self._get_home_page() # Then for the second page load, measure the number of queries. - with self.assert_database_query_count(47): + with self.assert_database_query_count(48): result = self._get_home_page() # Do a sanity check that our new streams were in the payload. diff --git a/zerver/tests/test_invite.py b/zerver/tests/test_invite.py index 06d2035439..2114ee1270 100644 --- a/zerver/tests/test_invite.py +++ b/zerver/tests/test_invite.py @@ -43,6 +43,7 @@ from zerver.actions.realm_settings import ( do_change_realm_plan_type, do_set_realm_property, ) +from zerver.actions.user_groups import check_add_user_group from zerver.actions.user_settings import do_change_full_name from zerver.actions.users import change_user_is_active from zerver.context_processors import common_context @@ -68,7 +69,7 @@ from zerver.models import ( UserProfile, ) from zerver.models.groups import SystemGroups -from zerver.models.realms import CommonPolicyEnum, InviteToRealmPolicyEnum, get_realm +from zerver.models.realms import CommonPolicyEnum, get_realm from zerver.models.streams import get_stream from zerver.models.users import get_user_by_delivery_email from zerver.views.invite import INVITATION_LINK_VALIDITY_MINUTES, get_invitee_emails_set @@ -850,26 +851,33 @@ class InviteUserTest(InviteUserBase): self.submit_reg_form_for_user(invitee, "password") self.check_user_subscribed_only_to_streams("test1", [denmark, sandbox, verona, zulip]) - def test_can_invite_others_to_realm(self) -> None: - def validation_func(user_profile: UserProfile) -> bool: - return user_profile.can_invite_users_by_email() - - realm = get_realm("zulip") - do_set_realm_property( - realm, "invite_to_realm_policy", InviteToRealmPolicyEnum.NOBODY, acting_user=None - ) - desdemona = self.example_user("desdemona") - self.assertFalse(validation_func(desdemona)) - - self.check_has_permission_policies("invite_to_realm_policy", validation_func) - def test_invite_others_to_realm_setting(self) -> None: """ - The invite_to_realm_policy realm setting works properly. + The `can_invite_users_group` realm setting works properly. """ realm = get_realm("zulip") - do_set_realm_property( - realm, "invite_to_realm_policy", InviteToRealmPolicyEnum.NOBODY, acting_user=None + + administrators_system_group = NamedUserGroup.objects.get( + name=SystemGroups.ADMINISTRATORS, realm=realm, is_system_group=True + ) + moderators_system_group = NamedUserGroup.objects.get( + name=SystemGroups.MODERATORS, realm=realm, is_system_group=True + ) + full_members_system_group = NamedUserGroup.objects.get( + name=SystemGroups.FULL_MEMBERS, realm=realm, is_system_group=True + ) + members_system_group = NamedUserGroup.objects.get( + name=SystemGroups.MEMBERS, realm=realm, is_system_group=True + ) + nobody_system_group = NamedUserGroup.objects.get( + name=SystemGroups.NOBODY, realm=realm, is_system_group=True + ) + + do_change_realm_permission_group_setting( + realm, + "can_invite_users_group", + nobody_system_group, + acting_user=None, ) self.login("desdemona") email = "alice-test@zulip.com" @@ -880,8 +888,11 @@ class InviteUserTest(InviteUserBase): "Insufficient permission", ) - do_set_realm_property( - realm, "invite_to_realm_policy", InviteToRealmPolicyEnum.ADMINS_ONLY, acting_user=None + do_change_realm_permission_group_setting( + realm, + "can_invite_users_group", + administrators_system_group, + acting_user=None, ) self.login("shiva") @@ -900,10 +911,10 @@ class InviteUserTest(InviteUserBase): mail.outbox = [] - do_set_realm_property( + do_change_realm_permission_group_setting( realm, - "invite_to_realm_policy", - InviteToRealmPolicyEnum.MODERATORS_ONLY, + "can_invite_users_group", + moderators_system_group, acting_user=None, ) self.login("hamlet") @@ -923,8 +934,11 @@ class InviteUserTest(InviteUserBase): mail.outbox = [] - do_set_realm_property( - realm, "invite_to_realm_policy", InviteToRealmPolicyEnum.MEMBERS_ONLY, acting_user=None + do_change_realm_permission_group_setting( + realm, + "can_invite_users_group", + members_system_group, + acting_user=None, ) self.login("polonius") @@ -941,16 +955,17 @@ class InviteUserTest(InviteUserBase): mail.outbox = [] - do_set_realm_property( + do_change_realm_permission_group_setting( realm, - "invite_to_realm_policy", - InviteToRealmPolicyEnum.FULL_MEMBERS_ONLY, + "can_invite_users_group", + full_members_system_group, 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() - timedelta(days=realm.waiting_period_threshold - 1) + hamlet.date_joined = timezone_now() - timedelta(days=9) + + do_set_realm_property(realm, "waiting_period_threshold", 10, acting_user=None) email = "issac-test@zulip.com" email2 = "steven-test@zulip.com" @@ -967,6 +982,60 @@ class InviteUserTest(InviteUserBase): self.assertTrue(find_key_by_email(email2)) self.check_sent_emails([email, email2]) + cordelia = self.example_user("cordelia") + + # Test for checking setting for non-system user group. + user_group = check_add_user_group( + realm, "new_group", [hamlet, cordelia], acting_user=hamlet + ) + do_change_realm_permission_group_setting( + realm, "can_invite_users_group", user_group, acting_user=None + ) + + # Hamlet and Cordelia are in the allowed user group, so can send email + # invitations. + self.login("hamlet") + self.assert_json_success(self.invite(invitee, ["Denmark"])) + self.login("cordelia") + self.assert_json_success(self.invite(invitee, ["Denmark"])) + + # Iago is not in the allowed user group, so cannot send email + # invitations. + self.login("iago") + self.assert_json_error( + self.invite(invitee, ["Denmark"]), + "Insufficient permission", + ) + + # Test for checking the setting for anonymous user group. + anonymous_user_group = self.create_or_update_anonymous_group_for_setting( + [hamlet], + [administrators_system_group], + ) + do_change_realm_permission_group_setting( + realm, + "can_invite_users_group", + anonymous_user_group, + acting_user=None, + ) + + # Hamlet is the direct member of the anonymous user group, so can send + # email invitations. + self.login("hamlet") + self.assert_json_success(self.invite(invitee, ["Denmark"])) + # Iago is in the `administrators_system_group` subgroup, so can send email + # invitations. + self.login("iago") + self.assert_json_success(self.invite(invitee, ["Denmark"])) + + # Shiva is not in the anonymous user group, so cannot send email + # invitations. + self.login("shiva") + self.assert_json_error( + self.invite(invitee, ["Denmark"]), + "Insufficient permission", + ) + def test_invite_user_signup_initial_history(self) -> None: """ Test that a new user invited to a stream receives some initial diff --git a/zerver/tests/test_realm.py b/zerver/tests/test_realm.py index dcac8f603c..be603d476f 100644 --- a/zerver/tests/test_realm.py +++ b/zerver/tests/test_realm.py @@ -113,13 +113,13 @@ class RealmTest(ZulipTestCase): ) self.assertEqual(realm.can_create_public_channel_group_id, admins_group.id) - self.assertEqual(realm.invite_to_realm_policy, InviteToRealmPolicyEnum.ADMINS_ONLY) self.assertEqual(realm.invite_to_stream_policy, CommonPolicyEnum.MODERATORS_ONLY) realm = get_realm("test_education_non_profit") moderators_group = NamedUserGroup.objects.get( name=SystemGroups.MODERATORS, realm=realm, is_system_group=True ) self.assertEqual(realm.can_create_groups.id, moderators_group.id) + self.assertEqual(realm.can_invite_users_group.id, admins_group.id) self.assertEqual(realm.can_move_messages_between_channels_group.id, moderators_group.id) def test_permission_for_education_for_profit_organization(self) -> None: @@ -134,13 +134,13 @@ class RealmTest(ZulipTestCase): ) self.assertEqual(realm.can_create_public_channel_group_id, admins_group.id) - self.assertEqual(realm.invite_to_realm_policy, InviteToRealmPolicyEnum.ADMINS_ONLY) self.assertEqual(realm.invite_to_stream_policy, CommonPolicyEnum.MODERATORS_ONLY) realm = get_realm("test_education_for_profit") moderators_group = NamedUserGroup.objects.get( name=SystemGroups.MODERATORS, realm=realm, is_system_group=True ) self.assertEqual(realm.can_create_groups.id, moderators_group.id) + self.assertEqual(realm.can_invite_users_group.id, admins_group.id) self.assertEqual(realm.can_move_messages_between_channels_group.id, moderators_group.id) def test_realm_enable_spectator_access(self) -> None: @@ -2349,6 +2349,9 @@ class RealmAPITest(ZulipTestCase): def test_can_create_groups_setting_requires_owner(self) -> None: self.do_test_changing_groups_setting_by_owners_only("can_create_groups") + def test_can_invite_users_group_setting_requires_owner(self) -> None: + self.do_test_changing_groups_setting_by_owners_only("can_invite_users_group") + def test_can_manage_all_groups_setting_requires_owner(self) -> None: self.do_test_changing_groups_setting_by_owners_only("can_manage_all_groups") diff --git a/zerver/views/realm.py b/zerver/views/realm.py index 6aa7f5e4ed..1bb3debca0 100644 --- a/zerver/views/realm.py +++ b/zerver/views/realm.py @@ -139,6 +139,7 @@ def update_realm( can_create_public_channel_group: Json[GroupSettingChangeRequest] | None = None, can_create_private_channel_group: Json[GroupSettingChangeRequest] | None = None, can_create_web_public_channel_group: Json[GroupSettingChangeRequest] | None = None, + can_invite_users_group: Json[GroupSettingChangeRequest] | None = None, can_manage_all_groups: Json[GroupSettingChangeRequest] | None = None, can_move_messages_between_channels_group: Json[GroupSettingChangeRequest] | None = None, can_move_messages_between_topics_group: Json[GroupSettingChangeRequest] | None = None, @@ -223,6 +224,7 @@ def update_realm( or invite_required is not None or create_multiuse_invite_group is not None or can_create_groups is not None + or can_invite_users_group is not None or can_manage_all_groups is not None ) and not user_profile.is_realm_owner: raise OrganizationOwnerRequiredError