Audit: implement logging of basic events like login, logout, failed login
This commit is contained in:
parent
633660c602
commit
a63f2be999
|
@ -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')
|
||||||
|
|
|
@ -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')]),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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')
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
django-ipware
|
|
@ -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