Enric Tobella
6 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