mirror of https://github.com/zulip/zulip.git
520 lines
18 KiB
Python
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.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.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
|
|
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
|
|
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)
|