audit: add basic login attempt tracking
This commit is contained in:
parent
0c53a95b06
commit
2e4a0297a4
28
passbook/audit/migrations/0006_auto_20181218_1252.py
Normal file
28
passbook/audit/migrations/0006_auto_20181218_1252.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# Generated by Django 2.1.4 on 2018-12-18 12:52
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('passbook_audit', '0005_auditentry_created'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='LoginAttempt',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', models.DateField(auto_now_add=True)),
|
||||||
|
('last_updated', models.DateTimeField(auto_now=True)),
|
||||||
|
('target_uid', models.CharField(max_length=254)),
|
||||||
|
('request_ip', models.GenericIPAddressField()),
|
||||||
|
('attempts', models.IntegerField(default=1)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='loginattempt',
|
||||||
|
unique_together={('target_uid', 'request_ip', 'created')},
|
||||||
|
),
|
||||||
|
]
|
|
@ -10,7 +10,7 @@ from django.utils.translation import gettext as _
|
||||||
from ipware import get_client_ip
|
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 CreatedUpdatedModel, UUIDModel
|
||||||
|
|
||||||
LOGGER = getLogger(__name__)
|
LOGGER = getLogger(__name__)
|
||||||
|
|
||||||
|
@ -80,3 +80,40 @@ class AuditEntry(UUIDModel):
|
||||||
|
|
||||||
verbose_name = _('Audit Entry')
|
verbose_name = _('Audit Entry')
|
||||||
verbose_name_plural = _('Audit Entries')
|
verbose_name_plural = _('Audit Entries')
|
||||||
|
|
||||||
|
|
||||||
|
class LoginAttempt(CreatedUpdatedModel):
|
||||||
|
"""Track failed login-attempts"""
|
||||||
|
|
||||||
|
target_uid = models.CharField(max_length=254)
|
||||||
|
request_ip = models.GenericIPAddressField()
|
||||||
|
attempts = models.IntegerField(default=1)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def attempt(target_uid, request):
|
||||||
|
"""Helper function to create attempt or count up existing one"""
|
||||||
|
client_ip, _ = get_client_ip(request)
|
||||||
|
# Since we can only use 254 chars for target_uid, truncate target_uid.
|
||||||
|
target_uid = target_uid[:254]
|
||||||
|
existing_attempts = LoginAttempt.objects.filter(
|
||||||
|
target_uid=target_uid,
|
||||||
|
request_ip=client_ip).order_by('created')
|
||||||
|
# TODO: Add logic to group attempts by timeframe, i.e. within 10 minutes
|
||||||
|
if existing_attempts.exists():
|
||||||
|
attempt = existing_attempts.first()
|
||||||
|
attempt.attempts += 1
|
||||||
|
attempt.save()
|
||||||
|
LOGGER.debug("Increased attempts on %s", attempt)
|
||||||
|
else:
|
||||||
|
attempt = LoginAttempt.objects.create(
|
||||||
|
target_uid=target_uid,
|
||||||
|
request_ip=client_ip)
|
||||||
|
LOGGER.debug("Created new attempt %s", attempt)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "LoginAttempt to %s from %s (x%d)" % (self.target_uid,
|
||||||
|
self.request_ip, self.attempts)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
|
||||||
|
unique_together = (('target_uid', 'request_ip', 'created'),)
|
||||||
|
|
|
@ -3,7 +3,7 @@ from django.contrib.auth.signals import (user_logged_in, user_logged_out,
|
||||||
user_login_failed)
|
user_login_failed)
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
from passbook.audit.models import AuditEntry
|
from passbook.audit.models import AuditEntry, LoginAttempt
|
||||||
from passbook.core.signals import (invitation_created, invitation_used,
|
from passbook.core.signals import (invitation_created, invitation_used,
|
||||||
user_signed_up)
|
user_signed_up)
|
||||||
|
|
||||||
|
@ -36,8 +36,6 @@ def on_invitation_used(sender, request, invitation, **kwargs):
|
||||||
invitation_uuid=invitation.uuid.hex)
|
invitation_uuid=invitation.uuid.hex)
|
||||||
|
|
||||||
@receiver(user_login_failed)
|
@receiver(user_login_failed)
|
||||||
def on_user_login_failed(sender, request, **kwargs):
|
def on_user_login_failed(sender, request, credentials, **kwargs):
|
||||||
"""Log failed login attempt"""
|
"""Log failed login attempt"""
|
||||||
# TODO: Implement sumarizing of signals here for brute-force attempts
|
LoginAttempt.attempt(target_uid=credentials.get('username'), request=request)
|
||||||
# AuditEntry.create(AuditEntry.ACTION_LOGOUT, request)
|
|
||||||
pass
|
|
||||||
|
|
Reference in a new issue