Received: from mail.netlandish.com (unknown [10.138.202.29])
	by code.netlandish.com (Postfix) with ESMTP id B2A698019C
	for <~petersanchez/public-inbox@lists.code.netlandish.com>; Mon, 23 Nov 2020 18:32:47 +0000 (UTC)
Received-SPF: Pass (mailfrom) identity=mailfrom; client-ip=209.85.208.51; helo=mail-ed1-f51.google.com; envelope-from=leo@jacobs-alumni.de; receiver=<UNKNOWN> 
Authentication-Results: mail.netlandish.com;
	dkim=pass (2048-bit key; unprotected) header.d=jacobs-alumni.de header.i=@jacobs-alumni.de header.b=DjfKo5Z2
Received: from mail-ed1-f51.google.com (mail-ed1-f51.google.com [209.85.208.51])
	by mail.netlandish.com (Postfix) with ESMTP id 5898149C5F
	for <~petersanchez/public-inbox@lists.code.netlandish.com>; Mon, 23 Nov 2020 10:32:45 -0800 (PST)
Received: by mail-ed1-f51.google.com with SMTP id cf17so14584708edb.2
        for <~petersanchez/public-inbox@lists.code.netlandish.com>; Mon, 23 Nov 2020 10:32:45 -0800 (PST)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
        d=jacobs-alumni.de; s=google;
        h=mime-version:content-transfer-encoding:subject:message-id
         :user-agent:date:from:to;
        bh=+DAfuKflXjFk8Gys8b2EnJoTdNviS9Wb2ovPc3l//dc=;
        b=DjfKo5Z2FXaGYUhAFpmfiH8ReyXQT6ALd1TPjV5rv5Uem3QLzAdDLYSxCa2r4YryGi
         xYE74jgN5RBWVQRzzkq0kblCIZa8wGYGIfla8zNseyHXyqe6JaiBy7Vij/mbEbBNZJku
         H0E+SjABT3ZCNVlLH4lFY73TWPb7mTqbKHbq7xQ47aYhxP48/lNvC2NolfHE+nqPVdk6
         9YGd0Wwajmb0E/LN1Kzi6zMFz74EKOfu1tCBpCnz7Uz9f2eBLiNq0OHeB0EU1sCBQb92
         kENBv1TapOCLswNSPeBWWylrnLOD1EHFtaQEDcfnHezk/LNhHQB8LqrNVxCdYiV6aIKs
         R4wA==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
        d=1e100.net; s=20161025;
        h=x-gm-message-state:mime-version:content-transfer-encoding:subject
         :message-id:user-agent:date:from:to;
        bh=+DAfuKflXjFk8Gys8b2EnJoTdNviS9Wb2ovPc3l//dc=;
        b=KFS357cOb+GOhvi8f5bjj6/hbUgRV0D+0ZszAR+xfcFfpZds2nAb64WY3NwgEYe63b
         MB2wJBnNJf01XWhoV/j17qW4WVk6dP8aLm8wrfSIxt7x85CMKqjnEyCpxx3xNzGXJl/K
         111Mco8T4fgpV38dzW37g87gRBdwld7/DSM8w5fWOyvv6Q1TfunRCKUgAoMOic5pRN0R
         Nq3rY8EtfLHH7iF+ika81AmRO8avcTxrzIEMZ9eM1GbXHjgsZMgH1uU5fe3uOf9SWqmL
         aUVm36neq8BhBFGNxibjK9mjkFw/JFz7K52E9ObJyH4+DoeJ7TdBDOF8w5hjz44suPMK
         DQKg==
X-Gm-Message-State: AOAM533geqKcuWdszDOiN4mcxMwpTs9sLAIxKQ+my5Iv1b8pFfSQxlI/
	cBwi/HQ8DlPlq7l7h9sAewzdc4XQ6esC8Flh
X-Google-Smtp-Source: ABdhPJw64TyD3hygQTTGUiUa4EsCUEiqGY/45U4hCbnvCnk+evO6FjAA3uUkyWVrlnaeiRWCkhpGxw==
X-Received: by 2002:aa7:ccda:: with SMTP id y26mr506232edt.123.1606156364207;
        Mon, 23 Nov 2020 10:32:44 -0800 (PST)
Received: from [127.0.1.1] (p200300c1071e7cd1c18331629db18758.dip0.t-ipconnect.de. [2003:c1:71e:7cd1:c183:3162:9db1:8758])
        by smtp.gmail.com with ESMTPSA id k17sm4210316ejh.103.2020.11.23.10.32.43
        for <~petersanchez/public-inbox@lists.code.netlandish.com>
        (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);
        Mon, 23 Nov 2020 10:32:43 -0800 (PST)
MIME-Version: 1.0
Content-Type: text/plain; charset="us-ascii"
Content-Transfer-Encoding: 7bit
Subject: [PATCH django-impersonate] add option for auto-expiring impersonate
 sesions,
 implements #45
X-Mercurial-Node: 5ae6838bdebd6fa362f7fd730476a045c6f2772d
X-Mercurial-Series-Index: 1
X-Mercurial-Series-Total: 1
Message-Id: <5ae6838bdebd6fa362f7.1606156358@red>
X-Mercurial-Series-Id: <5ae6838bdebd6fa362f7.1606156358@red>
User-Agent: Mercurial-patchbomb/5.6
Date: Mon, 23 Nov 2020 19:32:38 +0100
From: =?iso-8859-1?q?Leonhard_Kuboschek?= <leo@jacobs-alumni.de>
To: ~petersanchez/public-inbox@lists.code.netlandish.com

# HG changeset patch
# User Leonhard Kuboschek <leo@jacobs-alumni.de>
# Date 1606156336 -3600
#      Mon Nov 23 19:32:16 2020 +0100
# Node ID 5ae6838bdebd6fa362f7fd730476a045c6f2772d
# Parent  1e3192bbb6762d1756fd203d28714620ef43da15
add option for auto-expiring impersonate sesions, implements #45

diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -395,6 +395,13 @@
 
 Default is `True`
 
+    MAX_DURATION
+
+A number specifying the maximum allowed duration of impersonation 
+sessions in **seconds**.
+
+Default is `None`
+
 Admin
 =====
 
diff --git a/README.rst b/README.rst
--- a/README.rst
+++ b/README.rst
@@ -464,6 +464,15 @@
 
 Default is ``True``
 
+::
+
+    MAX_DURATION
+
+A number specifying the maximum allowed duration of impersonation 
+sessions in **seconds**.
+
+Default is `None`
+
 Admin
 =====
 
diff --git a/impersonate/middleware.py b/impersonate/middleware.py
--- a/impersonate/middleware.py
+++ b/impersonate/middleware.py
@@ -1,4 +1,6 @@
 # -*- coding: utf-8 -*-
+from datetime import datetime, timedelta
+
 from django.http import HttpResponseNotAllowed
 from django.utils.deprecation import MiddlewareMixin
 
@@ -12,6 +14,17 @@
         request.impersonator = None
 
         if request.user.is_authenticated and '_impersonate' in request.session:
+            if settings.MAX_DURATION:
+                if '_impersonate_start' not in request.session:
+                    return
+
+                start_time = datetime.fromtimestamp(request.session['_impersonate_start'])
+                if datetime.now() - start_time > timedelta(seconds=settings.MAX_DURATION):
+                    del request.session['_impersonate']
+                    del request.session['_impersonate_start']
+
+                    return
+
             new_user_id = request.session['_impersonate']
             if isinstance(new_user_id, User):
                 # Edge case for issue 15
diff --git a/impersonate/settings.py b/impersonate/settings.py
--- a/impersonate/settings.py
+++ b/impersonate/settings.py
@@ -23,6 +23,7 @@
     'ADMIN_DELETE_PERMISSION': False,
     'ADMIN_ADD_PERMISSION': False,
     'ADMIN_READ_ONLY': True,
+    'MAX_DURATION': None,
 }
 
 
diff --git a/impersonate/tests.py b/impersonate/tests.py
--- a/impersonate/tests.py
+++ b/impersonate/tests.py
@@ -22,6 +22,7 @@
 from distutils.version import LooseVersion
 from unittest.mock import PropertyMock, patch
 from urllib.parse import urlencode, urlsplit
+from datetime import datetime, timedelta
 
 import django
 from django.urls import include, path
@@ -110,11 +111,12 @@
             return None
         self.middleware = ImpersonateMiddleware(dummy_get_response)
 
-    def _impersonated_request(self, use_id=True):
+    def _impersonated_request(self, use_id=True, _impersonate_start=None):
         request = self.factory.get('/')
         request.user = self.superuser
         request.session = {
             '_impersonate': self.user.pk if use_id else self.user,
+            '_impersonate_start': _impersonate_start
         }
         self.middleware.process_request(request)
 
@@ -133,6 +135,45 @@
         '''
         self._impersonated_request(use_id=False)
 
+    @override_settings(IMPERSONATE={'MAX_DURATION': 3600})
+    def test_impersonated_request_with_max_duration(self):
+        self._impersonated_request(_impersonate_start=datetime.now().timestamp())
+
+    @override_settings(IMPERSONATE={'MAX_DURATION': 3600})
+    def test_reject_without_start_time(self):
+        ''' Test to ensure that requests without a start time
+            are rejected when MAX_DURATION is set
+        '''
+        request = self.factory.get('/')
+        request.user = self.superuser
+        request.session = {
+            '_impersonate': self.user.pk,
+        }
+        self.middleware.process_request(request)
+
+        self.assertEqual(request.user, self.superuser)
+        self.assertFalse(request.user.is_impersonate)
+
+
+    @override_settings(IMPERSONATE={'MAX_DURATION': 3600})
+    def test_reject_expired_impersonation(self):
+        ''' Test to ensure that requests with a start time before (now - MAX_DURATION)
+            are rejected
+        '''
+        request = self.factory.get('/')
+        request.user = self.superuser
+        request.session = {
+            '_impersonate': self.user.pk,
+            '_impersonate_start': (datetime.now() - timedelta(seconds=3601)).timestamp()
+        }
+        self.middleware.process_request(request)
+
+        self.assertEqual(request.user, self.superuser)
+        self.assertFalse(request.user.is_impersonate)
+        self.assertNotIn('_impersonate', request.session)
+        self.assertNotIn('_impersonate_start', request.session)
+
+
     def test_not_impersonated_request(self, use_id=True):
         """Check the real_user request attr is set correctly when **not** impersonating."""
         request = self.factory.get('/')
diff --git a/impersonate/views.py b/impersonate/views.py
--- a/impersonate/views.py
+++ b/impersonate/views.py
@@ -1,6 +1,8 @@
 # -*- coding: utf-8 -*-
 import logging
 
+from datetime import datetime
+
 from django.db.models import Q
 from django.http import Http404
 from django.shortcuts import get_object_or_404, redirect, render
@@ -36,6 +38,7 @@
         raise Http404('Invalid value given.')
     if check_allow_for_user(request, new_user):
         request.session['_impersonate'] = new_user.pk
+        request.session['_impersonate_start'] = datetime.now().timestamp()
         prev_path = request.META.get('HTTP_REFERER')
         if prev_path:
             request.session[
