Audit: implement logging of basic events like login, logout, failed login

This commit is contained in:
Jens Langhammer 2018-12-10 13:47:51 +01:00
parent 633660c602
commit a63f2be999
7 changed files with 148 additions and 3 deletions

5
passbook/audit/admin.py Normal file
View File

@ -0,0 +1,5 @@
"""passbook audit model admin"""
from passbook.lib.admin import admin_autoregister
admin_autoregister('passbook_audit')

View File

@ -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')

View 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')]),
),
]

View 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')]),
),
]

View File

@ -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')

View File

@ -0,0 +1 @@
django-ipware

24
passbook/audit/signals.py Normal file
View 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