Odoo18-Base/odoo/addons/test_http/tests/test_device.py

368 lines
18 KiB
Python
Raw Permalink Normal View History

2025-01-06 10:57:38 +07:00
# 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)