Greetings,

* Stephen Frost (sfr...@snowman.net) wrote:
> * Jacob Champion (pchamp...@vmware.com) wrote:
> > On Fri, 2022-03-11 at 19:39 -0500, Stephen Frost wrote:
> > > Even so, I’m not against adding an option… but exactly how would that
> > > option be configured?  Server level?  On the HBA line?  role level..?
> > 
> > In the OPTIONS for CREATE SERVER, maybe? At least for the FDW case.
> 
> I'm a bit confused on this.  The option to allow or not allow delegated
> credentials couldn't be something that's in the CREATE SERVER for FDWs
> as it applies to more than just FDWs but also dblink and anything else
> where we reach out from PG to contact some other system.

Thinking it through further, it seems like the right place to allow an
administrator to control if credentials are allowed to be delegated is
through a pg_hba option.  Attached patch adds such an option.

> > > > Also, we're
> > > > globally ignoring whatever ccache was set by an administrator. Can't
> > > > two postgres_fdw connections from the same backend process require
> > > > different settings?
> > > 
> > > Settings..? Perhaps, but delegated credentials aren’t really 
> > > settings, so not really sure what you’re suggesting here.
> > 
> > I mean that one backend server might require delegated credentials, and
> > another might require whatever the admin has already set up in the
> > ccache, and the user might want to use tables from both servers in the
> > same session.
> 
> That an admin might have a credential cache that's picked up and used
> for connections from a regular user backend to another system strikes me
> as an altogether concerning idea.  Even so, in such a case, the admin
> would have had to set up the user mapping with 'password required =
> false' or it wouldn't have worked for a non-superuser anyway, so I'm not
> sure that I'm too worried about this case.

To address this, I also added a new GUC which allows an administrator to
control what the credential cache is set to for user-authenticated
backends, with a default of MEMORY:, which should generally be safe and
won't cause a user backend to pick up on a file-based credential cache
which might exist on the server somewhere.  This gives the administrator
the option to set it to more-or-less whatever they'd like though, so if
they want to set it to a file-based credential cache, then they can do
so (I did put some caveats about doing that into the documentation as I
don't think it's generally a good idea to do...).

> > > > I notice that gss_store_cred_into() has a companion,
> > > > gss_acquire_cred_from(). Is it possible to use that to pull out our
> > > > delegated credential explicitly by name, instead of stomping on the
> > > > global setup?
> > > 
> > > Not really sure what is meant here by global setup..?  Feeling like
> > > this is a follow on confusion from maybe mixing server vs client
> > > libpq?
> > 
> > By my reading, the gss_store_cred_into() call followed by
> > the setenv("KRB5CCNAME", ...) is effectively performing global
> > configuration for the process. Any KRB5CCNAME already set up by the
> > server admin is going to be ignored from that point onward. Is that
> > accurate?
> 
> The process, yes, but I guess I disagree on that being 'global'- it's
> just for that PG backend process.

The new krb_user_ccache is a lot closer to 'global', though it's
specifically for user-authenticated backends (allowing the postmaster
and other things like replication connections to use whatever the
credential cache is set to by the administrator on startup), but that
seems like it makes sense to me- generally you're not going to want
regular user backends to be accessing the credential cache of the
'postgres' unix account on the server.

> Attached is an updated patch which adds the gss_release_creds call, a
> function in libpq to allow checking if the libpq connection was made
> using GSS, changes to dblink to have it check for password-or-gss when
> connecting to a remote system, and tests for dblink and postgres_fdw to
> make sure that this all works correctly.

I've added a couple more tests to address the new options too, along
with documentation for them.  This is starting to feel reasonably decent
to me, at least as a first pass at supporting kerberos credential
delegation, which is definitely a feature I've been hoping we would get
into PG for quite a while.  Would certainly appreciate some feedback on
this (from anyone who'd like to comment), though I know we're getting
into the last few hours before feature freeze ends.

Updated patch attached.

Thanks!

Stephen
From 83b2979e252866ce86a9bfb3d0b631db2f5b8404 Mon Sep 17 00:00:00 2001
From: Stephen Frost <sfr...@snowman.net>
Date: Thu, 7 Apr 2022 15:34:39 -0400
Subject: [PATCH] Add support for Kerberos credential delegation

Accept GSSAPI/Kerberos delegated credentials.  With this, a user could
authenticate to PostgreSQL using Kerberos credentials, delegate
credentials to the PostgreSQL server, and then the PostgreSQL server
could use those credentials to connect to another service, such as with
postgres_fdw or dblink or theoretically any other authenticated
connection which is able to use delegated credentials.

If an administrator prefers to not allow credentials to be delegated to
the server, they can be disallowed using a new pg_hba option for gss
called 'allow_cred_delegation'.

A new server GUC has also been introduced to allow an administrator to
control what the kerberos credential cache is configured to for user
authenticated backends, krb_user_ccache.  This defaults to MEMORY:,
which is where delegated credentials are stored (and is otherwise empty,
avoiding the risk of an administrator's credentials on the server being
mistakenly picked up and used).

Original patch by: Peifeng Qiu, whacked around some by me.
Reviewed-by: Jacob Champion
Discussion: https://postgr.es/m/co1pr05mb8023cc2cb575e0faad7df4f8a8...@co1pr05mb8023.namprd05.prod.outlook.com
---
 contrib/dblink/dblink.c                       |  6 +-
 .../postgres_fdw/expected/postgres_fdw.out    |  2 +-
 contrib/postgres_fdw/option.c                 |  3 +
 doc/src/sgml/client-auth.sgml                 | 13 +++
 doc/src/sgml/config.sgml                      | 25 ++++++
 doc/src/sgml/libpq.sgml                       | 19 ++++
 src/backend/libpq/auth.c                      | 24 +++++-
 src/backend/libpq/be-gssapi-common.c          | 51 +++++++++++
 src/backend/libpq/be-secure-gssapi.c          | 19 +++-
 src/backend/libpq/hba.c                       | 19 ++++
 src/backend/utils/adt/hbafuncs.c              |  4 +
 src/backend/utils/init/postinit.c             |  8 +-
 src/backend/utils/misc/guc.c                  | 15 ++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/include/libpq/auth.h                      |  1 +
 src/include/libpq/be-gssapi-common.h          |  3 +
 src/include/libpq/hba.h                       |  1 +
 src/include/libpq/libpq-be.h                  |  3 +
 src/interfaces/libpq/exports.txt              |  1 +
 src/interfaces/libpq/fe-auth.c                | 12 ++-
 src/interfaces/libpq/fe-connect.c             | 12 +++
 src/interfaces/libpq/fe-secure-gssapi.c       |  3 +-
 src/interfaces/libpq/libpq-fe.h               |  1 +
 src/interfaces/libpq/libpq-int.h              |  1 +
 src/test/kerberos/Makefile                    |  3 +
 src/test/kerberos/t/001_auth.pl               | 86 +++++++++++++++++--
 src/test/perl/PostgreSQL/Test/Utils.pm        | 27 ++++++
 27 files changed, 343 insertions(+), 20 deletions(-)

diff --git a/contrib/dblink/dblink.c b/contrib/dblink/dblink.c
index a06d4bd12d..e5b70e084e 100644
--- a/contrib/dblink/dblink.c
+++ b/contrib/dblink/dblink.c
@@ -2643,7 +2643,7 @@ dblink_security_check(PGconn *conn, remoteConn *rconn)
 {
 	if (!superuser())
 	{
-		if (!PQconnectionUsedPassword(conn))
+		if (!(PQconnectionUsedPassword(conn) || PQconnectionUsedGSSAPI(conn)))
 		{
 			PQfinish(conn);
 			ReleaseExternalFD();
@@ -2652,8 +2652,8 @@ dblink_security_check(PGconn *conn, remoteConn *rconn)
 
 			ereport(ERROR,
 					(errcode(ERRCODE_S_R_E_PROHIBITED_SQL_STATEMENT_ATTEMPTED),
-					 errmsg("password is required"),
-					 errdetail("Non-superuser cannot connect if the server does not request a password."),
+					 errmsg("password or GSSAPI is required"),
+					 errdetail("Non-superuser cannot connect if the server does not request a password or use GSSAPI."),
 					 errhint("Target server's authentication method must be changed.")));
 		}
 	}
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 11e9b4e8cc..c3bd8fe6b4 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, password_required, sslcert, sslkey, gssencmode
 -- 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 572591a558..05922cfe6d 100644
--- a/contrib/postgres_fdw/option.c
+++ b/contrib/postgres_fdw/option.c
@@ -262,6 +262,9 @@ InitPgFdwOptions(void)
 		{"sslcert", UserMappingRelationId, true},
 		{"sslkey", UserMappingRelationId, true},
 
+		/* gssencmode is also libpq option, same to above. */
+		{"gssencmode", UserMappingRelationId, true},
+
 		{NULL, InvalidOid, false}
 	};
 
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 142b0affcb..9d35aa3c67 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -1369,6 +1369,19 @@ omicron         bryanh                  guest1
        </para>
       </listitem>
      </varlistentry>
+
+     <varlistentry>
+      <term><literal>allow_cred_delegation</literal></term>
+      <listitem>
+       <para>
+        If set to 0, credential delegation will not be allowed.  The default
+        setting (1) allows an authenticated client to delegate credentials to the 
+        server which will allow the server to then use those credentials to
+        authenticate when connecting to other systems such as with dblink or when
+        using a foreign data wrapper (FDW).
+       </para>
+      </listitem>
+     </varlistentry>
     </variablelist>
    </para>
 
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 43e4ade83e..c1d9a0502d 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1126,6 +1126,31 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-krb-user-ccache" xreflabel="krb_user_ccache">
+      <term><varname>krb_user_ccache</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>krb_user_ccache</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Sets the location of the Kerberos credential cache to be used for
+        regular user backends which go through authentication.  The default is
+        <filename>MEMORY:</filename>, which is where delegated credentials
+        are stored (and is otherwise empty).  Care should be used when changing
+        this value- setting it to a file-based credential cache will mean that
+        user backends could potentially use any credentials stored to access
+        other systems.
+        If this parameter is set to an empty string, then the system-dependent
+        default is used, which may be a file-based credential cache with the same
+        caveats as previously mentioned.
+        This parameter can only be set in the
+        <filename>postgresql.conf</filename> file or on the server command line.
+        See <xref linkend="gssapi-auth"/> for more information.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-db-user-namespace" xreflabel="db_user_namespace">
       <term><varname>db_user_namespace</varname> (<type>boolean</type>)
       <indexterm>
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 1c20901c3c..22ba41ea10 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2489,6 +2489,25 @@ int PQconnectionUsedPassword(const PGconn *conn);
       </para>
      </listitem>
     </varlistentry>
+
+    <varlistentry id="libpq-PQconnectionUsedGSSAPI">
+     <term><function>PQconnectionUsedGSSAPI</function><indexterm><primary>PQconnectionUsedGSSAPI</primary></indexterm></term>
+     <listitem>
+      <para>
+       Returns true (1) if the connection authentication method
+       used GSSAPI. Returns false (0) if not.
+
+<synopsis>
+int PQconnectionUsedGSSAPI(const PGconn *conn);
+</synopsis>
+      </para>
+
+      <para>
+       This function can be applied to detect whether the connection was
+       authenticated with GSSAPI.
+      </para>
+     </listitem>
+    </varlistentry>
    </variablelist>
   </para>
 
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index efc53f3135..08544e370a 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -170,6 +170,7 @@ static int	CheckCertAuth(Port *port);
  */
 char	   *pg_krb_server_keyfile;
 bool		pg_krb_caseins_users;
+char	   *pg_krb_user_ccache;
 
 
 /*----------------------------------------------------------------
@@ -422,6 +423,14 @@ ClientAuthentication(Port *port)
 					 errmsg("connection requires a valid client certificate")));
 	}
 
+#ifdef ENABLE_GSS
+	/*
+	 * Set the credential cache to use for user backends which go through
+	 * regular authentication.
+	 */
+	setenv("KRB5CCNAME", pg_krb_user_ccache, 1);
+#endif
+
 	/*
 	 * Now proceed to do the actual authentication check
 	 */
@@ -564,6 +573,16 @@ ClientAuthentication(Port *port)
 				sendAuthRequest(port, AUTH_REQ_GSS, NULL, 0);
 				status = pg_GSS_recvauth(port);
 			}
+
+			/*
+			 * If the HBA line allows and we were delegated credentials then
+			 * store them.
+			 */
+			if (port->hba->allow_cred_delegation && port->gss->proxy)
+			{
+				pg_store_proxy_credential(port->gss->proxy);
+				port->gss->proxy_creds = true;
+			}
 #else
 			Assert(false);
 #endif
@@ -949,6 +968,9 @@ pg_GSS_recvauth(Port *port)
 	 */
 	port->gss->ctx = GSS_C_NO_CONTEXT;
 
+	port->gss->proxy = NULL;
+	port->gss->proxy_creds = false;
+
 	/*
 	 * Loop through GSSAPI message exchange. This exchange can consist of
 	 * multiple messages sent in both directions. First message is always from
@@ -999,7 +1021,7 @@ pg_GSS_recvauth(Port *port)
 										  &port->gss->outbuf,
 										  &gflags,
 										  NULL,
-										  NULL);
+										  &port->gss->proxy);
 
 		/* gbuf no longer used */
 		pfree(buf.data);
diff --git a/src/backend/libpq/be-gssapi-common.c b/src/backend/libpq/be-gssapi-common.c
index 71b796d5a2..f439cac4b6 100644
--- a/src/backend/libpq/be-gssapi-common.c
+++ b/src/backend/libpq/be-gssapi-common.c
@@ -92,3 +92,54 @@ pg_GSS_error(const char *errmsg,
 			(errmsg_internal("%s", errmsg),
 			 errdetail_internal("%s: %s", msg_major, msg_minor)));
 }
+
+/*
+ * Store the credentials passed in into the memory cache for later usage.
+ *
+ * This allows credentials to be delegated to us for us to use to connect
+ * to other systems with, using, e.g. postgres_fdw or dblink.
+ */
+#define GSS_MEMORY_CACHE "MEMORY:"
+void
+pg_store_proxy_credential(gss_cred_id_t cred)
+{
+	OM_uint32 major, minor;
+	gss_OID_set mech;
+	gss_cred_usage_t usage;
+	gss_key_value_element_desc cc;
+	gss_key_value_set_desc ccset;
+
+	cc.key = "ccache";
+	cc.value = GSS_MEMORY_CACHE;
+	ccset.count = 1;
+	ccset.elements = &cc;
+
+	/* Make the proxy credential only available to current process */
+	major = gss_store_cred_into(&minor,
+		cred,
+		GSS_C_INITIATE, /* credential only used for starting libpq connection */
+		GSS_C_NULL_OID, /* store all */
+		true, /* overwrite */
+		true, /* make default */
+		&ccset,
+		&mech,
+		&usage);
+
+	if (major != GSS_S_COMPLETE)
+	{
+		pg_GSS_error("gss_store_cred", major, minor);
+	}
+
+	/* Credential stored, so we can release our credential handle. */
+	major = gss_release_cred(&minor, &cred);
+	if (major != GSS_S_COMPLETE)
+	{
+		pg_GSS_error("gss_release_cred", major, minor);
+	}
+
+	/*
+	 * Be sure that KRB5CCNAME is set to MEMORY: for this backend, so that later
+	 * calls to gss_acquire_cred will find the proxied credentials we stored.
+	 */
+	setenv("KRB5CCNAME", GSS_MEMORY_CACHE, 1);
+}
diff --git a/src/backend/libpq/be-secure-gssapi.c b/src/backend/libpq/be-secure-gssapi.c
index 2844c5aa4b..8e7abff849 100644
--- a/src/backend/libpq/be-secure-gssapi.c
+++ b/src/backend/libpq/be-secure-gssapi.c
@@ -504,6 +504,9 @@ secure_open_gssapi(Port *port)
 	port->gss = (pg_gssinfo *)
 		MemoryContextAllocZero(TopMemoryContext, sizeof(pg_gssinfo));
 
+	port->gss->proxy = NULL;
+	port->gss->proxy_creds = false;
+
 	/*
 	 * Allocate buffers and initialize state variables.  By malloc'ing the
 	 * buffers at this point, we avoid wasting static data space in processes
@@ -588,7 +591,8 @@ secure_open_gssapi(Port *port)
 									   GSS_C_NO_CREDENTIAL, &input,
 									   GSS_C_NO_CHANNEL_BINDINGS,
 									   &port->gss->name, NULL, &output, NULL,
-									   NULL, NULL);
+									   NULL, &port->gss->proxy);
+
 		if (GSS_ERROR(major))
 		{
 			pg_GSS_error(_("could not accept GSSAPI security context"),
@@ -731,3 +735,16 @@ be_gssapi_get_princ(Port *port)
 
 	return port->gss->princ;
 }
+
+/*
+ * Return if GSSAPI delegated/proxy credentials were included on this
+ * connection.
+ */
+bool
+be_gssapi_get_proxy(Port *port)
+{
+	if (!port || !port->gss)
+		return NULL;
+
+	return port->gss->proxy_creds;
+}
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index f8393ca8ed..d286ab59b9 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -1471,6 +1471,16 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 		parsedline->upn_username = false;
 	}
 
+	/*
+	 * For GSS, set the default value of allow_cred_delegation to true.
+	 * This should generally be safe as the delegated credentials are those of
+	 * the user who has been authorized, and will only happen if the user chooses
+	 * to authenticate with a credential that can be delegated, but admins are
+	 * able to disable it if they wish to.
+	 */
+	if (parsedline->auth_method == uaGSS)
+		parsedline->allow_cred_delegation = true;
+
 	/* Parse remaining arguments */
 	while ((field = lnext(tok_line->fields, field)) != NULL)
 	{
@@ -1941,6 +1951,15 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 		else
 			hbaline->upn_username = false;
 	}
+	else if (strcmp(name, "allow_cred_delegation") == 0)
+	{
+		if (hbaline->auth_method != uaGSS)
+			INVALID_AUTH_OPTION("allow_cred_delegation", gettext_noop("gssapi"));
+		if (strcmp(val, "1") == 0)
+			hbaline->allow_cred_delegation = true;
+		else
+			hbaline->allow_cred_delegation = false;
+	}
 	else if (strcmp(name, "radiusservers") == 0)
 	{
 		struct addrinfo *gai_result;
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index 9fe7b62c9a..2167c7ba9e 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -64,6 +64,10 @@ get_hba_options(HbaLine *hba)
 		if (hba->krb_realm)
 			options[noptions++] =
 				CStringGetTextDatum(psprintf("krb_realm=%s", hba->krb_realm));
+
+		if (hba->allow_cred_delegation)
+			options[noptions++] =
+				CStringGetTextDatum("allow_cred_delegation=true");
 	}
 
 	if (hba->usermap)
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 6452b42dbf..e9b7cdc216 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -284,15 +284,17 @@ PerformAuthentication(Port *port)
 
 			if (princ)
 				appendStringInfo(&logmsg,
-								 _(" GSS (authenticated=%s, encrypted=%s, principal=%s)"),
+								 _(" GSS (authenticated=%s, encrypted=%s, proxy_credentials=%s, principal=%s)"),
 								 be_gssapi_get_auth(port) ? _("yes") : _("no"),
 								 be_gssapi_get_enc(port) ? _("yes") : _("no"),
+								 be_gssapi_get_proxy(port) ? _("yes") : _("no"),
 								 princ);
 			else
 				appendStringInfo(&logmsg,
-								 _(" GSS (authenticated=%s, encrypted=%s)"),
+								 _(" GSS (authenticated=%s, encrypted=%s, proxy_credentials=%s)"),
 								 be_gssapi_get_auth(port) ? _("yes") : _("no"),
-								 be_gssapi_get_enc(port) ? _("yes") : _("no"));
+								 be_gssapi_get_enc(port) ? _("yes") : _("no"),
+								 be_gssapi_get_proxy(port) ? _("yes") : _("no"));
 		}
 #endif
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 9e8ab1420d..7fdb9ed58a 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -117,6 +117,10 @@
 #define PG_KRB_SRVTAB ""
 #endif
 
+#ifndef PG_KRB_USER_CCACHE
+#define PG_KRB_USER_CCACHE "MEMORY:"
+#endif
+
 #define CONFIG_FILENAME "postgresql.conf"
 #define HBA_FILENAME	"pg_hba.conf"
 #define IDENT_FILENAME	"pg_ident.conf"
@@ -4146,6 +4150,17 @@ static struct config_string ConfigureNamesString[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"krb_user_ccache", PGC_SIGHUP, CONN_AUTH_AUTH,
+			gettext_noop("Sets the Kerberos credential cache location for user authenticated backends."),
+			NULL,
+			GUC_SUPERUSER_ONLY
+		},
+		&pg_krb_user_ccache,
+		PG_KRB_USER_CCACHE,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"bonjour_name", PGC_POSTMASTER, CONN_AUTH_SETTINGS,
 			gettext_noop("Sets the Bonjour service name."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 93d221a37b..fa668b6ea1 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -99,6 +99,7 @@
 # GSSAPI using Kerberos
 #krb_server_keyfile = 'FILE:${sysconfdir}/krb5.keytab'
 #krb_caseins_users = off
+#krb_user_ccache = MEMORY:
 
 # - SSL -
 
diff --git a/src/include/libpq/auth.h b/src/include/libpq/auth.h
index 6d7ee1acb9..c032e4224c 100644
--- a/src/include/libpq/auth.h
+++ b/src/include/libpq/auth.h
@@ -19,6 +19,7 @@
 extern char *pg_krb_server_keyfile;
 extern bool pg_krb_caseins_users;
 extern char *pg_krb_realm;
+extern char *pg_krb_user_ccache;
 
 extern void ClientAuthentication(Port *port);
 extern void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata,
diff --git a/src/include/libpq/be-gssapi-common.h b/src/include/libpq/be-gssapi-common.h
index ae8411245d..6953157f05 100644
--- a/src/include/libpq/be-gssapi-common.h
+++ b/src/include/libpq/be-gssapi-common.h
@@ -18,13 +18,16 @@
 
 #if defined(HAVE_GSSAPI_H)
 #include <gssapi.h>
+#include <gssapi_ext.h>
 #else
 #include <gssapi/gssapi.h>
+#include <gssapi/gssapi_ext.h>
 #endif
 
 extern void pg_GSS_error(const char *errmsg,
 						 OM_uint32 maj_stat, OM_uint32 min_stat);
 
+extern void pg_store_proxy_credential(gss_cred_id_t cred);
 #endif							/* ENABLE_GSS */
 
 #endif							/* BE_GSSAPI_COMMON_H */
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 90036f7bcd..99affd91fe 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -112,6 +112,7 @@ typedef struct HbaLine
 	bool		include_realm;
 	bool		compat_realm;
 	bool		upn_username;
+	bool		allow_cred_delegation;
 	List	   *radiusservers;
 	char	   *radiusservers_s;
 	List	   *radiussecrets;
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index dd3e5efba3..0938ee7ce2 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -95,6 +95,8 @@ typedef struct
 								 * GSSAPI auth was not used */
 	bool		auth;			/* GSSAPI Authentication used */
 	bool		enc;			/* GSSAPI encryption in use */
+	bool		proxy_creds;	/* GSSAPI Delegated/proxy credentials? */
+	gss_cred_id_t proxy;		/* GSSAPI Proxy credentials */
 #endif
 } pg_gssinfo;
 #endif
@@ -321,6 +323,7 @@ extern PGDLLIMPORT openssl_tls_init_hook_typ openssl_tls_init_hook;
 extern bool be_gssapi_get_auth(Port *port);
 extern bool be_gssapi_get_enc(Port *port);
 extern const char *be_gssapi_get_princ(Port *port);
+extern bool be_gssapi_get_proxy(Port *port);
 
 /* Read and write to a GSSAPI-encrypted connection. */
 extern ssize_t be_gssapi_read(Port *port, void *ptr, size_t len);
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index e8bcc88370..7ded77aff3 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -186,3 +186,4 @@ PQpipelineStatus          183
 PQsetTraceFlags           184
 PQmblenBounded            185
 PQsendFlushRequest        186
+PQconnectionUsedGSSAPI    187
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 6fceff561b..943db5c722 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -61,6 +61,7 @@ pg_GSS_continue(PGconn *conn, int payloadlen)
 				lmin_s;
 	gss_buffer_desc ginbuf;
 	gss_buffer_desc goutbuf;
+	gss_cred_id_t proxy;
 
 	/*
 	 * On first call, there's no input token. On subsequent calls, read the
@@ -93,12 +94,16 @@ pg_GSS_continue(PGconn *conn, int payloadlen)
 		ginbuf.value = NULL;
 	}
 
+	/* Check if we can aquire a proxy credential. */
+	if (!pg_GSS_have_cred_cache(&proxy))
+		proxy = GSS_C_NO_CREDENTIAL;
+
 	maj_stat = gss_init_sec_context(&min_stat,
-									GSS_C_NO_CREDENTIAL,
+									proxy,
 									&conn->gctx,
 									conn->gtarg_nam,
 									GSS_C_NO_OID,
-									GSS_C_MUTUAL_FLAG,
+									GSS_C_MUTUAL_FLAG | GSS_C_DELEG_FLAG,
 									0,
 									GSS_C_NO_CHANNEL_BINDINGS,
 									(ginbuf.value == NULL) ? GSS_C_NO_BUFFER : &ginbuf,
@@ -138,7 +143,10 @@ pg_GSS_continue(PGconn *conn, int payloadlen)
 	}
 
 	if (maj_stat == GSS_S_COMPLETE)
+	{
 		gss_release_name(&lmin_s, &conn->gtarg_nam);
+		conn->gssapi_used = true;
+	}
 
 	return STATUS_OK;
 }
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index cf554d389f..a296c82d1d 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -600,6 +600,7 @@ pqDropServerData(PGconn *conn)
 	conn->last_sqlstate[0] = '\0';
 	conn->auth_req_received = false;
 	conn->password_needed = false;
+	conn->gssapi_used = false;
 	conn->write_failed = false;
 	if (conn->write_err_msg)
 		free(conn->write_err_msg);
@@ -6981,6 +6982,17 @@ PQconnectionUsedPassword(const PGconn *conn)
 		return false;
 }
 
+int
+PQconnectionUsedGSSAPI(const PGconn *conn)
+{
+	if (!conn)
+		return false;
+	if (conn->gssapi_used)
+		return true;
+	else
+		return false;
+}
+
 int
 PQclientEncoding(const PGconn *conn)
 {
diff --git a/src/interfaces/libpq/fe-secure-gssapi.c b/src/interfaces/libpq/fe-secure-gssapi.c
index 6ea52ed866..5eeaca542d 100644
--- a/src/interfaces/libpq/fe-secure-gssapi.c
+++ b/src/interfaces/libpq/fe-secure-gssapi.c
@@ -631,7 +631,7 @@ pqsecure_open_gss(PGconn *conn)
 	 */
 	major = gss_init_sec_context(&minor, conn->gcred, &conn->gctx,
 								 conn->gtarg_nam, GSS_C_NO_OID,
-								 GSS_REQUIRED_FLAGS, 0, 0, &input, NULL,
+								 GSS_REQUIRED_FLAGS | GSS_C_DELEG_FLAG, 0, 0, &input, NULL,
 								 &output, NULL, NULL);
 
 	/* GSS Init Sec Context uses the whole packet, so clear it */
@@ -651,6 +651,7 @@ pqsecure_open_gss(PGconn *conn)
 		 * to do GSS wrapping/unwrapping.
 		 */
 		conn->gssenc = true;
+		conn->gssapi_used = true;
 
 		/* Clean up */
 		gss_release_cred(&minor, &conn->gcred);
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 7986445f1a..bdd073c645 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -354,6 +354,7 @@ extern int	PQbackendPID(const PGconn *conn);
 extern PGpipelineStatus PQpipelineStatus(const PGconn *conn);
 extern int	PQconnectionNeedsPassword(const PGconn *conn);
 extern int	PQconnectionUsedPassword(const PGconn *conn);
+extern int	PQconnectionUsedGSSAPI(const PGconn *conn);
 extern int	PQclientEncoding(const PGconn *conn);
 extern int	PQsetClientEncoding(PGconn *conn, const char *encoding);
 
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index e0cee4b142..5dd9a52305 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -449,6 +449,7 @@ struct pg_conn
 	int			sversion;		/* server version, e.g. 70401 for 7.4.1 */
 	bool		auth_req_received;	/* true if any type of auth req received */
 	bool		password_needed;	/* true if server demanded a password */
+	bool		gssapi_used;	/* true if authenticated via gssapi */
 	bool		sigpipe_so;		/* have we masked SIGPIPE via SO_NOSIGPIPE? */
 	bool		sigpipe_flag;	/* can we mask SIGPIPE via MSG_NOSIGNAL? */
 	bool		write_failed;	/* have we had a write failure on sock? */
diff --git a/src/test/kerberos/Makefile b/src/test/kerberos/Makefile
index c531998835..67dfaae901 100644
--- a/src/test/kerberos/Makefile
+++ b/src/test/kerberos/Makefile
@@ -13,6 +13,9 @@ subdir = src/test/kerberos
 top_builddir = ../../..
 include $(top_builddir)/src/Makefile.global
 
+EXTRA_INSTALL += contrib/postgres_fdw
+EXTRA_INSTALL += contrib/dblink
+
 export with_gssapi with_krb_srvnam
 
 check:
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 62e0542639..92bfb7c46a 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -45,6 +45,7 @@ elsif ($^O eq 'linux')
 
 my $krb5_config  = 'krb5-config';
 my $kinit        = 'kinit';
+my $klist        = 'klist';
 my $kdb5_util    = 'kdb5_util';
 my $kadmin_local = 'kadmin.local';
 my $krb5kdc      = 'krb5kdc';
@@ -53,6 +54,7 @@ if ($krb5_bin_dir && -d $krb5_bin_dir)
 {
 	$krb5_config = $krb5_bin_dir . '/' . $krb5_config;
 	$kinit       = $krb5_bin_dir . '/' . $kinit;
+	$klist       = $krb5_bin_dir . '/' . $klist;
 }
 if ($krb5_sbin_dir && -d $krb5_sbin_dir)
 {
@@ -97,6 +99,7 @@ kdc = FILE:$kdc_log
 
 [libdefaults]
 default_realm = $realm
+forwardable = false
 
 [realms]
 $realm = {
@@ -174,7 +177,21 @@ lc_messages = 'C'
 });
 $node->start;
 
+my $port = $node->port();
+
 $node->safe_psql('postgres', 'CREATE USER test1;');
+$node->safe_psql('postgres', 'CREATE EXTENSION postgres_fdw;');
+$node->safe_psql('postgres', 'CREATE EXTENSION dblink;');
+$node->safe_psql('postgres', "CREATE SERVER s1 FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host '$host', hostaddr '$hostaddr', port '$port', dbname 'postgres');");
+
+$node->safe_psql('postgres', 'GRANT USAGE ON FOREIGN SERVER s1 TO test1;');
+
+$node->safe_psql('postgres', "CREATE USER MAPPING FOR test1 SERVER s1 OPTIONS (user 'test1', password_required 'false');");
+$node->safe_psql('postgres', "CREATE TABLE t1 (c1 int);");
+$node->safe_psql('postgres', "INSERT INTO t1 VALUES (1);");
+$node->safe_psql('postgres', "CREATE FOREIGN TABLE tf1 (c1 int) SERVER s1 OPTIONS (schema_name 'public', table_name 't1');");
+$node->safe_psql('postgres', "GRANT SELECT ON t1 TO test1;");
+$node->safe_psql('postgres', "GRANT SELECT ON tf1 TO test1;");
 
 note "running tests";
 
@@ -240,6 +257,7 @@ $node->restart;
 test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket');
 
 run_log [ $kinit, 'test1' ], \$test1_password or BAIL_OUT($?);
+run_log [ $klist, '-f' ] or BAIL_OUT($?);
 
 test_access(
 	$node,
@@ -262,7 +280,7 @@ test_access(
 	'',
 	'succeeds with mapping with default gssencmode and host hba',
 	"connection authenticated: identity=\"test1\@$realm\" method=gss",
-	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
+	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, proxy_credentials=no, principal=test1\@$realm)"
 );
 
 test_access(
@@ -273,7 +291,7 @@ test_access(
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred with host hba',
 	"connection authenticated: identity=\"test1\@$realm\" method=gss",
-	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
+	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, proxy_credentials=no, principal=test1\@$realm)"
 );
 test_access(
 	$node,
@@ -283,7 +301,7 @@ test_access(
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required with host hba',
 	"connection authenticated: identity=\"test1\@$realm\" method=gss",
-	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
+	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, proxy_credentials=no, principal=test1\@$realm)"
 );
 
 # Test that we can transport a reasonable amount of data.
@@ -312,6 +330,11 @@ $node->append_conf('pg_hba.conf',
 	qq{hostgssenc all all $hostaddr/32 gss map=mymap});
 $node->restart;
 
+string_replace_file($krb5_conf, "forwardable = false", "forwardable = true");
+
+run_log [ $kinit, 'test1' ], \$test1_password or BAIL_OUT($?);
+run_log [ $klist, '-f' ] or BAIL_OUT($?);
+
 test_access(
 	$node,
 	'test1',
@@ -320,7 +343,7 @@ test_access(
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostgssenc hba',
 	"connection authenticated: identity=\"test1\@$realm\" method=gss",
-	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
+	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, proxy_credentials=yes, principal=test1\@$realm)"
 );
 test_access(
 	$node,
@@ -330,7 +353,7 @@ test_access(
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required and hostgssenc hba',
 	"connection authenticated: identity=\"test1\@$realm\" method=gss",
-	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
+	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, proxy_credentials=yes, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
 	'fails with GSS encryption disabled and hostgssenc hba');
@@ -348,7 +371,7 @@ test_access(
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostnogssenc hba, but no encryption',
 	"connection authenticated: identity=\"test1\@$realm\" method=gss",
-	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
+	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, proxy_credentials=yes, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=require',
 	'fails with GSS-encrypted access required and hostnogssenc hba');
@@ -360,9 +383,25 @@ test_access(
 	'gssencmode=disable',
 	'succeeds with GSS encryption disabled and hostnogssenc hba',
 	"connection authenticated: identity=\"test1\@$realm\" method=gss",
-	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
+	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, proxy_credentials=yes, principal=test1\@$realm)"
 );
 
+test_query(
+	$node,
+	'test1',
+	"SELECT * FROM dblink('user=test1 dbname=$dbname host=$host hostaddr=$hostaddr port=$port password=1234','select 1') as t1(c1 int);",
+	qr/^1$/s,
+	'gssencmode=prefer',
+	'dblink works not-encrypted');
+
+test_query(
+	$node,
+	'test1',
+	"TABLE tf1;",
+	qr/^1$/s,
+	'gssencmode=prefer',
+	'postgres_fdw works not-encrypted');
+
 truncate($node->data_dir . '/pg_ident.conf', 0);
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
@@ -377,9 +416,25 @@ test_access(
 	'',
 	'succeeds with include_realm=0 and defaults',
 	"connection authenticated: identity=\"test1\@$realm\" method=gss",
-	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
+	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, proxy_credentials=yes, principal=test1\@$realm)"
 );
 
+test_query(
+	$node,
+	'test1',
+	"SELECT * FROM dblink('user=test1 dbname=$dbname host=$host hostaddr=$hostaddr port=$port password=1234','select 1') as t1(c1 int);",
+	qr/^1$/s,
+	'gssencmode=require',
+	'dblink works encrypted');
+
+test_query(
+	$node,
+	'test1',
+	"TABLE tf1;",
+	qr/^1$/s,
+	'gssencmode=require',
+	'postgres_fdw works encrypted');
+
 # Reset pg_hba.conf, and cause a usermap failure with an authentication
 # that has passed.
 unlink($node->data_dir . '/pg_hba.conf');
@@ -396,4 +451,19 @@ test_access(
 	'fails with wrong krb_realm, but still authenticates',
 	"connection authenticated: identity=\"test1\@$realm\" method=gss");
 
+# Reset pg_hba.conf, and have the server refuse delegated credentials.
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf',
+	qq{host all all $hostaddr/32 gss include_realm=0 allow_cred_delegation=0});
+$node->restart;
+
+my ($psql_stdout, $psql_stderr, $psql_timed_out);
+my $psql_cmdret = $node->psql('postgres',
+	"SELECT * FROM dblink('user=test1 dbname=$dbname host=$host hostaddr=$hostaddr port=$port password=1234','select 1') as t1(c1 int);",
+	stdout => \$psql_stdout, stderr => \$psql_stderr,
+	connstr => $node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require");
+
+ok ($psql_cmdret == 3, 'error result from dblink failing without delegated credentials');
+like ($psql_stderr, qr/could not establish connection/, 'dblink fails due to missing delegated credentials');
+
 done_testing();
diff --git a/src/test/perl/PostgreSQL/Test/Utils.pm b/src/test/perl/PostgreSQL/Test/Utils.pm
index dca1b3b17c..0f94a628a5 100644
--- a/src/test/perl/PostgreSQL/Test/Utils.pm
+++ b/src/test/perl/PostgreSQL/Test/Utils.pm
@@ -65,6 +65,7 @@ our @EXPORT = qw(
   slurp_dir
   slurp_file
   append_to_file
+  string_replace_file
   check_mode_recursive
   chmod_recursive
   check_pg_config
@@ -544,6 +545,32 @@ sub append_to_file
 
 =pod
 
+=item string_replace_file(filename, find, replace)
+
+Find and replace string of a given file.
+
+=cut
+
+sub string_replace_file
+{
+	my ($filename, $find, $replace) = @_;
+	open(my $in, '<', $filename);
+	my $content;
+	while(<$in>)
+	{
+		$_ =~ s/$find/$replace/;
+		$content = $content.$_;
+	}
+	close $in;
+	open(my $out, '>', $filename);
+	print $out $content;
+	close($out);
+
+	return;
+}
+
+=pod
+
 =item check_mode_recursive(dir, expected_dir_mode, expected_file_mode, ignore_list)
 
 Check that all file/dir modes in a directory match the expected values,
-- 
2.30.2

Attachment: signature.asc
Description: PGP signature

Reply via email to