zulip/zerver/actions/user_groups.py

520 lines
18 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.timestamp import datetime_to_timestamp
from zerver.lib.types import AnonymousSettingGroupDict
from zerver.lib.user_groups import (
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.realm_audit_logs import AuditLogEventType
from zerver.models.users import active_user_ids
from zerver.tornado.django_api import send_event_on_commit
class MemberGroupUserDict(TypedDict):
id: int
role: int
date_joined: datetime
@transaction.atomic(savepoint=False)
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,
creator=acting_user,
)
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=AuditLogEventType.USER_GROUP_CREATED,
event_time=creation_time,
modified_user_group=user_group,
),
] + [
RealmAuditLog(
realm=realm,
acting_user=acting_user,
event_type=AuditLogEventType.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:
creator_id = user_group.creator_id
assert user_group.date_created is not None
date_created = datetime_to_timestamp(user_group.date_created)
event = dict(
type="user_group",
op="add",
group=dict(
name=user_group.name,
creator_id=creator_id,
date_created=date_created,
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_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,
),
)
send_event_on_commit(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)
if "name" in data:
# This field will be popped eventually before sending the event
# to client, but is needed to make sure we do not send the
# name update event for deactivated groups to client with
# 'include_deactivated_groups' client capability set to false.
event["deactivated"] = user_group.deactivated
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=AuditLogEventType.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=AuditLogEventType.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=AuditLogEventType.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=AuditLogEventType.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(savepoint=False)
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=AuditLogEventType.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=AuditLogEventType.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(savepoint=False)
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=AuditLogEventType.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=AuditLogEventType.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)
@transaction.atomic(savepoint=False)
def do_deactivate_user_group(
user_group: NamedUserGroup, *, acting_user: UserProfile | None
) -> None:
user_group.deactivated = True
user_group.save(update_fields=["deactivated"])
now = timezone_now()
RealmAuditLog.objects.create(
realm=user_group.realm,
modified_user_group_id=user_group.id,
event_type=AuditLogEventType.USER_GROUP_DEACTIVATED,
event_time=now,
acting_user=acting_user,
)
do_send_user_group_update_event(user_group, dict(deactivated=True))
event = dict(type="user_group", op="remove", group_id=user_group.id)
send_event_on_commit(user_group.realm, event, active_user_ids(user_group.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 | None = None,
acting_user: UserProfile | None,
) -> None:
old_value = getattr(user_group, setting_name)
setattr(user_group, setting_name, setting_value_group)
user_group.save()
if old_setting_api_value is None:
# Most production callers will have computed this as part of
# verifying whether there's an actual change to make, but it
# feels quite clumsy to have to pass it from unit tests, so we
# compute it here if not provided by the caller.
old_setting_api_value = get_group_setting_value_for_api(old_value)
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=AuditLogEventType.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)