From 7dd5bc685e5dca562d0d7757348bd51141e7731b Mon Sep 17 00:00:00 2001 From: Laurent Mignon Date: Fri, 25 Jul 2014 15:32:53 +0200 Subject: [PATCH 01/18] [ADD] This module initialize the session by looking for the field HTTP_REMOTE_USER in the HEADER of the HTTP request and trying^Co bind the given value to a user --- auth_from_http_remote_user/__init__.py | 24 ++++ auth_from_http_remote_user/__openerp__.py | 133 ++++++++++++++++++ .../controllers/__init__.py | 22 +++ .../controllers/session.py | 129 +++++++++++++++++ auth_from_http_remote_user/res_config.py | 60 ++++++++ .../res_config_data.xml | 9 ++ .../res_config_view.xml | 39 +++++ auth_from_http_remote_user/res_users.py | 64 +++++++++ .../src/js/auth_from_http_remote_user.js | 36 +++++ auth_from_http_remote_user/tests/__init__.py | 28 ++++ .../tests/test_res_users.py | 83 +++++++++++ auth_from_http_remote_user/utils.py | 22 +++ 12 files changed, 649 insertions(+) create mode 100644 auth_from_http_remote_user/__init__.py create mode 100644 auth_from_http_remote_user/__openerp__.py create mode 100644 auth_from_http_remote_user/controllers/__init__.py create mode 100644 auth_from_http_remote_user/controllers/session.py create mode 100644 auth_from_http_remote_user/res_config.py create mode 100644 auth_from_http_remote_user/res_config_data.xml create mode 100644 auth_from_http_remote_user/res_config_view.xml create mode 100644 auth_from_http_remote_user/res_users.py create mode 100644 auth_from_http_remote_user/static/src/js/auth_from_http_remote_user.js create mode 100644 auth_from_http_remote_user/tests/__init__.py create mode 100644 auth_from_http_remote_user/tests/test_res_users.py create mode 100644 auth_from_http_remote_user/utils.py diff --git a/auth_from_http_remote_user/__init__.py b/auth_from_http_remote_user/__init__.py new file mode 100644 index 000000000..6e0a37c8b --- /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_config +from . import res_users diff --git a/auth_from_http_remote_user/__openerp__.py b/auth_from_http_remote_user/__openerp__.py new file mode 100644 index 000000000..317cdb073 --- /dev/null +++ b/auth_from_http_remote_user/__openerp__.py @@ -0,0 +1,133 @@ +# -*- 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 +This module must be loaded at startup; Add the *--load* parameter to the startup +command: :: + + --load=web,web_kanban,auth_from_http_remote_user, ... + +If the field is not found or no user matches the given one, it can lets the +system redirect to the login page (default) or issue a login error page depending +of the configuration. + +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 OpenErp 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 + + + 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 OpenErp, 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 OpenErp, 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 OpenErp and are automatically logged in the system. + +.. [#] Based on a ubuntu 12.04 env + +""", + 'author': 'Acsone SA/NV', + 'maintainer': 'ACSONE SA/NV', + 'website': 'http://www.acsone.eu', + 'depends': ['web'], + "license": "AGPL-3", + "js": ['static/src/js/auth_from_http_remote_user.js'], + 'data': [ + 'res_config_view.xml', + 'res_config_data.xml'], + "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..7705efcef --- /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 session diff --git a/auth_from_http_remote_user/controllers/session.py b/auth_from_http_remote_user/controllers/session.py new file mode 100644 index 000000000..f1dc0450a --- /dev/null +++ b/auth_from_http_remote_user/controllers/session.py @@ -0,0 +1,129 @@ +# -*- 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 + +from openerp.addons.web import http +from openerp.addons.web.controllers import main +from openerp.modules.registry import RegistryManager +from .. import utils + +import random +import logging +import openerp.tools.config as config + +_logger = logging.getLogger(__name__) + + +class Session(main.Session): + _cp_path = "/web/session" + + _REQUIRED_ATTRIBUTES = ['HTTP_REMOTE_USER'] + _OPTIONAL_ATTRIBUTES = [] + + def _get_db(self, db): + if db is not None and len(db) > 0: + return db + db = config['db_name'] + if db is None or len(db) == 0: + _logger.error("No db found for SSO. Specify one in the URL using parameter " + "db=? or provide a default one in the configuration") + raise http.AuthenticationError() + + def _get_user_id_from_attributes(self, res_users, cr, attrs): + login = attrs.get('HTTP_REMOTE_USER', None) + 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 _get_attributes_form_header(self, req): + attrs = {} + + all_attrs = self._REQUIRED_ATTRIBUTES + self._OPTIONAL_ATTRIBUTES + + headers = req.httprequest.headers.environ + + for attr in all_attrs: + value = headers.get(attr, None) + if value is not None: + attrs[attr] = value + + attrs_found = set(attrs.keys()) + attrs_missing = set(all_attrs) - attrs_found + if len(attrs_found) > 0: + _logger.debug("Fields '%s' not found in http headers\n %s", attrs_missing, headers) + + missings = set(self._REQUIRED_ATTRIBUTES) - attrs_found + if len(missings) > 0: + _logger.error("Required fields '%s' not found in http headers\n %s", missings, headers) + return attrs + + def _bind_http_remote_user(self, req, db_name): + db_name = self._get_db(db_name) + try: + registry = RegistryManager.get(db_name) + with registry.cursor() as cr: + modules = registry.get('ir.module.module') + installed = modules.search_count(cr, SUPERUSER_ID, ['&', + ('name', '=', 'auth_from_http_remote_user'), + ('state', '=', 'installed')]) == 1 + if not installed: + return + config = registry.get('auth_from_http_remote_user.config.settings') + # get parameters for SSO + default_login_page_disabled = config.is_default_login_page_disabled(cr, SUPERUSER_ID, None) + + # get the user + res_users = registry.get('res.users') + attrs = self._get_attributes_form_header(req) + user_id = self._get_user_id_from_attributes(res_users, cr, attrs) + + if user_id is None: + if default_login_page_disabled: + raise http.AuthenticationError() + return + + # generate a specific key for authentication + key = randomString(utils.KEY_LENGTH, '0123456789abcdef') + res_users.write(cr, SUPERUSER_ID, [user_id], {'sso_key': key}) + login = res_users.browse(cr, SUPERUSER_ID, user_id).login + req.session.bind(db_name, user_id, login, key) + except http.AuthenticationError, e: + raise e + except Exception, e: + _logger.error("Error binding Http Remote User session", exc_info=True) + raise e + + @http.jsonrequest + def get_http_remote_user_session_info(self, req, db): + if not req.session._login: + self._bind_http_remote_user(req, db) + return self.session_info(req) + +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/res_config.py b/auth_from_http_remote_user/res_config.py new file mode 100644 index 000000000..5cbc9082d --- /dev/null +++ b/auth_from_http_remote_user/res_config.py @@ -0,0 +1,60 @@ +# -*- 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, fields +from openerp.tools.safe_eval import safe_eval +import types + + +class auth_from_http_remote_user_configuration(orm.TransientModel): + _name = 'auth_from_http_remote_user.config.settings' + _inherit = 'res.config.settings' + + _columns = { + 'default_login_page_disabled': fields.boolean("Disable login page", + help=""" +Disable the default login page. +If the HTTP_REMOTE_HEADER field is not found or no user matches the given one, +the system will display a login error page if the login page is disabled. +Otherwise the normal login page will be displayed. + """), + } + + def is_default_login_page_disabled(self, cr, uid, fields, context=None): + ir_config_obj = self.pool['ir.config_parameter'] + default_login_page_disabled = ir_config_obj.get_param(cr, + uid, + 'auth_from_http_remote_user.default_login_page_disabled') + if isinstance(default_login_page_disabled, types.BooleanType): + return default_login_page_disabled + return safe_eval(default_login_page_disabled) + + def get_default_default_login_page_disabled(self, cr, uid, fields, context=None): + default_login_page_disabled = self.is_default_login_page_disabled(cr, uid, fields, context) + return {'default_login_page_disabled': default_login_page_disabled} + + def set_default_default_login_page_disabled(self, cr, uid, ids, context=None): + config = self.browse(cr, uid, ids[0], context) + ir_config_parameter_obj = self.pool['ir.config_parameter'] + ir_config_parameter_obj.set_param(cr, + uid, + 'auth_from_http_remote_user.default_login_page_disabled', + repr(config.default_login_page_disabled)) diff --git a/auth_from_http_remote_user/res_config_data.xml b/auth_from_http_remote_user/res_config_data.xml new file mode 100644 index 000000000..ba9a2b1c1 --- /dev/null +++ b/auth_from_http_remote_user/res_config_data.xml @@ -0,0 +1,9 @@ + + + + + auth_from_http_remote_user.default_login_page_disabled + False + + + diff --git a/auth_from_http_remote_user/res_config_view.xml b/auth_from_http_remote_user/res_config_view.xml new file mode 100644 index 000000000..3869cb4ac --- /dev/null +++ b/auth_from_http_remote_user/res_config_view.xml @@ -0,0 +1,39 @@ + + + + + Auth HTTP_REMOTE_USER settings + auth_from_http_remote_user.config.settings + + +
+
+
+
+
+ + + + + +
+
+ + + Configure Auth HTTP_REMOTE_USER + ir.actions.act_window + auth_from_http_remote_user.config.settings + form + inline + + + + +
+
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..fef91596d --- /dev/null +++ b/auth_from_http_remote_user/res_users.py @@ -0,0 +1,64 @@ +# -*- 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/static/src/js/auth_from_http_remote_user.js b/auth_from_http_remote_user/static/src/js/auth_from_http_remote_user.js new file mode 100644 index 000000000..df34118cf --- /dev/null +++ b/auth_from_http_remote_user/static/src/js/auth_from_http_remote_user.js @@ -0,0 +1,36 @@ +openerp.auth_from_http_remote_user = function(instance) { + + instance.web.Session.include({ + session_load_response : function(response) { + //unregister the event since it must be called only if the rpc call + //is made by session_reload + this.off('response', this.session_load_response); + if (response.error && response.error.data.type === "session_invalid") { + $("body").html("

Access Denied

"); + } + + console.log("session_load_response called"); + }, + + session_reload : function() { + var self = this; + // we need to register an handler for 'response' since + // by default, the rpc doesn't call callback function + // if the response is of error type 'session_invalid' + this.on('response', this, this.session_load_response); + return this.rpc("/web/session/get_http_remote_user_session_info", { + db : $.deparam.querystring().db + }).done(function(result) { + // If immediately follows a login (triggered by trying to + // restore + // an invalid session or no session at all), refresh session + // data + // (should not change, but just in case...) + _.extend(self, result); + }).fail(function(result){ + $("body").html("

Server error

"); + }); + } + }); + +}; \ No newline at end of file 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..e2ba2f890 --- /dev/null +++ b/auth_from_http_remote_user/tests/test_res_users.py @@ -0,0 +1,83 @@ +# -*- 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') + uid = res = res_users_obj.login(common.DB, 'admin', 'admin') + self.assertTrue(res, "Basic login must works as expected") + token = "123456" + res = res_users_obj.login(common.DB, 'admin', token) + 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.login(common.DB, 'admin', token) + 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 From c893a706b8dcc0e31ffcb641f5bc349ccdce73a7 Mon Sep 17 00:00:00 2001 From: Laurent Mignon Date: Wed, 30 Jul 2014 10:23:52 +0200 Subject: [PATCH 02/18] port auth_from_http_remote_user to 8.0 --- auth_from_http_remote_user/__openerp__.py | 1 - .../controllers/__init__.py | 2 +- .../controllers/{session.py => main.py} | 41 ++++++++----------- .../src/js/auth_from_http_remote_user.js | 36 ---------------- .../tests/test_res_users.py | 7 ++-- 5 files changed, 21 insertions(+), 66 deletions(-) rename auth_from_http_remote_user/controllers/{session.py => main.py} (78%) delete mode 100644 auth_from_http_remote_user/static/src/js/auth_from_http_remote_user.js diff --git a/auth_from_http_remote_user/__openerp__.py b/auth_from_http_remote_user/__openerp__.py index 317cdb073..810ec427a 100644 --- a/auth_from_http_remote_user/__openerp__.py +++ b/auth_from_http_remote_user/__openerp__.py @@ -119,7 +119,6 @@ for a login and password outside OpenErp and are automatically logged in the sys 'website': 'http://www.acsone.eu', 'depends': ['web'], "license": "AGPL-3", - "js": ['static/src/js/auth_from_http_remote_user.js'], 'data': [ 'res_config_view.xml', 'res_config_data.xml'], diff --git a/auth_from_http_remote_user/controllers/__init__.py b/auth_from_http_remote_user/controllers/__init__.py index 7705efcef..6b12b8082 100644 --- a/auth_from_http_remote_user/controllers/__init__.py +++ b/auth_from_http_remote_user/controllers/__init__.py @@ -19,4 +19,4 @@ # ############################################################################## -from . import session +from . import main diff --git a/auth_from_http_remote_user/controllers/session.py b/auth_from_http_remote_user/controllers/main.py similarity index 78% rename from auth_from_http_remote_user/controllers/session.py rename to auth_from_http_remote_user/controllers/main.py index f1dc0450a..37f62b665 100644 --- a/auth_from_http_remote_user/controllers/session.py +++ b/auth_from_http_remote_user/controllers/main.py @@ -21,9 +21,10 @@ from openerp import SUPERUSER_ID -from openerp.addons.web import http +import openerp +from openerp import http +from openerp.http import request from openerp.addons.web.controllers import main -from openerp.modules.registry import RegistryManager from .. import utils import random @@ -33,20 +34,17 @@ import openerp.tools.config as config _logger = logging.getLogger(__name__) -class Session(main.Session): - _cp_path = "/web/session" +class Home(main.Home): _REQUIRED_ATTRIBUTES = ['HTTP_REMOTE_USER'] _OPTIONAL_ATTRIBUTES = [] - def _get_db(self, db): - if db is not None and len(db) > 0: - return db - db = config['db_name'] - if db is None or len(db) == 0: - _logger.error("No db found for SSO. Specify one in the URL using parameter " - "db=? or provide a default one in the configuration") - raise http.AuthenticationError() + @http.route('/web', type='http', auth="none") + def web_client(self, s_action=None, **kw): + main.ensure_db() + if not request.session.uid: + self._bind_http_remote_user(http.request.session.db) + return super(Home, self).web_client(s_action, **kw) def _get_user_id_from_attributes(self, res_users, cr, attrs): login = attrs.get('HTTP_REMOTE_USER', None) @@ -56,12 +54,12 @@ class Session(main.Session): return user_ids[0] return None - def _get_attributes_form_header(self, req): + def _get_attributes_form_header(self): attrs = {} all_attrs = self._REQUIRED_ATTRIBUTES + self._OPTIONAL_ATTRIBUTES - headers = req.httprequest.headers.environ + headers = http.request.httprequest.headers.environ for attr in all_attrs: value = headers.get(attr, None) @@ -78,10 +76,9 @@ class Session(main.Session): _logger.error("Required fields '%s' not found in http headers\n %s", missings, headers) return attrs - def _bind_http_remote_user(self, req, db_name): - db_name = self._get_db(db_name) + def _bind_http_remote_user(self, db_name): try: - registry = RegistryManager.get(db_name) + registry = openerp.registry(db_name) with registry.cursor() as cr: modules = registry.get('ir.module.module') installed = modules.search_count(cr, SUPERUSER_ID, ['&', @@ -95,7 +92,7 @@ class Session(main.Session): # get the user res_users = registry.get('res.users') - attrs = self._get_attributes_form_header(req) + attrs = self._get_attributes_form_header() user_id = self._get_user_id_from_attributes(res_users, cr, attrs) if user_id is None: @@ -107,19 +104,13 @@ class Session(main.Session): key = randomString(utils.KEY_LENGTH, '0123456789abcdef') res_users.write(cr, SUPERUSER_ID, [user_id], {'sso_key': key}) login = res_users.browse(cr, SUPERUSER_ID, user_id).login - req.session.bind(db_name, user_id, login, 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 - @http.jsonrequest - def get_http_remote_user_session_info(self, req, db): - if not req.session._login: - self._bind_http_remote_user(req, db) - return self.session_info(req) - randrange = random.SystemRandom().randrange diff --git a/auth_from_http_remote_user/static/src/js/auth_from_http_remote_user.js b/auth_from_http_remote_user/static/src/js/auth_from_http_remote_user.js deleted file mode 100644 index df34118cf..000000000 --- a/auth_from_http_remote_user/static/src/js/auth_from_http_remote_user.js +++ /dev/null @@ -1,36 +0,0 @@ -openerp.auth_from_http_remote_user = function(instance) { - - instance.web.Session.include({ - session_load_response : function(response) { - //unregister the event since it must be called only if the rpc call - //is made by session_reload - this.off('response', this.session_load_response); - if (response.error && response.error.data.type === "session_invalid") { - $("body").html("

Access Denied

"); - } - - console.log("session_load_response called"); - }, - - session_reload : function() { - var self = this; - // we need to register an handler for 'response' since - // by default, the rpc doesn't call callback function - // if the response is of error type 'session_invalid' - this.on('response', this, this.session_load_response); - return this.rpc("/web/session/get_http_remote_user_session_info", { - db : $.deparam.querystring().db - }).done(function(result) { - // If immediately follows a login (triggered by trying to - // restore - // an invalid session or no session at all), refresh session - // data - // (should not change, but just in case...) - _.extend(self, result); - }).fail(function(result){ - $("body").html("

Server error

"); - }); - } - }); - -}; \ No newline at end of file diff --git a/auth_from_http_remote_user/tests/test_res_users.py b/auth_from_http_remote_user/tests/test_res_users.py index e2ba2f890..9341661ee 100644 --- a/auth_from_http_remote_user/tests/test_res_users.py +++ b/auth_from_http_remote_user/tests/test_res_users.py @@ -45,10 +45,11 @@ class test_res_users(common.TransactionCase): def test_login(self): res_users_obj = self.registry('res.users') - uid = res = res_users_obj.login(common.DB, 'admin', 'admin') + 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.login(common.DB, 'admin', token) + 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) @@ -61,7 +62,7 @@ class test_res_users(common.TransactionCase): res_users_obj.check(common.DB, uid, token) # we are able to login with the new token - res = res_users_obj.login(common.DB, 'admin', token) + res = res_users_obj.authenticate(common.DB, 'admin', token, None) self.assertTrue(res) @unittest.skipIf(os.environ.get('TRAVIS'), From 2f510369281cd37174cd81906fe96d2693975853 Mon Sep 17 00:00:00 2001 From: Laurent Mignon Date: Wed, 30 Jul 2014 11:03:37 +0200 Subject: [PATCH 03/18] [PEP8] line lenght is now ridiculous (80 chars) --- auth_from_http_remote_user/__openerp__.py | 38 +++++++++++-------- .../controllers/main.py | 35 +++++++++++------ auth_from_http_remote_user/res_config.py | 23 +++++++---- auth_from_http_remote_user/res_users.py | 3 +- .../tests/test_res_users.py | 20 ++++++---- 5 files changed, 74 insertions(+), 45 deletions(-) diff --git a/auth_from_http_remote_user/__openerp__.py b/auth_from_http_remote_user/__openerp__.py index 810ec427a..cf7bbc34b 100644 --- a/auth_from_http_remote_user/__openerp__.py +++ b/auth_from_http_remote_user/__openerp__.py @@ -34,21 +34,22 @@ command: :: --load=web,web_kanban,auth_from_http_remote_user, ... If the field is not found or no user matches the given one, it can lets the -system redirect to the login page (default) or issue a login error page depending -of the configuration. +system redirect to the login page (default) or issue a login error page +depending of the configuration. 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. +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: :: +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 @@ -75,16 +76,20 @@ with the following content: :: 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 OpenErp, 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 OpenErp, 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: +.. 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 OpenErp, 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 OpenErp, 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') + {'HTTP_AUTHORIZATION': 'Basic YWRtaW46YWRtaW4=', ..., + 'HTTP_REMOTE_USER': 'demo') Enable the required apache modules: :: @@ -108,8 +113,9 @@ 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 OpenErp and are automatically logged in the system. +Open your browser and go to MY_VHOST.com. If everything is well configured, you +are prompted for a login and password outside OpenErp and are automatically +logged in the system. .. [#] Based on a ubuntu 12.04 env diff --git a/auth_from_http_remote_user/controllers/main.py b/auth_from_http_remote_user/controllers/main.py index 37f62b665..eff4d29a6 100644 --- a/auth_from_http_remote_user/controllers/main.py +++ b/auth_from_http_remote_user/controllers/main.py @@ -29,7 +29,6 @@ from .. import utils import random import logging -import openerp.tools.config as config _logger = logging.getLogger(__name__) @@ -48,7 +47,8 @@ class Home(main.Home): def _get_user_id_from_attributes(self, res_users, cr, attrs): login = attrs.get('HTTP_REMOTE_USER', None) - user_ids = res_users.search(cr, SUPERUSER_ID, [('login', '=', login), ('active', '=', True)]) + user_ids = res_users.search(cr, SUPERUSER_ID, [('login', '=', login), + ('active', '=', True)]) assert len(user_ids) < 2 if user_ids: return user_ids[0] @@ -69,11 +69,13 @@ class Home(main.Home): attrs_found = set(attrs.keys()) attrs_missing = set(all_attrs) - attrs_found if len(attrs_found) > 0: - _logger.debug("Fields '%s' not found in http headers\n %s", attrs_missing, headers) + _logger.debug("Fields '%s' not found in http headers\n %s", + attrs_missing, headers) missings = set(self._REQUIRED_ATTRIBUTES) - attrs_found if len(missings) > 0: - _logger.error("Required fields '%s' not found in http headers\n %s", missings, headers) + _logger.error("Required fields '%s' not found in http headers\n %s", + missings, headers) return attrs def _bind_http_remote_user(self, db_name): @@ -81,19 +83,26 @@ class Home(main.Home): registry = openerp.registry(db_name) with registry.cursor() as cr: modules = registry.get('ir.module.module') - installed = modules.search_count(cr, SUPERUSER_ID, ['&', - ('name', '=', 'auth_from_http_remote_user'), - ('state', '=', 'installed')]) == 1 + domain = ['&', + ('name', '=', 'auth_from_http_remote_user'), + ('state', '=', 'installed')] + installed = modules.search_count(cr, SUPERUSER_ID, domain) == 1 if not installed: return - config = registry.get('auth_from_http_remote_user.config.settings') + config = registry.get('auth_from_http_remote_user.' + 'config.settings') # get parameters for SSO - default_login_page_disabled = config.is_default_login_page_disabled(cr, SUPERUSER_ID, None) + default_login_page_disabled = \ + config.is_default_login_page_disabled(cr, + SUPERUSER_ID, + None) # get the user res_users = registry.get('res.users') attrs = self._get_attributes_form_header() - user_id = self._get_user_id_from_attributes(res_users, cr, attrs) + user_id = self._get_user_id_from_attributes(res_users, + cr, + attrs) if user_id is None: if default_login_page_disabled: @@ -104,11 +113,13 @@ class Home(main.Home): key = randomString(utils.KEY_LENGTH, '0123456789abcdef') res_users.write(cr, SUPERUSER_ID, [user_id], {'sso_key': key}) login = res_users.browse(cr, SUPERUSER_ID, user_id).login - request.session.authenticate(db_name, login=login, password=key, uid=user_id) + 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) + _logger.error("Error binding Http Remote User session", + exc_info=True) raise e randrange = random.SystemRandom().randrange diff --git a/auth_from_http_remote_user/res_config.py b/auth_from_http_remote_user/res_config.py index 5cbc9082d..dcbc2f97a 100644 --- a/auth_from_http_remote_user/res_config.py +++ b/auth_from_http_remote_user/res_config.py @@ -40,21 +40,28 @@ Otherwise the normal login page will be displayed. def is_default_login_page_disabled(self, cr, uid, fields, context=None): ir_config_obj = self.pool['ir.config_parameter'] - default_login_page_disabled = ir_config_obj.get_param(cr, - uid, - 'auth_from_http_remote_user.default_login_page_disabled') + default_login_page_disabled = \ + ir_config_obj.get_param(cr, + uid, + 'auth_from_http_remote_user.' + 'default_login_page_disabled') if isinstance(default_login_page_disabled, types.BooleanType): return default_login_page_disabled return safe_eval(default_login_page_disabled) - def get_default_default_login_page_disabled(self, cr, uid, fields, context=None): - default_login_page_disabled = self.is_default_login_page_disabled(cr, uid, fields, context) + def get_default_default_login_page_disabled(self, cr, uid, fields, + context=None): + default_login_page_disabled = \ + self.is_default_login_page_disabled(cr, uid, fields, context) return {'default_login_page_disabled': default_login_page_disabled} - def set_default_default_login_page_disabled(self, cr, uid, ids, context=None): + def set_default_default_login_page_disabled(self, cr, uid, ids, + context=None): config = self.browse(cr, uid, ids[0], context) ir_config_parameter_obj = self.pool['ir.config_parameter'] + param_value = repr(config.default_login_page_disabled) ir_config_parameter_obj.set_param(cr, uid, - 'auth_from_http_remote_user.default_login_page_disabled', - repr(config.default_login_page_disabled)) + 'auth_from_http_remote_user.' + 'default_login_page_disabled', + param_value) diff --git a/auth_from_http_remote_user/res_users.py b/auth_from_http_remote_user/res_users.py index fef91596d..638179405 100644 --- a/auth_from_http_remote_user/res_users.py +++ b/auth_from_http_remote_user/res_users.py @@ -43,7 +43,8 @@ class res_users(orm.Model): 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)]) + res = self.search(cr, SUPERUSER_ID, [('id', '=', uid), + ('sso_key', '=', password)]) if not res: raise openerp.exceptions.AccessDenied() diff --git a/auth_from_http_remote_user/tests/test_res_users.py b/auth_from_http_remote_user/tests/test_res_users.py index 9341661ee..486531bc7 100644 --- a/auth_from_http_remote_user/tests/test_res_users.py +++ b/auth_from_http_remote_user/tests/test_res_users.py @@ -55,10 +55,11 @@ class test_res_users(common.TransactionCase): # 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 + # 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 + # 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 @@ -66,11 +67,14 @@ class test_res_users(common.TransactionCase): 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') + '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 ''' From 9b99c2dc573e48e1e7e8421cb419cf0ba3dd4988 Mon Sep 17 00:00:00 2001 From: Laurent Mignon Date: Wed, 30 Jul 2014 11:05:07 +0200 Subject: [PATCH 04/18] [FIX] if the default login page is disabled, redirect with *401* --- auth_from_http_remote_user/controllers/main.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/auth_from_http_remote_user/controllers/main.py b/auth_from_http_remote_user/controllers/main.py index eff4d29a6..5724c5896 100644 --- a/auth_from_http_remote_user/controllers/main.py +++ b/auth_from_http_remote_user/controllers/main.py @@ -29,6 +29,7 @@ from .. import utils import random import logging +import werkzeug _logger = logging.getLogger(__name__) @@ -42,7 +43,10 @@ class Home(main.Home): def web_client(self, s_action=None, **kw): main.ensure_db() if not request.session.uid: - self._bind_http_remote_user(http.request.session.db) + try: + self._bind_http_remote_user(http.request.session.db) + except http.AuthenticationError: + return werkzeug.exceptions.Unauthorized() return super(Home, self).web_client(s_action, **kw) def _get_user_id_from_attributes(self, res_users, cr, attrs): From ec8f4a72928bf3dc23470e8adf4ac01b4cd5995b Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (aka lmi)" Date: Wed, 30 Jul 2014 15:50:12 +0200 Subject: [PATCH 05/18] Improve module description --- auth_from_http_remote_user/__openerp__.py | 36 +++++++++++++++++++---- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/auth_from_http_remote_user/__openerp__.py b/auth_from_http_remote_user/__openerp__.py index cf7bbc34b..8f79c1047 100644 --- a/auth_from_http_remote_user/__openerp__.py +++ b/auth_from_http_remote_user/__openerp__.py @@ -27,7 +27,7 @@ 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 +the HEADER of the HTTP request and trying to bind the given value to a user. This module must be loaded at startup; Add the *--load* parameter to the startup command: :: @@ -37,6 +37,31 @@ If the field is not found or no user matches the given one, it can lets the system redirect to the login page (default) or issue a login error page depending of the configuration. +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 [#]_ ---------------------------------------- @@ -60,7 +85,7 @@ with the following content: :: ProxyRequests Off AuthType Basic - AuthName "Test OpenErp auth_from_http_remote_user" + AuthName "Test Odoo auth_from_http_remote_user" AuthBasicProvider file AuthUserFile /etc/apache2/MY_VHOST.htpasswd Require valid-user @@ -71,6 +96,7 @@ with the following content: :: 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 @@ -79,9 +105,9 @@ with the following content: :: .. 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 OpenErp, header's fields name are normalized. As result of this + 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 OpenErp, run your + 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: :: @@ -114,7 +140,7 @@ 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 OpenErp and are automatically +are prompted for a login and password outside Odoo and are automatically logged in the system. .. [#] Based on a ubuntu 12.04 env From 8f9fcae4dce831ad4339f8a8fd140eb950c40704 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (aka lmi)" Date: Mon, 4 Aug 2014 11:48:09 +0200 Subject: [PATCH 06/18] =?UTF-8?q?Addons=20configuration=20parameter=20is?= =?UTF-8?q?=20now=20a=20field=20part=20of=20the=20Authentication=20group?= =?UTF-8?q?=20from=20base=5Fsetup=1B[C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- auth_from_http_remote_user/__openerp__.py | 2 +- .../controllers/main.py | 3 +- auth_from_http_remote_user/res_config.py | 45 ++++++++-------- .../res_config_view.xml | 51 ++++++------------- 4 files changed, 38 insertions(+), 63 deletions(-) diff --git a/auth_from_http_remote_user/__openerp__.py b/auth_from_http_remote_user/__openerp__.py index 8f79c1047..10750cbea 100644 --- a/auth_from_http_remote_user/__openerp__.py +++ b/auth_from_http_remote_user/__openerp__.py @@ -149,7 +149,7 @@ logged in the system. 'author': 'Acsone SA/NV', 'maintainer': 'ACSONE SA/NV', 'website': 'http://www.acsone.eu', - 'depends': ['web'], + 'depends': ['base', 'web', 'base_setup'], "license": "AGPL-3", 'data': [ 'res_config_view.xml', diff --git a/auth_from_http_remote_user/controllers/main.py b/auth_from_http_remote_user/controllers/main.py index 5724c5896..2f295fec3 100644 --- a/auth_from_http_remote_user/controllers/main.py +++ b/auth_from_http_remote_user/controllers/main.py @@ -93,8 +93,7 @@ class Home(main.Home): installed = modules.search_count(cr, SUPERUSER_ID, domain) == 1 if not installed: return - config = registry.get('auth_from_http_remote_user.' - 'config.settings') + config = registry.get('base.config.settings') # get parameters for SSO default_login_page_disabled = \ config.is_default_login_page_disabled(cr, diff --git a/auth_from_http_remote_user/res_config.py b/auth_from_http_remote_user/res_config.py index dcbc2f97a..37fca0bd5 100644 --- a/auth_from_http_remote_user/res_config.py +++ b/auth_from_http_remote_user/res_config.py @@ -21,15 +21,15 @@ from openerp.osv import orm, fields from openerp.tools.safe_eval import safe_eval -import types class auth_from_http_remote_user_configuration(orm.TransientModel): - _name = 'auth_from_http_remote_user.config.settings' - _inherit = 'res.config.settings' + _inherit = 'base.config.settings' _columns = { - 'default_login_page_disabled': fields.boolean("Disable login page", + 'default_login_page_disabled': fields.boolean("Disable login page when " + "login with HTTP Remote " + "User", help=""" Disable the default login page. If the HTTP_REMOTE_HEADER field is not found or no user matches the given one, @@ -39,29 +39,26 @@ Otherwise the normal login page will be displayed. } def is_default_login_page_disabled(self, cr, uid, fields, context=None): - ir_config_obj = self.pool['ir.config_parameter'] - default_login_page_disabled = \ - ir_config_obj.get_param(cr, - uid, - 'auth_from_http_remote_user.' - 'default_login_page_disabled') - if isinstance(default_login_page_disabled, types.BooleanType): - return default_login_page_disabled - return safe_eval(default_login_page_disabled) + vals = self.get_default_default_login_page_disabled(cr, + uid, + fields, + context=context) + return vals.get('default_login_page_disabled', False) def get_default_default_login_page_disabled(self, cr, uid, fields, context=None): - default_login_page_disabled = \ - self.is_default_login_page_disabled(cr, uid, fields, context) - return {'default_login_page_disabled': default_login_page_disabled} + icp = self.pool.get('ir.config_parameter') + # we use safe_eval on the result, since the value of + # the parameter is a nonempty string + is_disabled = icp.get_param(cr, uid, 'default_login_page_disabled', + 'False') + return {'default_login_page_disabled': safe_eval(is_disabled)} def set_default_default_login_page_disabled(self, cr, uid, ids, context=None): - config = self.browse(cr, uid, ids[0], context) - ir_config_parameter_obj = self.pool['ir.config_parameter'] - param_value = repr(config.default_login_page_disabled) - ir_config_parameter_obj.set_param(cr, - uid, - 'auth_from_http_remote_user.' - 'default_login_page_disabled', - param_value) + config = self.browse(cr, uid, ids[0], context=context) + icp = self.pool.get('ir.config_parameter') + # we store the repr of the value, since the value of the parameter + # is a required string + icp.set_param(cr, uid, 'default_login_page_disabled', + repr(config.default_login_page_disabled)) diff --git a/auth_from_http_remote_user/res_config_view.xml b/auth_from_http_remote_user/res_config_view.xml index 3869cb4ac..e2f8df348 100644 --- a/auth_from_http_remote_user/res_config_view.xml +++ b/auth_from_http_remote_user/res_config_view.xml @@ -1,39 +1,18 @@ - - - Auth HTTP_REMOTE_USER settings - auth_from_http_remote_user.config.settings - - -
-
-
-
-
- - - - - -
-
- - - Configure Auth HTTP_REMOTE_USER - ir.actions.act_window - auth_from_http_remote_user.config.settings - form - inline - - - - -
+ + + base.config.settings.auth_from_http_remote_user + base.config.settings + + + +
+ +
+
+
+
+
From 68e03a37112d8147533cf2a7838d8f87fe234774 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (aka lmi)" Date: Mon, 4 Aug 2014 11:52:54 +0200 Subject: [PATCH 07/18] Improve doc --- auth_from_http_remote_user/__openerp__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/auth_from_http_remote_user/__openerp__.py b/auth_from_http_remote_user/__openerp__.py index 10750cbea..9c589c667 100644 --- a/auth_from_http_remote_user/__openerp__.py +++ b/auth_from_http_remote_user/__openerp__.py @@ -28,8 +28,8 @@ 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. -This module must be loaded at startup; Add the *--load* parameter to the startup -command: :: +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, ... From 1675ae6fbf6244b36635ba7317d808c68ee6d62a Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (aka lmi)" Date: Mon, 4 Aug 2014 12:03:16 +0200 Subject: [PATCH 08/18] Code simplification: remove overkill method --- .../controllers/main.py | 39 ++++--------------- 1 file changed, 8 insertions(+), 31 deletions(-) diff --git a/auth_from_http_remote_user/controllers/main.py b/auth_from_http_remote_user/controllers/main.py index 2f295fec3..c388e3182 100644 --- a/auth_from_http_remote_user/controllers/main.py +++ b/auth_from_http_remote_user/controllers/main.py @@ -36,8 +36,7 @@ _logger = logging.getLogger(__name__) class Home(main.Home): - _REQUIRED_ATTRIBUTES = ['HTTP_REMOTE_USER'] - _OPTIONAL_ATTRIBUTES = [] + _REMOTE_USER_ATTRIBUTE = 'HTTP_REMOTE_USER' @http.route('/web', type='http', auth="none") def web_client(self, s_action=None, **kw): @@ -49,8 +48,12 @@ class Home(main.Home): return werkzeug.exceptions.Unauthorized() return super(Home, self).web_client(s_action, **kw) - def _get_user_id_from_attributes(self, res_users, cr, attrs): - login = attrs.get('HTTP_REMOTE_USER', None) + def _get_user_id_from_attributes(self, res_users, cr): + headers = http.request.httprequest.headers.environ + login = headers.get(self._REMOTE_USER_ATTRIBUTE, None) + if not login: + _logger.error("Required fields '%s' not found in http headers\n %s", + self._REMOTE_USER_ATTRIBUTE, headers) user_ids = res_users.search(cr, SUPERUSER_ID, [('login', '=', login), ('active', '=', True)]) assert len(user_ids) < 2 @@ -58,30 +61,6 @@ class Home(main.Home): return user_ids[0] return None - def _get_attributes_form_header(self): - attrs = {} - - all_attrs = self._REQUIRED_ATTRIBUTES + self._OPTIONAL_ATTRIBUTES - - headers = http.request.httprequest.headers.environ - - for attr in all_attrs: - value = headers.get(attr, None) - if value is not None: - attrs[attr] = value - - attrs_found = set(attrs.keys()) - attrs_missing = set(all_attrs) - attrs_found - if len(attrs_found) > 0: - _logger.debug("Fields '%s' not found in http headers\n %s", - attrs_missing, headers) - - missings = set(self._REQUIRED_ATTRIBUTES) - attrs_found - if len(missings) > 0: - _logger.error("Required fields '%s' not found in http headers\n %s", - missings, headers) - return attrs - def _bind_http_remote_user(self, db_name): try: registry = openerp.registry(db_name) @@ -102,10 +81,8 @@ class Home(main.Home): # get the user res_users = registry.get('res.users') - attrs = self._get_attributes_form_header() user_id = self._get_user_id_from_attributes(res_users, - cr, - attrs) + cr) if user_id is None: if default_login_page_disabled: From 8a23567fe3e7dbe1aa84abe23d6787f628ec8420 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (aka lmi)" Date: Mon, 4 Aug 2014 12:07:03 +0200 Subject: [PATCH 09/18] Improve doc --- auth_from_http_remote_user/__openerp__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/auth_from_http_remote_user/__openerp__.py b/auth_from_http_remote_user/__openerp__.py index 9c589c667..94928f702 100644 --- a/auth_from_http_remote_user/__openerp__.py +++ b/auth_from_http_remote_user/__openerp__.py @@ -40,9 +40,9 @@ depending of the configuration. 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. +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 @@ -143,6 +143,7 @@ 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 """, From a70e13365dbf33092dec867da25ce873e401e90c Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (aka lmi)" Date: Mon, 4 Aug 2014 12:20:26 +0200 Subject: [PATCH 10/18] [FIX] properlyt return the Unauthorized() response to avoid warning in the log file --- auth_from_http_remote_user/controllers/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth_from_http_remote_user/controllers/main.py b/auth_from_http_remote_user/controllers/main.py index c388e3182..687dd41f4 100644 --- a/auth_from_http_remote_user/controllers/main.py +++ b/auth_from_http_remote_user/controllers/main.py @@ -45,7 +45,7 @@ class Home(main.Home): try: self._bind_http_remote_user(http.request.session.db) except http.AuthenticationError: - return werkzeug.exceptions.Unauthorized() + return werkzeug.exceptions.Unauthorized().get_response() return super(Home, self).web_client(s_action, **kw) def _get_user_id_from_attributes(self, res_users, cr): From d68a47f05f9b7a9707101bc632752fadf1a4e110 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (aka lmi)" Date: Mon, 4 Aug 2014 12:55:58 +0200 Subject: [PATCH 11/18] [FIX] always check the uid in the session and the one in the request to avoid session mismatch... --- auth_from_http_remote_user/__init__.py | 1 + .../controllers/main.py | 29 +++++++++---------- auth_from_http_remote_user/model.py | 27 +++++++++++++++++ 3 files changed, 42 insertions(+), 15 deletions(-) create mode 100644 auth_from_http_remote_user/model.py diff --git a/auth_from_http_remote_user/__init__.py b/auth_from_http_remote_user/__init__.py index 6e0a37c8b..0d34cbc0c 100644 --- a/auth_from_http_remote_user/__init__.py +++ b/auth_from_http_remote_user/__init__.py @@ -22,3 +22,4 @@ from . import controllers from . import res_config from . import res_users +from . import model \ No newline at end of file diff --git a/auth_from_http_remote_user/controllers/main.py b/auth_from_http_remote_user/controllers/main.py index 687dd41f4..bde31b081 100644 --- a/auth_from_http_remote_user/controllers/main.py +++ b/auth_from_http_remote_user/controllers/main.py @@ -25,6 +25,8 @@ 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 @@ -41,11 +43,10 @@ class Home(main.Home): @http.route('/web', type='http', auth="none") def web_client(self, s_action=None, **kw): main.ensure_db() - if not request.session.uid: - try: - self._bind_http_remote_user(http.request.session.db) - except http.AuthenticationError: - return werkzeug.exceptions.Unauthorized().get_response() + 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 _get_user_id_from_attributes(self, res_users, cr): @@ -65,13 +66,15 @@ class Home(main.Home): try: registry = openerp.registry(db_name) with registry.cursor() as cr: - modules = registry.get('ir.module.module') - domain = ['&', - ('name', '=', 'auth_from_http_remote_user'), - ('state', '=', 'installed')] - installed = modules.search_count(cr, SUPERUSER_ID, domain) == 1 - if not installed: + if AuthFromHttpRemoteUserInstalled._name not in registry: + return + res_users = registry.get('res.users') + # get the user + user_id = self._get_user_id_from_attributes(res_users, + cr) + if request.session.uid and request.session.uid == user_id: return + config = registry.get('base.config.settings') # get parameters for SSO default_login_page_disabled = \ @@ -79,10 +82,6 @@ class Home(main.Home): SUPERUSER_ID, None) - # get the user - res_users = registry.get('res.users') - user_id = self._get_user_id_from_attributes(res_users, - cr) if user_id is None: if default_login_page_disabled: diff --git a/auth_from_http_remote_user/model.py b/auth_from_http_remote_user/model.py new file mode 100644 index 000000000..4e514b833 --- /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 now if the module is installed + """ + _name = 'auth_from_http_remote_user.installed' From 97f491724f5aef454221aa99d571519db9278c9e Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (aka lmi)" Date: Mon, 4 Aug 2014 13:00:44 +0200 Subject: [PATCH 12/18] [PEP8] --- auth_from_http_remote_user/__init__.py | 2 +- auth_from_http_remote_user/controllers/main.py | 1 - dbfilter_from_header/__openerp__.py | 18 +++++++++--------- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/auth_from_http_remote_user/__init__.py b/auth_from_http_remote_user/__init__.py index 0d34cbc0c..f2baf2765 100644 --- a/auth_from_http_remote_user/__init__.py +++ b/auth_from_http_remote_user/__init__.py @@ -22,4 +22,4 @@ from . import controllers from . import res_config from . import res_users -from . import model \ No newline at end of file +from . import model diff --git a/auth_from_http_remote_user/controllers/main.py b/auth_from_http_remote_user/controllers/main.py index bde31b081..fae1943c6 100644 --- a/auth_from_http_remote_user/controllers/main.py +++ b/auth_from_http_remote_user/controllers/main.py @@ -82,7 +82,6 @@ class Home(main.Home): SUPERUSER_ID, None) - if user_id is None: if default_login_page_disabled: raise http.AuthenticationError() diff --git a/dbfilter_from_header/__openerp__.py b/dbfilter_from_header/__openerp__.py index 7baf1f9e7..4570e55d5 100644 --- a/dbfilter_from_header/__openerp__.py +++ b/dbfilter_from_header/__openerp__.py @@ -19,14 +19,14 @@ # ############################################################################## { - "name" : "dbfilter_from_header", - "version" : "1.0", - "author" : "Therp BV", + "name": "dbfilter_from_header", + "version": "1.0", + "author": "Therp BV", "complexity": "normal", "description": """ This addon lets you pass a dbfilter as a HTTP header. - This is interesting for setups where database names can't be mapped to + This is interesting for setups where database names can't be mapped to proxied host names. In nginx, use @@ -34,11 +34,11 @@ The addon has to be loaded as server-wide module. """, - "category" : "Tools", - "depends" : [ + "category": "Tools", + "depends": [ 'web', ], - "data" : [ + "data": [ ], "js": [ ], @@ -46,7 +46,7 @@ ], "auto_install": False, "installable": True, - "external_dependencies" : { - 'python' : [], + "external_dependencies": { + 'python': [], }, } From 8c7115e2d3ae4bf8ba0b416de546f7331cde572d Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (aka lmi)" Date: Mon, 4 Aug 2014 15:08:47 +0200 Subject: [PATCH 13/18] [FIX] return 'None' if no user name is found in the request hearder --- auth_from_http_remote_user/controllers/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/auth_from_http_remote_user/controllers/main.py b/auth_from_http_remote_user/controllers/main.py index fae1943c6..9f4df7fbe 100644 --- a/auth_from_http_remote_user/controllers/main.py +++ b/auth_from_http_remote_user/controllers/main.py @@ -55,6 +55,7 @@ class Home(main.Home): if not login: _logger.error("Required fields '%s' not found in http headers\n %s", self._REMOTE_USER_ATTRIBUTE, headers) + return None user_ids = res_users.search(cr, SUPERUSER_ID, [('login', '=', login), ('active', '=', True)]) assert len(user_ids) < 2 From 436ffcd53f3be98217ddb161282c0564aa0597bb Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (aka lmi)" Date: Mon, 4 Aug 2014 16:42:02 +0200 Subject: [PATCH 14/18] If HTTP_REMOTE_USER is in the request headers and no corresponding user is found in odoo always issues Unauthorized (avoid redirect to the login page) If the uid in the session is not the same as the one from the binded HTTP_REMOTE_USER, always logout to clean up the session --- auth_from_http_remote_user/__init__.py | 1 - auth_from_http_remote_user/__openerp__.py | 9 +-- .../controllers/main.py | 39 +++++------ auth_from_http_remote_user/model.py | 2 +- auth_from_http_remote_user/res_config.py | 64 ------------------- .../res_config_data.xml | 9 --- .../res_config_view.xml | 18 ------ 7 files changed, 24 insertions(+), 118 deletions(-) delete mode 100644 auth_from_http_remote_user/res_config.py delete mode 100644 auth_from_http_remote_user/res_config_data.xml delete mode 100644 auth_from_http_remote_user/res_config_view.xml diff --git a/auth_from_http_remote_user/__init__.py b/auth_from_http_remote_user/__init__.py index f2baf2765..7fb1b9d59 100644 --- a/auth_from_http_remote_user/__init__.py +++ b/auth_from_http_remote_user/__init__.py @@ -20,6 +20,5 @@ ############################################################################## from . import controllers -from . import res_config 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 index 94928f702..5266d4d12 100644 --- a/auth_from_http_remote_user/__openerp__.py +++ b/auth_from_http_remote_user/__openerp__.py @@ -33,9 +33,8 @@ at startup; Add the *--load* parameter to the startup command: :: --load=web,web_kanban,auth_from_http_remote_user, ... -If the field is not found or no user matches the given one, it can lets the -system redirect to the login page (default) or issue a login error page -depending of the configuration. +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. --------- @@ -152,9 +151,7 @@ logged in the system. 'website': 'http://www.acsone.eu', 'depends': ['base', 'web', 'base_setup'], "license": "AGPL-3", - 'data': [ - 'res_config_view.xml', - 'res_config_data.xml'], + 'data': [], "demo": [], "test": [], "active": False, diff --git a/auth_from_http_remote_user/controllers/main.py b/auth_from_http_remote_user/controllers/main.py index 9f4df7fbe..13a3fa237 100644 --- a/auth_from_http_remote_user/controllers/main.py +++ b/auth_from_http_remote_user/controllers/main.py @@ -49,12 +49,11 @@ class Home(main.Home): return werkzeug.exceptions.Unauthorized().get_response() return super(Home, self).web_client(s_action, **kw) - def _get_user_id_from_attributes(self, res_users, cr): - headers = http.request.httprequest.headers.environ + def _get_user_id_from_headers(self, res_users, headers, cr): login = headers.get(self._REMOTE_USER_ATTRIBUTE, None) if not login: - _logger.error("Required fields '%s' not found in http headers\n %s", - self._REMOTE_USER_ATTRIBUTE, headers) + _logger.info("Expected fields '%s' not found in http headers\n %s", + self._REMOTE_USER_ATTRIBUTE, headers) return None user_ids = res_users.search(cr, SUPERUSER_ID, [('login', '=', login), ('active', '=', True)]) @@ -71,22 +70,24 @@ class Home(main.Home): return res_users = registry.get('res.users') # get the user - user_id = self._get_user_id_from_attributes(res_users, - cr) - if request.session.uid and request.session.uid == user_id: - return - - config = registry.get('base.config.settings') - # get parameters for SSO - default_login_page_disabled = \ - config.is_default_login_page_disabled(cr, - SUPERUSER_ID, - None) - - if user_id is None: - if default_login_page_disabled: + headers = http.request.httprequest.headers.environ + user_id = self._get_user_id_from_headers(res_users, + headers, + cr) + + if not user_id: + if self._REMOTE_USER_ATTRIBUTE in headers: + request.session.logout(keep_db=True) raise http.AuthenticationError() - return + else: + return None + + request_uid = request.session.uid + if request_uid: + if request_uid == user_id: + return + else: + request.session.logout(keep_db=True) # generate a specific key for authentication key = randomString(utils.KEY_LENGTH, '0123456789abcdef') diff --git a/auth_from_http_remote_user/model.py b/auth_from_http_remote_user/model.py index 4e514b833..c4ca50a33 100644 --- a/auth_from_http_remote_user/model.py +++ b/auth_from_http_remote_user/model.py @@ -22,6 +22,6 @@ from openerp.osv import orm class AuthFromHttpRemoteUserInstalled(orm.AbstractModel): - """An abstract model used to safely now if the module is installed + """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_config.py b/auth_from_http_remote_user/res_config.py deleted file mode 100644 index 37fca0bd5..000000000 --- a/auth_from_http_remote_user/res_config.py +++ /dev/null @@ -1,64 +0,0 @@ -# -*- 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, fields -from openerp.tools.safe_eval import safe_eval - - -class auth_from_http_remote_user_configuration(orm.TransientModel): - _inherit = 'base.config.settings' - - _columns = { - 'default_login_page_disabled': fields.boolean("Disable login page when " - "login with HTTP Remote " - "User", - help=""" -Disable the default login page. -If the HTTP_REMOTE_HEADER field is not found or no user matches the given one, -the system will display a login error page if the login page is disabled. -Otherwise the normal login page will be displayed. - """), - } - - def is_default_login_page_disabled(self, cr, uid, fields, context=None): - vals = self.get_default_default_login_page_disabled(cr, - uid, - fields, - context=context) - return vals.get('default_login_page_disabled', False) - - def get_default_default_login_page_disabled(self, cr, uid, fields, - context=None): - icp = self.pool.get('ir.config_parameter') - # we use safe_eval on the result, since the value of - # the parameter is a nonempty string - is_disabled = icp.get_param(cr, uid, 'default_login_page_disabled', - 'False') - return {'default_login_page_disabled': safe_eval(is_disabled)} - - def set_default_default_login_page_disabled(self, cr, uid, ids, - context=None): - config = self.browse(cr, uid, ids[0], context=context) - icp = self.pool.get('ir.config_parameter') - # we store the repr of the value, since the value of the parameter - # is a required string - icp.set_param(cr, uid, 'default_login_page_disabled', - repr(config.default_login_page_disabled)) diff --git a/auth_from_http_remote_user/res_config_data.xml b/auth_from_http_remote_user/res_config_data.xml deleted file mode 100644 index ba9a2b1c1..000000000 --- a/auth_from_http_remote_user/res_config_data.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - auth_from_http_remote_user.default_login_page_disabled - False - - - diff --git a/auth_from_http_remote_user/res_config_view.xml b/auth_from_http_remote_user/res_config_view.xml deleted file mode 100644 index e2f8df348..000000000 --- a/auth_from_http_remote_user/res_config_view.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - base.config.settings.auth_from_http_remote_user - base.config.settings - - - -
- -
-
-
-
-
-
From 68e96cd7822372000f09f79cddf11c8d9a86118d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Mon, 4 Aug 2014 17:50:13 +0200 Subject: [PATCH 15/18] auth_http_remote_user: slight reorganization and some comments (logic remains identical) --- .../controllers/main.py | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/auth_from_http_remote_user/controllers/main.py b/auth_from_http_remote_user/controllers/main.py index 13a3fa237..f91d50508 100644 --- a/auth_from_http_remote_user/controllers/main.py +++ b/auth_from_http_remote_user/controllers/main.py @@ -49,12 +49,7 @@ class Home(main.Home): return werkzeug.exceptions.Unauthorized().get_response() return super(Home, self).web_client(s_action, **kw) - def _get_user_id_from_headers(self, res_users, headers, cr): - login = headers.get(self._REMOTE_USER_ATTRIBUTE, None) - if not login: - _logger.info("Expected fields '%s' not found in http headers\n %s", - self._REMOTE_USER_ATTRIBUTE, headers) - return None + 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 @@ -67,24 +62,30 @@ class Home(main.Home): 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 - res_users = registry.get('res.users') - # get the user + headers = http.request.httprequest.headers.environ - user_id = self._get_user_id_from_headers(res_users, - headers, - cr) + login = headers.get(self._REMOTE_USER_ATTRIBUTE, None) + if not login: + # no HTTP_REMOTE_USER header, + # continue usual behavior + return + + res_users = registry.get('res.users') + + user_id = self._search_user(res_users, login, cr) if not user_id: - if self._REMOTE_USER_ATTRIBUTE in headers: - request.session.logout(keep_db=True) - raise http.AuthenticationError() - else: - return None + # HTTP_REMOTE_USER login not found in database + request.session.logout(keep_db=True) + raise http.AuthenticationError() request_uid = request.session.uid if request_uid: if request_uid == user_id: + # already authenticated return else: request.session.logout(keep_db=True) From 5e79ce29dcdb6d7c4f6283ff387f5c1c9523c202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Mon, 4 Aug 2014 17:53:24 +0200 Subject: [PATCH 16/18] auth_from_http_remote_user: remove unneeded browse --- auth_from_http_remote_user/controllers/main.py | 1 - 1 file changed, 1 deletion(-) diff --git a/auth_from_http_remote_user/controllers/main.py b/auth_from_http_remote_user/controllers/main.py index f91d50508..5214f125d 100644 --- a/auth_from_http_remote_user/controllers/main.py +++ b/auth_from_http_remote_user/controllers/main.py @@ -93,7 +93,6 @@ class Home(main.Home): # generate a specific key for authentication key = randomString(utils.KEY_LENGTH, '0123456789abcdef') res_users.write(cr, SUPERUSER_ID, [user_id], {'sso_key': key}) - login = res_users.browse(cr, SUPERUSER_ID, user_id).login request.session.authenticate(db_name, login=login, password=key, uid=user_id) except http.AuthenticationError, e: From c2fec40d136c91975cefc7cfad5a2ba16e483979 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Tue, 5 Aug 2014 09:24:50 +0200 Subject: [PATCH 17/18] auth_http_remote_user: test if already authenticated based on login instead of uid Avoids a database query unless authentication is actually required. --- auth_from_http_remote_user/controllers/main.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/auth_from_http_remote_user/controllers/main.py b/auth_from_http_remote_user/controllers/main.py index 5214f125d..85859619a 100644 --- a/auth_from_http_remote_user/controllers/main.py +++ b/auth_from_http_remote_user/controllers/main.py @@ -74,22 +74,21 @@ class Home(main.Home): # continue usual behavior return - res_users = registry.get('res.users') + 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() - request_uid = request.session.uid - if request_uid: - if request_uid == user_id: - # already authenticated - return - else: - request.session.logout(keep_db=True) - # generate a specific key for authentication key = randomString(utils.KEY_LENGTH, '0123456789abcdef') res_users.write(cr, SUPERUSER_ID, [user_id], {'sso_key': key}) From e3e191b4550f8682b119f1e6acf2fceb6f6f135f Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (aka lmi)" Date: Mon, 8 Dec 2014 11:15:15 +0100 Subject: [PATCH 18/18] [PEP-8] --- auth_from_http_remote_user/__openerp__.py | 4 ++-- auth_from_http_remote_user/tests/test_res_users.py | 14 ++++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/auth_from_http_remote_user/__openerp__.py b/auth_from_http_remote_user/__openerp__.py index 5266d4d12..d4e9731c2 100644 --- a/auth_from_http_remote_user/__openerp__.py +++ b/auth_from_http_remote_user/__openerp__.py @@ -40,8 +40,8 @@ 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. +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 diff --git a/auth_from_http_remote_user/tests/test_res_users.py b/auth_from_http_remote_user/tests/test_res_users.py index 486531bc7..43d5481c6 100644 --- a/auth_from_http_remote_user/tests/test_res_users.py +++ b/auth_from_http_remote_user/tests/test_res_users.py @@ -68,10 +68,10 @@ class test_res_users(common.TransactionCase): @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 ' + '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') @@ -81,8 +81,10 @@ class test_res_users(common.TransactionCase): 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']) + 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']) + read_vals = res_users_obj.read( + self.cr, self.uid, copy, ['sso_key']) self.assertFalse(read_vals.get('sso_key'))