Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

9.0 patch auth ldap #3

Open
wants to merge 3 commits into
base: 9.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 120 additions & 83 deletions addons/auth_ldap/users_ldap.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@
from openerp.osv import fields, osv
from openerp import SUPERUSER_ID
from openerp.modules.registry import RegistryManager

_logger = logging.getLogger(__name__)
import psycopg2


class CompanyLDAP(osv.osv):
_name = 'res.company.ldap'
_order = 'sequence'
_rec_name = 'ldap_server'

def get_ldap_dicts(self, cr, ids=None):
"""
"""
Retrieve res_company_ldap resources from the database in dictionary
format.

Expand All @@ -33,31 +36,39 @@ def get_ldap_dicts(self, cr, ids=None):
else:
id_clause = ''
args = []
cr.execute("""
SELECT id, company, ldap_server, ldap_server_port, ldap_binddn,
ldap_password, ldap_filter, ldap_base, "user", create_user,
ldap_tls
FROM res_company_ldap
WHERE ldap_server != '' """ + id_clause + """ ORDER BY sequence
""", args)
return cr.dictfetchall()
try:
cr.execute("""
SELECT id, company, ldap_server, ldap_server_port, ldap_bind_suffix, ldap_pre_bind, ldap_binddn,
ldap_password, ldap_filter, ldap_base, "user", create_user,
ldap_tls
FROM res_company_ldap
WHERE ldap_server != '' """ + id_clause + """ ORDER BY sequence
""", args)
return cr.dictfetchall()
except psycopg2.ProgrammingError:
# Do not fail during upgrade, some fields may be missing
_logger.exception("Error in get_ldap_dicts")
return []

def connect(self, conf):
"""
"""
Connect to an LDAP server specified by an ldap
configuration dictionary.

:param dict conf: LDAP configuration
:return: an LDAP object
"""

uri = 'ldap://%s:%d' % (conf['ldap_server'],
conf['ldap_server_port'])

connection = ldap.initialize(uri)
if conf['ldap_tls']:
connection.start_tls_s()
return connection
protocol = "ldaps"
else:
protocol = "ldap"

uri = '%s://%s:%d' % (protocol, conf['ldap_server'],
conf['ldap_server_port'])

_logger.debug("Using LDAP URI: %s" % repr(uri))
return ldap.initialize(uri)

def authenticate(self, conf, login, password):
"""
Expand All @@ -66,7 +77,7 @@ def authenticate(self, conf, login, password):
In order to prevent an unintended 'unauthenticated authentication',
which is an anonymous bind with a valid dn and a blank password,
check for empty passwords explicitely (:rfc:`4513#section-6.3.1`)

:param dict conf: LDAP configuration
:param login: username
:param password: Password for the LDAP user
Expand All @@ -78,41 +89,57 @@ def authenticate(self, conf, login, password):
return False

entry = False
filter = filter_format(conf['ldap_filter'], (login,))
conn = self.connect(conf)

try:
filter = filter_format(conf['ldap_filter'], (login,))
except TypeError:
_logger.warning('Could not format LDAP filter. Your filter should contain one \'%s\'.')
if conf['ldap_pre_bind']:
if conf['ldap_binddn']:
bind_dn = "%s%s" % (
conf['ldap_binddn'], conf['ldap_bind_suffix'] or '')
else:
bind_dn = ''
conn.simple_bind_s(bind_dn,
conf['ldap_password'] or '')
results = self.query(conn, conf['ldap_base'], filter)

if len(results) != 1:
_logger.error("Filter %s on base %s returned %s entries" % (
filter, conf['ldap_base'], len(results)))
return False

user_dn = results[0][0]
conn.simple_bind_s(user_dn, password.encode('utf-8'))
_logger.info("Successful LDAP login for %s" % login)
else:
bind_dn = "%s%s" % (login, conf['ldap_bind_suffix'] or '')
conn.simple_bind_s(bind_dn, password.encode('utf-8'))
_logger.info("Successful LDAP login for %s" % login)
results = self.query(conn, conf['ldap_base'], filter)

if len(results) != 1:
_logger.error("Filter %s on base %s returned %s entries" % (
filter, conf['ldap_base'], len(results)))
return False

entry = results[0]
except ldap.INVALID_CREDENTIALS, e:
_logger.debug(
"Invalid credentials for %s: %s" % (repr(bind_dn), repr(e)))
return False
try:
results = self.query(conf, filter.encode('utf-8'))

# Get rid of (None, attrs) for searchResultReference replies
results = [i for i in results if i[0]]
if results and len(results) == 1:
dn = results[0][0]
conn = self.connect(conf)
conn.simple_bind_s(dn, password.encode('utf-8'))
conn.unbind()
entry = results[0]
except ldap.INVALID_CREDENTIALS:
except ldap.LDAPError:
_logger.exception('An LDAP exception occurred')
return False
except ldap.LDAPError, e:
_logger.error('An LDAP exception occurred: %s', e)
return entry

def query(self, conf, filter, retrieve_attributes=None):
"""
Query an LDAP server with the filter argument and scope subtree.
finally:
conn.unbind()

Allow for all authentication methods of the simple authentication
method:
_logger.debug("LDAP result: %s" % repr(entry))

- authenticated bind (non-empty binddn + valid password)
- anonymous bind (empty binddn + empty password)
- unauthenticated authentication (non-empty binddn + empty password)
return entry

.. seealso::
:rfc:`4513#section-5.1` - LDAP: Simple Authentication Method.
def query(self, conn, base, filter, retrieve_attributes=None):
"""
Query an LDAP server with the filter argument and scope subtree.

:param dict conf: LDAP configuration
:param filter: valid LDAP filter
Expand All @@ -124,38 +151,35 @@ def query(self, conf, filter, retrieve_attributes=None):
"""

results = []
try:
conn = self.connect(conf)
ldap_password = conf['ldap_password'] or ''
ldap_binddn = conf['ldap_binddn'] or ''
conn.simple_bind_s(ldap_binddn.encode('utf-8'), ldap_password.encode('utf-8'))
results = conn.search_st(conf['ldap_base'], ldap.SCOPE_SUBTREE,
filter, retrieve_attributes, timeout=60)
conn.unbind()
except ldap.INVALID_CREDENTIALS:
_logger.error('LDAP bind failed.')
except ldap.LDAPError, e:
_logger.error('An LDAP exception occurred: %s', e)
results = conn.search_st(base.encode('utf-8'), ldap.SCOPE_SUBTREE,
filter, retrieve_attributes, timeout=60)

# Get rid of (None, attrs) for searchResultReference replies
results = [i for i in results if i[0]]

_logger.debug("LDAP search base=%s filter=%s returned %s results" % (
repr(base), repr(filter), len(results)))

return results

def map_ldap_attributes(self, cr, uid, conf, login, ldap_entry):
"""
Compose values for a new resource of model res_users,
based upon the retrieved ldap entry and the LDAP settings.

:param dict conf: LDAP configuration
:param login: the new user's login
:param tuple ldap_entry: single LDAP result (dn, attrs)
:return: parameters for a new resource of model res_users
:rtype: dict
"""

values = { 'name': ldap_entry[1]['cn'][0],
'login': login,
'company_id': conf['company']
}
values = {'name': ldap_entry[1]['cn'][0],
'login': login,
'company_id': conf['company']
}
return values

def get_or_create_user(self, cr, uid, conf, login, ldap_entry,
context=None):
"""
Expand All @@ -168,10 +192,11 @@ def get_or_create_user(self, cr, uid, conf, login, ldap_entry,
:return: res_users id
:rtype: int
"""

user_id = False
login = tools.ustr(login.lower().strip())
cr.execute("SELECT id, active FROM res_users WHERE lower(login)=%s", (login,))
cr.execute("SELECT id, active FROM res_users WHERE lower(login)=%s",
(login,))
res = cr.fetchone()
if res:
if res[1]:
Expand All @@ -191,25 +216,35 @@ def get_or_create_user(self, cr, uid, conf, login, ldap_entry,
_columns = {
'sequence': fields.integer('Sequence'),
'company': fields.many2one('res.company', 'Company', required=True,
ondelete='cascade'),
ondelete='cascade'),
'ldap_server': fields.char('LDAP Server address', required=True),
'ldap_server_port': fields.integer('LDAP Server port', required=True),
'ldap_binddn': fields.char('LDAP binddn',
help=("The user account on the LDAP server that is used to query "
"the directory. Leave empty to connect anonymously.")),
'ldap_bind_suffix': fields.char('LDAP bind suffix',
help="""Suffix to append to the user login, useful for automatically
specifying a domain like @company.com. Only used when ldap_pre_bind
is turned off."""),
'ldap_pre_bind': fields.boolean('Perform two-step bind', default=True,
help="""Traditionally Odoo binds two times: once anonymously or with
ldap_binddn, and a second time with the user login. Disable this
option to bind directly with the user-provided credentials."""),
'ldap_binddn': fields.char('LDAP binddn',
help="""The user account on the LDAP server that is used to query
the directory for two-step bind. Leave empty to connect
anonymously or to use one-step bind."""),
'ldap_password': fields.char('LDAP password',
help=("The password of the user account on the LDAP server that is "
"used to query the directory.")),
help=(
"The password of the user account on the LDAP server that is "
"used to query the directory for two-step bind.")),
'ldap_filter': fields.char('LDAP filter', required=True),
'ldap_base': fields.char('LDAP base', required=True),
'user': fields.many2one('res.users', 'Template User',
help="User to copy when creating new users"),
help="User to copy when creating new users"),
'create_user': fields.boolean('Create user',
help="Automatically create local user accounts for new users authenticating via LDAP"),
help="Automatically create local user accounts for new users authenticating via LDAP"),
'ldap_tls': fields.boolean('Use TLS',
help="Request secure TLS/SSL encryption when connecting to the LDAP server. "
"This option requires a server with STARTTLS enabled, "
"otherwise all authentication attempts will fail."),
help="Request secure TLS/SSL encryption when connecting to the LDAP server. "
"This option requires a server with STARTTLS enabled, "
"otherwise all authentication attempts will fail."),
}
_defaults = {
'ldap_server': '127.0.0.1',
Expand All @@ -219,24 +254,26 @@ def get_or_create_user(self, cr, uid, conf, login, ldap_entry,
}



class res_company(osv.osv):
_inherit = "res.company"
_columns = {
'ldaps': fields.one2many(
'res.company.ldap', 'company', 'LDAP Parameters', copy=True, groups="base.group_system"),
'res.company.ldap', 'company', 'LDAP Parameters', copy=True,
groups="base.group_system"),
}


class users(osv.osv):
_inherit = "res.users"

def _login(self, db, login, password):
user_id = super(users, self)._login(db, login, password)
if user_id:
return user_id
registry = RegistryManager.get(db)
with registry.cursor() as cr:
cr.execute("SELECT id FROM res_users WHERE lower(login)=%s", (login,))
cr.execute("SELECT id FROM res_users WHERE lower(login)=%s",
(login,))
res = cr.fetchone()
if res:
return False
Expand All @@ -255,13 +292,13 @@ def check_credentials(self, cr, uid, password):
super(users, self).check_credentials(cr, uid, password)
except openerp.exceptions.AccessDenied:

cr.execute('SELECT login FROM res_users WHERE id=%s AND active=TRUE',
(int(uid),))
cr.execute(
'SELECT login FROM res_users WHERE id=%s AND active=TRUE',
(int(uid),))
res = cr.fetchone()
if res:
ldap_obj = self.pool['res.company.ldap']
for conf in ldap_obj.get_ldap_dicts(cr):
if ldap_obj.authenticate(conf, res[0], password):
return
raise

3 changes: 2 additions & 1 deletion addons/auth_ldap/users_ldap_view.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@
<group col="4">
<field name="ldap_server"/>
<field name="ldap_server_port"/>
<field name="ldap_bind_suffix"/>
<field name="ldap_pre_bind"/>
<field name="ldap_binddn"/>
<field name="ldap_password" password="True"/>
<field name="ldap_base"/>
<field name="ldap_filter"/>
<field name="create_user"/>
<field name="user"/>
<newline/>
<field name="sequence"/>
<field name="ldap_tls"/>
</group>
Expand Down