mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 05:53:43 +00:00 
			
		
		
		
	Earlier, we were using 'send_event' in 'edit_user_group' codepath which can lead to a situation where we enqueue events but the function fails at a later stage. Events should not be sent until we know we're not rolling back. Fixes part of #30489.
		
			
				
	
	
		
			483 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			483 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
from collections.abc import Mapping, Sequence
 | 
						|
from datetime import datetime
 | 
						|
from typing import TypedDict
 | 
						|
 | 
						|
import django.db.utils
 | 
						|
from django.db import transaction
 | 
						|
from django.utils.timezone import now as timezone_now
 | 
						|
from django.utils.translation import gettext as _
 | 
						|
 | 
						|
from zerver.lib.exceptions import JsonableError
 | 
						|
from zerver.lib.user_groups import (
 | 
						|
    AnonymousSettingGroupDict,
 | 
						|
    get_group_setting_value_for_api,
 | 
						|
    get_group_setting_value_for_audit_log_data,
 | 
						|
    get_role_based_system_groups_dict,
 | 
						|
    set_defaults_for_group_settings,
 | 
						|
)
 | 
						|
from zerver.models import (
 | 
						|
    GroupGroupMembership,
 | 
						|
    NamedUserGroup,
 | 
						|
    Realm,
 | 
						|
    RealmAuditLog,
 | 
						|
    UserGroup,
 | 
						|
    UserGroupMembership,
 | 
						|
    UserProfile,
 | 
						|
)
 | 
						|
from zerver.models.groups import SystemGroups
 | 
						|
from zerver.models.users import active_user_ids
 | 
						|
from zerver.tornado.django_api import send_event, send_event_on_commit
 | 
						|
 | 
						|
 | 
						|
class MemberGroupUserDict(TypedDict):
 | 
						|
    id: int
 | 
						|
    role: int
 | 
						|
    date_joined: datetime
 | 
						|
 | 
						|
 | 
						|
@transaction.atomic
 | 
						|
def create_user_group_in_database(
 | 
						|
    name: str,
 | 
						|
    members: list[UserProfile],
 | 
						|
    realm: Realm,
 | 
						|
    *,
 | 
						|
    acting_user: UserProfile | None,
 | 
						|
    description: str = "",
 | 
						|
    group_settings_map: Mapping[str, UserGroup] = {},
 | 
						|
    is_system_group: bool = False,
 | 
						|
) -> NamedUserGroup:
 | 
						|
    user_group = NamedUserGroup(
 | 
						|
        name=name,
 | 
						|
        realm=realm,
 | 
						|
        description=description,
 | 
						|
        is_system_group=is_system_group,
 | 
						|
        realm_for_sharding=realm,
 | 
						|
    )
 | 
						|
 | 
						|
    for setting_name, setting_value in group_settings_map.items():
 | 
						|
        setattr(user_group, setting_name, setting_value)
 | 
						|
 | 
						|
    system_groups_name_dict = get_role_based_system_groups_dict(realm)
 | 
						|
    user_group = set_defaults_for_group_settings(
 | 
						|
        user_group, group_settings_map, system_groups_name_dict
 | 
						|
    )
 | 
						|
    user_group.save()
 | 
						|
 | 
						|
    UserGroupMembership.objects.bulk_create(
 | 
						|
        UserGroupMembership(user_profile=member, user_group=user_group) for member in members
 | 
						|
    )
 | 
						|
 | 
						|
    creation_time = timezone_now()
 | 
						|
    audit_log_entries = [
 | 
						|
        RealmAuditLog(
 | 
						|
            realm=realm,
 | 
						|
            acting_user=acting_user,
 | 
						|
            event_type=RealmAuditLog.USER_GROUP_CREATED,
 | 
						|
            event_time=creation_time,
 | 
						|
            modified_user_group=user_group,
 | 
						|
        ),
 | 
						|
    ] + [
 | 
						|
        RealmAuditLog(
 | 
						|
            realm=realm,
 | 
						|
            acting_user=acting_user,
 | 
						|
            event_type=RealmAuditLog.USER_GROUP_DIRECT_USER_MEMBERSHIP_ADDED,
 | 
						|
            event_time=creation_time,
 | 
						|
            modified_user=member,
 | 
						|
            modified_user_group=user_group,
 | 
						|
        )
 | 
						|
        for member in members
 | 
						|
    ]
 | 
						|
    RealmAuditLog.objects.bulk_create(audit_log_entries)
 | 
						|
    return user_group
 | 
						|
 | 
						|
 | 
						|
@transaction.atomic(savepoint=False)
 | 
						|
def update_users_in_full_members_system_group(
 | 
						|
    realm: Realm, affected_user_ids: Sequence[int] = [], *, acting_user: UserProfile | None
 | 
						|
) -> None:
 | 
						|
    full_members_system_group = NamedUserGroup.objects.get(
 | 
						|
        realm=realm, name=SystemGroups.FULL_MEMBERS, is_system_group=True
 | 
						|
    )
 | 
						|
    members_system_group = NamedUserGroup.objects.get(
 | 
						|
        realm=realm, name=SystemGroups.MEMBERS, is_system_group=True
 | 
						|
    )
 | 
						|
 | 
						|
    full_member_group_users: list[MemberGroupUserDict] = list()
 | 
						|
    member_group_users: list[MemberGroupUserDict] = list()
 | 
						|
 | 
						|
    if affected_user_ids:
 | 
						|
        full_member_group_users = list(
 | 
						|
            full_members_system_group.direct_members.filter(id__in=affected_user_ids).values(
 | 
						|
                "id", "role", "date_joined"
 | 
						|
            )
 | 
						|
        )
 | 
						|
        member_group_users = list(
 | 
						|
            members_system_group.direct_members.filter(id__in=affected_user_ids).values(
 | 
						|
                "id", "role", "date_joined"
 | 
						|
            )
 | 
						|
        )
 | 
						|
    else:
 | 
						|
        full_member_group_users = list(
 | 
						|
            full_members_system_group.direct_members.all().values("id", "role", "date_joined")
 | 
						|
        )
 | 
						|
        member_group_users = list(
 | 
						|
            members_system_group.direct_members.all().values("id", "role", "date_joined")
 | 
						|
        )
 | 
						|
 | 
						|
    def is_provisional_member(user: MemberGroupUserDict) -> bool:
 | 
						|
        diff = (timezone_now() - user["date_joined"]).days
 | 
						|
        if diff < realm.waiting_period_threshold:
 | 
						|
            return True
 | 
						|
        return False
 | 
						|
 | 
						|
    old_full_members = [
 | 
						|
        user
 | 
						|
        for user in full_member_group_users
 | 
						|
        if is_provisional_member(user) or user["role"] != UserProfile.ROLE_MEMBER
 | 
						|
    ]
 | 
						|
 | 
						|
    full_member_group_user_ids = [user["id"] for user in full_member_group_users]
 | 
						|
    members_excluding_full_members = [
 | 
						|
        user for user in member_group_users if user["id"] not in full_member_group_user_ids
 | 
						|
    ]
 | 
						|
 | 
						|
    new_full_members = [
 | 
						|
        user for user in members_excluding_full_members if not is_provisional_member(user)
 | 
						|
    ]
 | 
						|
 | 
						|
    old_full_member_ids = [user["id"] for user in old_full_members]
 | 
						|
    new_full_member_ids = [user["id"] for user in new_full_members]
 | 
						|
 | 
						|
    if len(old_full_members) > 0:
 | 
						|
        bulk_remove_members_from_user_groups(
 | 
						|
            [full_members_system_group], old_full_member_ids, acting_user=acting_user
 | 
						|
        )
 | 
						|
 | 
						|
    if len(new_full_members) > 0:
 | 
						|
        bulk_add_members_to_user_groups(
 | 
						|
            [full_members_system_group], new_full_member_ids, acting_user=acting_user
 | 
						|
        )
 | 
						|
 | 
						|
 | 
						|
def promote_new_full_members() -> None:
 | 
						|
    for realm in Realm.objects.filter(deactivated=False).exclude(waiting_period_threshold=0):
 | 
						|
        update_users_in_full_members_system_group(realm, acting_user=None)
 | 
						|
 | 
						|
 | 
						|
def do_send_create_user_group_event(
 | 
						|
    user_group: NamedUserGroup,
 | 
						|
    members: list[UserProfile],
 | 
						|
    direct_subgroups: Sequence[UserGroup] = [],
 | 
						|
) -> None:
 | 
						|
    event = dict(
 | 
						|
        type="user_group",
 | 
						|
        op="add",
 | 
						|
        group=dict(
 | 
						|
            name=user_group.name,
 | 
						|
            members=[member.id for member in members],
 | 
						|
            description=user_group.description,
 | 
						|
            id=user_group.id,
 | 
						|
            is_system_group=user_group.is_system_group,
 | 
						|
            direct_subgroup_ids=[direct_subgroup.id for direct_subgroup in direct_subgroups],
 | 
						|
            can_mention_group=get_group_setting_value_for_api(user_group.can_mention_group),
 | 
						|
        ),
 | 
						|
    )
 | 
						|
    send_event(user_group.realm, event, active_user_ids(user_group.realm_id))
 | 
						|
 | 
						|
 | 
						|
def check_add_user_group(
 | 
						|
    realm: Realm,
 | 
						|
    name: str,
 | 
						|
    initial_members: list[UserProfile],
 | 
						|
    description: str = "",
 | 
						|
    group_settings_map: Mapping[str, UserGroup] = {},
 | 
						|
    *,
 | 
						|
    acting_user: UserProfile | None,
 | 
						|
) -> NamedUserGroup:
 | 
						|
    try:
 | 
						|
        user_group = create_user_group_in_database(
 | 
						|
            name,
 | 
						|
            initial_members,
 | 
						|
            realm,
 | 
						|
            description=description,
 | 
						|
            group_settings_map=group_settings_map,
 | 
						|
            acting_user=acting_user,
 | 
						|
        )
 | 
						|
        do_send_create_user_group_event(user_group, initial_members)
 | 
						|
        return user_group
 | 
						|
    except django.db.utils.IntegrityError:
 | 
						|
        raise JsonableError(_("User group '{group_name}' already exists.").format(group_name=name))
 | 
						|
 | 
						|
 | 
						|
def do_send_user_group_update_event(
 | 
						|
    user_group: NamedUserGroup, data: dict[str, str | int | AnonymousSettingGroupDict]
 | 
						|
) -> None:
 | 
						|
    event = dict(type="user_group", op="update", group_id=user_group.id, data=data)
 | 
						|
    send_event_on_commit(user_group.realm, event, active_user_ids(user_group.realm_id))
 | 
						|
 | 
						|
 | 
						|
@transaction.atomic(savepoint=False)
 | 
						|
def do_update_user_group_name(
 | 
						|
    user_group: NamedUserGroup, name: str, *, acting_user: UserProfile | None
 | 
						|
) -> None:
 | 
						|
    try:
 | 
						|
        old_value = user_group.name
 | 
						|
        user_group.name = name
 | 
						|
        user_group.save(update_fields=["name"])
 | 
						|
        RealmAuditLog.objects.create(
 | 
						|
            realm=user_group.realm,
 | 
						|
            modified_user_group=user_group,
 | 
						|
            event_type=RealmAuditLog.USER_GROUP_NAME_CHANGED,
 | 
						|
            event_time=timezone_now(),
 | 
						|
            acting_user=acting_user,
 | 
						|
            extra_data={
 | 
						|
                RealmAuditLog.OLD_VALUE: old_value,
 | 
						|
                RealmAuditLog.NEW_VALUE: name,
 | 
						|
            },
 | 
						|
        )
 | 
						|
    except django.db.utils.IntegrityError:
 | 
						|
        raise JsonableError(_("User group '{group_name}' already exists.").format(group_name=name))
 | 
						|
    do_send_user_group_update_event(user_group, dict(name=name))
 | 
						|
 | 
						|
 | 
						|
@transaction.atomic(savepoint=False)
 | 
						|
def do_update_user_group_description(
 | 
						|
    user_group: NamedUserGroup, description: str, *, acting_user: UserProfile | None
 | 
						|
) -> None:
 | 
						|
    old_value = user_group.description
 | 
						|
    user_group.description = description
 | 
						|
    user_group.save(update_fields=["description"])
 | 
						|
    RealmAuditLog.objects.create(
 | 
						|
        realm=user_group.realm,
 | 
						|
        modified_user_group=user_group,
 | 
						|
        event_type=RealmAuditLog.USER_GROUP_DESCRIPTION_CHANGED,
 | 
						|
        event_time=timezone_now(),
 | 
						|
        acting_user=acting_user,
 | 
						|
        extra_data={
 | 
						|
            RealmAuditLog.OLD_VALUE: old_value,
 | 
						|
            RealmAuditLog.NEW_VALUE: description,
 | 
						|
        },
 | 
						|
    )
 | 
						|
    do_send_user_group_update_event(user_group, dict(description=description))
 | 
						|
 | 
						|
 | 
						|
def do_send_user_group_members_update_event(
 | 
						|
    event_name: str, user_group: NamedUserGroup, user_ids: list[int]
 | 
						|
) -> None:
 | 
						|
    event = dict(type="user_group", op=event_name, group_id=user_group.id, user_ids=user_ids)
 | 
						|
    send_event_on_commit(user_group.realm, event, active_user_ids(user_group.realm_id))
 | 
						|
 | 
						|
 | 
						|
@transaction.atomic(savepoint=False)
 | 
						|
def bulk_add_members_to_user_groups(
 | 
						|
    user_groups: list[NamedUserGroup],
 | 
						|
    user_profile_ids: list[int],
 | 
						|
    *,
 | 
						|
    acting_user: UserProfile | None,
 | 
						|
) -> None:
 | 
						|
    # All intended callers of this function involve a single user
 | 
						|
    # being added to one or more groups, or many users being added to
 | 
						|
    # a single group; but it's easy enough for the implementation to
 | 
						|
    # support both.
 | 
						|
 | 
						|
    memberships = [
 | 
						|
        UserGroupMembership(user_group_id=user_group.id, user_profile_id=user_id)
 | 
						|
        for user_id in user_profile_ids
 | 
						|
        for user_group in user_groups
 | 
						|
    ]
 | 
						|
    UserGroupMembership.objects.bulk_create(memberships)
 | 
						|
    now = timezone_now()
 | 
						|
    RealmAuditLog.objects.bulk_create(
 | 
						|
        RealmAuditLog(
 | 
						|
            realm=user_group.realm,
 | 
						|
            modified_user_id=user_id,
 | 
						|
            modified_user_group=user_group,
 | 
						|
            event_type=RealmAuditLog.USER_GROUP_DIRECT_USER_MEMBERSHIP_ADDED,
 | 
						|
            event_time=now,
 | 
						|
            acting_user=acting_user,
 | 
						|
        )
 | 
						|
        for user_id in user_profile_ids
 | 
						|
        for user_group in user_groups
 | 
						|
    )
 | 
						|
 | 
						|
    for user_group in user_groups:
 | 
						|
        do_send_user_group_members_update_event("add_members", user_group, user_profile_ids)
 | 
						|
 | 
						|
 | 
						|
@transaction.atomic(savepoint=False)
 | 
						|
def bulk_remove_members_from_user_groups(
 | 
						|
    user_groups: list[NamedUserGroup],
 | 
						|
    user_profile_ids: list[int],
 | 
						|
    *,
 | 
						|
    acting_user: UserProfile | None,
 | 
						|
) -> None:
 | 
						|
    # All intended callers of this function involve a single user
 | 
						|
    # being added to one or more groups, or many users being added to
 | 
						|
    # a single group; but it's easy enough for the implementation to
 | 
						|
    # support both.
 | 
						|
 | 
						|
    UserGroupMembership.objects.filter(
 | 
						|
        user_group__in=user_groups, user_profile_id__in=user_profile_ids
 | 
						|
    ).delete()
 | 
						|
    now = timezone_now()
 | 
						|
    RealmAuditLog.objects.bulk_create(
 | 
						|
        RealmAuditLog(
 | 
						|
            realm=user_group.realm,
 | 
						|
            modified_user_id=user_id,
 | 
						|
            modified_user_group=user_group,
 | 
						|
            event_type=RealmAuditLog.USER_GROUP_DIRECT_USER_MEMBERSHIP_REMOVED,
 | 
						|
            event_time=now,
 | 
						|
            acting_user=acting_user,
 | 
						|
        )
 | 
						|
        for user_id in user_profile_ids
 | 
						|
        for user_group in user_groups
 | 
						|
    )
 | 
						|
 | 
						|
    for user_group in user_groups:
 | 
						|
        do_send_user_group_members_update_event("remove_members", user_group, user_profile_ids)
 | 
						|
 | 
						|
 | 
						|
def do_send_subgroups_update_event(
 | 
						|
    event_name: str, user_group: NamedUserGroup, subgroup_ids: list[int]
 | 
						|
) -> None:
 | 
						|
    event = dict(
 | 
						|
        type="user_group", op=event_name, group_id=user_group.id, direct_subgroup_ids=subgroup_ids
 | 
						|
    )
 | 
						|
    send_event_on_commit(user_group.realm, event, active_user_ids(user_group.realm_id))
 | 
						|
 | 
						|
 | 
						|
@transaction.atomic
 | 
						|
def add_subgroups_to_user_group(
 | 
						|
    user_group: NamedUserGroup,
 | 
						|
    subgroups: list[NamedUserGroup],
 | 
						|
    *,
 | 
						|
    acting_user: UserProfile | None,
 | 
						|
) -> None:
 | 
						|
    group_memberships = [
 | 
						|
        GroupGroupMembership(supergroup=user_group, subgroup=subgroup) for subgroup in subgroups
 | 
						|
    ]
 | 
						|
    GroupGroupMembership.objects.bulk_create(group_memberships)
 | 
						|
 | 
						|
    subgroup_ids = [subgroup.id for subgroup in subgroups]
 | 
						|
    now = timezone_now()
 | 
						|
    audit_log_entries = [
 | 
						|
        RealmAuditLog(
 | 
						|
            realm=user_group.realm,
 | 
						|
            modified_user_group=user_group,
 | 
						|
            event_type=RealmAuditLog.USER_GROUP_DIRECT_SUBGROUP_MEMBERSHIP_ADDED,
 | 
						|
            event_time=now,
 | 
						|
            acting_user=acting_user,
 | 
						|
            extra_data={"subgroup_ids": subgroup_ids},
 | 
						|
        ),
 | 
						|
        *(
 | 
						|
            RealmAuditLog(
 | 
						|
                realm=user_group.realm,
 | 
						|
                modified_user_group_id=subgroup_id,
 | 
						|
                event_type=RealmAuditLog.USER_GROUP_DIRECT_SUPERGROUP_MEMBERSHIP_ADDED,
 | 
						|
                event_time=now,
 | 
						|
                acting_user=acting_user,
 | 
						|
                extra_data={"supergroup_ids": [user_group.id]},
 | 
						|
            )
 | 
						|
            for subgroup_id in subgroup_ids
 | 
						|
        ),
 | 
						|
    ]
 | 
						|
    RealmAuditLog.objects.bulk_create(audit_log_entries)
 | 
						|
 | 
						|
    do_send_subgroups_update_event("add_subgroups", user_group, subgroup_ids)
 | 
						|
 | 
						|
 | 
						|
@transaction.atomic
 | 
						|
def remove_subgroups_from_user_group(
 | 
						|
    user_group: NamedUserGroup,
 | 
						|
    subgroups: list[NamedUserGroup],
 | 
						|
    *,
 | 
						|
    acting_user: UserProfile | None,
 | 
						|
) -> None:
 | 
						|
    GroupGroupMembership.objects.filter(supergroup=user_group, subgroup__in=subgroups).delete()
 | 
						|
 | 
						|
    subgroup_ids = [subgroup.id for subgroup in subgroups]
 | 
						|
    now = timezone_now()
 | 
						|
    audit_log_entries = [
 | 
						|
        RealmAuditLog(
 | 
						|
            realm=user_group.realm,
 | 
						|
            modified_user_group=user_group,
 | 
						|
            event_type=RealmAuditLog.USER_GROUP_DIRECT_SUBGROUP_MEMBERSHIP_REMOVED,
 | 
						|
            event_time=now,
 | 
						|
            acting_user=acting_user,
 | 
						|
            extra_data={"subgroup_ids": subgroup_ids},
 | 
						|
        ),
 | 
						|
        *(
 | 
						|
            RealmAuditLog(
 | 
						|
                realm=user_group.realm,
 | 
						|
                modified_user_group_id=subgroup_id,
 | 
						|
                event_type=RealmAuditLog.USER_GROUP_DIRECT_SUPERGROUP_MEMBERSHIP_REMOVED,
 | 
						|
                event_time=now,
 | 
						|
                acting_user=acting_user,
 | 
						|
                extra_data={"supergroup_ids": [user_group.id]},
 | 
						|
            )
 | 
						|
            for subgroup_id in subgroup_ids
 | 
						|
        ),
 | 
						|
    ]
 | 
						|
    RealmAuditLog.objects.bulk_create(audit_log_entries)
 | 
						|
 | 
						|
    do_send_subgroups_update_event("remove_subgroups", user_group, subgroup_ids)
 | 
						|
 | 
						|
 | 
						|
def do_send_delete_user_group_event(realm: Realm, user_group_id: int, realm_id: int) -> None:
 | 
						|
    event = dict(type="user_group", op="remove", group_id=user_group_id)
 | 
						|
    send_event(realm, event, active_user_ids(realm_id))
 | 
						|
 | 
						|
 | 
						|
def check_delete_user_group(user_group: NamedUserGroup, *, acting_user: UserProfile) -> None:
 | 
						|
    user_group_id = user_group.id
 | 
						|
    user_group.delete()
 | 
						|
    do_send_delete_user_group_event(acting_user.realm, user_group_id, acting_user.realm.id)
 | 
						|
 | 
						|
 | 
						|
@transaction.atomic(savepoint=False)
 | 
						|
def do_change_user_group_permission_setting(
 | 
						|
    user_group: NamedUserGroup,
 | 
						|
    setting_name: str,
 | 
						|
    setting_value_group: UserGroup,
 | 
						|
    *,
 | 
						|
    old_setting_api_value: int | AnonymousSettingGroupDict,
 | 
						|
    acting_user: UserProfile | None,
 | 
						|
) -> None:
 | 
						|
    old_value = getattr(user_group, setting_name)
 | 
						|
    setattr(user_group, setting_name, setting_value_group)
 | 
						|
    user_group.save()
 | 
						|
 | 
						|
    new_setting_api_value = get_group_setting_value_for_api(setting_value_group)
 | 
						|
 | 
						|
    if not hasattr(old_value, "named_user_group") and hasattr(
 | 
						|
        setting_value_group, "named_user_group"
 | 
						|
    ):
 | 
						|
        # We delete the UserGroup which the setting was set to
 | 
						|
        # previously if it does not have any linked NamedUserGroup
 | 
						|
        # object, as it is not used anywhere else. A new UserGroup
 | 
						|
        # object would be created if the setting is later set to
 | 
						|
        # a combination of users and groups.
 | 
						|
        old_value.delete()
 | 
						|
 | 
						|
    RealmAuditLog.objects.create(
 | 
						|
        realm=user_group.realm,
 | 
						|
        acting_user=acting_user,
 | 
						|
        event_type=RealmAuditLog.USER_GROUP_GROUP_BASED_SETTING_CHANGED,
 | 
						|
        event_time=timezone_now(),
 | 
						|
        modified_user_group=user_group,
 | 
						|
        extra_data={
 | 
						|
            RealmAuditLog.OLD_VALUE: get_group_setting_value_for_audit_log_data(
 | 
						|
                old_setting_api_value
 | 
						|
            ),
 | 
						|
            RealmAuditLog.NEW_VALUE: get_group_setting_value_for_audit_log_data(
 | 
						|
                new_setting_api_value
 | 
						|
            ),
 | 
						|
            "property": setting_name,
 | 
						|
        },
 | 
						|
    )
 | 
						|
 | 
						|
    event_data_dict: dict[str, str | int | AnonymousSettingGroupDict] = {
 | 
						|
        setting_name: new_setting_api_value
 | 
						|
    }
 | 
						|
    do_send_user_group_update_event(user_group, event_data_dict)
 |