-<!-- $PostgreSQL: pgsql/doc/src/sgml/client-auth.sgml,v 1.124 2009/10/01 01:58:57 tgl Exp $ -->
+<!-- $PostgreSQL: pgsql/doc/src/sgml/client-auth.sgml,v 1.125 2009/12/12 21:35:21 mha Exp $ -->
<chapter id="client-authentication">
<title>Client Authentication</title>
</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
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>
</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 — 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>
<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 — 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>
*
*
* IDENTIFICATION
- * $PostgreSQL: pgsql/src/backend/libpq/auth.c,v 1.187 2009/10/16 22:08:36 tgl Exp $
+ * $PostgreSQL: pgsql/src/backend/libpq/auth.c,v 1.188 2009/12/12 21:35:21 mha Exp $
*
*-------------------------------------------------------------------------
*/
*/
#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,
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;
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;
* 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;
_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.")));
* 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);
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 */
*
*
* IDENTIFICATION
- * $PostgreSQL: pgsql/src/backend/libpq/hba.c,v 1.192 2009/10/03 20:04:39 tgl Exp $
+ * $PostgreSQL: pgsql/src/backend/libpq/hba.c,v 1.193 2009/12/12 21:35:21 mha Exp $
*
*-------------------------------------------------------------------------
*/
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");
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;
+ }
}
/*