368 lines
18 KiB
Python
368 lines
18 KiB
Python
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||
|
|
||
|
from freezegun import freeze_time
|
||
|
from unittest.mock import patch
|
||
|
|
||
|
import odoo
|
||
|
|
||
|
from odoo import Command
|
||
|
from odoo.addons.test_http.utils import (
|
||
|
TEST_IP,
|
||
|
USER_AGENT_android_chrome,
|
||
|
USER_AGENT_linux_chrome,
|
||
|
USER_AGENT_linux_firefox
|
||
|
)
|
||
|
from .test_common import TestHttpBase
|
||
|
|
||
|
|
||
|
class TestDevice(TestHttpBase):
|
||
|
|
||
|
def setUp(self):
|
||
|
super().setUp()
|
||
|
|
||
|
self.Device = self.env['res.device']
|
||
|
self.DeviceLog = self.env['res.device.log']
|
||
|
self.DeviceLog.search([]).unlink()
|
||
|
|
||
|
self.user_admin = self.env.ref('base.user_admin')
|
||
|
self.user_internal = self.env['res.users'].create({
|
||
|
'login': 'internal',
|
||
|
'password': 'internal',
|
||
|
'name': 'Internal',
|
||
|
'email': 'internal@example.com',
|
||
|
'groups_id': [Command.set([self.env.ref('base.group_user').id])],
|
||
|
})
|
||
|
|
||
|
def hit(self, time, endpoint, headers=None, ip=None):
|
||
|
if ip:
|
||
|
headers = headers or {}
|
||
|
headers = {
|
||
|
**headers,
|
||
|
'Host': '',
|
||
|
'X-Forwarded-For': ip,
|
||
|
'X-Forwarded-Host': 'odoo.com',
|
||
|
'X-Forwarded-Proto': 'https'
|
||
|
}
|
||
|
with freeze_time(time), \
|
||
|
patch.dict(odoo.tools.config.options, {'proxy_mode': bool(ip)}):
|
||
|
res = self.url_open(url=endpoint, headers=headers)
|
||
|
return res
|
||
|
|
||
|
def info_trace(self, trace):
|
||
|
return {
|
||
|
'elapsed_time': trace['last_activity'] - trace['first_activity'],
|
||
|
'platform': trace['platform'],
|
||
|
'browser': trace['browser'],
|
||
|
'ip_address': trace['ip_address'],
|
||
|
}
|
||
|
|
||
|
def get_devices_logs(self, user=None):
|
||
|
domain = [('user_id', '=', user.id)] if user else []
|
||
|
devices = self.Device.search(domain)
|
||
|
logs = self.DeviceLog.search([
|
||
|
('session_identifier', 'in', devices.mapped('session_identifier')),
|
||
|
('platform', 'in', devices.mapped('platform')),
|
||
|
('browser', 'in', devices.mapped('browser'))
|
||
|
])
|
||
|
return devices, logs
|
||
|
|
||
|
# --------------------
|
||
|
# DETECTION
|
||
|
# --------------------
|
||
|
|
||
|
def test_detection_device_readonly(self):
|
||
|
session = self.authenticate(self.user_admin.login, self.user_admin.login)
|
||
|
self.hit('2024-01-01 08:00:00', '/test_http/greeting-public')
|
||
|
|
||
|
devices, logs = self.get_devices_logs(self.user_admin)
|
||
|
self.assertEqual(len(devices), 1)
|
||
|
self.assertEqual(len(logs), 1)
|
||
|
self.assertEqual(len(session._trace), 1)
|
||
|
|
||
|
def test_detection_device_no_readonly(self):
|
||
|
session = self.authenticate(self.user_admin.login, self.user_admin.login)
|
||
|
self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0')
|
||
|
|
||
|
devices, logs = self.get_devices_logs(self.user_admin)
|
||
|
self.assertEqual(len(devices), 1)
|
||
|
self.assertEqual(len(logs), 1)
|
||
|
self.assertEqual(len(session._trace), 1)
|
||
|
|
||
|
def test_detection_user_public(self):
|
||
|
self.authenticate(None, None)
|
||
|
self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0')
|
||
|
|
||
|
devices, logs = self.get_devices_logs()
|
||
|
self.assertEqual(len(devices), 0)
|
||
|
self.assertEqual(len(logs), 0)
|
||
|
|
||
|
def test_detection_device_readonly_then_no_readonly(self):
|
||
|
session = self.authenticate(self.user_admin.login, self.user_admin.login)
|
||
|
self.hit('2024-01-01 08:00:00', '/test_http/greeting-public')
|
||
|
|
||
|
devices, logs = self.get_devices_logs(self.user_admin)
|
||
|
self.assertEqual(len(devices), 1)
|
||
|
self.assertEqual(len(logs), 1)
|
||
|
self.assertEqual(len(session._trace), 1)
|
||
|
|
||
|
self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0')
|
||
|
|
||
|
devices, logs = self.get_devices_logs(self.user_admin)
|
||
|
self.assertEqual(len(devices), 1)
|
||
|
self.assertEqual(len(logs), 1)
|
||
|
self.assertEqual(len(session._trace), 1)
|
||
|
|
||
|
def test_detection_device_according_to_time(self):
|
||
|
session = self.authenticate(self.user_admin.login, self.user_admin.login)
|
||
|
self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0')
|
||
|
|
||
|
devices, logs = self.get_devices_logs(self.user_admin)
|
||
|
self.assertEqual(len(devices), 1)
|
||
|
self.assertEqual(len(logs), 1)
|
||
|
self.assertEqual(len(session._trace), 1)
|
||
|
self.assertEqual(self.info_trace(session._trace[0])['elapsed_time'], 0)
|
||
|
|
||
|
self.hit('2024-01-01 08:30:00', '/test_http/greeting-public?readonly=0')
|
||
|
|
||
|
devices, logs = self.get_devices_logs(self.user_admin)
|
||
|
self.assertEqual(len(devices), 1)
|
||
|
self.assertEqual(len(logs), 1)
|
||
|
self.assertEqual(len(session._trace), 1)
|
||
|
self.assertEqual(self.info_trace(session._trace[0])['elapsed_time'], 0) # No trace update (< 3600 sec)
|
||
|
|
||
|
self.hit('2024-01-01 09:00:00', '/test_http/greeting-public?readonly=0')
|
||
|
|
||
|
devices, logs = self.get_devices_logs(self.user_admin)
|
||
|
self.assertEqual(len(devices), 1)
|
||
|
self.assertEqual(len(logs), 2)
|
||
|
self.assertEqual(len(session._trace), 1)
|
||
|
self.assertEqual(self.info_trace(session._trace[0])['elapsed_time'], 3600)
|
||
|
|
||
|
self.hit('2024-01-01 10:00:00', '/test_http/greeting-public?readonly=0')
|
||
|
|
||
|
devices, logs = self.get_devices_logs(self.user_admin)
|
||
|
self.assertEqual(len(devices), 1)
|
||
|
self.assertEqual(len(logs), 3)
|
||
|
self.assertEqual(len(session._trace), 1)
|
||
|
self.assertEqual(self.info_trace(session._trace[0])['elapsed_time'], 7200)
|
||
|
|
||
|
def test_detection_device_according_to_useragent(self):
|
||
|
session = self.authenticate(self.user_admin.login, self.user_admin.login)
|
||
|
|
||
|
self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0', headers={'User-Agent': USER_AGENT_linux_chrome})
|
||
|
|
||
|
devices, logs = self.get_devices_logs(self.user_admin)
|
||
|
self.assertEqual(len(devices), 1)
|
||
|
self.assertEqual(len(logs), 1)
|
||
|
self.assertEqual(len(session._trace), 1)
|
||
|
self.assertEqual(self.info_trace(session._trace[0])['platform'], 'linux')
|
||
|
self.assertEqual(self.info_trace(session._trace[0])['browser'], 'chrome')
|
||
|
|
||
|
self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0', headers={'User-Agent': USER_AGENT_linux_firefox})
|
||
|
|
||
|
devices, logs = self.get_devices_logs(self.user_admin)
|
||
|
self.assertEqual(len(devices), 2)
|
||
|
self.assertEqual(len(logs), 2)
|
||
|
self.assertEqual(len(session._trace), 2)
|
||
|
self.assertEqual(self.info_trace(session._trace[1])['platform'], 'linux')
|
||
|
self.assertEqual(self.info_trace(session._trace[1])['browser'], 'firefox')
|
||
|
|
||
|
def test_detection_device_according_to_ipaddress(self):
|
||
|
session = self.authenticate(self.user_admin.login, self.user_admin.login)
|
||
|
self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0')
|
||
|
|
||
|
devices, logs = self.get_devices_logs(self.user_admin)
|
||
|
self.assertEqual(len(devices), 1)
|
||
|
self.assertEqual(len(logs), 1)
|
||
|
self.assertEqual(len(session._trace), 1)
|
||
|
|
||
|
self.hit('2024-01-01 08:00:01', '/test_http/greeting-public?readonly=0', ip=TEST_IP)
|
||
|
|
||
|
devices, logs = self.get_devices_logs(self.user_admin)
|
||
|
self.assertEqual(len(devices), 1)
|
||
|
self.assertEqual(len(logs), 2)
|
||
|
self.assertEqual(len(session._trace), 2)
|
||
|
self.assertNotEqual(self.info_trace(session._trace[0])['ip_address'], TEST_IP)
|
||
|
self.assertEqual(self.info_trace(session._trace[1])['ip_address'], TEST_IP)
|
||
|
|
||
|
localized_device = devices.filtered(lambda device: device.ip_address == TEST_IP)
|
||
|
self.assertEqual(localized_device.country, 'France')
|
||
|
|
||
|
def test_detection_usurpation_sid(self):
|
||
|
session = self.authenticate(self.user_internal.login, self.user_internal.login)
|
||
|
self.hit('2024-01-01 08:00:00', '/test_http/greeting-user?readonly=0')
|
||
|
|
||
|
self.hit('2024-01-01 08:00:00', '/test_http/greeting-user?readonly=0', headers={'session_id': session.sid}, ip=TEST_IP)
|
||
|
devices, logs = self.get_devices_logs(self.user_internal)
|
||
|
self.assertEqual(len(devices), 1)
|
||
|
self.assertEqual(len(logs), 2)
|
||
|
self.assertEqual(len(self.user_internal.device_ids), 1)
|
||
|
|
||
|
def test_detection_devices_according_to_time_useragent(self):
|
||
|
self.authenticate(self.user_admin.login, self.user_admin.login)
|
||
|
self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0', headers={'User-Agent': USER_AGENT_linux_chrome})
|
||
|
self.assertEqual(len(self.user_admin.device_ids), 1)
|
||
|
|
||
|
self.hit('2024-01-01 09:00:00', '/test_http/greeting-public?readonly=0', headers={'User-Agent': USER_AGENT_linux_chrome})
|
||
|
self.assertEqual(len(self.user_admin.device_ids), 1)
|
||
|
|
||
|
self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0', headers={'User-Agent': USER_AGENT_linux_firefox})
|
||
|
self.assertEqual(len(self.user_admin.device_ids), 2)
|
||
|
|
||
|
self.hit('2024-01-01 09:00:00', '/test_http/greeting-public?readonly=0', headers={'User-Agent': USER_AGENT_linux_firefox})
|
||
|
self.assertEqual(len(self.user_admin.device_ids), 2)
|
||
|
|
||
|
def test_detection_devices_according_to_user_or_admin(self):
|
||
|
self.authenticate(self.user_admin.login, self.user_admin.login)
|
||
|
self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0')
|
||
|
self.hit('2024-01-01 09:00:00', '/test_http/greeting-public?readonly=0')
|
||
|
self.authenticate(self.user_internal.login, self.user_internal.login)
|
||
|
self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0')
|
||
|
self.hit('2024-01-01 09:00:00', '/test_http/greeting-public?readonly=0')
|
||
|
|
||
|
devices, logs = self.get_devices_logs()
|
||
|
self.assertEqual(len(devices), 2)
|
||
|
self.assertEqual(len(logs), 4)
|
||
|
self.assertEqual(len(self.user_admin.device_ids), 1)
|
||
|
self.assertEqual(len(self.user_internal.device_ids), 1)
|
||
|
|
||
|
devices_from_admin = self.Device.with_user(self.user_admin).search([])
|
||
|
devices_from_internal = self.Device.with_user(self.user_internal).search([])
|
||
|
self.assertEqual(len(devices_from_admin), 2)
|
||
|
self.assertEqual(len(devices_from_internal), 1)
|
||
|
|
||
|
def test_differentiate_computer_and_mobile(self):
|
||
|
self.authenticate(self.user_admin.login, self.user_admin.login)
|
||
|
self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0', headers={'User-Agent': USER_AGENT_linux_chrome})
|
||
|
self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0', headers={'User-Agent': USER_AGENT_android_chrome})
|
||
|
|
||
|
devices, logs = self.get_devices_logs(self.user_admin)
|
||
|
self.assertEqual(len(devices), 2)
|
||
|
self.assertEqual(len(logs), 2)
|
||
|
|
||
|
laptop_device = devices.filtered(lambda device: device.device_type == 'computer')
|
||
|
mobile_device = devices.filtered(lambda device: device.device_type == 'mobile')
|
||
|
self.assertEqual(len(laptop_device), 1)
|
||
|
self.assertEqual(len(mobile_device), 1)
|
||
|
|
||
|
def test_retrieve_linked_ip_addresses(self):
|
||
|
self.authenticate(self.user_admin.login, self.user_admin.login)
|
||
|
self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0', ip='193.0.3.43')
|
||
|
self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0', ip='192.0.2.42')
|
||
|
self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0', ip='191.0.1.41')
|
||
|
|
||
|
devices, _ = self.get_devices_logs(self.user_admin)
|
||
|
self.assertEqual(len(devices), 1)
|
||
|
self.assertIn('193.0.3.43', devices.linked_ip_addresses)
|
||
|
self.assertIn('192.0.2.42', devices.linked_ip_addresses)
|
||
|
self.assertIn('191.0.1.41', devices.linked_ip_addresses)
|
||
|
|
||
|
def test_retrieve_linked_ip_addresses_according_to_devices(self):
|
||
|
self.authenticate(self.user_admin.login, self.user_admin.login)
|
||
|
self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0', headers={'User-Agent': USER_AGENT_linux_chrome}, ip='193.0.3.43')
|
||
|
self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0', headers={'User-Agent': USER_AGENT_linux_chrome}, ip='192.0.2.42')
|
||
|
self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0', headers={'User-Agent': USER_AGENT_linux_firefox}, ip='191.0.1.41')
|
||
|
|
||
|
devices, _ = self.get_devices_logs(self.user_admin)
|
||
|
self.assertEqual(len(devices), 2)
|
||
|
device_chrome = devices.filtered(lambda device: device.browser == 'chrome')
|
||
|
device_firefox = devices.filtered(lambda device: device.browser == 'firefox')
|
||
|
self.assertIn('193.0.3.43', device_chrome.linked_ip_addresses)
|
||
|
self.assertIn('192.0.2.42', device_chrome.linked_ip_addresses)
|
||
|
self.assertNotIn('191.0.1.41', device_chrome.linked_ip_addresses)
|
||
|
self.assertIn('191.0.1.41', device_firefox.linked_ip_addresses)
|
||
|
|
||
|
def test_detection_no_trace_mechanism(self):
|
||
|
session = self.authenticate(self.user_admin.login, self.user_admin.login)
|
||
|
session._trace_disable = True
|
||
|
odoo.http.root.session_store.save(session)
|
||
|
res = self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0')
|
||
|
self.assertEqual(res.status_code, 200)
|
||
|
devices, logs = self.get_devices_logs(self.user_admin)
|
||
|
self.assertEqual(len(devices), 0)
|
||
|
self.assertEqual(len(logs), 0)
|
||
|
|
||
|
def test_detection_device_default_order(self):
|
||
|
self.authenticate(self.user_admin.login, self.user_admin.login)
|
||
|
self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0', headers={'User-Agent': USER_AGENT_linux_chrome})
|
||
|
self.hit('2024-01-01 10:00:00', '/test_http/greeting-public?readonly=0', headers={'User-Agent': USER_AGENT_linux_firefox})
|
||
|
self.hit('2024-01-01 09:00:00', '/test_http/greeting-public?readonly=0', headers={'User-Agent': USER_AGENT_android_chrome})
|
||
|
devices, _ = self.get_devices_logs(self.user_admin)
|
||
|
self.assertEqual(
|
||
|
list(zip(devices.mapped('platform'), devices.mapped('browser'))),
|
||
|
[('linux', 'firefox'), ('android', 'chrome'), ('linux', 'chrome')],
|
||
|
"By default, devices should be found from the most recent to the least recent (according to their last activity)."
|
||
|
)
|
||
|
|
||
|
# --------------------
|
||
|
# DELETION
|
||
|
# --------------------
|
||
|
|
||
|
def test_deletion_device(self):
|
||
|
"""
|
||
|
A user is authenticated and the administrator
|
||
|
wants to block his device (and therefore its session).
|
||
|
"""
|
||
|
self.authenticate(self.user_internal.login, self.user_internal.login)
|
||
|
res = self.hit('2024-01-01 08:00:00', '/test_http/greeting-user?readonly=0')
|
||
|
self.assertNotIn('/web/login', res.url)
|
||
|
|
||
|
user_internal_device = self.user_internal.device_ids
|
||
|
self.assertEqual(len(user_internal_device), 1)
|
||
|
self.assertEqual(user_internal_device.revoked, False)
|
||
|
|
||
|
user_internal_device._revoke()
|
||
|
|
||
|
res = self.hit('2024-01-01 08:00:01', '/test_http/greeting-user?readonly=0')
|
||
|
self.assertIn('/web/login', res.url)
|
||
|
|
||
|
def test_deletion_invalidate_sid(self):
|
||
|
session = self.authenticate(self.user_internal.login, self.user_internal.login)
|
||
|
self.hit('2024-01-01 08:00:00', '/test_http/greeting-user?readonly=0')
|
||
|
|
||
|
self.user_internal.device_ids._revoke()
|
||
|
|
||
|
res = self.hit('2024-01-01 08:00:00', '/test_http/greeting-user?readonly=0', headers={'session_id': session.sid})
|
||
|
self.assertIn('/web/login', res.url)
|
||
|
|
||
|
def test_deletion_specific_device(self):
|
||
|
self.authenticate(self.user_admin.login, self.user_admin.login)
|
||
|
self.hit('2024-01-01 08:00:00', '/test_http/greeting-user?readonly=0', headers={'User-Agent': USER_AGENT_linux_chrome})
|
||
|
self.hit('2024-01-01 09:00:00', '/test_http/greeting-user?readonly=0', headers={'User-Agent': USER_AGENT_linux_chrome})
|
||
|
self.authenticate(self.user_admin.login, self.user_admin.login)
|
||
|
self.hit('2024-01-01 08:00:00', '/test_http/greeting-user?readonly=0', headers={'User-Agent': USER_AGENT_linux_chrome})
|
||
|
self.hit('2024-01-01 09:00:00', '/test_http/greeting-user?readonly=0', headers={'User-Agent': USER_AGENT_linux_chrome})
|
||
|
self.hit('2024-01-01 08:00:00', '/test_http/greeting-user?readonly=0', headers={'User-Agent': USER_AGENT_linux_firefox})
|
||
|
|
||
|
devices, logs = self.get_devices_logs(self.user_admin)
|
||
|
self.assertEqual(len(devices), 3)
|
||
|
self.assertEqual(len(logs), 5)
|
||
|
self.assertEqual(len(self.user_admin.device_ids), 3)
|
||
|
|
||
|
self.user_admin.device_ids.filtered(lambda device: 'firefox' in device.browser)._revoke()
|
||
|
|
||
|
res = self.hit('2024-01-01 08:00:30', '/test_http/greeting-user?readonly=0', headers={'User-Agent': USER_AGENT_linux_firefox})
|
||
|
self.assertIn('/web/login', res.url)
|
||
|
|
||
|
# --------------------
|
||
|
# SPECIFIC USE CASE
|
||
|
# --------------------
|
||
|
|
||
|
def test_specific_public_user_write(self):
|
||
|
"""
|
||
|
A public user who hits a non-readonly route
|
||
|
does not have to create a session file if there
|
||
|
are no changes in the session itself.
|
||
|
"""
|
||
|
session = self.authenticate(None, None)
|
||
|
self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0')
|
||
|
|
||
|
# As we don't have a uid in the session, we shouldn't go through
|
||
|
# the session check and therefore we won't go through the device update.
|
||
|
# `authenticate` method in the test is not the real method.
|
||
|
# To check that we are not creating a session (by making it dirty),
|
||
|
# we can check that there is no `_trace`.
|
||
|
# This means that the device logic will not create a session file
|
||
|
# (because we are not passing in the `_update_device` logic).
|
||
|
self.assertFalse(session._trace)
|