--- a/src/pyams_ldap/interfaces/__init__.py Mon Jan 18 18:02:35 2016 +0100
+++ b/src/pyams_ldap/interfaces/__init__.py Thu Jun 02 15:38:05 2016 +0200
@@ -14,18 +14,24 @@
# import standard library
+import re
from ldap3 import SEARCH_SCOPE_BASE_OBJECT, SEARCH_SCOPE_SINGLE_LEVEL, SEARCH_SCOPE_WHOLE_SUBTREE
# import interfaces
from pyams_security.interfaces import IAuthenticationPlugin, IDirectorySearchPlugin
# import packages
-from zope.schema import TextLine, Bool, Int, Choice
+from zope.interface import Interface, Attribute
+from zope.schema import TextLine, Bool, Int, Choice, Dict, List
from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
from pyams_ldap import _
+#
+# Search scopes vocabulary
+#
+
SEARCH_SCOPES = {SEARCH_SCOPE_BASE_OBJECT: _("Base object"),
SEARCH_SCOPE_SINGLE_LEVEL: _("Single level"),
SEARCH_SCOPE_WHOLE_SUBTREE: _("Whole subtree")}
@@ -33,6 +39,48 @@
SEARCH_SCOPES_VOCABULARY = SimpleVocabulary([SimpleTerm(v, title=t) for v, t in SEARCH_SCOPES.items()])
+#
+# Group members query mode
+#
+
+GROUP_MEMBERS_QUERY_MODE = {'group': _("Use group attribute to get members list"),
+ 'member': _("Use member attribute to get groups list")}
+
+GROUP_MEMBERS_QUERY_MODE_VOCABULARY = SimpleVocabulary([SimpleTerm(v, title=t)
+ for v, t in GROUP_MEMBERS_QUERY_MODE.items()])
+
+
+#
+# Group mail mode
+#
+
+GROUP_MAIL_MODE = {'none': _("none (only use members own mail address"),
+ 'internal': _("Use group internal attribute"),
+ 'redirect': _("Use another group internal attribute")}
+
+GROUP_MAIL_MODE_VOCABULARY = SimpleVocabulary([SimpleTerm(v, title=t)
+ for v, t in GROUP_MAIL_MODE.items()])
+
+
+class ILDAPBaseInfo(Interface):
+ """LDAP base entry interface"""
+
+ dn = Attribute("LDAP DN")
+
+ attributes = Attribute("Entry attributes")
+
+
+class ILDAPUserInfo(ILDAPBaseInfo):
+ """LDAP user entry interface"""
+
+
+class ILDAPGroupInfo(ILDAPBaseInfo):
+ """LDAP group entry interface"""
+
+ def get_members(self):
+ """Get all group members"""
+
+
class ILDAPPlugin(IAuthenticationPlugin, IDirectorySearchPlugin):
"""LDAP authentication plug-in interface"""
@@ -101,6 +149,15 @@
required=True,
default='{givenName[0]} {sn[0]}')
+ mail_attribute = TextLine(title=_("Mail attribute"),
+ description=_("LDAP attribute storing mail address"),
+ required=True,
+ default='mail')
+
+ user_extra_attributes = TextLine(title=_("Extra attributes"),
+ description=_("Comma separated list of additional attributes"),
+ required=False)
+
groups_base_dn = TextLine(title=_("Groups base DN"),
description=_("Base DN used to search LDAP groups; keep empty to "
"disable groups usage"),
@@ -111,13 +168,6 @@
default=SEARCH_SCOPE_WHOLE_SUBTREE,
required=False)
- groups_query = TextLine(title=_("Groups query"),
- description=_("Query template used to get principal groups "
- "(based on DN and UID attributes called 'dn' "
- "and 'login')"),
- required=False,
- default="(&(objectClass=groupOfUniqueNames)(uniqueMember={dn}))")
-
group_prefix = TextLine(title=_("Group prefix"),
description=_("Prefix used to identify groups"),
required=False,
@@ -133,6 +183,52 @@
required=True,
default='{cn[0]}')
+ group_members_query_mode = Choice(title=_("Members query mode"),
+ description=_("Define how groups members are defined"),
+ vocabulary=GROUP_MEMBERS_QUERY_MODE_VOCABULARY,
+ default='group')
+
+ groups_query = TextLine(title=_("Groups query"),
+ description=_("When members are store inside a group attribute, "
+ "this query template is used to get principal groups "
+ "(based on DN and UID attributes called 'dn' "
+ "and 'login')"),
+ required=False,
+ default="(&(objectClass=groupOfUniqueNames)(uniqueMember={dn}))")
+
+ group_members_attribute = TextLine(title=_("Group members attribute"),
+ description=_("When groups members are stored inside a group attribute, "
+ "this is the attribute name"),
+ required=False,
+ default='uniqueMember')
+
+ user_groups_attribute = TextLine(title=_("User groups attribute"),
+ description=_("When user groups are stored inside a user attribute, "
+ "this is the attribute name"),
+ required=False,
+ default='memberOf')
+
+ group_mail_mode = Choice(title=_("Group mail mode"),
+ description=_("Define how an email can be sent to group members"),
+ vocabulary=GROUP_MAIL_MODE_VOCABULARY,
+ required=True,
+ default='none')
+
+ group_replace_expression = TextLine(title=_("DN replace expression"),
+ description=_("In 'redirect' mail mode, specify source and target DN parts, "
+ "separated by a pipe"),
+ constraint=re.compile("[a-zA-Z0-9-=,]+|[a-zA-Z0-9-=,]+").match,
+ default='ou=access|ou=lists')
+
+ group_mail_attribute = TextLine(title=_("Mail attribute"),
+ description=_("In 'internal' mail mode, specify name of group mail attribute"),
+ required=True,
+ default='mail')
+
+ group_extra_attributes = TextLine(title=_("Extra attributes"),
+ description=_("Comma separated list of additional attributes"),
+ required=False)
+
users_select_query = TextLine(title=_("Users select query"),
description=_("Query template used to select users"),
required=True,
--- a/src/pyams_ldap/plugin.py Mon Jan 18 18:02:35 2016 +0100
+++ b/src/pyams_ldap/plugin.py Thu Jun 02 15:38:05 2016 +0200
@@ -21,7 +21,8 @@
import re
# import interfaces
-from pyams_ldap.interfaces import ILDAPPlugin
+from pyams_ldap.interfaces import ILDAPPlugin, ILDAPUserInfo, ILDAPGroupInfo
+from pyams_mail.interfaces import IPrincipalMailInfo
from zope.intid.interfaces import IIntIds
# import packages
@@ -29,6 +30,7 @@
from persistent import Persistent
from pyams_ldap.query import LDAPQuery
from pyams_security.principal import PrincipalInfo
+from pyams_utils.adapter import adapter_config, ContextAdapter
from pyams_utils.registry import query_utility
from zope.container.contained import Contained
from zope.interface import implementer
@@ -78,6 +80,87 @@
auto_bind=True, lazy=False, read_only=True)
return conn
+
+@implementer(ILDAPUserInfo)
+class LDAPUserInfo(object):
+ """LDAP user info"""
+
+ def __init__(self, dn, attributes, plugin=None):
+ self.dn = dn
+ self.attributes = attributes
+ self.plugin = plugin
+
+
+@adapter_config(context=ILDAPUserInfo, provides=IPrincipalMailInfo)
+class LDAPUserMailInfoAdapter(ContextAdapter):
+ """LDAP user mail adapter"""
+
+ def get_addresses(self):
+ """Get mail address of given user"""
+
+ user = self.context
+ plugin = user.plugin
+
+ mail = user.attributes.get(plugin.mail_attribute)
+ if mail:
+ return {(plugin.title_format.format(**user.attributes), mail[0])}
+ else:
+ return set()
+
+
+@implementer(ILDAPGroupInfo)
+class LDAPGroupInfo(object):
+ """LDAP group info"""
+
+ def __init__(self, dn, attributes, plugin=None):
+ self.dn = dn
+ self.attributes = attributes
+ self.plugin = plugin
+
+ def get_members(self, info=True):
+ return self.plugin.get_members(self, info=info)
+
+
+@adapter_config(context=ILDAPGroupInfo, provides=IPrincipalMailInfo)
+class LDAPGroupMailInfoAdapter(ContextAdapter):
+ """LDAP group mail adapter"""
+
+ def get_addresses(self):
+ """Get mail address of given group"""
+
+ group = self.context
+ plugin = group.plugin
+
+ if plugin.group_mail_mode == 'none':
+ # use members address
+ for member in plugin.get_members(group, info=False):
+ mail_info = IPrincipalMailInfo(member, None)
+ if mail_info is not None:
+ for address in mail_info.get_addresses():
+ yield address
+
+ elif plugin.group_mail_mode == 'internal':
+ # use group internal attribute
+ mail = group.attributes.get(plugin.group_mail_attribute)
+ if mail:
+ yield plugin.group_title_format(**group.attributes), mail[0]
+
+ else:
+ # redirect: use internal attribute of another group
+ source, target = plugin.group_replace_expression.split('|')
+ target_dn = group.dn.replace(source, target)
+ conn = plugin.get_connection()
+ attributes = FORMAT_ATTRIBUTES.findall(plugin.group_title_format) + [plugin.group_mail_attribute]
+ search = LDAPQuery(target_dn, '(objectClass=*)', ldap3.SEARCH_SCOPE_BASE_OBJECT, attributes)
+ result = search.execute(conn)
+ if not result or len(result) > 1:
+ raise StopIteration
+ target_dn, attrs = result[0]
+ mail = attrs.get(plugin.group_mail_attribute)
+ if mail:
+ yield plugin.group_title_format(**attrs), mail[0]
+
+
@implementer(ILDAPPlugin)
class LDAPPlugin(Persistent, Contained):
"""LDAP authentication plug-in"""
@@ -100,17 +183,28 @@
pool_lifetime = FieldProperty(ILDAPPlugin['pool_lifetime'])
base_dn = FieldProperty(ILDAPPlugin['base_dn'])
search_scope = FieldProperty(ILDAPPlugin['search_scope'])
+
login_attribute = FieldProperty(ILDAPPlugin['login_attribute'])
login_query = FieldProperty(ILDAPPlugin['login_query'])
uid_attribute = FieldProperty(ILDAPPlugin['uid_attribute'])
uid_query = FieldProperty(ILDAPPlugin['uid_query'])
title_format = FieldProperty(ILDAPPlugin['title_format'])
+ mail_attribute = FieldProperty(ILDAPPlugin['mail_attribute'])
+ user_extra_attributes = FieldProperty(ILDAPPlugin['user_extra_attributes'])
+
groups_base_dn = FieldProperty(ILDAPPlugin['groups_base_dn'])
groups_search_scope = FieldProperty(ILDAPPlugin['groups_search_scope'])
- groups_query = FieldProperty(ILDAPPlugin['groups_query'])
group_prefix = FieldProperty(ILDAPPlugin['group_prefix'])
group_uid_attribute = FieldProperty(ILDAPPlugin['group_uid_attribute'])
group_title_format = FieldProperty(ILDAPPlugin['group_title_format'])
+ group_members_query_mode = FieldProperty(ILDAPPlugin['group_members_query_mode'])
+ groups_query = FieldProperty(ILDAPPlugin['groups_query'])
+ group_members_attribute = FieldProperty(ILDAPPlugin['group_members_attribute'])
+ user_groups_attribute = FieldProperty(ILDAPPlugin['user_groups_attribute'])
+ group_mail_mode = FieldProperty(ILDAPPlugin['group_mail_mode'])
+ group_replace_expression = FieldProperty(ILDAPPlugin['group_replace_expression'])
+ group_mail_attribute = FieldProperty(ILDAPPlugin['group_mail_attribute'])
+ group_extra_attributes = FieldProperty(ILDAPPlugin['group_extra_attributes'])
users_select_query = FieldProperty(ILDAPPlugin['users_select_query'])
users_search_query = FieldProperty(ILDAPPlugin['users_search_query'])
@@ -213,7 +307,9 @@
conn = self.get_connection()
if login.startswith(self.group_prefix + ':'):
group_prefix, group_id = login.split(':', 1)
- attributes = FORMAT_ATTRIBUTES.findall(self.group_title_format)
+ attributes = FORMAT_ATTRIBUTES.findall(self.group_title_format) + [self.group_mail_attribute]
+ if self.group_extra_attributes:
+ attributes += self.group_extra_attributes.split(',')
if self.group_uid_attribute == 'dn':
search = LDAPQuery(group_id, '(objectClass=*)', ldap3.SEARCH_SCOPE_BASE_OBJECT, attributes)
else:
@@ -222,13 +318,21 @@
if not result or len(result) > 1:
return None
group_dn, attrs = result[0]
- return PrincipalInfo(id='{prefix}:{group_prefix}:{group_id}'.format(prefix=self.prefix,
- group_prefix=self.group_prefix,
- group_id=group_id),
- title=self.group_title_format.format(**attrs),
- dn=group_dn)
+ if info:
+ return PrincipalInfo(id='{prefix}:{group_prefix}:{group_id}'.format(prefix=self.prefix,
+ group_prefix=self.group_prefix,
+ group_id=group_id),
+ title=self.group_title_format.format(**attrs),
+ dn=group_dn)
+ else:
+ attrs.update({'principal_id': '{prefix}:{group_prefix}:{group_id}'.format(prefix=self.prefix,
+ group_prefix=self.group_prefix,
+ group_id=group_id)})
+ return LDAPGroupInfo(group_dn, attrs, self)
else:
- attributes = FORMAT_ATTRIBUTES.findall(self.title_format)
+ attributes = FORMAT_ATTRIBUTES.findall(self.title_format) + [self.mail_attribute]
+ if self.user_extra_attributes:
+ attributes += self.user_extra_attributes.split(',')
if self.uid_attribute == 'dn':
search = LDAPQuery(login, '(objectClass=*)', ldap3.SEARCH_SCOPE_BASE_OBJECT, attributes)
else:
@@ -237,32 +341,61 @@
if not result or len(result) > 1:
return None
user_dn, attrs = result[0]
- return PrincipalInfo(id='{prefix}:{login}'.format(prefix=self.prefix,
- login=login),
- title=self.title_format.format(**attrs),
- dn=user_dn)
+ if info:
+ return PrincipalInfo(id='{prefix}:{login}'.format(prefix=self.prefix,
+ login=login),
+ title=self.title_format.format(**attrs),
+ dn=user_dn)
+ else:
+ attrs.update({'principal_id': '{prefix}:{login}'.format(prefix=self.prefix, login=login)})
+ return LDAPUserInfo(user_dn, attrs, self)
def _get_groups(self, principal):
- if not self.groups_base_dn:
- raise StopIteration
+ """Get principal groups"""
principal_dn = principal.attributes.get('dn')
if principal_dn is None:
raise StopIteration
- conn = self.get_connection()
- attributes = FORMAT_ATTRIBUTES.findall(self.group_title_format)
- search = LDAPQuery(self.groups_base_dn, self.groups_query, self.groups_search_scope, attributes)
- for group_dn, group_attrs in search.execute(conn, dn=principal_dn):
- if self.group_uid_attribute == 'dn':
- yield '{prefix}:{group_prefix}:{dn}'.format(prefix=self.prefix,
- group_prefix=self.group_prefix,
- dn=group_dn)
- else:
- yield '{prefix}:{group_prefix}:{attr}'.format(prefix=self.prefix,
- group_prefix=self.group_prefix,
- attr=group_attrs[self.group_uid_attribute])
+ if self.group_members_query_mode == 'group':
+ # group members are defined inside group
+ if not self.groups_base_dn:
+ raise StopIteration
+ conn = self.get_connection()
+ attributes = FORMAT_ATTRIBUTES.findall(self.group_title_format)
+ search = LDAPQuery(self.groups_base_dn, self.groups_query, self.groups_search_scope, attributes)
+ for group_dn, group_attrs in search.execute(conn, dn=principal_dn):
+ if self.group_uid_attribute == 'dn':
+ yield '{prefix}:{group_prefix}:{dn}'.format(prefix=self.prefix,
+ group_prefix=self.group_prefix,
+ dn=group_dn)
+ else:
+ yield '{prefix}:{group_prefix}:{attr}'.format(prefix=self.prefix,
+ group_prefix=self.group_prefix,
+ attr=group_attrs[self.group_uid_attribute])
+ else:
+ # a member defines it's groups
+ conn = self.get_connection()
+ attributes = [self.user_groups_attribute]
+ user_search = LDAPQuery(principal_dn, '(objectClass=*)', ldap3.SEARCH_SCOPE_BASE_OBJECT, attributes)
+ for user_dn, user_attrs in user_search.execute(conn):
+ if self.group_uid_attribute == 'dn':
+ for group_dn in user_attrs.get(self.user_groups_attribute, ()):
+ yield '{prefix}:{group_prefix}:{dn}'.format(prefix=self.prefix,
+ group_prefix=self.group_prefix,
+ dn=group_dn)
+ else:
+ attributes = [self.group_uid_attribute]
+ for group_dn in user_attrs.get(self.user_groups_attribute, ()):
+ group_search = LDAPQuery(group_dn, '(objectClass=*)', ldap3.SEARCH_SCOPE_BASE_OBJECT,
+ attributes)
+ for group_search_dn, group_search_attrs in group_search.execute(conn):
+ yield '{prefix}:{group_prefix}:{attr}'.format(prefix=self.prefix,
+ group_prefix=self.group_prefix,
+ attr=group_search_attrs[
+ self.group_uid_attribute])
@cache_region('short')
def get_all_principals(self, principal_id):
+ """Get all principals (including groups) for given principal ID"""
if not self.enabled:
return set()
principal = self.get_principal(principal_id)
@@ -273,6 +406,47 @@
return result
return set()
+ def get_members(self, group, info=True):
+ """Get all members of given LDAP group as LDAP users"""
+ if not self.enabled:
+ return set()
+ conn = self.get_connection()
+ if self.group_members_query_mode == 'group':
+ # group members are defined into group attribute
+ attributes = [self.group_members_attribute]
+ user_attributes = FORMAT_ATTRIBUTES.findall(self.title_format) + [self.mail_attribute]
+ search = LDAPQuery(group.dn, '(objectClass=*)', ldap3.SEARCH_SCOPE_BASE_OBJECT, attributes)
+ for group_dn, attrs in search.execute(conn):
+ for user_dn in attrs.get(self.group_members_attribute):
+ user_search = LDAPQuery(user_dn, '(objectClass=*)', ldap3.SEARCH_SCOPE_BASE_OBJECT, user_attributes)
+ for user_search_dn, user_search_attrs in user_search.execute(conn):
+ if info:
+ yield PrincipalInfo(id='{prefix}:{dn}'.format(prefix=self.prefix,
+ dn=user_search_dn),
+ title=self.title_format.format(**user_search_attrs),
+ dn=user_search_dn)
+ else:
+ yield LDAPUserInfo(dn=user_search_dn, attributes=user_search_attrs, plugin=self)
+ else:
+ # member groups are defined into member attribute
+ attributes = FORMAT_ATTRIBUTES.findall(self.title_format) + [self.uid_attribute, self.mail_attribute]
+ search = LDAPQuery(self.base_dn, '({attribute}={{group_dn}})'.format(attribute=self.user_groups_attribute),
+ self.search_scope, attributes)
+ for user_dn, user_attrs in search.execute(conn, group_dn=group.dn):
+ if info:
+ if self.uid_attribute == 'dn':
+ yield PrincipalInfo(id='{prefix}:{dn}'.format(prefix=self.prefix,
+ dn=user_dn),
+ title=self.title_format.format(**user_attrs),
+ dn=user_dn)
+ else:
+ yield PrincipalInfo(id='{prefix}:{attr}'.format(prefix=self.prefix,
+ attr=user_attrs[self.uid_attribute][0]),
+ title=self.title_format.format(**user_attrs),
+ dn=user_dn)
+ else:
+ yield LDAPUserInfo(dn=user_dn, attributes=user_attrs, plugin=self)
+
def find_principals(self, query):
if not self.enabled:
raise StopIteration
--- a/src/pyams_ldap/zmi/plugin.py Mon Jan 18 18:02:35 2016 +0100
+++ b/src/pyams_ldap/zmi/plugin.py Thu Jun 02 15:38:05 2016 +0200
@@ -137,7 +137,8 @@
tab_label = _("Users schema")
legend = None
fields = field.Fields(ILDAPPlugin).select('base_dn', 'search_scope', 'login_attribute', 'login_query',
- 'uid_attribute', 'uid_query', 'title_format')
+ 'uid_attribute', 'uid_query', 'title_format',
+ 'mail_attribute', 'user_extra_attributes')
weight = 2
@@ -148,8 +149,12 @@
id = 'ldap_groups_form'
tab_label = _("Groups schema")
legend = None
- fields = field.Fields(ILDAPPlugin).select('groups_base_dn', 'groups_search_scope', 'groups_query',
- 'group_prefix', 'group_uid_attribute', 'group_title_format')
+ fields = field.Fields(ILDAPPlugin).select('groups_base_dn', 'groups_search_scope', 'group_prefix',
+ 'group_uid_attribute', 'group_title_format',
+ 'group_members_query_mode', 'groups_query',
+ 'group_members_attribute', 'user_groups_attribute', 'group_mail_mode',
+ 'group_replace_expression', 'group_mail_attribute',
+ 'group_extra_attributes')
weight = 3
@@ -232,7 +237,8 @@
tab_label = _("Users schema")
legend = None
fields = field.Fields(ILDAPPlugin).select('base_dn', 'search_scope', 'login_attribute', 'login_query',
- 'uid_attribute', 'uid_query', 'title_format')
+ 'uid_attribute', 'uid_query', 'title_format',
+ 'mail_attribute', 'user_extra_attributes')
weight = 2
@@ -243,8 +249,12 @@
id = 'ldap_groups_form'
tab_label = _("Groups schema")
legend = None
- fields = field.Fields(ILDAPPlugin).select('groups_base_dn', 'groups_search_scope', 'groups_query',
- 'group_prefix', 'group_uid_attribute', 'group_title_format')
+ fields = field.Fields(ILDAPPlugin).select('groups_base_dn', 'groups_search_scope', 'group_prefix',
+ 'group_uid_attribute', 'group_title_format',
+ 'group_members_query_mode', 'groups_query',
+ 'group_members_attribute', 'user_groups_attribute', 'group_mail_mode',
+ 'group_replace_expression', 'group_mail_attribute',
+ 'group_extra_attributes')
weight = 3