On Tue, 2021-09-28 at 18:15 +0000, Jacob Champion wrote: > | authn authz > ---------+----------------------------------- > envvar | PGAUTHUSER PGUSER > conninfo | authuser user > frontend | conn->pgauthuser conn->pguser > backend | port->auth_user port->user_name
v3 attached, which uses the above naming scheme and removes the stale TODO. Changes in since-v2. --Jacob
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 838ca83b55..cf74aeac41 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -188,7 +188,7 @@ ALTER USER MAPPING FOR public SERVER testserver1 ALTER USER MAPPING FOR public SERVER testserver1 OPTIONS (ADD sslmode 'require'); ERROR: invalid option "sslmode" -HINT: Valid options in this context are: user, password, sslpassword, ldapuser, password_required, sslcert, sslkey +HINT: Valid options in this context are: user, password, sslpassword, authuser, password_required, sslcert, sslkey -- But we can add valid ones fine ALTER USER MAPPING FOR public SERVER testserver1 OPTIONS (ADD sslpassword 'dummy'); diff --git a/contrib/postgres_fdw/option.c b/contrib/postgres_fdw/option.c index c734467f01..310e30598c 100644 --- a/contrib/postgres_fdw/option.c +++ b/contrib/postgres_fdw/option.c @@ -315,11 +315,11 @@ InitPgFdwOptions(void) popt->keyword = lopt->keyword; /* - * "user", "ldapuser", and any secret options are allowed only on user + * "user", "authuser", and any secret options are allowed only on user * mappings. Everything else is a server option. */ if (strcmp(lopt->keyword, "user") == 0 || - strcmp(lopt->keyword, "ldapuser") == 0 || + strcmp(lopt->keyword, "authuser") == 0 || strchr(lopt->dispchar, '*')) popt->optcontext = UserMappingRelationId; else diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index 8a0ec715e4..9e62d36fb4 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -1455,8 +1455,8 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname </listitem> </varlistentry> - <varlistentry id="libpq-connect-ldapuser" xreflabel="ldapuser"> - <term><literal>ldapuser</literal></term> + <varlistentry id="libpq-connect-authuser" xreflabel="authuser"> + <term><literal>authuser</literal></term> <listitem> <para> When connecting to servers that use LDAP authentication, this option @@ -7943,10 +7943,10 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough) <listitem> <para> <indexterm> - <primary><envar>PGLDAPUSER</envar></primary> + <primary><envar>PGAUTHUSER</envar></primary> </indexterm> - <envar>PGLDAPUSER</envar> behaves the same as the - <xref linkend="libpq-connect-ldapuser"/> connection parameter. + <envar>PGAUTHUSER</envar> behaves the same as the + <xref linkend="libpq-connect-authuser"/> connection parameter. </para> </listitem> diff --git a/doc/src/sgml/postgres-fdw.sgml b/doc/src/sgml/postgres-fdw.sgml index 1165550955..4286dcfc47 100644 --- a/doc/src/sgml/postgres-fdw.sgml +++ b/doc/src/sgml/postgres-fdw.sgml @@ -120,7 +120,7 @@ <itemizedlist spacing="compact"> <listitem> <para> - <literal>user</literal>, <literal>ldapuser</literal>, + <literal>user</literal>, <literal>authuser</literal>, <literal>password</literal> and <literal>sslpassword</literal> (specify these in a user mapping, instead, or use a service file) </para> diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c index e2010fa402..5205bc1a53 100644 --- a/src/backend/libpq/auth.c +++ b/src/backend/libpq/auth.c @@ -2534,8 +2534,8 @@ CheckLDAPAuth(Port *port) return STATUS_ERROR; } - /* If a PGLDAPUSER was not provided, default to PGUSER. */ - ldapuser = port->ldapuser; + /* If a PGAUTHUSER was not provided, default to PGUSER. */ + ldapuser = port->auth_user; if (!ldapuser || !ldapuser[0]) ldapuser = port->user_name; diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c index bc7e727dc5..e34d6f5726 100644 --- a/src/backend/postmaster/postmaster.c +++ b/src/backend/postmaster/postmaster.c @@ -2208,8 +2208,8 @@ retry1: port->database_name = pstrdup(valptr); else if (strcmp(nameptr, "user") == 0) port->user_name = pstrdup(valptr); - else if (strcmp(nameptr, "ldapuser") == 0) - port->ldapuser = pstrdup(valptr); + else if (strcmp(nameptr, "authuser") == 0) + port->auth_user = pstrdup(valptr); else if (strcmp(nameptr, "options") == 0) port->cmdline_options = pstrdup(valptr); else if (strcmp(nameptr, "replication") == 0) diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h index 26f764aae9..ae9332cd45 100644 --- a/src/include/libpq/libpq-be.h +++ b/src/include/libpq/libpq-be.h @@ -147,6 +147,12 @@ typedef struct Port char *cmdline_options; List *guc_options; + /* + * The username to use during authentication, if different from user_name, + * or else NULL. Currently only supported for the LDAP auth method. + */ + const char *auth_user; + /* * The startup packet application name, only used here for the "connection * authorized" log message. We shouldn't use this post-startup, instead @@ -203,11 +209,6 @@ typedef struct Port void *gss; #endif - /* - * LDAP structures. - */ - const char *ldapuser; - /* * SSL structures. */ diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 6c7a3a94cb..75d0d7d4ec 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -344,9 +344,9 @@ static const internalPQconninfoOption PQconninfoOptions[] = { "Target-Session-Attrs", "", 15, /* sizeof("prefer-standby") = 15 */ offsetof(struct pg_conn, target_session_attrs)}, - {"ldapuser", "PGLDAPUSER", NULL, NULL, - "LDAP-User", "", 20, - offsetof(struct pg_conn, pgldapuser)}, + {"authuser", "PGAUTHUSER", NULL, NULL, + "Auth-User", "", 20, + offsetof(struct pg_conn, pgauthuser)}, /* Terminating entry --- MUST BE LAST */ {NULL, NULL, NULL, NULL, @@ -1440,8 +1440,6 @@ connectOptions2(PGconn *conn) goto oom_error; } - /* TODO: unset pgldapuser if it's the same as pguser, for compatibility? */ - /* * Only if we get this far is it appropriate to try to connect. (We need a * state flag, rather than just the boolean result of this function, in diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 42e4e535ea..308fdcdf6f 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -2220,8 +2220,8 @@ build_startup_packet(const PGconn *conn, char *packet, ADD_STARTUP_OPTION("replication", conn->replication); if (conn->pgoptions && conn->pgoptions[0]) ADD_STARTUP_OPTION("options", conn->pgoptions); - if (conn->pgldapuser && conn->pgldapuser[0]) - ADD_STARTUP_OPTION("ldapuser", conn->pgldapuser); + if (conn->pgauthuser && conn->pgauthuser[0]) + ADD_STARTUP_OPTION("authuser", conn->pgauthuser); if (conn->send_appname) { /* Use appname if present, otherwise use fallback */ diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index ea8e23da08..288e93f367 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -390,7 +390,7 @@ struct pg_conn char *krbsrvname; /* Kerberos service name */ char *gsslib; /* What GSS library to use ("gssapi" or * "sspi") */ - char *pgldapuser; /* LDAP username, if not pguser */ + char *pgauthuser; /* LDAP username, if not pguser */ char *ssl_min_protocol_version; /* minimum TLS protocol version */ char *ssl_max_protocol_version; /* maximum TLS protocol version */ char *target_session_attrs; /* desired session properties */ diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl index 4a771e1b79..963d4a97ce 100644 --- a/src/test/ldap/t/001_auth.pl +++ b/src/test/ldap/t/001_auth.pl @@ -246,15 +246,15 @@ test_access( qr/no match in usermap "mymap" for user "test2" authenticated as "test2"/, ]); -$ENV{"PGLDAPUSER"} = 'test2'; +$ENV{"PGAUTHUSER"} = 'test2'; test_access( $node, 'test1', 0, - 'succeeds with different PGLDAPUSER', + 'succeeds with different PGAUTHUSER', log_like => [ qr/connection authenticated: identity="uid=test2,dc=example,dc=net" method=ldap/, qr/connection authorized: user=test1/, ]); -delete $ENV{"PGLDAPUSER"}; +delete $ENV{"PGAUTHUSER"}; unlink($node->data_dir . '/pg_hba.conf'); $node->append_conf('pg_hba.conf', @@ -339,15 +339,15 @@ test_access( qr/no match in usermap "mymap" for user "test2" authenticated as "test2"/, ]); -$ENV{"PGLDAPUSER"} = 'test2'; +$ENV{"PGAUTHUSER"} = 'test2'; test_access( $node, 'test1', 0, - 'succeeds with different PGLDAPUSER', + 'succeeds with different PGAUTHUSER', log_like => [ qr/connection authenticated: identity="uid=test2,dc=example,dc=net" method=ldap/, qr/connection authorized: user=test1/, ]); -delete $ENV{"PGLDAPUSER"}; +delete $ENV{"PGAUTHUSER"}; unlink($node->data_dir . '/pg_hba.conf'); $node->append_conf('pg_hba.conf',
From e2d1c987a363e486e4e4457d928ca393dbbc6eb3 Mon Sep 17 00:00:00 2001 From: Jacob Champion <pchamp...@vmware.com> Date: Mon, 30 Aug 2021 16:29:59 -0700 Subject: [PATCH v3] Allow user name mapping with LDAP Enable the `map` HBA option for the ldap auth method. To make effective use of this from the client side, the authuser connection option (and a corresponding environment variable, PGAUTHUSER) has been added; it defaults to the PGUSER. For more advanced mapping, the ldap_map_dn HBA option can be set to use the full user Distinguished Name during user mapping. (This parallels the include_realm=1 and clientname=DN options.) --- .../postgres_fdw/expected/postgres_fdw.out | 2 +- contrib/postgres_fdw/option.c | 8 +- doc/src/sgml/client-auth.sgml | 45 +++++- doc/src/sgml/libpq.sgml | 32 ++++ doc/src/sgml/postgres-fdw.sgml | 3 +- src/backend/libpq/auth.c | 38 +++-- src/backend/libpq/hba.c | 29 +++- src/backend/postmaster/postmaster.c | 2 + src/include/libpq/hba.h | 1 + src/include/libpq/libpq-be.h | 6 + src/interfaces/libpq/fe-connect.c | 4 + src/interfaces/libpq/fe-protocol3.c | 2 + src/interfaces/libpq/libpq-int.h | 1 + src/test/ldap/t/001_auth.pl | 145 +++++++++++++++++- 14 files changed, 298 insertions(+), 20 deletions(-) diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index fd141a0fa5..cf74aeac41 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -188,7 +188,7 @@ ALTER USER MAPPING FOR public SERVER testserver1 ALTER USER MAPPING FOR public SERVER testserver1 OPTIONS (ADD sslmode 'require'); ERROR: invalid option "sslmode" -HINT: Valid options in this context are: user, password, sslpassword, password_required, sslcert, sslkey +HINT: Valid options in this context are: user, password, sslpassword, authuser, password_required, sslcert, sslkey -- But we can add valid ones fine ALTER USER MAPPING FOR public SERVER testserver1 OPTIONS (ADD sslpassword 'dummy'); diff --git a/contrib/postgres_fdw/option.c b/contrib/postgres_fdw/option.c index 48c7417e6e..310e30598c 100644 --- a/contrib/postgres_fdw/option.c +++ b/contrib/postgres_fdw/option.c @@ -315,10 +315,12 @@ InitPgFdwOptions(void) popt->keyword = lopt->keyword; /* - * "user" and any secret options are allowed only on user mappings. - * Everything else is a server option. + * "user", "authuser", and any secret options are allowed only on user + * mappings. Everything else is a server option. */ - if (strcmp(lopt->keyword, "user") == 0 || strchr(lopt->dispchar, '*')) + if (strcmp(lopt->keyword, "user") == 0 || + strcmp(lopt->keyword, "authuser") == 0 || + strchr(lopt->dispchar, '*')) popt->optcontext = UserMappingRelationId; else popt->optcontext = ForeignServerRelationId; diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml index 02f0489112..d902eb9d01 100644 --- a/doc/src/sgml/client-auth.sgml +++ b/doc/src/sgml/client-auth.sgml @@ -1624,10 +1624,11 @@ omicron bryanh guest1 This authentication method operates similarly to <literal>password</literal> except that it uses LDAP as the password verification method. LDAP is used only to validate - the user name/password pairs. Therefore the user must already - exist in the database before LDAP can be used for - authentication. - </para> + the user name/password pairs. Therefore database users must already + exist in the database before LDAP can be used for authentication. User name + mapping can be used to allow the LDAP user name to be different from the + database user name. +</para> <para> LDAP authentication can operate in two modes. In the first mode, @@ -1703,6 +1704,42 @@ omicron bryanh guest1 </para> </listitem> </varlistentry> + <varlistentry> + <term><literal>map</literal></term> + <listitem> + <para> + Allows for mapping between LDAP and database user names. See + <xref linkend="auth-username-maps"/> for details. + </para> + </listitem> + </varlistentry> + <varlistentry> + <term><literal>ldap_map_dn</literal></term> + <listitem> + <para> + Set to 1 to use the user's full Distinguished Name (e.g. + <literal>uid=someuser,dc=example,dc=com</literal>) during user name + mapping. When set to 0 (the default), only the bare username (in this + example, <literal>someuser</literal>) will be mapped. This option may + only be used in conjunction with the <literal>map</literal> option. + </para> + <note> + <para> + When using regular expression matching in combination with this option, + care should be taken to correctly anchor the regular expression in + order to avoid false positives. For example, the anchored expression + <literal>/,dc=example,dc=com$</literal> will match only DNs underneath + the <literal>example.com</literal> subtree, whereas the unanchored + <literal>/dc=example,dc=com</literal> could match + <literal>uid=mal,dc=example,dc=community,dc=invalid</literal> + or any number of DNs that are not rooted at + <literal>example.com</literal>. (Whether these DNs exist in the LDAP + tree to be abused is dependent on the LDAP deployment and outside the + scope of this documentation.) + </para> + </note> + </listitem> + </varlistentry> </variablelist> </para> diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index b449c834a9..9e62d36fb4 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -1455,6 +1455,28 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname </listitem> </varlistentry> + <varlistentry id="libpq-connect-authuser" xreflabel="authuser"> + <term><literal>authuser</literal></term> + <listitem> + <para> + When connecting to servers that use LDAP authentication, this option + sets the username that should be used to bind to the LDAP server. + By default, the <productname>PostgreSQL</productname> user name + (<xref linkend="libpq-connect-user"/>) is used to bind. + </para> + + <note> + <para> + For this option to be useful, the server must be appropriately + configured with a user name map (<xref linkend="auth-username-maps"/>). + Server versions prior to 15 always use the + <productname>PostgreSQL</productname> user name to bind and cannot make + use of this option. + </para> + </note> + </listitem> + </varlistentry> + <varlistentry id="libpq-connect-gssencmode" xreflabel="gssencmode"> <term><literal>gssencmode</literal></term> <listitem> @@ -7918,6 +7940,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough) </para> </listitem> + <listitem> + <para> + <indexterm> + <primary><envar>PGAUTHUSER</envar></primary> + </indexterm> + <envar>PGAUTHUSER</envar> behaves the same as the + <xref linkend="libpq-connect-authuser"/> connection parameter. + </para> + </listitem> + <listitem> <para> <indexterm> diff --git a/doc/src/sgml/postgres-fdw.sgml b/doc/src/sgml/postgres-fdw.sgml index 41c952fbe3..4286dcfc47 100644 --- a/doc/src/sgml/postgres-fdw.sgml +++ b/doc/src/sgml/postgres-fdw.sgml @@ -120,7 +120,8 @@ <itemizedlist spacing="compact"> <listitem> <para> - <literal>user</literal>, <literal>password</literal> and <literal>sslpassword</literal> (specify these + <literal>user</literal>, <literal>authuser</literal>, + <literal>password</literal> and <literal>sslpassword</literal> (specify these in a user mapping, instead, or use a service file) </para> </listitem> diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c index a317aef1c9..5205bc1a53 100644 --- a/src/backend/libpq/auth.c +++ b/src/backend/libpq/auth.c @@ -2479,8 +2479,10 @@ CheckLDAPAuth(Port *port) char *passwd; LDAP *ldap; int r; + const char *ldapuser; char *fulluser; const char *server_name; + const char *map_name; #ifdef HAVE_LDAP_INITIALIZE @@ -2532,6 +2534,11 @@ CheckLDAPAuth(Port *port) return STATUS_ERROR; } + /* If a PGAUTHUSER was not provided, default to PGUSER. */ + ldapuser = port->auth_user; + if (!ldapuser || !ldapuser[0]) + ldapuser = port->user_name; + if (port->hba->ldapbasedn) { /* @@ -2543,7 +2550,7 @@ CheckLDAPAuth(Port *port) LDAPMessage *entry; char *attributes[] = {LDAP_NO_ATTRS, NULL}; char *dn; - char *c; + const char *c; int count; /* @@ -2552,7 +2559,7 @@ CheckLDAPAuth(Port *port) * them would make it possible to inject any kind of custom filters in * the LDAP filter. */ - for (c = port->user_name; *c; c++) + for (c = ldapuser; *c; c++) { if (*c == '*' || *c == '(' || @@ -2590,11 +2597,11 @@ CheckLDAPAuth(Port *port) /* Build a custom filter or a single attribute filter? */ if (port->hba->ldapsearchfilter) - filter = FormatSearchFilter(port->hba->ldapsearchfilter, port->user_name); + filter = FormatSearchFilter(port->hba->ldapsearchfilter, ldapuser); else if (port->hba->ldapsearchattribute) - filter = psprintf("(%s=%s)", port->hba->ldapsearchattribute, port->user_name); + filter = psprintf("(%s=%s)", port->hba->ldapsearchattribute, ldapuser); else - filter = psprintf("(uid=%s)", port->user_name); + filter = psprintf("(uid=%s)", ldapuser); r = ldap_search_s(ldap, port->hba->ldapbasedn, @@ -2621,12 +2628,12 @@ CheckLDAPAuth(Port *port) { if (count == 0) ereport(LOG, - (errmsg("LDAP user \"%s\" does not exist", port->user_name), + (errmsg("LDAP user \"%s\" does not exist", ldapuser), errdetail("LDAP search for filter \"%s\" on server \"%s\" returned no entries.", filter, server_name))); else ereport(LOG, - (errmsg("LDAP user \"%s\" is not unique", port->user_name), + (errmsg("LDAP user \"%s\" is not unique", ldapuser), errdetail_plural("LDAP search for filter \"%s\" on server \"%s\" returned %d entry.", "LDAP search for filter \"%s\" on server \"%s\" returned %d entries.", count, @@ -2691,7 +2698,7 @@ CheckLDAPAuth(Port *port) else fulluser = psprintf("%s%s%s", port->hba->ldapprefix ? port->hba->ldapprefix : "", - port->user_name, + ldapuser, port->hba->ldapsuffix ? port->hba->ldapsuffix : ""); r = ldap_simple_bind_s(ldap, fulluser, passwd); @@ -2713,9 +2720,22 @@ CheckLDAPAuth(Port *port) ldap_unbind(ldap); pfree(passwd); + + /* + * Check the usermap for authorization. If ldap_map_dn is set and the admin + * has set up an explicit map, we use the full distinguised name during the + * mapping. Otherwise we just check the bare LDAP username. + */ + map_name = ldapuser; + if (port->hba->usermap && port->hba->usermap[0] && port->hba->ldap_map_dn) + map_name = fulluser; + + r = check_usermap(port->hba->usermap, port->user_name, map_name, + pg_krb_caseins_users); + pfree(fulluser); - return STATUS_OK; + return r; } /* diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c index 3be8778d21..da9e1ae509 100644 --- a/src/backend/libpq/hba.c +++ b/src/backend/libpq/hba.c @@ -1596,6 +1596,24 @@ parse_hba_line(TokenizedLine *tok_line, int elevel) *err_msg = "cannot use ldapsearchattribute together with ldapsearchfilter"; return NULL; } + + /* + * Setting ldap_map_dn=1 tells the server to use the full DN when + * mapping users, but it doesn't do anything if there's no usermap + * defined. To prevent confusion, don't allow this to be set without a + * map. + */ + if (parsedline->ldap_map_dn + && !(parsedline->usermap && parsedline->usermap[0])) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("authentication option \"ldap_map_dn\" requires argument \"map\" to be set"), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = "authentication option \"ldap_map_dn\" requires argument \"map\" to be set"; + return NULL; + } } if (parsedline->auth_method == uaRADIUS) @@ -1713,8 +1731,9 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline, hbaline->auth_method != uaPeer && hbaline->auth_method != uaGSS && hbaline->auth_method != uaSSPI && + hbaline->auth_method != uaLDAP && hbaline->auth_method != uaCert) - INVALID_AUTH_OPTION("map", gettext_noop("ident, peer, gssapi, sspi, and cert")); + INVALID_AUTH_OPTION("map", gettext_noop("ident, peer, gssapi, sspi, ldap, and cert")); hbaline->usermap = pstrdup(val); } else if (strcmp(name, "clientcert") == 0) @@ -1931,6 +1950,14 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline, REQUIRE_AUTH_OPTION(uaLDAP, "ldapsuffix", "ldap"); hbaline->ldapsuffix = pstrdup(val); } + else if (strcmp(name, "ldap_map_dn") == 0) + { + REQUIRE_AUTH_OPTION(uaLDAP, "ldap_map_dn", "ldap"); + if (strcmp(val, "1") == 0) + hbaline->ldap_map_dn = true; + else + hbaline->ldap_map_dn = false; + } else if (strcmp(name, "krb_realm") == 0) { if (hbaline->auth_method != uaGSS && diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c index e2a76ba055..e34d6f5726 100644 --- a/src/backend/postmaster/postmaster.c +++ b/src/backend/postmaster/postmaster.c @@ -2208,6 +2208,8 @@ retry1: port->database_name = pstrdup(valptr); else if (strcmp(nameptr, "user") == 0) port->user_name = pstrdup(valptr); + else if (strcmp(nameptr, "authuser") == 0) + port->auth_user = pstrdup(valptr); else if (strcmp(nameptr, "options") == 0) port->cmdline_options = pstrdup(valptr); else if (strcmp(nameptr, "replication") == 0) diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h index 8d9f3821b1..8729056f09 100644 --- a/src/include/libpq/hba.h +++ b/src/include/libpq/hba.h @@ -106,6 +106,7 @@ typedef struct HbaLine int ldapscope; char *ldapprefix; char *ldapsuffix; + bool ldap_map_dn; ClientCertMode clientcert; ClientCertName clientcertname; char *krb_realm; diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h index 02015efe13..ae9332cd45 100644 --- a/src/include/libpq/libpq-be.h +++ b/src/include/libpq/libpq-be.h @@ -147,6 +147,12 @@ typedef struct Port char *cmdline_options; List *guc_options; + /* + * The username to use during authentication, if different from user_name, + * or else NULL. Currently only supported for the LDAP auth method. + */ + const char *auth_user; + /* * The startup packet application name, only used here for the "connection * authorized" log message. We shouldn't use this post-startup, instead diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index b288d346f9..75d0d7d4ec 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -344,6 +344,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = { "Target-Session-Attrs", "", 15, /* sizeof("prefer-standby") = 15 */ offsetof(struct pg_conn, target_session_attrs)}, + {"authuser", "PGAUTHUSER", NULL, NULL, + "Auth-User", "", 20, + offsetof(struct pg_conn, pgauthuser)}, + /* Terminating entry --- MUST BE LAST */ {NULL, NULL, NULL, NULL, NULL, NULL, 0} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 9ab3bf1fcb..308fdcdf6f 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -2220,6 +2220,8 @@ build_startup_packet(const PGconn *conn, char *packet, ADD_STARTUP_OPTION("replication", conn->replication); if (conn->pgoptions && conn->pgoptions[0]) ADD_STARTUP_OPTION("options", conn->pgoptions); + if (conn->pgauthuser && conn->pgauthuser[0]) + ADD_STARTUP_OPTION("authuser", conn->pgauthuser); if (conn->send_appname) { /* Use appname if present, otherwise use fallback */ diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 490458adef..288e93f367 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -390,6 +390,7 @@ struct pg_conn char *krbsrvname; /* Kerberos service name */ char *gsslib; /* What GSS library to use ("gssapi" or * "sspi") */ + char *pgauthuser; /* LDAP username, if not pguser */ char *ssl_min_protocol_version; /* minimum TLS protocol version */ char *ssl_max_protocol_version; /* maximum TLS protocol version */ char *target_session_attrs; /* desired session properties */ diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl index 5a9a009832..963d4a97ce 100644 --- a/src/test/ldap/t/001_auth.pl +++ b/src/test/ldap/t/001_auth.pl @@ -9,7 +9,7 @@ use Test::More; if ($ENV{with_ldap} eq 'yes') { - plan tests => 28; + plan tests => 60; } else { @@ -210,6 +210,77 @@ test_access( qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/ ],); +note "ident mapping"; + +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" map=mymap} +); +$node->restart; + +$ENV{"PGPASSWORD"} = 'secret1'; +test_access( + $node, 'test1', 2, + 'fails without mapping', + log_like => [ + qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/, + qr/no match in usermap "mymap" for user "test1"/, + ]); + +$node->append_conf('pg_ident.conf', qq{mymap /^ test1}); +$node->restart; + +test_access( + $node, 'test1', 0, + 'succeeds with mapping', + log_like => [ + qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/, + ]); + +$ENV{"PGPASSWORD"} = 'secret2'; +test_access( + $node, 'test2', 2, + 'fails with unmapped role', + log_like => [ + qr/connection authenticated: identity="uid=test2,dc=example,dc=net" method=ldap/, + qr/no match in usermap "mymap" for user "test2" authenticated as "test2"/, + ]); + +$ENV{"PGAUTHUSER"} = 'test2'; +test_access( + $node, 'test1', 0, + 'succeeds with different PGAUTHUSER', + log_like => [ + qr/connection authenticated: identity="uid=test2,dc=example,dc=net" method=ldap/, + qr/connection authorized: user=test1/, + ]); +delete $ENV{"PGAUTHUSER"}; + +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" map=mymap ldap_map_dn=1} +); +unlink($node->data_dir . '/pg_ident.conf'); +$node->append_conf('pg_ident.conf', qq{mymap "/,dc=example,dc=net\$" test1}); +$node->restart; + +$ENV{"PGPASSWORD"} = 'secret1'; +test_access( + $node, 'test1', 0, + 'succeeds with full DN mapping', + log_like => [ + qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/, + ]); + +$ENV{"PGPASSWORD"} = 'secret2'; +test_access( + $node, 'test2', 2, + 'fails with unmapped role for full DN mapping', + log_like => [ + qr/connection authenticated: identity="uid=test2,dc=example,dc=net" method=ldap/, + qr/no match in usermap "mymap" for user "test2" authenticated as "uid=test2,dc=example,dc=net"/, + ]); + note "search+bind"; unlink($node->data_dir . '/pg_hba.conf'); @@ -231,6 +302,78 @@ test_access( qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/ ],); +note "search+bind ident mapping"; + +unlink($node->data_dir . '/pg_ident.conf'); +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" map=mymap} +); +$node->restart; + +$ENV{"PGPASSWORD"} = 'secret1'; +test_access( + $node, 'test1', 2, + 'fails without mapping', + log_like => [ + qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/, + qr/no match in usermap "mymap" for user "test1" authenticated as "test1"/, + ]); + +$node->append_conf('pg_ident.conf', qq{mymap /^ test1}); +$node->restart; + +test_access( + $node, 'test1', 0, + 'succeeds with mapping', + log_like => [ + qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/, + ]); + +$ENV{"PGPASSWORD"} = 'secret2'; +test_access( + $node, 'test2', 2, + 'fails with unmapped role', + log_like => [ + qr/connection authenticated: identity="uid=test2,dc=example,dc=net" method=ldap/, + qr/no match in usermap "mymap" for user "test2" authenticated as "test2"/, + ]); + +$ENV{"PGAUTHUSER"} = 'test2'; +test_access( + $node, 'test1', 0, + 'succeeds with different PGAUTHUSER', + log_like => [ + qr/connection authenticated: identity="uid=test2,dc=example,dc=net" method=ldap/, + qr/connection authorized: user=test1/, + ]); +delete $ENV{"PGAUTHUSER"}; + +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" map=mymap ldap_map_dn=1} +); +unlink($node->data_dir . '/pg_ident.conf'); +$node->append_conf('pg_ident.conf', qq{mymap "/,dc=example,dc=net\$" test1}); +$node->restart; + +$ENV{"PGPASSWORD"} = 'secret1'; +test_access( + $node, 'test1', 0, + 'succeeds with full DN mapping', + log_like => [ + qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/, + ]); + +$ENV{"PGPASSWORD"} = 'secret2'; +test_access( + $node, 'test2', 2, + 'fails with unmapped role for full DN mapping', + log_like => [ + qr/connection authenticated: identity="uid=test2,dc=example,dc=net" method=ldap/, + qr/no match in usermap "mymap" for user "test2" authenticated as "uid=test2,dc=example,dc=net"/, + ]); + note "multiple servers"; unlink($node->data_dir . '/pg_hba.conf'); -- 2.25.1