Commit 83aaac41 authored by Peter Eisentraut's avatar Peter Eisentraut

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
parent 35e15688
...@@ -1507,6 +1507,17 @@ omicron bryanh guest1 ...@@ -1507,6 +1507,17 @@ omicron bryanh guest1
</para> </para>
</listitem> </listitem>
</varlistentry> </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> <varlistentry>
<term><literal>ldapurl</literal></term> <term><literal>ldapurl</literal></term>
<listitem> <listitem>
...@@ -1514,13 +1525,16 @@ omicron bryanh guest1 ...@@ -1514,13 +1525,16 @@ omicron bryanh guest1
An RFC 4516 LDAP URL. This is an alternative way to write some of the 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 other LDAP options in a more compact and standard form. The format is
<synopsis> <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> </synopsis>
<replaceable>scope</replaceable> must be one <replaceable>scope</replaceable> must be one
of <literal>base</literal>, <literal>one</literal>, <literal>sub</literal>, of <literal>base</literal>, <literal>one</literal>, <literal>sub</literal>,
typically the latter. Only one attribute is used, and some other typically the last. <replaceable>attribute</replaceable> can
components of standard LDAP URLs such as filters and extensions are nominate a single attribute, in which case it is used as a value for
not supported. <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>
<para> <para>
...@@ -1549,6 +1563,17 @@ ldap://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replac ...@@ -1549,6 +1563,17 @@ ldap://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replac
for search+bind. for search+bind.
</para> </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> <para>
Here is an example for a simple-bind LDAP configuration: Here is an example for a simple-bind LDAP configuration:
<programlisting> <programlisting>
...@@ -1584,6 +1609,16 @@ host ... ldap ldapurl="ldap://ldap.example.net/dc=example,dc=net?uid?sub" ...@@ -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. same URL format, so it will be easier to share the configuration.
</para> </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> <tip>
<para> <para>
Since LDAP often uses commas and spaces to separate the different Since LDAP often uses commas and spaces to separate the different
......
...@@ -2394,6 +2394,34 @@ InitializeLDAPConnection(Port *port, LDAP **ldap) ...@@ -2394,6 +2394,34 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
return STATUS_OK; 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;
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 * Perform LDAP authentication
*/ */
...@@ -2437,7 +2465,7 @@ CheckLDAPAuth(Port *port) ...@@ -2437,7 +2465,7 @@ CheckLDAPAuth(Port *port)
char *filter; char *filter;
LDAPMessage *search_message; LDAPMessage *search_message;
LDAPMessage *entry; LDAPMessage *entry;
char *attributes[2]; char *attributes[] = { LDAP_NO_ATTRS, NULL };
char *dn; char *dn;
char *c; char *c;
int count; int count;
...@@ -2479,13 +2507,13 @@ CheckLDAPAuth(Port *port) ...@@ -2479,13 +2507,13 @@ CheckLDAPAuth(Port *port)
return STATUS_ERROR; return STATUS_ERROR;
} }
/* Fetch just one attribute, else *all* attributes are returned */ /* Build a custom filter or a single attribute filter? */
attributes[0] = port->hba->ldapsearchattribute ? port->hba->ldapsearchattribute : "uid"; if (port->hba->ldapsearchfilter)
attributes[1] = NULL; filter = FormatSearchFilter(port->hba->ldapsearchfilter, port->user_name);
else if (port->hba->ldapsearchattribute)
filter = psprintf("(%s=%s)", filter = psprintf("(%s=%s)", port->hba->ldapsearchattribute, port->user_name);
attributes[0], else
port->user_name); filter = psprintf("(uid=%s)", port->user_name);
r = ldap_search_s(ldap, r = ldap_search_s(ldap,
port->hba->ldapbasedn, port->hba->ldapbasedn,
......
...@@ -1505,22 +1505,24 @@ parse_hba_line(TokenizedLine *tok_line, int elevel) ...@@ -1505,22 +1505,24 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
/* /*
* LDAP can operate in two modes: either with a direct bind, using * LDAP can operate in two modes: either with a direct bind, using
* ldapprefix and ldapsuffix, or using a search+bind, using * ldapprefix and ldapsuffix, or using a search+bind, using
* ldapbasedn, ldapbinddn, ldapbindpasswd and ldapsearchattribute. * ldapbasedn, ldapbinddn, ldapbindpasswd and one of
* Disallow mixing these parameters. * ldapsearchattribute or ldapsearchfilter. Disallow mixing these
* parameters.
*/ */
if (parsedline->ldapprefix || parsedline->ldapsuffix) if (parsedline->ldapprefix || parsedline->ldapsuffix)
{ {
if (parsedline->ldapbasedn || if (parsedline->ldapbasedn ||
parsedline->ldapbinddn || parsedline->ldapbinddn ||
parsedline->ldapbindpasswd || parsedline->ldapbindpasswd ||
parsedline->ldapsearchattribute) parsedline->ldapsearchattribute ||
parsedline->ldapsearchfilter)
{ {
ereport(elevel, ereport(elevel,
(errcode(ERRCODE_CONFIG_FILE_ERROR), (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\"", errcontext("line %d of configuration file \"%s\"",
line_num, HbaFileName))); 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; return NULL;
} }
} }
...@@ -1534,6 +1536,22 @@ parse_hba_line(TokenizedLine *tok_line, int elevel) ...@@ -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"; *err_msg = "authentication method \"ldap\" requires argument \"ldapbasedn\", \"ldapprefix\", or \"ldapsuffix\" to be set";
return NULL; 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) if (parsedline->auth_method == uaRADIUS)
...@@ -1729,14 +1747,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline, ...@@ -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->ldapsearchattribute = pstrdup(urldata->lud_attrs[0]); /* only use first one */
hbaline->ldapscope = urldata->lud_scope; hbaline->ldapscope = urldata->lud_scope;
if (urldata->lud_filter) if (urldata->lud_filter)
{ hbaline->ldapsearchfilter = pstrdup(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;
}
ldap_free_urldesc(urldata); ldap_free_urldesc(urldata);
#else /* not OpenLDAP */ #else /* not OpenLDAP */
ereport(elevel, ereport(elevel,
...@@ -1788,6 +1799,11 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline, ...@@ -1788,6 +1799,11 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
REQUIRE_AUTH_OPTION(uaLDAP, "ldapsearchattribute", "ldap"); REQUIRE_AUTH_OPTION(uaLDAP, "ldapsearchattribute", "ldap");
hbaline->ldapsearchattribute = pstrdup(val); 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) else if (strcmp(name, "ldapbasedn") == 0)
{ {
REQUIRE_AUTH_OPTION(uaLDAP, "ldapbasedn", "ldap"); REQUIRE_AUTH_OPTION(uaLDAP, "ldapbasedn", "ldap");
...@@ -2266,6 +2282,11 @@ gethba_options(HbaLine *hba) ...@@ -2266,6 +2282,11 @@ gethba_options(HbaLine *hba)
CStringGetTextDatum(psprintf("ldapsearchattribute=%s", CStringGetTextDatum(psprintf("ldapsearchattribute=%s",
hba->ldapsearchattribute)); hba->ldapsearchattribute));
if (hba->ldapsearchfilter)
options[noptions++] =
CStringGetTextDatum(psprintf("ldapsearchfilter=%s",
hba->ldapsearchfilter));
if (hba->ldapscope) if (hba->ldapscope)
options[noptions++] = options[noptions++] =
CStringGetTextDatum(psprintf("ldapscope=%d", hba->ldapscope)); CStringGetTextDatum(psprintf("ldapscope=%d", hba->ldapscope));
......
...@@ -80,6 +80,7 @@ typedef struct HbaLine ...@@ -80,6 +80,7 @@ typedef struct HbaLine
char *ldapbinddn; char *ldapbinddn;
char *ldapbindpasswd; char *ldapbindpasswd;
char *ldapsearchattribute; char *ldapsearchattribute;
char *ldapsearchfilter;
char *ldapbasedn; char *ldapbasedn;
int ldapscope; int ldapscope;
char *ldapprefix; char *ldapprefix;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment