From 4dd3e43075ba1175e3c22e7b9f1a6f8cddf244fe Mon Sep 17 00:00:00 2001 From: Holger Brunn Date: Mon, 5 Feb 2018 18:03:14 +0100 Subject: [PATCH] [MIG] users_ldap_groups --- users_ldap_groups/README.rst | 36 +++-- users_ldap_groups/__init__.py | 24 +--- users_ldap_groups/__manifest__.py | 38 ++---- users_ldap_groups/models/__init__.py | 7 + users_ldap_groups/models/res_company_ldap.py | 47 +++++++ .../models/res_company_ldap_group_mapping.py | 33 +++++ .../models/res_company_ldap_operator.py | 43 ++++++ users_ldap_groups/models/res_users.py | 27 ++++ users_ldap_groups/tests/__init__.py | 4 + .../tests/test_users_ldap_groups.py | 65 +++++++++ users_ldap_groups/users_ldap_groups.py | 128 ------------------ users_ldap_groups/users_ldap_groups.xml | 27 ---- .../views/base_config_settings.xml | 24 ++++ 13 files changed, 279 insertions(+), 224 deletions(-) create mode 100644 users_ldap_groups/models/__init__.py create mode 100644 users_ldap_groups/models/res_company_ldap.py create mode 100644 users_ldap_groups/models/res_company_ldap_group_mapping.py create mode 100644 users_ldap_groups/models/res_company_ldap_operator.py create mode 100644 users_ldap_groups/models/res_users.py create mode 100644 users_ldap_groups/tests/__init__.py create mode 100644 users_ldap_groups/tests/test_users_ldap_groups.py delete mode 100644 users_ldap_groups/users_ldap_groups.py delete mode 100644 users_ldap_groups/users_ldap_groups.xml create mode 100644 users_ldap_groups/views/base_config_settings.xml diff --git a/users_ldap_groups/README.rst b/users_ldap_groups/README.rst index 2dbc81de3..19c1eff63 100644 --- a/users_ldap_groups/README.rst +++ b/users_ldap_groups/README.rst @@ -1,34 +1,27 @@ .. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg :alt: License: AGPL-3 -users_ldap_groups -================= +LDAP groups assignment +====================== Adds user accounts to groups based on rules defined by the administrator. - Usage ===== -Define mappings in Settings->Companies->[your company]->tab configuration->[ -your ldap server]. +Define mappings in Settings / General Settings / LDAP Parameters + +Decide whether you want only groups mapped from ldap (`Only ldap groups` checked) or a mix of manually set groups and ldap groups (`Only ldap groups` unchecked). Setting this to 'no' will result in users never losing privileges when you remove them from a ldap group, so that's a potential security issue. It is still the default to prevent losing group information by accident. -Decide whether you want only groups mapped from ldap (Only ldap groups=y) or a -mix of manually set groups and ldap groups (Only ldap groups=n). Setting this -to 'no' will result in users never losing privileges when you remove them from -a ldap group, so that's a potential security issue. It is still the default to -prevent losing group information by accident. +For active directory, use LDAP attribute 'memberOf' and operator 'contains'. Fill in the DN of the windows group as value and choose an Odoo group users with this windows group are to be assigned to. -For active directory, use LDAP attribute 'memberOf' and operator 'contains'. -Fill in the DN of the windows group as value and choose an OpenERP group users -with this windows group are to be assigned to. +For posix accounts, use operator 'query' and a value like:: -For posix accounts, use operator 'query' and a value like -(&(cn=bzr)(objectClass=posixGroup)(memberUid=$uid)) + (&(cn=bzr)(objectClass=posixGroup)(memberUid=$uid)) The operator query matches if the filter in value returns something, and value -can contain $[attribute] which will be replaced by the first value of the -user's ldap record's attribute named [attribute]. +can contain ``$attribute`` which will be replaced by the first value of the +user's ldap record's attribute named `attribute`. Bug Tracker =========== @@ -42,10 +35,15 @@ If you spotted it first, help us smashing it by providing a detailed and welcome Credits ======= +Images +------ + +* Odoo Community Association: `Icon `_. + Contributors ------------ -* Therp BV +* Holger Brunn * Giacomo Spettoli Maintainer @@ -61,4 +59,4 @@ OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use. -To contribute to this module, please visit http://odoo-community.org. +To contribute to this module, please visit https://odoo-community.org. diff --git a/users_ldap_groups/__init__.py b/users_ldap_groups/__init__.py index d1066f41b..751782586 100644 --- a/users_ldap_groups/__init__.py +++ b/users_ldap_groups/__init__.py @@ -1,22 +1,4 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# This module copyright (C) 2012 Therp BV (). -# -# 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 . -# -############################################################################## - -from . import users_ldap_groups +# Copyright 2012-2018 Therp BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from . import models diff --git a/users_ldap_groups/__manifest__.py b/users_ldap_groups/__manifest__.py index 34a14bbf7..16dcb1b6d 100644 --- a/users_ldap_groups/__manifest__.py +++ b/users_ldap_groups/__manifest__.py @@ -1,39 +1,19 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# This module copyright (C) 2012 Therp BV (). -# -# 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 . -# -############################################################################## - +# Copyright 2012-2018 Therp BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). { - "name": "Groups assignment", - "version": "8.0.1.2.0", + "name": "LDAP groups assignment", + "version": "10.0.0.0.0", "depends": ["auth_ldap"], - "author": "Therp BV,Odoo Community Association (OCA)", + "author": "Therp BV, Odoo Community Association (OCA)", "license": "AGPL-3", - "summary": """ -Adds user accounts to groups based on rules defined by the administrator. -""", - "category": "Tools", + "summary": "Adds user accounts to groups based on rules defined " + "by the administrator.", + "category": "Authentication", "data": [ - 'users_ldap_groups.xml', + 'views/base_config_settings.xml', 'security/ir.model.access.csv', ], - 'installable': False, "external_dependencies": { 'python': ['ldap'], }, diff --git a/users_ldap_groups/models/__init__.py b/users_ldap_groups/models/__init__.py new file mode 100644 index 000000000..9adbf06b7 --- /dev/null +++ b/users_ldap_groups/models/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# Copyright 2012-2018 Therp BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from . import res_company_ldap +from . import res_company_ldap_operator +from . import res_company_ldap_group_mapping +from . import res_users diff --git a/users_ldap_groups/models/res_company_ldap.py b/users_ldap_groups/models/res_company_ldap.py new file mode 100644 index 000000000..800446fd8 --- /dev/null +++ b/users_ldap_groups/models/res_company_ldap.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Copyright 2012-2018 Therp BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from logging import getLogger +from odoo import api, fields, models +_logger = getLogger(__name__) + + +class ResCompanyLdap(models.Model): + _inherit = 'res.company.ldap' + + group_mapping_ids = fields.One2many( + 'res.company.ldap.group_mapping', + 'ldap_id', 'Group mappings', + help='Define how Odoo groups are assigned to ldap users', + ) + only_ldap_groups = fields.Boolean( + 'Only ldap groups', default=False, + help='If this is checked, manual changes to group membership are ' + 'undone on every login (so Odoo groups are always synchronous ' + 'with LDAP groups). If not, manually added groups are preserved.', + ) + + @api.model + def get_or_create_user(self, conf, login, ldap_entry): + op_obj = self.env['res.company.ldap.operator'] + user_id = super(ResCompanyLdap, self).get_or_create_user( + conf, login, ldap_entry + ) + if not user_id: + return user_id + this = self.browse(conf['id']) + user = self.env['res.users'].browse(user_id) + if this.only_ldap_groups: + _logger.debug('deleting all groups from user %d', user_id) + user.write({'groups_id': [(5, False, False)]}) + + for mapping in this.group_mapping_ids: + operator = getattr(op_obj, mapping.operator) + _logger.debug('checking mapping %s', mapping) + + if operator(ldap_entry, mapping): + _logger.debug( + 'adding user %d to group %s', user, mapping.group_id.name, + ) + user.write({'groups_id': [(4, mapping.group_id.id)]}) + return user_id diff --git a/users_ldap_groups/models/res_company_ldap_group_mapping.py b/users_ldap_groups/models/res_company_ldap_group_mapping.py new file mode 100644 index 000000000..e78faed19 --- /dev/null +++ b/users_ldap_groups/models/res_company_ldap_group_mapping.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# © 2012-2018 Therp BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import fields, models + + +class ResCompanyLdapGroupMapping(models.Model): + _name = 'res.company.ldap.group_mapping' + _rec_name = 'ldap_attribute' + _order = 'ldap_attribute' + + ldap_id = fields.Many2one( + 'res.company.ldap', 'LDAP server', required=True, ondelete='cascade', + ) + ldap_attribute = fields.Char( + 'LDAP attribute', + help='The LDAP attribute to check.\n' + 'For active directory, use memberOf.') + operator = fields.Selection( + lambda self: [ + (o, o) for o in self.env['res.company.ldap.operator'].operators() + ], + 'Operator', + help='The operator to check the attribute against the value\n' + 'For active directory, use \'contains\'', required=True) + value = fields.Char( + 'Value', + help='The value to check the attribute against.\n' + 'For active directory, use the dn of the desired group', + required=True) + group_id = fields.Many2one( + 'res.groups', 'Odoo group', oldname='group', + help='The Odoo group to assign', required=True) diff --git a/users_ldap_groups/models/res_company_ldap_operator.py b/users_ldap_groups/models/res_company_ldap_operator.py new file mode 100644 index 000000000..7dbd46f6c --- /dev/null +++ b/users_ldap_groups/models/res_company_ldap_operator.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# © 2012-2018 Therp BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from logging import getLogger +from odoo import api, models +from string import Template +_logger = getLogger(__name__) + + +class ResCompanyLdapOperator(models.AbstractModel): + """Define operators for group mappings""" + + _name = "res.company.ldap.operator" + _description = "Definition op LDAP operations" + + @api.model + def operators(self): + """Return names of function to call on this model as operator""" + return ('contains', 'equals', 'query') + + @api.model + def contains(self, ldap_entry, mapping): + return mapping.ldap_attribute in ldap_entry[1] and \ + mapping.value in ldap_entry[1][mapping.ldap_attribute] + + def equals(self, ldap_entry, mapping): + return mapping.ldap_attribute in ldap_entry[1] and \ + unicode(mapping.value) == unicode( + ldap_entry[1][mapping.ldap_attribute] + ) + + def query(self, ldap_entry, mapping): + query_string = Template(mapping.value).safe_substitute({ + attr: ldap_entry[1][attr][0] for attr in ldap_entry[1] + }) + _logger.debug( + 'evaluating query group mapping, filter: %s' % query_string + ) + results = mapping.ldap_id.query( + mapping.ldap_id.read()[0], query_string + ) + _logger.debug(results) + return bool(results) diff --git a/users_ldap_groups/models/res_users.py b/users_ldap_groups/models/res_users.py new file mode 100644 index 000000000..ac75d73db --- /dev/null +++ b/users_ldap_groups/models/res_users.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# © 2018 Therp BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import SUPERUSER_ID, api, models, registry + + +class ResUsers(models.Model): + _inherit = 'res.users' + + @classmethod + def _login(cls, db, login, password): + user_id = super(ResUsers, cls)._login(db, login, password) + if not user_id: + return user_id + with registry(db).cursor() as cr: + env = api.Environment(cr, SUPERUSER_ID, {}) + user = env['res.users'].browse(user_id) + # check if this user came from ldap, rerun get_or_create_user in + # this case to apply ldap groups if necessary + ldaps = user.company_id.ldaps + if user.active and any(ldaps.mapped('only_ldap_groups')): + for conf in ldaps.get_ldap_dicts(): + entry = ldaps.authenticate(conf, login, password) + if entry: + ldaps.get_or_create_user(conf, login, entry) + break + return user_id diff --git a/users_ldap_groups/tests/__init__.py b/users_ldap_groups/tests/__init__.py new file mode 100644 index 000000000..be1aab57a --- /dev/null +++ b/users_ldap_groups/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Therp BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from . import test_users_ldap_groups diff --git a/users_ldap_groups/tests/test_users_ldap_groups.py b/users_ldap_groups/tests/test_users_ldap_groups.py new file mode 100644 index 000000000..0f6dbecf5 --- /dev/null +++ b/users_ldap_groups/tests/test_users_ldap_groups.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Therp BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from mock import Mock, patch +from odoo.tests.common import TransactionCase + + +@patch('ldap.initialize', return_value=Mock( + search_st=Mock(return_value=[ + ('cn=hello', {'name': ['hello', 'hello2']}) + ]), +)) +class TestUsersLdapGroups(TransactionCase): + def test_users_ldap_groups(self, ldap_initialize): + # _login does its work in a new cursor, so we need one too + with self.env.registry.cursor() as cr: + env = self.env(cr=cr) + group_contains = env['res.groups'].create({'name': 'contains'}) + group_equals = env['res.groups'].create({'name': 'equals'}) + group_query = env['res.groups'].create({'name': 'query'}) + env.ref('base.main_company').write({'ldaps': [(0, 0, { + 'ldap_server': 'localhost', + 'ldap_filter': '(&(objectClass=*),(uid=%s))', + 'ldap_base': 'base', + 'only_ldap_groups': True, + 'group_mapping_ids': [ + (0, 0, { + 'ldap_attribute': 'name', + 'operator': 'contains', + 'value': 'hello3', + 'group_id': env.ref('base.group_system').id, + }), + (0, 0, { + 'ldap_attribute': 'name', + 'operator': 'contains', + 'value': 'hello2', + 'group_id': group_contains.id, + }), + (0, 0, { + 'ldap_attribute': 'name', + 'operator': 'equals', + 'value': 'hello', + 'group_id': group_equals.id, + }), + (0, 0, { + 'ldap_attribute': '', + 'operator': 'query', + 'value': 'is not run because of patching', + 'group_id': group_query.id, + }), + ], + })]}) + + self.env['res.users']._login(self.env.cr.dbname, 'demo', 'wrong') + with self.env.registry.cursor() as cr: + env = self.env(cr=cr) + demo_user = env.ref('base.user_demo') + # this asserts group mappings from demo data + groups = demo_user.groups_id + self.assertIn(group_contains, groups) + self.assertNotIn(group_equals, groups) + self.assertIn(group_query, groups) + self.assertNotIn(env.ref('base.group_system'), groups) + # clean up + env.ref('base.main_company').write({'ldaps': [(6, False, [])]}) diff --git a/users_ldap_groups/users_ldap_groups.py b/users_ldap_groups/users_ldap_groups.py deleted file mode 100644 index 4d08d8aaf..000000000 --- a/users_ldap_groups/users_ldap_groups.py +++ /dev/null @@ -1,128 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# This module copyright (C) 2012 Therp BV (). -# -# 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 . -# -############################################################################## - -from openerp import models -from openerp import fields -from openerp import api -import logging -from string import Template - - -class LDAPOperator(models.AbstractModel): - _name = "res.company.ldap.operator" - - def operators(self): - return ('contains', 'equals', 'query') - - def contains(self, ldap_entry, attribute, value, ldap_config, company, - logger): - return (attribute in ldap_entry[1]) and \ - (value in ldap_entry[1][attribute]) - - def equals(self, ldap_entry, attribute, value, ldap_config, company, - logger): - return attribute in ldap_entry[1] and \ - unicode(value) == unicode(ldap_entry[1][attribute]) - - def query(self, ldap_entry, attribute, value, ldap_config, company, - logger): - query_string = Template(value).safe_substitute(dict( - [(attr, ldap_entry[1][attribute][0]) for attr in ldap_entry[1]] - ) - ) - logger.debug('evaluating query group mapping, filter: %s' % - query_string) - results = company.query(ldap_config, query_string) - logger.debug(results) - return bool(results) - - -class CompanyLDAPGroupMapping(models.Model): - _name = 'res.company.ldap.group_mapping' - _rec_name = 'ldap_attribute' - _order = 'ldap_attribute' - - def _get_operators(self): - op_obj = self.env['res.company.ldap.operator'] - operators = [(op, op) for op in op_obj.operators()] - return tuple(operators) - - ldap_id = fields.Many2one('res.company.ldap', 'LDAP server', required=True) - ldap_attribute = fields.Char( - 'LDAP attribute', - help='The LDAP attribute to check.\n' - 'For active directory, use memberOf.') - operator = fields.Selection( - _get_operators, 'Operator', - help='The operator to check the attribute against the value\n' - 'For active directory, use \'contains\'', required=True) - value = fields.Char( - 'Value', - help='The value to check the attribute against.\n' - 'For active directory, use the dn of the desired group', - required=True) - group = fields.Many2one( - 'res.groups', 'OpenERP group', - help='The OpenERP group to assign', required=True) - - -class CompanyLDAP(models.Model): - _inherit = 'res.company.ldap' - - group_mappings = fields.One2many( - 'res.company.ldap.group_mapping', - 'ldap_id', 'Group mappings', - help='Define how OpenERP groups are assigned to ldap users') - only_ldap_groups = fields.Boolean( - 'Only ldap groups', - help='If this is checked, manual changes to group membership are ' - 'undone on every login (so OpenERP groups are always synchronous ' - 'with LDAP groups). If not, manually added groups are preserved.') - - _default = { - 'only_ldap_groups': False, - } - - @api.model - def get_or_create_user(self, conf, login, ldap_entry): - op_obj = self.env['res.company.ldap.operator'] - id_ = conf['id'] - this = self.browse(id_) - user_id = super(CompanyLDAP, self).get_or_create_user( - conf, login, ldap_entry) - if not user_id: - return user_id - userobj = self.env['res.users'] - user = userobj.browse(user_id) - logger = logging.getLogger('users_ldap_groups') - if self.only_ldap_groups: - logger.debug('deleting all groups from user %d' % user_id) - user.write({'groups_id': [(5, )]}) - - for mapping in this.group_mappings: - operator = getattr(op_obj, mapping.operator) - logger.debug('checking mapping %s' % mapping) - if operator(ldap_entry, mapping['ldap_attribute'], - mapping['value'], conf, self, logger): - logger.debug('adding user %d to group %s' % - (user_id, mapping.group.name)) - user.write({'groups_id': [(4, mapping.group.id)]}) - return user_id diff --git a/users_ldap_groups/users_ldap_groups.xml b/users_ldap_groups/users_ldap_groups.xml deleted file mode 100644 index cee402bff..000000000 --- a/users_ldap_groups/users_ldap_groups.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - res.company.form.inherit.users_ldap_groups - res.company - - - - - - - - - - - - - - - - - - - - - diff --git a/users_ldap_groups/views/base_config_settings.xml b/users_ldap_groups/views/base_config_settings.xml new file mode 100644 index 000000000..7a0ada56a --- /dev/null +++ b/users_ldap_groups/views/base_config_settings.xml @@ -0,0 +1,24 @@ + + + + base.config.settings + + + + + + + + + + +