Authorization-code flow with PKCE through an idp adapter (with a fake), ID token validation, identity and roles resolved from the directory with a short cache, refusal of unknown, disabled and locked accounts with one generic page, sessions that end when the account becomes unavailable, sign-out that ends the server-side session, role decorators and the trusted-proxy client address rule. Adds the OIDC_* and PUBLIC_BASE_URL configuration. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N3t4WL7bd9bdHHNjEsxzqC
126 lines
4.5 KiB
Python
126 lines
4.5 KiB
Python
import time
|
|
from datetime import timedelta
|
|
|
|
import pytest
|
|
from django.conf import settings
|
|
from django.test import Client, RequestFactory
|
|
from django.utils import timezone
|
|
|
|
from portal.accounts.middleware import AbsoluteSessionLimitMiddleware
|
|
from portal.accounts.models import UserSession
|
|
from portal.accounts.session_store import CREATED_AT, SessionStore
|
|
from portal.accounts.sessions import SessionUser, current_user, end_sessions, start_session
|
|
|
|
pytestmark = pytest.mark.django_db
|
|
|
|
|
|
def make_session(sam):
|
|
request = RequestFactory().get("/")
|
|
request.session = SessionStore()
|
|
start_session(request, sam)
|
|
request.session.save()
|
|
return request
|
|
|
|
|
|
def test_username_column_is_lowercase_key_and_sam_keeps_case():
|
|
request = make_session("Alice")
|
|
row = UserSession.objects.get(session_key=request.session.session_key)
|
|
assert row.username == "alice"
|
|
assert current_user(request) == SessionUser(key="alice", sam="Alice")
|
|
|
|
|
|
def test_start_session_rotates_key_and_drops_old_data():
|
|
request = RequestFactory().get("/")
|
|
request.session = SessionStore()
|
|
request.session["stale"] = 1
|
|
request.session.save()
|
|
old_key = request.session.session_key
|
|
start_session(request, "alice")
|
|
request.session.save()
|
|
assert request.session.session_key != old_key
|
|
assert "stale" not in request.session
|
|
assert not UserSession.objects.filter(session_key=old_key).exists()
|
|
|
|
|
|
def test_end_sessions_only_target_user():
|
|
for _ in range(3):
|
|
make_session("a")
|
|
for _ in range(2):
|
|
make_session("b")
|
|
assert end_sessions("a") == 3
|
|
assert UserSession.objects.filter(username="a").count() == 0
|
|
assert UserSession.objects.filter(username="b").count() == 2
|
|
|
|
|
|
def test_end_sessions_except_session_key_and_unknown_user():
|
|
keep = make_session("a").session.session_key
|
|
make_session("a")
|
|
assert end_sessions(SessionUser("a", "A"), except_session_key=keep) == 1
|
|
assert list(UserSession.objects.values_list("session_key", flat=True)) == [keep]
|
|
assert end_sessions("nobody") == 0
|
|
|
|
|
|
def test_idle_limit_expired_session_is_gone():
|
|
request = make_session("a")
|
|
UserSession.objects.update(expire_date=timezone.now() - timedelta(seconds=1))
|
|
assert SessionStore(request.session.session_key).get(CREATED_AT) is None
|
|
|
|
|
|
def test_settings_defaults():
|
|
assert settings.SESSION_COOKIE_AGE == 3600
|
|
assert settings.SESSION_ABSOLUTE_SECONDS == 14400
|
|
assert settings.SESSION_SAVE_EVERY_REQUEST is True
|
|
|
|
|
|
def _client_with_session(created_at, adapters):
|
|
adapters.directory.add_user("alice")
|
|
store = SessionStore()
|
|
request = RequestFactory().get("/")
|
|
request.session = store
|
|
start_session(request, "alice")
|
|
store[CREATED_AT] = created_at
|
|
store.save()
|
|
client = Client()
|
|
client.cookies[settings.SESSION_COOKIE_NAME] = store.session_key
|
|
return client, store.session_key
|
|
|
|
|
|
def test_absolute_limit_removes_session_and_clears_cookie(adapters):
|
|
assert any(m.endswith("AbsoluteSessionLimitMiddleware") for m in settings.MIDDLEWARE)
|
|
too_old = time.time() - settings.SESSION_ABSOLUTE_SECONDS - 5
|
|
client, key = _client_with_session(too_old, adapters)
|
|
response = client.get("/healthz")
|
|
assert not UserSession.objects.filter(session_key=key).exists()
|
|
assert response.cookies[settings.SESSION_COOKIE_NAME].value == ""
|
|
|
|
|
|
def test_session_within_limits_survives_a_request(adapters):
|
|
client, key = _client_with_session(time.time() - 60, adapters)
|
|
client.get("/healthz")
|
|
assert UserSession.objects.filter(session_key=key).exists()
|
|
|
|
|
|
def test_middleware_callable_directly():
|
|
request = make_session("alice")
|
|
AbsoluteSessionLimitMiddleware(lambda r: None)(request)
|
|
assert current_user(request) is not None
|
|
|
|
|
|
def test_request_renews_expire_date_to_one_hour_ahead(adapters):
|
|
client, key = _client_with_session(time.time() - 60, adapters)
|
|
UserSession.objects.filter(session_key=key).update(
|
|
expire_date=timezone.now() + timedelta(minutes=5)
|
|
)
|
|
client.get("/healthz")
|
|
remaining = UserSession.objects.get(session_key=key).expire_date - timezone.now()
|
|
assert timedelta(minutes=59) < remaining <= timedelta(hours=1)
|
|
|
|
|
|
def test_request_with_idle_expired_session_has_no_session_and_clears_cookie(adapters):
|
|
client, key = _client_with_session(time.time() - 60, adapters)
|
|
UserSession.objects.filter(session_key=key).update(
|
|
expire_date=timezone.now() - timedelta(seconds=1)
|
|
)
|
|
response = client.get("/healthz")
|
|
assert response.cookies[settings.SESSION_COOKIE_NAME].value == ""
|