1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
|
"""
Purpose
-------
This fdw can be used to access directory servers via the LDAP protocol.
Tested with OpenLDAP.
It supports: simple bind, multiple scopes (subtree, base, etc)
.. api_compat: :read:
Dependencies
------------
If using Multicorn >= 1.1.0, you will need the `ldap3`_ library:
.. _ldap3: http://pythonhosted.org/python3-ldap/
For prior version, you will need the `ldap`_ library:
.. _ldap: http://www.python-ldap.org/
Required options
----------------
``uri`` (string)
The URI for the server, for example "ldap://localhost".
``path`` (string)
The base in which the search is performed, for example "dc=example,dc=com".
``objectclass`` (string)
The objectClass for which is searched, for example "inetOrgPerson".
``scope`` (string)
The scope: one, sub or base.
Optional options
----------------
``binddn`` (string)
The binddn for example 'cn=admin,dc=example,dc=com'.
``bindpwd`` (string)
The credentials for the binddn.
Usage Example
-------------
To search for a person
definition:
.. code-block:: sql
CREATE SERVER ldap_srv foreign data wrapper multicorn options (
wrapper 'multicorn.ldapfdw.LdapFdw'
);
CREATE FOREIGN TABLE ldapexample (
mail character varying,
cn character varying,
description character varying
) server ldap_srv options (
uri 'ldap://localhost',
path 'dc=lab,dc=example,dc=com',
scope 'sub',
binddn 'cn=Admin,dc=example,dc=com',
bindpwd 'admin',
objectClass '*'
);
select * from ldapexample;
.. code-block:: bash
mail | cn | description
-----------------------+----------------+--------------------
test@example.com | test |
admin@example.com | admin | LDAP administrator
someuser@example.com | Some Test User |
(3 rows)
"""
from . import ForeignDataWrapper
import ldap3
from multicorn.utils import log_to_postgres, ERROR
from multicorn.compat import unicode_
SPECIAL_CHARS = {
ord('*'): '\\2a',
ord('('): '\\28',
ord(')'): '\29',
ord('\\'): '\\5c',
ord('\x00'): '\\00',
ord('/'): '\\2f'
}
class LdapFdw(ForeignDataWrapper):
"""An Ldap Foreign Wrapper.
The following options are required:
uri -- the ldap URI to connect. (ex: 'ldap://localhost')
address -- the ldap host to connect. (obsolete)
path -- the ldap path (ex: ou=People,dc=example,dc=com)
objectClass -- the ldap object class (ex: 'inetOrgPerson')
scope -- the ldap scope (one, sub or base)
binddn -- the ldap bind DN (ex: 'cn=Admin,dc=example,dc=com')
bindpwd -- the ldap bind Password
"""
def __init__(self, fdw_options, fdw_columns):
super(LdapFdw, self).__init__(fdw_options, fdw_columns)
if "address" in fdw_options:
self.ldapuri = "ldap://" + fdw_options["address"]
else:
self.ldapuri = fdw_options["uri"]
self.ldap = ldap3.Connection(
ldap3.Server(self.ldapuri),
user=fdw_options.get("binddn", None),
password=fdw_options.get("bindpwd", None),
client_strategy=ldap3.RESTARTABLE if ldap3.version.__version__ > '2.0.0' else ldap3.STRATEGY_SYNC_RESTARTABLE)
self.path = fdw_options["path"]
self.scope = self.parse_scope(fdw_options.get("scope", None))
self.object_class = fdw_options["objectclass"]
self.field_list = fdw_columns
self.field_definitions = dict(
(name.lower(), field) for name, field in self.field_list.items())
self.array_columns = [
col.column_name for name, col in self.field_definitions.items()
if col.type_name.endswith('[]')]
def execute(self, quals, columns):
request = unicode_("(objectClass=%s)") % self.object_class
for qual in quals:
if isinstance(qual.operator, tuple):
operator = qual.operator[0]
else:
operator = qual.operator
if operator in ("=", "~~"):
if hasattr(qual.value, "translate"):
baseval = qual.value.translate(SPECIAL_CHARS)
val = (baseval.replace("%", "*")
if operator == "~~" else baseval)
else:
val = qual.value
request = unicode_("(&%s(%s=%s))") % (
request, qual.field_name, val)
self.ldap.search(
self.path, request, self.scope,
attributes=list(self.field_definitions))
for entry in self.ldap.response:
# Case insensitive lookup for the attributes
litem = dict()
for key, value in entry["attributes"].items():
if key.lower() in self.field_definitions:
pgcolname = self.field_definitions[key.lower()].column_name
if ldap3.version.__version__ > '2.0.0':
value = value
else:
if pgcolname in self.array_columns:
value = value
else:
value = value[0]
litem[pgcolname] = value
yield litem
def parse_scope(self, scope=None):
if scope in (None, "", "one"):
return ldap3.LEVEL if ldap3.version.__version__ > '2.0.0' else ldap3.SEARCH_SCOPE_SINGLE_LEVEL
elif scope == "sub":
return ldap3.SUBTREE if ldap3.version.__version__ > '2.0.0' else ldap3.SEARCH_SCOPE_WHOLE_SUBTREE
elif scope == "base":
return ldap3.BASE if ldap3.version.__version__ > '2.0.0' else ldap3.SEARCH_SCOPE_BASE_OBJECT
else:
log_to_postgres("Invalid scope specified: %s" % scope, ERROR)
|