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.urls import reverse_lazy
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from qsessions.models import Session
import bid_main.file_utils import bid_main.file_utils
@ -408,3 +409,7 @@ class AccessTokenAdmin(admin.ModelAdmin):
list_filter = ("application", "scope") list_filter = ("application", "scope")
raw_id_fields = ("user", "source_refresh_token", "id_token") raw_id_fields = ("user", "source_refresh_token", "id_token")
search_fields = ("scope",) 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"> <div class="bid-roles container bid box mt-3">
<h2>Account</h2> <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"> <div class="btn-row-fluid mt-3">
<a class="btn" href="{% url 'bid_main:password_change' %}"> <a class="btn" href="{% url 'bid_main:password_change' %}">
<span>Change Password</span> <span>Change Password</span>

View File

@ -139,6 +139,12 @@ urlpatterns = [
registration_email.ConfirmEmailPollView.as_view(), registration_email.ConfirmEmailPollView.as_view(),
name="confirm-email-poll", 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: # 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.utils.translation import gettext as _
from django.db import transaction, IntegrityError from django.db import transaction, IntegrityError
from django.db.models import Count from django.db.models import Count
from django.http import HttpResponseRedirect from django.http import HttpResponseNotFound, HttpResponseRedirect
from django.shortcuts import resolve_url, render from django.shortcuts import redirect, resolve_url, render
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.utils.decorators import method_decorator 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.csrf import csrf_exempt
from django.views.decorators.debug import sensitive_post_parameters from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import TemplateView, FormView from django.views.generic import TemplateView, FormView
from django.views.generic.base import View
from django.views.generic.edit import UpdateView from django.views.generic.edit import UpdateView
import loginas.utils import loginas.utils
import oauth2_provider.models as oauth2_models import oauth2_provider.models as oauth2_models
@ -31,6 +32,7 @@ from .. import forms, email
from . import mixins from . import mixins
from bid_main.email import send_verify_address from bid_main.email import send_verify_address
import bid_main.file_utils import bid_main.file_utils
import bid_main.sessions
User = get_user_model() User = get_user_model()
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -429,3 +431,28 @@ class DeleteUserView(
if not ok: if not ok:
log.error("Failed to send an email about deletion of account %s", user.pk) 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) 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.admindocs",
"django.contrib.auth", "django.contrib.auth",
"django.contrib.contenttypes", "django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages", "django.contrib.messages",
"django.contrib.staticfiles", "django.contrib.staticfiles",
"django.contrib.sites", "django.contrib.sites",
"django.contrib.flatpages", "django.contrib.flatpages",
"django.contrib.humanize",
"oauth2_provider", "oauth2_provider",
"pipeline", "pipeline",
"sorl.thumbnail", "sorl.thumbnail",
"django_admin_select2", "django_admin_select2",
"loginas", "loginas",
"qsessions",
"bid_main", "bid_main",
"bid_api", "bid_api",
"bid_addon_support", "bid_addon_support",
@ -61,7 +62,7 @@ INSTALLED_APPS = [
MIDDLEWARE = [ MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware", "qsessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware", "django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware", "django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware",
@ -75,6 +76,10 @@ AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend", "django.contrib.auth.backends.ModelBackend",
] ]
SESSION_ENGINE = "qsessions.backends.db"
GEOIP_PATH = os.getenv('GEOIP_PATH')
ROOT_URLCONF = "blenderid.urls" ROOT_URLCONF = "blenderid.urls"
TEMPLATES = [ 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-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-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-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==4.2.13 ; python_version >= "3.8" and python_version < "4"
django[bcrypt]==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" 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" htmlmin==0.1.12 ; python_version >= "3.8" and python_version < "4"
idna==2.8 ; 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" importlib-metadata==3.6.0 ; python_version >= "3.8" and python_version < "4"