Browse Source
Merge pull request #43 from acsone/auth_from_http_remote_user-8.0
Merge pull request #43 from acsone/auth_from_http_remote_user-8.0
[ADD] Authentication via HTTP Remote User 8.0pull/122/head
Pedro M. Baeza
10 years ago
9 changed files with 550 additions and 0 deletions
-
24auth_from_http_remote_user/__init__.py
-
162auth_from_http_remote_user/__openerp__.py
-
22auth_from_http_remote_user/controllers/__init__.py
-
110auth_from_http_remote_user/controllers/main.py
-
27auth_from_http_remote_user/model.py
-
65auth_from_http_remote_user/res_users.py
-
28auth_from_http_remote_user/tests/__init__.py
-
90auth_from_http_remote_user/tests/test_res_users.py
-
22auth_from_http_remote_user/utils.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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
|
|||
from . import controllers |
|||
from . import res_users |
|||
from . import model |
@ -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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
{ |
|||
'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 :: |
|||
|
|||
<VirtualHost *:80> |
|||
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 |
|||
</VirtualHost> |
|||
|
|||
|
|||
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: :: |
|||
|
|||
<VirtualHost *:80> |
|||
ServerName MY_VHOST.com |
|||
ProxyRequests Off |
|||
<Location /> |
|||
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 |
|||
</Location> |
|||
|
|||
RequestHeader unset Remote-User early |
|||
ProxyPass / http://127.0.0.1:8069/ retry=10 |
|||
ProxyPassReverse / http://127.0.0.1:8069/ |
|||
ProxyPreserveHost On |
|||
</VirtualHost> |
|||
|
|||
.. 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, |
|||
} |
@ -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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
|
|||
from . import main |
@ -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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
|
|||
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)]) |
@ -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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
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' |
@ -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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
|
|||
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 |
@ -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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
|
|||
from . import test_res_users |
|||
fast_suite = [ |
|||
] |
|||
|
|||
checks = [ |
|||
test_res_users, |
|||
] |
@ -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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
|
|||
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')) |
@ -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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
|
|||
KEY_LENGTH = 16 |
Write
Preview
Loading…
Cancel
Save
Reference in new issue