From 0f2eab5c74a01aa3e0abf990a1294f7d16843a7f Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Mon, 18 Jun 2018 18:34:40 +0200 Subject: [PATCH] [ADD] remote_base --- remote_base/README.rst | 58 +++++++++++++++++++++ remote_base/__init__.py | 1 + remote_base/__manifest__.py | 18 +++++++ remote_base/models/__init__.py | 5 ++ remote_base/models/base.py | 17 ++++++ remote_base/models/res_remote.py | 49 ++++++++++++++++++ remote_base/models/res_users.py | 53 +++++++++++++++++++ remote_base/security/ir.model.access.csv | 3 ++ remote_base/tests/__init__.py | 1 + remote_base/tests/test_remote.py | 66 ++++++++++++++++++++++++ remote_base/views/res_remote_views.xml | 46 +++++++++++++++++ 11 files changed, 317 insertions(+) create mode 100644 remote_base/README.rst create mode 100644 remote_base/__init__.py create mode 100644 remote_base/__manifest__.py create mode 100644 remote_base/models/__init__.py create mode 100644 remote_base/models/base.py create mode 100644 remote_base/models/res_remote.py create mode 100644 remote_base/models/res_users.py create mode 100644 remote_base/security/ir.model.access.csv create mode 100644 remote_base/tests/__init__.py create mode 100644 remote_base/tests/test_remote.py create mode 100644 remote_base/views/res_remote_views.xml diff --git a/remote_base/README.rst b/remote_base/README.rst new file mode 100644 index 000000000..0183a3d0c --- /dev/null +++ b/remote_base/README.rst @@ -0,0 +1,58 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +=========== +Remote Base +=========== + +This module allows to store all the connected remotes (external ip addresses) to odoo. +It should be used with other modules in order to check remote's configurations. + +Usage +===== + +When installed, all remotes will be stored by `hostname` on `res.remote`. +They can be viewed on `Settings / Users & Companies / Remotes`. +The last Ip of the remote will be stored. + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/144/11.0 + + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed feedback. + +Credits +======= + +Images +------ + +* Odoo Community Association: `Icon `_. + +Contributors +------------ + +* Enric Tobella + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/remote_base/__init__.py b/remote_base/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/remote_base/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/remote_base/__manifest__.py b/remote_base/__manifest__.py new file mode 100644 index 000000000..225e4238e --- /dev/null +++ b/remote_base/__manifest__.py @@ -0,0 +1,18 @@ +# Copyright (c) 2018 Creu Blanca +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + 'name': "Remote Base", + 'version': '11.0.1.0.0', + 'category': 'Generic Modules/Base', + 'author': "Creu Blanca, Odoo Community Association (OCA)", + 'website': 'http://github.com/OCA/server-tools', + 'license': 'AGPL-3', + "depends": ['web', 'base'], + 'data': [ + 'security/ir.model.access.csv', + 'views/res_remote_views.xml', + ], + 'installable': True, + 'application': True, +} diff --git a/remote_base/models/__init__.py b/remote_base/models/__init__.py new file mode 100644 index 000000000..11252a793 --- /dev/null +++ b/remote_base/models/__init__.py @@ -0,0 +1,5 @@ +from . import base +from . import res_remote +from . import res_users + + diff --git a/remote_base/models/base.py b/remote_base/models/base.py new file mode 100644 index 000000000..87bf5fd42 --- /dev/null +++ b/remote_base/models/base.py @@ -0,0 +1,17 @@ +# Copyright 2018 Creu Blanca +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models +from threading import current_thread + + +class Base(models.AbstractModel): + _inherit = 'base' + + @property + def remote(self): + try: + remote_addr = current_thread().environ["REMOTE_ADDR"] + except KeyError: + remote_addr = False + return self.env['res.remote']._get_remote(remote_addr) diff --git a/remote_base/models/res_remote.py b/remote_base/models/res_remote.py new file mode 100644 index 000000000..0abc850da --- /dev/null +++ b/remote_base/models/res_remote.py @@ -0,0 +1,49 @@ +# Copyright 2018 Creu Blanca +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import api, models, fields +import socket +import logging + + +class ResRemote(models.Model): + _name = 'res.remote' + _description = 'Remotes' + + name = fields.Char( + required=True, + string='Hostname', + index=True, + readonly=True + ) + ip = fields.Char(required=True) + in_network = fields.Boolean( + required=True, + help='Shows if the remote can be found through the socket' + ) + + _sql_constraints = [ + ('name_unique', 'unique(name)', 'Hostname must be unique') + ] + + @api.model + def _create_vals(self, addr, hostname): + return { + 'name': hostname or addr, + 'ip': addr, + 'in_network': bool(hostname), + } + + @api.model + def _get_remote(self, addr): + try: + hostname, alias, ips = socket.gethostbyaddr(addr) + except socket.herror: + logging.warning('Remote with ip %s could not be found' % addr) + hostname = False + remote = self.search([('name', '=', hostname or addr)]) + if not remote: + remote = self.create(self._create_vals(addr, hostname)) + if remote.ip != addr: + # IPs can change through time, but hostname should not change + remote.write({'ip': addr}) + return remote diff --git a/remote_base/models/res_users.py b/remote_base/models/res_users.py new file mode 100644 index 000000000..0cfbb4b0f --- /dev/null +++ b/remote_base/models/res_users.py @@ -0,0 +1,53 @@ +# Copyright 2018 Creu Blanca +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from threading import current_thread +from odoo import api, models, SUPERUSER_ID +from odoo.exceptions import AccessDenied +from odoo.service import wsgi_server + + +class ResUsers(models.Model): + _inherit = "res.users" + + # HACK https://github.com/odoo/odoo/issues/24183 + # TODO Remove in v12, and use normal odoo.http.request to get details + @api.model_cr + def _register_hook(self): + """🐒-patch XML-RPC controller to know remote address.""" + original_fn = wsgi_server.application_unproxied + + def _patch(environ, start_response): + current_thread().environ = environ + return original_fn(environ, start_response) + + wsgi_server.application_unproxied = _patch + + @classmethod + def _auth_check_remote(cls, login, method): + """Force a method to raise an AccessDenied on falsey return.""" + with cls.pool.cursor() as cr: + env = api.Environment(cr, SUPERUSER_ID, {}) + remote = env["res.users"].remote + remote.ensure_one() + result = method() + if not result: + # Force exception to record auth failure + raise AccessDenied() + return result + + # Override all auth-related core methods + @classmethod + def _login(cls, db, login, password): + return cls._auth_check_remote( + login, + lambda: super(ResUsers, cls)._login(db, login, password), + ) + + @classmethod + def authenticate(cls, db, login, password, user_agent_env): + return cls._auth_check_remote( + login, + lambda: super(ResUsers, cls).authenticate( + db, login, password, user_agent_env), + ) diff --git a/remote_base/security/ir.model.access.csv b/remote_base/security/ir.model.access.csv new file mode 100644 index 000000000..7642160aa --- /dev/null +++ b/remote_base/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_remote,access_remote,model_res_remote,base.group_user,1,0,0,0 +manage_remote,manage_remote,model_res_remote,base.group_system,1,1,0,0 diff --git a/remote_base/tests/__init__.py b/remote_base/tests/__init__.py new file mode 100644 index 000000000..2dbcf34b1 --- /dev/null +++ b/remote_base/tests/__init__.py @@ -0,0 +1 @@ +from . import test_remote diff --git a/remote_base/tests/test_remote.py b/remote_base/tests/test_remote.py new file mode 100644 index 000000000..056e3a00a --- /dev/null +++ b/remote_base/tests/test_remote.py @@ -0,0 +1,66 @@ +# Copyright 2018 Creu Blanca +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from xmlrpc.client import Fault + +from mock import patch +from werkzeug.utils import redirect + +from odoo import http +from odoo.tests.common import at_install, HttpCase, post_install + + +@at_install(False) +@post_install(True) +# Skip CSRF validation on tests +@patch(http.__name__ + ".WebRequest.validate_csrf", return_value=True) +# Skip specific browser forgery on redirections +@patch(http.__name__ + ".redirect_with_hash", side_effect=redirect) +class TestRemote(HttpCase): + def setUp(self): + super().setUp() + # HACK https://github.com/odoo/odoo/issues/24183 + # TODO Remove in v12 + # Complex password to avoid conflicts with `password_security` + self.good_password = "Admin$%02584" + self.data_demo = { + "login": "demo", + "password": "Demo%&/(908409**", + } + self.remote_addr = '127.0.0.1' + with self.cursor() as cr: + env = self.env(cr) + # Make sure involved users have good passwords + env.user.password = self.good_password + env["res.users"].search([ + ("login", "=", self.data_demo["login"]), + ]).password = self.data_demo["password"] + remote = self.env['res.remote'].search([ + ('ip', '=', self.remote_addr) + ]) + if remote: + remote.unlink() + + def test_xmlrpc_login_ok(self, *args): + """Test Login""" + data1 = self.data_demo + self.assertTrue(self.xmlrpc_common.authenticate( + self.env.cr.dbname, data1["login"], data1["password"], {})) + with self.cursor() as cr: + env = self.env(cr) + self.assertTrue( + env['res.remote'].search([('ip', '=', self.remote_addr)]) + ) + + def test_xmlrpc_login_failure(self, *args): + """Test Login Failure""" + data1 = self.data_demo + data1['password'] = 'Failure!' + with self.assertRaises(Fault): + self.assertFalse(self.xmlrpc_common.authenticate( + self.env.cr.dbname, data1["login"], data1["password"], {})) + with self.cursor() as cr: + env = self.env(cr) + self.assertTrue( + env['res.remote'].search([('ip', '=', self.remote_addr)]) + ) diff --git a/remote_base/views/res_remote_views.xml b/remote_base/views/res_remote_views.xml new file mode 100644 index 000000000..3ceda5de6 --- /dev/null +++ b/remote_base/views/res_remote_views.xml @@ -0,0 +1,46 @@ + + + + res.remote.form + res.remote + +
+ +
+

+
+ + + + + + + +
+
+
+
+ + res.remote.tree + res.remote + + + + + + + + + Remotes + ir.actions.act_window + res.remote + form + tree,form + + + +