User session tracking #93587

Merged
Oleg-Komarov merged 18 commits from user-session into main 2024-08-02 16:04:09 +02:00
5 changed files with 77 additions and 35 deletions
Showing only changes of commit d3a6bc2556 - Show all commits

View File

@ -259,35 +259,10 @@ def check_verification_payload(
return VerificationResult.OK, payload
def send_new_user_session(session):
if not hasattr(session, 'user'):
log.error('programming error: called for a session without a user')
return False
user = session.user
# sending only a text/plain email to reduce the room for look-alike phishing emails
email_body_txt, subject = construct_new_user_session(session)
email = user.email
try:
send_mail(
subject,
message=email_body_txt,
from_email=None, # just use the configured default From-address.
recipient_list=[email],
fail_silently=False,
)
except (smtplib.SMTPException, OSError):
log.exception("failed to send a new user session email for account %s", user.pk)
return False
log.info("sent a new user session email for account %s", user.pk)
return True
def construct_new_user_session(session):
def construct_new_user_session(user, session_data):
context = {
"session": session,
"user": session.user,
"session_data": session_data,
"user": user,
"subject": "Blender ID new sign-in",
}

View File

@ -6,9 +6,10 @@ from django.db.models import F
from django.db.models.signals import m2m_changed, post_delete
from django.dispatch import receiver
from . import email, models
from . import models
import bid_main.utils as utils
import bid_main.file_utils
import bid_main.tasks
log = logging.getLogger(__name__)
@ -35,11 +36,15 @@ def process_new_login(sender, request, user, **kwargs):
user.last_login_ip = F("current_login_ip")
user.current_login_ip = request_ip
fields.update({"last_login_ip", "current_login_ip"})
if user.has_confirmed_email:
Oleg-Komarov marked this conversation as resolved
Review

should be a call of a task

should be a call of a task
try:
email.send_new_user_session(user_session)
except Exception:
log.exception('failed to send a new user session email')
bid_main.tasks.send_new_user_session(
user_pk=user.pk,
session_data={
'device': str(user_session.device or 'Unknown'),
'ip': user_session.ip,
},
)
user.save(update_fields=fields)

28
bid_main/tasks.py Normal file
View File

@ -0,0 +1,28 @@
import logging
from background_task import background
from background_task.tasks import TaskSchedule
from django.core.mail import send_mail
from bid_main.models import User
import bid_main.email
log = logging.getLogger(__name__)
@background(schedule={'action': TaskSchedule.RESCHEDULE_EXISTING})
def send_new_user_session(user_pk, session_data):
user = User.objects.get(pk=user_pk)
log.info("sending a new user session email for account %s", user.pk)
# sending only a text/plain email to reduce the room for look-alike phishing emails
email_body_txt, subject = bid_main.email.construct_new_user_session(user, session_data)
email = user.email
send_mail(
subject=subject,
message=email_body_txt,
from_email=None, # just use the configured default From-address.
recipient_list=[email],
)

View File

@ -3,8 +3,8 @@ Dear {{ user.full_name|default:user.email }}!
A new sign-in for your Blender ID account {{ user.email }}
IP address: {{ session.ip }}
Device: {{ session.device }}
IP address: {{ session_data.ip }}
Device: {{ session_data.device }}
If this was you, you can ignore this message.
If this wasn't you, please change or reset your password.

View File

@ -1,8 +1,13 @@
from unittest.mock import patch
from django.core import mail
from django.test.client import Client
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
from bid_main.tests.factories import UserFactory
import bid_main.tasks
class TestUserSessions(TestCase):
@ -41,3 +46,32 @@ class TestActiveSessions(TestCase):
response = client1.get(reverse('bid_main:active_sessions'))
self.assertEqual(response.status_code, 302)
self.assertEqual(response['Location'], '/login?next=/active-sessions/')
class TestNewUserSessionEmail(TestCase):
@patch(
'bid_main.tasks.send_new_user_session',
new=bid_main.tasks.send_new_user_session.task_function,
)
@patch(
'django.contrib.auth.base_user.AbstractBaseUser.check_password',
new=lambda _, pwd: pwd == 'hunter2',
)
def test_new_user_session_email(self):
user = UserFactory(confirmed_email_at=timezone.now())
client1 = Client()
client1.force_login(user)
# force_login doesn't set custom env for HttpRequest, so sending a real POST instead
client2 = Client(REMOTE_ADDR='127.0.1.1')
ua = 'Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0'
response = client2.post(
'/login',
{'username': user.email, 'password': 'hunter2'},
headers={'user-agent': ua},
)
self.assertEqual(response.status_code, 302)
sent_email = mail.outbox.pop()
self.assertEqual(sent_email.to[0], user.email)
self.assertIn('IP address: 127.0.1.1', sent_email.body)
self.assertIn('Device: PC / Linux / Firefox 128.0', sent_email.body)