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 69 additions and 3 deletions
Showing only changes of commit 6bc5d1eccd - Show all commits

View File

@ -257,3 +257,42 @@ def check_verification_payload(
log.debug("verification OK") log.debug("verification OK")
return VerificationResult.OK, 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):
Oleg-Komarov marked this conversation as resolved Outdated

might be better to make this a background task instead of wrapping into except.

might be better to make this a background task instead of wrapping into except.
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):
context = {
"session": session,
"user": session.user,
"subject": "Blender ID new sign-in",
}
email_body_txt = loader.render_to_string(
"bid_main/emails/new_user_session.txt", context
)
return email_body_txt, context["subject"]

View File

@ -19,6 +19,7 @@ from django.utils import timezone
from django.utils.deconstruct import deconstructible from django.utils.deconstruct import deconstructible
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
import oauth2_provider.models as oa2_models import oauth2_provider.models as oa2_models
import user_agents
from . import fields from . import fields
from . import hashers from . import hashers
@ -682,3 +683,10 @@ class UserSession(models.Model):
def __str__(self): def __str__(self):
return f'UserSession pk={self.pk} for {self.user}' return f'UserSession pk={self.pk} for {self.user}'
@property
def device(self):
if self.user_agent:
return user_agents.parse(self.user_agent)
else:
return None
Oleg-Komarov marked this conversation as resolved Outdated

superfluous else

superfluous else

View File

@ -6,7 +6,7 @@ from django.db.models import F
from django.db.models.signals import m2m_changed, post_delete from django.db.models.signals import m2m_changed, post_delete
from django.dispatch import receiver from django.dispatch import receiver
from . import models from . import email, models
import bid_main.utils as utils import bid_main.utils as utils
import bid_main.file_utils import bid_main.file_utils
@ -20,7 +20,7 @@ def log_exception(sender, **kwargs):
@receiver(user_logged_in) @receiver(user_logged_in)
def process_new_login(sender, request, user, **kwargs): def process_new_login(sender, request, user, **kwargs):
"""Updates user fields upon login. """Updates user fields and creates a UserSession upon login. Sends and email if IP is new.
Only saves specific fields, so that the webhook trigger knows what changed. Only saves specific fields, so that the webhook trigger knows what changed.
""" """
@ -33,8 +33,11 @@ def process_new_login(sender, request, user, **kwargs):
if request_ip and user.current_login_ip != request_ip: if request_ip and user.current_login_ip != request_ip:
user.last_login_ip = F("current_login_ip") user.last_login_ip = F("current_login_ip")
user.current_login_ip = request_ip user.current_login_ip = request_ip
fields.update({"last_login_ip", "current_login_ip"}) fields.update({"last_login_ip", "current_login_ip"})
try:
email.send_new_user_session(request.session.create_model_instance({}))
except Exception:
log.exception('failed to send a new user session email')
Oleg-Komarov marked this conversation as resolved
Review

should be a call of a task

should be a call of a task
user.save(update_fields=fields) user.save(update_fields=fields)
models.UserSession.update_or_create_from_request(request, user) models.UserSession.update_or_create_from_request(request, user)

View File

@ -0,0 +1,15 @@
{% autoescape off %}
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 }}
If this was you, you can ignore this message.
If this wasn't you, please change or reset your password.
--
Kind regards,
The Blender Web Team
{% endautoescape %}

View File

@ -51,6 +51,7 @@ sorl-thumbnail==12.7.0 ; python_version >= "3.8" and python_version < "4"
sqlparse==0.5.0 ; python_version >= "3.8" and python_version < "4" sqlparse==0.5.0 ; python_version >= "3.8" and python_version < "4"
tornado==6.0.3 ; python_version >= "3.8" and python_version < "4" tornado==6.0.3 ; python_version >= "3.8" and python_version < "4"
urllib3==1.25.11 ; python_version >= "3.8" and python_version < "4" urllib3==1.25.11 ; python_version >= "3.8" and python_version < "4"
user-agents==2.2.0
uwsgi==2.0.23 uwsgi==2.0.23
wrapt==1.15.0 ; python_version >= "3.8" and python_version < "4" wrapt==1.15.0 ; python_version >= "3.8" and python_version < "4"
zipp==0.6.0 ; python_version >= "3.8" and python_version < "4" zipp==0.6.0 ; python_version >= "3.8" and python_version < "4"