More flexible LDAP auth search filters?
Hi hackers,
A customer asked how to use pg_hba.conf LDAP search+bind
authentication to restrict logins to users in one of a small number of
groups. ldapsearchattribute only lets you make filters like
"(foo=username)", so it couldn't be done. Is there any reason we
should allow a more general kind of search filter constructions?
A post on planet.postgresql.org today reminded me that a colleague had
asked me to post this POC patch here for discussion. It allows custom
filters with ldapsearchprefix and ldapsearchsuffix. Another approach
might be to take a filter pattern with "%USERNAME%" or whatever in it.
There's an existing precedent for the prefix and suffix approach, but
on the other hand a pattern approach would allow filters where the
username is inserted more than once.
Motivating example:
ldapsearchprefix="(&(cn="
ldapsearchsuffix = ")(|(memberof=cn=Paris DBA
Team)(memberof=cn=Tokyo DBA Team))"
Note that with this patch ldapsearchattribute=cn is equivalent to:
ldasearchprefix="(cn="
ldapsearchsuffix=")"
Perhaps there are better ways to organise your LDAP servers so that
this sort of thing isn't necessary. I don't know. Thoughts?
--
Thomas Munro
http://www.enterprisedb.com
Attachments:
ldap-search-filters-v1.patchapplication/octet-stream; name=ldap-search-filters-v1.patchDownload
From e7194dc73ad65488233981652da57f87d3b9357a Mon Sep 17 00:00:00 2001
From: Thomas Munro <munro@ip9.org>
Date: Thu, 13 Jul 2017 19:23:06 +1200
Subject: [PATCH] Allow custom search filters to be configured for LDAP auth.
Before, only filters of the form "(<ldapsearchattribute>=<user>)"
could be used to search an LDAP server. Allow filters to be built
from prefix + user + suffix so that extra restrictions can be placed
on the set of LDAP entries that can be used to log into PostgreSQL.
---
doc/src/sgml/client-auth.sgml | 30 +++++++++++++++++++++
src/backend/libpq/auth.c | 11 +++++---
src/backend/libpq/hba.c | 61 ++++++++++++++++++++++++++++++++++++++++---
src/include/libpq/hba.h | 2 ++
4 files changed, 97 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 819db811b2..825c89ebe3 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -1508,6 +1508,25 @@ omicron bryanh guest1
</listitem>
</varlistentry>
<varlistentry>
+ <term><literal>ldapsearchprefix</literal></term>
+ <listitem>
+ <para>
+ The prefix of a search filter used to search for the user name
+ when doing search+bind authentication. This allows for more general
+ search filters than <literal>ldapsearchattribute</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><literal>ldapsearchsuffix</literal></term>
+ <listitem>
+ <para>
+ The suffix of a search filter used to search for the user name
+ when doing search+bind authentication.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
<term><literal>ldapurl</literal></term>
<listitem>
<para>
@@ -1550,6 +1569,17 @@ ldap://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replac
</para>
<para>
+ When using search+bind mode, the search can be performed either with a
+ single attribute specified with <literal>ldapsearchattribute</literal>,
+ or with a custom search filter specified with
+ <literal>ldapsearchprefix</literal> and <literal>ldapsearchsuffix</literal>.
+ Specifying <literal>ldapsearchattribute=foo</literal> is equivalent
+ to specifying <literal>ldapsearchprefix="(foo=" ldapsearchsuffix=")"</literal>.
+ If none of these options are specified explicitly, the default is
+ <literal>ldapsearchattribute=uid</literal>.
+ </para>
+
+ <para>
Here is an example for a simple-bind LDAP configuration:
<programlisting>
host ... ldap ldapserver=ldap.example.net ldapprefix="cn=" ldapsuffix=", dc=example, dc=net"
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index dd7de7c3a4..c3c9008c8c 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2471,9 +2471,14 @@ CheckLDAPAuth(Port *port)
attributes[0] = port->hba->ldapsearchattribute ? port->hba->ldapsearchattribute : "uid";
attributes[1] = NULL;
- filter = psprintf("(%s=%s)",
- attributes[0],
- port->user_name);
+ /* Build an entirely custom filter or a single attribute filter? */
+ if (port->hba->ldapsearchprefix != NULL)
+ filter = psprintf("%s%s%s",
+ port->hba->ldapsearchprefix,
+ port->user_name,
+ port->hba->ldapsearchsuffix);
+ else
+ filter = psprintf("(%s=%s)", attributes[0], port->user_name);
r = ldap_search_s(ldap,
port->hba->ldapbasedn,
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 42afead9fd..92844edb18 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -1505,7 +1505,8 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
/*
* 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.
+ * ldapbasedn, ldapbinddn, ldapbindpasswd and either
+ * ldapsearchattribute or ldapsearchprefix and ldapsearchsuffix.
* Disallow mixing these parameters.
*/
if (parsedline->ldapprefix || parsedline->ldapsuffix)
@@ -1513,14 +1514,16 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
if (parsedline->ldapbasedn ||
parsedline->ldapbinddn ||
parsedline->ldapbindpasswd ||
- parsedline->ldapsearchattribute)
+ parsedline->ldapsearchattribute ||
+ parsedline->ldapsearchprefix ||
+ parsedline->ldapsearchsuffix)
{
ereport(elevel,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
- errmsg("cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, or ldapurl together with ldapprefix"),
+ errmsg("cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchprefix, ldapsearchsuffix or ldapurl together with ldapprefix"),
errcontext("line %d of configuration file \"%s\"",
line_num, HbaFileName)));
- *err_msg = "cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, or ldapurl together with ldapprefix";
+ *err_msg = "cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchprefix or ldapsearchsuffix or ldapurl together with ldapprefix";
return NULL;
}
}
@@ -1534,6 +1537,36 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
*err_msg = "authentication method \"ldap\" requires argument \"ldapbasedn\", \"ldapprefix\", or \"ldapsuffix\" to be set";
return NULL;
}
+
+ /*
+ * When using search+bind, you can either user a simple attribute
+ * defaulting to "uid" or a fully custom search filter. You can't
+ * do both.
+ */
+ if (parsedline->ldapsearchattribute &&
+ (parsedline->ldapsearchprefix || parsedline->ldapsearchsuffix))
+ {
+ ereport(elevel,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("cannot use ldapsearchattribute together with ldapsearchprefix and ldapsearchsuffix"),
+ errcontext("line %d of configuration file \"%s\"",
+ line_num, HbaFileName)));
+ *err_msg = "cannot use ldapsearchattribute together with ldapsearchprefix and ldapsearchsuffix";
+ return NULL;
+ }
+
+ /* If using a fully custom filter, you need to supply both ends. */
+ if ((parsedline->ldapsearchprefix != NULL) !=
+ (parsedline->ldapsearchsuffix != NULL))
+ {
+ ereport(elevel,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("ldapsearchprefix and ldapsearchsuffix must be specified together"),
+ errcontext("line %d of configuration file \"%s\"",
+ line_num, HbaFileName)));
+ *err_msg = "ldapsearchprefix and ldapsearchsuffix must be specified together";
+ return NULL;
+ }
}
if (parsedline->auth_method == uaRADIUS)
@@ -1788,6 +1821,16 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
REQUIRE_AUTH_OPTION(uaLDAP, "ldapsearchattribute", "ldap");
hbaline->ldapsearchattribute = pstrdup(val);
}
+ else if (strcmp(name, "ldapsearchprefix") == 0)
+ {
+ REQUIRE_AUTH_OPTION(uaLDAP, "ldapsearchprefix", "ldap");
+ hbaline->ldapsearchprefix = pstrdup(val);
+ }
+ else if (strcmp(name, "ldapsearchsuffix") == 0)
+ {
+ REQUIRE_AUTH_OPTION(uaLDAP, "ldapsearchsuffix", "ldap");
+ hbaline->ldapsearchsuffix = pstrdup(val);
+ }
else if (strcmp(name, "ldapbasedn") == 0)
{
REQUIRE_AUTH_OPTION(uaLDAP, "ldapbasedn", "ldap");
@@ -2266,6 +2309,16 @@ gethba_options(HbaLine *hba)
CStringGetTextDatum(psprintf("ldapsearchattribute=%s",
hba->ldapsearchattribute));
+ if (hba->ldapsearchprefix)
+ options[noptions++] =
+ CStringGetTextDatum(psprintf("ldapsearchprefix=%s",
+ hba->ldapsearchprefix));
+
+ if (hba->ldapsearchsuffix)
+ options[noptions++] =
+ CStringGetTextDatum(psprintf("ldapsearchsuffix=%s",
+ hba->ldapsearchsuffix));
+
if (hba->ldapscope)
options[noptions++] =
CStringGetTextDatum(psprintf("ldapscope=%d", hba->ldapscope));
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 07d92d4f9f..c14322ffe6 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -80,6 +80,8 @@ typedef struct HbaLine
char *ldapbinddn;
char *ldapbindpasswd;
char *ldapsearchattribute;
+ char *ldapsearchprefix;
+ char *ldapsearchsuffix;
char *ldapbasedn;
int ldapscope;
char *ldapprefix;
--
2.13.2
On Thu, Jul 13, 2017 at 9:31 AM, Thomas Munro <thomas.munro@enterprisedb.com
wrote:
Hi hackers,
A customer asked how to use pg_hba.conf LDAP search+bind
authentication to restrict logins to users in one of a small number of
groups. ldapsearchattribute only lets you make filters like
"(foo=username)", so it couldn't be done. Is there any reason we
should allow a more general kind of search filter constructions?A post on planet.postgresql.org today reminded me that a colleague had
asked me to post this POC patch here for discussion. It allows custom
filters with ldapsearchprefix and ldapsearchsuffix. Another approach
might be to take a filter pattern with "%USERNAME%" or whatever in it.
There's an existing precedent for the prefix and suffix approach, but
on the other hand a pattern approach would allow filters where the
username is inserted more than once.
Do we even need prefix/suffix? If we just make it "ldapsearchpattern", then
you could have something like:
ldapsearchattribute="uid"
ldapsearchfilter="|(memberof=cn=Paris DBA Team)(memberof=cn=Tokyo DBA Team)"
We could then always to substitution of the kind:
(&(attr=<uid>)(<filter>))
which would in this case give:
(&(uid=mha)(|(memberof=cn=Paris DBA Team)(memberof=cn=Tokyo DBA Team)))
Basically we'd always AND together the username lookup with the additional
filter.
Perhaps there are better ways to organise your LDAP servers so that
this sort of thing isn't necessary. I don't know. Thoughts?
I think something along this way is definitely wanted. We can argue the
syntax, but being able to filter like this is definitely useful.
(FWIW, a workaround I've applied more than once to this in AD environments
(where kerberos for one reason or other can't be done, sorry Stephen) is to
set up a RADIUS server and use that one as a "middle man". But it would be
much better if we could do it natively)
--
Magnus Hagander
Me: https://www.hagander.net/ <http://www.hagander.net/>
Work: https://www.redpill-linpro.com/ <http://www.redpill-linpro.com/>
On Fri, Jul 14, 2017 at 11:04 PM, Magnus Hagander <magnus@hagander.net> wrote:
On Thu, Jul 13, 2017 at 9:31 AM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:A post on planet.postgresql.org today reminded me that a colleague had
asked me to post this POC patch here for discussion. It allows custom
filters with ldapsearchprefix and ldapsearchsuffix. Another approach
might be to take a filter pattern with "%USERNAME%" or whatever in it.
There's an existing precedent for the prefix and suffix approach, but
on the other hand a pattern approach would allow filters where the
username is inserted more than once.Do we even need prefix/suffix? If we just make it "ldapsearchpattern", then
you could have something like:ldapsearchattribute="uid"
ldapsearchfilter="|(memberof=cn=Paris DBA Team)(memberof=cn=Tokyo DBA Team)"We could then always to substitution of the kind:
(&(attr=<uid>)(<filter>))which would in this case give:
(&(uid=mha)(|(memberof=cn=Paris DBA Team)(memberof=cn=Tokyo DBA Team)))Basically we'd always AND together the username lookup with the additional
filter.
Ok, so we have 3 ideas put forward:
1. Wrap username with ldapsearchprefix ldapsearchsuffix to build
filter (as implemented by POC patch)
2. Optionally AND ldapsearchfilter with the existing
ldapsearchattribute-based filter (Magnus's proposal)
3. Pattern-based ldapsearchfilter so that %USER% is replaced with
username (my other suggestion)
The main argument for approach 1 is that it follows the style of the
bind-only mode.
With idea 2, I wonder if there are some more general kinds of things
that people might want to do that that wouldn't be possible because it
has to include (attribute=user)... perhaps something involving a
substring or other transformation functions (but I'm no LDAP expert,
that may not make sense).
With idea 3 it would allow "(|(foo=%USER%)(bar=%USER%))", though I
don't know if any such multiple-mention filters would ever make sense
in a sane LDAP configuration.
Any other views from LDAP-users?
--
Thomas Munro
http://www.enterprisedb.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Sun, Jul 16, 2017 at 1:08 AM, Thomas Munro <thomas.munro@enterprisedb.com
wrote:
On Fri, Jul 14, 2017 at 11:04 PM, Magnus Hagander <magnus@hagander.net>
wrote:On Thu, Jul 13, 2017 at 9:31 AM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:A post on planet.postgresql.org today reminded me that a colleague had
asked me to post this POC patch here for discussion. It allows custom
filters with ldapsearchprefix and ldapsearchsuffix. Another approach
might be to take a filter pattern with "%USERNAME%" or whatever in it.
There's an existing precedent for the prefix and suffix approach, but
on the other hand a pattern approach would allow filters where the
username is inserted more than once.Do we even need prefix/suffix? If we just make it "ldapsearchpattern",
then
you could have something like:
ldapsearchattribute="uid"
ldapsearchfilter="|(memberof=cn=Paris DBA Team)(memberof=cn=Tokyo DBATeam)"
We could then always to substitution of the kind:
(&(attr=<uid>)(<filter>))which would in this case give:
(&(uid=mha)(|(memberof=cn=Paris DBA Team)(memberof=cn=Tokyo DBA Team)))Basically we'd always AND together the username lookup with the
additional
filter.
Ok, so we have 3 ideas put forward:
1. Wrap username with ldapsearchprefix ldapsearchsuffix to build
filter (as implemented by POC patch)
2. Optionally AND ldapsearchfilter with the existing
ldapsearchattribute-based filter (Magnus's proposal)
3. Pattern-based ldapsearchfilter so that %USER% is replaced with
username (my other suggestion)The main argument for approach 1 is that it follows the style of the
bind-only mode.
Agreed, but I'm not sure it's a good style to follow (and yes, I think I
may be the original author of it..). I'd rank option 3 over option 1.
With idea 2, I wonder if there are some more general kinds of things
that people might want to do that that wouldn't be possible because it
has to include (attribute=user)... perhaps something involving a
substring or other transformation functions (but I'm no LDAP expert,
that may not make sense).
Yeah, that's exactly what I'm wondering about it :)
With idea 3 it would allow "(|(foo=%USER%)(bar=%USER%))", though I
don't know if any such multiple-mention filters would ever make sense
in a sane LDAP configuration.Any other views from LDAP-users?
+1 for some input from people who directly use it in larger LDAP
environments. If we're going to change how it works, let's make it right
this time!
--
Magnus Hagander
Me: https://www.hagander.net/ <http://www.hagander.net/>
Work: https://www.redpill-linpro.com/ <http://www.redpill-linpro.com/>
On 16/07/17 00:08, Thomas Munro wrote:
On Fri, Jul 14, 2017 at 11:04 PM, Magnus Hagander <magnus@hagander.net> wrote:
On Thu, Jul 13, 2017 at 9:31 AM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:A post on planet.postgresql.org today reminded me that a colleague had
asked me to post this POC patch here for discussion. It allows custom
filters with ldapsearchprefix and ldapsearchsuffix. Another approach
might be to take a filter pattern with "%USERNAME%" or whatever in it.
There's an existing precedent for the prefix and suffix approach, but
on the other hand a pattern approach would allow filters where the
username is inserted more than once.Do we even need prefix/suffix? If we just make it "ldapsearchpattern", then
you could have something like:ldapsearchattribute="uid"
ldapsearchfilter="|(memberof=cn=Paris DBA Team)(memberof=cn=Tokyo DBA Team)"We could then always to substitution of the kind:
(&(attr=<uid>)(<filter>))which would in this case give:
(&(uid=mha)(|(memberof=cn=Paris DBA Team)(memberof=cn=Tokyo DBA Team)))Basically we'd always AND together the username lookup with the additional
filter.Ok, so we have 3 ideas put forward:
1. Wrap username with ldapsearchprefix ldapsearchsuffix to build
filter (as implemented by POC patch)
2. Optionally AND ldapsearchfilter with the existing
ldapsearchattribute-based filter (Magnus's proposal)
3. Pattern-based ldapsearchfilter so that %USER% is replaced with
username (my other suggestion)The main argument for approach 1 is that it follows the style of the
bind-only mode.With idea 2, I wonder if there are some more general kinds of things
that people might want to do that that wouldn't be possible because it
has to include (attribute=user)... perhaps something involving a
substring or other transformation functions (but I'm no LDAP expert,
that may not make sense).With idea 3 it would allow "(|(foo=%USER%)(bar=%USER%))", though I
don't know if any such multiple-mention filters would ever make sense
in a sane LDAP configuration.Any other views from LDAP-users?
I've spent quite a bit of time integrating various bits of
non-PostgreSQL software to LDAP and in my experience option 3 tends to
be the standard.
Generally you find that you will be given the option to set the
attribute for the default search filter of the form
"(attribute=username)" which defaults to uid for UNIX-type systems and
sAMAccountName for AD. However there is always the ability to specify a
custom filter where the user is substituted via e.g. %u to cover all the
other use-cases.
As an example, I don't know if anyone would actually do this with
PostgreSQL but I've been asked on multiple occasions to configure
software so that users should be allowed to log in with either their
email address or username which is easily done with a custom LDAP filter
like "(|(mail=%u)(uid=%u))".
ATB,
Mark.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Magnus, all,
* Magnus Hagander (magnus@hagander.net) wrote:
(FWIW, a workaround I've applied more than once to this in AD environments
(where kerberos for one reason or other can't be done, sorry Stephen) is to
set up a RADIUS server and use that one as a "middle man". But it would be
much better if we could do it natively)
I'd suggest that we try to understand why Kerberos couldn't be used in
that environment. I suspect in at least some cases what users would
like is the ability to do Kerberos auth but then have LDAP checked to
see if a given user (who has now auth'd through Kerberos) is allowed to
connect. We don't currently have any way to do that, but if we were
looking for things to do, that's what I'd suggest working on rather than
adding more to our LDAP auth system and implying by doing so that it's
reasonable to use.
I find it particularly disappointing to see recommendations for using
LDAP auth, particularly in AD environments, that don't even mention
Kerberos or bother to explain how using LDAP sends the user's PW to the
server in cleartext.
Thanks!
Stephen
On Sun, Jul 16, 2017 at 11:05 PM, Stephen Frost <sfrost@snowman.net> wrote:
Magnus, all,
* Magnus Hagander (magnus@hagander.net) wrote:
(FWIW, a workaround I've applied more than once to this in AD
environments
(where kerberos for one reason or other can't be done, sorry Stephen) is
to
set up a RADIUS server and use that one as a "middle man". But it would
be
much better if we could do it natively)
I'd suggest that we try to understand why Kerberos couldn't be used in
that environment. I suspect in at least some cases what users would
like is the ability to do Kerberos auth but then have LDAP checked to
see if a given user (who has now auth'd through Kerberos) is allowed to
connect. We don't currently have any way to do that, but if we were
looking for things to do, that's what I'd suggest working on rather than
adding more to our LDAP auth system and implying by doing so that it's
reasonable to use.I find it particularly disappointing to see recommendations for using
LDAP auth, particularly in AD environments, that don't even mention
Kerberos or bother to explain how using LDAP sends the user's PW to the
server in cleartext.
You do realize, I'm sure, that there are many LDAP servers out there that
are not AD, and that do not come with a Kerberos server attached to them...
I agree that Kerberos is usually the better choice *if it's available*.
It's several orders of magnitude more complicated to set up though, and
there are many environments that have ldap but don't have Kerberos.
Refusing to improve LDAP for the users who have no choice seems like a very
unfriendly thing to do.
(And you can actually reasonably solve the case of
kerberos-for-auth-ldap-for-priv by syncing the groups into postgres roles)
--
Magnus Hagander
Me: https://www.hagander.net/ <http://www.hagander.net/>
Work: https://www.redpill-linpro.com/ <http://www.redpill-linpro.com/>
On Mon, Jul 17, 2017 at 5:58 AM, Mark Cave-Ayland
<mark.cave-ayland@ilande.co.uk> wrote:
Any other views from LDAP-users?
I've spent quite a bit of time integrating various bits of
non-PostgreSQL software to LDAP and in my experience option 3 tends to
be the standard.Generally you find that you will be given the option to set the
attribute for the default search filter of the form
"(attribute=username)" which defaults to uid for UNIX-type systems and
sAMAccountName for AD. However there is always the ability to specify a
custom filter where the user is substituted via e.g. %u to cover all the
other use-cases.
Cool. Here is a new version of the patch updated to do it exactly
like that. I tested it against OpenLDAP.
As an example, I don't know if anyone would actually do this with
PostgreSQL but I've been asked on multiple occasions to configure
software so that users should be allowed to log in with either their
email address or username which is easily done with a custom LDAP filter
like "(|(mail=%u)(uid=%u))".
Thank you very much for this feedback and example, which I used in the
documentation in the patch. I see similar examples in the
documentation for other things on the web.
I'll leave it up to Magnus and Stephen to duke it out over whether we
want to encourage LDAP usage, extend documentation to warn about
cleartext passwords with certain LDAP implementations or
configurations, etc etc. I'll add this patch to the commitfest and
get some popcorn.
--
Thomas Munro
http://www.enterprisedb.com
Attachments:
ldap-search-filters-v2.patchapplication/octet-stream; name=ldap-search-filters-v2.patchDownload
From 0b86ecf4c3b5aa5559b36a201bc91d9bdc8b7e78 Mon Sep 17 00:00:00 2001
From: Thomas Munro <thomas.munro@enterprisedb.com>
Date: Mon, 17 Jul 2017 10:11:28 +1200
Subject: [PATCH] Allow custom search filters to be configured for LDAP auth.
Before, only filters of the form "(<ldapsearchattribute>=<user>)"
could be used to search an LDAP server. Introduce ldapsearchfilter
so that more general filters can be configured using patterns like
"(|(uid=%u)(mail=%u))" and "(&(uid=%u)(objectClass=posixAccount))".
---
doc/src/sgml/client-auth.sgml | 32 ++++++++++++++++++++++++
src/backend/libpq/auth.c | 57 ++++++++++++++++++++++++++++++++++++++++---
src/backend/libpq/hba.c | 38 +++++++++++++++++++++++++----
src/include/libpq/hba.h | 1 +
4 files changed, 120 insertions(+), 8 deletions(-)
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 819db811b2..e599357ffa 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -1508,6 +1508,17 @@ omicron bryanh guest1
</listitem>
</varlistentry>
<varlistentry>
+ <term><literal>ldapsearchfilter</literal></term>
+ <listitem>
+ <para>
+ The search filter to use when doing search+bind authentication.
+ Occurences of <literal>%u</literal> will be replaced with the
+ user name. This allows for more flexible search filters than
+ <literal>ldapsearchattribute</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
<term><literal>ldapurl</literal></term>
<listitem>
<para>
@@ -1550,6 +1561,17 @@ ldap://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replac
</para>
<para>
+ When using search+bind mode, the search can be performed using a single
+ attribute specified with <literal>ldapsearchattribute</literal>, or using
+ a custom search filter specified with
+ <literal>ldapsearchfilter</literal>.
+ Specifying <literal>ldapsearchattribute=foo</literal> is equivalent to
+ specifying <literal>ldapsearchfilter="(foo=%u)"</literal>. If neither
+ option is specified the default is
+ <literal>ldapsearchattribute=uid</literal>.
+ </para>
+
+ <para>
Here is an example for a simple-bind LDAP configuration:
<programlisting>
host ... ldap ldapserver=ldap.example.net ldapprefix="cn=" ldapsuffix=", dc=example, dc=net"
@@ -1584,6 +1606,16 @@ host ... ldap ldapurl="ldap://ldap.example.net/dc=example,dc=net?uid?sub"
same URL format, so it will be easier to share the configuration.
</para>
+ <para>
+ Here is an example for a search+bind configuration that uses
+ <literal>ldapsearchfilter</literal> instead of
+ <literal>ldapsearchattribute</literal> to allow authentication by
+ user ID or email address:
+<programlisting>
+host ... ldap ldapserver=ldap.example.net ldapbasedn="dc=example, dc=net" ldapsearchfilter="(|(uid=%u)(mail=%u))"
+</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 dd7de7c3a4..d062ebfcba 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2381,6 +2381,55 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
}
/*
+ * Return a newly allocated C string copied from "pattern" with all occurences
+ * of "%u" replaced with "user_name".
+ */
+static char *
+FormatSearchFilter(const char *pattern, const char *user_name)
+{
+ Size user_name_size = strlen(user_name);
+ Size buffer_size = 0;
+ const char *in;
+ char *out;
+ char *result;
+
+ /* Compute the size of the output buffer. */
+ in = pattern;
+ while (*in != '\0')
+ {
+ if (in[0] == '%' && in[1] == 'u')
+ {
+ buffer_size += user_name_size;
+ in += 2;
+ }
+ else
+ {
+ buffer_size++;
+ in++;
+ }
+ }
+ buffer_size++; /* terminator */
+
+ /* Build the output string. */
+ out = result = palloc(buffer_size);
+ in = pattern;
+ while (*in != '\0')
+ {
+ if (in[0] == '%' && in[1] == 'u')
+ {
+ memcpy(out, user_name, user_name_size);
+ out += user_name_size;
+ in += 2;
+ }
+ else
+ *out++ = *in++;
+ }
+ *out = '\0'; /* terminator */
+
+ return result;
+}
+
+/*
* Perform LDAP authentication
*/
static int
@@ -2471,9 +2520,11 @@ CheckLDAPAuth(Port *port)
attributes[0] = port->hba->ldapsearchattribute ? port->hba->ldapsearchattribute : "uid";
attributes[1] = NULL;
- filter = psprintf("(%s=%s)",
- attributes[0],
- port->user_name);
+ /* Build a custom filter or a single attribute filter? */
+ if (port->hba->ldapsearchfilter != NULL)
+ filter = FormatSearchFilter(port->hba->ldapsearchfilter, port->user_name);
+ else
+ filter = psprintf("(%s=%s)", attributes[0], port->user_name);
r = ldap_search_s(ldap,
port->hba->ldapbasedn,
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 42afead9fd..4ae10f6d02 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -1505,22 +1505,24 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
/*
* 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.
+ * ldapbasedn, ldapbinddn, ldapbindpasswd and one of
+ * ldapsearchattribute or ldapsearchfilter. Disallow mixing these
+ * parameters.
*/
if (parsedline->ldapprefix || parsedline->ldapsuffix)
{
if (parsedline->ldapbasedn ||
parsedline->ldapbinddn ||
parsedline->ldapbindpasswd ||
- parsedline->ldapsearchattribute)
+ parsedline->ldapsearchattribute ||
+ parsedline->ldapsearchfilter)
{
ereport(elevel,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
- errmsg("cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, or ldapurl together with ldapprefix"),
+ errmsg("cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter or ldapurl together with ldapprefix"),
errcontext("line %d of configuration file \"%s\"",
line_num, HbaFileName)));
- *err_msg = "cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, or ldapurl together with ldapprefix";
+ *err_msg = "cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter or ldapurl together with ldapprefix";
return NULL;
}
}
@@ -1534,6 +1536,22 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
*err_msg = "authentication method \"ldap\" requires argument \"ldapbasedn\", \"ldapprefix\", or \"ldapsuffix\" to be set";
return NULL;
}
+
+ /*
+ * When using search+bind, you can either use a simple attribute
+ * (defaulting to "uid") or a fully custom search filter. You can't
+ * do both.
+ */
+ if (parsedline->ldapsearchattribute && parsedline->ldapsearchfilter)
+ {
+ ereport(elevel,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("cannot use ldapsearchattribute together with ldapsearchfilter"),
+ errcontext("line %d of configuration file \"%s\"",
+ line_num, HbaFileName)));
+ *err_msg = "cannot use ldapsearchattribute together with ldapsearchfilter";
+ return NULL;
+ }
}
if (parsedline->auth_method == uaRADIUS)
@@ -1788,6 +1806,11 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
REQUIRE_AUTH_OPTION(uaLDAP, "ldapsearchattribute", "ldap");
hbaline->ldapsearchattribute = pstrdup(val);
}
+ else if (strcmp(name, "ldapsearchfilter") == 0)
+ {
+ REQUIRE_AUTH_OPTION(uaLDAP, "ldapsearchfilter", "ldap");
+ hbaline->ldapsearchfilter = pstrdup(val);
+ }
else if (strcmp(name, "ldapbasedn") == 0)
{
REQUIRE_AUTH_OPTION(uaLDAP, "ldapbasedn", "ldap");
@@ -2266,6 +2289,11 @@ gethba_options(HbaLine *hba)
CStringGetTextDatum(psprintf("ldapsearchattribute=%s",
hba->ldapsearchattribute));
+ if (hba->ldapsearchfilter)
+ options[noptions++] =
+ CStringGetTextDatum(psprintf("ldapsearchfilter=%s",
+ hba->ldapsearchfilter));
+
if (hba->ldapscope)
options[noptions++] =
CStringGetTextDatum(psprintf("ldapscope=%d", hba->ldapscope));
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 07d92d4f9f..e711bee8bf 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -80,6 +80,7 @@ typedef struct HbaLine
char *ldapbinddn;
char *ldapbindpasswd;
char *ldapsearchattribute;
+ char *ldapsearchfilter;
char *ldapbasedn;
int ldapscope;
char *ldapprefix;
--
2.13.2
On 16/07/17 23:26, Thomas Munro wrote:
Thank you very much for this feedback and example, which I used in the
documentation in the patch. I see similar examples in the
documentation for other things on the web.I'll leave it up to Magnus and Stephen to duke it out over whether we
want to encourage LDAP usage, extend documentation to warn about
cleartext passwords with certain LDAP implementations or
configurations, etc etc. I'll add this patch to the commitfest and
get some popcorn.
If it helps, we normally recommend that clients use ldaps for both AD
and UNIX environments, although this can be trickier from an
administrative perspective in AD environments because it can require
changes to the Windows firewall and certificate installation.
Whilst OpenLDAP will support ldap+starttls you can end up with some
clients with starttls either disabled or misconfigured sending plaintext
passwords over the wire regardless, so it's generally easiest to
firewall ldap port 389 at the edge of the trusted VLAN so that only
ldaps port 636 connections make it out onto the untrusted network
hosting the local AD/OpenLDAP server.
ATB,
Mark.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Mon, Jul 17, 2017 at 10:26 AM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:
ldap-search-filters-v2.patch
Gah, it would help if I could spell "occurrences" correctly. Fixed in
the attached.
--
Thomas Munro
http://www.enterprisedb.com
Attachments:
ldap-search-filters-v3.patchapplication/octet-stream; name=ldap-search-filters-v3.patchDownload
From 11ec0288dbfbdc9601b7cfce121306dd13bfe31c Mon Sep 17 00:00:00 2001
From: Thomas Munro <thomas.munro@enterprisedb.com>
Date: Mon, 17 Jul 2017 10:11:28 +1200
Subject: [PATCH] Allow custom search filters to be configured for LDAP auth.
Before, only filters of the form "(<ldapsearchattribute>=<user>)"
could be used to search an LDAP server. Introduce ldapsearchfilter
so that more general filters can be configured using patterns like
"(|(uid=%u)(mail=%u))" and "(&(uid=%u)(objectClass=posixAccount))".
---
doc/src/sgml/client-auth.sgml | 32 ++++++++++++++++++++++++
src/backend/libpq/auth.c | 57 ++++++++++++++++++++++++++++++++++++++++---
src/backend/libpq/hba.c | 38 +++++++++++++++++++++++++----
src/include/libpq/hba.h | 1 +
4 files changed, 120 insertions(+), 8 deletions(-)
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 819db811b2..93945c5c4c 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -1508,6 +1508,17 @@ omicron bryanh guest1
</listitem>
</varlistentry>
<varlistentry>
+ <term><literal>ldapsearchfilter</literal></term>
+ <listitem>
+ <para>
+ The search filter to use when doing search+bind authentication.
+ Occurrences of <literal>%u</literal> will be replaced with the
+ user name. This allows for more flexible search filters than
+ <literal>ldapsearchattribute</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
<term><literal>ldapurl</literal></term>
<listitem>
<para>
@@ -1550,6 +1561,17 @@ ldap://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replac
</para>
<para>
+ When using search+bind mode, the search can be performed using a single
+ attribute specified with <literal>ldapsearchattribute</literal>, or using
+ a custom search filter specified with
+ <literal>ldapsearchfilter</literal>.
+ Specifying <literal>ldapsearchattribute=foo</literal> is equivalent to
+ specifying <literal>ldapsearchfilter="(foo=%u)"</literal>. If neither
+ option is specified the default is
+ <literal>ldapsearchattribute=uid</literal>.
+ </para>
+
+ <para>
Here is an example for a simple-bind LDAP configuration:
<programlisting>
host ... ldap ldapserver=ldap.example.net ldapprefix="cn=" ldapsuffix=", dc=example, dc=net"
@@ -1584,6 +1606,16 @@ host ... ldap ldapurl="ldap://ldap.example.net/dc=example,dc=net?uid?sub"
same URL format, so it will be easier to share the configuration.
</para>
+ <para>
+ Here is an example for a search+bind configuration that uses
+ <literal>ldapsearchfilter</literal> instead of
+ <literal>ldapsearchattribute</literal> to allow authentication by
+ user ID or email address:
+<programlisting>
+host ... ldap ldapserver=ldap.example.net ldapbasedn="dc=example, dc=net" ldapsearchfilter="(|(uid=%u)(mail=%u))"
+</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 dd7de7c3a4..e43818e91f 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2381,6 +2381,55 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
}
/*
+ * Return a newly allocated C string copied from "pattern" with all
+ * occurrences of "%u" replaced with "user_name".
+ */
+static char *
+FormatSearchFilter(const char *pattern, const char *user_name)
+{
+ Size user_name_size = strlen(user_name);
+ Size buffer_size = 0;
+ const char *in;
+ char *out;
+ char *result;
+
+ /* Compute the size of the output buffer. */
+ in = pattern;
+ while (*in != '\0')
+ {
+ if (in[0] == '%' && in[1] == 'u')
+ {
+ buffer_size += user_name_size;
+ in += 2;
+ }
+ else
+ {
+ buffer_size++;
+ in++;
+ }
+ }
+ buffer_size++; /* terminator */
+
+ /* Build the output string. */
+ out = result = palloc(buffer_size);
+ in = pattern;
+ while (*in != '\0')
+ {
+ if (in[0] == '%' && in[1] == 'u')
+ {
+ memcpy(out, user_name, user_name_size);
+ out += user_name_size;
+ in += 2;
+ }
+ else
+ *out++ = *in++;
+ }
+ *out = '\0'; /* terminator */
+
+ return result;
+}
+
+/*
* Perform LDAP authentication
*/
static int
@@ -2471,9 +2520,11 @@ CheckLDAPAuth(Port *port)
attributes[0] = port->hba->ldapsearchattribute ? port->hba->ldapsearchattribute : "uid";
attributes[1] = NULL;
- filter = psprintf("(%s=%s)",
- attributes[0],
- port->user_name);
+ /* Build a custom filter or a single attribute filter? */
+ if (port->hba->ldapsearchfilter != NULL)
+ filter = FormatSearchFilter(port->hba->ldapsearchfilter, port->user_name);
+ else
+ filter = psprintf("(%s=%s)", attributes[0], port->user_name);
r = ldap_search_s(ldap,
port->hba->ldapbasedn,
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 42afead9fd..4ae10f6d02 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -1505,22 +1505,24 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
/*
* 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.
+ * ldapbasedn, ldapbinddn, ldapbindpasswd and one of
+ * ldapsearchattribute or ldapsearchfilter. Disallow mixing these
+ * parameters.
*/
if (parsedline->ldapprefix || parsedline->ldapsuffix)
{
if (parsedline->ldapbasedn ||
parsedline->ldapbinddn ||
parsedline->ldapbindpasswd ||
- parsedline->ldapsearchattribute)
+ parsedline->ldapsearchattribute ||
+ parsedline->ldapsearchfilter)
{
ereport(elevel,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
- errmsg("cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, or ldapurl together with ldapprefix"),
+ errmsg("cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter or ldapurl together with ldapprefix"),
errcontext("line %d of configuration file \"%s\"",
line_num, HbaFileName)));
- *err_msg = "cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, or ldapurl together with ldapprefix";
+ *err_msg = "cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter or ldapurl together with ldapprefix";
return NULL;
}
}
@@ -1534,6 +1536,22 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
*err_msg = "authentication method \"ldap\" requires argument \"ldapbasedn\", \"ldapprefix\", or \"ldapsuffix\" to be set";
return NULL;
}
+
+ /*
+ * When using search+bind, you can either use a simple attribute
+ * (defaulting to "uid") or a fully custom search filter. You can't
+ * do both.
+ */
+ if (parsedline->ldapsearchattribute && parsedline->ldapsearchfilter)
+ {
+ ereport(elevel,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("cannot use ldapsearchattribute together with ldapsearchfilter"),
+ errcontext("line %d of configuration file \"%s\"",
+ line_num, HbaFileName)));
+ *err_msg = "cannot use ldapsearchattribute together with ldapsearchfilter";
+ return NULL;
+ }
}
if (parsedline->auth_method == uaRADIUS)
@@ -1788,6 +1806,11 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
REQUIRE_AUTH_OPTION(uaLDAP, "ldapsearchattribute", "ldap");
hbaline->ldapsearchattribute = pstrdup(val);
}
+ else if (strcmp(name, "ldapsearchfilter") == 0)
+ {
+ REQUIRE_AUTH_OPTION(uaLDAP, "ldapsearchfilter", "ldap");
+ hbaline->ldapsearchfilter = pstrdup(val);
+ }
else if (strcmp(name, "ldapbasedn") == 0)
{
REQUIRE_AUTH_OPTION(uaLDAP, "ldapbasedn", "ldap");
@@ -2266,6 +2289,11 @@ gethba_options(HbaLine *hba)
CStringGetTextDatum(psprintf("ldapsearchattribute=%s",
hba->ldapsearchattribute));
+ if (hba->ldapsearchfilter)
+ options[noptions++] =
+ CStringGetTextDatum(psprintf("ldapsearchfilter=%s",
+ hba->ldapsearchfilter));
+
if (hba->ldapscope)
options[noptions++] =
CStringGetTextDatum(psprintf("ldapscope=%d", hba->ldapscope));
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 07d92d4f9f..e711bee8bf 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -80,6 +80,7 @@ typedef struct HbaLine
char *ldapbinddn;
char *ldapbindpasswd;
char *ldapsearchattribute;
+ char *ldapsearchfilter;
char *ldapbasedn;
int ldapscope;
char *ldapprefix;
--
2.13.2
Mark,
* Mark Cave-Ayland (mark.cave-ayland@ilande.co.uk) wrote:
On 16/07/17 23:26, Thomas Munro wrote:
Thank you very much for this feedback and example, which I used in the
documentation in the patch. I see similar examples in the
documentation for other things on the web.I'll leave it up to Magnus and Stephen to duke it out over whether we
want to encourage LDAP usage, extend documentation to warn about
cleartext passwords with certain LDAP implementations or
configurations, etc etc. I'll add this patch to the commitfest and
get some popcorn.If it helps, we normally recommend that clients use ldaps for both AD
and UNIX environments, although this can be trickier from an
administrative perspective in AD environments because it can require
changes to the Windows firewall and certificate installation.
LDAPS is better than straight LDAP, of course, but it still doesn't
address the issue that the password is sent to the server, which both
SCRAM and Kerberos do and is why AD environments use Kerberos for
authentication, and why everything in an AD environment also should use
Kerberos.
Using Kerberos should also avoid the need to hack the Windows firewall
or deal with certificate installation. In an AD environment, it's
actually pretty straight-forward to add a PG server too. Further, in my
experience at least, there's been other changes recommended by Microsoft
that prevent using LDAP for auth because it's insecure.
Thanks!
Stephen
Magnus,
* Magnus Hagander (magnus@hagander.net) wrote:
On Sun, Jul 16, 2017 at 11:05 PM, Stephen Frost <sfrost@snowman.net> wrote:
I'd suggest that we try to understand why Kerberos couldn't be used in
that environment. I suspect in at least some cases what users would
like is the ability to do Kerberos auth but then have LDAP checked to
see if a given user (who has now auth'd through Kerberos) is allowed to
connect. We don't currently have any way to do that, but if we were
looking for things to do, that's what I'd suggest working on rather than
adding more to our LDAP auth system and implying by doing so that it's
reasonable to use.I find it particularly disappointing to see recommendations for using
LDAP auth, particularly in AD environments, that don't even mention
Kerberos or bother to explain how using LDAP sends the user's PW to the
server in cleartext.You do realize, I'm sure, that there are many LDAP servers out there that
are not AD, and that do not come with a Kerberos server attached to them...
I am aware that some exist, I've even contributed to their development
and packaging, but that doesn't make it a good idea to use them for
authentication.
I agree that Kerberos is usually the better choice *if it's available*.
Which is the case in an AD environment..
It's several orders of magnitude more complicated to set up though, and
there are many environments that have ldap but don't have Kerberos.
Frankly, I simply don't agree with this.
Refusing to improve LDAP for the users who have no choice seems like a very
unfriendly thing to do.
I'm fine with improving LDAP in general, but, as I tried to point out,
having a way to make it easier to integrate PG into an AD environment
would be better. It's not my intent to stop this patch but rather to
point out the issues with LDAP auth that far too frequently are not
properly understood.
(And you can actually reasonably solve the case of
kerberos-for-auth-ldap-for-priv by syncing the groups into postgres roles)
Yes, but sync'ing roles is a bit of a pain and it'd be nice if we could
avoid it, or perhaps make it easier.
Thanks!
Stephen
On 17/07/17 00:14, Stephen Frost wrote:
If it helps, we normally recommend that clients use ldaps for both AD
and UNIX environments, although this can be trickier from an
administrative perspective in AD environments because it can require
changes to the Windows firewall and certificate installation.LDAPS is better than straight LDAP, of course, but it still doesn't
address the issue that the password is sent to the server, which both
SCRAM and Kerberos do and is why AD environments use Kerberos for
authentication, and why everything in an AD environment also should use
Kerberos.Using Kerberos should also avoid the need to hack the Windows firewall
or deal with certificate installation. In an AD environment, it's
actually pretty straight-forward to add a PG server too. Further, in my
experience at least, there's been other changes recommended by Microsoft
that prevent using LDAP for auth because it's insecure.
Oh sure - I'm not questioning that Kerberos is a far better choice in
pure AD environments, it's just that I spend the majority of my time in
mixed-mode environments where Windows is very much in the minority.
In my experience LDAP is often implemented badly; for example the
majority of software still uses simple binds (i.e. plain logins) rather
than using SASL binds which support a whole range of better
authentication methods (e.g. GSSAPI, and even DIGEST-MD5 has been
mandatory for v3 and is implemented on AD).
And yes, while better authentication mechanisms do exist, I find that
all too often most software packages claim LDAP support rather than
Kerberos, and even then it is often limited to LDAP simple binds without
ldaps support.
ATB,
Mark.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Sun, Jul 16, 2017 at 7:58 PM, Mark Cave-Ayland <
mark.cave-ayland@ilande.co.uk> wrote:
On 16/07/17 00:08, Thomas Munro wrote:
On Fri, Jul 14, 2017 at 11:04 PM, Magnus Hagander <magnus@hagander.net>
wrote:
On Thu, Jul 13, 2017 at 9:31 AM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:A post on planet.postgresql.org today reminded me that a colleague had
asked me to post this POC patch here for discussion. It allows custom
filters with ldapsearchprefix and ldapsearchsuffix. Another approach
might be to take a filter pattern with "%USERNAME%" or whatever in it.
There's an existing precedent for the prefix and suffix approach, but
on the other hand a pattern approach would allow filters where the
username is inserted more than once.Do we even need prefix/suffix? If we just make it "ldapsearchpattern",
then
you could have something like:
ldapsearchattribute="uid"
ldapsearchfilter="|(memberof=cn=Paris DBA Team)(memberof=cn=Tokyo DBATeam)"
We could then always to substitution of the kind:
(&(attr=<uid>)(<filter>))which would in this case give:
(&(uid=mha)(|(memberof=cn=Paris DBA Team)(memberof=cn=Tokyo DBA Team)))Basically we'd always AND together the username lookup with the
additional
filter.
Ok, so we have 3 ideas put forward:
1. Wrap username with ldapsearchprefix ldapsearchsuffix to build
filter (as implemented by POC patch)
2. Optionally AND ldapsearchfilter with the existing
ldapsearchattribute-based filter (Magnus's proposal)
3. Pattern-based ldapsearchfilter so that %USER% is replaced with
username (my other suggestion)The main argument for approach 1 is that it follows the style of the
bind-only mode.With idea 2, I wonder if there are some more general kinds of things
that people might want to do that that wouldn't be possible because it
has to include (attribute=user)... perhaps something involving a
substring or other transformation functions (but I'm no LDAP expert,
that may not make sense).With idea 3 it would allow "(|(foo=%USER%)(bar=%USER%))", though I
don't know if any such multiple-mention filters would ever make sense
in a sane LDAP configuration.Any other views from LDAP-users?
I've spent quite a bit of time integrating various bits of
non-PostgreSQL software to LDAP and in my experience option 3 tends to
be the standard.Generally you find that you will be given the option to set the
attribute for the default search filter of the form
"(attribute=username)" which defaults to uid for UNIX-type systems and
sAMAccountName for AD. However there is always the ability to specify a
custom filter where the user is substituted via e.g. %u to cover all the
other use-cases.
Right, but that's something we do already today. It just defaults to uid,
but it's easy to change.
As an example, I don't know if anyone would actually do this with
PostgreSQL but I've been asked on multiple occasions to configure
software so that users should be allowed to log in with either their
email address or username which is easily done with a custom LDAP filter
like "(|(mail=%u)(uid=%u))".
How would that actually work, though? Given the same user in ldap could now
potentially match multiple different users in PostgreSQL. Would you then
create two accounts for the user, one with the uid as name and one with
email as name? Wouldn't that actually cause more issues than it solves?
--
Magnus Hagander
Me: https://www.hagander.net/ <http://www.hagander.net/>
Work: https://www.redpill-linpro.com/ <http://www.redpill-linpro.com/>
On Mon, Jul 17, 2017 at 1:23 AM, Stephen Frost <sfrost@snowman.net> wrote:
* Magnus Hagander (magnus@hagander.net) wrote:
On Sun, Jul 16, 2017 at 11:05 PM, Stephen Frost <sfrost@snowman.net>
wrote:
I'd suggest that we try to understand why Kerberos couldn't be used in
that environment. I suspect in at least some cases what users would
like is the ability to do Kerberos auth but then have LDAP checked to
see if a given user (who has now auth'd through Kerberos) is allowed to
connect. We don't currently have any way to do that, but if we were
looking for things to do, that's what I'd suggest working on ratherthan
adding more to our LDAP auth system and implying by doing so that it's
reasonable to use.I find it particularly disappointing to see recommendations for using
LDAP auth, particularly in AD environments, that don't even mention
Kerberos or bother to explain how using LDAP sends the user's PW to the
server in cleartext.You do realize, I'm sure, that there are many LDAP servers out there that
are not AD, and that do not come with a Kerberos server attached tothem...
I am aware that some exist, I've even contributed to their development
and packaging, but that doesn't make it a good idea to use them for
authentication.
Pretty sure that doesn't include any of the ones I'm talking about, but
sure :)
I agree that Kerberos is usually the better choice *if it's available*.
Which is the case in an AD environment..
Yes. But we shouldn't force everybody to use AD :P
It's several orders of magnitude more complicated to set up though, and
there are many environments that have ldap but don't have Kerberos.Frankly, I simply don't agree with this.
Really?
For LDAP auth I don't need to do *anything* in preparation. For Kerberos
auth I need to create an account, set encryption type, export keys, etc.
You can't possibly claim this is the same level of complexity?
Refusing to improve LDAP for the users who have no choice seems like a
very
unfriendly thing to do.
I'm fine with improving LDAP in general, but, as I tried to point out,
having a way to make it easier to integrate PG into an AD environment
would be better. It's not my intent to stop this patch but rather to
point out the issues with LDAP auth that far too frequently are not
properly understood.
A documentation patch for that would certainly be a good place to start.
Perhaps with up to date instructions for how to actually set up Kerberos in
an AD environment including all steps required?
(And you can actually reasonably solve the case of
kerberos-for-auth-ldap-for-priv by syncing the groups into postgresroles)
Yes, but sync'ing roles is a bit of a pain and it'd be nice if we could
avoid it, or perhaps make it easier.
Certainly.
//Magnus
On 17/07/17 13:09, Magnus Hagander wrote:
Hi Magnus,
Great to hear from you! It has definitely been a while...
Generally you find that you will be given the option to set the
attribute for the default search filter of the form
"(attribute=username)" which defaults to uid for UNIX-type systems and
sAMAccountName for AD. However there is always the ability to specify a
custom filter where the user is substituted via e.g. %u to cover all the
other use-cases.Right, but that's something we do already today. It just defaults to
uid, but it's easy to change.
Yes, I think that bit is fine as long as the default can be overridden.
There's always a choice as to whether the defaults favour a POSIX-based
LDAP or AD environment but that happens with all installations so it's
not a big deal.
As an example, I don't know if anyone would actually do this with
PostgreSQL but I've been asked on multiple occasions to configure
software so that users should be allowed to log in with either their
email address or username which is easily done with a custom LDAP filter
like "(|(mail=%u)(uid=%u))".How would that actually work, though? Given the same user in ldap could
now potentially match multiple different users in PostgreSQL. Would you
then create two accounts for the user, one with the uid as name and one
with email as name? Wouldn't that actually cause more issues than it solves?
Normally what happens for search+bind is that you execute the custom
filter with substitutions in order to find the entry for your bind DN,
but you also request the value of ldapsearchattribute (or equivalent) at
the same time. Say for example you had an entry like this:
dn: uid=mha,dc=users,dc=hagander,dc=net
uid: mha
mail: magnus@hagander.net
Using the filter "(|(mail=%u)(uid=%u))" would locate the same bind DN
"uid=mha,dc=users,dc=hagander,dc=net" with either one of your uid or
email address.
If the bind is successful then the current user identity should be set
to the value of the ldapsearchattribute retrieved from the bind DN
entry, which with the default of "uid" would be "mha". Hence you end up
with the same user role "mha" regardless of whether a uid or email
address was entered.
In terms of matching multiple users, all LDAP authentication routines
I've seen will fail if more than one DN matches the search filter, so
this nicely handles the cases where either the custom filter is
incorrect or more than one entry is accidentally matched in the directory.
ATB,
Mark.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Mon, Jul 17, 2017 at 6:47 PM, Mark Cave-Ayland <
mark.cave-ayland@ilande.co.uk> wrote:
On 17/07/17 13:09, Magnus Hagander wrote:
Hi Magnus,
Great to hear from you! It has definitely been a while...
Indeed. You should spend more time on these lists :P
Generally you find that you will be given the option to set the
attribute for the default search filter of the form
"(attribute=username)" which defaults to uid for UNIX-type systemsand
sAMAccountName for AD. However there is always the ability to
specify a
custom filter where the user is substituted via e.g. %u to cover all
the
other use-cases.
Right, but that's something we do already today. It just defaults to
uid, but it's easy to change.Yes, I think that bit is fine as long as the default can be overridden.
There's always a choice as to whether the defaults favour a POSIX-based
LDAP or AD environment but that happens with all installations so it's
not a big deal.
It's easy enough to change.
As an example, I don't know if anyone would actually do this with
PostgreSQL but I've been asked on multiple occasions to configure
software so that users should be allowed to log in with either their
email address or username which is easily done with a custom LDAPfilter
like "(|(mail=%u)(uid=%u))".
How would that actually work, though? Given the same user in ldap could
now potentially match multiple different users in PostgreSQL. Would you
then create two accounts for the user, one with the uid as name and one
with email as name? Wouldn't that actually cause more issues than itsolves?
Normally what happens for search+bind is that you execute the custom
filter with substitutions in order to find the entry for your bind DN,
but you also request the value of ldapsearchattribute (or equivalent) at
the same time. Say for example you had an entry like this:dn: uid=mha,dc=users,dc=hagander,dc=net
uid: mha
mail: magnus@hagander.netUsing the filter "(|(mail=%u)(uid=%u))" would locate the same bind DN
"uid=mha,dc=users,dc=hagander,dc=net" with either one of your uid or
email address.If the bind is successful then the current user identity should be set
to the value of the ldapsearchattribute retrieved from the bind DN
entry, which with the default of "uid" would be "mha". Hence you end up
with the same user role "mha" regardless of whether a uid or email
address was entered.
Right. This is the part that doesn't work for PostgreSQL. Because we have
already specified the username -- it goes in the startup packet in order to
pick the correct row from pg_hba.conf.
I guess in theory we could treat it like Kerberos or another one of the
systems where we get the username from an external entity. But then you'd
still have to specify the mapping again in pg_ident.conf, and it would be a
pretty strong break of backwards compatibility.
In terms of matching multiple users, all LDAP authentication routines
I've seen will fail if more than one DN matches the search filter, so
this nicely handles the cases where either the custom filter is
incorrect or more than one entry is accidentally matched in the directory.
So do we, in the current implementation. But it's a lot less likely to
happen in the current implementation, since we do a single equals lookup.
--
Magnus Hagander
Me: https://www.hagander.net/ <http://www.hagander.net/>
Work: https://www.redpill-linpro.com/ <http://www.redpill-linpro.com/>
On 17/07/17 18:08, Magnus Hagander wrote:
On Mon, Jul 17, 2017 at 6:47 PM, Mark Cave-Ayland
<mark.cave-ayland@ilande.co.uk <mailto:mark.cave-ayland@ilande.co.uk>>
wrote:
Great to hear from you! It has definitely been a while...Indeed. You should spend more time on these lists :P
Well I do get the emails, unfortunately it's trying to find the time to
read them all ;)
How would that actually work, though? Given the same user in ldap could
now potentially match multiple different users in PostgreSQL. Would you
then create two accounts for the user, one with the uid as name and one
with email as name? Wouldn't that actually cause more issues than it solves?Normally what happens for search+bind is that you execute the custom
filter with substitutions in order to find the entry for your bind DN,
but you also request the value of ldapsearchattribute (or equivalent) at
the same time. Say for example you had an entry like this:dn: uid=mha,dc=users,dc=hagander,dc=net
uid: mha
mail: magnus@hagander.net <mailto:magnus@hagander.net>Using the filter "(|(mail=%u)(uid=%u))" would locate the same bind DN
"uid=mha,dc=users,dc=hagander,dc=net" with either one of your uid or
email address.If the bind is successful then the current user identity should be set
to the value of the ldapsearchattribute retrieved from the bind DN
entry, which with the default of "uid" would be "mha". Hence you end up
with the same user role "mha" regardless of whether a uid or email
address was entered.Right. This is the part that doesn't work for PostgreSQL. Because we
have already specified the username -- it goes in the startup packet in
order to pick the correct row from pg_hba.conf.
I don't think that's necessarily going to be an issue here because if
you're specifying a custom LDAP filter then there's a very good chance
that you're delegating access control to information held in the
directory anyway, e.g.
(&(memberOf=cn=pgusers,dc=groups,dc=hagander,dc=net)(uid=%u))
((&(uid=%u)(|(uid=mha)(uid=mark)(uid=thomas)))
In the mail example above when you're using more than one attribute, I
think it's fair enough to simply say in the documentation you must set
user to "all" in pg_hba.conf since it is impossible to know which
attribute is being used to identify the user role until after
authentication.
I should add that personally I don't recommend such setups where the
user can log in using more than one identifier, but there are clients
out there who absolutely will insist on it (think internal vs. external
users) so if the LDAP support is being updated then it's worth exploring
to see if these cases can be supported.
I guess in theory we could treat it like Kerberos or another one of the
systems where we get the username from an external entity. But then
you'd still have to specify the mapping again in pg_ident.conf, and it
would be a pretty strong break of backwards compatibility.
(goes and glances at the code)
Is there no reason why you couldn't just overwrite port->user_name based
upon ldapsearchattribute at the end of CheckLDAPAuth?
And if this were only enabled when a custom filter were specified then
it shouldn't cause any backwards compatibility issues being a new feature?
In terms of matching multiple users, all LDAP authentication routines
I've seen will fail if more than one DN matches the search filter, so
this nicely handles the cases where either the custom filter is
incorrect or more than one entry is accidentally matched in the
directory.So do we, in the current implementation. But it's a lot less likely to
happen in the current implementation, since we do a single equals lookup.
Great, that's absolutely fine :)
ATB,
Mark.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Sun, Jul 16, 2017 at 7:23 PM, Stephen Frost <sfrost@snowman.net> wrote:
Refusing to improve LDAP for the users who have no choice seems like a very
unfriendly thing to do.I'm fine with improving LDAP in general, but, as I tried to point out,
having a way to make it easier to integrate PG into an AD environment
would be better. It's not my intent to stop this patch but rather to
point out the issues with LDAP auth that far too frequently are not
properly understood.
Then it's off-topic for this thread.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 7/16/17 19:09, Thomas Munro wrote:
On Mon, Jul 17, 2017 at 10:26 AM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:ldap-search-filters-v2.patch
Gah, it would help if I could spell "occurrences" correctly. Fixed in
the attached.
Please also add the corresponding support for specifying search filters
in LDAP URLs. See RFC 4516 for the format and
https://linux.die.net/man/3/ldap_url_parse for the API. You might just
need to grab LDAPURLDesc.lud_filter and use it.
--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Wed, Aug 2, 2017 at 5:36 AM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:
On 7/16/17 19:09, Thomas Munro wrote:
On Mon, Jul 17, 2017 at 10:26 AM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:ldap-search-filters-v2.patch
Gah, it would help if I could spell "occurrences" correctly. Fixed in
the attached.Please also add the corresponding support for specifying search filters
in LDAP URLs. See RFC 4516 for the format and
https://linux.die.net/man/3/ldap_url_parse for the API. You might just
need to grab LDAPURLDesc.lud_filter and use it.
Good idea. Yes, it seems to be that simple. Here's a version like
that. Here's an example of how it looks in pg_hba.conf:
host all all 127.0.0.1/32 ldap
ldapurl="ldap://localhost/ou=people1,dc=my-domain,dc=com??sub?(cn=%25u)"
Maybe we could choose a better token than %u for user name, since it
has to be escaped when included in a URL like that, but on the other
hand there seems to be wide precedent for %u in other software.
--
Thomas Munro
http://www.enterprisedb.com
Attachments:
ldap-search-filters-v4.patchapplication/octet-stream; name=ldap-search-filters-v4.patchDownload
From c1faf1a8a1ac79a4c1a1b721aeb699da9d56db9c Mon Sep 17 00:00:00 2001
From: Thomas Munro <thomas.munro@enterprisedb.com>
Date: Mon, 17 Jul 2017 10:11:28 +1200
Subject: [PATCH] Allow custom search filters to be configured for LDAP auth.
Before, only filters of the form "(<ldapsearchattribute>=<user>)"
could be used to search an LDAP server. Introduce ldapsearchfilter
so that more general filters can be configured using patterns like
"(|(uid=%u)(mail=%u))" and "(&(uid=%u)(objectClass=posixAccount))".
Also allow search filters to be included in an LDAP URL.
---
doc/src/sgml/client-auth.sgml | 45 +++++++++++++++++++++++++++++++---
src/backend/libpq/auth.c | 57 ++++++++++++++++++++++++++++++++++++++++---
src/backend/libpq/hba.c | 47 +++++++++++++++++++++++++----------
src/include/libpq/hba.h | 1 +
4 files changed, 131 insertions(+), 19 deletions(-)
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 819db811b2..e83fa9a457 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -1508,18 +1508,36 @@ omicron bryanh guest1
</listitem>
</varlistentry>
<varlistentry>
+ <term><literal>ldapsearchfilter</literal></term>
+ <listitem>
+ <para>
+ The search filter to use when doing search+bind authentication.
+ Occurrences of <literal>%u</literal> will be replaced with the
+ user name. This allows for more flexible search filters than
+ <literal>ldapsearchattribute</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
<term><literal>ldapurl</literal></term>
<listitem>
<para>
An RFC 4516 LDAP URL. This is an alternative way to write some of the
other LDAP options in a more compact and standard form. The format is
<synopsis>
-ldap://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replaceable>basedn</replaceable>[?[<replaceable>attribute</replaceable>][?[<replaceable>scope</replaceable>]]]
+ldap://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replaceable>basedn</replaceable>[[[?[<replaceable>attribute</replaceable>]][?<replaceable>scope</replaceable>]][?<replaceable>filter</replaceable>]]
</synopsis>
<replaceable>scope</replaceable> must be one
of <literal>base</literal>, <literal>one</literal>, <literal>sub</literal>,
- typically the latter. Only one attribute is used, and some other
- components of standard LDAP URLs such as filters and extensions are
+ typically the last. <replaceable>attribute</replaceable> can
+ nominate a single attribute, in which case it is used as a value for
+ <literal>ldapsearchattribute</literal>. If
+ <replaceable>attribute</replaceable> is empty then
+ <replaceable>filter</replaceable> can be used as a value for
+ <literal>ldapsearchfilter</literal>. When specifying a search filter
+ as part of a URL, the special token <literal>%u</literal> standing
+ for the user name must be written as <literal>%25u</literal>.
+ Some other components of standard LDAP URLs such as extensions are
not supported.
</para>
@@ -1550,6 +1568,17 @@ ldap://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replac
</para>
<para>
+ When using search+bind mode, the search can be performed using a single
+ attribute specified with <literal>ldapsearchattribute</literal>, or using
+ a custom search filter specified with
+ <literal>ldapsearchfilter</literal>.
+ Specifying <literal>ldapsearchattribute=foo</literal> is equivalent to
+ specifying <literal>ldapsearchfilter="(foo=%u)"</literal>. If neither
+ option is specified the default is
+ <literal>ldapsearchattribute=uid</literal>.
+ </para>
+
+ <para>
Here is an example for a simple-bind LDAP configuration:
<programlisting>
host ... ldap ldapserver=ldap.example.net ldapprefix="cn=" ldapsuffix=", dc=example, dc=net"
@@ -1584,6 +1613,16 @@ host ... ldap ldapurl="ldap://ldap.example.net/dc=example,dc=net?uid?sub"
same URL format, so it will be easier to share the configuration.
</para>
+ <para>
+ Here is an example for a search+bind configuration that uses
+ <literal>ldapsearchfilter</literal> instead of
+ <literal>ldapsearchattribute</literal> to allow authentication by
+ user ID or email address:
+<programlisting>
+host ... ldap ldapserver=ldap.example.net ldapbasedn="dc=example, dc=net" ldapsearchfilter="(|(uid=%u)(mail=%u))"
+</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 dd7de7c3a4..e43818e91f 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2381,6 +2381,55 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
}
/*
+ * Return a newly allocated C string copied from "pattern" with all
+ * occurrences of "%u" replaced with "user_name".
+ */
+static char *
+FormatSearchFilter(const char *pattern, const char *user_name)
+{
+ Size user_name_size = strlen(user_name);
+ Size buffer_size = 0;
+ const char *in;
+ char *out;
+ char *result;
+
+ /* Compute the size of the output buffer. */
+ in = pattern;
+ while (*in != '\0')
+ {
+ if (in[0] == '%' && in[1] == 'u')
+ {
+ buffer_size += user_name_size;
+ in += 2;
+ }
+ else
+ {
+ buffer_size++;
+ in++;
+ }
+ }
+ buffer_size++; /* terminator */
+
+ /* Build the output string. */
+ out = result = palloc(buffer_size);
+ in = pattern;
+ while (*in != '\0')
+ {
+ if (in[0] == '%' && in[1] == 'u')
+ {
+ memcpy(out, user_name, user_name_size);
+ out += user_name_size;
+ in += 2;
+ }
+ else
+ *out++ = *in++;
+ }
+ *out = '\0'; /* terminator */
+
+ return result;
+}
+
+/*
* Perform LDAP authentication
*/
static int
@@ -2471,9 +2520,11 @@ CheckLDAPAuth(Port *port)
attributes[0] = port->hba->ldapsearchattribute ? port->hba->ldapsearchattribute : "uid";
attributes[1] = NULL;
- filter = psprintf("(%s=%s)",
- attributes[0],
- port->user_name);
+ /* Build a custom filter or a single attribute filter? */
+ if (port->hba->ldapsearchfilter != NULL)
+ filter = FormatSearchFilter(port->hba->ldapsearchfilter, port->user_name);
+ else
+ filter = psprintf("(%s=%s)", attributes[0], port->user_name);
r = ldap_search_s(ldap,
port->hba->ldapbasedn,
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 42afead9fd..3a482bead4 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -1505,22 +1505,24 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
/*
* 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.
+ * ldapbasedn, ldapbinddn, ldapbindpasswd and one of
+ * ldapsearchattribute or ldapsearchfilter. Disallow mixing these
+ * parameters.
*/
if (parsedline->ldapprefix || parsedline->ldapsuffix)
{
if (parsedline->ldapbasedn ||
parsedline->ldapbinddn ||
parsedline->ldapbindpasswd ||
- parsedline->ldapsearchattribute)
+ parsedline->ldapsearchattribute ||
+ parsedline->ldapsearchfilter)
{
ereport(elevel,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
- errmsg("cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, or ldapurl together with ldapprefix"),
+ errmsg("cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter or ldapurl together with ldapprefix"),
errcontext("line %d of configuration file \"%s\"",
line_num, HbaFileName)));
- *err_msg = "cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, or ldapurl together with ldapprefix";
+ *err_msg = "cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter or ldapurl together with ldapprefix";
return NULL;
}
}
@@ -1534,6 +1536,22 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
*err_msg = "authentication method \"ldap\" requires argument \"ldapbasedn\", \"ldapprefix\", or \"ldapsuffix\" to be set";
return NULL;
}
+
+ /*
+ * When using search+bind, you can either use a simple attribute
+ * (defaulting to "uid") or a fully custom search filter. You can't
+ * do both.
+ */
+ if (parsedline->ldapsearchattribute && parsedline->ldapsearchfilter)
+ {
+ ereport(elevel,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("cannot use ldapsearchattribute together with ldapsearchfilter"),
+ errcontext("line %d of configuration file \"%s\"",
+ line_num, HbaFileName)));
+ *err_msg = "cannot use ldapsearchattribute together with ldapsearchfilter";
+ return NULL;
+ }
}
if (parsedline->auth_method == uaRADIUS)
@@ -1729,14 +1747,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
hbaline->ldapsearchattribute = pstrdup(urldata->lud_attrs[0]); /* only use first one */
hbaline->ldapscope = urldata->lud_scope;
if (urldata->lud_filter)
- {
- ereport(elevel,
- (errcode(ERRCODE_CONFIG_FILE_ERROR),
- errmsg("filters not supported in LDAP URLs")));
- *err_msg = "filters not supported in LDAP URLs";
- ldap_free_urldesc(urldata);
- return false;
- }
+ hbaline->ldapsearchfilter = pstrdup(urldata->lud_filter);
ldap_free_urldesc(urldata);
#else /* not OpenLDAP */
ereport(elevel,
@@ -1788,6 +1799,11 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
REQUIRE_AUTH_OPTION(uaLDAP, "ldapsearchattribute", "ldap");
hbaline->ldapsearchattribute = pstrdup(val);
}
+ else if (strcmp(name, "ldapsearchfilter") == 0)
+ {
+ REQUIRE_AUTH_OPTION(uaLDAP, "ldapsearchfilter", "ldap");
+ hbaline->ldapsearchfilter = pstrdup(val);
+ }
else if (strcmp(name, "ldapbasedn") == 0)
{
REQUIRE_AUTH_OPTION(uaLDAP, "ldapbasedn", "ldap");
@@ -2266,6 +2282,11 @@ gethba_options(HbaLine *hba)
CStringGetTextDatum(psprintf("ldapsearchattribute=%s",
hba->ldapsearchattribute));
+ if (hba->ldapsearchfilter)
+ options[noptions++] =
+ CStringGetTextDatum(psprintf("ldapsearchfilter=%s",
+ hba->ldapsearchfilter));
+
if (hba->ldapscope)
options[noptions++] =
CStringGetTextDatum(psprintf("ldapscope=%d", hba->ldapscope));
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 07d92d4f9f..e711bee8bf 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -80,6 +80,7 @@ typedef struct HbaLine
char *ldapbinddn;
char *ldapbindpasswd;
char *ldapsearchattribute;
+ char *ldapsearchfilter;
char *ldapbasedn;
int ldapscope;
char *ldapprefix;
--
2.13.2
On 01/08/17 23:17, Thomas Munro wrote:
On Wed, Aug 2, 2017 at 5:36 AM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:On 7/16/17 19:09, Thomas Munro wrote:
On Mon, Jul 17, 2017 at 10:26 AM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:ldap-search-filters-v2.patch
Gah, it would help if I could spell "occurrences" correctly. Fixed in
the attached.Please also add the corresponding support for specifying search filters
in LDAP URLs. See RFC 4516 for the format and
https://linux.die.net/man/3/ldap_url_parse for the API. You might just
need to grab LDAPURLDesc.lud_filter and use it.Good idea. Yes, it seems to be that simple. Here's a version like
that. Here's an example of how it looks in pg_hba.conf:host all all 127.0.0.1/32 ldap
ldapurl="ldap://localhost/ou=people1,dc=my-domain,dc=com??sub?(cn=%25u)"Maybe we could choose a better token than %u for user name, since it
has to be escaped when included in a URL like that, but on the other
hand there seems to be wide precedent for %u in other software.
Yeah, mostly I only ever see ldapurls used programatically, i.e. the
configuration allows you to set the various fields separately and then
the software generates the URL with the correct encoding itself. But if
it's documented that's not a reason to reject the patch as I definitely
see it as an improvement.
As I mentioned previously in the thread, the main barrier preventing
people from using LDAP is that the role cannot be generated from other
attributes in the directory. In a lot of real-life cases I see, that
would be enough to discount PostgreSQL's LDAP authentication completely.
ATB,
Mark.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
A couple of comments on this patch. I have attached a "fixup" patch on
top of your v4 that should address them.
- I think the bracketing of the LDAP URL synopsis is wrong.
- I have dropped the sentence that LDAP URL extensions are not
supported. That sentence was written mainly to point out that filters
are not supported, which they are now. There is nothing beyond filters
and extensions left in LDAP URLs, AFAIK.
- We have previously used the ldapsearchattribute a bit strangely. We
use it as both the search filter and as the attribute to return from the
search, but we don't actually care about the returned attribute (we only
use the DN (which is not a real attribute)). That hasn't been a real
problem, but now if you specify a filter, as the code stands it will
always request the "uid" attribute, which won't work if there is no such
attribute. I have found that there is an official way to request "no
attribute", so I have fixed the code to do that.
- I was wondering whether we need a way to escape the % (%%?) but it
doesn't seem worth bothering.
I have been looking around the Internet how this functionality compares
to other LDAP authentication systems.
I didn't see the origin of the %u idea in this thread, but perhaps it
came from Dovecot. But there it's a general configuration file syntax,
nothing specific to LDAP. In nginx you use %(username), which again is
general configuration file syntax. I'm OK with using %u.
The original LDAP URL design was adapted from Apache
(https://httpd.apache.org/docs/2.4/mod/mod_authnz_ldap.html#authldapurl).
They combine the attribute filter and the general filter if both are
specified, but I think they got that a bit wrong. The attribute field
in the URL is actually not supposed to be a filter but a return field,
which is also the confusion mentioned above.
The PAM module (https://linux.die.net/man/5/pam_ldap) also has a way to
specify a search attribute and a general filter and combines them.
Neither of these allows substituting the user name into the search filter.
I think there would be a case to be made to allow the searchattribute
and the searchfilter both be specified. One typical use would be to use
the first one to locate the user and the second one to "filter" out
certain things, which I think is the approach the PAM and Apache modules
take. This wouldn't work well, however, if you use the %u placeholder,
because then you'd have to explicitly unset ldapsearchattribute, which
would be annoying. So maybe not.
Please check my patch. I think it's ready to go then.
--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
Attachments:
0001-fixup-Allow-custom-search-filters-to-be-configured-f.patchtext/plain; charset=UTF-8; name=0001-fixup-Allow-custom-search-filters-to-be-configured-f.patch; x-mac-creator=0; x-mac-type=0Download
From cd5536fa70eed74f8b1aa4f856edae8b84d76ef5 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter_e@gmx.net>
Date: Fri, 8 Sep 2017 10:58:53 -0400
Subject: [PATCH] fixup! Allow custom search filters to be configured for LDAP
auth.
---
doc/src/sgml/client-auth.sgml | 4 +---
src/backend/libpq/auth.c | 18 ++++++++----------
2 files changed, 9 insertions(+), 13 deletions(-)
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 1773ce29a9..1c3db96134 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -1525,7 +1525,7 @@ <title>LDAP Authentication</title>
An RFC 4516 LDAP URL. This is an alternative way to write some of the
other LDAP options in a more compact and standard form. The format is
<synopsis>
-ldap://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replaceable>basedn</replaceable>[[[?[<replaceable>attribute</replaceable>]][?<replaceable>scope</replaceable>]][?<replaceable>filter</replaceable>]]
+ldap://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replaceable>basedn</replaceable>[?[<replaceable>attribute</replaceable>][?[<replaceable>scope</replaceable>][?[<replaceable>filter</replaceable>]]]]
</synopsis>
<replaceable>scope</replaceable> must be one
of <literal>base</literal>, <literal>one</literal>, <literal>sub</literal>,
@@ -1537,8 +1537,6 @@ <title>LDAP Authentication</title>
<literal>ldapsearchfilter</literal>. When specifying a search filter
as part of a URL, the special token <literal>%u</literal> standing
for the user name must be written as <literal>%25u</literal>.
- Some other components of standard LDAP URLs such as extensions are
- not supported.
</para>
<para>
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 6c96e87d37..c43f203eb3 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2401,8 +2401,8 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
static char *
FormatSearchFilter(const char *pattern, const char *user_name)
{
- Size user_name_size = strlen(user_name);
- Size buffer_size = 0;
+ size_t user_name_size = strlen(user_name);
+ size_t buffer_size = 0;
const char *in;
char *out;
char *result;
@@ -2413,7 +2413,7 @@ FormatSearchFilter(const char *pattern, const char *user_name)
{
if (in[0] == '%' && in[1] == 'u')
{
- buffer_size += user_name_size;
+ buffer_size += user_name_size - 2;
in += 2;
}
else
@@ -2486,7 +2486,7 @@ CheckLDAPAuth(Port *port)
char *filter;
LDAPMessage *search_message;
LDAPMessage *entry;
- char *attributes[2];
+ char *attributes[] = { LDAP_NO_ATTRS, NULL };
char *dn;
char *c;
int count;
@@ -2528,15 +2528,13 @@ CheckLDAPAuth(Port *port)
return STATUS_ERROR;
}
- /* Fetch just one attribute, else *all* attributes are returned */
- attributes[0] = port->hba->ldapsearchattribute ? port->hba->ldapsearchattribute : "uid";
- attributes[1] = NULL;
-
/* Build a custom filter or a single attribute filter? */
- if (port->hba->ldapsearchfilter != NULL)
+ if (port->hba->ldapsearchfilter)
filter = FormatSearchFilter(port->hba->ldapsearchfilter, port->user_name);
+ else if (port->hba->ldapsearchattribute)
+ filter = psprintf("(%s=%s)", port->hba->ldapsearchattribute, port->user_name);
else
- filter = psprintf("(%s=%s)", attributes[0], port->user_name);
+ filter = psprintf("(uid=%s)", port->user_name);
r = ldap_search_s(ldap,
port->hba->ldapbasedn,
--
2.11.0 (Apple Git-81)
For additional entertainment I have written a test suite for this LDAP
authentication functionality. It's not quite robust enough to be run by
default, because it needs a full OpenLDAP installation, but it's been
very helpful for reviewing this patch. Here it is.
--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
Attachments:
0001-Add-LDAP-authentication-test-suite.patchtext/plain; charset=UTF-8; name=0001-Add-LDAP-authentication-test-suite.patch; x-mac-creator=0; x-mac-type=0Download
From 9457ef272d011dbb34b1a204cac9cbac08e4d652 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter_e@gmx.net>
Date: Fri, 8 Sep 2017 10:33:49 -0400
Subject: [PATCH 1/3] Add LDAP authentication test suite
---
src/test/Makefile | 2 +-
src/test/ldap/.gitignore | 2 +
src/test/ldap/Makefile | 20 +++++++
src/test/ldap/README | 16 +++++
src/test/ldap/data.ldif | 19 ++++++
src/test/ldap/t/001_auth.pl | 139 ++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 197 insertions(+), 1 deletion(-)
create mode 100644 src/test/ldap/.gitignore
create mode 100644 src/test/ldap/Makefile
create mode 100644 src/test/ldap/README
create mode 100644 src/test/ldap/data.ldif
create mode 100644 src/test/ldap/t/001_auth.pl
diff --git a/src/test/Makefile b/src/test/Makefile
index dbfa799a84..193b977bf3 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -17,7 +17,7 @@ SUBDIRS = perl regress isolation modules authentication recovery subscription
# We don't build or execute examples/, locale/, or thread/ by default,
# but we do want "make clean" etc to recurse into them. Likewise for ssl/,
# because the SSL test suite is not secure to run on a multi-user system.
-ALWAYS_SUBDIRS = examples locale thread ssl
+ALWAYS_SUBDIRS = examples ldap locale thread ssl
# We want to recurse to all subdirs for all standard targets, except that
# installcheck and install should not recurse into the subdirectory "modules".
diff --git a/src/test/ldap/.gitignore b/src/test/ldap/.gitignore
new file mode 100644
index 0000000000..871e943d50
--- /dev/null
+++ b/src/test/ldap/.gitignore
@@ -0,0 +1,2 @@
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/ldap/Makefile b/src/test/ldap/Makefile
new file mode 100644
index 0000000000..9dd1bbeade
--- /dev/null
+++ b/src/test/ldap/Makefile
@@ -0,0 +1,20 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/ldap
+#
+# Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/ldap/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = src/test/ldap
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+check:
+ $(prove_check)
+
+clean distclean maintainer-clean:
+ rm -rf tmp_check
diff --git a/src/test/ldap/README b/src/test/ldap/README
new file mode 100644
index 0000000000..20a7a0b5de
--- /dev/null
+++ b/src/test/ldap/README
@@ -0,0 +1,16 @@
+src/test/ldap/README
+
+Tests for LDAP functionality
+============================
+
+This directory contains a test suite for LDAP functionality. This
+requires a full OpenLDAP installation, including server and client
+tools, and is therefore kept separate and not run by default.
+
+
+Running the tests
+=================
+
+ make check
+
+NOTE: This requires the --enable-tap-tests argument to configure.
diff --git a/src/test/ldap/data.ldif b/src/test/ldap/data.ldif
new file mode 100644
index 0000000000..b30604e1f0
--- /dev/null
+++ b/src/test/ldap/data.ldif
@@ -0,0 +1,19 @@
+dn: dc=example,dc=net
+objectClass: top
+objectClass: dcObject
+objectClass: organization
+dc: example
+o: ExmapleCo
+
+dn: uid=test1,dc=example,dc=net
+objectClass: inetOrgPerson
+objectClass: posixAccount
+uid: test1
+sn: Lastname
+givenName: Firstname
+cn: First Test User
+displayName: First Test User
+uidNumber: 101
+gidNumber: 100
+homeDirectory: /home/test1
+mail: test1@example.net
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
new file mode 100644
index 0000000000..af9e34d7cf
--- /dev/null
+++ b/src/test/ldap/t/001_auth.pl
@@ -0,0 +1,139 @@
+use strict;
+use warnings;
+use TestLib;
+use PostgresNode;
+use Test::More tests => 9;
+
+my ($slapd, $ldap_bin_dir, $ldap_schema_dir);
+
+$ldap_bin_dir = undef; # usually in PATH
+
+if ($^O eq 'darwin')
+{
+ $slapd = '/usr/local/opt/openldap/libexec/slapd';
+ $ldap_schema_dir = '/usr/local/etc/openldap/schema';
+}
+elsif ($^O eq 'linux')
+{
+ $slapd = '/usr/sbin/slapd';
+ $ldap_schema_dir = '/etc/ldap/schema' if -f '/etc/ldap/schema';
+ $ldap_schema_dir = '/etc/openldap/schema' if -f '/etc/openldap/schema';
+}
+
+# make your own edits here
+#$slapd = '';
+#$ldap_bin_dir = '';
+#$ldap_schema_dir = '';
+
+$ENV{PATH} = "$ldap_bin_dir:$ENV{PATH}" if $ldap_bin_dir;
+
+my $ldap_datadir = "${TestLib::tmp_check}/openldap-data";
+my $slapd_conf = "${TestLib::tmp_check}/slapd.conf";
+my $slapd_pidfile = "${TestLib::tmp_check}/slapd.pid";
+my $slapd_logfile = "${TestLib::tmp_check}/slapd.log";
+my $ldap_conf = "${TestLib::tmp_check}/ldap.conf";
+my $ldap_server = '127.0.0.1';
+my $ldap_port = int(rand() * 16384) + 49152;
+my $ldap_url = "ldap://$ldap_server:$ldap_port";
+my $ldap_basedn = 'dc=example,dc=net';
+my $ldap_rootdn = 'cn=Manager,dc=example,dc=net';
+my $ldap_rootpw = 'secret';
+my $ldap_pwfile = "${TestLib::tmp_check}/ldappassword";
+
+note "setting up slapd";
+
+append_to_file($slapd_conf,
+qq{include $ldap_schema_dir/core.schema
+include $ldap_schema_dir/cosine.schema
+include $ldap_schema_dir/nis.schema
+include $ldap_schema_dir/inetorgperson.schema
+
+pidfile $slapd_pidfile
+logfile $slapd_logfile
+
+access to *
+ by * read
+ by anonymous auth
+
+database mdb
+dbnosync
+directory $ldap_datadir
+
+suffix "dc=example,dc=net"
+rootdn "$ldap_rootdn"
+rootpw $ldap_rootpw});
+
+mkdir $ldap_datadir or die;
+
+system_or_bail $slapd, '-f', $slapd_conf, '-h', $ldap_url;
+
+END
+{
+ kill 'INT', `cat $slapd_pidfile` if -f $slapd_pidfile;
+}
+
+append_to_file($ldap_pwfile, $ldap_rootpw);
+chmod 0600, $ldap_pwfile or die;
+
+$ENV{'LDAPURI'} = $ldap_url;
+$ENV{'LDAPBINDDN'} = $ldap_rootdn;
+
+note "loading LDAP data";
+
+system_or_bail 'ldapadd', '-x', '-y', $ldap_pwfile, '-f', 'data.ldif';
+system_or_bail 'ldappasswd', '-x', '-y', $ldap_pwfile, '-s', 'secret1', 'uid=test1,dc=example,dc=net';
+
+note "setting up PostgreSQL instance";
+
+my $node = get_new_node('node');
+$node->init;
+$node->start;
+
+$node->safe_psql('postgres', 'CREATE USER test0;');
+$node->safe_psql('postgres', 'CREATE USER test1;');
+
+note "running tests";
+
+sub test_access
+{
+ my ($node, $role, $expected_res, $test_name) = @_;
+
+ my $res = $node->psql('postgres', 'SELECT 1', extra_params => [ '-U', $role ]);
+ is($res, $expected_res, $test_name);
+}
+
+note "simple bind";
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf', qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapprefix="uid=" ldapsuffix=",dc=example,dc=net"});
+$node->reload;
+
+$ENV{"PGPASSWORD"} = 'wrong';
+test_access($node, 'test0', 2, 'simple bind authentication fails if user not found in LDAP');
+test_access($node, 'test1', 2, 'simple bind authentication fails with wrong password');
+$ENV{"PGPASSWORD"} = 'secret1';
+test_access($node, 'test1', 0, 'simple bind authentication succeeds');
+
+note "search+bind";
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf', qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapbasedn="$ldap_basedn"});
+$node->reload;
+
+$ENV{"PGPASSWORD"} = 'wrong';
+test_access($node, 'test0', 2, 'search+bind authentication fails if user not found in LDAP');
+test_access($node, 'test1', 2, 'search+bind authentication fails with wrong password');
+$ENV{"PGPASSWORD"} = 'secret1';
+test_access($node, 'test1', 0, 'search+bind authentication succeeds');
+
+note "LDAP URLs";
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf', qq{local all all ldap ldapurl="$ldap_url/$ldap_basedn?uid?sub"});
+$node->reload;
+
+$ENV{"PGPASSWORD"} = 'wrong';
+test_access($node, 'test0', 2, 'search+bind with LDAP URL authentication fails if user not found in LDAP');
+test_access($node, 'test1', 2, 'search+bind with LDAP URL authentication fails with wrong password');
+$ENV{"PGPASSWORD"} = 'secret1';
+test_access($node, 'test1', 0, 'search+bind with LDAP URL authentication succeeds');
--
2.11.0 (Apple Git-81)
0002-Add-tests-for-ldapsearchfilter-functionality.patchtext/plain; charset=UTF-8; name=0002-Add-tests-for-ldapsearchfilter-functionality.patch; x-mac-creator=0; x-mac-type=0Download
From 44aa04e26916bd8d16b680db21987ccb1cedbf34 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter_e@gmx.net>
Date: Fri, 8 Sep 2017 10:52:08 -0400
Subject: [PATCH 2/3] Add tests for ldapsearchfilter functionality
---
src/test/ldap/data.ldif | 13 +++++++++++++
src/test/ldap/t/001_auth.pl | 36 +++++++++++++++++++++++++++++++++++-
2 files changed, 48 insertions(+), 1 deletion(-)
diff --git a/src/test/ldap/data.ldif b/src/test/ldap/data.ldif
index b30604e1f0..8d82c284b1 100644
--- a/src/test/ldap/data.ldif
+++ b/src/test/ldap/data.ldif
@@ -17,3 +17,16 @@ uidNumber: 101
gidNumber: 100
homeDirectory: /home/test1
mail: test1@example.net
+
+dn: uid=test2,dc=example,dc=net
+objectClass: inetOrgPerson
+objectClass: posixAccount
+uid: test2
+sn: Lastname
+givenName: Firstname
+cn: Second Test User
+displayName: Second Test User
+uidNumber: 102
+gidNumber: 100
+homeDirectory: /home/test2
+mail: test2@example.net
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index af9e34d7cf..eb72b16a0a 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -2,7 +2,7 @@
use warnings;
use TestLib;
use PostgresNode;
-use Test::More tests => 9;
+use Test::More tests => 14;
my ($slapd, $ldap_bin_dir, $ldap_schema_dir);
@@ -82,6 +82,7 @@ END
system_or_bail 'ldapadd', '-x', '-y', $ldap_pwfile, '-f', 'data.ldif';
system_or_bail 'ldappasswd', '-x', '-y', $ldap_pwfile, '-s', 'secret1', 'uid=test1,dc=example,dc=net';
+system_or_bail 'ldappasswd', '-x', '-y', $ldap_pwfile, '-s', 'secret2', 'uid=test2,dc=example,dc=net';
note "setting up PostgreSQL instance";
@@ -91,6 +92,7 @@ END
$node->safe_psql('postgres', 'CREATE USER test0;');
$node->safe_psql('postgres', 'CREATE USER test1;');
+$node->safe_psql('postgres', 'CREATE USER "test2@example.net";');
note "running tests";
@@ -137,3 +139,35 @@ sub test_access
test_access($node, 'test1', 2, 'search+bind with LDAP URL authentication fails with wrong password');
$ENV{"PGPASSWORD"} = 'secret1';
test_access($node, 'test1', 0, 'search+bind with LDAP URL authentication succeeds');
+
+note "search filters";
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf', qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapbasedn="$ldap_basedn" ldapsearchfilter="(|(uid=%u)(mail=%u))"});
+$node->reload;
+
+$ENV{"PGPASSWORD"} = 'secret1';
+test_access($node, 'test1', 0, 'search filter finds by uid');
+$ENV{"PGPASSWORD"} = 'secret2';
+test_access($node, 'test2@example.net', 0, 'search filter finds by mail');
+
+note "search filters in LDAP URLs";
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf', qq{local all all ldap ldapurl="$ldap_url/$ldap_basedn??sub?(|(uid=%25u)(mail=%25u))"});
+$node->reload;
+
+$ENV{"PGPASSWORD"} = 'secret1';
+test_access($node, 'test1', 0, 'search filter finds by uid');
+$ENV{"PGPASSWORD"} = 'secret2';
+test_access($node, 'test2@example.net', 0, 'search filter finds by mail');
+
+# This is not documented: You can combine ldapurl and other ldap*
+# settings. ldapurl is always parsed first, then the other settings
+# override. It might be useful in a case like this.
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf', qq{local all all ldap ldapurl="$ldap_url/$ldap_basedn??sub" ldapsearchfilter="(|(uid=%u)(mail=%u))"});
+$node->reload;
+
+$ENV{"PGPASSWORD"} = 'secret1';
+test_access($node, 'test1', 0, 'combined LDAP URL and search filter');
--
2.11.0 (Apple Git-81)
On 08/09/17 16:33, Peter Eisentraut wrote:
A couple of comments on this patch. I have attached a "fixup" patch on
top of your v4 that should address them.- I think the bracketing of the LDAP URL synopsis is wrong.
- I have dropped the sentence that LDAP URL extensions are not
supported. That sentence was written mainly to point out that filters
are not supported, which they are now. There is nothing beyond filters
and extensions left in LDAP URLs, AFAIK.- We have previously used the ldapsearchattribute a bit strangely. We
use it as both the search filter and as the attribute to return from the
search, but we don't actually care about the returned attribute (we only
use the DN (which is not a real attribute)). That hasn't been a real
problem, but now if you specify a filter, as the code stands it will
always request the "uid" attribute, which won't work if there is no such
attribute. I have found that there is an official way to request "no
attribute", so I have fixed the code to do that.- I was wondering whether we need a way to escape the % (%%?) but it
doesn't seem worth bothering.I have been looking around the Internet how this functionality compares
to other LDAP authentication systems.I didn't see the origin of the %u idea in this thread, but perhaps it
came from Dovecot. But there it's a general configuration file syntax,
nothing specific to LDAP. In nginx you use %(username), which again is
general configuration file syntax. I'm OK with using %u.The original LDAP URL design was adapted from Apache
(https://httpd.apache.org/docs/2.4/mod/mod_authnz_ldap.html#authldapurl).
They combine the attribute filter and the general filter if both are
specified, but I think they got that a bit wrong. The attribute field
in the URL is actually not supposed to be a filter but a return field,
which is also the confusion mentioned above.The PAM module (https://linux.die.net/man/5/pam_ldap) also has a way to
specify a search attribute and a general filter and combines them.Neither of these allows substituting the user name into the search filter.
Great work! Having installed quite a few LDAP-based authentication
setups in the past, I can promise you that pam_ldap is the last tool I
would consider for the job so please don't hold to this as being the
gold standard(!).
My weapon of choice for LDAP deployments on POSIX-based systems is
Arthur De Jong's nss-pam-ldapd (https://arthurdejong.org/nss-pam-ldapd)
which is far more flexible than pam_ldap and fixes a large number of
bugs, including the tendency for pam_ldap to hang infinitely if it can't
contact its LDAP server.
Take a look at nss-pam-ldapd's man page for nslcd.conf and in particular
pam_authz_search - this is exactly the type of filters I would end up
deploying onto servers. This happens a lot in large organisations
whereby getting group memberships updated in the main directory can take
days/weeks whereas someone with root access to the server itself can
hard-code an authentication list of users and/or groups in an LDAP
filter in just a few minutes.
ATB,
Mark.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Sat, Sep 9, 2017 at 3:36 AM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:
For additional entertainment I have written a test suite for this LDAP
authentication functionality. It's not quite robust enough to be run by
default, because it needs a full OpenLDAP installation, but it's been
very helpful for reviewing this patch. Here it is.
Very nice!
+if ($^O eq 'darwin')
+{
+ $slapd = '/usr/local/opt/openldap/libexec/slapd';
+ $ldap_schema_dir = '/usr/local/etc/openldap/schema';
+}
I'm guessing this is the MacPorts location, and someone from that
other tribe that uses Brew can eventually post a patch to make this
look in more places.
+my $ldap_port = int(rand() * 16384) + 49152;
Hmm. I guess ldapi (Unix domain sockets) would be less roulette-like,
but require client side support too.
Here's a change I needed to make to run this here. It seems that to
use "database mdb" I'd need to add a config line to tell it the path
to load back_mdb.so from. I could have done, but I noticed that if I
tell it to use raw ldif files instead it's happy. Does this still
work for you on the systems you tested?
--
Thomas Munro
http://www.enterprisedb.com
Attachments:
0001-fixup-Add-LDAP-authentication-test-suite.patchapplication/octet-stream; name=0001-fixup-Add-LDAP-authentication-test-suite.patchDownload
From 3266b450cda76d69f386525f51754f70a025086b Mon Sep 17 00:00:00 2001
From: Thomas Munro <thomas.munro@enterprisedb.com>
Date: Sat, 9 Sep 2017 13:03:09 +1200
Subject: [PATCH] fixup! Add LDAP authentication test suite
---
src/test/ldap/t/001_auth.pl | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index af9e34d7cf..d8be2de88f 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -19,6 +19,11 @@ elsif ($^O eq 'linux')
$ldap_schema_dir = '/etc/ldap/schema' if -f '/etc/ldap/schema';
$ldap_schema_dir = '/etc/openldap/schema' if -f '/etc/openldap/schema';
}
+elsif ($^O eq 'freebsd')
+{
+ $slapd = '/usr/local/libexec/slapd';
+ $ldap_schema_dir = '/usr/local/etc/openldap/schema';
+}
# make your own edits here
#$slapd = '';
@@ -55,8 +60,7 @@ access to *
by * read
by anonymous auth
-database mdb
-dbnosync
+database ldif
directory $ldap_datadir
suffix "dc=example,dc=net"
--
2.13.5
On Sat, Sep 9, 2017 at 3:33 AM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:
A couple of comments on this patch. I have attached a "fixup" patch on
top of your v4 that should address them.- I think the bracketing of the LDAP URL synopsis is wrong.
+1
- I have dropped the sentence that LDAP URL extensions are not
supported. That sentence was written mainly to point out that filters
are not supported, which they are now. There is nothing beyond filters
and extensions left in LDAP URLs, AFAIK.
+1
- We have previously used the ldapsearchattribute a bit strangely. We
use it as both the search filter and as the attribute to return from the
search, but we don't actually care about the returned attribute (we only
use the DN (which is not a real attribute)). That hasn't been a real
problem, but now if you specify a filter, as the code stands it will
always request the "uid" attribute, which won't work if there is no such
attribute. I have found that there is an official way to request "no
attribute", so I have fixed the code to do that.
Ah. Good.
- I was wondering whether we need a way to escape the % (%%?) but it
doesn't seem worth bothering.I have been looking around the Internet how this functionality compares
to other LDAP authentication systems.I didn't see the origin of the %u idea in this thread, but perhaps it
came from Dovecot. But there it's a general configuration file syntax,
nothing specific to LDAP. In nginx you use %(username), which again is
general configuration file syntax. I'm OK with using %u.The original LDAP URL design was adapted from Apache
(https://httpd.apache.org/docs/2.4/mod/mod_authnz_ldap.html#authldapurl).
They combine the attribute filter and the general filter if both are
specified, but I think they got that a bit wrong. The attribute field
in the URL is actually not supposed to be a filter but a return field,
which is also the confusion mentioned above.The PAM module (https://linux.die.net/man/5/pam_ldap) also has a way to
specify a search attribute and a general filter and combines them.Neither of these allows substituting the user name into the search filter.
I think there would be a case to be made to allow the searchattribute
and the searchfilter both be specified. One typical use would be to use
the first one to locate the user and the second one to "filter" out
certain things, which I think is the approach the PAM and Apache modules
take. This wouldn't work well, however, if you use the %u placeholder,
because then you'd have to explicitly unset ldapsearchattribute, which
would be annoying. So maybe not.
I like this way, because it doesn't leave the user wondering how the
filter is constructed. It's either the user's filter using %u
placeholders or a system-built one, but not a combination where you
have to wonder whether it's an implicit AND, OR or something else...
Please check my patch. I think it's ready to go then.
+1 from me to all your changes except this one:
- buffer_size += user_name_size;
+ buffer_size += user_name_size - 2;
The algorithm is: start with buffer_size = 0; add user_name_size if
you see "%u" but otherwise just add one per character; finally add one
for the terminator. There is no reason to subtract 2, since we didn't
count the "%u" characters. Consider a username of "x" and a search
filter of "%u": your change would underflow buffer_size.
Here's a patch with your fixup squashed (except for the above-noted thing).
Thanks for all your work on this.
--
Thomas Munro
http://www.enterprisedb.com
Attachments:
ldap-search-filters-v5.patchapplication/octet-stream; name=ldap-search-filters-v5.patchDownload
From 2d2cd45d6b1e8f8099eef1872676984b736fbb23 Mon Sep 17 00:00:00 2001
From: Thomas Munro <thomas.munro@enterprisedb.com>
Date: Mon, 17 Jul 2017 10:11:28 +1200
Subject: [PATCH] Allow custom search filters to be configured for LDAP auth.
Before, only filters of the form "(<ldapsearchattribute>=<user>)"
could be used to search an LDAP server. Introduce ldapsearchfilter
so that more general filters can be configured using patterns like
"(|(uid=%u)(mail=%u))" and "(&(uid=%u)(objectClass=posixAccount))".
Also allow search filters to be included in an LDAP URL.
Author: Thomas Munro
Reviewed-By: Peter Eisentraut, Mark Cave-Ayland, Magnus Hagander
Discussion: https://postgr.es/m/CAEepm=0XTkYvMci0WRubZcf_1am8=gP=7oJErpsUfRYcKF2gwg@mail.gmail.com
---
doc/src/sgml/client-auth.sgml | 45 +++++++++++++++++++++++++++---
src/backend/libpq/auth.c | 65 +++++++++++++++++++++++++++++++++++++------
src/backend/libpq/hba.c | 47 ++++++++++++++++++++++---------
src/include/libpq/hba.h | 1 +
4 files changed, 133 insertions(+), 25 deletions(-)
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 1b568683a4..1c3db96134 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -1508,19 +1508,35 @@ omicron bryanh guest1
</listitem>
</varlistentry>
<varlistentry>
+ <term><literal>ldapsearchfilter</literal></term>
+ <listitem>
+ <para>
+ The search filter to use when doing search+bind authentication.
+ Occurrences of <literal>%u</literal> will be replaced with the
+ user name. This allows for more flexible search filters than
+ <literal>ldapsearchattribute</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
<term><literal>ldapurl</literal></term>
<listitem>
<para>
An RFC 4516 LDAP URL. This is an alternative way to write some of the
other LDAP options in a more compact and standard form. The format is
<synopsis>
-ldap://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replaceable>basedn</replaceable>[?[<replaceable>attribute</replaceable>][?[<replaceable>scope</replaceable>]]]
+ldap://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replaceable>basedn</replaceable>[?[<replaceable>attribute</replaceable>][?[<replaceable>scope</replaceable>][?[<replaceable>filter</replaceable>]]]]
</synopsis>
<replaceable>scope</replaceable> must be one
of <literal>base</literal>, <literal>one</literal>, <literal>sub</literal>,
- typically the latter. Only one attribute is used, and some other
- components of standard LDAP URLs such as filters and extensions are
- not supported.
+ typically the last. <replaceable>attribute</replaceable> can
+ nominate a single attribute, in which case it is used as a value for
+ <literal>ldapsearchattribute</literal>. If
+ <replaceable>attribute</replaceable> is empty then
+ <replaceable>filter</replaceable> can be used as a value for
+ <literal>ldapsearchfilter</literal>. When specifying a search filter
+ as part of a URL, the special token <literal>%u</literal> standing
+ for the user name must be written as <literal>%25u</literal>.
</para>
<para>
@@ -1550,6 +1566,17 @@ ldap://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replac
</para>
<para>
+ When using search+bind mode, the search can be performed using a single
+ attribute specified with <literal>ldapsearchattribute</literal>, or using
+ a custom search filter specified with
+ <literal>ldapsearchfilter</literal>.
+ Specifying <literal>ldapsearchattribute=foo</literal> is equivalent to
+ specifying <literal>ldapsearchfilter="(foo=%u)"</literal>. If neither
+ option is specified the default is
+ <literal>ldapsearchattribute=uid</literal>.
+ </para>
+
+ <para>
Here is an example for a simple-bind LDAP configuration:
<programlisting>
host ... ldap ldapserver=ldap.example.net ldapprefix="cn=" ldapsuffix=", dc=example, dc=net"
@@ -1584,6 +1611,16 @@ host ... ldap ldapurl="ldap://ldap.example.net/dc=example,dc=net?uid?sub"
same URL format, so it will be easier to share the configuration.
</para>
+ <para>
+ Here is an example for a search+bind configuration that uses
+ <literal>ldapsearchfilter</literal> instead of
+ <literal>ldapsearchattribute</literal> to allow authentication by
+ user ID or email address:
+<programlisting>
+host ... ldap ldapserver=ldap.example.net ldapbasedn="dc=example, dc=net" ldapsearchfilter="(|(uid=%u)(mail=%u))"
+</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 cb30fc7b71..1b1c815910 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2395,6 +2395,55 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
}
/*
+ * Return a newly allocated C string copied from "pattern" with all
+ * occurrences of "%u" replaced with "user_name".
+ */
+static char *
+FormatSearchFilter(const char *pattern, const char *user_name)
+{
+ size_t user_name_size = strlen(user_name);
+ size_t buffer_size = 0;
+ const char *in;
+ char *out;
+ char *result;
+
+ /* Compute the size of the output buffer. */
+ in = pattern;
+ while (*in != '\0')
+ {
+ if (in[0] == '%' && in[1] == 'u')
+ {
+ buffer_size += user_name_size;
+ in += 2;
+ }
+ else
+ {
+ buffer_size++;
+ in++;
+ }
+ }
+ buffer_size++; /* terminator */
+
+ /* Build the output string. */
+ out = result = palloc(buffer_size);
+ in = pattern;
+ while (*in != '\0')
+ {
+ if (in[0] == '%' && in[1] == 'u')
+ {
+ memcpy(out, user_name, user_name_size);
+ out += user_name_size;
+ in += 2;
+ }
+ else
+ *out++ = *in++;
+ }
+ *out = '\0'; /* terminator */
+
+ return result;
+}
+
+/*
* Perform LDAP authentication
*/
static int
@@ -2437,7 +2486,7 @@ CheckLDAPAuth(Port *port)
char *filter;
LDAPMessage *search_message;
LDAPMessage *entry;
- char *attributes[2];
+ char *attributes[] = { LDAP_NO_ATTRS, NULL };
char *dn;
char *c;
int count;
@@ -2479,13 +2528,13 @@ CheckLDAPAuth(Port *port)
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 = psprintf("(%s=%s)",
- attributes[0],
- port->user_name);
+ /* Build a custom filter or a single attribute filter? */
+ if (port->hba->ldapsearchfilter)
+ filter = FormatSearchFilter(port->hba->ldapsearchfilter, port->user_name);
+ else if (port->hba->ldapsearchattribute)
+ filter = psprintf("(%s=%s)", port->hba->ldapsearchattribute, port->user_name);
+ else
+ filter = psprintf("(uid=%s)", port->user_name);
r = ldap_search_s(ldap,
port->hba->ldapbasedn,
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 42afead9fd..3a482bead4 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -1505,22 +1505,24 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
/*
* 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.
+ * ldapbasedn, ldapbinddn, ldapbindpasswd and one of
+ * ldapsearchattribute or ldapsearchfilter. Disallow mixing these
+ * parameters.
*/
if (parsedline->ldapprefix || parsedline->ldapsuffix)
{
if (parsedline->ldapbasedn ||
parsedline->ldapbinddn ||
parsedline->ldapbindpasswd ||
- parsedline->ldapsearchattribute)
+ parsedline->ldapsearchattribute ||
+ parsedline->ldapsearchfilter)
{
ereport(elevel,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
- errmsg("cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, or ldapurl together with ldapprefix"),
+ errmsg("cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter or ldapurl together with ldapprefix"),
errcontext("line %d of configuration file \"%s\"",
line_num, HbaFileName)));
- *err_msg = "cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, or ldapurl together with ldapprefix";
+ *err_msg = "cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter or ldapurl together with ldapprefix";
return NULL;
}
}
@@ -1534,6 +1536,22 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
*err_msg = "authentication method \"ldap\" requires argument \"ldapbasedn\", \"ldapprefix\", or \"ldapsuffix\" to be set";
return NULL;
}
+
+ /*
+ * When using search+bind, you can either use a simple attribute
+ * (defaulting to "uid") or a fully custom search filter. You can't
+ * do both.
+ */
+ if (parsedline->ldapsearchattribute && parsedline->ldapsearchfilter)
+ {
+ ereport(elevel,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("cannot use ldapsearchattribute together with ldapsearchfilter"),
+ errcontext("line %d of configuration file \"%s\"",
+ line_num, HbaFileName)));
+ *err_msg = "cannot use ldapsearchattribute together with ldapsearchfilter";
+ return NULL;
+ }
}
if (parsedline->auth_method == uaRADIUS)
@@ -1729,14 +1747,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
hbaline->ldapsearchattribute = pstrdup(urldata->lud_attrs[0]); /* only use first one */
hbaline->ldapscope = urldata->lud_scope;
if (urldata->lud_filter)
- {
- ereport(elevel,
- (errcode(ERRCODE_CONFIG_FILE_ERROR),
- errmsg("filters not supported in LDAP URLs")));
- *err_msg = "filters not supported in LDAP URLs";
- ldap_free_urldesc(urldata);
- return false;
- }
+ hbaline->ldapsearchfilter = pstrdup(urldata->lud_filter);
ldap_free_urldesc(urldata);
#else /* not OpenLDAP */
ereport(elevel,
@@ -1788,6 +1799,11 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
REQUIRE_AUTH_OPTION(uaLDAP, "ldapsearchattribute", "ldap");
hbaline->ldapsearchattribute = pstrdup(val);
}
+ else if (strcmp(name, "ldapsearchfilter") == 0)
+ {
+ REQUIRE_AUTH_OPTION(uaLDAP, "ldapsearchfilter", "ldap");
+ hbaline->ldapsearchfilter = pstrdup(val);
+ }
else if (strcmp(name, "ldapbasedn") == 0)
{
REQUIRE_AUTH_OPTION(uaLDAP, "ldapbasedn", "ldap");
@@ -2266,6 +2282,11 @@ gethba_options(HbaLine *hba)
CStringGetTextDatum(psprintf("ldapsearchattribute=%s",
hba->ldapsearchattribute));
+ if (hba->ldapsearchfilter)
+ options[noptions++] =
+ CStringGetTextDatum(psprintf("ldapsearchfilter=%s",
+ hba->ldapsearchfilter));
+
if (hba->ldapscope)
options[noptions++] =
CStringGetTextDatum(psprintf("ldapscope=%d", hba->ldapscope));
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 07d92d4f9f..e711bee8bf 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -80,6 +80,7 @@ typedef struct HbaLine
char *ldapbinddn;
char *ldapbindpasswd;
char *ldapsearchattribute;
+ char *ldapsearchfilter;
char *ldapbasedn;
int ldapscope;
char *ldapprefix;
--
2.13.5
On 9/8/17 13:24, Mark Cave-Ayland wrote:
My weapon of choice for LDAP deployments on POSIX-based systems is
Arthur De Jong's nss-pam-ldapd (https://arthurdejong.org/nss-pam-ldapd)
which is far more flexible than pam_ldap and fixes a large number of
bugs, including the tendency for pam_ldap to hang infinitely if it can't
contact its LDAP server.Take a look at nss-pam-ldapd's man page for nslcd.conf and in particular
pam_authz_search - this is exactly the type of filters I would end up
deploying onto servers. This happens a lot in large organisations
whereby getting group memberships updated in the main directory can take
days/weeks whereas someone with root access to the server itself can
hard-code an authentication list of users and/or groups in an LDAP
filter in just a few minutes.
Thomas, would you consider using the placeholder syntax described at
<https://arthurdejong.org/nss-pam-ldapd/nslcd.conf.5> under
pam_authz_search?
--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 9/8/17 21:31, Thomas Munro wrote:
+if ($^O eq 'darwin') +{ + $slapd = '/usr/local/opt/openldap/libexec/slapd'; + $ldap_schema_dir = '/usr/local/etc/openldap/schema'; +}I'm guessing this is the MacPorts location, and someone from that
other tribe that uses Brew can eventually post a patch to make this
look in more places.
Or the other way around :)
+my $ldap_port = int(rand() * 16384) + 49152;
Hmm. I guess ldapi (Unix domain sockets) would be less roulette-like,
but require client side support too.
Yeah, issue similar to the SSL tests. The above formula is what
PostgresNode uses.
Here's a change I needed to make to run this here. It seems that to
use "database mdb" I'd need to add a config line to tell it the path
to load back_mdb.so from. I could have done, but I noticed that if I
tell it to use raw ldif files instead it's happy. Does this still
work for you on the systems you tested?
Yes, that seems like a better choice.
--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Tue, Sep 12, 2017 at 7:21 AM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:
On 9/8/17 13:24, Mark Cave-Ayland wrote:
My weapon of choice for LDAP deployments on POSIX-based systems is
Arthur De Jong's nss-pam-ldapd (https://arthurdejong.org/nss-pam-ldapd)
which is far more flexible than pam_ldap and fixes a large number of
bugs, including the tendency for pam_ldap to hang infinitely if it can't
contact its LDAP server.Take a look at nss-pam-ldapd's man page for nslcd.conf and in particular
pam_authz_search - this is exactly the type of filters I would end up
deploying onto servers. This happens a lot in large organisations
whereby getting group memberships updated in the main directory can take
days/weeks whereas someone with root access to the server itself can
hard-code an authentication list of users and/or groups in an LDAP
filter in just a few minutes.Thomas, would you consider using the placeholder syntax described at
<https://arthurdejong.org/nss-pam-ldapd/nslcd.conf.5> under
pam_authz_search?
Sounds good. Here it is with $username. It's nice not to have to
escape any characters in URLs. I suppose more keywords could be added
in follow-up patches if someone thinks that would be useful
($hostname, $dbname, ...?). I got sick of that buffer sizing code and
changed it to use StringInfo. Here also are your test patches tweaked
slightly: 0002 just adds FreeBSD support as per previous fixup and
0003 changes to $username.
--
Thomas Munro
http://www.enterprisedb.com
Attachments:
0001-Allow-custom-search-filters-to-be-configured-for-LDA.patchapplication/octet-stream; name=0001-Allow-custom-search-filters-to-be-configured-for-LDA.patchDownload
From 1b98d13d969a9301f24023f08d6a974f2a86585a Mon Sep 17 00:00:00 2001
From: Thomas Munro <thomas.munro@enterprisedb.com>
Date: Mon, 17 Jul 2017 10:11:28 +1200
Subject: [PATCH 1/3] Allow custom search filters to be configured for LDAP
auth.
Before, only filters of the form "(<ldapsearchattribute>=<user>)"
could be used to search an LDAP server. Introduce ldapsearchfilter
so that more general filters can be configured using patterns like
"(|(uid=$username)(mail=$username))" and "(&(uid=$username)
(objectClass=posixAccount))". Also allow search filters to be
included in an LDAP URL.
Author: Thomas Munro
Reviewed-By: Peter Eisentraut, Mark Cave-Ayland, Magnus Hagander
Discussion: https://postgr.es/m/CAEepm=0XTkYvMci0WRubZcf_1am8=gP=7oJErpsUfRYcKF2gwg@mail.gmail.com
---
doc/src/sgml/client-auth.sgml | 43 +++++++++++++++++++++++++++++++++++----
src/backend/libpq/auth.c | 45 +++++++++++++++++++++++++++++++++--------
src/backend/libpq/hba.c | 47 +++++++++++++++++++++++++++++++------------
src/include/libpq/hba.h | 1 +
4 files changed, 111 insertions(+), 25 deletions(-)
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 1b568683a4..405bf26832 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -1508,19 +1508,33 @@ omicron bryanh guest1
</listitem>
</varlistentry>
<varlistentry>
+ <term><literal>ldapsearchfilter</literal></term>
+ <listitem>
+ <para>
+ The search filter to use when doing search+bind authentication.
+ Occurrences of <literal>$username</literal> will be replaced with the
+ user name. This allows for more flexible search filters than
+ <literal>ldapsearchattribute</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
<term><literal>ldapurl</literal></term>
<listitem>
<para>
An RFC 4516 LDAP URL. This is an alternative way to write some of the
other LDAP options in a more compact and standard form. The format is
<synopsis>
-ldap://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replaceable>basedn</replaceable>[?[<replaceable>attribute</replaceable>][?[<replaceable>scope</replaceable>]]]
+ldap://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replaceable>basedn</replaceable>[?[<replaceable>attribute</replaceable>][?[<replaceable>scope</replaceable>][?[<replaceable>filter</replaceable>]]]]
</synopsis>
<replaceable>scope</replaceable> must be one
of <literal>base</literal>, <literal>one</literal>, <literal>sub</literal>,
- typically the latter. Only one attribute is used, and some other
- components of standard LDAP URLs such as filters and extensions are
- not supported.
+ typically the last. <replaceable>attribute</replaceable> can
+ nominate a single attribute, in which case it is used as a value for
+ <literal>ldapsearchattribute</literal>. If
+ <replaceable>attribute</replaceable> is empty then
+ <replaceable>filter</replaceable> can be used as a value for
+ <literal>ldapsearchfilter</literal>.
</para>
<para>
@@ -1550,6 +1564,17 @@ ldap://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replac
</para>
<para>
+ When using search+bind mode, the search can be performed using a single
+ attribute specified with <literal>ldapsearchattribute</literal>, or using
+ a custom search filter specified with
+ <literal>ldapsearchfilter</literal>.
+ Specifying <literal>ldapsearchattribute=foo</literal> is equivalent to
+ specifying <literal>ldapsearchfilter="(foo=$username)"</literal>. If neither
+ option is specified the default is
+ <literal>ldapsearchattribute=uid</literal>.
+ </para>
+
+ <para>
Here is an example for a simple-bind LDAP configuration:
<programlisting>
host ... ldap ldapserver=ldap.example.net ldapprefix="cn=" ldapsuffix=", dc=example, dc=net"
@@ -1584,6 +1609,16 @@ host ... ldap ldapurl="ldap://ldap.example.net/dc=example,dc=net?uid?sub"
same URL format, so it will be easier to share the configuration.
</para>
+ <para>
+ Here is an example for a search+bind configuration that uses
+ <literal>ldapsearchfilter</literal> instead of
+ <literal>ldapsearchattribute</literal> to allow authentication by
+ user ID or email address:
+<programlisting>
+host ... ldap ldapserver=ldap.example.net ldapbasedn="dc=example, dc=net" ldapsearchfilter="(|(uid=$username)(mail=$username))"
+</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 cb30fc7b71..f8bb135384 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2394,6 +2394,35 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
return STATUS_OK;
}
+/* Placeholders recognized by FormatSearchFilter. For now just one. */
+#define LPH_USERNAME "$username"
+#define LPH_USERNAME_LEN (sizeof(LPH_USERNAME) - 1)
+
+/*
+ * Return a newly allocated C string copied from "pattern" with all
+ * occurrences of the placeholder "$username" replaced with "user_name".
+ */
+static char *
+FormatSearchFilter(const char *pattern, const char *user_name)
+{
+ StringInfoData output;
+ size_t user_name_size = strlen(user_name);
+
+ initStringInfo(&output);
+ while (*pattern != '\0')
+ {
+ if (strncmp(pattern, LPH_USERNAME, LPH_USERNAME_LEN) == 0)
+ {
+ appendStringInfoString(&output, user_name);
+ pattern += LPH_USERNAME_LEN;
+ }
+ else
+ appendStringInfoChar(&output, *pattern++);
+ }
+
+ return output.data;
+}
+
/*
* Perform LDAP authentication
*/
@@ -2437,7 +2466,7 @@ CheckLDAPAuth(Port *port)
char *filter;
LDAPMessage *search_message;
LDAPMessage *entry;
- char *attributes[2];
+ char *attributes[] = { LDAP_NO_ATTRS, NULL };
char *dn;
char *c;
int count;
@@ -2479,13 +2508,13 @@ CheckLDAPAuth(Port *port)
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 = psprintf("(%s=%s)",
- attributes[0],
- port->user_name);
+ /* Build a custom filter or a single attribute filter? */
+ if (port->hba->ldapsearchfilter)
+ filter = FormatSearchFilter(port->hba->ldapsearchfilter, port->user_name);
+ else if (port->hba->ldapsearchattribute)
+ filter = psprintf("(%s=%s)", port->hba->ldapsearchattribute, port->user_name);
+ else
+ filter = psprintf("(uid=%s)", port->user_name);
r = ldap_search_s(ldap,
port->hba->ldapbasedn,
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 42afead9fd..3a482bead4 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -1505,22 +1505,24 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
/*
* 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.
+ * ldapbasedn, ldapbinddn, ldapbindpasswd and one of
+ * ldapsearchattribute or ldapsearchfilter. Disallow mixing these
+ * parameters.
*/
if (parsedline->ldapprefix || parsedline->ldapsuffix)
{
if (parsedline->ldapbasedn ||
parsedline->ldapbinddn ||
parsedline->ldapbindpasswd ||
- parsedline->ldapsearchattribute)
+ parsedline->ldapsearchattribute ||
+ parsedline->ldapsearchfilter)
{
ereport(elevel,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
- errmsg("cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, or ldapurl together with ldapprefix"),
+ errmsg("cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter or ldapurl together with ldapprefix"),
errcontext("line %d of configuration file \"%s\"",
line_num, HbaFileName)));
- *err_msg = "cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, or ldapurl together with ldapprefix";
+ *err_msg = "cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter or ldapurl together with ldapprefix";
return NULL;
}
}
@@ -1534,6 +1536,22 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
*err_msg = "authentication method \"ldap\" requires argument \"ldapbasedn\", \"ldapprefix\", or \"ldapsuffix\" to be set";
return NULL;
}
+
+ /*
+ * When using search+bind, you can either use a simple attribute
+ * (defaulting to "uid") or a fully custom search filter. You can't
+ * do both.
+ */
+ if (parsedline->ldapsearchattribute && parsedline->ldapsearchfilter)
+ {
+ ereport(elevel,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("cannot use ldapsearchattribute together with ldapsearchfilter"),
+ errcontext("line %d of configuration file \"%s\"",
+ line_num, HbaFileName)));
+ *err_msg = "cannot use ldapsearchattribute together with ldapsearchfilter";
+ return NULL;
+ }
}
if (parsedline->auth_method == uaRADIUS)
@@ -1729,14 +1747,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
hbaline->ldapsearchattribute = pstrdup(urldata->lud_attrs[0]); /* only use first one */
hbaline->ldapscope = urldata->lud_scope;
if (urldata->lud_filter)
- {
- ereport(elevel,
- (errcode(ERRCODE_CONFIG_FILE_ERROR),
- errmsg("filters not supported in LDAP URLs")));
- *err_msg = "filters not supported in LDAP URLs";
- ldap_free_urldesc(urldata);
- return false;
- }
+ hbaline->ldapsearchfilter = pstrdup(urldata->lud_filter);
ldap_free_urldesc(urldata);
#else /* not OpenLDAP */
ereport(elevel,
@@ -1788,6 +1799,11 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
REQUIRE_AUTH_OPTION(uaLDAP, "ldapsearchattribute", "ldap");
hbaline->ldapsearchattribute = pstrdup(val);
}
+ else if (strcmp(name, "ldapsearchfilter") == 0)
+ {
+ REQUIRE_AUTH_OPTION(uaLDAP, "ldapsearchfilter", "ldap");
+ hbaline->ldapsearchfilter = pstrdup(val);
+ }
else if (strcmp(name, "ldapbasedn") == 0)
{
REQUIRE_AUTH_OPTION(uaLDAP, "ldapbasedn", "ldap");
@@ -2266,6 +2282,11 @@ gethba_options(HbaLine *hba)
CStringGetTextDatum(psprintf("ldapsearchattribute=%s",
hba->ldapsearchattribute));
+ if (hba->ldapsearchfilter)
+ options[noptions++] =
+ CStringGetTextDatum(psprintf("ldapsearchfilter=%s",
+ hba->ldapsearchfilter));
+
if (hba->ldapscope)
options[noptions++] =
CStringGetTextDatum(psprintf("ldapscope=%d", hba->ldapscope));
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 07d92d4f9f..e711bee8bf 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -80,6 +80,7 @@ typedef struct HbaLine
char *ldapbinddn;
char *ldapbindpasswd;
char *ldapsearchattribute;
+ char *ldapsearchfilter;
char *ldapbasedn;
int ldapscope;
char *ldapprefix;
--
2.13.5
0002-Add-LDAP-authentication-test-suite.patchapplication/octet-stream; name=0002-Add-LDAP-authentication-test-suite.patchDownload
From 84474703ed0309e84e3d62e02ed4e5d6f0551725 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter_e@gmx.net>
Date: Fri, 8 Sep 2017 10:33:49 -0400
Subject: [PATCH 2/3] Add LDAP authentication test suite
---
src/test/Makefile | 2 +-
src/test/ldap/.gitignore | 2 +
src/test/ldap/Makefile | 20 +++++++
src/test/ldap/README | 16 +++++
src/test/ldap/data.ldif | 19 ++++++
src/test/ldap/t/001_auth.pl | 143 ++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 201 insertions(+), 1 deletion(-)
create mode 100644 src/test/ldap/.gitignore
create mode 100644 src/test/ldap/Makefile
create mode 100644 src/test/ldap/README
create mode 100644 src/test/ldap/data.ldif
create mode 100644 src/test/ldap/t/001_auth.pl
diff --git a/src/test/Makefile b/src/test/Makefile
index dbfa799a84..193b977bf3 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -17,7 +17,7 @@ SUBDIRS = perl regress isolation modules authentication recovery subscription
# We don't build or execute examples/, locale/, or thread/ by default,
# but we do want "make clean" etc to recurse into them. Likewise for ssl/,
# because the SSL test suite is not secure to run on a multi-user system.
-ALWAYS_SUBDIRS = examples locale thread ssl
+ALWAYS_SUBDIRS = examples ldap locale thread ssl
# We want to recurse to all subdirs for all standard targets, except that
# installcheck and install should not recurse into the subdirectory "modules".
diff --git a/src/test/ldap/.gitignore b/src/test/ldap/.gitignore
new file mode 100644
index 0000000000..871e943d50
--- /dev/null
+++ b/src/test/ldap/.gitignore
@@ -0,0 +1,2 @@
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/ldap/Makefile b/src/test/ldap/Makefile
new file mode 100644
index 0000000000..9dd1bbeade
--- /dev/null
+++ b/src/test/ldap/Makefile
@@ -0,0 +1,20 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/ldap
+#
+# Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/ldap/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = src/test/ldap
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+check:
+ $(prove_check)
+
+clean distclean maintainer-clean:
+ rm -rf tmp_check
diff --git a/src/test/ldap/README b/src/test/ldap/README
new file mode 100644
index 0000000000..20a7a0b5de
--- /dev/null
+++ b/src/test/ldap/README
@@ -0,0 +1,16 @@
+src/test/ldap/README
+
+Tests for LDAP functionality
+============================
+
+This directory contains a test suite for LDAP functionality. This
+requires a full OpenLDAP installation, including server and client
+tools, and is therefore kept separate and not run by default.
+
+
+Running the tests
+=================
+
+ make check
+
+NOTE: This requires the --enable-tap-tests argument to configure.
diff --git a/src/test/ldap/data.ldif b/src/test/ldap/data.ldif
new file mode 100644
index 0000000000..b30604e1f0
--- /dev/null
+++ b/src/test/ldap/data.ldif
@@ -0,0 +1,19 @@
+dn: dc=example,dc=net
+objectClass: top
+objectClass: dcObject
+objectClass: organization
+dc: example
+o: ExmapleCo
+
+dn: uid=test1,dc=example,dc=net
+objectClass: inetOrgPerson
+objectClass: posixAccount
+uid: test1
+sn: Lastname
+givenName: Firstname
+cn: First Test User
+displayName: First Test User
+uidNumber: 101
+gidNumber: 100
+homeDirectory: /home/test1
+mail: test1@example.net
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
new file mode 100644
index 0000000000..d8be2de88f
--- /dev/null
+++ b/src/test/ldap/t/001_auth.pl
@@ -0,0 +1,143 @@
+use strict;
+use warnings;
+use TestLib;
+use PostgresNode;
+use Test::More tests => 9;
+
+my ($slapd, $ldap_bin_dir, $ldap_schema_dir);
+
+$ldap_bin_dir = undef; # usually in PATH
+
+if ($^O eq 'darwin')
+{
+ $slapd = '/usr/local/opt/openldap/libexec/slapd';
+ $ldap_schema_dir = '/usr/local/etc/openldap/schema';
+}
+elsif ($^O eq 'linux')
+{
+ $slapd = '/usr/sbin/slapd';
+ $ldap_schema_dir = '/etc/ldap/schema' if -f '/etc/ldap/schema';
+ $ldap_schema_dir = '/etc/openldap/schema' if -f '/etc/openldap/schema';
+}
+elsif ($^O eq 'freebsd')
+{
+ $slapd = '/usr/local/libexec/slapd';
+ $ldap_schema_dir = '/usr/local/etc/openldap/schema';
+}
+
+# make your own edits here
+#$slapd = '';
+#$ldap_bin_dir = '';
+#$ldap_schema_dir = '';
+
+$ENV{PATH} = "$ldap_bin_dir:$ENV{PATH}" if $ldap_bin_dir;
+
+my $ldap_datadir = "${TestLib::tmp_check}/openldap-data";
+my $slapd_conf = "${TestLib::tmp_check}/slapd.conf";
+my $slapd_pidfile = "${TestLib::tmp_check}/slapd.pid";
+my $slapd_logfile = "${TestLib::tmp_check}/slapd.log";
+my $ldap_conf = "${TestLib::tmp_check}/ldap.conf";
+my $ldap_server = '127.0.0.1';
+my $ldap_port = int(rand() * 16384) + 49152;
+my $ldap_url = "ldap://$ldap_server:$ldap_port";
+my $ldap_basedn = 'dc=example,dc=net';
+my $ldap_rootdn = 'cn=Manager,dc=example,dc=net';
+my $ldap_rootpw = 'secret';
+my $ldap_pwfile = "${TestLib::tmp_check}/ldappassword";
+
+note "setting up slapd";
+
+append_to_file($slapd_conf,
+qq{include $ldap_schema_dir/core.schema
+include $ldap_schema_dir/cosine.schema
+include $ldap_schema_dir/nis.schema
+include $ldap_schema_dir/inetorgperson.schema
+
+pidfile $slapd_pidfile
+logfile $slapd_logfile
+
+access to *
+ by * read
+ by anonymous auth
+
+database ldif
+directory $ldap_datadir
+
+suffix "dc=example,dc=net"
+rootdn "$ldap_rootdn"
+rootpw $ldap_rootpw});
+
+mkdir $ldap_datadir or die;
+
+system_or_bail $slapd, '-f', $slapd_conf, '-h', $ldap_url;
+
+END
+{
+ kill 'INT', `cat $slapd_pidfile` if -f $slapd_pidfile;
+}
+
+append_to_file($ldap_pwfile, $ldap_rootpw);
+chmod 0600, $ldap_pwfile or die;
+
+$ENV{'LDAPURI'} = $ldap_url;
+$ENV{'LDAPBINDDN'} = $ldap_rootdn;
+
+note "loading LDAP data";
+
+system_or_bail 'ldapadd', '-x', '-y', $ldap_pwfile, '-f', 'data.ldif';
+system_or_bail 'ldappasswd', '-x', '-y', $ldap_pwfile, '-s', 'secret1', 'uid=test1,dc=example,dc=net';
+
+note "setting up PostgreSQL instance";
+
+my $node = get_new_node('node');
+$node->init;
+$node->start;
+
+$node->safe_psql('postgres', 'CREATE USER test0;');
+$node->safe_psql('postgres', 'CREATE USER test1;');
+
+note "running tests";
+
+sub test_access
+{
+ my ($node, $role, $expected_res, $test_name) = @_;
+
+ my $res = $node->psql('postgres', 'SELECT 1', extra_params => [ '-U', $role ]);
+ is($res, $expected_res, $test_name);
+}
+
+note "simple bind";
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf', qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapprefix="uid=" ldapsuffix=",dc=example,dc=net"});
+$node->reload;
+
+$ENV{"PGPASSWORD"} = 'wrong';
+test_access($node, 'test0', 2, 'simple bind authentication fails if user not found in LDAP');
+test_access($node, 'test1', 2, 'simple bind authentication fails with wrong password');
+$ENV{"PGPASSWORD"} = 'secret1';
+test_access($node, 'test1', 0, 'simple bind authentication succeeds');
+
+note "search+bind";
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf', qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapbasedn="$ldap_basedn"});
+$node->reload;
+
+$ENV{"PGPASSWORD"} = 'wrong';
+test_access($node, 'test0', 2, 'search+bind authentication fails if user not found in LDAP');
+test_access($node, 'test1', 2, 'search+bind authentication fails with wrong password');
+$ENV{"PGPASSWORD"} = 'secret1';
+test_access($node, 'test1', 0, 'search+bind authentication succeeds');
+
+note "LDAP URLs";
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf', qq{local all all ldap ldapurl="$ldap_url/$ldap_basedn?uid?sub"});
+$node->reload;
+
+$ENV{"PGPASSWORD"} = 'wrong';
+test_access($node, 'test0', 2, 'search+bind with LDAP URL authentication fails if user not found in LDAP');
+test_access($node, 'test1', 2, 'search+bind with LDAP URL authentication fails with wrong password');
+$ENV{"PGPASSWORD"} = 'secret1';
+test_access($node, 'test1', 0, 'search+bind with LDAP URL authentication succeeds');
--
2.13.5
0003-Add-tests-for-ldapsearchfilter-functionality.patchapplication/octet-stream; name=0003-Add-tests-for-ldapsearchfilter-functionality.patchDownload
From 30d3f741a947f11b0c16dc408bb2ee0ef97b4a39 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter_e@gmx.net>
Date: Fri, 8 Sep 2017 10:52:08 -0400
Subject: [PATCH 3/3] Add tests for ldapsearchfilter functionality
---
src/test/ldap/data.ldif | 13 +++++++++++++
src/test/ldap/t/001_auth.pl | 36 +++++++++++++++++++++++++++++++++++-
2 files changed, 48 insertions(+), 1 deletion(-)
diff --git a/src/test/ldap/data.ldif b/src/test/ldap/data.ldif
index b30604e1f0..8d82c284b1 100644
--- a/src/test/ldap/data.ldif
+++ b/src/test/ldap/data.ldif
@@ -17,3 +17,16 @@ uidNumber: 101
gidNumber: 100
homeDirectory: /home/test1
mail: test1@example.net
+
+dn: uid=test2,dc=example,dc=net
+objectClass: inetOrgPerson
+objectClass: posixAccount
+uid: test2
+sn: Lastname
+givenName: Firstname
+cn: Second Test User
+displayName: Second Test User
+uidNumber: 102
+gidNumber: 100
+homeDirectory: /home/test2
+mail: test2@example.net
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index d8be2de88f..b6da6bfa18 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -2,7 +2,7 @@ use strict;
use warnings;
use TestLib;
use PostgresNode;
-use Test::More tests => 9;
+use Test::More tests => 14;
my ($slapd, $ldap_bin_dir, $ldap_schema_dir);
@@ -86,6 +86,7 @@ note "loading LDAP data";
system_or_bail 'ldapadd', '-x', '-y', $ldap_pwfile, '-f', 'data.ldif';
system_or_bail 'ldappasswd', '-x', '-y', $ldap_pwfile, '-s', 'secret1', 'uid=test1,dc=example,dc=net';
+system_or_bail 'ldappasswd', '-x', '-y', $ldap_pwfile, '-s', 'secret2', 'uid=test2,dc=example,dc=net';
note "setting up PostgreSQL instance";
@@ -95,6 +96,7 @@ $node->start;
$node->safe_psql('postgres', 'CREATE USER test0;');
$node->safe_psql('postgres', 'CREATE USER test1;');
+$node->safe_psql('postgres', 'CREATE USER "test2@example.net";');
note "running tests";
@@ -141,3 +143,35 @@ test_access($node, 'test0', 2, 'search+bind with LDAP URL authentication fails i
test_access($node, 'test1', 2, 'search+bind with LDAP URL authentication fails with wrong password');
$ENV{"PGPASSWORD"} = 'secret1';
test_access($node, 'test1', 0, 'search+bind with LDAP URL authentication succeeds');
+
+note "search filters";
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf', qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapbasedn="$ldap_basedn" ldapsearchfilter="(|(uid=\$username)(mail=\$username))"});
+$node->reload;
+
+$ENV{"PGPASSWORD"} = 'secret1';
+test_access($node, 'test1', 0, 'search filter finds by uid');
+$ENV{"PGPASSWORD"} = 'secret2';
+test_access($node, 'test2@example.net', 0, 'search filter finds by mail');
+
+note "search filters in LDAP URLs";
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf', qq{local all all ldap ldapurl="$ldap_url/$ldap_basedn??sub?(|(uid=\$username)(mail=\$username))"});
+$node->reload;
+
+$ENV{"PGPASSWORD"} = 'secret1';
+test_access($node, 'test1', 0, 'search filter finds by uid');
+$ENV{"PGPASSWORD"} = 'secret2';
+test_access($node, 'test2@example.net', 0, 'search filter finds by mail');
+
+# This is not documented: You can combine ldapurl and other ldap*
+# settings. ldapurl is always parsed first, then the other settings
+# override. It might be useful in a case like this.
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf', qq{local all all ldap ldapurl="$ldap_url/$ldap_basedn??sub" ldapsearchfilter="(|(uid=\$username)(mail=\$username))"});
+$node->reload;
+
+$ENV{"PGPASSWORD"} = 'secret1';
+test_access($node, 'test1', 0, 'combined LDAP URL and search filter');
--
2.13.5
On 9/11/17 23:58, Thomas Munro wrote:
Sounds good. Here it is with $username. It's nice not to have to
escape any characters in URLs. I suppose more keywords could be added
in follow-up patches if someone thinks that would be useful
($hostname, $dbname, ...?). I got sick of that buffer sizing code and
changed it to use StringInfo. Here also are your test patches tweaked
slightly: 0002 just adds FreeBSD support as per previous fixup and
0003 changes to $username.
Committed the feature patch.
Any further thoughts on the test suite? Otherwise I'll commit it as we
have it, for manual use.
--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Wed, Sep 13, 2017 at 1:55 AM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:
On 9/11/17 23:58, Thomas Munro wrote:
Sounds good. Here it is with $username. It's nice not to have to
escape any characters in URLs. I suppose more keywords could be added
in follow-up patches if someone thinks that would be useful
($hostname, $dbname, ...?). I got sick of that buffer sizing code and
changed it to use StringInfo. Here also are your test patches tweaked
slightly: 0002 just adds FreeBSD support as per previous fixup and
0003 changes to $username.Committed the feature patch.
Thanks!
Any further thoughts on the test suite? Otherwise I'll commit it as we
have it, for manual use.
I wonder if there is a reasonable way to indicate or determine whether
you have slapd installed so that check-world could run this test...
--
Thomas Munro
http://www.enterprisedb.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Wed, Sep 13, 2017 at 8:04 AM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:
I wonder if there is a reasonable way to indicate or determine whether
you have slapd installed so that check-world could run this test...
Module::Install's requires_external_bin is one:
http://search.cpan.org/~ether/Module-Install-1.18/lib/Module/Install.pod#requires_external_bin
But the bar to add a new module dependency is high.
Another trick that you could use is to attempt to run it, see if it is
present by checking for 127 sounds fragile though, for Windows
particularly, and otherwise skip the test.
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 9/12/17 19:04, Thomas Munro wrote:
Any further thoughts on the test suite? Otherwise I'll commit it as we
have it, for manual use.
done
I wonder if there is a reasonable way to indicate or determine whether
you have slapd installed so that check-world could run this test...
The other problem is the open TCP/IP socket, so we can't really run it
by default anyway, like the SSL tests.
--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers