WIP: active-sessions #93586

Closed
Oleg-Komarov wants to merge 3 commits from active-sessions into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
8 changed files with 107 additions and 4 deletions
Showing only changes of commit f6cdaa33e7 - Show all commits

View File

@ -9,6 +9,7 @@ from django.contrib.contenttypes.models import ContentType
from django.urls import reverse_lazy
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from qsessions.models import Session
import bid_main.file_utils
@ -408,3 +409,7 @@ class AccessTokenAdmin(admin.ModelAdmin):
list_filter = ("application", "scope")
raw_id_fields = ("user", "source_refresh_token", "id_token")
search_fields = ("scope",)
# avoid exposing session_key values in admin
admin.site.unregister(Session)

5
bid_main/sessions.py Normal file
View File

@ -0,0 +1,5 @@
import hashlib
def get_hashed_key(session):
return hashlib.sha256(session.session_key.encode('utf-8')).hexdigest()

View File

@ -0,0 +1,48 @@
{% extends 'layout.html' %}
{% load humanize pipeline static %}
{% block page_title %}
Active Sessions
{% endblock %}
{% block body %}
<div class="bid box">
<h2>Active Sessions</h2>
<table class="table w-100 mt-4">
<thead>
<tr>
<th>Created</th>
<th>Location</th>
<th>Device</th>
<th class="text-center"></th>
</tr>
</thead>
<tbody>
{% for session in sessions %}
<tr>
<td title="{{ session.created_at }}">{{ session.created_at|naturaltime }}</td>
<td>
{% if session.location %}
{{ session.location }}
{% else %}
Not detected
{% endif %}
({{ session.ip }})
</td>
<td>{{ session.device }}</td>
<td class="text-center">
{% if session.is_current %}
Current Session
{% else %}
<form action="{% url 'bid_main:terminate_session' %}" method="post">
{% csrf_token %}
<input type="hidden" name="session_key_hashed" value="{{ session.session_key_hashed }}" />
<button type="submit" class="btn-danger">Terminate Session</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -135,6 +135,11 @@ Profile
<div class="bid-roles container bid box mt-3">
<h2>Account</h2>
<div class="btn-row-fluid mt-3">
<a class="btn" href="{% url 'bid_main:active_sessions' %}">
<span>Active Sessions</span>
</a>
</div>
<div class="btn-row-fluid mt-3">
<a class="btn" href="{% url 'bid_main:password_change' %}">
<span>Change Password</span>

View File

@ -139,6 +139,12 @@ urlpatterns = [
registration_email.ConfirmEmailPollView.as_view(),
name="confirm-email-poll",
),
path('active-sessions/', normal_pages.ActiveSessionsView.as_view(), name='active_sessions'),
path(
'terminate-session/',
normal_pages.TerminateSessionView.as_view(),
name='terminate_session',
),
]
# Only enable this on a dev server:

View File

@ -14,8 +14,8 @@ from django.core.exceptions import ValidationError
from django.utils.translation import gettext as _
from django.db import transaction, IntegrityError
from django.db.models import Count
from django.http import HttpResponseRedirect
from django.shortcuts import resolve_url, render
from django.http import HttpResponseNotFound, HttpResponseRedirect
from django.shortcuts import redirect, resolve_url, render
from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.decorators import method_decorator
@ -23,6 +23,7 @@ from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import TemplateView, FormView
from django.views.generic.base import View
from django.views.generic.edit import UpdateView
import loginas.utils
import oauth2_provider.models as oauth2_models
@ -31,6 +32,7 @@ from .. import forms, email
from . import mixins
from bid_main.email import send_verify_address
import bid_main.file_utils
import bid_main.sessions
User = get_user_model()
log = logging.getLogger(__name__)
@ -429,3 +431,28 @@ class DeleteUserView(
if not ok:
log.error("Failed to send an email about deletion of account %s", user.pk)
return render(self.request, "bid_main/delete_user/confirm.html", context=ctx)
class ActiveSessionsView(LoginRequiredMixin, TemplateView):
template_name = "bid_main/active_sessions.html"
def get_context_data(self, **kwargs):
sessions = self.request.user.session_set.order_by('-created_at')
for session in sessions:
session.session_key_hashed = bid_main.sessions.get_hashed_key(session)
if session.session_key == self.request.session.session_key:
session.is_current = True
return {
**super().get_context_data(**kwargs),
'sessions': sessions,
}
class TerminateSessionView(LoginRequiredMixin, View):
def post(self, request):
session_key_hashed = request.POST.get('session_key_hashed')
for session in self.request.user.session_set.all():
if session_key_hashed == bid_main.sessions.get_hashed_key(session):
session.delete()
return redirect('bid_main:active_sessions')
return HttpResponseNotFound("session not found")

View File

@ -43,16 +43,17 @@ INSTALLED_APPS = [
"django.contrib.admindocs",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django.contrib.sites",
"django.contrib.flatpages",
"django.contrib.humanize",
"oauth2_provider",
"pipeline",
"sorl.thumbnail",
"django_admin_select2",
"loginas",
"qsessions",
"bid_main",
"bid_api",
"bid_addon_support",
@ -61,7 +62,7 @@ INSTALLED_APPS = [
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"qsessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
@ -75,6 +76,10 @@ AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
]
SESSION_ENGINE = "qsessions.backends.db"
GEOIP_PATH = os.getenv('GEOIP_PATH')
ROOT_URLCONF = "blenderid.urls"
TEMPLATES = [

View File

@ -15,9 +15,11 @@ django-compat==1.0.15 ; python_version >= "3.8" and python_version < "4"
django-loginas==0.3.11 ; python_version >= "3.8" and python_version < "4"
django-oauth-toolkit @ git+https://projects.blender.org/Oleg-Komarov/django-oauth-toolkit.git@0b056a99ca943771615b859f48aaff0e12357f22 ; python_version >= "3.8" and python_version < "4"
django-pipeline==3.1.0 ; python_version >= "3.8" and python_version < "4"
django-qsessions==1.1.5
django==4.2.13 ; python_version >= "3.8" and python_version < "4"
django[bcrypt]==4.2.13 ; python_version >= "3.8" and python_version < "4"
docutils==0.14 ; python_version >= "3.8" and python_version < "4"
geoip2==4.8.0
htmlmin==0.1.12 ; python_version >= "3.8" and python_version < "4"
idna==2.8 ; python_version >= "3.8" and python_version < "4"
importlib-metadata==3.6.0 ; python_version >= "3.8" and python_version < "4"