2018-11-23 16:05:41 +00:00
|
|
|
"""passbook audit models"""
|
2018-12-19 09:16:52 +00:00
|
|
|
from datetime import timedelta
|
2018-12-10 12:51:16 +00:00
|
|
|
from logging import getLogger
|
2018-11-23 16:05:41 +00:00
|
|
|
|
|
|
|
from django.conf import settings
|
2018-12-10 14:26:28 +00:00
|
|
|
from django.contrib.auth.models import AnonymousUser
|
2019-02-21 15:06:57 +00:00
|
|
|
from django.contrib.postgres.fields import JSONField
|
2018-12-10 12:47:51 +00:00
|
|
|
from django.core.exceptions import ValidationError
|
2018-11-23 16:05:41 +00:00
|
|
|
from django.db import models
|
2018-12-19 09:16:52 +00:00
|
|
|
from django.utils import timezone
|
2018-12-10 12:47:51 +00:00
|
|
|
from django.utils.translation import gettext as _
|
|
|
|
from ipware import get_client_ip
|
2018-11-23 16:05:41 +00:00
|
|
|
|
2018-12-18 14:35:23 +00:00
|
|
|
from passbook.lib.models import CreatedUpdatedModel, UUIDModel
|
2018-11-23 16:05:41 +00:00
|
|
|
|
2018-12-10 12:47:51 +00:00
|
|
|
LOGGER = getLogger(__name__)
|
2018-11-23 16:05:41 +00:00
|
|
|
|
|
|
|
class AuditEntry(UUIDModel):
|
|
|
|
"""An individual audit log entry"""
|
|
|
|
|
2018-12-10 12:47:51 +00:00
|
|
|
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'
|
2018-12-10 13:26:10 +00:00
|
|
|
ACTION_PASSWORD_RESET = 'password_reset' # noqa
|
2018-12-10 13:21:42 +00:00
|
|
|
ACTION_INVITE_CREATED = 'invitation_created'
|
|
|
|
ACTION_INVITE_USED = 'invitation_used'
|
2018-12-10 12:47:51 +00:00
|
|
|
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),
|
2018-12-10 13:05:27 +00:00
|
|
|
(ACTION_INVITE_CREATED, ACTION_INVITE_CREATED),
|
2018-12-10 12:47:51 +00:00
|
|
|
(ACTION_INVITE_USED, ACTION_INVITE_USED),
|
|
|
|
)
|
|
|
|
|
2018-11-23 16:05:41 +00:00
|
|
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL)
|
2018-12-10 12:47:51 +00:00
|
|
|
action = models.TextField(choices=ACTIONS)
|
2018-11-23 16:05:41 +00:00
|
|
|
date = models.DateTimeField(auto_now_add=True)
|
|
|
|
app = models.TextField()
|
2019-02-21 15:06:57 +00:00
|
|
|
context = JSONField(default=dict, blank=True)
|
2018-12-10 12:47:51 +00:00
|
|
|
request_ip = models.GenericIPAddressField()
|
2018-12-13 17:01:45 +00:00
|
|
|
created = models.DateTimeField(auto_now_add=True)
|
2018-12-10 12:47:51 +00:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def create(action, request, **kwargs):
|
|
|
|
"""Create AuditEntry from arguments"""
|
|
|
|
client_ip, _ = get_client_ip(request)
|
2018-12-10 14:26:28 +00:00
|
|
|
user = request.user
|
|
|
|
if isinstance(user, AnonymousUser):
|
|
|
|
user = kwargs.get('user', None)
|
2018-12-10 12:47:51 +00:00
|
|
|
entry = AuditEntry.objects.create(
|
|
|
|
action=action,
|
2018-12-10 14:26:28 +00:00
|
|
|
user=user,
|
2018-12-10 13:26:10 +00:00
|
|
|
# User 255.255.255.255 as fallback if IP cannot be determined
|
|
|
|
request_ip=client_ip or '255.255.255.255',
|
2019-02-21 15:06:57 +00:00
|
|
|
context=kwargs)
|
2018-12-10 12:47:51 +00:00
|
|
|
LOGGER.debug("Logged %s from %s (%s)", action, request.user, client_ip)
|
|
|
|
return entry
|
2018-11-23 16:05:41 +00:00
|
|
|
|
|
|
|
def save(self, *args, **kwargs):
|
2018-12-10 12:47:51 +00:00
|
|
|
if not self._state.adding:
|
|
|
|
raise ValidationError("you may not edit an existing %s" % self._meta.model_name)
|
2018-11-23 16:05:41 +00:00
|
|
|
super().save(*args, **kwargs)
|
2018-12-10 12:47:51 +00:00
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
|
|
|
verbose_name = _('Audit Entry')
|
|
|
|
verbose_name_plural = _('Audit Entries')
|
2018-12-18 14:35:23 +00:00
|
|
|
|
|
|
|
|
|
|
|
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"""
|
2019-02-25 13:10:29 +00:00
|
|
|
if not target_uid:
|
|
|
|
return
|
2018-12-18 14:35:23 +00:00
|
|
|
client_ip, _ = get_client_ip(request)
|
|
|
|
# Since we can only use 254 chars for target_uid, truncate target_uid.
|
|
|
|
target_uid = target_uid[:254]
|
2018-12-19 09:16:52 +00:00
|
|
|
time_threshold = timezone.now() - timedelta(minutes=10)
|
2018-12-18 14:35:23 +00:00
|
|
|
existing_attempts = LoginAttempt.objects.filter(
|
|
|
|
target_uid=target_uid,
|
2018-12-19 09:16:52 +00:00
|
|
|
request_ip=client_ip,
|
|
|
|
last_updated__gt=time_threshold).order_by('created')
|
2018-12-18 14:35:23 +00:00
|
|
|
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'),)
|