diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml index 6442aa1efed565dc0915475328d9ec7d9cc78d17..e1d8c9503b56554333056a849b4f155752c7fe26 100644 --- a/doc/src/sgml/client-auth.sgml +++ b/doc/src/sgml/client-auth.sgml @@ -1,4 +1,4 @@ -<!-- $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> @@ -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 — 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 — 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> diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c index 1e080465b675249bbf6b9b11488848bb5f3e4d79..7b6b5a5a071b72223b65a1ce2a88cc96c2e81e11 100644 --- a/src/backend/libpq/auth.c +++ b/src/backend/libpq/auth.c @@ -8,7 +8,7 @@ * * * 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 $ * *------------------------------------------------------------------------- */ @@ -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 */ diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c index a51ea53082b423606dfd15297a223b0bcdc7acf4..65d5beadd3f905dac5dedf5b55aa55089b6148c7 100644 --- a/src/backend/libpq/hba.c +++ b/src/backend/libpq/hba.c @@ -10,7 +10,7 @@ * * * 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 $ * *------------------------------------------------------------------------- */ @@ -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; + } } /* diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h index b444e09ecf201e97000734782308db9bc37ec0a6..8ee71a7e093647b84047e71374edce20ef5c3510 100644 --- a/src/include/libpq/hba.h +++ b/src/include/libpq/hba.h @@ -4,7 +4,7 @@ * Interface to hba.c * * - * $PostgreSQL: pgsql/src/include/libpq/hba.h,v 1.59 2009/10/01 01:58:58 tgl Exp $ + * $PostgreSQL: pgsql/src/include/libpq/hba.h,v 1.60 2009/12/12 21:35:21 mha Exp $ * *------------------------------------------------------------------------- */ @@ -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;