user_groups: Add can_manage_group field in database.

This commit adds a new group level setting can_manage_group
for configuring who can manage a group. This commit only adds
the field in database and make changes to automatically create
single user groups corresponsing to acting user
which will be the default value for this setting.

Fixes part of #25928.
This commit is contained in:
Ujjawal Modi 2023-08-21 15:36:41 +05:30 committed by Tim Abbott
parent 9e699dfc85
commit 03220ba456
8 changed files with 125 additions and 5 deletions

View File

@ -754,6 +754,7 @@ def bulk_import_named_user_groups(data: TableData) -> None:
group["name"], group["name"],
group["description"], group["description"],
group["is_system_group"], group["is_system_group"],
group["can_manage_group_id"],
group["can_mention_group_id"], group["can_mention_group_id"],
) )
for group in data["zerver_namedusergroup"] for group in data["zerver_namedusergroup"]
@ -761,7 +762,7 @@ def bulk_import_named_user_groups(data: TableData) -> None:
query = SQL( query = SQL(
""" """
INSERT INTO zerver_namedusergroup (usergroup_ptr_id, realm_id, name, description, is_system_group, can_mention_group_id) INSERT INTO zerver_namedusergroup (usergroup_ptr_id, realm_id, name, description, is_system_group, can_manage_group_id, can_mention_group_id)
VALUES %s VALUES %s
""" """
) )

View File

@ -598,19 +598,20 @@ def bulk_create_system_user_groups(groups: list[dict[str, str]], realm: Realm) -
user_group_ids = [id for (id,) in cursor.fetchall()] user_group_ids = [id for (id,) in cursor.fetchall()]
rows = [ rows = [
SQL("({},{},{},{},{},{})").format( SQL("({},{},{},{},{},{},{})").format(
Literal(user_group_ids[idx]), Literal(user_group_ids[idx]),
Literal(realm.id), Literal(realm.id),
Literal(group["name"]), Literal(group["name"]),
Literal(group["description"]), Literal(group["description"]),
Literal(True), Literal(True),
Literal(initial_group_setting_value), Literal(initial_group_setting_value),
Literal(initial_group_setting_value),
) )
for idx, group in enumerate(groups) for idx, group in enumerate(groups)
] ]
query = SQL( query = SQL(
""" """
INSERT INTO zerver_namedusergroup (usergroup_ptr_id, realm_id, name, description, is_system_group, can_mention_group_id) INSERT INTO zerver_namedusergroup (usergroup_ptr_id, realm_id, name, description, is_system_group, can_manage_group_id, can_mention_group_id)
VALUES {rows} VALUES {rows}
""" """
).format(rows=SQL(", ").join(rows)) ).format(rows=SQL(", ").join(rows))
@ -691,7 +692,9 @@ def create_system_user_groups_for_realm(realm: Realm) -> dict[int, NamedUserGrou
for group in system_user_groups_list: for group in system_user_groups_list:
user_group = set_defaults_for_group_settings(group, {}, system_groups_name_dict) user_group = set_defaults_for_group_settings(group, {}, system_groups_name_dict)
groups_with_updated_settings.append(user_group) groups_with_updated_settings.append(user_group)
NamedUserGroup.objects.bulk_update(groups_with_updated_settings, ["can_mention_group"]) NamedUserGroup.objects.bulk_update(
groups_with_updated_settings, ["can_manage_group", "can_mention_group"]
)
subgroup_objects: list[GroupGroupMembership] = [] subgroup_objects: list[GroupGroupMembership] = []
# "Nobody" system group is not a subgroup of any user group, since it is already empty. # "Nobody" system group is not a subgroup of any user group, since it is already empty.

View File

@ -0,0 +1,23 @@
# Generated by Django 4.2.2 on 2023-07-15 16:28
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("zerver", "0569_remove_userprofile_tutorial_status"),
]
operations = [
migrations.AddField(
model_name="namedusergroup",
name="can_manage_group",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.RESTRICT,
related_name="+",
to="zerver.usergroup",
),
),
]

View File

@ -0,0 +1,55 @@
# Generated by Django 4.2.2 on 2023-07-15 17: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_manage_group(
apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
) -> None:
NamedUserGroup = apps.get_model("zerver", "NamedUserGroup")
BATCH_SIZE = 1000
max_id = NamedUserGroup.objects.filter(can_manage_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_manage_group=None).aggregate(Min("id"))[
"id__min"
]
while lower_bound <= max_id:
upper_bound = lower_bound + BATCH_SIZE - 1
print(f"Processing batch {lower_bound} to {upper_bound} for NamedUserGroup")
with transaction.atomic():
NamedUserGroup.objects.filter(
id__range=(lower_bound, upper_bound), can_manage_group=None
).update(
can_manage_group=NamedUserGroup.objects.filter(
name="role:nobody",
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", "0570_namedusergroup_can_manage_group"),
]
operations = [
migrations.RunPython(
set_default_value_for_can_manage_group,
elidable=True,
reverse_code=migrations.RunPython.noop,
)
]

View File

@ -0,0 +1,22 @@
# Generated by Django 4.2.2 on 2023-07-16 12:57
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("zerver", "0571_set_default_for_can_manage_group"),
]
operations = [
migrations.AlterField(
model_name="namedusergroup",
name="can_manage_group",
field=models.ForeignKey(
on_delete=django.db.models.deletion.RESTRICT,
related_name="+",
to="zerver.usergroup",
),
),
]

View File

@ -54,6 +54,7 @@ class NamedUserGroup(UserGroup): # type: ignore[django-manager-missing] # djang
description = models.TextField(default="", db_column="description") description = models.TextField(default="", db_column="description")
is_system_group = models.BooleanField(default=False, db_column="is_system_group") is_system_group = models.BooleanField(default=False, db_column="is_system_group")
can_manage_group = models.ForeignKey(UserGroup, on_delete=models.RESTRICT, related_name="+")
can_mention_group = models.ForeignKey( can_mention_group = models.ForeignKey(
UserGroup, on_delete=models.RESTRICT, db_column="can_mention_group_id" UserGroup, on_delete=models.RESTRICT, db_column="can_mention_group_id"
) )
@ -87,6 +88,16 @@ class NamedUserGroup(UserGroup): # type: ignore[django-manager-missing] # djang
} }
GROUP_PERMISSION_SETTINGS = { GROUP_PERMISSION_SETTINGS = {
"can_manage_group": GroupPermissionSetting(
require_system_group=False,
allow_internet_group=False,
allow_owners_group=True,
allow_nobody_group=True,
allow_everyone_group=False,
default_group_name=SystemGroups.NOBODY,
default_for_system_groups=SystemGroups.NOBODY,
id_field_name="can_manage_group_id",
),
"can_mention_group": GroupPermissionSetting( "can_mention_group": GroupPermissionSetting(
require_system_group=False, require_system_group=False,
allow_internet_group=False, allow_internet_group=False,

View File

@ -266,6 +266,7 @@ def get_temp_user_group_id() -> dict[str, object]:
user_group, _ = NamedUserGroup.objects.get_or_create( user_group, _ = NamedUserGroup.objects.get_or_create(
name="temp", name="temp",
realm=get_realm("zulip"), realm=get_realm("zulip"),
can_manage_group_id=11,
can_mention_group_id=11, can_mention_group_id=11,
realm_for_sharding=get_realm("zulip"), realm_for_sharding=get_realm("zulip"),
) )

View File

@ -343,11 +343,15 @@ class UserGroupAPITestCase(UserGroupTestCase):
self.assert_json_success(result) self.assert_json_success(result)
self.assert_length(NamedUserGroup.objects.filter(realm=hamlet.realm), 10) self.assert_length(NamedUserGroup.objects.filter(realm=hamlet.realm), 10)
# Check default value of can_mention_group setting. # Check default value of settings.
everyone_system_group = NamedUserGroup.objects.get( everyone_system_group = NamedUserGroup.objects.get(
name="role:everyone", realm=hamlet.realm, is_system_group=True name="role:everyone", realm=hamlet.realm, is_system_group=True
) )
nobody_system_group = NamedUserGroup.objects.get(
name=SystemGroups.NOBODY, realm=hamlet.realm, is_system_group=True
)
support_group = NamedUserGroup.objects.get(name="support", realm=hamlet.realm) support_group = NamedUserGroup.objects.get(name="support", realm=hamlet.realm)
self.assertEqual(support_group.can_manage_group, nobody_system_group.usergroup_ptr)
self.assertEqual(support_group.can_mention_group, everyone_system_group.usergroup_ptr) self.assertEqual(support_group.can_mention_group, everyone_system_group.usergroup_ptr)
# Test invalid member error # Test invalid member error