src/pyams_ldap/plugin.py
changeset 0 94ee60dd51e1
child 2 68423cd701bb
equal deleted inserted replaced
-1:000000000000 0:94ee60dd51e1
       
     1 #
       
     2 # Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
       
     3 # All Rights Reserved.
       
     4 #
       
     5 # This software is subject to the provisions of the Zope Public License,
       
     6 # Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
       
     7 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
       
     8 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
       
     9 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
       
    10 # FOR A PARTICULAR PURPOSE.
       
    11 #
       
    12 
       
    13 __docformat__ = 'restructuredtext'
       
    14 
       
    15 
       
    16 # import standard library
       
    17 import ldap3
       
    18 import logging
       
    19 logger = logging.getLogger('pyams_ldap')
       
    20 
       
    21 import re
       
    22 
       
    23 # import interfaces
       
    24 from pyams_ldap.interfaces import ILDAPPlugin
       
    25 from zope.intid.interfaces import IIntIds
       
    26 
       
    27 # import packages
       
    28 from beaker.cache import cache_region
       
    29 from persistent import Persistent
       
    30 from pyams_ldap.query import LDAPQuery
       
    31 from pyams_security.principal import PrincipalInfo
       
    32 from pyams_utils.registry import query_utility
       
    33 from zope.container.contained import Contained
       
    34 from zope.interface import implementer
       
    35 from zope.schema.fieldproperty import FieldProperty
       
    36 
       
    37 
       
    38 managers = {}
       
    39 
       
    40 
       
    41 FORMAT_ATTRIBUTES = re.compile("\{(\w+)\[?\d*\]?\}")
       
    42 
       
    43 
       
    44 class ConnectionManager(object):
       
    45     """LDAP connections manager"""
       
    46 
       
    47     def __init__(self, plugin):
       
    48         self.server = ldap3.Server(plugin.host,
       
    49                                    port=plugin.port,
       
    50                                    use_ssl=plugin.use_ssl,
       
    51                                    tls=plugin.use_tls)
       
    52         self.bind_dn = plugin.bind_dn
       
    53         self.password = plugin.bind_password
       
    54         if plugin.use_pool:
       
    55             self.strategy = ldap3.STRATEGY_REUSABLE_THREADED
       
    56             self.pool_name = 'pyams_ldap:{prefix}'.format(prefix=plugin.prefix)
       
    57             self.pool_size = plugin.pool_size
       
    58             self.pool_lifetime = plugin.pool_lifetime
       
    59         else:
       
    60             self.strategy = ldap3.STRATEGY_ASYNC_THREADED
       
    61             self.pool_name = None
       
    62             self.pool_size = None
       
    63             self.pool_lifetime = None
       
    64 
       
    65     def get_connection(self, user=None, password=None):
       
    66         if user:
       
    67             conn = ldap3.Connection(self.server,
       
    68                                     user=user, password=password,
       
    69                                     client_strategy=ldap3.STRATEGY_SYNC,
       
    70                                     auto_bind=True, lazy=False, read_only=True)
       
    71         else:
       
    72             conn = ldap3.Connection(self.server,
       
    73                                     user=self.bind_dn, password=self.password,
       
    74                                     client_strategy=self.strategy,
       
    75                                     pool_name=self.pool_name,
       
    76                                     pool_size=self.pool_size,
       
    77                                     pool_lifetime=self.pool_lifetime,
       
    78                                     auto_bind=True, lazy=False, read_only=True)
       
    79         return conn
       
    80 
       
    81 @implementer(ILDAPPlugin)
       
    82 class LDAPPlugin(Persistent, Contained):
       
    83     """LDAP authentication plug-in"""
       
    84 
       
    85     prefix = FieldProperty(ILDAPPlugin['prefix'])
       
    86     title = FieldProperty(ILDAPPlugin['title'])
       
    87     enabled = FieldProperty(ILDAPPlugin['enabled'])
       
    88 
       
    89     _scheme = None
       
    90     _host = None
       
    91     _port = None
       
    92     _use_ssl = False
       
    93 
       
    94     _server_uri = FieldProperty(ILDAPPlugin['server_uri'])
       
    95     bind_dn = FieldProperty(ILDAPPlugin['bind_dn'])
       
    96     bind_password = FieldProperty(ILDAPPlugin['bind_password'])
       
    97     use_tls = FieldProperty(ILDAPPlugin['use_tls'])
       
    98     use_pool = FieldProperty(ILDAPPlugin['use_pool'])
       
    99     pool_size = FieldProperty(ILDAPPlugin['pool_size'])
       
   100     pool_lifetime = FieldProperty(ILDAPPlugin['pool_lifetime'])
       
   101     base_dn = FieldProperty(ILDAPPlugin['base_dn'])
       
   102     search_scope = FieldProperty(ILDAPPlugin['search_scope'])
       
   103     login_attribute = FieldProperty(ILDAPPlugin['login_attribute'])
       
   104     login_query = FieldProperty(ILDAPPlugin['login_query'])
       
   105     uid_attribute = FieldProperty(ILDAPPlugin['uid_attribute'])
       
   106     uid_query = FieldProperty(ILDAPPlugin['uid_query'])
       
   107     title_format = FieldProperty(ILDAPPlugin['title_format'])
       
   108     groups_base_dn = FieldProperty(ILDAPPlugin['groups_base_dn'])
       
   109     groups_search_scope = FieldProperty(ILDAPPlugin['groups_search_scope'])
       
   110     groups_query = FieldProperty(ILDAPPlugin['groups_query'])
       
   111     group_prefix = FieldProperty(ILDAPPlugin['group_prefix'])
       
   112     group_uid_attribute = FieldProperty(ILDAPPlugin['group_uid_attribute'])
       
   113     group_title_format = FieldProperty(ILDAPPlugin['group_title_format'])
       
   114 
       
   115     users_select_query = FieldProperty(ILDAPPlugin['users_select_query'])
       
   116     users_search_query = FieldProperty(ILDAPPlugin['users_search_query'])
       
   117     groups_select_query = FieldProperty(ILDAPPlugin['groups_select_query'])
       
   118     groups_search_query = FieldProperty(ILDAPPlugin['groups_search_query'])
       
   119 
       
   120     @property
       
   121     def server_uri(self):
       
   122         return self._server_uri
       
   123 
       
   124     @server_uri.setter
       
   125     def server_uri(self, value):
       
   126         self._server_uri = value
       
   127         try:
       
   128             scheme, host = value.split('://', 1)
       
   129         except ValueError:
       
   130             scheme = 'ldap'
       
   131             host = value
       
   132         self._use_ssl = scheme == 'ldaps'
       
   133         self._scheme = scheme
       
   134         try:
       
   135             host, port = host.split(':', 1)
       
   136             port = int(port)
       
   137         except ValueError:
       
   138             port = 636 if self._use_ssl else 389
       
   139         self._host = host
       
   140         self._port = port
       
   141 
       
   142     @property
       
   143     def scheme(self):
       
   144         return self._scheme
       
   145 
       
   146     @property
       
   147     def host(self):
       
   148         return self._host
       
   149 
       
   150     @property
       
   151     def port(self):
       
   152         return self._port
       
   153 
       
   154     @property
       
   155     def use_ssl(self):
       
   156         return self._use_ssl
       
   157 
       
   158     def _get_id(self):
       
   159         intids = query_utility(IIntIds)
       
   160         return intids.register(self)
       
   161 
       
   162     def clear(self):
       
   163         self_id = self._get_id()
       
   164         if self_id in managers:
       
   165             del managers[self_id]
       
   166 
       
   167     def get_connection(self, user=None, password=None):
       
   168         self_id = self._get_id()
       
   169         if self_id not in managers:
       
   170             managers[self_id] = ConnectionManager(self)
       
   171         return managers[self_id].get_connection(user, password)
       
   172 
       
   173     def authenticate(self, credentials, request):
       
   174         if not self.enabled:
       
   175             return None
       
   176         attrs = credentials.attributes
       
   177         login = attrs.get('login')
       
   178         password = attrs.get('password')
       
   179         conn = self.get_connection()
       
   180         search = LDAPQuery(self.base_dn, self.login_query, self.search_scope, (self.login_attribute,
       
   181                                                                                self.uid_attribute))
       
   182         result = search.execute(conn, login=login, password=password)
       
   183         if not result or len(result) > 1:
       
   184             return None
       
   185         result = result[0]
       
   186         login_dn = result[0]
       
   187         try:
       
   188             login_conn = self.get_connection(user=login_dn, password=password)
       
   189             login_conn.unbind()
       
   190         except ldap3.LDAPException:
       
   191             logger.debug("LDAP authentication exception with login %r", login, exc_info=True)
       
   192             return None
       
   193         else:
       
   194             if self.uid_attribute == 'dn':
       
   195                 return "{prefix}:{dn}".format(prefix=self.prefix,
       
   196                                               dn=login_dn)
       
   197             else:
       
   198                 attrs = result[1]
       
   199                 if self.login_attribute in attrs:
       
   200                     return "{prefix}:{attr}".format(prefix=self.prefix,
       
   201                                                     attr=attrs[self.uid_attribute][0])
       
   202 
       
   203     def _get_group(self, group_id):
       
   204         if not self.enabled:
       
   205             return None
       
   206 
       
   207     def get_principal(self, principal_id):
       
   208         if not self.enabled:
       
   209             return None
       
   210         if not principal_id.startswith(self.prefix + ':'):
       
   211             return None
       
   212         prefix, login = principal_id.split(':', 1)
       
   213         conn = self.get_connection()
       
   214         if login.startswith(self.group_prefix + ':'):
       
   215             group_prefix, group_id = login.split(':', 1)
       
   216             attributes = FORMAT_ATTRIBUTES.findall(self.group_title_format)
       
   217             if self.group_uid_attribute == 'dn':
       
   218                 search = LDAPQuery(group_id, '(objectClass=*)', ldap3.SEARCH_SCOPE_BASE_OBJECT, attributes)
       
   219             else:
       
   220                 search = LDAPQuery(self.base_dn, self.uid_query, self.search_scope, attributes)
       
   221             result = search.execute(conn, login=group_id)
       
   222             if not result or len(result) > 1:
       
   223                 return None
       
   224             group_dn, attrs = result[0]
       
   225             return PrincipalInfo(id='{prefix}:{group_prefix}:{group_id}'.format(prefix=self.prefix,
       
   226                                                                                 group_prefix=self.group_prefix,
       
   227                                                                                 group_id=group_id),
       
   228                                  title=self.group_title_format.format(**attrs),
       
   229                                  dn=group_dn)
       
   230         else:
       
   231             attributes = FORMAT_ATTRIBUTES.findall(self.title_format)
       
   232             if self.uid_attribute == 'dn':
       
   233                 search = LDAPQuery(login, '(objectClass=*)', ldap3.SEARCH_SCOPE_BASE_OBJECT, attributes)
       
   234             else:
       
   235                 search = LDAPQuery(self.base_dn, self.uid_query, self.search_scope, attributes)
       
   236             result = search.execute(conn, login=login)
       
   237             if not result or len(result) > 1:
       
   238                 return None
       
   239             user_dn, attrs = result[0]
       
   240             return PrincipalInfo(id='{prefix}:{login}'.format(prefix=self.prefix,
       
   241                                                               login=login),
       
   242                                  title=self.title_format.format(**attrs),
       
   243                                  dn=user_dn)
       
   244 
       
   245     def _get_groups(self, principal):
       
   246         if not self.groups_base_dn:
       
   247             raise StopIteration
       
   248         principal_dn = principal.attributes.get('dn')
       
   249         if principal_dn is None:
       
   250             raise StopIteration
       
   251         conn = self.get_connection()
       
   252         attributes = FORMAT_ATTRIBUTES.findall(self.group_title_format)
       
   253         search = LDAPQuery(self.groups_base_dn, self.groups_query, self.groups_search_scope, attributes)
       
   254         for group_dn, group_attrs in search.execute(conn, dn=principal_dn):
       
   255             if self.group_uid_attribute == 'dn':
       
   256                 yield '{prefix}:{group_prefix}:{dn}'.format(prefix=self.prefix,
       
   257                                                             group_prefix=self.group_prefix,
       
   258                                                             dn=group_dn)
       
   259             else:
       
   260                 yield '{prefix}:{group_prefix}:{attr}'.format(prefix=self.prefix,
       
   261                                                               group_prefix=self.group_prefix,
       
   262                                                               attr=group_attrs[self.group_uid_attribute])
       
   263 
       
   264     @cache_region('short')
       
   265     def get_all_principals(self, principal_id):
       
   266         if not self.enabled:
       
   267             return set()
       
   268         principal = self.get_principal(principal_id)
       
   269         if principal is not None:
       
   270             result = {principal_id}
       
   271             if self.groups_query:
       
   272                 result |= set(self._get_groups(principal))
       
   273             return result
       
   274         return set()
       
   275 
       
   276     def find_principals(self, query):
       
   277         if not self.enabled:
       
   278             raise StopIteration
       
   279         if not query:
       
   280             return None
       
   281         conn = self.get_connection()
       
   282         # users search
       
   283         attributes = FORMAT_ATTRIBUTES.findall(self.title_format) + [self.uid_attribute, ]
       
   284         search = LDAPQuery(self.base_dn, self.users_select_query, self.search_scope, attributes)
       
   285         for user_dn, user_attrs in search.execute(conn, query=query):
       
   286             if self.uid_attribute == 'dn':
       
   287                 yield PrincipalInfo(id='{prefix}:{dn}'.format(prefix=self.prefix,
       
   288                                                               dn=user_dn),
       
   289                                     title=self.title_format.format(**user_attrs),
       
   290                                     dn=user_dn)
       
   291             else:
       
   292                 yield PrincipalInfo(id='{prefix}:{attr}'.format(prefix=self.prefix,
       
   293                                                                 attr=user_attrs[self.uid_attribute][0]),
       
   294                                     title=self.title_format.format(**user_attrs),
       
   295                                     dn=user_dn)
       
   296         # groups search
       
   297         if self.groups_base_dn:
       
   298             attributes = FORMAT_ATTRIBUTES.findall(self.group_title_format) + [self.group_uid_attribute, ]
       
   299             search = LDAPQuery(self.groups_base_dn, self.groups_select_query, self.groups_search_scope, attributes)
       
   300             for group_dn, group_attrs in search.execute(conn, query=query):
       
   301                 if self.group_uid_attribute == 'dn':
       
   302                     yield PrincipalInfo(id='{prefix}:{group_prefix}:{dn}'.format(prefix=self.prefix,
       
   303                                                                                  group_prefix=self.group_prefix,
       
   304                                                                                  dn=group_dn),
       
   305                                         title=self.group_title_format.format(**group_attrs),
       
   306                                         dn=group_dn)
       
   307                 else:
       
   308                     yield PrincipalInfo(id='{prefix}:{group_prefix}:{attr}'.format(prefix=self.prefix,
       
   309                                                                                    group_prefix=self.group_prefix,
       
   310                                                                                    attr=group_attrs[
       
   311                                                                                        self.group_uid_attribute][0]),
       
   312                                         title=self.group_title_format.format(**group_attrs),
       
   313                                         dn=group_dn)
       
   314 
       
   315     def get_search_results(self, data):
       
   316         # LDAP search results are made of tuples containing DN and all
       
   317         # entries attributes
       
   318         query = data.get('query')
       
   319         if not query:
       
   320             return ()
       
   321         conn = self.get_connection()
       
   322         # users search
       
   323         search = LDAPQuery(self.base_dn, self.users_search_query, self.search_scope, ldap3.ALL_ATTRIBUTES)
       
   324         for user_dn, user_attrs in search.execute(conn, query=query):
       
   325             yield user_dn, user_attrs
       
   326         # groups search
       
   327         if self.groups_base_dn:
       
   328             search = LDAPQuery(self.groups_base_dn, self.groups_search_query, self.groups_search_scope, ldap3.ALL_ATTRIBUTES)
       
   329             for group_dn, group_attrs in search.execute(conn, query=query):
       
   330                 yield group_dn, group_attrs