diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/auth_ldap/models | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/auth_ldap/models')
| -rw-r--r-- | addons/auth_ldap/models/__init__.py | 6 | ||||
| -rw-r--r-- | addons/auth_ldap/models/res_company.py | 11 | ||||
| -rw-r--r-- | addons/auth_ldap/models/res_company_ldap.py | 226 | ||||
| -rw-r--r-- | addons/auth_ldap/models/res_config_settings.py | 10 | ||||
| -rw-r--r-- | addons/auth_ldap/models/res_users.py | 60 |
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,) + ) |
