user_groups: Audit UserGroup subgroup memberships changes.

It's worth noting that instead of adding another field to the
RealmAuditLog model, we store the modified subgroup ids in extra_data as
a JSON encoded dict with the key "subgroup_ids". We don't create audit
log entries for supergroup changes at this point.

Signed-off-by: Zixuan James Li <p359101898@gmail.com>
This commit is contained in:
Zixuan James Li
2022-12-11 21:29:10 -05:00
committed by Tim Abbott
parent 44781ddfa9
commit ad698d597a
4 changed files with 101 additions and 3 deletions

View File

@@ -2,6 +2,7 @@ import datetime
from typing import Dict, List, Mapping, Optional, Sequence, TypedDict, Union from typing import Dict, List, Mapping, Optional, Sequence, TypedDict, Union
import django.db.utils import django.db.utils
import orjson
from django.db import transaction from django.db import transaction
from django.utils.timezone import now as timezone_now from django.utils.timezone import now as timezone_now
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
@@ -298,6 +299,16 @@ def add_subgroups_to_user_group(
GroupGroupMembership.objects.bulk_create(group_memberships) GroupGroupMembership.objects.bulk_create(group_memberships)
subgroup_ids = [subgroup.id for subgroup in subgroups] subgroup_ids = [subgroup.id for subgroup in subgroups]
now = timezone_now()
RealmAuditLog.objects.create(
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=orjson.dumps({"subgroup_ids": subgroup_ids}).decode(),
)
do_send_subgroups_update_event("add_subgroups", user_group, subgroup_ids) do_send_subgroups_update_event("add_subgroups", user_group, subgroup_ids)
@@ -308,6 +319,16 @@ def remove_subgroups_from_user_group(
GroupGroupMembership.objects.filter(supergroup=user_group, subgroup__in=subgroups).delete() GroupGroupMembership.objects.filter(supergroup=user_group, subgroup__in=subgroups).delete()
subgroup_ids = [subgroup.id for subgroup in subgroups] subgroup_ids = [subgroup.id for subgroup in subgroups]
now = timezone_now()
RealmAuditLog.objects.create(
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=orjson.dumps({"subgroup_ids": subgroup_ids}).decode(),
)
do_send_subgroups_update_event("remove_subgroups", user_group, subgroup_ids) do_send_subgroups_update_event("remove_subgroups", user_group, subgroup_ids)

View File

@@ -1,5 +1,6 @@
from typing import Dict, Iterable, List, Mapping, Sequence, TypedDict from typing import Dict, Iterable, List, Mapping, Sequence, TypedDict
import orjson
from django.db import transaction from django.db import transaction
from django.db.models import F, QuerySet from django.db.models import F, QuerySet
from django.utils.timezone import now as timezone_now from django.utils.timezone import now as timezone_now
@@ -328,7 +329,7 @@ def create_system_user_groups_for_realm(realm: Realm) -> Dict[int, UserGroup]:
creation_time = timezone_now() creation_time = timezone_now()
UserGroup.objects.bulk_create(system_user_groups_list) UserGroup.objects.bulk_create(system_user_groups_list)
RealmAuditLog.objects.bulk_create( realmauditlog_objects = [
RealmAuditLog( RealmAuditLog(
realm=realm, realm=realm,
acting_user=None, acting_user=None,
@@ -337,7 +338,7 @@ def create_system_user_groups_for_realm(realm: Realm) -> Dict[int, UserGroup]:
modified_user_group=user_group, modified_user_group=user_group,
) )
for user_group in system_user_groups_list for user_group in system_user_groups_list
) ]
groups_with_updated_settings = [] groups_with_updated_settings = []
system_groups_name_dict = get_role_based_system_groups_dict(realm) system_groups_name_dict = get_role_based_system_groups_dict(realm)
@@ -346,14 +347,25 @@ def create_system_user_groups_for_realm(realm: Realm) -> Dict[int, UserGroup]:
groups_with_updated_settings.append(group) groups_with_updated_settings.append(group)
UserGroup.objects.bulk_update(groups_with_updated_settings, ["can_mention_group"]) UserGroup.objects.bulk_update(groups_with_updated_settings, ["can_mention_group"])
subgroup_objects = [] 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.
subgroup, remaining_groups = system_user_groups_list[1], system_user_groups_list[2:] subgroup, remaining_groups = system_user_groups_list[1], system_user_groups_list[2:]
for supergroup in remaining_groups: for supergroup in remaining_groups:
subgroup_objects.append(GroupGroupMembership(subgroup=subgroup, supergroup=supergroup)) subgroup_objects.append(GroupGroupMembership(subgroup=subgroup, supergroup=supergroup))
realmauditlog_objects.append(
RealmAuditLog(
realm=realm,
modified_user_group=supergroup,
event_type=RealmAuditLog.USER_GROUP_DIRECT_SUBGROUP_MEMBERSHIP_ADDED,
event_time=timezone_now(),
acting_user=None,
extra_data=orjson.dumps({"subgroup_ids": [subgroup.id]}).decode(),
)
)
subgroup = supergroup subgroup = supergroup
GroupGroupMembership.objects.bulk_create(subgroup_objects) GroupGroupMembership.objects.bulk_create(subgroup_objects)
RealmAuditLog.objects.bulk_create(realmauditlog_objects)
return role_system_groups_dict return role_system_groups_dict

View File

@@ -46,9 +46,11 @@ from zerver.actions.streams import (
do_rename_stream, do_rename_stream,
) )
from zerver.actions.user_groups import ( from zerver.actions.user_groups import (
add_subgroups_to_user_group,
bulk_add_members_to_user_group, bulk_add_members_to_user_group,
check_add_user_group, check_add_user_group,
remove_members_from_user_group, remove_members_from_user_group,
remove_subgroups_from_user_group,
) )
from zerver.actions.user_settings import ( from zerver.actions.user_settings import (
do_change_avatar_fields, do_change_avatar_fields,
@@ -1090,6 +1092,32 @@ class TestRealmAuditLog(ZulipTestCase):
) )
self.assertListEqual(logged_system_group_ids, system_user_group_ids) self.assertListEqual(logged_system_group_ids, system_user_group_ids)
logged_subgroup_entries = sorted(
RealmAuditLog.objects.filter(
realm=realm,
event_type=RealmAuditLog.USER_GROUP_DIRECT_SUBGROUP_MEMBERSHIP_ADDED,
event_time__gte=now,
acting_user=None,
).values_list("modified_user_group_id", "extra_data")
)
# Excluding nobody_system_group, the rest of the user groups should have
# a chain of subgroup memberships in between.
self.assert_length(logged_subgroup_entries, expected_system_user_group_count - 2)
for i in range(len(logged_subgroup_entries)):
# The offset of 1 is due to nobody_system_group being skipped as
# the first user group in the list.
# For supergroup, we add an additional 1 because of the order we
# put the chain together.
expected_subgroup_id = system_user_group_ids[i + 1]
expected_supergroup_id = system_user_group_ids[i + 2]
supergroup_id, subgroup_extra_data = logged_subgroup_entries[i]
assert subgroup_extra_data is not None
self.assertEqual(
orjson.loads(subgroup_extra_data)["subgroup_ids"][0], expected_subgroup_id
)
self.assertEqual(supergroup_id, expected_supergroup_id)
def test_user_group_creation(self) -> None: def test_user_group_creation(self) -> None:
hamlet = self.example_user("hamlet") hamlet = self.example_user("hamlet")
cordelia = self.example_user("cordelia") cordelia = self.example_user("cordelia")
@@ -1146,3 +1174,39 @@ class TestRealmAuditLog(ZulipTestCase):
) )
self.assert_length(audit_log_entries, 1) self.assert_length(audit_log_entries, 1)
self.assertEqual(audit_log_entries[0].modified_user, hamlet) self.assertEqual(audit_log_entries[0].modified_user, hamlet)
def test_change_user_group_subgroups_memberships(self) -> None:
hamlet = self.example_user("hamlet")
user_group = check_add_user_group(hamlet.realm, "main", [], acting_user=None)
subgroups = [
check_add_user_group(hamlet.realm, f"subgroup{num}", [], acting_user=hamlet)
for num in range(3)
]
now = timezone_now()
add_subgroups_to_user_group(user_group, subgroups, acting_user=hamlet)
# Only one audit log entry for the subgroup membership is expected.
audit_log_entry = RealmAuditLog.objects.get(
realm=hamlet.realm,
event_time__gte=now,
event_type=RealmAuditLog.USER_GROUP_DIRECT_SUBGROUP_MEMBERSHIP_ADDED,
)
self.assertEqual(audit_log_entry.modified_user_group, user_group)
self.assertEqual(audit_log_entry.acting_user, hamlet)
self.assertDictEqual(
orjson.loads(assert_is_not_none(audit_log_entry.extra_data)),
{"subgroup_ids": [subgroup.id for subgroup in subgroups]},
)
remove_subgroups_from_user_group(user_group, subgroups[:2], acting_user=hamlet)
audit_log_entry = RealmAuditLog.objects.get(
realm=hamlet.realm,
event_time__gte=now,
event_type=RealmAuditLog.USER_GROUP_DIRECT_SUBGROUP_MEMBERSHIP_REMOVED,
)
self.assertEqual(audit_log_entry.modified_user_group, user_group)
self.assertEqual(audit_log_entry.acting_user, hamlet)
self.assertDictEqual(
orjson.loads(assert_is_not_none(audit_log_entry.extra_data)),
{"subgroup_ids": [subgroup.id for subgroup in subgroups[:2]]},
)

View File

@@ -1335,6 +1335,7 @@ class SlackImporter(ZulipTestCase):
RealmAuditLog.REALM_PLAN_TYPE_CHANGED, RealmAuditLog.REALM_PLAN_TYPE_CHANGED,
RealmAuditLog.REALM_CREATED, RealmAuditLog.REALM_CREATED,
RealmAuditLog.USER_GROUP_CREATED, RealmAuditLog.USER_GROUP_CREATED,
RealmAuditLog.USER_GROUP_DIRECT_SUBGROUP_MEMBERSHIP_ADDED,
}, },
) )