On Wed, Nov 7, 2018 at 4:39 PM Thomas Munro <thomas.mu...@enterprisedb.com> wrote: > On Tue, Sep 25, 2018 at 2:09 PM Thomas Munro > <thomas.mu...@enterprisedb.com> wrote: > > Some people like to use DNS SRV records to advertise LDAP servers on > > their network. Microsoft Active Directory is usually (always?) set up > > that way. Here is a patch to allow our LDAP auth module to support > > that kind of discovery.
Rebased. I took the liberty of CCing Mark Cave-Ayland, who had some great advice on the last round of LDAP feature tweaks[1]. Mark, if you have any comments on the sanity of this proposal, they'd be much appreciated, otherwise of course please feel free to ignore. Thanks! [1] https://www.postgresql.org/message-id/flat/CAEepm%3D0XTkYvMci0WRubZcf_1am8%3DgP%3D7oJErpsUfRYcKF2gwg%40mail.gmail.com -- Thomas Munro http://www.enterprisedb.com
From 4be9142998e76614146f88c825179264573bcf7c Mon Sep 17 00:00:00 2001 From: Thomas Munro <thomas.munro@enterprisedb.com> Date: Fri, 16 Nov 2018 14:32:00 +1300 Subject: [PATCH] Add DNS SRV support for LDAP server discovery. LDAP servers can be advertised on a network by registering DNS SRV records for _ldap._tcp.<domain>. The OpenLDAP command-line tools know how to find servers via those records, if no server name is provided by the user. Teach PostgreSQL to follow the same convention using non-standard extensions provided by OpenLDAP, where available. Author: Thomas Munro Reviewed-by: Discussion: https://postgr.es/m/CAEepm=2hAnSfhdsd6vXsM6VZVN0br-FbAZ-O+Swk18S5HkCP=A@mail.gmail.com --- doc/src/sgml/client-auth.sgml | 19 ++++++ src/backend/libpq/auth.c | 113 +++++++++++++++++++++++++--------- src/backend/libpq/hba.c | 2 + 3 files changed, 105 insertions(+), 29 deletions(-) diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml index c2114021c3..f9e7416c79 100644 --- a/doc/src/sgml/client-auth.sgml +++ b/doc/src/sgml/client-auth.sgml @@ -1671,6 +1671,16 @@ ldap[s]://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<rep <literal>ldapsearchattribute=uid</literal>. </para> + <para> + If <productname>PostgreSQL</productname> was compiled with OpenLDAP as + the LDAP client library, the <literal>ldapserver</literal> setting may be + omitted. In that case, the hostname and port are looked up via DNS + service records. The "SRV" records for the service + <literal>_ldap._tcp.domain</literal> are requested, where + <literal>domain</literal> is extracted from <literal>basedn</literal>. + This follows a convention used by OpenLDAP command-line tools. + </para> + <para> Here is an example for a simple-bind LDAP configuration: <programlisting> @@ -1716,6 +1726,15 @@ host ... ldap ldapserver=ldap.example.net ldapbasedn="dc=example, dc=net" ldapse </programlisting> </para> + <para> + Here is an example for a search+bind configuration that uses DNS SRV + discovery to find the hostname and port for the LDAP service using the + domain name <literal>example.net</literal>": +<programlisting> +host ... ldap ldapurl="ldap:///ou=people,dc=example,dc=net?cn" +</programlisting> + </para> + <tip> <para> 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 4f9d697d6d..cb62540c9f 100644 --- a/src/backend/libpq/auth.c +++ b/src/backend/libpq/auth.c @@ -2356,37 +2356,81 @@ InitializeLDAPConnection(Port *port, LDAP **ldap) char *uris = NULL; /* - * We have a space-separated list of hostnames. Convert it - * to a space-separated list of URIs. + * If the user provided no hostname, we can ask OpenLDAP to try to + * find one by extracting a domain name from the base DN and then + * using a DSN SRV record for _ldap._tcp.<domain>. If one or more + * such SRV records have been defined, we can get a hostname and + * port. The same convention is used by the OpenLDAP command line + * tools. */ - do + if (!hostnames || hostnames[0] == '\0') { - 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); + char *domain; + char *hostlist; + char *end; + + /* ou=blah,dc=foo,dc=bar -> foo.bar */ + if (ldap_dn2domain(port->hba->ldapbasedn, &domain)) + { + ereport(LOG, + (errmsg("could not extract domain name from basedn"))); + return STATUS_ERROR; + } + /* Look up host:port using DNS SRV for _ldap._tcp.foo.bar */ + if (ldap_domain2hostlist(domain, &hostlist)) + { + ereport(LOG, + (errmsg("could not look up a hostlist for %s", + domain))); + ldap_memfree(domain); + return STATUS_ERROR; + } + ldap_memfree(domain); + /* + * OpenLDAP already ordered by weight and shuffled equal weight + * servers, so we'll just take the first one. The string is + * of the format "host:port", separated by spaces. + */ + if ((end = strchr(hostlist, ' '))) + *end = '\0'; + uris = psprintf("%s://%s", scheme, hostlist); + ldap_memfree(hostlist); + } + else + { + /* + * We have a space-separated list of hostnames. Convert it + * to a space-separated list of 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); @@ -2536,12 +2580,23 @@ CheckLDAPAuth(Port *port) int r; char *fulluser; +#ifdef HAVE_LDAP_INITIALIZE + /* OpenLDAP allows empty hostname, if we have a basedn. */ + 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 (port->hba->ldapport == 0) { diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c index 0129dd24d0..ab4cc99001 100644 --- a/src/backend/libpq/hba.c +++ b/src/backend/libpq/hba.c @@ -1500,7 +1500,9 @@ parse_hba_line(TokenizedLine *tok_line, int elevel) */ if (parsedline->auth_method == uaLDAP) { +#ifndef HAVE_LDAP_INITIALIZE MANDATORY_AUTH_ARG(parsedline->ldapserver, "ldapserver", "ldap"); +#endif /* * LDAP can operate in two modes: either with a direct bind, using -- 2.19.1