Skip to content

Commit 0f086f8

Browse files
committed
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
1 parent 8aa9dd7 commit 0f086f8

File tree

3 files changed

+135
-41
lines changed

3 files changed

+135
-41
lines changed

doc/src/sgml/client-auth.sgml

+20-1
Original file line numberDiff line numberDiff line change
@@ -1655,7 +1655,8 @@ ldap[s]://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<rep
16551655
</para>
16561656

16571657
<para>
1658-
LDAP URLs are currently only supported with OpenLDAP, not on Windows.
1658+
LDAP URLs are currently only supported with
1659+
<productname>OpenLDAP</productname>, not on Windows.
16591660
</para>
16601661
</listitem>
16611662
</varlistentry>
@@ -1678,6 +1679,15 @@ ldap[s]://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<rep
16781679
<literal>ldapsearchattribute=uid</literal>.
16791680
</para>
16801681

1682+
<para>
1683+
If <productname>PostgreSQL</productname> was compiled with
1684+
<productname>OpenLDAP</productname> as the LDAP client library, the
1685+
<literal>ldapserver</literal> setting may be omitted. In that case, a
1686+
list of hostnames and ports is looked up via RFC 2782 DNS SRV records.
1687+
The name <literal>_ldap._tcp.DOMAIN</literal> is looked up, where
1688+
<literal>DOMAIN</literal> is extracted from <literal>ldapbasedn</literal>.
1689+
</para>
1690+
16811691
<para>
16821692
Here is an example for a simple-bind LDAP configuration:
16831693
<programlisting>
@@ -1723,6 +1733,15 @@ host ... ldap ldapserver=ldap.example.net ldapbasedn="dc=example, dc=net" ldapse
17231733
</programlisting>
17241734
</para>
17251735

1736+
<para>
1737+
Here is an example for a search+bind configuration that uses DNS SRV
1738+
discovery to find the hostname(s) and port(s) for the LDAP service for the
1739+
domain name <literal>example.net</literal>:
1740+
<programlisting>
1741+
host ... ldap ldapbasedn="dc=example,dc=net"
1742+
</programlisting>
1743+
</para>
1744+
17261745
<tip>
17271746
<para>
17281747
Since LDAP often uses commas and spaces to separate the different

src/backend/libpq/auth.c

+112-40
Original file line numberDiff line numberDiff line change
@@ -2368,45 +2368,95 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
23682368
}
23692369
#else
23702370
#ifdef HAVE_LDAP_INITIALIZE
2371+
2372+
/*
2373+
* OpenLDAP provides a non-standard extension ldap_initialize() that takes
2374+
* a list of URIs, allowing us to request "ldaps" instead of "ldap". It
2375+
* also provides ldap_domain2hostlist() to find LDAP servers automatically
2376+
* using DNS SRV. They were introduced in the same version, so for now we
2377+
* don't have an extra configure check for the latter.
2378+
*/
23712379
{
2372-
const char *hostnames = port->hba->ldapserver;
2373-
char *uris = NULL;
2380+
StringInfoData uris;
2381+
char *hostlist = NULL;
2382+
char *p;
2383+
bool append_port;
2384+
2385+
/* We'll build a space-separated scheme://hostname:port list here */
2386+
initStringInfo(&uris);
23742387

23752388
/*
2376-
* We have a space-separated list of hostnames. Convert it
2377-
* to a space-separated list of URIs.
2389+
* If pg_hba.conf provided no hostnames, we can ask OpenLDAP to try to
2390+
* find some by extracting a domain name from the base DN and looking
2391+
* up DSN SRV records for _ldap._tcp.<domain>.
23782392
*/
2393+
if (!port->hba->ldapserver || port->hba->ldapserver[0] == '\0')
2394+
{
2395+
char *domain;
2396+
2397+
/* ou=blah,dc=foo,dc=bar -> foo.bar */
2398+
if (ldap_dn2domain(port->hba->ldapbasedn, &domain))
2399+
{
2400+
ereport(LOG,
2401+
(errmsg("could not extract domain name from ldapbasedn")));
2402+
return STATUS_ERROR;
2403+
}
2404+
2405+
/* Look up a list of LDAP server hosts and port numbers */
2406+
if (ldap_domain2hostlist(domain, &hostlist))
2407+
{
2408+
ereport(LOG,
2409+
(errmsg("LDAP authentication could not find DNS SRV records for \"%s\"",
2410+
domain),
2411+
(errhint("Set an LDAP server name explicitly."))));
2412+
ldap_memfree(domain);
2413+
return STATUS_ERROR;
2414+
}
2415+
ldap_memfree(domain);
2416+
2417+
/* We have a space-separated list of host:port entries */
2418+
p = hostlist;
2419+
append_port = false;
2420+
}
2421+
else
2422+
{
2423+
/* We have a space-separated list of hosts from pg_hba.conf */
2424+
p = port->hba->ldapserver;
2425+
append_port = true;
2426+
}
2427+
2428+
/* Convert the list of host[:port] entries to full URIs */
23792429
do
23802430
{
2381-
char *hostname;
2382-
size_t hostname_size;
2383-
char *new_uris;
2384-
2385-
/* Find the leading hostname. */
2386-
hostname_size = strcspn(hostnames, " ");
2387-
hostname = pnstrdup(hostnames, hostname_size);
2388-
2389-
/* Append a URI for this hostname. */
2390-
new_uris = psprintf("%s%s%s://%s:%d",
2391-
uris ? uris : "",
2392-
uris ? " " : "",
2393-
scheme,
2394-
hostname,
2395-
port->hba->ldapport);
2396-
2397-
pfree(hostname);
2398-
if (uris)
2399-
pfree(uris);
2400-
uris = new_uris;
2401-
2402-
/* Step over this hostname and any spaces. */
2403-
hostnames += hostname_size;
2404-
while (*hostnames == ' ')
2405-
++hostnames;
2406-
} while (*hostnames);
2407-
2408-
r = ldap_initialize(ldap, uris);
2409-
pfree(uris);
2431+
size_t size;
2432+
2433+
/* Find the span of the next entry */
2434+
size = strcspn(p, " ");
2435+
2436+
/* Append a space separator if this isn't the first URI */
2437+
if (uris.len > 0)
2438+
appendStringInfoChar(&uris, ' ');
2439+
2440+
/* Append scheme://host:port */
2441+
appendStringInfoString(&uris, scheme);
2442+
appendStringInfoString(&uris, "://");
2443+
appendBinaryStringInfo(&uris, p, size);
2444+
if (append_port)
2445+
appendStringInfo(&uris, ":%d", port->hba->ldapport);
2446+
2447+
/* Step over this entry and any number of trailing spaces */
2448+
p += size;
2449+
while (*p == ' ')
2450+
++p;
2451+
} while (*p);
2452+
2453+
/* Free memory from OpenLDAP if we looked up SRV records */
2454+
if (hostlist)
2455+
ldap_memfree(hostlist);
2456+
2457+
/* Finally, try to connect using the URI list */
2458+
r = ldap_initialize(ldap, uris.data);
2459+
pfree(uris.data);
24102460
if (r != LDAP_SUCCESS)
24112461
{
24122462
ereport(LOG,
@@ -2552,13 +2602,35 @@ CheckLDAPAuth(Port *port)
25522602
LDAP *ldap;
25532603
int r;
25542604
char *fulluser;
2605+
const char *server_name;
25552606

2607+
#ifdef HAVE_LDAP_INITIALIZE
2608+
2609+
/*
2610+
* For OpenLDAP, allow empty hostname if we have a basedn. We'll look for
2611+
* servers with DNS SRV records via OpenLDAP library facilities.
2612+
*/
2613+
if ((!port->hba->ldapserver || port->hba->ldapserver[0] == '\0') &&
2614+
(!port->hba->ldapbasedn || port->hba->ldapbasedn[0] == '\0'))
2615+
{
2616+
ereport(LOG,
2617+
(errmsg("LDAP server not specified, and no ldapbasedn")));
2618+
return STATUS_ERROR;
2619+
}
2620+
#else
25562621
if (!port->hba->ldapserver || port->hba->ldapserver[0] == '\0')
25572622
{
25582623
ereport(LOG,
25592624
(errmsg("LDAP server not specified")));
25602625
return STATUS_ERROR;
25612626
}
2627+
#endif
2628+
2629+
/*
2630+
* If we're using SRV records, we don't have a server name so we'll just
2631+
* show an empty string in error messages.
2632+
*/
2633+
server_name = port->hba->ldapserver ? port->hba->ldapserver : "";
25622634

25632635
if (port->hba->ldapport == 0)
25642636
{
@@ -2630,7 +2702,7 @@ CheckLDAPAuth(Port *port)
26302702
ereport(LOG,
26312703
(errmsg("could not perform initial LDAP bind for ldapbinddn \"%s\" on server \"%s\": %s",
26322704
port->hba->ldapbinddn ? port->hba->ldapbinddn : "",
2633-
port->hba->ldapserver,
2705+
server_name,
26342706
ldap_err2string(r)),
26352707
errdetail_for_ldap(ldap)));
26362708
ldap_unbind(ldap);
@@ -2658,7 +2730,7 @@ CheckLDAPAuth(Port *port)
26582730
{
26592731
ereport(LOG,
26602732
(errmsg("could not search LDAP for filter \"%s\" on server \"%s\": %s",
2661-
filter, port->hba->ldapserver, ldap_err2string(r)),
2733+
filter, server_name, ldap_err2string(r)),
26622734
errdetail_for_ldap(ldap)));
26632735
ldap_unbind(ldap);
26642736
pfree(passwd);
@@ -2673,14 +2745,14 @@ CheckLDAPAuth(Port *port)
26732745
ereport(LOG,
26742746
(errmsg("LDAP user \"%s\" does not exist", port->user_name),
26752747
errdetail("LDAP search for filter \"%s\" on server \"%s\" returned no entries.",
2676-
filter, port->hba->ldapserver)));
2748+
filter, server_name)));
26772749
else
26782750
ereport(LOG,
26792751
(errmsg("LDAP user \"%s\" is not unique", port->user_name),
26802752
errdetail_plural("LDAP search for filter \"%s\" on server \"%s\" returned %d entry.",
26812753
"LDAP search for filter \"%s\" on server \"%s\" returned %d entries.",
26822754
count,
2683-
filter, port->hba->ldapserver, count)));
2755+
filter, server_name, count)));
26842756

26852757
ldap_unbind(ldap);
26862758
pfree(passwd);
@@ -2698,7 +2770,7 @@ CheckLDAPAuth(Port *port)
26982770
(void) ldap_get_option(ldap, LDAP_OPT_ERROR_NUMBER, &error);
26992771
ereport(LOG,
27002772
(errmsg("could not get dn for the first entry matching \"%s\" on server \"%s\": %s",
2701-
filter, port->hba->ldapserver,
2773+
filter, server_name,
27022774
ldap_err2string(error)),
27032775
errdetail_for_ldap(ldap)));
27042776
ldap_unbind(ldap);
@@ -2719,7 +2791,7 @@ CheckLDAPAuth(Port *port)
27192791
{
27202792
ereport(LOG,
27212793
(errmsg("could not unbind after searching for user \"%s\" on server \"%s\"",
2722-
fulluser, port->hba->ldapserver)));
2794+
fulluser, server_name)));
27232795
pfree(passwd);
27242796
pfree(fulluser);
27252797
return STATUS_ERROR;
@@ -2750,7 +2822,7 @@ CheckLDAPAuth(Port *port)
27502822
{
27512823
ereport(LOG,
27522824
(errmsg("LDAP login failed for user \"%s\" on server \"%s\": %s",
2753-
fulluser, port->hba->ldapserver, ldap_err2string(r)),
2825+
fulluser, server_name, ldap_err2string(r)),
27542826
errdetail_for_ldap(ldap)));
27552827
ldap_unbind(ldap);
27562828
pfree(passwd);

src/backend/libpq/hba.c

+3
Original file line numberDiff line numberDiff line change
@@ -1500,7 +1500,10 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
15001500
*/
15011501
if (parsedline->auth_method == uaLDAP)
15021502
{
1503+
#ifndef HAVE_LDAP_INITIALIZE
1504+
/* Not mandatory for OpenLDAP, because it can use DNS SRV records */
15031505
MANDATORY_AUTH_ARG(parsedline->ldapserver, "ldapserver", "ldap");
1506+
#endif
15041507

15051508
/*
15061509
* LDAP can operate in two modes: either with a direct bind, using

0 commit comments

Comments
 (0)