From 0f086f84ad9041888b789af5871c7432f0e19c5b Mon Sep 17 00:00:00 2001 From: Thomas Munro Date: Thu, 21 Mar 2019 15:19:03 +1300 Subject: [PATCH] Add DNS SRV support for LDAP server discovery. LDAP servers can be advertised on a network with RFC 2782 DNS SRV records. The OpenLDAP command-line tools automatically try to find servers that way, if no server name is provided by the user. Teach PostgreSQL to do the same using OpenLDAP's support functions, when building with OpenLDAP. For now, we assume that HAVE_LDAP_INITIALIZE (an OpenLDAP extension available since OpenLDAP 2.0 and also present in Apple LDAP) implies that you also have ldap_domain2hostlist() (which arrived in the same OpenLDAP version and is also present in Apple LDAP). Author: Thomas Munro Reviewed-by: Daniel Gustafsson Discussion: https://postgr.es/m/CAEepm=2hAnSfhdsd6vXsM6VZVN0br-FbAZ-O+Swk18S5HkCP=A@mail.gmail.com --- doc/src/sgml/client-auth.sgml | 21 ++++- src/backend/libpq/auth.c | 152 +++++++++++++++++++++++++--------- src/backend/libpq/hba.c | 3 + 3 files changed, 135 insertions(+), 41 deletions(-) diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml index 411f1e1679..c7e4d3817f 100644 --- a/doc/src/sgml/client-auth.sgml +++ b/doc/src/sgml/client-auth.sgml @@ -1655,7 +1655,8 @@ ldap[s]://host[:port]/ - LDAP URLs are currently only supported with OpenLDAP, not on Windows. + LDAP URLs are currently only supported with + OpenLDAP, not on Windows. @@ -1678,6 +1679,15 @@ ldap[s]://host[:port]/ldapsearchattribute=uid. + + If PostgreSQL was compiled with + OpenLDAP as the LDAP client library, the + ldapserver setting may be omitted. In that case, a + list of hostnames and ports is looked up via RFC 2782 DNS SRV records. + The name _ldap._tcp.DOMAIN is looked up, where + DOMAIN is extracted from ldapbasedn. + + Here is an example for a simple-bind LDAP configuration: @@ -1723,6 +1733,15 @@ host ... ldap ldapserver=ldap.example.net ldapbasedn="dc=example, dc=net" ldapse + + Here is an example for a search+bind configuration that uses DNS SRV + discovery to find the hostname(s) and port(s) for the LDAP service for the + domain name example.net: + +host ... ldap ldapbasedn="dc=example,dc=net" + + + Since LDAP often uses commas and spaces to separate the different diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c index fb86e9e9d4..6f03c7c2a5 100644 --- a/src/backend/libpq/auth.c +++ b/src/backend/libpq/auth.c @@ -2368,45 +2368,95 @@ InitializeLDAPConnection(Port *port, LDAP **ldap) } #else #ifdef HAVE_LDAP_INITIALIZE + + /* + * OpenLDAP provides a non-standard extension ldap_initialize() that takes + * a list of URIs, allowing us to request "ldaps" instead of "ldap". It + * also provides ldap_domain2hostlist() to find LDAP servers automatically + * using DNS SRV. They were introduced in the same version, so for now we + * don't have an extra configure check for the latter. + */ { - const char *hostnames = port->hba->ldapserver; - char *uris = NULL; + StringInfoData uris; + char *hostlist = NULL; + char *p; + bool append_port; + + /* We'll build a space-separated scheme://hostname:port list here */ + initStringInfo(&uris); /* - * We have a space-separated list of hostnames. Convert it - * to a space-separated list of URIs. + * If pg_hba.conf provided no hostnames, we can ask OpenLDAP to try to + * find some by extracting a domain name from the base DN and looking + * up DSN SRV records for _ldap._tcp.. */ + if (!port->hba->ldapserver || port->hba->ldapserver[0] == '\0') + { + char *domain; + + /* ou=blah,dc=foo,dc=bar -> foo.bar */ + if (ldap_dn2domain(port->hba->ldapbasedn, &domain)) + { + ereport(LOG, + (errmsg("could not extract domain name from ldapbasedn"))); + return STATUS_ERROR; + } + + /* Look up a list of LDAP server hosts and port numbers */ + if (ldap_domain2hostlist(domain, &hostlist)) + { + ereport(LOG, + (errmsg("LDAP authentication could not find DNS SRV records for \"%s\"", + domain), + (errhint("Set an LDAP server name explicitly.")))); + ldap_memfree(domain); + return STATUS_ERROR; + } + ldap_memfree(domain); + + /* We have a space-separated list of host:port entries */ + p = hostlist; + append_port = false; + } + else + { + /* We have a space-separated list of hosts from pg_hba.conf */ + p = port->hba->ldapserver; + append_port = true; + } + + /* Convert the list of host[:port] entries to full URIs */ do { - char *hostname; - size_t hostname_size; - char *new_uris; - - /* Find the leading hostname. */ - hostname_size = strcspn(hostnames, " "); - hostname = pnstrdup(hostnames, hostname_size); - - /* Append a URI for this hostname. */ - new_uris = psprintf("%s%s%s://%s:%d", - uris ? uris : "", - uris ? " " : "", - scheme, - hostname, - port->hba->ldapport); - - pfree(hostname); - if (uris) - pfree(uris); - uris = new_uris; - - /* Step over this hostname and any spaces. */ - hostnames += hostname_size; - while (*hostnames == ' ') - ++hostnames; - } while (*hostnames); - - r = ldap_initialize(ldap, uris); - pfree(uris); + size_t size; + + /* Find the span of the next entry */ + size = strcspn(p, " "); + + /* Append a space separator if this isn't the first URI */ + if (uris.len > 0) + appendStringInfoChar(&uris, ' '); + + /* Append scheme://host:port */ + appendStringInfoString(&uris, scheme); + appendStringInfoString(&uris, "://"); + appendBinaryStringInfo(&uris, p, size); + if (append_port) + appendStringInfo(&uris, ":%d", port->hba->ldapport); + + /* Step over this entry and any number of trailing spaces */ + p += size; + while (*p == ' ') + ++p; + } while (*p); + + /* Free memory from OpenLDAP if we looked up SRV records */ + if (hostlist) + ldap_memfree(hostlist); + + /* Finally, try to connect using the URI list */ + r = ldap_initialize(ldap, uris.data); + pfree(uris.data); if (r != LDAP_SUCCESS) { ereport(LOG, @@ -2552,13 +2602,35 @@ CheckLDAPAuth(Port *port) LDAP *ldap; int r; char *fulluser; + const char *server_name; +#ifdef HAVE_LDAP_INITIALIZE + + /* + * For OpenLDAP, allow empty hostname if we have a basedn. We'll look for + * servers with DNS SRV records via OpenLDAP library facilities. + */ + if ((!port->hba->ldapserver || port->hba->ldapserver[0] == '\0') && + (!port->hba->ldapbasedn || port->hba->ldapbasedn[0] == '\0')) + { + ereport(LOG, + (errmsg("LDAP server not specified, and no ldapbasedn"))); + return STATUS_ERROR; + } +#else if (!port->hba->ldapserver || port->hba->ldapserver[0] == '\0') { ereport(LOG, (errmsg("LDAP server not specified"))); return STATUS_ERROR; } +#endif + + /* + * If we're using SRV records, we don't have a server name so we'll just + * show an empty string in error messages. + */ + server_name = port->hba->ldapserver ? port->hba->ldapserver : ""; if (port->hba->ldapport == 0) { @@ -2630,7 +2702,7 @@ CheckLDAPAuth(Port *port) ereport(LOG, (errmsg("could not perform initial LDAP bind for ldapbinddn \"%s\" on server \"%s\": %s", port->hba->ldapbinddn ? port->hba->ldapbinddn : "", - port->hba->ldapserver, + server_name, ldap_err2string(r)), errdetail_for_ldap(ldap))); ldap_unbind(ldap); @@ -2658,7 +2730,7 @@ CheckLDAPAuth(Port *port) { ereport(LOG, (errmsg("could not search LDAP for filter \"%s\" on server \"%s\": %s", - filter, port->hba->ldapserver, ldap_err2string(r)), + filter, server_name, ldap_err2string(r)), errdetail_for_ldap(ldap))); ldap_unbind(ldap); pfree(passwd); @@ -2673,14 +2745,14 @@ CheckLDAPAuth(Port *port) ereport(LOG, (errmsg("LDAP user \"%s\" does not exist", port->user_name), errdetail("LDAP search for filter \"%s\" on server \"%s\" returned no entries.", - filter, port->hba->ldapserver))); + filter, server_name))); else ereport(LOG, (errmsg("LDAP user \"%s\" is not unique", port->user_name), errdetail_plural("LDAP search for filter \"%s\" on server \"%s\" returned %d entry.", "LDAP search for filter \"%s\" on server \"%s\" returned %d entries.", count, - filter, port->hba->ldapserver, count))); + filter, server_name, count))); ldap_unbind(ldap); pfree(passwd); @@ -2698,7 +2770,7 @@ CheckLDAPAuth(Port *port) (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, + filter, server_name, ldap_err2string(error)), errdetail_for_ldap(ldap))); ldap_unbind(ldap); @@ -2719,7 +2791,7 @@ CheckLDAPAuth(Port *port) { ereport(LOG, (errmsg("could not unbind after searching for user \"%s\" on server \"%s\"", - fulluser, port->hba->ldapserver))); + fulluser, server_name))); pfree(passwd); pfree(fulluser); return STATUS_ERROR; @@ -2750,7 +2822,7 @@ CheckLDAPAuth(Port *port) { ereport(LOG, (errmsg("LDAP login failed for user \"%s\" on server \"%s\": %s", - fulluser, port->hba->ldapserver, ldap_err2string(r)), + fulluser, server_name, ldap_err2string(r)), errdetail_for_ldap(ldap))); ldap_unbind(ldap); pfree(passwd); diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c index 59de1b7639..ce9bca868c 100644 --- a/src/backend/libpq/hba.c +++ b/src/backend/libpq/hba.c @@ -1500,7 +1500,10 @@ parse_hba_line(TokenizedLine *tok_line, int elevel) */ if (parsedline->auth_method == uaLDAP) { +#ifndef HAVE_LDAP_INITIALIZE + /* Not mandatory for OpenLDAP, because it can use DNS SRV records */ MANDATORY_AUTH_ARG(parsedline->ldapserver, "ldapserver", "ldap"); +#endif /* * LDAP can operate in two modes: either with a direct bind, using -- 2.40.0