2022-03-02 11:58:37 +01:00
|
|
|
from typing import Any, Dict, List, Sequence
|
2020-06-11 00:54:34 +02:00
|
|
|
|
2017-09-25 09:47:15 +02:00
|
|
|
from django.db import transaction
|
2021-09-29 02:46:57 +02:00
|
|
|
from django.db.models import QuerySet
|
2021-04-16 00:57:30 +02:00
|
|
|
from django.utils.translation import gettext as _
|
2021-09-29 02:46:57 +02:00
|
|
|
from django_cte import With
|
2020-06-11 00:54:34 +02:00
|
|
|
|
2017-11-01 10:04:16 +01:00
|
|
|
from zerver.lib.exceptions import JsonableError
|
2021-08-11 15:10:17 +02:00
|
|
|
from zerver.models import GroupGroupMembership, Realm, UserGroup, UserGroupMembership, UserProfile
|
2020-06-11 00:54:34 +02:00
|
|
|
|
2017-09-25 09:47:15 +02:00
|
|
|
|
2022-03-18 14:38:11 +01:00
|
|
|
def access_user_group_by_id(
|
|
|
|
user_group_id: int, user_profile: UserProfile, *, for_read: bool = False
|
|
|
|
) -> UserGroup:
|
2017-11-01 10:04:16 +01:00
|
|
|
try:
|
2018-02-19 13:38:18 +01:00
|
|
|
user_group = UserGroup.objects.get(id=user_group_id, realm=user_profile.realm)
|
2022-03-18 14:38:11 +01:00
|
|
|
if for_read and not user_profile.is_guest:
|
|
|
|
# Everyone is allowed to read a user group and check who
|
|
|
|
# are its members. Guests should be unable to reach this
|
|
|
|
# code path, since they can't access user groups API
|
|
|
|
# endpoints, but we check for guests here for defense in
|
|
|
|
# depth.
|
|
|
|
return user_group
|
2021-12-30 01:18:46 +01:00
|
|
|
if user_group.is_system_group:
|
2021-08-06 15:22:08 +02:00
|
|
|
raise JsonableError(_("Insufficient permission"))
|
2022-04-27 11:59:25 +02:00
|
|
|
group_member_ids = get_user_group_direct_member_ids(user_group)
|
2021-05-21 07:02:43 +02:00
|
|
|
if (
|
|
|
|
not user_profile.is_realm_admin
|
|
|
|
and not user_profile.is_moderator
|
|
|
|
and user_profile.id not in group_member_ids
|
|
|
|
):
|
2021-06-30 09:13:19 +02:00
|
|
|
raise JsonableError(_("Insufficient permission"))
|
2017-11-01 10:04:16 +01:00
|
|
|
except UserGroup.DoesNotExist:
|
|
|
|
raise JsonableError(_("Invalid user group"))
|
|
|
|
return user_group
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2022-03-02 11:58:37 +01:00
|
|
|
def access_user_groups_as_potential_subgroups(
|
|
|
|
user_group_ids: Sequence[int], acting_user: UserProfile
|
|
|
|
) -> List[UserGroup]:
|
|
|
|
user_groups = UserGroup.objects.filter(id__in=user_group_ids, realm=acting_user.realm)
|
|
|
|
|
|
|
|
valid_group_ids = [group.id for group in user_groups]
|
|
|
|
invalid_group_ids = [group_id for group_id in user_group_ids if group_id not in valid_group_ids]
|
|
|
|
if invalid_group_ids:
|
|
|
|
raise JsonableError(_("Invalid user group ID: {}").format(invalid_group_ids[0]))
|
|
|
|
|
|
|
|
return list(user_groups)
|
|
|
|
|
|
|
|
|
2018-05-10 19:13:36 +02:00
|
|
|
def user_groups_in_realm_serialized(realm: Realm) -> List[Dict[str, Any]]:
|
2017-11-30 01:09:23 +01:00
|
|
|
"""This function is used in do_events_register code path so this code
|
|
|
|
should be performant. We need to do 2 database queries because
|
|
|
|
Django's ORM doesn't properly support the left join between
|
|
|
|
UserGroup and UserGroupMembership that we need.
|
2017-11-07 07:56:26 +01:00
|
|
|
"""
|
2017-11-30 01:09:23 +01:00
|
|
|
realm_groups = UserGroup.objects.filter(realm=realm)
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
group_dicts: Dict[str, Any] = {}
|
2017-11-30 01:09:23 +01:00
|
|
|
for user_group in realm_groups:
|
|
|
|
group_dicts[user_group.id] = dict(
|
|
|
|
id=user_group.id,
|
|
|
|
name=user_group.name,
|
|
|
|
description=user_group.description,
|
|
|
|
members=[],
|
2022-05-16 17:02:44 +02:00
|
|
|
direct_subgroup_ids=[],
|
2021-08-06 19:31:00 +02:00
|
|
|
is_system_group=user_group.is_system_group,
|
2017-11-30 01:09:23 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
membership = UserGroupMembership.objects.filter(user_group__realm=realm).values_list(
|
2021-02-12 08:20:45 +01:00
|
|
|
"user_group_id", "user_profile_id"
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2017-11-30 01:09:23 +01:00
|
|
|
for (user_group_id, user_profile_id) in membership:
|
2021-02-12 08:20:45 +01:00
|
|
|
group_dicts[user_group_id]["members"].append(user_profile_id)
|
2022-02-28 11:50:33 +01:00
|
|
|
|
|
|
|
group_membership = GroupGroupMembership.objects.filter(subgroup__realm=realm).values_list(
|
|
|
|
"subgroup_id", "supergroup_id"
|
|
|
|
)
|
|
|
|
for (subgroup_id, supergroup_id) in group_membership:
|
2022-05-16 17:02:44 +02:00
|
|
|
group_dicts[supergroup_id]["direct_subgroup_ids"].append(subgroup_id)
|
2022-02-28 11:50:33 +01:00
|
|
|
|
2017-11-30 01:09:23 +01:00
|
|
|
for group_dict in group_dicts.values():
|
2021-02-12 08:20:45 +01:00
|
|
|
group_dict["members"] = sorted(group_dict["members"])
|
2022-05-16 17:02:44 +02:00
|
|
|
group_dict["direct_subgroup_ids"] = sorted(group_dict["direct_subgroup_ids"])
|
2017-11-30 01:09:23 +01:00
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
return sorted(group_dicts.values(), key=lambda group_dict: group_dict["id"])
|
2017-11-07 07:56:26 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2021-10-09 19:53:03 +02:00
|
|
|
def get_direct_user_groups(user_profile: UserProfile) -> List[UserGroup]:
|
2021-10-11 08:37:15 +02:00
|
|
|
return list(user_profile.direct_groups.all())
|
2017-09-25 09:47:15 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2017-11-05 11:15:10 +01:00
|
|
|
def remove_user_from_user_group(user_profile: UserProfile, user_group: UserGroup) -> int:
|
2017-09-25 09:47:15 +02:00
|
|
|
num_deleted, _ = UserGroupMembership.objects.filter(
|
2021-02-12 08:19:30 +01:00
|
|
|
user_profile=user_profile, user_group=user_group
|
|
|
|
).delete()
|
2017-09-25 09:47:15 +02:00
|
|
|
return num_deleted
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
|
|
|
def create_user_group(
|
2021-08-06 15:22:08 +02:00
|
|
|
name: str,
|
|
|
|
members: List[UserProfile],
|
|
|
|
realm: Realm,
|
|
|
|
*,
|
|
|
|
description: str = "",
|
|
|
|
is_system_group: bool = False,
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> UserGroup:
|
2017-09-25 09:47:15 +02:00
|
|
|
with transaction.atomic():
|
2021-08-06 15:22:08 +02:00
|
|
|
user_group = UserGroup.objects.create(
|
|
|
|
name=name, realm=realm, description=description, is_system_group=is_system_group
|
|
|
|
)
|
2020-09-02 06:20:26 +02:00
|
|
|
UserGroupMembership.objects.bulk_create(
|
2021-02-12 08:19:30 +01:00
|
|
|
UserGroupMembership(user_profile=member, user_group=user_group) for member in members
|
2020-09-02 06:20:26 +02:00
|
|
|
)
|
2017-09-25 09:47:15 +02:00
|
|
|
return user_group
|
2017-11-02 08:53:30 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2022-04-27 11:59:25 +02:00
|
|
|
def get_user_group_direct_member_ids(user_group: UserGroup) -> List[int]:
|
2021-10-12 11:47:36 +02:00
|
|
|
return UserGroupMembership.objects.filter(user_group=user_group).values_list(
|
|
|
|
"user_profile_id", flat=True
|
|
|
|
)
|
2018-02-19 13:38:18 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2022-04-27 12:38:19 +02:00
|
|
|
def get_user_group_direct_members(user_group: UserGroup) -> "QuerySet[UserGroup]":
|
|
|
|
return user_group.direct_members.all()
|
|
|
|
|
|
|
|
|
2021-10-09 20:02:39 +02:00
|
|
|
def get_direct_memberships_of_users(user_group: UserGroup, members: List[UserProfile]) -> List[int]:
|
2021-02-12 08:19:30 +01:00
|
|
|
return list(
|
|
|
|
UserGroupMembership.objects.filter(
|
|
|
|
user_group=user_group, user_profile__in=members
|
2021-02-12 08:20:45 +01:00
|
|
|
).values_list("user_profile_id", flat=True)
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2021-09-29 02:46:57 +02:00
|
|
|
|
|
|
|
|
|
|
|
# These recursive lookups use standard PostgreSQL common table
|
|
|
|
# expression (CTE) queries. These queries use the django-cte library,
|
|
|
|
# because upstream Django does not yet support CTE.
|
|
|
|
#
|
|
|
|
# https://www.postgresql.org/docs/current/queries-with.html
|
|
|
|
# https://pypi.org/project/django-cte/
|
|
|
|
# https://code.djangoproject.com/ticket/28919
|
|
|
|
|
|
|
|
|
|
|
|
def get_recursive_subgroups(user_group: UserGroup) -> "QuerySet[UserGroup]":
|
|
|
|
cte = With.recursive(
|
|
|
|
lambda cte: UserGroup.objects.filter(id=user_group.id)
|
|
|
|
.values("id")
|
|
|
|
.union(cte.join(UserGroup, direct_supergroups=cte.col.id).values("id"))
|
|
|
|
)
|
|
|
|
return cte.join(UserGroup, id=cte.col.id).with_cte(cte)
|
|
|
|
|
|
|
|
|
|
|
|
def get_recursive_group_members(user_group: UserGroup) -> "QuerySet[UserProfile]":
|
|
|
|
return UserProfile.objects.filter(direct_groups__in=get_recursive_subgroups(user_group))
|
|
|
|
|
|
|
|
|
|
|
|
def get_recursive_membership_groups(user_profile: UserProfile) -> "QuerySet[UserGroup]":
|
|
|
|
cte = With.recursive(
|
|
|
|
lambda cte: user_profile.direct_groups.values("id").union(
|
|
|
|
cte.join(UserGroup, direct_subgroups=cte.col.id).values("id")
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return cte.join(UserGroup, id=cte.col.id).with_cte(cte)
|
2021-08-11 15:10:17 +02:00
|
|
|
|
|
|
|
|
2022-03-28 15:55:51 +02:00
|
|
|
def is_user_in_group(
|
|
|
|
user_group: UserGroup, user: UserProfile, *, direct_member_only: bool = False
|
|
|
|
) -> bool:
|
|
|
|
if direct_member_only:
|
2022-04-27 12:38:19 +02:00
|
|
|
return get_user_group_direct_members(user_group=user_group).filter(id=user.id).exists()
|
2022-03-28 15:55:51 +02:00
|
|
|
|
|
|
|
return get_recursive_group_members(user_group=user_group).filter(id=user.id).exists()
|
|
|
|
|
|
|
|
|
2022-03-24 11:39:57 +01:00
|
|
|
def get_user_group_member_ids(
|
|
|
|
user_group: UserGroup, *, direct_member_only: bool = False
|
|
|
|
) -> List[int]:
|
|
|
|
if direct_member_only:
|
2022-04-27 11:59:25 +02:00
|
|
|
member_ids = get_user_group_direct_member_ids(user_group)
|
2022-03-24 11:39:57 +01:00
|
|
|
else:
|
|
|
|
member_ids = get_recursive_group_members(user_group).values_list("id", flat=True)
|
|
|
|
|
|
|
|
return list(member_ids)
|
|
|
|
|
|
|
|
|
2022-04-04 13:59:25 +02:00
|
|
|
def get_subgroup_ids(user_group: UserGroup, *, direct_subgroup_only: bool = False) -> List[int]:
|
|
|
|
if direct_subgroup_only:
|
|
|
|
subgroup_ids = user_group.direct_subgroups.all().values_list("id", flat=True)
|
|
|
|
else:
|
|
|
|
subgroup_ids = (
|
|
|
|
get_recursive_subgroups(user_group)
|
|
|
|
.exclude(id=user_group.id)
|
|
|
|
.values_list("id", flat=True)
|
|
|
|
)
|
|
|
|
|
|
|
|
return list(subgroup_ids)
|
|
|
|
|
|
|
|
|
2021-08-11 15:10:17 +02:00
|
|
|
def create_system_user_groups_for_realm(realm: Realm) -> Dict[int, UserGroup]:
|
2021-08-12 12:15:06 +02:00
|
|
|
"""Any changes to this function likely require a migration to adjust
|
|
|
|
existing realms. See e.g. migration 0375_create_role_based_system_groups.py,
|
|
|
|
which is a copy of this function from when we introduced system groups.
|
|
|
|
"""
|
2021-08-11 15:10:17 +02:00
|
|
|
role_system_groups_dict: Dict[int, UserGroup] = {}
|
|
|
|
for role in UserGroup.SYSTEM_USER_GROUP_ROLE_MAP.keys():
|
|
|
|
user_group_params = UserGroup.SYSTEM_USER_GROUP_ROLE_MAP[role]
|
|
|
|
user_group = UserGroup(
|
|
|
|
name=user_group_params["name"],
|
|
|
|
description=user_group_params["description"],
|
|
|
|
realm=realm,
|
|
|
|
is_system_group=True,
|
|
|
|
)
|
|
|
|
role_system_groups_dict[role] = user_group
|
|
|
|
|
|
|
|
full_members_system_group = UserGroup(
|
|
|
|
name="@role:fullmembers",
|
|
|
|
description="Members of this organization, not including new accounts and guests",
|
|
|
|
realm=realm,
|
|
|
|
is_system_group=True,
|
|
|
|
)
|
|
|
|
everyone_on_internet_system_group = UserGroup(
|
|
|
|
name="@role:internet",
|
|
|
|
description="Everyone on the Internet",
|
|
|
|
realm=realm,
|
|
|
|
is_system_group=True,
|
|
|
|
)
|
|
|
|
# Order of this list here is important to create correct GroupGroupMembership objects
|
|
|
|
system_user_groups_list = [
|
|
|
|
role_system_groups_dict[UserProfile.ROLE_REALM_OWNER],
|
|
|
|
role_system_groups_dict[UserProfile.ROLE_REALM_ADMINISTRATOR],
|
|
|
|
role_system_groups_dict[UserProfile.ROLE_MODERATOR],
|
|
|
|
full_members_system_group,
|
|
|
|
role_system_groups_dict[UserProfile.ROLE_MEMBER],
|
|
|
|
role_system_groups_dict[UserProfile.ROLE_GUEST],
|
|
|
|
everyone_on_internet_system_group,
|
|
|
|
]
|
|
|
|
|
|
|
|
UserGroup.objects.bulk_create(system_user_groups_list)
|
|
|
|
|
|
|
|
subgroup_objects = []
|
|
|
|
subgroup, remaining_groups = system_user_groups_list[0], system_user_groups_list[1:]
|
|
|
|
for supergroup in remaining_groups:
|
|
|
|
subgroup_objects.append(GroupGroupMembership(subgroup=subgroup, supergroup=supergroup))
|
|
|
|
subgroup = supergroup
|
|
|
|
|
|
|
|
GroupGroupMembership.objects.bulk_create(subgroup_objects)
|
|
|
|
|
|
|
|
return role_system_groups_dict
|
2021-08-12 12:15:06 +02:00
|
|
|
|
|
|
|
|
|
|
|
def get_system_user_group_for_user(user_profile: UserProfile) -> UserGroup:
|
|
|
|
system_user_group_name = UserGroup.SYSTEM_USER_GROUP_ROLE_MAP[user_profile.role]["name"]
|
|
|
|
|
|
|
|
system_user_group = UserGroup.objects.get(
|
|
|
|
name=system_user_group_name, realm=user_profile.realm, is_system_group=True
|
|
|
|
)
|
|
|
|
return system_user_group
|