From 2edc9c7a62f6bc96cc2a7b0759291e3962ac3953 Mon Sep 17 00:00:00 2001 From: omfgroflbbq <omfgroflbbq> Date: Wed, 15 Jun 2016 11:00:44 +0000 Subject: [PATCH 1/7] Introduced an option to the LDAP-fdw, which optionally makes the originating DN available to the RDMBS. --- python/multicorn/ldapfdw.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/python/multicorn/ldapfdw.py b/python/multicorn/ldapfdw.py index 46f81c1c3..7479ae2bf 100755 --- a/python/multicorn/ldapfdw.py +++ b/python/multicorn/ldapfdw.py @@ -34,6 +34,9 @@ ``scope`` (string) The scope: one, sub or base. +``origdn`` (string) +Whether the originating dn should be returned to the RDBMS: true or false. + Optional options ---------------- @@ -56,6 +59,7 @@ ); CREATE FOREIGN TABLE ldapexample ( + dn character varying, mail character varying, cn character varying, description character varying @@ -66,6 +70,7 @@ binddn 'cn=Admin,dc=example,dc=com', bindpwd 'admin', objectClass '*' + origdn 'true' ); select * from ldapexample; @@ -110,6 +115,7 @@ class LdapFdw(ForeignDataWrapper): 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 + origdn -- return originating dn to RDBMS """ @@ -124,6 +130,7 @@ def __init__(self, fdw_options, fdw_columns): user=fdw_options.get("binddn", None), password=fdw_options.get("bindpwd", None), client_strategy=ldap3.STRATEGY_SYNC_RESTARTABLE) + self.origdn = fdw_options["origdn"] self.path = fdw_options["path"] self.scope = self.parse_scope(fdw_options.get("scope", None)) self.object_class = fdw_options["objectclass"] @@ -156,6 +163,8 @@ def execute(self, quals, columns): for entry in self.ldap.response: # Case insensitive lookup for the attributes litem = dict() + if self.origdn == 'true': + litem["dn"] = entry["dn"] for key, value in entry["attributes"].items(): if key.lower() in self.field_definitions: pgcolname = self.field_definitions[key.lower()].column_name From 9416b246a86c581f1c14579a2a4f429faeaf6bd9 Mon Sep 17 00:00:00 2001 From: omfgroflbbq <omfgroflbbq> Date: Wed, 15 Jun 2016 11:29:00 +0000 Subject: [PATCH 2/7] The origdn option is actually optionally. --- python/multicorn/ldapfdw.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/python/multicorn/ldapfdw.py b/python/multicorn/ldapfdw.py index 7479ae2bf..6745263f2 100755 --- a/python/multicorn/ldapfdw.py +++ b/python/multicorn/ldapfdw.py @@ -34,9 +34,6 @@ ``scope`` (string) The scope: one, sub or base. -``origdn`` (string) -Whether the originating dn should be returned to the RDBMS: true or false. - Optional options ---------------- @@ -46,6 +43,10 @@ ``bindpwd`` (string) The credentials for the binddn. +``origdn`` (string) +Whether the originating dn should be returned to the RDBMS: true or false. +(default = false) + Usage Example ------------- @@ -130,7 +131,7 @@ def __init__(self, fdw_options, fdw_columns): user=fdw_options.get("binddn", None), password=fdw_options.get("bindpwd", None), client_strategy=ldap3.STRATEGY_SYNC_RESTARTABLE) - self.origdn = fdw_options["origdn"] + self.origdn = fdw_options.get("origdn", None) self.path = fdw_options["path"] self.scope = self.parse_scope(fdw_options.get("scope", None)) self.object_class = fdw_options["objectclass"] From cbe3b9d4fcdccccba9632e06eccfe73459b5d0f1 Mon Sep 17 00:00:00 2001 From: omfgroflbbq <omfgroflbbq> Date: Thu, 16 Jun 2016 14:31:18 +0000 Subject: [PATCH 3/7] Made it possible to filter on DN's as well. --- python/multicorn/ldapfdw.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/python/multicorn/ldapfdw.py b/python/multicorn/ldapfdw.py index 6745263f2..e0db9d4de 100755 --- a/python/multicorn/ldapfdw.py +++ b/python/multicorn/ldapfdw.py @@ -144,6 +144,7 @@ def __init__(self, fdw_options, fdw_columns): def execute(self, quals, columns): request = unicode_("(objectClass=%s)") % self.object_class + path = self.path for qual in quals: if isinstance(qual.operator, tuple): operator = qual.operator[0] @@ -156,10 +157,15 @@ def execute(self, quals, columns): if operator == "~~" else baseval) else: val = qual.value - request = unicode_("(&%s(%s=%s))") % ( - request, qual.field_name, val) + + if qual.field_name != "dn": + request = unicode_("(&%s(%s=%s))") % ( + request, qual.field_name, val) + else: + path = val + self.ldap.search( - self.path, request, self.scope, + path, request, self.scope, attributes=list(self.field_definitions)) for entry in self.ldap.response: # Case insensitive lookup for the attributes From 49efdb23723b1be5058b7029baa44e5221fe63d9 Mon Sep 17 00:00:00 2001 From: omfgroflbbq <omfgroflbbq> Date: Fri, 17 Jun 2016 09:03:44 +0000 Subject: [PATCH 4/7] Introduced some checks. --- python/multicorn/ldapfdw.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/python/multicorn/ldapfdw.py b/python/multicorn/ldapfdw.py index e0db9d4de..ff98cdb99 100755 --- a/python/multicorn/ldapfdw.py +++ b/python/multicorn/ldapfdw.py @@ -162,7 +162,12 @@ def execute(self, quals, columns): request = unicode_("(&%s(%s=%s))") % ( request, qual.field_name, val) else: - path = val + if path == self.path and val.endswith(self.path): + path = val + else: + log_to_postgres( + "Only one instance of a DN can be used as filter, " + "and it must end with the user defined base path.", ERROR) self.ldap.search( path, request, self.scope, From ee8e97ea5943dc7c4fa28e11aec2428526deae1f Mon Sep 17 00:00:00 2001 From: omfgroflbbq <omfgroflbbq> Date: Fri, 17 Jun 2016 14:01:25 +0000 Subject: [PATCH 5/7] - Introduced write-support. - Removed the origdn option, and made it's behaviour implicit. --- python/multicorn/ldapfdw.py | 42 +++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/python/multicorn/ldapfdw.py b/python/multicorn/ldapfdw.py index ff98cdb99..ef72ff335 100755 --- a/python/multicorn/ldapfdw.py +++ b/python/multicorn/ldapfdw.py @@ -43,10 +43,6 @@ ``bindpwd`` (string) The credentials for the binddn. -``origdn`` (string) -Whether the originating dn should be returned to the RDBMS: true or false. -(default = false) - Usage Example ------------- @@ -71,7 +67,6 @@ binddn 'cn=Admin,dc=example,dc=com', bindpwd 'admin', objectClass '*' - origdn 'true' ); select * from ldapexample; @@ -116,12 +111,12 @@ class LdapFdw(ForeignDataWrapper): 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 - origdn -- return originating dn to RDBMS """ def __init__(self, fdw_options, fdw_columns): super(LdapFdw, self).__init__(fdw_options, fdw_columns) + self._row_id_column = "dn" if "address" in fdw_options: self.ldapuri = "ldap://" + fdw_options["address"] else: @@ -131,7 +126,6 @@ def __init__(self, fdw_options, fdw_columns): user=fdw_options.get("binddn", None), password=fdw_options.get("bindpwd", None), client_strategy=ldap3.STRATEGY_SYNC_RESTARTABLE) - self.origdn = fdw_options.get("origdn", None) self.path = fdw_options["path"] self.scope = self.parse_scope(fdw_options.get("scope", None)) self.object_class = fdw_options["objectclass"] @@ -175,8 +169,7 @@ def execute(self, quals, columns): for entry in self.ldap.response: # Case insensitive lookup for the attributes litem = dict() - if self.origdn == 'true': - litem["dn"] = entry["dn"] + litem["dn"] = entry["dn"] for key, value in entry["attributes"].items(): if key.lower() in self.field_definitions: pgcolname = self.field_definitions[key.lower()].column_name @@ -196,3 +189,34 @@ def parse_scope(self, scope=None): return ldap3.SEARCH_SCOPE_BASE_OBJECT else: log_to_postgres("Invalid scope specified: %s" % scope, ERROR) + + @property + def rowid_column(self): + return self._row_id_column + + def insert(self, values): + self.ldap.add( + values.pop("dn"), attributes=values) + if self.ldap.result["result"]: + log_to_postgres( + "The ADD operation failed.\n " + self.ldap.result["message"], + ERROR) + + def update(self, dn, newvalues): + changes = {} + newvalues.pop("dn", None) + for k, v in newvalues.iteritems(): + changes[k] = [(ldap3.MODIFY_REPLACE, v)] + + self.ldap.modify(dn, changes) + if self.ldap.result["result"]: + log_to_postgres( + "The MODIFY operation failed.\n " + self.ldap.result["message"], + ERROR) + + def delete(self, dn): + self.ldap.delete(dn) + if self.ldap.result["result"]: + log_to_postgres( + "The DELETE operation failed.\n " + self.ldap.result["message"], + ERROR) From 9147e6de6080115cabe6ad088c3629e8e35d9467 Mon Sep 17 00:00:00 2001 From: omfgroflbbq <omfgroflbbq> Date: Mon, 20 Jun 2016 08:50:40 +0000 Subject: [PATCH 6/7] - Made it possible to specify/use multiple LDAP-servers. --- python/multicorn/ldapfdw.py | 87 ++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 40 deletions(-) diff --git a/python/multicorn/ldapfdw.py b/python/multicorn/ldapfdw.py index ef72ff335..f473851a7 100755 --- a/python/multicorn/ldapfdw.py +++ b/python/multicorn/ldapfdw.py @@ -23,7 +23,7 @@ ---------------- ``uri`` (string) -The URI for the server, for example "ldap://localhost". +The URI(s) for the server(s), for example "ldap://localhost,ldap://foobar". ``path`` (string) The base in which the search is performed, for example "dc=example,dc=com". @@ -117,15 +117,19 @@ class LdapFdw(ForeignDataWrapper): def __init__(self, fdw_options, fdw_columns): super(LdapFdw, self).__init__(fdw_options, fdw_columns) self._row_id_column = "dn" + + self.uris = [] if "address" in fdw_options: - self.ldapuri = "ldap://" + fdw_options["address"] + for addr in fdw_options["address"].split(","): + self.uris.append("ldap://" + addr) else: - self.ldapuri = fdw_options["uri"] - self.ldap = ldap3.Connection( - ldap3.Server(self.ldapuri), + for uri in fdw_options["uri"].split(","): + self.uris.append(uri) + self.connections = [ldap3.Connection( + ldap3.Server(uri), user=fdw_options.get("binddn", None), password=fdw_options.get("bindpwd", None), - client_strategy=ldap3.STRATEGY_SYNC_RESTARTABLE) + client_strategy=ldap3.STRATEGY_SYNC_RESTARTABLE) for uri in self.uris] self.path = fdw_options["path"] self.scope = self.parse_scope(fdw_options.get("scope", None)) self.object_class = fdw_options["objectclass"] @@ -163,22 +167,23 @@ def execute(self, quals, columns): "Only one instance of a DN can be used as filter, " "and it must end with the user defined base path.", ERROR) - self.ldap.search( - path, request, self.scope, - attributes=list(self.field_definitions)) - for entry in self.ldap.response: - # Case insensitive lookup for the attributes - litem = dict() - litem["dn"] = entry["dn"] - for key, value in entry["attributes"].items(): - if key.lower() in self.field_definitions: - pgcolname = self.field_definitions[key.lower()].column_name - if pgcolname in self.array_columns: - value = value - else: - value = value[0] - litem[pgcolname] = value - yield litem + for conn in self.connections: + conn.search( + path, request, self.scope, + attributes=list(self.field_definitions)) + for entry in conn.response: + # Case insensitive lookup for the attributes + litem = dict() + litem["dn"] = entry["dn"] + for key, value in entry["attributes"].items(): + if key.lower() in self.field_definitions: + pgcolname = self.field_definitions[key.lower()].column_name + 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"): @@ -195,28 +200,30 @@ def rowid_column(self): return self._row_id_column def insert(self, values): - self.ldap.add( - values.pop("dn"), attributes=values) - if self.ldap.result["result"]: - log_to_postgres( - "The ADD operation failed.\n " + self.ldap.result["message"], - ERROR) + for conn in self.connections: + conn.add(values.pop("dn"), attributes=values) + if conn.result["result"]: + log_to_postgres( + conn.server.host + ": The ADD operation failed.\n " + conn.result["message"], + ERROR) def update(self, dn, newvalues): - changes = {} + changes = {} newvalues.pop("dn", None) - for k, v in newvalues.iteritems(): + for k, v in newvalues.iteritems(): changes[k] = [(ldap3.MODIFY_REPLACE, v)] - self.ldap.modify(dn, changes) - if self.ldap.result["result"]: - log_to_postgres( - "The MODIFY operation failed.\n " + self.ldap.result["message"], - ERROR) + for conn in self.connections: + conn.modify(dn, changes) + if conn.result["result"]: + log_to_postgres( + conn.server.host + ": The MODIFY operation failed.\n " + conn.result["message"], + ERROR) def delete(self, dn): - self.ldap.delete(dn) - if self.ldap.result["result"]: - log_to_postgres( - "The DELETE operation failed.\n " + self.ldap.result["message"], - ERROR) + for conn in self.connections: + conn.delete(dn) + if conn.result["result"]: + log_to_postgres( + conn.server.host + ": The DELETE operation failed.\n " + conn.result["message"], + ERROR) From d8d3b68fd88bb355458129608147888de2ab0b0b Mon Sep 17 00:00:00 2001 From: omfgroflbbq <omfgroflbbq> Date: Tue, 21 Jun 2016 12:43:12 +0000 Subject: [PATCH 7/7] - follow up of 9147e6de6080115cabe6ad088c3629e8e35d9467. --- python/multicorn/ldapfdw.py | 83 +++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/python/multicorn/ldapfdw.py b/python/multicorn/ldapfdw.py index f473851a7..b47d263cb 100755 --- a/python/multicorn/ldapfdw.py +++ b/python/multicorn/ldapfdw.py @@ -23,7 +23,7 @@ ---------------- ``uri`` (string) -The URI(s) for the server(s), for example "ldap://localhost,ldap://foobar". +The URI(s) for the server(s), for example "ldap://localhost,ldap://fallback". ``path`` (string) The base in which the search is performed, for example "dc=example,dc=com". @@ -125,11 +125,16 @@ def __init__(self, fdw_options, fdw_columns): else: for uri in fdw_options["uri"].split(","): self.uris.append(uri) - self.connections = [ldap3.Connection( - ldap3.Server(uri), - user=fdw_options.get("binddn", None), - password=fdw_options.get("bindpwd", None), - client_strategy=ldap3.STRATEGY_SYNC_RESTARTABLE) for uri in self.uris] + + for uri in self.uris: + self.ldap = ldap3.Connection( + ldap3.Server(uri), + user=fdw_options.get("binddn", None), + password=fdw_options.get("bindpwd", None), + client_strategy=ldap3.STRATEGY_SYNC_RESTARTABLE) + if self.ldap != None: + break + self.path = fdw_options["path"] self.scope = self.parse_scope(fdw_options.get("scope", None)) self.object_class = fdw_options["objectclass"] @@ -167,23 +172,22 @@ def execute(self, quals, columns): "Only one instance of a DN can be used as filter, " "and it must end with the user defined base path.", ERROR) - for conn in self.connections: - conn.search( - path, request, self.scope, - attributes=list(self.field_definitions)) - for entry in conn.response: - # Case insensitive lookup for the attributes - litem = dict() - litem["dn"] = entry["dn"] - for key, value in entry["attributes"].items(): - if key.lower() in self.field_definitions: - pgcolname = self.field_definitions[key.lower()].column_name - if pgcolname in self.array_columns: - value = value - else: - value = value[0] - litem[pgcolname] = value - yield litem + self.ldap.search( + path, request, self.scope, + attributes=list(self.field_definitions)) + for entry in self.ldap.response: + # Case insensitive lookup for the attributes + litem = dict() + litem["dn"] = entry["dn"] + for key, value in entry["attributes"].items(): + if key.lower() in self.field_definitions: + pgcolname = self.field_definitions[key.lower()].column_name + 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"): @@ -200,12 +204,11 @@ def rowid_column(self): return self._row_id_column def insert(self, values): - for conn in self.connections: - conn.add(values.pop("dn"), attributes=values) - if conn.result["result"]: - log_to_postgres( - conn.server.host + ": The ADD operation failed.\n " + conn.result["message"], - ERROR) + self.ldap.add(values.pop("dn"), attributes=values) + if self.ldap.result["result"]: + log_to_postgres( + self.ldap.server.host + ": The ADD operation failed.\n " + self.ldap.result["message"], + ERROR) def update(self, dn, newvalues): changes = {} @@ -213,17 +216,15 @@ def update(self, dn, newvalues): for k, v in newvalues.iteritems(): changes[k] = [(ldap3.MODIFY_REPLACE, v)] - for conn in self.connections: - conn.modify(dn, changes) - if conn.result["result"]: - log_to_postgres( - conn.server.host + ": The MODIFY operation failed.\n " + conn.result["message"], - ERROR) + self.ldap.modify(dn, changes) + if self.ldap.result["result"]: + log_to_postgres( + self.ldap.server.host + ": The MODIFY operation failed.\n " + self.ldap.result["message"], + ERROR) def delete(self, dn): - for conn in self.connections: - conn.delete(dn) - if conn.result["result"]: - log_to_postgres( - conn.server.host + ": The DELETE operation failed.\n " + conn.result["message"], - ERROR) + self.ldap.delete(dn) + if self.ldap.result["result"]: + log_to_postgres( + self.ldap.server.host + ": The DELETE operation failed.\n " + self.ldap.result["message"], + ERROR)