diff --git a/auth_brute_force/README.rst b/auth_brute_force/README.rst index 60b0a73aa..2ad67486a 100644 --- a/auth_brute_force/README.rst +++ b/auth_brute_force/README.rst @@ -1,5 +1,6 @@ .. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg - :alt: License + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 =============================================================== Tracks Authentication Attempts and Prevents Brute-force Attacks @@ -18,27 +19,15 @@ will **not** indicate to the user that his IP is banned and the regular message This module realizes a call to a web API (http://ip-api.com) to try to have extra information about remote IP. -Known issue / Roadmap ---------------------- -The ID used to identify a remote request is the IP provided in the request -(key 'REMOTE_ADDR'). -Depending of server and / or user network configuration, the idenfication -of the user can be wrong, and mainly in the following cases: - -* if the Odoo server is behind an Apache / NGinx proxy without redirection, - all the request will be have the value '127.0.0.1' for the REMOTE_ADDR key; -* If some users are behind the same Internet Service Provider, if a user is - banned, all the other users will be banned too; - Configuration -------------- +============= Once installed, you can change the ir.config_parameter value for the key 'auth_brute_force.max_attempt_qty' (10 by default) that define the max number of attempts allowed before the user was banned. Usage ------ +===== Admin user have the possibility to unblock a banned IP. @@ -69,26 +58,33 @@ Screenshot .. image:: /auth_brute_force/static/description/screenshot_custom_ban.png -Usage -===== - -* go to ... - .. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas - :alt: Try me on Runbot - :target: https://runbot.odoo-community.org/runbot/149/8.0 +:alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/149/10.0 For further information, please visit: * https://www.odoo.com/forum/help-1 +Known issues / Roadmap +====================== + +* The ID used to identify a remote request is the IP provided in the request + (key 'REMOTE_ADDR'). +* Depending of server and / or user network configuration, the idenfication + of the user can be wrong, and mainly in the following cases: +* If the Odoo server is behind an Apache / NGinx proxy without redirection, + all the request will be have the value '127.0.0.1' for the REMOTE_ADDR key; +* If some users are behind the same Internet Service Provider, if a user is + banned, all the other users will be banned too; + 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 -`here `_. +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 smash it by providing detailed and welcomed feedback. Credits ======= @@ -97,13 +93,14 @@ Contributors ------------ * Sylvain LE GAL (https://twitter.com/legalsylvain) +* David Vidal Maintainer ---------- -.. image:: http://odoo-community.org/logo.png +.. image:: https://odoo-community.org/logo.png :alt: Odoo Community Association - :target: http://odoo-community.org + :target: https://odoo-community.org This module is maintained by the OCA. @@ -111,4 +108,4 @@ 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 http://odoo-community.org. +To contribute to this module, please visit https://odoo-community.org. diff --git a/auth_brute_force/__init__.py b/auth_brute_force/__init__.py index b8166bd36..1f9880145 100644 --- a/auth_brute_force/__init__.py +++ b/auth_brute_force/__init__.py @@ -1,3 +1,4 @@ # -*- encoding: utf-8 -*- + from . import models from . import controllers diff --git a/auth_brute_force/__manifest__.py b/auth_brute_force/__manifest__.py new file mode 100644 index 000000000..1af88faed --- /dev/null +++ b/auth_brute_force/__manifest__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 GRAP - Sylvain LE GAL +# Copyright 2017 Tecnativa - David Vidal +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + 'name': 'Authentification - Brute-force Attack', + 'version': '10.0.1.0.0', + 'category': 'Tools', + 'summary': "Tracks Authentication Attempts and Prevents Brute-force" + " Attacks module", + 'author': "GRAP, " + "Tecnativa, " + "Odoo Community Association (OCA)", + 'website': 'http://www.grap.coop', + 'license': 'AGPL-3', + 'depends': [ + 'web', + ], + 'data': [ + 'security/ir_model_access.yml', + 'data/ir_config_parameter.xml', + 'views/view.xml', + 'views/action.xml', + 'views/menu.xml', + ], + 'installable': True, +} diff --git a/auth_brute_force/__openerp__.py b/auth_brute_force/__openerp__.py deleted file mode 100644 index b05790164..000000000 --- a/auth_brute_force/__openerp__.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- encoding: utf-8 -*- -############################################################################## -# -# Tracks Authentication Attempts and Prevents Brute-force Attacks module -# Copyright (C) 2015-Today GRAP (http://www.grap.coop) -# @author Sylvain LE GAL (https://twitter.com/legalsylvain) -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - -{ - 'name': 'Authentification - Brute-force Attack', - 'version': '8.0.1.0.0', - 'category': 'base', - 'summary': "Tracks Authentication Attempts and Prevents Brute-force" - " Attacks module", - 'author': "GRAP,Odoo Community Association (OCA)", - 'website': 'http://www.grap.coop', - 'license': 'AGPL-3', - 'depends': [ - 'web', - ], - 'data': [ - 'security/ir_model_access.yml', - 'data/ir_config_parameter.xml', - 'views/view.xml', - 'views/action.xml', - 'views/menu.xml', - ], -} diff --git a/auth_brute_force/controllers/__init__.py b/auth_brute_force/controllers/__init__.py index 153a9e31e..65a8c1201 100644 --- a/auth_brute_force/controllers/__init__.py +++ b/auth_brute_force/controllers/__init__.py @@ -1,2 +1,3 @@ # -*- coding: utf-8 -*- -from . import controllers + +from . import main diff --git a/auth_brute_force/controllers/controllers.py b/auth_brute_force/controllers/controllers.py deleted file mode 100644 index f752eee95..000000000 --- a/auth_brute_force/controllers/controllers.py +++ /dev/null @@ -1,104 +0,0 @@ -# -*- encoding: utf-8 -*- -############################################################################## -# -# Tracks Authentication Attempts and Prevents Brute-force Attacks module -# Copyright (C) 2015-Today GRAP (http://www.grap.coop) -# @author Sylvain LE GAL (https://twitter.com/legalsylvain) -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - -import logging - -from openerp import fields, http, registry, SUPERUSER_ID -from openerp.http import request -from openerp.addons.web.controllers.main import Home, ensure_db - -_logger = logging.getLogger(__name__) - - -class LoginController(Home): - @http.route() - def web_login(self, redirect=None, **kw): - if request.httprequest.method == 'POST': - ensure_db() - remote = request.httprequest.remote_addr - # Get registry and cursor - config_obj = registry(request.session.db)['ir.config_parameter'] - attempt_obj = registry( - request.session.db)['res.authentication.attempt'] - banned_remote_obj = registry( - request.session.db)['res.banned.remote'] - cursor = attempt_obj.pool.cursor() - - # Get Settings - max_attempts_qty = int(config_obj.search_read( - cursor, SUPERUSER_ID, - [('key', '=', 'auth_brute_force.max_attempt_qty')], - ['value'])[0]['value']) - - # Test if remote user is banned - banned = banned_remote_obj.search(cursor, SUPERUSER_ID, [ - ('remote', '=', remote)]) - if banned: - _logger.warning( - "Authentication tried from remote '%s'. The request has" - " been ignored because the remote has been banned after" - " %d attempts without success. Login tried : '%s'." % ( - remote, max_attempts_qty, request.params['login'])) - request.params['password'] = '' - - else: - # Try to authenticate - result = request.session.authenticate( - request.session.db, request.params['login'], - request.params['password']) - - # Log attempt - cursor.commit() - attempt_obj.create(cursor, SUPERUSER_ID, { - 'attempt_date': fields.Datetime.now(), - 'login': request.params['login'], - 'remote': remote, - 'result': banned and 'banned' or ( - result and 'successfull' or 'failed'), - }) - cursor.commit() - if not banned and not result: - # Get last bad attempts quantity - attempts_qty = len(attempt_obj.search_last_failed( - cursor, SUPERUSER_ID, remote)) - - if max_attempts_qty <= attempts_qty: - # We ban the remote - _logger.warning( - "Authentication failed from remote '%s'. " - "The remote has been banned. Login tried : '%s'." % ( - remote, request.params['login'])) - banned_remote_obj.create(cursor, SUPERUSER_ID, { - 'remote': remote, - 'ban_date': fields.Datetime.now(), - }) - cursor.commit() - - else: - _logger.warning( - "Authentication failed from remote '%s'." - " Login tried : '%s'. Attempt %d / %d." % ( - remote, request.params['login'], attempts_qty, - max_attempts_qty)) - cursor.close() - - return super(LoginController, self).web_login(redirect=redirect, **kw) diff --git a/auth_brute_force/controllers/main.py b/auth_brute_force/controllers/main.py new file mode 100644 index 000000000..222a62bdb --- /dev/null +++ b/auth_brute_force/controllers/main.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 GRAP - Sylvain LE GAL +# Copyright 2017 Tecnativa - David Vidal +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import logging + +from odoo import fields, http, registry, SUPERUSER_ID +from odoo.api import Environment +from odoo.http import request +from odoo.addons.web.controllers.main import Home, ensure_db + +_logger = logging.getLogger(__name__) + + +class LoginController(Home): + + @http.route() + def web_login(self, redirect=None, **kw): + if request.httprequest.method == 'POST': + ensure_db() + remote = request.httprequest.remote_addr + # Get registry and cursor + with registry(request.session.db).cursor() as cursor: + env = Environment(cursor, SUPERUSER_ID, {}) + config_obj = env['ir.config_parameter'] + attempt_obj = env['res.authentication.attempt'] + banned_remote_obj = env['res.banned.remote'] + # Get Settings + max_attempts_qty = int(config_obj.get_param( + 'auth_brute_force.max_attempt_qty')) + # Test if remote user is banned + banned = banned_remote_obj.search([('remote', '=', remote)]) + if banned: + request.params['password'] = '' + _logger.warning( + "Authentication tried from remote '%s'. The request " + "has been ignored because the remote has been banned " + "after %d attempts without success. Login tried : '%s'" + "." % (remote, max_attempts_qty, + request.params['login'])) + else: + # Try to authenticate + result = request.session.authenticate( + request.session.db, request.params['login'], + request.params['password']) + # Log attempt + attempt_obj.create({ + 'attempt_date': fields.Datetime.now(), + 'login': request.params['login'], + 'remote': remote, + 'result': banned and 'banned' or ( + result and 'successfull' or 'failed'), + }) + cursor.commit() + if not banned and not result: + # Get last bad attempts quantity + attempts_qty = len(attempt_obj.search_last_failed(remote)) + if max_attempts_qty <= attempts_qty: + # We ban the remote + _logger.warning( + "Authentication failed from remote '%s'. " + "The remote has been banned. Login tried : '%s'" + "." % (remote, request.params['login'])) + banned_remote_obj.sudo().create({ + 'remote': remote, + 'ban_date': fields.Datetime.now(), + }) + cursor.commit() + else: + _logger.warning( + "Authentication failed from remote '%s'." + " Login tried : '%s'. Attempt %d / %d." % ( + remote, request.params['login'], attempts_qty, + max_attempts_qty)) + return super(LoginController, self).web_login(redirect=redirect, **kw) diff --git a/auth_brute_force/data/ir_config_parameter.xml b/auth_brute_force/data/ir_config_parameter.xml index 0eab93cd2..4fe744f32 100644 --- a/auth_brute_force/data/ir_config_parameter.xml +++ b/auth_brute_force/data/ir_config_parameter.xml @@ -1,24 +1,9 @@ - - - - + + - - - - - - - - - - - - - - - + auth_brute_force.max_attempt_qty @@ -26,4 +11,5 @@ - + + diff --git a/auth_brute_force/i18n/auth_brute_force.pot b/auth_brute_force/i18n/auth_brute_force.pot deleted file mode 100644 index 52f9bf6f4..000000000 --- a/auth_brute_force/i18n/auth_brute_force.pot +++ /dev/null @@ -1,150 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * auth_brute_force -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 8.0\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-09-26 00:39+0000\n" -"PO-Revision-Date: 2015-09-26 00:39+0000\n" -"Last-Translator: <>\n" -"Language-Team: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Plural-Forms: \n" - -#. module: auth_brute_force -#: code:addons/auth_brute_force/models/res_banned_remote.py:75 -#, python-format -msgid "%s %s - %s %s (ISP: %s)" -msgstr "" - -#. module: auth_brute_force -#: field:res.banned.remote,active:0 -msgid "Active" -msgstr "" - -#. module: auth_brute_force -#: field:res.authentication.attempt,attempt_date:0 -msgid "Attempt Date" -msgstr "" - -#. module: auth_brute_force -#: model:ir.actions.act_window,name:auth_brute_force.action_res_authentication_attempt -#: model:ir.ui.menu,name:auth_brute_force.menu_res_authentication_attempt -msgid "Authentication Attempts" -msgstr "" - -#. module: auth_brute_force -#: field:res.authentication.attempt,result:0 -msgid "Authentication Result" -msgstr "" - -#. module: auth_brute_force -#: field:res.banned.remote,ban_date:0 -msgid "Ban Date" -msgstr "" - -#. module: auth_brute_force -#: code:addons/auth_brute_force/models/res_authentication_attempt.py:34 -#: view:res.authentication.attempt:auth_brute_force.view_res_authentication_attempt_search -#: selection:res.authentication.attempt,result:0 -#, python-format -msgid "Banned" -msgstr "" - -#. module: auth_brute_force -#: model:ir.actions.act_window,name:auth_brute_force.action_res_banned_remote -#: model:ir.ui.menu,name:auth_brute_force.menu_res_banned_remote -msgid "Banned Remotes" -msgstr "" - -#. module: auth_brute_force -#: field:res.authentication.attempt,create_uid:0 -#: field:res.banned.remote,create_uid:0 -msgid "Created by" -msgstr "" - -#. module: auth_brute_force -#: field:res.authentication.attempt,create_date:0 -#: field:res.banned.remote,create_date:0 -msgid "Created on" -msgstr "" - -#. module: auth_brute_force -#: code:addons/auth_brute_force/models/res_authentication_attempt.py:33 -#: view:res.authentication.attempt:auth_brute_force.view_res_authentication_attempt_search -#: selection:res.authentication.attempt,result:0 -#, python-format -msgid "Failed" -msgstr "" - -#. module: auth_brute_force -#: field:res.authentication.attempt,id:0 -#: field:res.banned.remote,id:0 -msgid "ID" -msgstr "" - -#. module: auth_brute_force -#: field:res.authentication.attempt,write_uid:0 -#: field:res.banned.remote,write_uid:0 -msgid "Last Updated by" -msgstr "" - -#. module: auth_brute_force -#: field:res.authentication.attempt,write_date:0 -#: field:res.banned.remote,write_date:0 -msgid "Last Updated on" -msgstr "" - -#. module: auth_brute_force -#: field:res.banned.remote,name:0 -msgid "Name" -msgstr "" - -#. module: auth_brute_force -#: field:res.banned.remote,description:0 -msgid "Remote Description" -msgstr "" - -#. module: auth_brute_force -#: field:res.authentication.attempt,remote:0 -#: field:res.banned.remote,remote:0 -msgid "Remote ID" -msgstr "" - -#. module: auth_brute_force -#: view:res.authentication.attempt:auth_brute_force.view_res_authentication_attempt_search -msgid "Successful" -msgstr "" - -#. module: auth_brute_force -#: code:addons/auth_brute_force/models/res_authentication_attempt.py:32 -#: selection:res.authentication.attempt,result:0 -#, python-format -msgid "Successfull" -msgstr "" - -#. module: auth_brute_force -#: field:res.authentication.attempt,login:0 -msgid "Tried Login" -msgstr "" - -#. module: auth_brute_force -#: help:res.banned.remote,active:0 -msgid "Uncheck this box to unban the remote" -msgstr "" - -#. module: auth_brute_force -#: code:addons/auth_brute_force/models/res_banned_remote.py:77 -#, python-format -msgid "Unidentified Call from %s" -msgstr "" - -#. module: auth_brute_force -#: view:res.authentication.attempt:auth_brute_force.view_res_authentication_attempt_search -msgid "Without Success" -msgstr "" - diff --git a/auth_brute_force/models/__init__.py b/auth_brute_force/models/__init__.py index 85cc3145c..f5bb7766e 100644 --- a/auth_brute_force/models/__init__.py +++ b/auth_brute_force/models/__init__.py @@ -1,3 +1,4 @@ # -*- encoding: utf-8 -*- + from . import res_banned_remote from . import res_authentication_attempt diff --git a/auth_brute_force/models/res_authentication_attempt.py b/auth_brute_force/models/res_authentication_attempt.py index 84e735bd3..a75542816 100644 --- a/auth_brute_force/models/res_authentication_attempt.py +++ b/auth_brute_force/models/res_authentication_attempt.py @@ -1,27 +1,8 @@ -# -*- encoding: utf-8 -*- -############################################################################## -# -# Tracks Authentication Attempts and Prevents Brute-force Attacks module -# Copyright (C) 2015-Today GRAP (http://www.grap.coop) -# @author Sylvain LE GAL (https://twitter.com/legalsylvain) -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## +# -*- coding: utf-8 -*- +# Copyright 2015 GRAP - Sylvain LE GAL +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from openerp import models, fields, api -from openerp.tools.translate import _ +from odoo import _, api, fields, models class ResAuthenticationAttempt(models.Model): @@ -36,11 +17,8 @@ class ResAuthenticationAttempt(models.Model): # Column Section attempt_date = fields.Datetime(string='Attempt Date') - login = fields.Char(string='Tried Login') - remote = fields.Char(string='Remote ID') - result = fields.Selection( selection=_ATTEMPT_RESULT, string='Authentication Result') diff --git a/auth_brute_force/models/res_banned_remote.py b/auth_brute_force/models/res_banned_remote.py index 661dfc8a5..a10caad47 100644 --- a/auth_brute_force/models/res_banned_remote.py +++ b/auth_brute_force/models/res_banned_remote.py @@ -1,29 +1,11 @@ -# -*- encoding: utf-8 -*- -############################################################################## -# -# Tracks Authentication Attempts and Prevents Brute-force Attacks module -# Copyright (C) 2015-Today GRAP (http://www.grap.coop) -# @author Sylvain LE GAL (https://twitter.com/legalsylvain) -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## +# -*- coding: utf-8 -*- +# Copyright 2015 GRAP - Sylvain LE GAL +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). import urllib import json -from openerp import models, fields, api +from odoo import api, fields, models class ResBannedRemote(models.Model): @@ -32,23 +14,15 @@ class ResBannedRemote(models.Model): _GEOLOCALISATION_URL = "http://ip-api.com/json/{}" - # Default Section - def _default_ban_date(self): - return fields.Datetime.now() - # Column Section description = fields.Text( string='Description', compute='_compute_description', store=True) - ban_date = fields.Datetime( - string='Ban Date', required=True, default=_default_ban_date) - + string='Ban Date', required=True, default=fields.Datetime.now) remote = fields.Char(string='Remote ID', required=True) - active = fields.Boolean( string='Active', help="Uncheck this box to unban the remote", default=True) - attempt_ids = fields.Many2many( comodel_name='res.authentication.attempt', string='Attempts', compute='_compute_attempt_ids') @@ -68,4 +42,4 @@ class ResBannedRemote(models.Model): def _compute_attempt_ids(self): for item in self: attempt_obj = self.env['res.authentication.attempt'] - item.attempt_ids = attempt_obj.search_last_failed(item.remote).ids + item.attempt_ids = attempt_obj.search_last_failed(item.remote) diff --git a/auth_brute_force/views/action.xml b/auth_brute_force/views/action.xml index 7b19a7e90..ea7ac4862 100644 --- a/auth_brute_force/views/action.xml +++ b/auth_brute_force/views/action.xml @@ -1,24 +1,7 @@ - - - - - - - - - - - - - - - - - - - - + + Authentication Attempts @@ -35,5 +18,4 @@ tree,form - - + diff --git a/auth_brute_force/views/menu.xml b/auth_brute_force/views/menu.xml index 99661eeb4..cd246ae33 100644 --- a/auth_brute_force/views/menu.xml +++ b/auth_brute_force/views/menu.xml @@ -1,24 +1,7 @@ - - - - - - - - - - - - - - - - - - - - + + - - + diff --git a/auth_brute_force/views/view.xml b/auth_brute_force/views/view.xml index 7b7de28c3..4865978d7 100644 --- a/auth_brute_force/views/view.xml +++ b/auth_brute_force/views/view.xml @@ -1,24 +1,7 @@ - - - - - - - - - - - - - - - - - - - - + + @@ -94,5 +77,4 @@ - - +