Skip to content

Commit 1d61db9

Browse files
jmorgannzbizzappdev
authored andcommitted
Module auth_session_timeout: Pluggability (OCA#887)
* Module auth_session_timeout: --------------------------- * Refactor to allow other modules to inherit and augment or override the following: ** Session expiry time (deadline) calculation ** Ignored URLs ** Final session expiry (with possibility to late-abort) * Re-ordered functionality to remove unnecessary work, as this code is called very often. * Do not expire a session if delay gets set to zero (or unset / false) * WIP * Fixed flake8 lint errors * Fixed flake8 lint errors * WIP * WIP * WIP * WIP * WIP * WIP * Module: auth-session-timeout: Refactor ResUser tests to use `unittest.mock` patching * Module: auth_session_timeout: Fixed flake8 lint errors * Module: auth_session_timeout: Fixed flake8 lint errors
1 parent 6393187 commit 1d61db9

File tree

6 files changed

+211
-54
lines changed

6 files changed

+211
-54
lines changed

auth_session_timeout/README.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Contributors
5050

5151
* Cédric Pigeon <cedric.pigeon@acsone.eu>
5252
* Dhinesh D <dvdhinesh.mail@gmail.com>
53+
* Jesse Morgan <jmorgan.nz@gmail.com>
5354
* Dave Lasley <dave@laslabs.com>
5455

5556
Maintainer

auth_session_timeout/__manifest__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
This module disable all inactive sessions since a given delay""",
99
'author': "ACSONE SA/NV, "
1010
"Dhinesh D, "
11+
"Jesse Morgan, "
1112
"LasLabs, "
1213
"Odoo Community Association (OCA)",
1314
'maintainer': 'Odoo Community Association (OCA)',
Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# -*- coding: utf-8 -*-
22
# (c) 2015 ACSONE SA/NV, Dhinesh D
3-
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
43

5-
from odoo import models, api, tools
4+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
65

6+
from odoo import models, api, tools, SUPERUSER_ID
77

88
DELAY_KEY = 'inactive_session_time_out_delay'
99
IGNORED_PATH_KEY = 'inactive_session_time_out_ignored_url'
@@ -12,18 +12,34 @@
1212
class IrConfigParameter(models.Model):
1313
_inherit = 'ir.config_parameter'
1414

15-
@api.model
16-
@tools.ormcache('self.env.cr.dbname')
17-
def get_session_parameters(self):
18-
ConfigParam = self.env['ir.config_parameter']
19-
delay = ConfigParam.get_param(DELAY_KEY, 7200)
20-
urls = ConfigParam.get_param(IGNORED_PATH_KEY, '').split(',')
21-
return int(delay), urls
15+
@tools.ormcache('db')
16+
def get_session_parameters(self, db):
17+
param_model = self.pool['ir.config_parameter']
18+
cr = self.pool.cursor()
19+
delay = False
20+
urls = []
21+
try:
22+
delay = int(param_model.get_param(
23+
cr, SUPERUSER_ID, DELAY_KEY, 7200))
24+
urls = param_model.get_param(
25+
cr, SUPERUSER_ID, IGNORED_PATH_KEY, '').split(',')
26+
finally:
27+
cr.close()
28+
return delay, urls
29+
30+
def _auth_timeout_get_parameter_delay(self):
31+
delay, urls = self.get_session_parameters(self.pool.db_name)
32+
return delay
33+
34+
def _auth_timeout_get_parameter_ignoredurls(self):
35+
delay, urls = self.get_session_parameters(self.pool.db_name)
36+
return urls
2237

2338
@api.multi
24-
def write(self, vals):
39+
def write(self, vals, context=None):
2540
res = super(IrConfigParameter, self).write(vals)
26-
for rec_id in self:
27-
if rec_id.key in (DELAY_KEY, IGNORED_PATH_KEY):
28-
self.get_session_parameters.clear_cache(self)
41+
if self.key == DELAY_KEY:
42+
self.get_session_parameters.clear_cache(self)
43+
elif self.key == IGNORED_PATH_KEY:
44+
self.get_session_parameters.clear_cache(self)
2945
return res
Lines changed: 92 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,110 @@
11
# -*- coding: utf-8 -*-
22
# (c) 2015 ACSONE SA/NV, Dhinesh D
3+
34
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
45

6+
import logging
7+
8+
from odoo import models
9+
10+
from odoo.http import root
11+
from odoo.http import request
12+
513
from os import utime
614
from os.path import getmtime
715
from time import time
816

9-
from odoo import models, http
17+
_logger = logging.getLogger(__name__)
1018

1119

1220
class ResUsers(models.Model):
1321
_inherit = 'res.users'
1422

15-
@classmethod
16-
def _check_session_validity(cls, db, uid, passwd):
17-
if not http.request:
23+
def _auth_timeout_ignoredurls_get(self):
24+
"""Pluggable method for calculating ignored urls
25+
Defaults to stored config param
26+
"""
27+
param_model = self.pool['ir.config_parameter']
28+
return param_model._auth_timeout_get_parameter_ignoredurls()
29+
30+
def _auth_timeout_deadline_calculate(self):
31+
"""Pluggable method for calculating timeout deadline
32+
Defaults to current time minus delay using delay stored as config param
33+
"""
34+
param_model = self.pool['ir.config_parameter']
35+
delay = param_model._auth_timeout_get_parameter_delay()
36+
if delay is False or delay <= 0:
37+
return False
38+
return time() - delay
39+
40+
def _auth_timeout_session_terminate(self, session):
41+
"""Pluggable method for terminating a timed-out session
42+
43+
This is a late stage where a session timeout can be aborted.
44+
Useful if you want to do some heavy checking, as it won't be
45+
called unless the session inactivity deadline has been reached.
46+
47+
Return:
48+
True: session terminated
49+
False: session timeout cancelled
50+
"""
51+
if session.db and session.uid:
52+
session.logout(keep_db=True)
53+
return True
54+
55+
def _auth_timeout_check(self):
56+
if not request:
1857
return
19-
session = http.request.session
20-
session_store = http.root.session_store
21-
ConfigParam = http.request.env['ir.config_parameter']
22-
delay, urls = ConfigParam.get_session_parameters()
23-
deadline = time() - delay
24-
path = session_store.get_session_filename(session.sid)
25-
try:
26-
if getmtime(path) < deadline:
27-
if session.db and session.uid:
28-
session.logout(keep_db=True)
29-
elif http.request.httprequest.path not in urls:
30-
# the session is not expired, update the last modification
31-
# and access time.
58+
59+
session = request.session
60+
61+
# Calculate deadline
62+
deadline = self._auth_timeout_deadline_calculate()
63+
64+
# Check if past deadline
65+
expired = False
66+
if deadline is not False:
67+
path = root.session_store.get_session_filename(session.sid)
68+
try:
69+
expired = getmtime(path) < deadline
70+
except OSError as e:
71+
_logger.warning(
72+
'Exception reading session file modified time: %s'
73+
% e
74+
)
75+
pass
76+
77+
# Try to terminate the session
78+
terminated = False
79+
if expired:
80+
terminated = self._auth_timeout_session_terminate(session)
81+
82+
# If session terminated, all done
83+
if terminated:
84+
return
85+
86+
# Else, conditionally update session modified and access times
87+
ignoredurls = self._auth_timeout_ignoredurls_get()
88+
89+
if request.httprequest.path not in ignoredurls:
90+
if 'path' not in locals():
91+
path = root.session_store.get_session_filename(session.sid)
92+
try:
3293
utime(path, None)
33-
except OSError:
34-
pass
94+
except OSError as e:
95+
_logger.warning(
96+
'Exception updating session file access/modified times: %s'
97+
% e
98+
)
99+
pass
100+
35101
return
36102

37-
@classmethod
38-
def check(cls, db, uid, passwd):
39-
res = super(ResUsers, cls).check(db, uid, passwd)
40-
cls._check_session_validity(db, uid, passwd)
103+
def _check_session_validity(self, db, uid, passwd):
104+
"""Adaptor method for backward compatibility"""
105+
return self._auth_timeout_check()
106+
107+
def check(self, db, uid, passwd):
108+
res = super(ResUsers, self).check(db, uid, passwd)
109+
self._check_session_validity(db, uid, passwd)
41110
return res
Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,71 @@
11
# -*- coding: utf-8 -*-
22
# (c) 2015 ACSONE SA/NV, Dhinesh D
3-
# Copyright 2016 LasLabs Inc.
3+
44
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
55

6-
from odoo.tests.common import TransactionCase
6+
from odoo.tests import common
77

88

9-
class TestIrConfigParameter(TransactionCase):
9+
class TestIrConfigParameter(common.TransactionCase):
1010

1111
def setUp(self):
1212
super(TestIrConfigParameter, self).setUp()
13+
self.db = self.env.cr.dbname
1314
self.param_obj = self.env['ir.config_parameter']
1415
self.data_obj = self.env['ir.model.data']
1516
self.delay = self.env.ref(
16-
'auth_session_timeout.inactive_session_time_out_delay'
17-
)
18-
self.url = self.env.ref(
19-
'auth_session_timeout.inactive_session_time_out_ignored_url'
20-
)
21-
self.urls = ['url1', 'url2']
22-
self.url.value = ','.join(self.urls)
23-
24-
def test_get_session_parameters_delay(self):
25-
""" It should return the proper delay """
26-
delay, _ = self.param_obj.get_session_parameters()
17+
'auth_session_timeout.inactive_session_time_out_delay')
18+
19+
def test_check_session_params(self):
20+
delay, urls = self.param_obj.get_session_parameters(self.db)
21+
self.assertEqual(delay, int(self.delay.value))
22+
self.assertIsInstance(delay, int)
23+
self.assertIsInstance(urls, list)
24+
25+
def test_check_session_param_delay(self):
26+
delay = self.param_obj._auth_timeout_get_parameter_delay()
2727
self.assertEqual(delay, int(self.delay.value))
28+
self.assertIsInstance(delay, int)
29+
30+
def test_check_session_param_urls(self):
31+
urls = self.param_obj._auth_timeout_get_parameter_ignoredurls()
32+
self.assertIsInstance(urls, list)
33+
34+
35+
class TestIrConfigParameterCaching(common.TransactionCase):
36+
37+
def setUp(self):
38+
super(TestIrConfigParameterCaching, self).setUp()
39+
self.db = self.env.cr.dbname
40+
self.param_obj = self.env['ir.config_parameter']
41+
self.get_param_called = False
42+
test = self
43+
44+
def get_param(*args, **kwargs):
45+
test.get_param_called = True
46+
return orig_get_param(args[3], args[4])
47+
orig_get_param = self.param_obj.get_param
48+
self.param_obj._patch_method(
49+
'get_param',
50+
get_param)
51+
52+
def tearDown(self):
53+
super(TestIrConfigParameterCaching, self).tearDown()
54+
self.param_obj._revert_method('get_param')
55+
56+
def test_check_param_cache_working(self):
57+
self.get_param_called = False
58+
delay, urls = self.param_obj.get_session_parameters(self.db)
59+
self.assertTrue(self.get_param_called)
60+
self.get_param_called = False
61+
delay, urls = self.param_obj.get_session_parameters(self.db)
62+
self.assertFalse(self.get_param_called)
2863

29-
def test_get_session_parameters_url(self):
30-
""" It should return URIs split by comma """
31-
_, urls = self.param_obj.get_session_parameters()
32-
self.assertEqual(urls, self.urls)
64+
def test_check_param_writes_clear_cache(self):
65+
self.get_param_called = False
66+
delay, urls = self.param_obj.get_session_parameters(self.db)
67+
self.assertTrue(self.get_param_called)
68+
self.get_param_called = False
69+
self.param_obj.set_param('inactive_session_time_out_delay', 7201)
70+
delay, urls = self.param_obj.get_session_parameters(self.db)
71+
self.assertTrue(self.get_param_called)

auth_session_timeout/tests/test_res_users.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@
33
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
44

55
import mock
6+
from os import strerror
7+
from errno import ENOENT
68

79
from contextlib import contextmanager
810

911
from odoo.tests.common import TransactionCase
1012

13+
_package_path = 'odoo.addons.auth_session_timeout'
14+
1115

1216
class EndTestException(Exception):
1317
""" It stops tests from continuing """
@@ -102,3 +106,30 @@ def test_session_validity_os_error_guard(self):
102106
assets['getmtime'].side_effect = OSError
103107
res = self._check_session_validity()
104108
self.assertFalse(res)
109+
110+
@mock.patch(_package_path + '.models.res_users.request')
111+
@mock.patch(_package_path + '.models.res_users.root')
112+
@mock.patch(_package_path + '.models.res_users.getmtime')
113+
def test_on_timeout_session_loggedout(self, mock_getmtime,
114+
mock_root, mock_request):
115+
mock_getmtime.return_value = 0
116+
mock_request.session.uid = self.env.uid
117+
mock_request.session.dbname = self.env.cr.dbname
118+
mock_request.session.sid = 123
119+
mock_request.session.logout = mock.Mock()
120+
self.resUsers._auth_timeout_check()
121+
self.assertTrue(mock_request.session.logout.called)
122+
123+
@mock.patch(_package_path + '.models.res_users.request')
124+
@mock.patch(_package_path + '.models.res_users.root')
125+
@mock.patch(_package_path + '.models.res_users.getmtime')
126+
@mock.patch(_package_path + '.models.res_users.utime')
127+
def test_sessionfile_io_exceptions_managed(self, mock_utime, mock_getmtime,
128+
mock_root, mock_request):
129+
mock_getmtime.side_effect = OSError(
130+
ENOENT, strerror(ENOENT), 'non-existent-filename')
131+
mock_request.session.uid = self.env.uid
132+
mock_request.session.dbname = self.env.cr.dbname
133+
mock_request.session.sid = 123
134+
self.resUsers._auth_timeout_check()
135+

0 commit comments

Comments
 (0)