Browse Source

Merge pull request #43 from acsone/auth_from_http_remote_user-8.0

[ADD] Authentication via HTTP Remote User 8.0
pull/122/head
Pedro M. Baeza 10 years ago
parent
commit
87900d4437
  1. 24
      auth_from_http_remote_user/__init__.py
  2. 162
      auth_from_http_remote_user/__openerp__.py
  3. 22
      auth_from_http_remote_user/controllers/__init__.py
  4. 110
      auth_from_http_remote_user/controllers/main.py
  5. 27
      auth_from_http_remote_user/model.py
  6. 65
      auth_from_http_remote_user/res_users.py
  7. 28
      auth_from_http_remote_user/tests/__init__.py
  8. 90
      auth_from_http_remote_user/tests/test_res_users.py
  9. 22
      auth_from_http_remote_user/utils.py

24
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 <http://www.gnu.org/licenses/>.
#
##############################################################################
from . import controllers
from . import res_users
from . import model

162
auth_from_http_remote_user/__openerp__.py

@ -0,0 +1,162 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Laurent Mignon
# Copyright 2014 'ACSONE SA/NV'
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <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,
}

22
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 <http://www.gnu.org/licenses/>.
#
##############################################################################
from . import main

110
auth_from_http_remote_user/controllers/main.py

@ -0,0 +1,110 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Laurent Mignon
# Copyright 2014 'ACSONE SA/NV'
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <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)])

27
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 <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'

65
auth_from_http_remote_user/res_users.py

@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Laurent Mignon
# Copyright 2014 'ACSONE SA/NV'
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <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

28
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 <http://www.gnu.org/licenses/>.
#
##############################################################################
from . import test_res_users
fast_suite = [
]
checks = [
test_res_users,
]

90
auth_from_http_remote_user/tests/test_res_users.py

@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Laurent Mignon
# Copyright 2014 'ACSONE SA/NV'
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <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'))

22
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 <http://www.gnu.org/licenses/>.
#
##############################################################################
KEY_LENGTH = 16
Loading…
Cancel
Save