Skip to content

Commit 70cc637

Browse files
authored
Merge pull request #73 from maykinmedia/feature/46-admin-sessions
Feature/46 admin sessions
2 parents 896bd40 + b631bbf commit 70cc637

File tree

7 files changed

+276
-1
lines changed

7 files changed

+276
-1
lines changed

open_api_framework/admin.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from django.contrib import admin
2+
3+
from sessionprofile.models import SessionProfile
4+
5+
from open_api_framework.utils import get_session_store
6+
7+
8+
@admin.register(SessionProfile)
9+
class SessionProfileAdmin(admin.ModelAdmin):
10+
list_display = ["session_key", "user", "exists"]
11+
12+
@property
13+
def SessionStore(self):
14+
15+
return get_session_store()
16+
17+
def get_queryset(self, request):
18+
qs = super().get_queryset(request)
19+
return qs.filter(user=request.user)
20+
21+
def has_add_permission(self, request, obj=None):
22+
return False
23+
24+
def has_change_permission(self, request, obj=None):
25+
return False
26+
27+
@admin.display(boolean=True)
28+
def exists(self, obj):
29+
return self.SessionStore().exists(obj.session_key)
30+
31+
def delete_model(self, request, obj):
32+
self.SessionStore(obj.session_key).flush()
33+
super().delete_model(request, obj)
34+
35+
def delete_queryset(self, request, queryset):
36+
37+
for session_profile in queryset.iterator():
38+
self.SessionStore(session_profile.session_key).flush()
39+
40+
super().delete_queryset(request, queryset)

open_api_framework/conf/base.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,19 +229,20 @@
229229
"mozilla_django_oidc_db",
230230
"log_outgoing_requests",
231231
"django_setup_configuration",
232+
"sessionprofile",
232233
"open_api_framework",
233234
PROJECT_DIRNAME,
234235
]
235236

236237
MIDDLEWARE = [
237238
"django.middleware.security.SecurityMiddleware",
239+
"sessionprofile.middleware.SessionProfileMiddleware",
238240
"django.contrib.sessions.middleware.SessionMiddleware",
239241
"corsheaders.middleware.CorsMiddleware",
240242
"django.middleware.common.CommonMiddleware",
241243
"django.middleware.csrf.CsrfViewMiddleware",
242244
"django.contrib.auth.middleware.AuthenticationMiddleware",
243245
"maykin_2fa.middleware.OTPMiddleware",
244-
"mozilla_django_oidc_db.middleware.SessionRefresh",
245246
"django.contrib.messages.middleware.MessageMiddleware",
246247
"django.middleware.clickjacking.XFrameOptionsMiddleware",
247248
"axes.middleware.AxesMiddleware",
@@ -535,6 +536,12 @@
535536

536537
SESSION_COOKIE_NAME = f"{PROJECT_DIRNAME}_sessionid"
537538
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
539+
SESSION_COOKIE_AGE = config(
540+
"SESSION_COOKIE_AGE",
541+
default=1209600,
542+
help_text="For how long, in seconds, the session cookie will be valid.",
543+
)
544+
538545

539546
LOGIN_URL = reverse_lazy("admin:login")
540547
LOGIN_REDIRECT_URL = reverse_lazy("admin:index")

open_api_framework/utils.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from importlib import import_module
2+
3+
from django.conf import settings
4+
from django.contrib.sessions.backends.base import SessionBase
5+
6+
7+
def get_session_store() -> SessionBase:
8+
return import_module(settings.SESSION_ENGINE).SessionStore

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ dependencies = [
5656
"flower>=2.0.1",
5757
"maykin-2fa>=1.0.1",
5858
"django-setup-configuration>=0.1.0",
59+
"django-sessionprofile>=3.0.0",
5960
]
6061

6162
[project.urls]
@@ -72,6 +73,7 @@ tests = [
7273
"isort",
7374
"black",
7475
"flake8",
76+
"factory-boy",
7577
]
7678
coverage = [
7779
"pytest-cov",

testapp/settings.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,13 @@
4444
"django.contrib.messages",
4545
"django.contrib.admin",
4646
"open_api_framework",
47+
"sessionprofile",
4748
"testapp",
4849
]
4950

5051
MIDDLEWARE = [
5152
"django.middleware.security.SecurityMiddleware",
53+
"sessionprofile.middleware.SessionProfileMiddleware",
5254
"django.contrib.sessions.middleware.SessionMiddleware",
5355
"django.middleware.common.CommonMiddleware",
5456
"django.middleware.csrf.CsrfViewMiddleware",
@@ -78,3 +80,5 @@
7880
# These are excluded from generate_envvar_docs test by their group
7981
VARIABLE_TO_BE_EXCLUDED = config("VARIABLE_TO_BE_EXCLUDED1", "foo", group="Excluded")
8082
VARIABLE_TO_BE_EXCLUDED = config("VARIABLE_TO_BE_EXCLUDED2", "bar", group="Excluded")
83+
84+
SESSION_ENGINE = "django.contrib.sessions.backends.db"

tests/factories.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import factory.fuzzy
2+
from sessionprofile.models import SessionProfile
3+
4+
from open_api_framework.utils import get_session_store
5+
6+
7+
class SessionProfileFactory(factory.django.DjangoModelFactory):
8+
9+
session_key = factory.fuzzy.FuzzyText(length=40)
10+
11+
class Meta:
12+
model = SessionProfile
13+
14+
@factory.post_generation
15+
def session(self, create, extracted, **kwargs):
16+
SessionStore = get_session_store()
17+
SessionStore(self.session_key).save(True)

tests/test_admin.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
from django.contrib.sessions.backends.cache import SessionStore as CachedSessionStore
2+
from django.contrib.sessions.backends.db import SessionStore as DBSessionStore
3+
from django.contrib.sessions.models import Session
4+
from django.test import override_settings
5+
from django.urls import reverse
6+
7+
import pytest
8+
from sessionprofile.models import SessionProfile
9+
10+
from open_api_framework.utils import get_session_store
11+
12+
from .factories import SessionProfileFactory
13+
14+
15+
@pytest.fixture
16+
def session_changelist_url():
17+
return reverse("admin:sessionprofile_sessionprofile_changelist")
18+
19+
20+
def test_session_profile_sanity(client, admin_user, session_changelist_url):
21+
22+
client.force_login(admin_user)
23+
response = client.get(session_changelist_url)
24+
assert response.status_code == 200
25+
26+
assert SessionProfile.objects.count() == 1
27+
28+
session = SessionProfile.objects.get()
29+
assert client.session.session_key == session.session_key
30+
31+
32+
def test_only_session_profile_of_user_shown(
33+
client, admin_user, django_user_model, session_changelist_url
34+
):
35+
36+
other_admin = django_user_model.objects.create_superuser("garry")
37+
38+
client.force_login(other_admin)
39+
response = client.get(session_changelist_url)
40+
assert response.status_code == 200
41+
42+
client.force_login(admin_user)
43+
response = client.get(session_changelist_url)
44+
assert response.status_code == 200
45+
46+
# two sessions, one for each user
47+
assert SessionProfile.objects.count() == 2
48+
49+
# Session created after response, needs to be called again
50+
response = client.get(session_changelist_url)
51+
52+
admin_user_session = SessionProfile.objects.get(user=admin_user)
53+
assert admin_user_session.session_key in response.content.decode()
54+
55+
other_user_session = SessionProfile.objects.get(user=other_admin)
56+
assert other_user_session.session_key not in response.content.decode()
57+
58+
# should only be able to access own page
59+
change_url = reverse(
60+
"admin:sessionprofile_sessionprofile_change",
61+
args=[admin_user_session.session_key],
62+
)
63+
response = client.get(change_url)
64+
assert response.status_code == 200
65+
66+
change_url = reverse(
67+
"admin:sessionprofile_sessionprofile_change",
68+
args=[other_user_session.session_key],
69+
)
70+
response = client.get(change_url)
71+
assert response.status_code == 302
72+
assert response.url == reverse("admin:index")
73+
74+
75+
def test_cant_delete_other_users_session(client, admin_user, django_user_model):
76+
client.force_login(admin_user)
77+
78+
other_admin = django_user_model.objects.create_superuser("garry")
79+
80+
other_user_session = SessionProfileFactory(user=other_admin)
81+
82+
delete_url = reverse(
83+
"admin:sessionprofile_sessionprofile_delete",
84+
args=[other_user_session.session_key],
85+
)
86+
87+
response = client.post(delete_url, {"post": "yes"})
88+
assert response.status_code == 302
89+
90+
SessionStore = get_session_store()
91+
92+
assert SessionStore().exists(other_user_session.session_key)
93+
94+
95+
def test_delete_with_session_db_backend(client, admin_user, session_changelist_url):
96+
client.force_login(admin_user)
97+
98+
session = SessionProfileFactory(user=admin_user)
99+
100+
assert SessionProfile.objects.count() == 1
101+
# sesison created by login
102+
assert Session.objects.count() == 2
103+
assert DBSessionStore().exists(session.session_key)
104+
105+
url = reverse("admin:sessionprofile_sessionprofile_delete", args=[session.pk])
106+
107+
response = client.post(url, {"post": "yes"})
108+
assert response.status_code == 302
109+
110+
# new session saved upon request
111+
assert SessionProfile.objects.count() == 1
112+
assert SessionProfile.objects.count() != session
113+
assert Session.objects.count() == 1
114+
assert not DBSessionStore().exists(session.session_key)
115+
116+
117+
@override_settings(SESSION_ENGINE="django.contrib.sessions.backends.cache")
118+
def test_delete_with_session_cache_backend(client, admin_user, session_changelist_url):
119+
client.force_login(admin_user)
120+
121+
session = SessionProfileFactory(user=admin_user)
122+
123+
assert SessionProfile.objects.count() == 1
124+
assert Session.objects.count() == 0
125+
assert CachedSessionStore().exists(session.session_key)
126+
127+
url = reverse("admin:sessionprofile_sessionprofile_delete", args=[session.pk])
128+
129+
response = client.post(url, {"post": "yes"})
130+
assert response.status_code == 302
131+
132+
# new session saved upon request
133+
assert SessionProfile.objects.count() == 1
134+
assert SessionProfile.objects.count() != session
135+
assert Session.objects.count() == 0
136+
assert not CachedSessionStore().exists(session.session_key)
137+
138+
139+
def test_delete_action_with_session_db_backend(
140+
client, admin_user, session_changelist_url
141+
):
142+
client.force_login(admin_user)
143+
sessions = SessionProfileFactory.create_batch(5, user=admin_user)
144+
145+
# one created from user login
146+
assert Session.objects.count() == 6
147+
assert SessionProfile.objects.count() == 5
148+
149+
session_keys = [session.session_key for session in sessions]
150+
for session_key in session_keys:
151+
assert DBSessionStore().exists(session_key)
152+
153+
response = client.post(
154+
session_changelist_url,
155+
{"action": "delete_selected", "_selected_action": session_keys, "post": "yes"},
156+
)
157+
assert response.status_code == 302
158+
159+
# one is created as the post request is sent
160+
assert SessionProfile.objects.count() == 1
161+
assert Session.objects.count() == 1
162+
163+
for session_key in session_keys:
164+
assert not DBSessionStore().exists(session_key)
165+
166+
167+
@override_settings(SESSION_ENGINE="django.contrib.sessions.backends.cache")
168+
def test_delete_action_with_session_cache_backend(
169+
client, admin_user, session_changelist_url
170+
):
171+
172+
client.force_login(admin_user)
173+
sessions = SessionProfileFactory.create_batch(5, user=admin_user)
174+
175+
# no db sessions are created
176+
assert Session.objects.count() == 0
177+
assert SessionProfile.objects.count() == 5
178+
179+
session_keys = [session.session_key for session in sessions]
180+
181+
# sessions are created
182+
for session_key in session_keys:
183+
assert CachedSessionStore().exists(session_key)
184+
185+
response = client.post(
186+
session_changelist_url,
187+
{"action": "delete_selected", "_selected_action": session_keys, "post": "yes"},
188+
)
189+
assert response.status_code == 302
190+
191+
# one is created as the post request is sent
192+
assert SessionProfile.objects.count() == 1
193+
assert Session.objects.count() == 0
194+
195+
# sessions should be deleted
196+
for session_key in session_keys:
197+
assert not CachedSessionStore().exists(session_key)

0 commit comments

Comments
 (0)