Audit: implement logging of basic events like login, logout, failed login
This commit is contained in:
		
							
								
								
									
										5
									
								
								passbook/audit/admin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								passbook/audit/admin.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | |||||||
|  | """passbook audit model admin""" | ||||||
|  |  | ||||||
|  | from passbook.lib.admin import admin_autoregister | ||||||
|  |  | ||||||
|  | admin_autoregister('passbook_audit') | ||||||
| @ -1,4 +1,6 @@ | |||||||
| """passbook audit app""" | """passbook audit app""" | ||||||
|  | from importlib import import_module | ||||||
|  |  | ||||||
| from django.apps import AppConfig | from django.apps import AppConfig | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -8,3 +10,6 @@ class PassbookAuditConfig(AppConfig): | |||||||
|     name = 'passbook.audit' |     name = 'passbook.audit' | ||||||
|     label = 'passbook_audit' |     label = 'passbook_audit' | ||||||
|     mountpoint = 'audit/' |     mountpoint = 'audit/' | ||||||
|  |  | ||||||
|  |     def ready(self): | ||||||
|  |         import_module('passbook.audit.signals') | ||||||
|  | |||||||
							
								
								
									
										30
									
								
								passbook/audit/migrations/0002_auto_20181210_1039.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								passbook/audit/migrations/0002_auto_20181210_1039.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | |||||||
|  | # Generated by Django 2.1.4 on 2018-12-10 10:39 | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ('passbook_audit', '0001_initial'), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='auditentry', | ||||||
|  |             name='context', | ||||||
|  |             field=models.TextField(default=''), | ||||||
|  |             preserve_default=False, | ||||||
|  |         ), | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='auditentry', | ||||||
|  |             name='request_ip', | ||||||
|  |             field=models.GenericIPAddressField(default=''), | ||||||
|  |             preserve_default=False, | ||||||
|  |         ), | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name='auditentry', | ||||||
|  |             name='action', | ||||||
|  |             field=models.TextField(choices=[('login', 'login'), ('login_failed', 'login_failed'), ('logout', 'logout'), ('authorize_application', 'authorize_application'), ('suspicious_request', 'suspicious_request'), ('sign_up', 'sign_up'), ('password_reset', 'password_reset')]), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
							
								
								
									
										27
									
								
								passbook/audit/migrations/0003_auto_20181210_1213.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								passbook/audit/migrations/0003_auto_20181210_1213.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | |||||||
|  | # Generated by Django 2.1.4 on 2018-12-10 12:13 | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ('passbook_audit', '0002_auto_20181210_1039'), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AlterModelOptions( | ||||||
|  |             name='auditentry', | ||||||
|  |             options={'verbose_name': 'Audit Entry', 'verbose_name_plural': 'Audit Entries'}, | ||||||
|  |         ), | ||||||
|  |         migrations.RenameField( | ||||||
|  |             model_name='auditentry', | ||||||
|  |             old_name='context', | ||||||
|  |             new_name='_context', | ||||||
|  |         ), | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name='auditentry', | ||||||
|  |             name='action', | ||||||
|  |             field=models.TextField(choices=[('login', 'login'), ('login_failed', 'login_failed'), ('logout', 'logout'), ('authorize_application', 'authorize_application'), ('suspicious_request', 'suspicious_request'), ('sign_up', 'sign_up'), ('password_reset', 'password_reset'), ('invite_used', 'invite_used')]), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -1,22 +1,75 @@ | |||||||
| """passbook audit models""" | """passbook audit models""" | ||||||
|  | from logging import getLogger | ||||||
|  | from json import dumps, loads | ||||||
|  |  | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
|  | from django.core.exceptions import ValidationError | ||||||
| from django.db import models | from django.db import models | ||||||
|  | from django.utils.translation import gettext as _ | ||||||
|  | from ipware import get_client_ip | ||||||
| from reversion import register | from reversion import register | ||||||
|  |  | ||||||
| from passbook.lib.models import UUIDModel | from passbook.lib.models import UUIDModel | ||||||
|  |  | ||||||
|  | LOGGER = getLogger(__name__) | ||||||
|  |  | ||||||
| @register() | @register() | ||||||
| class AuditEntry(UUIDModel): | class AuditEntry(UUIDModel): | ||||||
|     """An individual audit log entry""" |     """An individual audit log entry""" | ||||||
|  |  | ||||||
|  |     ACTION_LOGIN = 'login' | ||||||
|  |     ACTION_LOGIN_FAILED = 'login_failed' | ||||||
|  |     ACTION_LOGOUT = 'logout' | ||||||
|  |     ACTION_AUTHORIZE_APPLICATION = 'authorize_application' | ||||||
|  |     ACTION_SUSPICIOUS_REQUEST = 'suspicious_request' | ||||||
|  |     ACTION_SIGN_UP = 'sign_up' | ||||||
|  |     ACTION_PASSWORD_RESET = 'password_reset' | ||||||
|  |     ACTION_INVITE_USED = 'invite_used' | ||||||
|  |     ACTIONS = ( | ||||||
|  |         (ACTION_LOGIN, ACTION_LOGIN), | ||||||
|  |         (ACTION_LOGIN_FAILED, ACTION_LOGIN_FAILED), | ||||||
|  |         (ACTION_LOGOUT, ACTION_LOGOUT), | ||||||
|  |         (ACTION_AUTHORIZE_APPLICATION, ACTION_AUTHORIZE_APPLICATION), | ||||||
|  |         (ACTION_SUSPICIOUS_REQUEST, ACTION_SUSPICIOUS_REQUEST), | ||||||
|  |         (ACTION_SIGN_UP, ACTION_SIGN_UP), | ||||||
|  |         (ACTION_PASSWORD_RESET, ACTION_PASSWORD_RESET), | ||||||
|  |         (ACTION_INVITE_USED, ACTION_INVITE_USED), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL) |     user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL) | ||||||
|     action = models.TextField() |     action = models.TextField(choices=ACTIONS) | ||||||
|     date = models.DateTimeField(auto_now_add=True) |     date = models.DateTimeField(auto_now_add=True) | ||||||
|     app = models.TextField() |     app = models.TextField() | ||||||
|  |     _context = models.TextField() | ||||||
|  |     _context_cache = None | ||||||
|  |     request_ip = models.GenericIPAddressField() | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def context(self): | ||||||
|  |         """Load context data and load json""" | ||||||
|  |         if not self._context_cache: | ||||||
|  |             self._context_cache = loads(self._context) | ||||||
|  |         return self._context_cache | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def create(action, request, **kwargs): | ||||||
|  |         """Create AuditEntry from arguments""" | ||||||
|  |         client_ip, _ = get_client_ip(request) | ||||||
|  |         entry = AuditEntry.objects.create( | ||||||
|  |             action=action, | ||||||
|  |             user=request.user, | ||||||
|  |             # User 0.0.0.0 as fallback if IP cannot be determined | ||||||
|  |             request_ip=client_ip or '0.0.0.0', | ||||||
|  |             _context=dumps(kwargs)) | ||||||
|  |         LOGGER.debug("Logged %s from %s (%s)", action, request.user, client_ip) | ||||||
|  |         return entry | ||||||
|  |  | ||||||
|     def save(self, *args, **kwargs): |     def save(self, *args, **kwargs): | ||||||
|         if self.pk: |         if not self._state.adding: | ||||||
|             raise NotImplementedError("you may not edit an existing %s" % self._meta.model_name) |             raise ValidationError("you may not edit an existing %s" % self._meta.model_name) | ||||||
|         super().save(*args, **kwargs) |         super().save(*args, **kwargs) | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |  | ||||||
|  |         verbose_name = _('Audit Entry') | ||||||
|  |         verbose_name_plural = _('Audit Entries') | ||||||
|  | |||||||
							
								
								
									
										1
									
								
								passbook/audit/requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								passbook/audit/requirements.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | django-ipware | ||||||
							
								
								
									
										24
									
								
								passbook/audit/signals.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								passbook/audit/signals.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | |||||||
|  | """passbook audit signal listener""" | ||||||
|  | from django.contrib.auth.signals import (user_logged_in, user_logged_out, | ||||||
|  |                                          user_login_failed) | ||||||
|  | from django.dispatch import receiver | ||||||
|  |  | ||||||
|  | from passbook.audit.models import AuditEntry | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @receiver(user_logged_in) | ||||||
|  | def on_user_logged_in(sender, request, user, **kwargs): | ||||||
|  |     """Log successful login""" | ||||||
|  |     AuditEntry.create(AuditEntry.ACTION_LOGIN, request) | ||||||
|  |  | ||||||
|  | @receiver(user_logged_out) | ||||||
|  | def on_user_logged_out(sender, request, user, **kwargs): | ||||||
|  |     """Log successfully logout""" | ||||||
|  |     AuditEntry.create(AuditEntry.ACTION_LOGOUT, request) | ||||||
|  |  | ||||||
|  | @receiver(user_login_failed) | ||||||
|  | def on_user_login_failed(sender, request, user, **kwargs): | ||||||
|  |     """Log failed login attempt""" | ||||||
|  |     # TODO: Implement sumarizing of signals here for brute-force attempts | ||||||
|  |     # AuditEntry.create(AuditEntry.ACTION_LOGOUT, request) | ||||||
|  |     pass | ||||||
		Reference in New Issue
	
	Block a user
	 Jens Langhammer
					Jens Langhammer