Allow LDAP authentication to operate in search+bind mode, meaning it
authorMagnus Hagander <[email protected]>
Sat, 12 Dec 2009 21:35:21 +0000 (21:35 +0000)
committerMagnus Hagander <[email protected]>
Sat, 12 Dec 2009 21:35:21 +0000 (21:35 +0000)
does a search for the user in the directory first, and then binds with
the DN found for this user.

This allows for LDAP logins in scenarios where the DN of the user cannot
be determined simply by prefix and suffix, such as the case where different
users are located in different containers.

The old way of authentication can be significantly faster, so it's kept
as an option.

Robert Fleming and Magnus Hagander

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

index e2f09313b314530ea8a14316da9080875d2d1d6f..cf913adf0202c3aa486449d33abd10a5aa49c3b9 100644 (file)
@@ -1202,7 +1202,8 @@ omicron       bryanh            guest1
    </para>
 
    <para>
-    The server will bind to the distinguished name constructed as
+    LDAP authentication can operate in two modes. In the first mode,
+    the server will bind to the distinguished name constructed as
     <replaceable>prefix</> <replaceable>username</> <replaceable>suffix</>.
     Typically, the <replaceable>prefix</> parameter is used to specify
     <literal>cn=</>, or <replaceable>DOMAIN</><literal>\</> in an Active
@@ -1210,6 +1211,23 @@ omicron       bryanh            guest1
     remaining part of the DN in a non-Active Directory environment.
    </para>
 
+   <para>
+    In the second mode, the server first binds to the LDAP directory with
+    a fixed username and password, specified with <replaceable>ldapbinduser</>
+    and <replaceable>ldapbinddn</>, and performs a search for the user trying
+    to log in to the database. If no user and password is configured, an
+    anonymous bind will be attempted to the directory. The search will be
+    performed over the subtree at <replaceable>ldapbasedn</>, and will try to
+    do an exact match of the attribute specified in
+    <replaceable>ldapsearchattribute</>. If no attribute is specified, the
+    <literal>uid</> attribute will be used. Once the user has been found in
+    this search, the server disconnects and re-binds to the directory as
+    this user, using the password specified by the client, to verify that the
+    login is correct. This method allows for significantly more flexibility
+    in where the user objects are located in the directory, but will cause
+    two separate connections to the LDAP server to be made.
+   </para>
+
    <para>
     The following configuration options are supported for LDAP:
     <variablelist>
@@ -1221,11 +1239,32 @@ omicron       bryanh            guest1
        </para>
       </listitem>
      </varlistentry>
+     <varlistentry>
+      <term><literal>ldapport</literal></term>
+      <listitem>
+       <para>
+        Port number on LDAP server to connect to. If no port is specified,
+        the default port in the LDAP library will be used.
+       </para>
+      </listitem>
+     </varlistentry>
+     <varlistentry>
+      <term><literal>ldaptls</literal></term>
+      <listitem>
+       <para>
+        Set to <literal>1</> to make the connection between PostgreSQL and the
+        LDAP server use TLS encryption. Note that this only encrypts
+        the traffic to the LDAP server &mdash; the connection to the client
+        will still be unencrypted unless SSL is used.
+       </para>
+      </listitem>
+     </varlistentry>
      <varlistentry>
       <term><literal>ldapprefix</literal></term>
       <listitem>
        <para>
-        String to prepend to the username when forming the DN to bind as.
+        String to prepend to the username when forming the DN to bind as,
+        when doing simple bind authentication.
        </para>
       </listitem>
      </varlistentry>
@@ -1233,30 +1272,47 @@ omicron       bryanh            guest1
       <term><literal>ldapsuffix</literal></term>
       <listitem>
        <para>
-        String to append to the username when forming the DN to bind as.
+        String to append to the username when forming the DN to bind as,
+        when doing simple bind authentication.
        </para>
       </listitem>
      </varlistentry>
      <varlistentry>
-      <term><literal>ldapport</literal></term>
+      <term><literal>ldapbasedn</literal></term>
       <listitem>
        <para>
-        Port number on LDAP server to connect to. If no port is specified,
-        the default port in the LDAP library will be used.
+        DN to root the search for the user in, when doing search+bind
+        authentication.
        </para>
       </listitem>
      </varlistentry>
      <varlistentry>
-      <term><literal>ldaptls</literal></term>
+      <term><literal>ldapbinddn</literal></term>
       <listitem>
        <para>
-        Set to <literal>1</> to make the connection between PostgreSQL and the
-        LDAP server use TLS encryption. Note that this only encrypts
-        the traffic to the LDAP server &mdash; the connection to the client
-        will still be unencrypted unless SSL is used.
+        DN of user to bind to the directory with to perform the search when
+        doing search+bind authentication.
        </para>
       </listitem>
      </varlistentry>
+     <varlistentry>
+      <term><literal>ldapbindpasswd</literal></term>
+      <listitem>
+       <para>
+        Password for user to bind to the directory with to perform the search
+        when doing search+bind authentication.
+       </para>
+      </listitem>
+      </varlistentry>
+      <varlistentry>
+       <term><literal>ldapsearchattribute</literal></term>
+       <listitem>
+        <para>
+         Attribute to match against the username in the search when doing
+         search+bind authentication.
+        </para>
+       </listitem>
+      </varlistentry>
     </variablelist>
    </para>
 
index f303fb8699c084126813e295d0ca90af284c4b0a..4212987da901ceaa6ea87957e54346d8dfc2c5c0 100644 (file)
@@ -2096,40 +2096,18 @@ CheckPAMAuth(Port *port, char *user, char *password)
  */
 #ifdef USE_LDAP
 
+/*
+ * Initialize a connection to the LDAP server, including setting up
+ * TLS if requested.
+ */
 static int
-CheckLDAPAuth(Port *port)
+InitializeLDAPConnection(Port *port, LDAP **ldap)
 {
-   char       *passwd;
-   LDAP       *ldap;
-   int         r;
    int         ldapversion = LDAP_VERSION3;
-   char        fulluser[NAMEDATALEN + 256 + 1];
-
-   if (!port->hba->ldapserver || port->hba->ldapserver[0] == '\0')
-   {
-       ereport(LOG,
-               (errmsg("LDAP server not specified")));
-       return STATUS_ERROR;
-   }
-
-   if (port->hba->ldapport == 0)
-       port->hba->ldapport = LDAP_PORT;
-
-   sendAuthRequest(port, AUTH_REQ_PASSWORD);
-
-   passwd = recv_password_packet(port);
-   if (passwd == NULL)
-       return STATUS_EOF;      /* client wouldn't send password */
-
-   if (strlen(passwd) == 0)
-   {
-       ereport(LOG,
-               (errmsg("empty password returned by client")));
-       return STATUS_ERROR;
-   }
+   int         r;
 
-   ldap = ldap_init(port->hba->ldapserver, port->hba->ldapport);
-   if (!ldap)
+   *ldap = ldap_init(port->hba->ldapserver, port->hba->ldapport);
+   if (!*ldap)
    {
 #ifndef WIN32
        ereport(LOG,
@@ -2143,9 +2121,9 @@ CheckLDAPAuth(Port *port)
        return STATUS_ERROR;
    }
 
-   if ((r = ldap_set_option(ldap, LDAP_OPT_PROTOCOL_VERSION, &ldapversion)) != LDAP_SUCCESS)
+   if ((r = ldap_set_option(*ldap, LDAP_OPT_PROTOCOL_VERSION, &ldapversion)) != LDAP_SUCCESS)
    {
-       ldap_unbind(ldap);
+       ldap_unbind(*ldap);
        ereport(LOG,
          (errmsg("could not set LDAP protocol version: error code %d", r)));
        return STATUS_ERROR;
@@ -2154,7 +2132,7 @@ CheckLDAPAuth(Port *port)
    if (port->hba->ldaptls)
    {
 #ifndef WIN32
-       if ((r = ldap_start_tls_s(ldap, NULL, NULL)) != LDAP_SUCCESS)
+       if ((r = ldap_start_tls_s(*ldap, NULL, NULL)) != LDAP_SUCCESS)
 #else
        static __ldap_start_tls_sA _ldap_start_tls_sA = NULL;
 
@@ -2174,7 +2152,7 @@ CheckLDAPAuth(Port *port)
                 * should never happen since we import other files from
                 * wldap32, but check anyway
                 */
-               ldap_unbind(ldap);
+               ldap_unbind(*ldap);
                ereport(LOG,
                        (errmsg("could not load wldap32.dll")));
                return STATUS_ERROR;
@@ -2182,7 +2160,7 @@ CheckLDAPAuth(Port *port)
            _ldap_start_tls_sA = (__ldap_start_tls_sA) GetProcAddress(ldaphandle, "ldap_start_tls_sA");
            if (_ldap_start_tls_sA == NULL)
            {
-               ldap_unbind(ldap);
+               ldap_unbind(*ldap);
                ereport(LOG,
                        (errmsg("could not load function _ldap_start_tls_sA in wldap32.dll"),
                         errdetail("LDAP over SSL is not supported on this platform.")));
@@ -2195,21 +2173,202 @@ CheckLDAPAuth(Port *port)
             * per process and is automatically cleaned up on process exit.
             */
        }
-       if ((r = _ldap_start_tls_sA(ldap, NULL, NULL, NULL, NULL)) != LDAP_SUCCESS)
+       if ((r = _ldap_start_tls_sA(*ldap, NULL, NULL, NULL, NULL)) != LDAP_SUCCESS)
 #endif
        {
-           ldap_unbind(ldap);
+           ldap_unbind(*ldap);
            ereport(LOG,
             (errmsg("could not start LDAP TLS session: error code %d", r)));
            return STATUS_ERROR;
        }
    }
 
-   snprintf(fulluser, sizeof(fulluser), "%s%s%s",
-            port->hba->ldapprefix ? port->hba->ldapprefix : "",
-            port->user_name,
-            port->hba->ldapsuffix ? port->hba->ldapsuffix : "");
-   fulluser[sizeof(fulluser) - 1] = '\0';
+   return STATUS_OK;
+}
+
+/*
+ * Perform LDAP authentication
+ */
+static int
+CheckLDAPAuth(Port *port)
+{
+   char       *passwd;
+   LDAP       *ldap;
+   int         r;
+   char       *fulluser;
+
+   if (!port->hba->ldapserver || port->hba->ldapserver[0] == '\0')
+   {
+       ereport(LOG,
+               (errmsg("LDAP server not specified")));
+       return STATUS_ERROR;
+   }
+
+   if (port->hba->ldapport == 0)
+       port->hba->ldapport = LDAP_PORT;
+
+   sendAuthRequest(port, AUTH_REQ_PASSWORD);
+
+   passwd = recv_password_packet(port);
+   if (passwd == NULL)
+       return STATUS_EOF;      /* client wouldn't send password */
+
+   if (strlen(passwd) == 0)
+   {
+       ereport(LOG,
+               (errmsg("empty password returned by client")));
+       return STATUS_ERROR;
+   }
+
+   if (InitializeLDAPConnection(port, &ldap) == STATUS_ERROR)
+       /* Error message already sent */
+       return STATUS_ERROR;
+
+   if (port->hba->ldapbasedn)
+   {
+       /*
+        * First perform an LDAP search to find the DN for the user we are trying to log
+        * in as.
+        */
+       char           *filter;
+       LDAPMessage    *search_message;
+       LDAPMessage    *entry;
+       char           *attributes[2];
+       char           *dn;
+       char           *c;
+
+       /*
+        * Disallow any characters that we would otherwise need to escape, since they
+        * aren't really reasonable in a username anyway. Allowing them would make it
+        * possible to inject any kind of custom filters in the LDAP filter.
+        */
+       for (c = port->user_name; *c; c++)
+       {
+           if (*c == '*' ||
+               *c == '(' ||
+               *c == ')' ||
+               *c == '\\' ||
+               *c == '/')
+           {
+               ereport(LOG,
+                       (errmsg("invalid character in username for LDAP authentication")));
+               return STATUS_ERROR;
+           }
+       }
+
+       /*
+        * Bind with a pre-defined username/password (if available) for searching. If
+        * none is specified, this turns into an anonymous bind.
+        */
+       r = ldap_simple_bind_s(ldap,
+                              port->hba->ldapbinddn ? port->hba->ldapbinddn : "",
+                              port->hba->ldapbindpasswd ? port->hba->ldapbindpasswd : "");
+       if (r != LDAP_SUCCESS)
+       {
+           ereport(LOG,
+                   (errmsg("could not perform initial LDAP bind for ldapbinddn \"%s\" on server \"%s\": error code %d",
+                           port->hba->ldapbinddn, port->hba->ldapserver, r)));
+           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 = palloc(strlen(attributes[0])+strlen(port->user_name)+4);
+       sprintf(filter, "(%s=%s)",
+                attributes[0],
+                port->user_name);
+
+       r = ldap_search_s(ldap,
+                         port->hba->ldapbasedn,
+                         LDAP_SCOPE_SUBTREE,
+                         filter,
+                         attributes,
+                         0,
+                         &search_message);
+
+       if (r != LDAP_SUCCESS)
+       {
+           ereport(LOG,
+                   (errmsg("could not search LDAP for filter \"%s\" on server \"%s\": error code %d",
+                           filter, port->hba->ldapserver, r)));
+           pfree(filter);
+           return STATUS_ERROR;
+       }
+
+       if (ldap_count_entries(ldap, search_message) != 1)
+       {
+           if (ldap_count_entries(ldap, search_message) == 0)
+               ereport(LOG,
+                       (errmsg("LDAP search failed for filter \"%s\" on server \"%s\": no such user",
+                               filter, port->hba->ldapserver)));
+           else
+               ereport(LOG,
+                       (errmsg("LDAP search failed for filter \"%s\" on server \"%s\": user is not unique (%d matches)",
+                               filter, port->hba->ldapserver, ldap_count_entries(ldap, search_message))));
+
+           pfree(filter);
+           ldap_msgfree(search_message);
+           return STATUS_ERROR;
+       }
+
+       entry = ldap_first_entry(ldap, search_message);
+       dn = ldap_get_dn(ldap, entry);
+       if (dn == NULL)
+       {
+           int error;
+           (void)ldap_get_option(ldap, LDAP_OPT_ERROR_NUMBER, &error);
+           ereport(LOG,
+                   (errmsg("could not get dn for the first entry matching \"%s\" on server \"%s\": %s",
+                           filter, port->hba->ldapserver, ldap_err2string(error))));
+           pfree(filter);
+           ldap_msgfree(search_message);
+           return STATUS_ERROR;
+       }
+       fulluser = pstrdup(dn);
+
+       pfree(filter);
+       ldap_memfree(dn);
+       ldap_msgfree(search_message);
+
+       /* Unbind and disconnect from the LDAP server */
+       r = ldap_unbind_s(ldap);
+       if (r != LDAP_SUCCESS)
+       {
+           int error;
+           (void)ldap_get_option(ldap, LDAP_OPT_ERROR_NUMBER, &error);
+           ereport(LOG,
+                   (errmsg("could not unbind after searching for user \"%s\" on server \"%s\": %s",
+                           fulluser, port->hba->ldapserver, ldap_err2string(error))));
+           pfree(fulluser);
+           return STATUS_ERROR;
+       }
+
+       /*
+        * Need to re-initialize the LDAP connection, so that we can bind
+        * to it with a different username.
+        */
+       if (InitializeLDAPConnection(port, &ldap) == STATUS_ERROR)
+       {
+           pfree(fulluser);
+
+           /* Error message already sent */
+           return STATUS_ERROR;
+       }
+   }
+   else
+   {
+       fulluser = palloc((port->hba->ldapprefix ? strlen(port->hba->ldapprefix) : 0) +
+                         strlen(port->user_name) +
+                         (port->hba->ldapsuffix ? strlen(port->hba->ldapsuffix) : 0) +
+                         1);
+
+       sprintf(fulluser, "%s%s%s",
+                port->hba->ldapprefix ? port->hba->ldapprefix : "",
+                port->user_name,
+                port->hba->ldapsuffix ? port->hba->ldapsuffix : "");
+   }
 
    r = ldap_simple_bind_s(ldap, fulluser, passwd);
    ldap_unbind(ldap);
@@ -2219,9 +2378,12 @@ CheckLDAPAuth(Port *port)
        ereport(LOG,
                (errmsg("LDAP login failed for user \"%s\" on server \"%s\": error code %d",
                        fulluser, port->hba->ldapserver, r)));
+       pfree(fulluser);
        return STATUS_ERROR;
    }
 
+   pfree(fulluser);
+
    return STATUS_OK;
 }
 #endif   /* USE_LDAP */
index 433590f5e7c0fa0e6596c138b7980f30e1380710..b0fabef6d2ee006023b25ec408822588d2a5d2d1 100644 (file)
@@ -1103,6 +1103,26 @@ parse_hba_line(List *line, int line_num, HbaLine *parsedline)
                    return false;
                }
            }
+           else if (strcmp(token, "ldapbinddn") == 0)
+           {
+               REQUIRE_AUTH_OPTION(uaLDAP, "ldapbinddn", "ldap");
+               parsedline->ldapbinddn = pstrdup(c);
+           }
+           else if (strcmp(token, "ldapbindpasswd") == 0)
+           {
+               REQUIRE_AUTH_OPTION(uaLDAP, "ldapbindpasswd", "ldap");
+               parsedline->ldapbindpasswd = pstrdup(c);
+           }
+           else if (strcmp(token, "ldapsearchattribute") == 0)
+           {
+               REQUIRE_AUTH_OPTION(uaLDAP, "ldapsearchattribute", "ldap");
+               parsedline->ldapsearchattribute = pstrdup(c);
+           }
+           else if (strcmp(token, "ldapbasedn") == 0)
+           {
+               REQUIRE_AUTH_OPTION(uaLDAP, "ldapbasedn", "ldap");
+               parsedline->ldapbasedn = pstrdup(c);
+           }
            else if (strcmp(token, "ldapprefix") == 0)
            {
                REQUIRE_AUTH_OPTION(uaLDAP, "ldapprefix", "ldap");
@@ -1156,6 +1176,37 @@ parse_hba_line(List *line, int line_num, HbaLine *parsedline)
    if (parsedline->auth_method == uaLDAP)
    {
        MANDATORY_AUTH_ARG(parsedline->ldapserver, "ldapserver", "ldap");
+
+       /*
+        * 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.
+        */
+       if (parsedline->ldapprefix || parsedline->ldapsuffix)
+       {
+           if (parsedline->ldapbasedn ||
+               parsedline->ldapbinddn ||
+               parsedline->ldapbindpasswd ||
+               parsedline->ldapsearchattribute)
+           {
+               ereport(LOG,
+                       (errcode(ERRCODE_CONFIG_FILE_ERROR),
+                        errmsg("cannot use ldapbasedn, ldapbinddn, ldapbindpasswd or ldapsearchattribute together with ldapprefix"),
+                        errcontext("line %d of configuration file \"%s\"",
+                                   line_num, HbaFileName)));
+               return false;
+           }
+       }
+       else if (!parsedline->ldapbasedn)
+       {
+           ereport(LOG,
+                   (errcode(ERRCODE_CONFIG_FILE_ERROR),
+                    errmsg("authentication method \"ldap\" requires argument \"ldapbasedn\", \"ldapprefix\" or \"ldapsuffix\" to be set"),
+                    errcontext("line %d of configuration file \"%s\"",
+                               line_num, HbaFileName)));
+           return false;
+       }
    }
 
    /*
index 74168ee9a6d1267f4b1e3a05207f1a58ef26e631..10561fe2ba7e49e985d7ec3d877e0feb27339068 100644 (file)
@@ -61,6 +61,10 @@ typedef struct
    bool        ldaptls;
    char       *ldapserver;
    int         ldapport;
+   char       *ldapbinddn;
+   char       *ldapbindpasswd;
+   char       *ldapsearchattribute;
+   char       *ldapbasedn;
    char       *ldapprefix;
    char       *ldapsuffix;
    bool        clientcert;