]> granicus.if.org Git - postgresql/commitdiff
Allow custom search filters to be configured for LDAP auth
authorPeter Eisentraut <peter_e@gmx.net>
Tue, 12 Sep 2017 13:46:14 +0000 (09:46 -0400)
committerPeter Eisentraut <peter_e@gmx.net>
Tue, 12 Sep 2017 13:49:04 +0000 (09:49 -0400)
Before, only filters of the form "(<ldapsearchattribute>=<user>)"
could be used to search an LDAP server.  Introduce ldapsearchfilter
so that more general filters can be configured using patterns, like
"(|(uid=$username)(mail=$username))" and "(&(uid=$username)
(objectClass=posixAccount))".  Also allow search filters to be included
in an LDAP URL.

Author: Thomas Munro
Reviewed-By: Peter Eisentraut, Mark Cave-Ayland, Magnus Hagander
Discussion: https://postgr.es/m/CAEepm=0XTkYvMci0WRubZcf_1am8=gP=7oJErpsUfRYcKF2gwg@mail.gmail.com

doc/src/sgml/client-auth.sgml
src/backend/libpq/auth.c
src/backend/libpq/hba.c
src/include/libpq/hba.h

index 1b568683a4733f80314015e434a7a282b4bb98f3..405bf268327b819b8434f54c29bacfc70506016d 100644 (file)
@@ -1507,6 +1507,17 @@ omicron         bryanh                  guest1
         </para>
        </listitem>
       </varlistentry>
+      <varlistentry>
+       <term><literal>ldapsearchfilter</literal></term>
+       <listitem>
+        <para>
+         The search filter to use when doing search+bind authentication.
+         Occurrences of <literal>$username</literal> will be replaced with the
+         user name.  This allows for more flexible search filters than
+         <literal>ldapsearchattribute</literal>.
+        </para>
+       </listitem>
+      </varlistentry>
       <varlistentry>
        <term><literal>ldapurl</literal></term>
        <listitem>
@@ -1514,13 +1525,16 @@ omicron         bryanh                  guest1
          An RFC 4516 LDAP URL.  This is an alternative way to write some of the
          other LDAP options in a more compact and standard form.  The format is
 <synopsis>
-ldap://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replaceable>basedn</replaceable>[?[<replaceable>attribute</replaceable>][?[<replaceable>scope</replaceable>]]]
+ldap://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replaceable>basedn</replaceable>[?[<replaceable>attribute</replaceable>][?[<replaceable>scope</replaceable>][?[<replaceable>filter</replaceable>]]]]
 </synopsis>
          <replaceable>scope</replaceable> must be one
          of <literal>base</literal>, <literal>one</literal>, <literal>sub</literal>,
-         typically the latter.  Only one attribute is used, and some other
-         components of standard LDAP URLs such as filters and extensions are
-         not supported.
+         typically the last.  <replaceable>attribute</replaceable> can
+         nominate a single attribute, in which case it is used as a value for
+         <literal>ldapsearchattribute</literal>.  If
+         <replaceable>attribute</replaceable> is empty then
+         <replaceable>filter</replaceable> can be used as a value for
+         <literal>ldapsearchfilter</literal>.
         </para>
 
         <para>
@@ -1549,6 +1563,17 @@ ldap://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replac
     for search+bind.
    </para>
 
+   <para>
+    When using search+bind mode, the search can be performed using a single
+    attribute specified with <literal>ldapsearchattribute</literal>, or using
+    a custom search filter specified with
+    <literal>ldapsearchfilter</literal>.
+    Specifying <literal>ldapsearchattribute=foo</literal> is equivalent to
+    specifying <literal>ldapsearchfilter="(foo=$username)"</literal>.  If neither
+    option is specified the default is
+    <literal>ldapsearchattribute=uid</literal>.
+   </para>
+
    <para>
     Here is an example for a simple-bind LDAP configuration:
 <programlisting>
@@ -1584,6 +1609,16 @@ host ... ldap ldapurl="ldap://ldap.example.net/dc=example,dc=net?uid?sub"
     same URL format, so it will be easier to share the configuration.
    </para>
 
+   <para>
+    Here is an example for a search+bind configuration that uses
+    <literal>ldapsearchfilter</literal> instead of
+    <literal>ldapsearchattribute</literal> to allow authentication by
+    user ID or email address:
+<programlisting>
+host ... ldap ldapserver=ldap.example.net ldapbasedn="dc=example, dc=net" ldapsearchfilter="(|(uid=$username)(mail=$username))"
+</programlisting>
+   </para>
+
    <tip>
     <para>
      Since LDAP often uses commas and spaces to separate the different
index cb30fc7b7146f055b1115ba318f0fbb8d2a09180..62ff624dbd70eadd4522436a17ebed4434aae25d 100644 (file)
@@ -2394,6 +2394,34 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
        return STATUS_OK;
 }
 
+/* Placeholders recognized by FormatSearchFilter.  For now just one. */
+#define LPH_USERNAME "$username"
+#define LPH_USERNAME_LEN (sizeof(LPH_USERNAME) - 1)
+
+/*
+ * Return a newly allocated C string copied from "pattern" with all
+ * occurrences of the placeholder "$username" replaced with "user_name".
+ */
+static char *
+FormatSearchFilter(const char *pattern, const char *user_name)
+{
+       StringInfoData output;
+
+       initStringInfo(&output);
+       while (*pattern != '\0')
+       {
+               if (strncmp(pattern, LPH_USERNAME, LPH_USERNAME_LEN) == 0)
+               {
+                       appendStringInfoString(&output, user_name);
+                       pattern += LPH_USERNAME_LEN;
+               }
+               else
+                       appendStringInfoChar(&output, *pattern++);
+       }
+
+       return output.data;
+}
+
 /*
  * Perform LDAP authentication
  */
@@ -2437,7 +2465,7 @@ CheckLDAPAuth(Port *port)
                char       *filter;
                LDAPMessage *search_message;
                LDAPMessage *entry;
-               char       *attributes[2];
+               char       *attributes[] = { LDAP_NO_ATTRS, NULL };
                char       *dn;
                char       *c;
                int                     count;
@@ -2479,13 +2507,13 @@ CheckLDAPAuth(Port *port)
                        return STATUS_ERROR;
                }
 
-               /* Fetch just one attribute, else *all* attributes are returned */
-               attributes[0] = port->hba->ldapsearchattribute ? port->hba->ldapsearchattribute : "uid";
-               attributes[1] = NULL;
-
-               filter = psprintf("(%s=%s)",
-                                                 attributes[0],
-                                                 port->user_name);
+               /* Build a custom filter or a single attribute filter? */
+               if (port->hba->ldapsearchfilter)
+                       filter = FormatSearchFilter(port->hba->ldapsearchfilter, port->user_name);
+               else if (port->hba->ldapsearchattribute)
+                       filter = psprintf("(%s=%s)", port->hba->ldapsearchattribute, port->user_name);
+               else
+                       filter = psprintf("(uid=%s)", port->user_name);
 
                r = ldap_search_s(ldap,
                                                  port->hba->ldapbasedn,
index ba011b6d61bf97460fb465ac6daef17ca8a6588a..b2c487a8e8609bc5457cd15f50c938969aebf3a1 100644 (file)
@@ -1505,22 +1505,24 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
                /*
                 * LDAP can operate in two modes: either with a direct bind, using
                 * ldapprefix and ldapsuffix, or using a search+bind, using
-                * ldapbasedn, ldapbinddn, ldapbindpasswd and ldapsearchattribute.
-                * Disallow mixing these parameters.
+                * ldapbasedn, ldapbinddn, ldapbindpasswd and one of
+                * ldapsearchattribute or ldapsearchfilter.  Disallow mixing these
+                * parameters.
                 */
                if (parsedline->ldapprefix || parsedline->ldapsuffix)
                {
                        if (parsedline->ldapbasedn ||
                                parsedline->ldapbinddn ||
                                parsedline->ldapbindpasswd ||
-                               parsedline->ldapsearchattribute)
+                               parsedline->ldapsearchattribute ||
+                               parsedline->ldapsearchfilter)
                        {
                                ereport(elevel,
                                                (errcode(ERRCODE_CONFIG_FILE_ERROR),
-                                                errmsg("cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, or ldapurl together with ldapprefix"),
+                                                errmsg("cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter or ldapurl together with ldapprefix"),
                                                 errcontext("line %d of configuration file \"%s\"",
                                                                        line_num, HbaFileName)));
-                               *err_msg = "cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, or ldapurl together with ldapprefix";
+                               *err_msg = "cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter or ldapurl together with ldapprefix";
                                return NULL;
                        }
                }
@@ -1534,6 +1536,22 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
                        *err_msg = "authentication method \"ldap\" requires argument \"ldapbasedn\", \"ldapprefix\", or \"ldapsuffix\" to be set";
                        return NULL;
                }
+
+               /*
+                * When using search+bind, you can either use a simple attribute
+                * (defaulting to "uid") or a fully custom search filter.  You can't
+                * do both.
+                */
+               if (parsedline->ldapsearchattribute && parsedline->ldapsearchfilter)
+               {
+                       ereport(elevel,
+                                       (errcode(ERRCODE_CONFIG_FILE_ERROR),
+                                        errmsg("cannot use ldapsearchattribute together with ldapsearchfilter"),
+                                        errcontext("line %d of configuration file \"%s\"",
+                                                               line_num, HbaFileName)));
+                       *err_msg = "cannot use ldapsearchattribute together with ldapsearchfilter";
+                       return NULL;
+               }
        }
 
        if (parsedline->auth_method == uaRADIUS)
@@ -1729,14 +1747,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
                        hbaline->ldapsearchattribute = pstrdup(urldata->lud_attrs[0]);  /* only use first one */
                hbaline->ldapscope = urldata->lud_scope;
                if (urldata->lud_filter)
-               {
-                       ereport(elevel,
-                                       (errcode(ERRCODE_CONFIG_FILE_ERROR),
-                                        errmsg("filters not supported in LDAP URLs")));
-                       *err_msg = "filters not supported in LDAP URLs";
-                       ldap_free_urldesc(urldata);
-                       return false;
-               }
+                       hbaline->ldapsearchfilter = pstrdup(urldata->lud_filter);
                ldap_free_urldesc(urldata);
 #else                                                  /* not OpenLDAP */
                ereport(elevel,
@@ -1788,6 +1799,11 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
                REQUIRE_AUTH_OPTION(uaLDAP, "ldapsearchattribute", "ldap");
                hbaline->ldapsearchattribute = pstrdup(val);
        }
+       else if (strcmp(name, "ldapsearchfilter") == 0)
+       {
+               REQUIRE_AUTH_OPTION(uaLDAP, "ldapsearchfilter", "ldap");
+               hbaline->ldapsearchfilter = pstrdup(val);
+       }
        else if (strcmp(name, "ldapbasedn") == 0)
        {
                REQUIRE_AUTH_OPTION(uaLDAP, "ldapbasedn", "ldap");
@@ -2266,6 +2282,11 @@ gethba_options(HbaLine *hba)
                                CStringGetTextDatum(psprintf("ldapsearchattribute=%s",
                                                                                         hba->ldapsearchattribute));
 
+               if (hba->ldapsearchfilter)
+                       options[noptions++] =
+                               CStringGetTextDatum(psprintf("ldapsearchfilter=%s",
+                                                                                        hba->ldapsearchfilter));
+
                if (hba->ldapscope)
                        options[noptions++] =
                                CStringGetTextDatum(psprintf("ldapscope=%d", hba->ldapscope));
index 07d92d4f9f3a975a4ed9bebb11b91fbf01c29a46..e711bee8bffdae39c612628d418553032d10ace1 100644 (file)
@@ -80,6 +80,7 @@ typedef struct HbaLine
        char       *ldapbinddn;
        char       *ldapbindpasswd;
        char       *ldapsearchattribute;
+       char       *ldapsearchfilter;
        char       *ldapbasedn;
        int                     ldapscope;
        char       *ldapprefix;