diff --git a/analytics/management/commands/populate_analytics_db.py b/analytics/management/commands/populate_analytics_db.py index bdec83cc4c..c9654d574e 100644 --- a/analytics/management/commands/populate_analytics_db.py +++ b/analytics/management/commands/populate_analytics_db.py @@ -11,7 +11,8 @@ from analytics.lib.time_utils import time_range from analytics.models import BaseCount, InstallationCount, RealmCount, \ UserCount, StreamCount, FillState from zerver.lib.timestamp import floor_to_day -from zerver.models import Realm, UserProfile, Stream, Message, Client +from zerver.models import Realm, UserProfile, Stream, Message, Client, \ + RealmAuditLog from datetime import datetime, timedelta @@ -26,10 +27,14 @@ class Command(BaseCommand): def create_user(self, email, full_name, is_staff, date_joined, realm): # type: (Text, Text, Text, bool, datetime, Realm) -> UserProfile - return UserProfile.objects.create( + user = UserProfile.objects.create( email=email, full_name=full_name, is_staff=is_staff, realm=realm, short_name=full_name, pointer=-1, last_pointer_updater='none', api_key='42', date_joined=date_joined) + RealmAuditLog.objects.create( + realm=realm, modified_user=user, event_type='user_created', + event_time=user.date_joined) + return user def generate_fixture_data(self, stat, business_hours_base, non_business_hours_base, growth, autocorrelation, spikiness, holiday_rate=0): diff --git a/zerver/lib/actions.py b/zerver/lib/actions.py index 2f5fa00aa3..2a55748eab 100644 --- a/zerver/lib/actions.py +++ b/zerver/lib/actions.py @@ -27,7 +27,7 @@ from zerver.lib.message import ( ) from zerver.lib.realm_icon import realm_icon_url from zerver.models import Realm, RealmEmoji, Stream, UserProfile, UserActivity, RealmAlias, \ - Subscription, Recipient, Message, Attachment, UserMessage, \ + Subscription, Recipient, Message, Attachment, UserMessage, RealmAuditLog, \ Client, DefaultStream, UserPresence, Referral, PushDeviceToken, MAX_SUBJECT_LENGTH, \ MAX_MESSAGE_LENGTH, get_client, get_stream, get_recipient, get_huddle, \ get_user_profile_by_id, PreregistrationUser, get_display_recipient, \ @@ -399,8 +399,19 @@ def do_create_user(email, password, realm, full_name, short_name, default_all_public_streams=None, prereg_user=None, newsletter_data=None): # type: (Text, Text, Realm, Text, Text, bool, Optional[int], Optional[UserProfile], Optional[Text], Text, Optional[Stream], Optional[Stream], bool, Optional[PreregistrationUser], Optional[Dict[str, str]]) -> UserProfile + user_profile = create_user(email=email, password=password, realm=realm, + full_name=full_name, short_name=short_name, + active=active, bot_type=bot_type, bot_owner=bot_owner, + tos_version=tos_version, avatar_source=avatar_source, + default_sending_stream=default_sending_stream, + default_events_register_stream=default_events_register_stream, + default_all_public_streams=default_all_public_streams) + + RealmAuditLog.objects.create(realm=user_profile.realm, modified_user=user_profile, + event_type='user_created', event_time=user_profile.date_joined) + event = {'type': 'user_created', - 'timestamp': time.time(), + 'timestamp': datetime_to_timestamp(user_profile.date_joined), 'full_name': full_name, 'short_name': short_name, 'user': email, @@ -410,14 +421,6 @@ def do_create_user(email, password, realm, full_name, short_name, event['bot_owner'] = bot_owner.email log_event(event) - user_profile = create_user(email=email, password=password, realm=realm, - full_name=full_name, short_name=short_name, - active=active, bot_type=bot_type, bot_owner=bot_owner, - tos_version=tos_version, avatar_source=avatar_source, - default_sending_stream=default_sending_stream, - default_events_register_stream=default_events_register_stream, - default_all_public_streams=default_all_public_streams) - notify_created_user(user_profile) if bot_type: notify_created_bot(user_profile) @@ -629,9 +632,13 @@ def do_deactivate_user(user_profile, log=True, _cascade=True): delete_user_sessions(user_profile) + event_time = timezone.now() + RealmAuditLog.objects.create(realm=user_profile.realm, modified_user=user_profile, + event_type='user_deactivated', event_time=event_time) + if log: log_event({'type': 'user_deactivated', - 'timestamp': time.time(), + 'timestamp': datetime_to_timestamp(event_time), 'user': user_profile.email, 'domain': user_profile.realm.domain}) @@ -1846,9 +1853,14 @@ def do_activate_user(user_profile, log=True, join_date=timezone.now()): user_profile.save(update_fields=["is_active", "date_joined", "password", "is_mirror_dummy", "tos_version"]) + event_time = timezone.now() + RealmAuditLog.objects.create(realm=user_profile.realm, modified_user=user_profile, + event_type='user_activated', event_time=event_time) + if log: domain = user_profile.realm.domain log_event({'type': 'user_activated', + 'timestamp': datetime_to_timestamp(event_time), 'user': user_profile.email, 'domain': domain}) @@ -1861,8 +1873,13 @@ def do_reactivate_user(user_profile): user_profile.is_active = True user_profile.save(update_fields=["is_active"]) + event_time = timezone.now() + RealmAuditLog.objects.create(realm=user_profile.realm, modified_user=user_profile, + event_type='user_reactivated', event_time=event_time) + domain = user_profile.realm.domain log_event({'type': 'user_reactivated', + 'timestamp': datetime_to_timestamp(event_time), 'user': user_profile.email, 'domain': domain}) diff --git a/zerver/lib/bulk_create.py b/zerver/lib/bulk_create.py index 058fe41288..b3593644a5 100644 --- a/zerver/lib/bulk_create.py +++ b/zerver/lib/bulk_create.py @@ -3,7 +3,7 @@ from typing import Any, Dict, Iterable, List, Mapping, Optional, Set, Tuple, Tex from zerver.lib.initial_password import initial_password from zerver.models import Realm, Stream, UserProfile, Huddle, \ - Subscription, Recipient, Client, get_huddle_hash + Subscription, Recipient, Client, RealmAuditLog, get_huddle_hash from zerver.lib.create_user import create_user_profile def bulk_create_realms(realm_list): @@ -35,6 +35,11 @@ def bulk_create_users(realm, users_raw, bot_type=None, tos_version=None): profiles_to_create.append(profile) UserProfile.objects.bulk_create(profiles_to_create) + RealmAuditLog.objects.bulk_create( + [RealmAuditLog(realm=profile_.realm, modified_user=profile_, + event_type='user_created', event_time=profile_.date_joined) + for profile_ in profiles_to_create]) + profiles_by_email = {} # type: Dict[Text, UserProfile] profiles_by_id = {} # type: Dict[int, UserProfile] for profile in UserProfile.objects.select_related().all(): diff --git a/zerver/migrations/0057_realmauditlog.py b/zerver/migrations/0057_realmauditlog.py new file mode 100644 index 0000000000..b98d019863 --- /dev/null +++ b/zerver/migrations/0057_realmauditlog.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-03-04 07:33 +from __future__ import unicode_literals + +from django.db.backends.postgresql_psycopg2.schema import DatabaseSchemaEditor +from django.db.migrations.state import StateApps + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + +from django.utils import timezone + +from zerver.models import RealmAuditLog, UserProfile + +def backfill_user_activations_and_deactivations(apps, schema_editor): + # type: (StateApps, DatabaseSchemaEditor) -> None + migration_time = timezone.now() + + for user in UserProfile.objects.all(): + RealmAuditLog.objects.create(realm=user.realm, modified_user=user, + event_type='user_created', event_time=user.date_joined, + backfilled=False) + + for user in UserProfile.objects.filter(is_active=False): + RealmAuditLog.objects.create(realm=user.realm, modified_user=user, + event_type='user_deactivated', event_time=migration_time, + backfilled=True) + +def reverse_code(apps, schema_editor): + # type: (StateApps, DatabaseSchemaEditor) -> None + RealmAuditLog.objects.filter(event_type='user_created').delete() + RealmAuditLog.objects.filter(event_type='user_deactivated').delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('zerver', '0056_userprofile_emoji_alt_code'), + ] + + operations = [ + migrations.CreateModel( + name='RealmAuditLog', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('event_type', models.CharField(max_length=40)), + ('backfilled', models.BooleanField(default=False)), + ('event_time', models.DateTimeField()), + ('acting_user', models.ForeignKey(null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name='+', + to=settings.AUTH_USER_MODEL)), + ('modified_stream', models.ForeignKey(null=True, + on_delete=django.db.models.deletion.CASCADE, + to='zerver.Stream')), + ('modified_user', models.ForeignKey(null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name='+', + to=settings.AUTH_USER_MODEL)), + ('realm', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='zerver.Realm')), + ], + ), + + migrations.RunPython(backfill_user_activations_and_deactivations, + reverse_code=reverse_code), + + ] diff --git a/zerver/models.py b/zerver/models.py index 8bf93b063f..9dbf51e14a 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -1485,3 +1485,12 @@ class ScheduledJob(models.Model): # Kind if like a ForeignKey, but table is determined by type. filter_id = models.IntegerField(null=True) # type: Optional[int] filter_string = models.CharField(max_length=100) # type: Text + +class RealmAuditLog(models.Model): + realm = models.ForeignKey(Realm) # type: Realm + acting_user = models.ForeignKey(UserProfile, null=True, related_name='+') # type: Optional[UserProfile] + modified_user = models.ForeignKey(UserProfile, null=True, related_name='+') # type: Optional[UserProfile] + modified_stream = models.ForeignKey(Stream, null=True) # type: Optional[Stream] + event_type = models.CharField(max_length=40) # type: Text + event_time = models.DateTimeField() # type: datetime.datetime + backfilled = models.BooleanField(default=False) # type: bool diff --git a/zerver/tests/test_audit_log.py b/zerver/tests/test_audit_log.py new file mode 100644 index 0000000000..543db5eb6f --- /dev/null +++ b/zerver/tests/test_audit_log.py @@ -0,0 +1,27 @@ + +from django.utils import timezone + +from zerver.lib.actions import do_create_user, do_deactivate_user, \ + do_activate_user, do_reactivate_user +from zerver.lib.test_classes import ZulipTestCase +from zerver.models import RealmAuditLog, get_realm + +from datetime import timedelta + +class TestUserActivation(ZulipTestCase): + def test_user_activation(self): + # type: () -> None + realm = get_realm('zulip') + now = timezone.now() + user = do_create_user('email', 'password', realm, 'full_name', 'short_name') + do_deactivate_user(user) + do_activate_user(user) + do_deactivate_user(user) + do_reactivate_user(user) + self.assertEqual(RealmAuditLog.objects.filter(event_time__gte=now).count(), 5) + event_types = list(RealmAuditLog.objects.filter( + realm=realm, acting_user=None, modified_user=user, modified_stream=None, + event_time__gte=now, event_time__lte=now+timedelta(minutes=60)) + .order_by('event_time').values_list('event_type', flat=True)) + self.assertEqual(event_types, ['user_created', 'user_deactivated', 'user_activated', + 'user_deactivated', 'user_reactivated'])