Enric Tobella
7 years ago
11 changed files with 317 additions and 0 deletions
-
58remote_base/README.rst
-
1remote_base/__init__.py
-
18remote_base/__manifest__.py
-
5remote_base/models/__init__.py
-
17remote_base/models/base.py
-
49remote_base/models/res_remote.py
-
53remote_base/models/res_users.py
-
3remote_base/security/ir.model.access.csv
-
1remote_base/tests/__init__.py
-
66remote_base/tests/test_remote.py
-
46remote_base/views/res_remote_views.xml
@ -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 |
||||
|
<https://github.com/OCA/report-print-send/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 <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_. |
||||
|
|
||||
|
Contributors |
||||
|
------------ |
||||
|
|
||||
|
* Enric Tobella <etobella@creublanca.es> |
||||
|
|
||||
|
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. |
@ -0,0 +1 @@ |
|||||
|
from . import models |
@ -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, |
||||
|
} |
@ -0,0 +1,5 @@ |
|||||
|
from . import base |
||||
|
from . import res_remote |
||||
|
from . import res_users |
||||
|
|
||||
|
|
@ -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) |
@ -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 |
@ -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), |
||||
|
) |
@ -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 |
@ -0,0 +1 @@ |
|||||
|
from . import test_remote |
@ -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)]) |
||||
|
) |
@ -0,0 +1,46 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8" ?> |
||||
|
<odoo> |
||||
|
<record id="res_remote_form" model="ir.ui.view"> |
||||
|
<field name="name">res.remote.form</field> |
||||
|
<field name="model">res.remote</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<form string="Remote"> |
||||
|
<sheet> |
||||
|
<div class="oe_title"> |
||||
|
<h1><field name="name"/></h1> |
||||
|
</div> |
||||
|
<group name="technical"> |
||||
|
<group name="network"> |
||||
|
<field name="ip"/> |
||||
|
<field name="in_network"/> |
||||
|
</group> |
||||
|
</group> |
||||
|
<notebook/> |
||||
|
</sheet> |
||||
|
</form> |
||||
|
</field> |
||||
|
</record> |
||||
|
<record id="res_remote_tree" model="ir.ui.view"> |
||||
|
<field name="name">res.remote.tree</field> |
||||
|
<field name="model">res.remote</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<tree string="Remotes"> |
||||
|
<field name="name"/> |
||||
|
<field name="ip"/> |
||||
|
</tree> |
||||
|
</field> |
||||
|
</record> |
||||
|
<record id="res_remote_action" model="ir.actions.act_window"> |
||||
|
<field name="name">Remotes</field> |
||||
|
<field name="type">ir.actions.act_window</field> |
||||
|
<field name="res_model">res.remote</field> |
||||
|
<field name="view_type">form</field> |
||||
|
<field name="view_mode">tree,form</field> |
||||
|
</record> |
||||
|
|
||||
|
<menuitem id="res_remote_menu" |
||||
|
name="Remotes" |
||||
|
sequence="30" |
||||
|
parent="base.menu_users" |
||||
|
action="res_remote_action"/> |
||||
|
</odoo> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue