Browse Source

[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

pull/34/head
Laurent Mignon 10 years ago
parent
commit
7dd5bc685e
  1. 24
      auth_from_http_remote_user/__init__.py
  2. 133
      auth_from_http_remote_user/__openerp__.py
  3. 22
      auth_from_http_remote_user/controllers/__init__.py
  4. 129
      auth_from_http_remote_user/controllers/session.py
  5. 60
      auth_from_http_remote_user/res_config.py
  6. 9
      auth_from_http_remote_user/res_config_data.xml
  7. 39
      auth_from_http_remote_user/res_config_view.xml
  8. 64
      auth_from_http_remote_user/res_users.py
  9. 36
      auth_from_http_remote_user/static/src/js/auth_from_http_remote_user.js
  10. 28
      auth_from_http_remote_user/tests/__init__.py
  11. 83
      auth_from_http_remote_user/tests/test_res_users.py
  12. 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_config
from . import res_users

133
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 <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
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: ::
<VirtualHost *:80>
ServerName MY_VHOST.com
ProxyRequests Off
<Location />
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
</Location>
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 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,
}

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 session

129
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 <http://www.gnu.org/licenses/>.
#
##############################################################################
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)])

60
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 <http://www.gnu.org/licenses/>.
#
##############################################################################
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))

9
auth_from_http_remote_user/res_config_data.xml

@ -0,0 +1,9 @@
<?xml version="1.0"?>
<openerp>
<data noupdate="1">
<record model="ir.config_parameter" id="auth_from_http_remote_user.default_login_page_disabled">
<field name="key">auth_from_http_remote_user.default_login_page_disabled</field>
<field name="value">False</field>
</record>
</data>
</openerp>

39
auth_from_http_remote_user/res_config_view.xml

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record model="ir.ui.view" id="help_view_config_settings">
<field name="name">Auth HTTP_REMOTE_USER settings</field>
<field name="model">auth_from_http_remote_user.config.settings</field>
<field name="priority" eval="20" />
<field name="arch" type="xml">
<form string="Configure Auth HTTP_REMOTE_USER" version="7.0"
class="oe_form_configuration">
<header>
<header>
<button string="Apply" type="object" name="execute" class="oe_highlight"/>
or
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
</header>
</header>
<separator string="Auth HTTP_REMOTE_USER" />
<group>
<field name="default_login_page_disabled" />
</group>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="auth_from_http_remote_user_action_config_settings">
<field name="name">Configure Auth HTTP_REMOTE_USER</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">auth_from_http_remote_user.config.settings</field>
<field name="view_mode">form</field>
<field name="target">inline</field>
</record>
<menuitem id="base.menu_auth_from_http_remote_user_config" name="Auth HTTP_REMOTE_USER"
parent="base.menu_config" sequence="10"
action="auth_from_http_remote_user_action_config_settings" />
</data>
</openerp>

64
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 <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

36
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("<h1>Access Denied</h1>");
}
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("<h1>Server error</h1>");
});
}
});
};

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,
]

83
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 <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')
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'))

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