From 060156fca41495163bfc0e23a012670fdd91d2eb Mon Sep 17 00:00:00 2001 From: Shubham Padia Date: Mon, 14 Oct 2024 06:05:55 +0000 Subject: [PATCH] user_groups: Add can_leave_group setting for user group. This field will be used to control permission for who can leave a user group. --- api_docs/changelog.md | 12 +++ version.py | 2 +- zerver/actions/user_groups.py | 1 + zerver/lib/event_schema.py | 2 + zerver/lib/import_realm.py | 3 +- zerver/lib/user_groups.py | 19 +++- ...0607_namedusergroup_add_can_leave_group.py | 23 +++++ .../0608_set_default_for_can_leave_group.py | 57 ++++++++++++ ...09_alter_namedusergroup_can_leave_group.py | 22 +++++ zerver/models/groups.py | 11 +++ zerver/openapi/curl_param_value_generators.py | 3 + zerver/openapi/zulip.yaml | 88 +++++++++++++++++++ zerver/views/user_groups.py | 4 + 13 files changed, 242 insertions(+), 5 deletions(-) create mode 100644 zerver/migrations/0607_namedusergroup_add_can_leave_group.py create mode 100644 zerver/migrations/0608_set_default_for_can_leave_group.py create mode 100644 zerver/migrations/0609_alter_namedusergroup_can_leave_group.py diff --git a/api_docs/changelog.md b/api_docs/changelog.md index 01c655132e..706e595b49 100644 --- a/api_docs/changelog.md +++ b/api_docs/changelog.md @@ -20,6 +20,18 @@ format used by the Zulip server that they are interacting with. ## Changes in Zulip 10.0 +**Feature level 308** + +* [`POST /register`](/api/register-queue), [`GET /events`](/api/get-events), + [`GET /user_groups`](/api/get-user-groups): Add `can_leave_group` to + user group objects. +* [`POST /user_groups/create`](/api/create-user-group): Added `can_leave_group` + parameter to support setting the user group whose members can leave the user + group. +* [`PATCH /user_groups/{user_group_id}`](/api/update-user-group): Added + `can_leave_group` parameter to support changing the user group whose + members can leave the specified user group. + **Feature level 307** * `PATCH /realm`, [`GET /events`](/api/get-events), diff --git a/version.py b/version.py index 6c73d6df0a..29923db71e 100644 --- a/version.py +++ b/version.py @@ -34,7 +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 = 307 # Last bumped for can_add_custom_emoji_group. +API_FEATURE_LEVEL = 308 # Last bumped for can_leave_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/zerver/actions/user_groups.py b/zerver/actions/user_groups.py index 4bcca3437a..e6fc48dc66 100644 --- a/zerver/actions/user_groups.py +++ b/zerver/actions/user_groups.py @@ -190,6 +190,7 @@ def do_send_create_user_group_event( direct_subgroup_ids=[direct_subgroup.id for direct_subgroup in direct_subgroups], can_add_members_group=get_group_setting_value_for_api(user_group.can_add_members_group), can_join_group=get_group_setting_value_for_api(user_group.can_join_group), + can_leave_group=get_group_setting_value_for_api(user_group.can_leave_group), can_manage_group=get_group_setting_value_for_api(user_group.can_manage_group), can_mention_group=get_group_setting_value_for_api(user_group.can_mention_group), deactivated=False, diff --git a/zerver/lib/event_schema.py b/zerver/lib/event_schema.py index d0512eb2af..5b94fe66bd 100644 --- a/zerver/lib/event_schema.py +++ b/zerver/lib/event_schema.py @@ -1846,6 +1846,7 @@ group_type = DictType( ("is_system_group", bool), ("can_add_members_group", group_setting_type), ("can_join_group", group_setting_type), + ("can_leave_group", group_setting_type), ("can_manage_group", group_setting_type), ("can_mention_group", group_setting_type), ("deactivated", bool), @@ -1897,6 +1898,7 @@ user_group_data_type = DictType( ("description", str), ("can_add_members_group", group_setting_type), ("can_join_group", group_setting_type), + ("can_leave_group", group_setting_type), ("can_manage_group", group_setting_type), ("can_mention_group", group_setting_type), ("deactivated", bool), diff --git a/zerver/lib/import_realm.py b/zerver/lib/import_realm.py index 83064ca041..d58cbda996 100644 --- a/zerver/lib/import_realm.py +++ b/zerver/lib/import_realm.py @@ -759,6 +759,7 @@ def bulk_import_named_user_groups(data: TableData) -> None: group["is_system_group"], group["can_add_members_group_id"], group["can_join_group_id"], + group["can_leave_group_id"], group["can_manage_group_id"], group["can_mention_group_id"], group["deactivated"], @@ -769,7 +770,7 @@ def bulk_import_named_user_groups(data: TableData) -> None: query = SQL( """ - INSERT INTO zerver_namedusergroup (usergroup_ptr_id, realm_id, name, description, is_system_group, can_add_members_group_id, can_join_group_id, can_manage_group_id, can_mention_group_id, deactivated, date_created) + INSERT INTO zerver_namedusergroup (usergroup_ptr_id, realm_id, name, description, is_system_group, can_add_members_group_id, can_join_group_id, can_leave_group_id, can_manage_group_id, can_mention_group_id, deactivated, date_created) VALUES %s """ ) diff --git a/zerver/lib/user_groups.py b/zerver/lib/user_groups.py index 0ef7defd1b..74f3dbde45 100644 --- a/zerver/lib/user_groups.py +++ b/zerver/lib/user_groups.py @@ -57,6 +57,7 @@ class UserGroupDict(TypedDict): is_system_group: bool can_add_members_group: int | AnonymousSettingGroupDict can_join_group: int | AnonymousSettingGroupDict + can_leave_group: int | AnonymousSettingGroupDict can_manage_group: int | AnonymousSettingGroupDict can_mention_group: int | AnonymousSettingGroupDict deactivated: bool @@ -561,6 +562,8 @@ def user_groups_in_realm_serialized( "can_add_members_group__named_user_group", "can_join_group", "can_join_group__named_user_group", + "can_leave_group", + "can_leave_group__named_user_group", "can_manage_group", "can_manage_group__named_user_group", "can_mention_group", @@ -621,6 +624,9 @@ def user_groups_in_realm_serialized( can_join_group=get_setting_value_for_user_group_object( user_group.can_join_group, group_members, group_subgroups ), + can_leave_group=get_setting_value_for_user_group_object( + user_group.can_leave_group, group_members, group_subgroups + ), can_manage_group=get_setting_value_for_user_group_object( user_group.can_manage_group, group_members, group_subgroups ), @@ -842,7 +848,7 @@ def bulk_create_system_user_groups(groups: list[dict[str, str]], realm: Realm) - user_group_ids = [id for (id,) in cursor.fetchall()] rows = [ - SQL("({},{},{},{},{},{},{},{},{},{})").format( + SQL("({},{},{},{},{},{},{},{},{},{},{})").format( Literal(user_group_ids[idx]), Literal(realm.id), Literal(group["name"]), @@ -852,13 +858,14 @@ def bulk_create_system_user_groups(groups: list[dict[str, str]], realm: Realm) - Literal(initial_group_setting_value), Literal(initial_group_setting_value), Literal(initial_group_setting_value), + Literal(initial_group_setting_value), Literal(False), ) for idx, group in enumerate(groups) ] query = SQL( """ - INSERT INTO zerver_namedusergroup (usergroup_ptr_id, realm_id, name, description, is_system_group, can_add_members_group_id, can_join_group_id, can_manage_group_id, can_mention_group_id, deactivated) + INSERT INTO zerver_namedusergroup (usergroup_ptr_id, realm_id, name, description, is_system_group, can_add_members_group_id, can_join_group_id, can_leave_group_id, can_manage_group_id, can_mention_group_id, deactivated) VALUES {rows} """ ).format(rows=SQL(", ").join(rows)) @@ -941,7 +948,13 @@ def create_system_user_groups_for_realm(realm: Realm) -> dict[int, NamedUserGrou groups_with_updated_settings.append(user_group) NamedUserGroup.objects.bulk_update( groups_with_updated_settings, - ["can_add_members_group", "can_join_group", "can_manage_group", "can_mention_group"], + [ + "can_add_members_group", + "can_join_group", + "can_leave_group", + "can_manage_group", + "can_mention_group", + ], ) subgroup_objects: list[GroupGroupMembership] = [] diff --git a/zerver/migrations/0607_namedusergroup_add_can_leave_group.py b/zerver/migrations/0607_namedusergroup_add_can_leave_group.py new file mode 100644 index 0000000000..8700b05299 --- /dev/null +++ b/zerver/migrations/0607_namedusergroup_add_can_leave_group.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.9 on 2024-10-14 04:57 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("zerver", "0606_remove_realm_add_custom_emoji_policy"), + ] + + operations = [ + migrations.AddField( + model_name="namedusergroup", + name="can_leave_group", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="+", + to="zerver.usergroup", + ), + ), + ] diff --git a/zerver/migrations/0608_set_default_for_can_leave_group.py b/zerver/migrations/0608_set_default_for_can_leave_group.py new file mode 100644 index 0000000000..e100831d6d --- /dev/null +++ b/zerver/migrations/0608_set_default_for_can_leave_group.py @@ -0,0 +1,57 @@ +# Generated by Django 5.0.9 on 2024-10-14 05:08 + +from django.db import migrations, transaction +from django.db.backends.base.schema import BaseDatabaseSchemaEditor +from django.db.migrations.state import StateApps +from django.db.models import Max, Min, OuterRef + + +def set_default_value_for_can_leave_group( + apps: StateApps, schema_editor: BaseDatabaseSchemaEditor +) -> None: + NamedUserGroup = apps.get_model("zerver", "NamedUserGroup") + BATCH_SIZE = 1000 + + max_id = NamedUserGroup.objects.filter(can_leave_group=None).aggregate(Max("id"))["id__max"] + if max_id is None: + # Do nothing if there are no user groups on the server. + return + + lower_bound = NamedUserGroup.objects.filter(can_leave_group=None).aggregate(Min("id"))[ + "id__min" + ] + while lower_bound <= max_id + BATCH_SIZE / 2: + upper_bound = lower_bound + BATCH_SIZE - 1 + print(f"Processing batch {lower_bound} to {upper_bound} for NamedUserGroup") + + with transaction.atomic(): + # Everyone should be able to leave the group they have + # joined by default. + NamedUserGroup.objects.filter( + id__range=(lower_bound, upper_bound), + can_leave_group=None, + ).update( + can_leave_group=NamedUserGroup.objects.filter( + name="role:everyone", + realm_for_sharding=OuterRef("realm_for_sharding"), + is_system_group=True, + ).values("pk") + ) + + lower_bound += BATCH_SIZE + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ("zerver", "0607_namedusergroup_add_can_leave_group"), + ] + + operations = [ + migrations.RunPython( + set_default_value_for_can_leave_group, + elidable=True, + reverse_code=migrations.RunPython.noop, + ) + ] diff --git a/zerver/migrations/0609_alter_namedusergroup_can_leave_group.py b/zerver/migrations/0609_alter_namedusergroup_can_leave_group.py new file mode 100644 index 0000000000..fd4708d986 --- /dev/null +++ b/zerver/migrations/0609_alter_namedusergroup_can_leave_group.py @@ -0,0 +1,22 @@ +# Generated by Django 5.0.9 on 2024-10-14 04:57 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("zerver", "0608_set_default_for_can_leave_group"), + ] + + operations = [ + migrations.AlterField( + model_name="namedusergroup", + name="can_leave_group", + field=models.ForeignKey( + on_delete=django.db.models.deletion.RESTRICT, + related_name="+", + to="zerver.usergroup", + ), + ), + ] diff --git a/zerver/models/groups.py b/zerver/models/groups.py index 80373ba09f..69bef7955f 100644 --- a/zerver/models/groups.py +++ b/zerver/models/groups.py @@ -63,6 +63,7 @@ class NamedUserGroup(UserGroup): # type: ignore[django-manager-missing] # djang UserGroup, on_delete=models.RESTRICT, related_name="+" ) can_join_group = models.ForeignKey(UserGroup, on_delete=models.RESTRICT, related_name="+") + can_leave_group = models.ForeignKey(UserGroup, on_delete=models.RESTRICT, related_name="+") can_manage_group = models.ForeignKey(UserGroup, on_delete=models.RESTRICT, related_name="+") can_mention_group = models.ForeignKey( UserGroup, on_delete=models.RESTRICT, db_column="can_mention_group_id" @@ -118,6 +119,16 @@ class NamedUserGroup(UserGroup): # type: ignore[django-manager-missing] # djang default_for_system_groups=SystemGroups.NOBODY, id_field_name="can_join_group_id", ), + "can_leave_group": GroupPermissionSetting( + require_system_group=False, + allow_internet_group=False, + allow_owners_group=True, + allow_nobody_group=True, + allow_everyone_group=True, + default_group_name=SystemGroups.EVERYONE, + default_for_system_groups=SystemGroups.NOBODY, + id_field_name="can_leave_group_id", + ), "can_manage_group": GroupPermissionSetting( require_system_group=False, allow_internet_group=False, diff --git a/zerver/openapi/curl_param_value_generators.py b/zerver/openapi/curl_param_value_generators.py index 52eca0c01b..7d0ecc416d 100644 --- a/zerver/openapi/curl_param_value_generators.py +++ b/zerver/openapi/curl_param_value_generators.py @@ -266,6 +266,7 @@ def get_temp_user_group_id() -> dict[str, object]: realm=get_realm("zulip"), can_add_members_group_id=11, can_join_group_id=11, + can_leave_group_id=15, can_manage_group_id=11, can_mention_group_id=11, realm_for_sharding=get_realm("zulip"), @@ -277,11 +278,13 @@ def get_temp_user_group_id() -> dict[str, object]: @openapi_param_value_generator(["/user_groups/{user_group_id}/deactivate:post"]) def get_temp_user_group_id_for_deactivation() -> dict[str, object]: + print(NamedUserGroup.objects.all()) user_group, _ = NamedUserGroup.objects.get_or_create( name="temp-deactivation", realm=get_realm("zulip"), can_add_members_group_id=11, can_join_group_id=11, + can_leave_group_id=15, can_manage_group_id=11, can_mention_group_id=11, realm_for_sharding=get_realm("zulip"), diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index bf2f3522cd..b6d71af67a 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -3177,6 +3177,7 @@ paths: "is_system_group": false, "can_add_members_group": 16, "can_join_group": 16, + "can_leave_group": 15, "can_manage_group": 16, "can_mention_group": 11, }, @@ -3259,6 +3260,19 @@ paths: Will be one of the following: + [setting-values]: /api/group-setting-values + can_leave_group: + allOf: + - $ref: "#/components/schemas/GroupSettingValue" + - description: | + A [group-setting value][setting-values] defining the set of users who + have permission to leave this group. Only present if this user group + permission setting changed. + + **Changes**: New in Zulip 10.0 (feature level 308). + + Will be one of the following: + [setting-values]: /api/group-setting-values can_manage_group: allOf: @@ -20303,6 +20317,18 @@ paths: [system-groups]: /api/group-setting-values#system-groups - $ref: "#/components/schemas/GroupSettingValue" example: 11 + can_leave_group: + allOf: + - description: | + A [group-setting value][setting-values] defining the set of users who + have permission to leave this user group. + + **Changes**: New in Zulip 10.0 (feature level 308). + + [setting-values]: /api/group-setting-values + [system-groups]: /api/group-setting-values#system-groups + - $ref: "#/components/schemas/GroupSettingValue" + example: 15 can_manage_group: allOf: - description: | @@ -20353,6 +20379,8 @@ paths: contentType: application/json can_join_group: contentType: application/json + can_leave_group: + contentType: application/json can_manage_group: contentType: application/json can_mention_group: @@ -20550,6 +20578,37 @@ paths: "new": {"direct_members": [10], "direct_subgroups": [11]}, "old": 11, } + can_leave_group: + description: | + The set of users who have permission to leave this user group + expressed as an [update to a group-setting value][update-group-setting]. + + **Changes**: New in Zulip 10.0 (feature level 308). + + [update-group-setting]: /api/group-setting-values#updating-group-setting-values + [system-groups]: /api/group-setting-values#system-groups + type: object + additionalProperties: false + properties: + new: + allOf: + - description: | + The new [group-setting value](/api/group-setting-values) for who would + have the permission to leave the group. + - $ref: "#/components/schemas/GroupSettingValue" + old: + allOf: + - description: | + The expected current [group-setting value](/api/group-setting-values) + for who has the permission to leave the group. + - $ref: "#/components/schemas/GroupSettingValue" + required: + - new + example: + { + "new": {"direct_members": [10], "direct_subgroups": [11]}, + "old": 15, + } can_manage_group: description: | The set of users who have permission to [manage this user group][manage-user-groups] @@ -20638,6 +20697,8 @@ paths: contentType: application/json can_join_group: contentType: application/json + can_leave_group: + contentType: application/json can_manage_group: contentType: application/json can_mention_group: @@ -20809,6 +20870,18 @@ paths: Will be one of the following: + [setting-values]: /api/group-setting-values + can_leave_group: + allOf: + - $ref: "#/components/schemas/GroupSettingValue" + - description: | + A [group-setting value][setting-values] defining the set of users who + have permission to leave this user group. + + **Changes**: New in Zulip 10.0 (feature level 308). + + Will be one of the following: + [setting-values]: /api/group-setting-values can_manage_group: allOf: @@ -20871,6 +20944,7 @@ paths: "is_system_group": true, "can_add_members_group": 16, "can_join_group": 16, + "can_leave_group": 15, "can_manage_group": 16, "can_mention_group": 11, }, @@ -20885,6 +20959,7 @@ paths: "is_system_group": true, "can_add_members_group": 17, "can_join_group": 17, + "can_leave_group": 15, "can_manage_group": 17, "can_mention_group": 12, }, @@ -20899,6 +20974,7 @@ paths: "is_system_group": false, "can_add_members_group": 20, "can_join_group": 20, + "can_leave_group": 15, "can_manage_group": 20, "can_mention_group": 13, }, @@ -22188,6 +22264,18 @@ components: Will be one of the following: + [setting-values]: /api/group-setting-values + can_leave_group: + allOf: + - $ref: "#/components/schemas/GroupSettingValue" + - description: | + A [group-setting value][setting-values] defining the set of users who + have permission to leave this user group. + + **Changes**: New in Zulip 10.0 (feature level 308). + + Will be one of the following: + [setting-values]: /api/group-setting-values can_manage_group: allOf: diff --git a/zerver/views/user_groups.py b/zerver/views/user_groups.py index ec7bb0f89a..d351163c9b 100644 --- a/zerver/views/user_groups.py +++ b/zerver/views/user_groups.py @@ -59,6 +59,7 @@ def add_user_group( description: str, can_add_members_group: Json[int | AnonymousSettingGroupDict] | None = None, can_join_group: Json[int | AnonymousSettingGroupDict] | None = None, + can_leave_group: Json[int | AnonymousSettingGroupDict] | None = None, can_manage_group: Json[int | AnonymousSettingGroupDict] | None = None, can_mention_group: Json[int | AnonymousSettingGroupDict] | None = None, ) -> HttpResponse: @@ -120,6 +121,7 @@ def edit_user_group( description: str | None = None, can_add_members_group: Json[GroupSettingChangeRequest] | None = None, can_join_group: Json[GroupSettingChangeRequest] | None = None, + can_leave_group: Json[GroupSettingChangeRequest] | None = None, can_manage_group: Json[GroupSettingChangeRequest] | None = None, can_mention_group: Json[GroupSettingChangeRequest] | None = None, ) -> HttpResponse: @@ -128,6 +130,7 @@ def edit_user_group( and description is None and can_add_members_group is None and can_join_group is None + and can_leave_group is None and can_manage_group is None and can_mention_group is None ): @@ -141,6 +144,7 @@ def edit_user_group( description is not None or can_add_members_group is not None or can_join_group is not None + or can_leave_group is not None or can_mention_group is not None or can_manage_group is not None ):