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)