Browse Source

[MIG] users_ldap_groups

pull/1144/head
Holger Brunn 7 years ago
parent
commit
4dd3e43075
No known key found for this signature in database GPG Key ID: 1C9760FECA3AE18
  1. 36
      users_ldap_groups/README.rst
  2. 24
      users_ldap_groups/__init__.py
  3. 38
      users_ldap_groups/__manifest__.py
  4. 7
      users_ldap_groups/models/__init__.py
  5. 47
      users_ldap_groups/models/res_company_ldap.py
  6. 33
      users_ldap_groups/models/res_company_ldap_group_mapping.py
  7. 43
      users_ldap_groups/models/res_company_ldap_operator.py
  8. 27
      users_ldap_groups/models/res_users.py
  9. 4
      users_ldap_groups/tests/__init__.py
  10. 65
      users_ldap_groups/tests/test_users_ldap_groups.py
  11. 128
      users_ldap_groups/users_ldap_groups.py
  12. 27
      users_ldap_groups/users_ldap_groups.xml
  13. 24
      users_ldap_groups/views/base_config_settings.xml

36
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 <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
Contributors
------------
* Therp BV <info@therp.nl>
* Holger Brunn <hbrunn@therp.nl>
* Giacomo Spettoli <giacomo.spettoli@gmail.com>
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.

24
users_ldap_groups/__init__.py

@ -1,22 +1,4 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# This module copyright (C) 2012 Therp BV (<http://therp.nl>).
#
# 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 users_ldap_groups
# Copyright 2012-2018 Therp BV <https://therp.nl>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from . import models

38
users_ldap_groups/__manifest__.py

@ -1,39 +1,19 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# This module copyright (C) 2012 Therp BV (<http://therp.nl>).
#
# 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/>.
#
##############################################################################
# Copyright 2012-2018 Therp BV <https://therp.nl>
# 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'],
},

7
users_ldap_groups/models/__init__.py

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright 2012-2018 Therp BV <https://therp.nl>
# 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

47
users_ldap_groups/models/res_company_ldap.py

@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
# Copyright 2012-2018 Therp BV <https://therp.nl>
# 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

33
users_ldap_groups/models/res_company_ldap_group_mapping.py

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# © 2012-2018 Therp BV <https://therp.nl>
# 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)

43
users_ldap_groups/models/res_company_ldap_operator.py

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
# © 2012-2018 Therp BV <https://therp.nl>
# 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)

27
users_ldap_groups/models/res_users.py

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# © 2018 Therp BV <https://therp.nl>
# 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

4
users_ldap_groups/tests/__init__.py

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Therp BV <https://therp.nl>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from . import test_users_ldap_groups

65
users_ldap_groups/tests/test_users_ldap_groups.py

@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Therp BV <https://therp.nl>
# 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, [])]})

128
users_ldap_groups/users_ldap_groups.py

@ -1,128 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# This module copyright (C) 2012 Therp BV (<http://therp.nl>).
#
# 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 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

27
users_ldap_groups/users_ldap_groups.xml

@ -1,27 +0,0 @@
<?xml version="1.0"?>
<openerp>
<data>
<record model="ir.ui.view" id="company_form_view">
<field name="name">res.company.form.inherit.users_ldap_groups</field>
<field name="model">res.company</field>
<field name="inherit_id" ref="auth_ldap.company_form_view"/>
<field name="arch" type="xml">
<xpath expr="//form[@string='LDAP Configuration']" position="inside">
<group string="Map User Groups" >
<field name="only_ldap_groups" />
<field name="group_mappings" colspan="4" nolabel="1">
<tree editable="top">
<field name="ldap_attribute" attrs="{'required': [('operator','not in',['query'])], 'readonly': [('operator','in',['query'])]}" />
<field name="operator" />
<field name="value" />
<field name="group" />
</tree>
</field>
</group>
</xpath>
</field>
</record>
</data>
</openerp>

24
users_ldap_groups/views/base_config_settings.xml

@ -0,0 +1,24 @@
<?xml version="1.0"?>
<odoo>
<record model="ir.ui.view" id="company_form_view">
<field name="model">base.config.settings</field>
<field name="inherit_id" ref="auth_ldap.view_general_configuration_form_inherit_auth_ldap"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='ldaps']/form/group" position="after">
<group string="Map User Groups" >
<field name="only_ldap_groups" />
<label for="group_mapping_ids" />
<field name="group_mapping_ids" nolabel="1">
<tree editable="top">
<field name="ldap_attribute" attrs="{'required': [('operator','not in',['query'])], 'readonly': [('operator','in',['query'])]}" />
<field name="operator" />
<field name="value" />
<field name="group_id" />
</tree>
</field>
</group>
</xpath>
</field>
</record>
</odoo>
Loading…
Cancel
Save