diff --git a/auth_from_http_remote_user/__init__.py b/auth_from_http_remote_user/__init__.py new file mode 100644 index 000000000..7fb1b9d59 --- /dev/null +++ b/auth_from_http_remote_user/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Laurent Mignon +# Copyright 2014 'ACSONE SA/NV' +# +# 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 . +# +############################################################################## + +from . import controllers +from . import res_users +from . import model diff --git a/auth_from_http_remote_user/__openerp__.py b/auth_from_http_remote_user/__openerp__.py new file mode 100644 index 000000000..d4e9731c2 --- /dev/null +++ b/auth_from_http_remote_user/__openerp__.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Laurent Mignon +# Copyright 2014 'ACSONE SA/NV' +# +# 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': 'Authenticate via HTTP Remote User', + 'version': '1.0', + 'category': 'Tools', + 'description': """ +Allow users to be automatically logged in. +========================================== + +This module initialize the session by looking for the field HTTP_REMOTE_USER in +the HEADER of the HTTP request and trying to bind the given value to a user. +To be active, the module must be installed in the expected databases and loaded +at startup; Add the *--load* parameter to the startup command: :: + + --load=web,web_kanban,auth_from_http_remote_user, ... + +If the field is found in the header and no user matches the given one, the +system issue a login error page. (*401* `Unauthorized`) + +Use case. +--------- + +The module allows integration with external security systems [#]_ that can pass +along authentication of a user via Remote_User HTTP header field. In many +cases, this is achieved via server like Apache HTTPD or nginx proxying Odoo. + +.. important:: When proxying your Odoo server with Apache or nginx, It's + important to filter out the Remote_User HTTP header field before your + request is processed by the proxy to avoid security issues. In apache you + can do it by using the RequestHeader directive in your VirtualHost + section :: + + + ServerName MY_VHOST.com + ProxyRequests Off + ... + + RequestHeader unset Remote-User early + ProxyPass / http://127.0.0.1:8069/ retry=10 + ProxyPassReverse / http://127.0.0.1:8069/ + ProxyPreserveHost On + + + +How to test the module with Apache [#]_ +---------------------------------------- + +Apache can be used as a reverse proxy providing the authentication and adding +the required field in the Http headers. + +Install apache: :: + + $ sudo apt-get install apache2 + + +Define a new vhost to Apache by putting a new file in +/etc/apache2/sites-available: :: + + $ sudo vi /etc/apache2/sites-available/MY_VHOST.com + +with the following content: :: + + + ServerName MY_VHOST.com + ProxyRequests Off + + AuthType Basic + AuthName "Test Odoo auth_from_http_remote_user" + AuthBasicProvider file + AuthUserFile /etc/apache2/MY_VHOST.htpasswd + Require valid-user + + RewriteEngine On + RewriteCond %{LA-U:REMOTE_USER} (.+) + RewriteRule . - [E=RU:%1] + RequestHeader set Remote-User "%{RU}e" env=RU + + + RequestHeader unset Remote-User early + ProxyPass / http://127.0.0.1:8069/ retry=10 + ProxyPassReverse / http://127.0.0.1:8069/ + ProxyPreserveHost On + + +.. important:: The *RequestHeader* directive is used to add the *Remote-User* + field in the http headers. By default an *'Http-'* prefix is added to the + field name. + In Odoo, header's fields name are normalized. As result of this + normalization, the 'Http-Remote-User' is available as 'HTTP_REMOTE_USER'. + If you don't know how your specified field is seen by Odoo, run your + server in debug mode once the module is activated and look for an entry + like: :: + + DEBUG openerp1 openerp.addons.auth_from_http_remote_user.controllers. + session: + Field 'HTTP_MY_REMOTE_USER' not found in http headers + {'HTTP_AUTHORIZATION': 'Basic YWRtaW46YWRtaW4=', ..., + 'HTTP_REMOTE_USER': 'demo') + +Enable the required apache modules: :: + + $ sudo a2enmod headers + $ sudo a2enmod proxy + $ sudo a2enmod rewrite + $ sudo a2enmod proxy_http + +Enable your new vhost: :: + + $ sudo a2ensite MY_VHOST.com + +Create the *htpassword* file used by the configured basic authentication: :: + + $ sudo htpasswd -cb /etc/apache2/MY_VHOST.htpasswd admin admin + $ sudo htpasswd -b /etc/apache2/MY_VHOST.htpasswd demo demo + +For local test, add the *MY_VHOST.com* in your /etc/vhosts file. + +Finally reload the configuration: :: + + $ sudo service apache2 reload + +Open your browser and go to MY_VHOST.com. If everything is well configured, you +are prompted for a login and password outside Odoo and are automatically +logged in the system. + +.. [#] Shibolleth, Tivoli access manager, .. +.. [#] Based on a ubuntu 12.04 env + +""", + 'author': 'Acsone SA/NV', + 'maintainer': 'ACSONE SA/NV', + 'website': 'http://www.acsone.eu', + 'depends': ['base', 'web', 'base_setup'], + "license": "AGPL-3", + 'data': [], + "demo": [], + "test": [], + "active": False, + "license": "AGPL-3", + "installable": True, + "auto_install": False, + "application": False, +} diff --git a/auth_from_http_remote_user/controllers/__init__.py b/auth_from_http_remote_user/controllers/__init__.py new file mode 100644 index 000000000..6b12b8082 --- /dev/null +++ b/auth_from_http_remote_user/controllers/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Laurent Mignon +# Copyright 2014 'ACSONE SA/NV' +# +# 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 . +# +############################################################################## + +from . import main diff --git a/auth_from_http_remote_user/controllers/main.py b/auth_from_http_remote_user/controllers/main.py new file mode 100644 index 000000000..85859619a --- /dev/null +++ b/auth_from_http_remote_user/controllers/main.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Laurent Mignon +# Copyright 2014 'ACSONE SA/NV' +# +# 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 . +# +############################################################################## + +from openerp import SUPERUSER_ID + +import openerp +from openerp import http +from openerp.http import request +from openerp.addons.web.controllers import main +from openerp.addons.auth_from_http_remote_user.model import \ + AuthFromHttpRemoteUserInstalled +from .. import utils + +import random +import logging +import werkzeug + +_logger = logging.getLogger(__name__) + + +class Home(main.Home): + + _REMOTE_USER_ATTRIBUTE = 'HTTP_REMOTE_USER' + + @http.route('/web', type='http', auth="none") + def web_client(self, s_action=None, **kw): + main.ensure_db() + try: + self._bind_http_remote_user(http.request.session.db) + except http.AuthenticationError: + return werkzeug.exceptions.Unauthorized().get_response() + return super(Home, self).web_client(s_action, **kw) + + def _search_user(self, res_users, login, cr): + user_ids = res_users.search(cr, SUPERUSER_ID, [('login', '=', login), + ('active', '=', True)]) + assert len(user_ids) < 2 + if user_ids: + return user_ids[0] + return None + + def _bind_http_remote_user(self, db_name): + try: + registry = openerp.registry(db_name) + with registry.cursor() as cr: + if AuthFromHttpRemoteUserInstalled._name not in registry: + # module not installed in database, + # continue usual behavior + return + + headers = http.request.httprequest.headers.environ + + login = headers.get(self._REMOTE_USER_ATTRIBUTE, None) + if not login: + # no HTTP_REMOTE_USER header, + # continue usual behavior + return + + request_login = request.session.login + if request_login: + if request_login == login: + # already authenticated + return + else: + request.session.logout(keep_db=True) + + res_users = registry.get('res.users') + user_id = self._search_user(res_users, login, cr) + if not user_id: + # HTTP_REMOTE_USER login not found in database + request.session.logout(keep_db=True) + raise http.AuthenticationError() + + # generate a specific key for authentication + key = randomString(utils.KEY_LENGTH, '0123456789abcdef') + res_users.write(cr, SUPERUSER_ID, [user_id], {'sso_key': key}) + request.session.authenticate(db_name, login=login, + password=key, uid=user_id) + except http.AuthenticationError, e: + raise e + except Exception, e: + _logger.error("Error binding Http Remote User session", + exc_info=True) + raise e + +randrange = random.SystemRandom().randrange + + +def randomString(length, chrs): + """Produce a string of length random bytes, chosen from chrs.""" + n = len(chrs) + return ''.join([chrs[randrange(n)] for _ in xrange(length)]) diff --git a/auth_from_http_remote_user/model.py b/auth_from_http_remote_user/model.py new file mode 100644 index 000000000..c4ca50a33 --- /dev/null +++ b/auth_from_http_remote_user/model.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Laurent Mignon +# Copyright 2014 'ACSONE SA/NV' +# +# 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 . +# +############################################################################## +from openerp.osv import orm + + +class AuthFromHttpRemoteUserInstalled(orm.AbstractModel): + """An abstract model used to safely know if the module is installed + """ + _name = 'auth_from_http_remote_user.installed' diff --git a/auth_from_http_remote_user/res_users.py b/auth_from_http_remote_user/res_users.py new file mode 100644 index 000000000..638179405 --- /dev/null +++ b/auth_from_http_remote_user/res_users.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Laurent Mignon +# Copyright 2014 'ACSONE SA/NV' +# +# 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 . +# +############################################################################## + +from openerp.modules.registry import RegistryManager +from openerp.osv import orm, fields +from openerp import SUPERUSER_ID +import openerp.exceptions +from openerp.addons.auth_from_http_remote_user import utils + + +class res_users(orm.Model): + _inherit = 'res.users' + + _columns = { + 'sso_key': fields.char('SSO Key', size=utils.KEY_LENGTH, + readonly=True), + } + + def copy(self, cr, uid, rid, defaults=None, context=None): + defaults = defaults or {} + defaults['sso_key'] = False + return super(res_users, self).copy(cr, uid, rid, defaults, context) + + def check_credentials(self, cr, uid, password): + try: + return super(res_users, self).check_credentials(cr, uid, password) + except openerp.exceptions.AccessDenied: + res = self.search(cr, SUPERUSER_ID, [('id', '=', uid), + ('sso_key', '=', password)]) + if not res: + raise openerp.exceptions.AccessDenied() + + def check(self, db, uid, passwd): + try: + return super(res_users, self).check(db, uid, passwd) + except openerp.exceptions.AccessDenied: + if not passwd: + raise + with RegistryManager.get(db).cursor() as cr: + cr.execute('''SELECT COUNT(1) + FROM res_users + WHERE id=%s + AND sso_key=%s + AND active=%s''', (int(uid), passwd, True)) + if not cr.fetchone()[0]: + raise + self._uid_cache.setdefault(db, {})[uid] = passwd diff --git a/auth_from_http_remote_user/tests/__init__.py b/auth_from_http_remote_user/tests/__init__.py new file mode 100644 index 000000000..bf16e2154 --- /dev/null +++ b/auth_from_http_remote_user/tests/__init__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Laurent Mignon +# Copyright 2014 'ACSONE SA/NV' +# +# 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 . +# +############################################################################## + +from . import test_res_users +fast_suite = [ +] + +checks = [ + test_res_users, +] diff --git a/auth_from_http_remote_user/tests/test_res_users.py b/auth_from_http_remote_user/tests/test_res_users.py new file mode 100644 index 000000000..43d5481c6 --- /dev/null +++ b/auth_from_http_remote_user/tests/test_res_users.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Laurent Mignon +# Copyright 2014 'ACSONE SA/NV' +# +# 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 . +# +############################################################################## + +from openerp.tests import common +import mock +import os +from contextlib import contextmanager +import unittest + + +@contextmanager +def mock_cursor(cr): + with mock.patch('openerp.sql_db.Connection.cursor') as mocked_cursor_call: + org_close = cr.close + org_autocommit = cr.autocommit + try: + cr.close = mock.Mock() + cr.autocommit = mock.Mock() + mocked_cursor_call.return_value = cr + yield + finally: + cr.close = org_close + cr.autocommit = org_autocommit + + +class test_res_users(common.TransactionCase): + + def test_login(self): + res_users_obj = self.registry('res.users') + res = res_users_obj.authenticate(common.DB, 'admin', 'admin', None) + uid = res + self.assertTrue(res, "Basic login must works as expected") + token = "123456" + res = res_users_obj.authenticate(common.DB, 'admin', token, None) + self.assertFalse(res) + # mimic what the new controller do when it find a value in + # the http header (HTTP_REMODE_USER) + res_users_obj.write(self.cr, self.uid, uid, {'sso_key': token}) + + # Here we need to mock the cursor since the login is natively done + # inside its own connection + with mock_cursor(self.cr): + # We can verifies that the given (uid, token) is authorized for + # the database + res_users_obj.check(common.DB, uid, token) + + # we are able to login with the new token + res = res_users_obj.authenticate(common.DB, 'admin', token, None) + self.assertTrue(res) + + @unittest.skipIf(os.environ.get('TRAVIS'), + 'When run by travis, tests runs on a database with all ' + 'required addons from server-tools and their dependencies' + ' installed. Even if `auth_from_http_remote_user` does ' + 'not require the `mail` module, The previous installation' + ' of the mail module has created the column ' + '`notification_email_send` as REQUIRED into the table ' + 'res_partner. BTW, it\'s no more possible to copy a ' + 'res_user without an intefirty error') + def test_copy(self): + '''Check that the sso_key is not copied on copy + ''' + res_users_obj = self.registry('res.users') + vals = {'sso_key': '123'} + res_users_obj.write(self.cr, self.uid, self.uid, vals) + read_vals = res_users_obj.read( + self.cr, self.uid, self.uid, ['sso_key']) + self.assertDictContainsSubset(vals, read_vals) + copy = res_users_obj.copy(self.cr, self.uid, self.uid) + read_vals = res_users_obj.read( + self.cr, self.uid, copy, ['sso_key']) + self.assertFalse(read_vals.get('sso_key')) diff --git a/auth_from_http_remote_user/utils.py b/auth_from_http_remote_user/utils.py new file mode 100644 index 000000000..ee1eacf68 --- /dev/null +++ b/auth_from_http_remote_user/utils.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Laurent Mignon +# Copyright 2014 'ACSONE SA/NV' +# +# 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 . +# +############################################################################## + +KEY_LENGTH = 16