summaryrefslogtreecommitdiff
path: root/addons/auth_ldap/models
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/auth_ldap/models
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/auth_ldap/models')
-rw-r--r--addons/auth_ldap/models/__init__.py6
-rw-r--r--addons/auth_ldap/models/res_company.py11
-rw-r--r--addons/auth_ldap/models/res_company_ldap.py226
-rw-r--r--addons/auth_ldap/models/res_config_settings.py10
-rw-r--r--addons/auth_ldap/models/res_users.py60
5 files changed, 313 insertions, 0 deletions
diff --git a/addons/auth_ldap/models/__init__.py b/addons/auth_ldap/models/__init__.py
new file mode 100644
index 00000000..9e041b4c
--- /dev/null
+++ b/addons/auth_ldap/models/__init__.py
@@ -0,0 +1,6 @@
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import res_company
+from . import res_company_ldap
+from . import res_users
+from . import res_config_settings \ No newline at end of file
diff --git a/addons/auth_ldap/models/res_company.py b/addons/auth_ldap/models/res_company.py
new file mode 100644
index 00000000..f6a519ff
--- /dev/null
+++ b/addons/auth_ldap/models/res_company.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+class ResCompany(models.Model):
+ _inherit = "res.company"
+
+ ldaps = fields.One2many('res.company.ldap', 'company', string='LDAP Parameters',
+ copy=True, groups="base.group_system")
diff --git a/addons/auth_ldap/models/res_company_ldap.py b/addons/auth_ldap/models/res_company_ldap.py
new file mode 100644
index 00000000..c7a3b758
--- /dev/null
+++ b/addons/auth_ldap/models/res_company_ldap.py
@@ -0,0 +1,226 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import ldap
+import logging
+from ldap.filter import filter_format
+
+from odoo import _, api, fields, models, tools
+from odoo.exceptions import AccessDenied
+from odoo.tools.pycompat import to_text
+
+_logger = logging.getLogger(__name__)
+
+
+class CompanyLDAP(models.Model):
+ _name = 'res.company.ldap'
+ _description = 'Company LDAP configuration'
+ _order = 'sequence'
+ _rec_name = 'ldap_server'
+
+ sequence = fields.Integer(default=10)
+ company = fields.Many2one('res.company', string='Company', required=True, ondelete='cascade')
+ ldap_server = fields.Char(string='LDAP Server address', required=True, default='127.0.0.1')
+ ldap_server_port = fields.Integer(string='LDAP Server port', required=True, default=389)
+ 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_password = fields.Char(string='LDAP password',
+ help="The password of the user account on the LDAP server that is used to query the directory.")
+ ldap_filter = fields.Char(string='LDAP filter', required=True)
+ ldap_base = fields.Char(string='LDAP base', required=True)
+ user = fields.Many2one('res.users', string='Template User',
+ help="User to copy when creating new users")
+ create_user = fields.Boolean(default=True,
+ help="Automatically create local user accounts for new users authenticating via LDAP")
+ ldap_tls = fields.Boolean(string='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.")
+
+ def _get_ldap_dicts(self):
+ """
+ Retrieve res_company_ldap resources from the database in dictionary
+ format.
+ :return: ldap configurations
+ :rtype: list of dictionaries
+ """
+
+ ldaps = self.sudo().search([('ldap_server', '!=', False)], order='sequence')
+ res = ldaps.read([
+ 'id',
+ 'company',
+ 'ldap_server',
+ 'ldap_server_port',
+ 'ldap_binddn',
+ 'ldap_password',
+ 'ldap_filter',
+ 'ldap_base',
+ 'user',
+ 'create_user',
+ 'ldap_tls'
+ ])
+ return res
+
+ 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
+
+ def _get_entry(self, conf, login):
+ filter, dn, entry = False, False, False
+ try:
+ filter = filter_format(conf['ldap_filter'], (login,))
+ except TypeError:
+ _logger.warning('Could not format LDAP filter. Your filter should contain one \'%s\'.')
+ if filter:
+ results = self._query(conf, tools.ustr(filter))
+
+ # Get rid of (None, attrs) for searchResultReference replies
+ results = [i for i in results if i[0]]
+ if len(results) == 1:
+ entry = results[0]
+ dn = results[0][0]
+ return dn, entry
+
+ def _authenticate(self, conf, login, password):
+ """
+ Authenticate a user against the specified LDAP server.
+
+ 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
+ :return: LDAP entry of authenticated user or False
+ :rtype: dictionary of attributes
+ """
+
+ if not password:
+ return False
+
+ dn, entry = self._get_entry(conf, login)
+ if not dn:
+ return False
+ try:
+ conn = self._connect(conf)
+ conn.simple_bind_s(dn, to_text(password))
+ conn.unbind()
+ except ldap.INVALID_CREDENTIALS:
+ return False
+ except ldap.LDAPError as e:
+ _logger.error('An LDAP exception occurred: %s', e)
+ return False
+ return entry
+
+ def _query(self, conf, filter, retrieve_attributes=None):
+ """
+ Query an LDAP server with the filter argument and scope subtree.
+
+ Allow for all authentication methods of the simple authentication
+ method:
+
+ - authenticated bind (non-empty binddn + valid password)
+ - anonymous bind (empty binddn + empty password)
+ - unauthenticated authentication (non-empty binddn + empty password)
+
+ .. seealso::
+ :rfc:`4513#section-5.1` - LDAP: Simple Authentication Method.
+
+ :param dict conf: LDAP configuration
+ :param filter: valid LDAP filter
+ :param list retrieve_attributes: LDAP attributes to be retrieved. \
+ If not specified, return all attributes.
+ :return: ldap entries
+ :rtype: list of tuples (dn, attrs)
+
+ """
+
+ results = []
+ try:
+ conn = self._connect(conf)
+ ldap_password = conf['ldap_password'] or ''
+ ldap_binddn = conf['ldap_binddn'] or ''
+ conn.simple_bind_s(to_text(ldap_binddn), to_text(ldap_password))
+ results = conn.search_st(to_text(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 as e:
+ _logger.error('An LDAP exception occurred: %s', e)
+ return results
+
+ def _map_ldap_attributes(self, 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
+ """
+
+ return {
+ 'name': tools.ustr(ldap_entry[1]['cn'][0]),
+ 'login': login,
+ 'company_id': conf['company'][0]
+ }
+
+ def _get_or_create_user(self, conf, login, ldap_entry):
+ """
+ Retrieve an active resource of model res_users with the specified
+ login. Create the user if it is not initially found.
+
+ :param dict conf: LDAP configuration
+ :param login: the user's login
+ :param tuple ldap_entry: single LDAP result (dn, attrs)
+ :return: res_users id
+ :rtype: int
+ """
+ login = tools.ustr(login.lower().strip())
+ self.env.cr.execute("SELECT id, active FROM res_users WHERE lower(login)=%s", (login,))
+ res = self.env.cr.fetchone()
+ if res:
+ if res[1]:
+ return res[0]
+ elif conf['create_user']:
+ _logger.debug("Creating new Odoo user \"%s\" from LDAP" % login)
+ values = self._map_ldap_attributes(conf, login, ldap_entry)
+ SudoUser = self.env['res.users'].sudo().with_context(no_reset_password=True)
+ if conf['user']:
+ values['active'] = True
+ return SudoUser.browse(conf['user'][0]).copy(default=values).id
+ else:
+ return SudoUser.create(values).id
+
+ raise AccessDenied(_("No local user found for LDAP login and not configured to create one"))
+
+ def _change_password(self, conf, login, old_passwd, new_passwd):
+ changed = False
+ dn, entry = self._get_entry(conf, login)
+ if not dn:
+ return False
+ try:
+ conn = self._connect(conf)
+ conn.simple_bind_s(dn, to_text(old_passwd))
+ conn.passwd_s(dn, old_passwd, new_passwd)
+ changed = True
+ conn.unbind()
+ except ldap.INVALID_CREDENTIALS:
+ pass
+ except ldap.LDAPError as e:
+ _logger.error('An LDAP exception occurred: %s', e)
+ return changed
diff --git a/addons/auth_ldap/models/res_config_settings.py b/addons/auth_ldap/models/res_config_settings.py
new file mode 100644
index 00000000..4cc12be3
--- /dev/null
+++ b/addons/auth_ldap/models/res_config_settings.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+class ResConfigSettings(models.TransientModel):
+ _inherit = 'res.config.settings'
+
+ ldaps = fields.One2many(related='company_id.ldaps', string="LDAP Parameters", readonly=False)
diff --git a/addons/auth_ldap/models/res_users.py b/addons/auth_ldap/models/res_users.py
new file mode 100644
index 00000000..a1532217
--- /dev/null
+++ b/addons/auth_ldap/models/res_users.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.exceptions import AccessDenied
+
+from odoo import api, models, registry, SUPERUSER_ID
+
+
+class Users(models.Model):
+ _inherit = "res.users"
+
+ @classmethod
+ def _login(cls, db, login, password, user_agent_env):
+ try:
+ return super(Users, cls)._login(db, login, password, user_agent_env=user_agent_env)
+ except AccessDenied as e:
+ with registry(db).cursor() as cr:
+ cr.execute("SELECT id FROM res_users WHERE lower(login)=%s", (login,))
+ res = cr.fetchone()
+ if res:
+ raise e
+
+ env = api.Environment(cr, SUPERUSER_ID, {})
+ Ldap = env['res.company.ldap']
+ for conf in Ldap._get_ldap_dicts():
+ entry = Ldap._authenticate(conf, login, password)
+ if entry:
+ return Ldap._get_or_create_user(conf, login, entry)
+ raise e
+
+ def _check_credentials(self, password, env):
+ try:
+ return super(Users, self)._check_credentials(password, env)
+ except AccessDenied:
+ passwd_allowed = env['interactive'] or not self.env.user._rpc_api_keys_only()
+ if passwd_allowed and self.env.user.active:
+ Ldap = self.env['res.company.ldap']
+ for conf in Ldap._get_ldap_dicts():
+ if Ldap._authenticate(conf, self.env.user.login, password):
+ return
+ raise
+
+ @api.model
+ def change_password(self, old_passwd, new_passwd):
+ if new_passwd:
+ Ldap = self.env['res.company.ldap']
+ for conf in Ldap._get_ldap_dicts():
+ changed = Ldap._change_password(conf, self.env.user.login, old_passwd, new_passwd)
+ if changed:
+ uid = self.env.user.id
+ self._set_empty_password(uid)
+ self.invalidate_cache(['password'], [uid])
+ return True
+ return super(Users, self).change_password(old_passwd, new_passwd)
+
+ def _set_empty_password(self, uid):
+ self.env.cr.execute(
+ 'UPDATE res_users SET password=NULL WHERE id=%s',
+ (uid,)
+ )