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"""
 | 
			
		||||
from importlib import import_module
 | 
			
		||||
 | 
			
		||||
from django.apps import AppConfig
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -8,3 +10,6 @@ class PassbookAuditConfig(AppConfig):
 | 
			
		||||
    name = 'passbook.audit'
 | 
			
		||||
    label = 'passbook_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"""
 | 
			
		||||
from logging import getLogger
 | 
			
		||||
from json import dumps, loads
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.core.exceptions import ValidationError
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.utils.translation import gettext as _
 | 
			
		||||
from ipware import get_client_ip
 | 
			
		||||
from reversion import register
 | 
			
		||||
 | 
			
		||||
from passbook.lib.models import UUIDModel
 | 
			
		||||
 | 
			
		||||
LOGGER = getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
@register()
 | 
			
		||||
class AuditEntry(UUIDModel):
 | 
			
		||||
    """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)
 | 
			
		||||
    action = models.TextField()
 | 
			
		||||
    action = models.TextField(choices=ACTIONS)
 | 
			
		||||
    date = models.DateTimeField(auto_now_add=True)
 | 
			
		||||
    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):
 | 
			
		||||
        if self.pk:
 | 
			
		||||
            raise NotImplementedError("you may not edit an existing %s" % self._meta.model_name)
 | 
			
		||||
        if not self._state.adding:
 | 
			
		||||
            raise ValidationError("you may not edit an existing %s" % self._meta.model_name)
 | 
			
		||||
        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