Hi hackers,

I'd like to add kerberos authentication support for postgres_fdw by adding two
options to user mapping: krb_client_keyfile and gssencmode.

In the backend we have krb_server_keyfile option to specify a keytab file to
be used by postgres server, krb_client_keyfile is doing mostly the same thing.
This allows postgres_fdw(backend process) to authenticate on behalf of a
logged in user who is querying the foreign table. The credential is kept in
the backend process memory instead of local file to prevent abuse by users
on the same host.

Because backend process is accessing the filesystem of the server host, this
option should only be manipulated by super user. Otherwise, normal user may
steal the identity or probe the server filesystem. This principal is the same to
sslcert and sslkey options in user mapping.

Thoughts?

Best regards,
Peifeng




diff --git a/contrib/postgres_fdw/connection.c b/contrib/postgres_fdw/connection.c
index 82aa14a65d..ba2edf0cba 100644
--- a/contrib/postgres_fdw/connection.c
+++ b/contrib/postgres_fdw/connection.c
@@ -333,6 +333,38 @@ make_new_connection(ConnCacheEntry *entry, UserMapping *user)
 		 entry->conn, server->servername, user->umid, user->userid);
 }
 
+
+/*
+ * Look for "krb_client_keyfile" option in user mapping.
+ * If found, set the KRB5_CLIENT_KTNAME environment just like we do
+ * in backend. Also remove this option from option list.
+ *
+ * If there are multiple principals in the keytab file, only the
+ * default(first) one will be selected.
+ */
+static void setup_kerberos_env(UserMapping *user)
+{
+	ListCell *lc;
+	DefElem    *d;
+	StringInfo si = NULL;
+	foreach(lc, user->options)
+	{
+		d = (DefElem *) lfirst(lc);
+		if (strcmp(d->defname, "krb_client_keyfile") == 0)
+		{
+			si = makeStringInfo();
+			appendStringInfo(si, "KRB5_CLIENT_KTNAME=%s", defGetString(d));
+			user->options = list_delete_cell(user->options, lc);
+			putenv(si->data);
+			/* We don't want credential to be cached as file, keep it within current process.
+			 * The credential can be reused within the same session.
+			 */
+			putenv("KRB5CCNAME=MEMORY:");
+			return;
+		}
+	}
+}
+
 /*
  * Connect to remote server using specified server and user mapping properties.
  */
@@ -350,6 +382,8 @@ connect_pg_server(ForeignServer *server, UserMapping *user)
 		const char **values;
 		int			n;
 
+		setup_kerberos_env(user);
+
 		/*
 		 * Construct connection params from generic options of ForeignServer
 		 * and UserMapping.  (Some of them might not be libpq options, in
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index b8561d6a3c..afb2dc9b14 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, krb_client_keyfile, gssencmode
 -- But we can add valid ones fine
 ALTER USER MAPPING FOR public SERVER testserver1
 	OPTIONS (ADD sslpassword 'dummy');
@@ -196,6 +196,8 @@ ALTER USER MAPPING FOR public SERVER testserver1
 -- permitted to check validation.
 ALTER USER MAPPING FOR public SERVER testserver1
 	OPTIONS (ADD sslkey 'value', ADD sslcert 'value');
+ALTER USER MAPPING FOR public SERVER testserver1
+	OPTIONS (ADD krb_client_keyfile 'value', ADD gssencmode 'value');
 ALTER FOREIGN TABLE ft1 OPTIONS (schema_name 'S 1', table_name 'T 1');
 ALTER FOREIGN TABLE ft2 OPTIONS (schema_name 'S 1', table_name 'T 1');
 ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (column_name 'C 1');
@@ -9298,6 +9300,13 @@ HINT:  User mappings with the sslcert or sslkey options set may only be created
 ALTER USER MAPPING FOR CURRENT_USER SERVER loopback_nopw OPTIONS (ADD sslkey 'foo.key');
 ERROR:  sslcert and sslkey are superuser-only
 HINT:  User mappings with the sslcert or sslkey options set may only be created or modified by the superuser
+-- unpriv user also cannot set krb_client_keyfile / gssencmode on the user mapping
+ALTER USER MAPPING FOR CURRENT_USER SERVER loopback_nopw OPTIONS (ADD krb_client_keyfile 'foo.ktab');
+ERROR:  krb_client_keyfile and gssencmode are superuser-only
+HINT:  User mappings with the krb_client_keyfile or gssencmode options set may only be created or modified by the superuser
+ALTER USER MAPPING FOR CURRENT_USER SERVER loopback_nopw OPTIONS (ADD gssencmode 'require');
+ERROR:  krb_client_keyfile and gssencmode are superuser-only
+HINT:  User mappings with the krb_client_keyfile or gssencmode options set may only be created or modified by the superuser
 -- We're done with the role named after a specific user and need to check the
 -- changes to the public mapping.
 DROP USER MAPPING FOR CURRENT_USER SERVER loopback_nopw;
diff --git a/contrib/postgres_fdw/kerberos/.gitignore b/contrib/postgres_fdw/kerberos/.gitignore
new file mode 100644
index 0000000000..871e943d50
--- /dev/null
+++ b/contrib/postgres_fdw/kerberos/.gitignore
@@ -0,0 +1,2 @@
+# Generated by test suite
+/tmp_check/
diff --git a/contrib/postgres_fdw/kerberos/Makefile b/contrib/postgres_fdw/kerberos/Makefile
new file mode 100644
index 0000000000..3efec922f0
--- /dev/null
+++ b/contrib/postgres_fdw/kerberos/Makefile
@@ -0,0 +1,25 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/kerberos
+#
+# Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/kerberos/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = contrib/postgres_fdw/kerberos
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+export with_gssapi with_krb_srvnam
+
+check:
+	$(prove_check)
+
+installcheck:
+	$(prove_installcheck)
+
+clean distclean maintainer-clean:
+	rm -rf tmp_check
diff --git a/contrib/postgres_fdw/kerberos/README b/contrib/postgres_fdw/kerberos/README
new file mode 100644
index 0000000000..40b8758543
--- /dev/null
+++ b/contrib/postgres_fdw/kerberos/README
@@ -0,0 +1,45 @@
+contrib/postgres_fdw/kerberos/README
+
+Tests for Kerberos/GSSAPI functionality
+=======================================
+
+This directory contains a test suite for Kerberos/GSSAPI
+functionality.  This requires a full MIT Kerberos installation,
+including server and client tools, and is therefore kept separate and
+not run by default.
+
+CAUTION: The test server run by this test is configured to listen for TCP
+connections on localhost. Any user on the same host is able to log in to the
+test server while the tests are running. Do not run this suite on a multi-user
+system where you don't trust all local users! Also, this test suite creates a
+KDC server that listens for TCP/IP connections on localhost without any real
+access control.
+
+Running the tests
+=================
+
+NOTE: You must have given the --enable-tap-tests argument to configure.
+
+Run
+    make check
+or
+    make installcheck
+You can use "make installcheck" if you previously did "make install".
+In that case, the code in the installation tree is tested.  With
+"make check", a temporary installation tree is built from the current
+sources and then tested.
+
+Either way, this test initializes, starts, and stops a test Postgres
+cluster, as well as a test KDC server.
+
+Requirements
+============
+
+MIT Kerberos server and client tools are required.  Heimdal is not
+supported.
+
+Debian/Ubuntu packages: krb5-admin-server krb5-kdc krb5-user
+
+RHEL/CentOS/Fedora packages: krb5-server krb5-workstation
+
+FreeBSD port: krb5 (base system has Heimdal)
diff --git a/contrib/postgres_fdw/kerberos/t/001_auth.pl b/contrib/postgres_fdw/kerberos/t/001_auth.pl
new file mode 100644
index 0000000000..156fe96f07
--- /dev/null
+++ b/contrib/postgres_fdw/kerberos/t/001_auth.pl
@@ -0,0 +1,471 @@
+
+# Copyright (c) 2021, PostgreSQL Global Development Group
+
+# Sets up a KDC and then runs a variety of tests to make sure that the
+# GSSAPI/Kerberos authentication and encryption are working properly,
+# that the options in pg_hba.conf and pg_ident.conf are handled correctly,
+# and that the server-side pg_stat_gssapi view reports what we expect to
+# see for each test.
+#
+# Since this requires setting up a full KDC, it doesn't make much sense
+# to have multiple test scripts (since they'd have to also create their
+# own KDC and that could cause race conditions or other problems)- so
+# just add whatever other tests are needed to here.
+#
+# See the README for additional information.
+
+use strict;
+use warnings;
+use TestLib;
+use PostgresNode;
+use Test::More;
+use Time::HiRes qw(usleep);
+use Cwd 'abs_path';
+
+if ($ENV{with_gssapi} eq 'yes')
+{
+	plan tests => 53;
+}
+else
+{
+	plan skip_all => 'GSSAPI/Kerberos not supported by this build';
+}
+
+my ($krb5_bin_dir, $krb5_sbin_dir);
+
+if ($^O eq 'darwin')
+{
+	$krb5_bin_dir  = '/usr/local/opt/krb5/bin';
+	$krb5_sbin_dir = '/usr/local/opt/krb5/sbin';
+}
+elsif ($^O eq 'freebsd')
+{
+	$krb5_bin_dir  = '/usr/local/bin';
+	$krb5_sbin_dir = '/usr/local/sbin';
+}
+elsif ($^O eq 'linux')
+{
+	$krb5_sbin_dir = '/usr/sbin';
+}
+
+my $krb5_config  = 'krb5-config';
+my $kinit        = 'kinit';
+my $kdb5_util    = 'kdb5_util';
+my $kadmin_local = 'kadmin.local';
+my $krb5kdc      = 'krb5kdc';
+
+if ($krb5_bin_dir && -d $krb5_bin_dir)
+{
+	$krb5_config = $krb5_bin_dir . '/' . $krb5_config;
+	$kinit       = $krb5_bin_dir . '/' . $kinit;
+}
+if ($krb5_sbin_dir && -d $krb5_sbin_dir)
+{
+	$kdb5_util    = $krb5_sbin_dir . '/' . $kdb5_util;
+	$kadmin_local = $krb5_sbin_dir . '/' . $kadmin_local;
+	$krb5kdc      = $krb5_sbin_dir . '/' . $krb5kdc;
+}
+
+my $host     = 'auth-test-localhost.postgresql.example.com';
+my $hostaddr = '127.0.0.1';
+my $realm    = 'EXAMPLE.COM';
+
+
+my $tmp_check = abs_path(${TestLib::tmp_check});
+my $krb5_conf   = "$tmp_check/krb5.conf";
+my $kdc_conf    = "$tmp_check/kdc.conf";
+my $krb5_cache  = "$tmp_check/krb5cc";
+my $krb5_log    = "${TestLib::log_path}/krb5libs.log";
+my $kdc_log     = "${TestLib::log_path}/krb5kdc.log";
+my $kdc_port    = get_free_port();
+my $kdc_datadir = "$tmp_check/krb5kdc";
+my $kdc_pidfile = "$tmp_check/krb5kdc.pid";
+my $keytab      = "$tmp_check/krb5.keytab";
+
+my $dbname      = 'testdb';
+my $testuser    = 'testuser';
+my $test1   = 'test1';
+my $application = '001_auth.pl';
+
+note "setting up Kerberos";
+
+my ($stdout, $krb5_version);
+run_log [ $krb5_config, '--version' ], '>', \$stdout
+  or BAIL_OUT("could not execute krb5-config");
+BAIL_OUT("Heimdal is not supported") if $stdout =~ m/heimdal/;
+$stdout =~ m/Kerberos 5 release ([0-9]+\.[0-9]+)/
+  or BAIL_OUT("could not get Kerberos version");
+$krb5_version = $1;
+
+append_to_file(
+	$krb5_conf,
+	qq![logging]
+default = FILE:$krb5_log
+kdc = FILE:$kdc_log
+
+[libdefaults]
+default_realm = $realm
+
+[realms]
+$realm = {
+    kdc = $hostaddr:$kdc_port
+}!);
+
+append_to_file(
+	$kdc_conf,
+	qq![kdcdefaults]
+!);
+
+# For new-enough versions of krb5, use the _listen settings rather
+# than the _ports settings so that we can bind to localhost only.
+if ($krb5_version >= 1.15)
+{
+	append_to_file(
+		$kdc_conf,
+		qq!kdc_listen = $hostaddr:$kdc_port
+kdc_tcp_listen = $hostaddr:$kdc_port
+!);
+}
+else
+{
+	append_to_file(
+		$kdc_conf,
+		qq!kdc_ports = $kdc_port
+kdc_tcp_ports = $kdc_port
+!);
+}
+
+append_to_file(
+	$kdc_conf,
+	qq!
+[realms]
+$realm = {
+    database_name = $kdc_datadir/principal
+    admin_keytab = FILE:$kdc_datadir/kadm5.keytab
+    acl_file = $kdc_datadir/kadm5.acl
+    key_stash_file = $kdc_datadir/_k5.$realm
+}!);
+
+mkdir $kdc_datadir or die;
+
+# Ensure that we use test's config and cache files, not global ones.
+$ENV{'KRB5_CONFIG'}      = $krb5_conf;
+$ENV{'KRB5_KDC_PROFILE'} = $kdc_conf;
+$ENV{'KRB5CCNAME'}       = $krb5_cache;
+
+my $service_principal = "$ENV{with_krb_srvnam}/$host";
+
+system_or_bail $kdb5_util, 'create', '-s', '-P', 'secret0';
+
+my $test1_password = 'secret1';
+system_or_bail $kadmin_local, '-q', "addprinc -randkey $testuser";
+system_or_bail $kadmin_local, '-q', "addprinc -randkey $service_principal";
+system_or_bail $kadmin_local, '-q', "ktadd -k $keytab $testuser";
+system_or_bail $kadmin_local, '-q', "ktadd -k $keytab $service_principal";
+
+system_or_bail $krb5kdc, '-P', $kdc_pidfile;
+
+#system_or_bail $krb5kdc;
+
+END
+{
+	kill 'INT', `cat $kdc_pidfile` if -f $kdc_pidfile;
+}
+
+note "setting up PostgreSQL instance";
+
+# server node for fdw to connect
+my $server_node = get_new_node('node');
+$server_node->init;
+$server_node->append_conf(
+	'postgresql.conf', qq{
+listen_addresses = '$hostaddr'
+krb_server_keyfile = '$keytab'
+log_connections = on
+lc_messages = 'C'
+});
+$server_node->start;
+
+note "setting up user on server";
+$server_node->safe_psql('postgres', "CREATE USER $testuser;");
+# there's no direct way to get the backend pid from postgres_fdw, create a VIEW at server side to provide this.
+$server_node->safe_psql('postgres', 'CREATE VIEW my_pg_stat_gssapi AS
+    SELECT  S.pid,
+            S.gss_auth AS gss_authenticated,
+            S.gss_princ AS principal,
+            S.gss_enc AS encrypted
+    FROM pg_stat_get_activity(NULL) AS S
+    WHERE S.client_port IS NOT NULL AND S.pid = pg_backend_pid();');
+$server_node->safe_psql('postgres', "GRANT ALL ON my_pg_stat_gssapi TO $testuser;");
+
+my $server_node_port = $server_node->port;
+note "setting up PostgreSQL fdw instance";
+# client node that runs postgres fdw
+my $fdw_node = get_new_node('node_fdw');
+$fdw_node->init;
+$fdw_node->append_conf(
+	'postgresql.conf', qq{
+listen_addresses = '$hostaddr'
+log_connections = on
+lc_messages = 'C'
+});
+$fdw_node->start;
+
+$fdw_node->safe_psql('postgres', "CREATE USER $testuser;");
+$fdw_node->safe_psql('postgres', "CREATE DATABASE $dbname;");
+$fdw_node->safe_psql($dbname, 'CREATE EXTENSION postgres_fdw');
+$fdw_node->safe_psql($dbname, "CREATE SERVER postgres_server FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host '$host', hostaddr '$hostaddr', port '$server_node_port', dbname 'postgres');");
+$fdw_node->safe_psql($dbname, "CREATE FOREIGN TABLE my_pg_stat_gssapi\(pid int, gss_authenticated boolean, principal text, encrypted boolean\) SERVER postgres_server OPTIONS(table_name 'my_pg_stat_gssapi'); ");
+$fdw_node->safe_psql($dbname, "CREATE USER MAPPING for $testuser SERVER postgres_server OPTIONS (user '$testuser');");
+$fdw_node->safe_psql($dbname, "GRANT SELECT ON my_pg_stat_gssapi TO $testuser;");
+
+note "running tests";
+
+sub set_user_mapping_option
+{
+	my ($option, $value, $action) = @_;
+
+	if (!defined $action)
+	{
+		$action = "SET";
+	}
+
+	my $option_str = "$option";
+	if (defined $value)
+	{
+		$option_str = "$option '$value'";
+	}
+
+	$fdw_node->safe_psql($dbname, "ALTER USER MAPPING for $testuser SERVER postgres_server OPTIONS ($action $option_str);");	
+}
+
+sub set_gssencmode
+{
+	my ($mode, $action) = @_;
+	set_user_mapping_option('gssencmode', $mode, $action);
+}
+
+# Test connection success or failure, and if success, that query returns true.
+sub test_access
+{
+	my ($query, $expected_res, $test_name,
+		$expect_output, @expect_log_msgs)
+	  = @_;
+
+	my %params = (sql => $query,);
+
+	# Check log in server node. Obtain log file size before we run any query.
+	# This way we can obtain logs for this psql connection only.
+	my $log_location = -s $server_node->logfile;
+
+	# Run psql in fdw node.
+	my ($ret, $stdout, $stderr) = $fdw_node->psql(
+		$dbname,
+		$query,
+		extra_params => ['-w'],
+		connstr      => "user=$testuser dbname=$dbname host=$host hostaddr=$hostaddr");
+
+	is($ret, $expected_res, $test_name);
+
+	if (defined $expect_output)
+	{
+		if ($expected_res eq 0)
+		{
+			like($stdout, $expect_output, "$test_name: result matches");
+		}
+		else
+		{
+			like($stderr, $expect_output, "$test_name: result matches");
+		}
+	}
+
+	if (@expect_log_msgs)
+	{
+		# Match every message literally.
+		my @regexes = map { qr/\Q$_\E/ } @expect_log_msgs;
+		my $log_contents = TestLib::slurp_file($server_node->logfile, $log_location);
+
+		while (my $regex = shift @regexes)
+		{
+			like($log_contents, $regex, "$test_name: matches");
+		}
+	}
+}
+
+test_access(
+	'SELECT * FROM pg_user_mapping',
+	3,
+	'no access to user mapping',
+	'/permission denied for table pg_user_mapping/'
+);
+
+test_access(
+	'SELECT pid FROM my_pg_stat_gssapi',
+	3,
+	'fails without password',
+	'/Non-superusers must provide a password in the user mapping/'
+);
+
+set_user_mapping_option('password_required', 'false', 'ADD');
+
+unlink($server_node->data_dir . '/pg_hba.conf');
+$server_node->append_conf('pg_hba.conf',
+	qq{host all all $hostaddr/32 gss map=mymap});
+$server_node->restart;
+
+test_access(
+	'SELECT pid FROM my_pg_stat_gssapi',
+	3,
+	'fails without ticket',
+	'/No Kerberos credentials available/'
+);
+
+my $keytabfile = abs_path($keytab);
+$fdw_node->safe_psql($dbname, "ALTER USER MAPPING for $testuser SERVER postgres_server OPTIONS (ADD krb_client_keyfile '$keytabfile');");
+test_access(
+	'SELECT true from my_pg_stat_gssapi',
+	3,
+	'fails without mapping',
+	"/GSSAPI authentication failed for user \"$testuser\"/",
+	"connection authenticated: identity=\"$testuser\@$realm\" method=gss",
+	"no match in usermap \"mymap\" for user \"$testuser\""
+);
+
+$server_node->append_conf('pg_ident.conf', qq{mymap  /^(.*)\@$realm\$  \\1});
+$server_node->restart;
+
+test_access(
+	'SELECT gss_authenticated AND encrypted from my_pg_stat_gssapi;',
+	0,
+	'succeeds with mapping with default gssencmode and host hba',
+	"/^t\$/",
+	"connection authenticated: identity=\"$testuser\@$realm\" method=gss",
+	"connection authorized: user=$testuser database=postgres application_name=postgres_fdw GSS (authenticated=yes, encrypted=yes, principal=$testuser\@$realm)"
+);
+
+# set gssencmode in user mapping
+set_gssencmode('prefer', 'ADD');
+test_access(
+	'SELECT gss_authenticated AND encrypted from my_pg_stat_gssapi;',
+	0,
+	'succeeds with GSS-encrypted access preferred with host hba',
+	"/^t\$/",
+	"connection authenticated: identity=\"$testuser\@$realm\" method=gss",
+	"connection authorized: user=$testuser database=postgres application_name=postgres_fdw GSS (authenticated=yes, encrypted=yes, principal=$testuser\@$realm)"
+);
+
+set_gssencmode('require');
+test_access(
+	'SELECT gss_authenticated AND encrypted from my_pg_stat_gssapi;',
+	0,
+	'succeeds with GSS-encrypted access required with host hba',
+	"/^t\$/",
+	"connection authenticated: identity=\"$testuser\@$realm\" method=gss",
+	"connection authorized: user=$testuser database=postgres application_name=postgres_fdw GSS (authenticated=yes, encrypted=yes, principal=$testuser\@$realm)"
+);
+
+
+# Test that we can transport a reasonable amount of data.
+test_access(
+	'SELECT generate_series(1, 100000) FROM my_pg_stat_gssapi;',
+	0,
+	'receiving 100K lines works',
+	qr/^1\n.*\n1024\n.*\n9999\n.*\n100000$/s
+);
+
+unlink($server_node->data_dir . '/pg_hba.conf');
+$server_node->append_conf('pg_hba.conf',
+	qq{hostgssenc all all $hostaddr/32 gss map=mymap});
+$server_node->restart;
+
+set_gssencmode('prefer');
+test_access(
+	'SELECT gss_authenticated AND encrypted from my_pg_stat_gssapi',
+	0,
+	'succeeds with GSS-encrypted access preferred and hostgssenc hba',
+	"/^t\$/",
+	"connection authenticated: identity=\"$testuser\@$realm\" method=gss",
+	"connection authorized: user=$testuser database=postgres application_name=postgres_fdw GSS (authenticated=yes, encrypted=yes, principal=$testuser\@$realm)"
+);
+
+set_gssencmode('require');
+test_access(
+	'SELECT gss_authenticated AND encrypted from my_pg_stat_gssapi',
+	0,
+	'succeeds with GSS-encrypted access required and hostgssenc hba',
+	"/^t\$/",
+	"connection authenticated: identity=\"$testuser\@$realm\" method=gss",
+	"connection authorized: user=$testuser database=postgres application_name=postgres_fdw GSS (authenticated=yes, encrypted=yes, principal=$testuser\@$realm)"
+);
+
+set_gssencmode('disable');
+test_access(
+	'SELECT gss_authenticated AND encrypted from my_pg_stat_gssapi',
+	3,
+	'fails with GSS encryption disabled and hostgssenc hba',
+	qr/\Qno pg_hba.conf entry for host "$hostaddr", user "$testuser", database "postgres", no encryption\E/,
+	"no pg_hba.conf entry for host \"$hostaddr\", user \"$testuser\", database \"postgres\", no encryption");
+
+unlink($server_node->data_dir . '/pg_hba.conf');
+$server_node->append_conf('pg_hba.conf',
+	qq{hostnogssenc all all $hostaddr/32 gss map=mymap});
+$server_node->restart;
+
+set_gssencmode('prefer');
+test_access(
+	'SELECT gss_authenticated and not encrypted from my_pg_stat_gssapi',
+	0,
+	'succeeds with GSS-encrypted access preferred and hostnogssenc hba, but no encryption',
+	"/^t\$/",
+	"connection authenticated: identity=\"$testuser\@$realm\" method=gss",
+	"connection authorized: user=$testuser database=postgres application_name=postgres_fdw GSS (authenticated=yes, encrypted=no, principal=$testuser\@$realm)"
+);
+
+set_gssencmode('require');
+test_access(
+	'SELECT gss_authenticated and not encrypted from my_pg_stat_gssapi',
+	3,
+	'fails with GSS-encrypted access required and hostnogssenc hba',
+	qr/\Qno pg_hba.conf entry for host "$hostaddr", user "$testuser", database "postgres", GSS encryption\E/,
+	"no pg_hba.conf entry for host \"$hostaddr\", user \"$testuser\", database \"postgres\", GSS encryption");
+
+set_gssencmode('disable');
+test_access(
+	'SELECT gss_authenticated and not encrypted from my_pg_stat_gssapi;',
+	0,
+	'succeeds with GSS encryption disabled and hostnogssenc hba',
+	"/^t\$/",
+	"connection authenticated: identity=\"$testuser\@$realm\" method=gss",
+	"connection authorized: user=$testuser database=postgres application_name=postgres_fdw GSS (authenticated=yes, encrypted=no, principal=$testuser\@$realm)"
+);
+
+
+truncate($server_node->data_dir . '/pg_ident.conf', 0);
+unlink($server_node->data_dir . '/pg_hba.conf');
+$server_node->append_conf('pg_hba.conf',
+	qq{host all all $hostaddr/32 gss include_realm=0});
+$server_node->restart;
+
+set_gssencmode(undef, 'DROP');
+test_access(
+	'SELECT gss_authenticated AND encrypted from my_pg_stat_gssapi;',
+	0,
+	'succeeds with include_realm=0 and defaults',
+	"/^t\$/",
+	"connection authenticated: identity=\"$testuser\@$realm\" method=gss",
+	"connection authorized: user=$testuser database=postgres application_name=postgres_fdw GSS (authenticated=yes, encrypted=yes, principal=$testuser\@$realm)"
+);
+
+# Reset pg_hba.conf, and cause a usermap failure with an authentication
+# that has passed.
+unlink($server_node->data_dir . '/pg_hba.conf');
+$server_node->append_conf('pg_hba.conf',
+	qq{host all all $hostaddr/32 gss include_realm=0 krb_realm=EXAMPLE.ORG});
+$server_node->restart;
+
+test_access(
+	'SELECT gss_authenticated AND encrypted from my_pg_stat_gssapi;',
+	3,
+	'fails with wrong krb_realm, but still authenticates',
+	"/GSSAPI authentication failed for user \"$testuser\"/",
+	"connection authenticated: identity=\"$testuser\@$realm\" method=gss");
\ No newline at end of file
diff --git a/contrib/postgres_fdw/option.c b/contrib/postgres_fdw/option.c
index 4593cbc540..4326f95846 100644
--- a/contrib/postgres_fdw/option.c
+++ b/contrib/postgres_fdw/option.c
@@ -193,6 +193,16 @@ postgres_fdw_validator(PG_FUNCTION_ARGS)
 						 errmsg("sslcert and sslkey are superuser-only"),
 						 errhint("User mappings with the sslcert or sslkey options set may only be created or modified by the superuser")));
 		}
+		else if (strcmp(def->defname, "krb_client_keyfile") == 0 ||
+				 strcmp(def->defname, "gssencmode") == 0)
+		{
+			/* similarly for krb_client_keyfile / gssencmode on user mapping */
+			if (catalog == UserMappingRelationId && !superuser())
+				ereport(ERROR,
+						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+						 errmsg("krb_client_keyfile and gssencmode are superuser-only"),
+						 errhint("User mappings with the krb_client_keyfile or gssencmode options set may only be created or modified by the superuser")));
+		}
 	}
 
 	PG_RETURN_VOID();
@@ -247,6 +257,10 @@ InitPgFdwOptions(void)
 		{"sslcert", UserMappingRelationId, true},
 		{"sslkey", UserMappingRelationId, true},
 
+		/* kerberos settings */
+		{"krb_client_keyfile", UserMappingRelationId, false},
+		{"gssencmode", UserMappingRelationId, false},
+
 		{NULL, InvalidOid, false}
 	};
 
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index c283e74715..9209c32681 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -211,6 +211,8 @@ ALTER USER MAPPING FOR public SERVER testserver1
 -- permitted to check validation.
 ALTER USER MAPPING FOR public SERVER testserver1
 	OPTIONS (ADD sslkey 'value', ADD sslcert 'value');
+ALTER USER MAPPING FOR public SERVER testserver1
+	OPTIONS (ADD krb_client_keyfile 'value', ADD gssencmode 'value');
 
 ALTER FOREIGN TABLE ft1 OPTIONS (schema_name 'S 1', table_name 'T 1');
 ALTER FOREIGN TABLE ft2 OPTIONS (schema_name 'S 1', table_name 'T 1');
@@ -2793,6 +2795,9 @@ SELECT 1 FROM ft1_nopw LIMIT 1;
 ALTER USER MAPPING FOR CURRENT_USER SERVER loopback_nopw OPTIONS (SET password_required 'true');
 ALTER USER MAPPING FOR CURRENT_USER SERVER loopback_nopw OPTIONS (ADD sslcert 'foo.crt');
 ALTER USER MAPPING FOR CURRENT_USER SERVER loopback_nopw OPTIONS (ADD sslkey 'foo.key');
+-- unpriv user also cannot set krb_client_keyfile / gssencmode on the user mapping
+ALTER USER MAPPING FOR CURRENT_USER SERVER loopback_nopw OPTIONS (ADD krb_client_keyfile 'foo.ktab');
+ALTER USER MAPPING FOR CURRENT_USER SERVER loopback_nopw OPTIONS (ADD gssencmode 'require');
 
 -- We're done with the role named after a specific user and need to check the
 -- changes to the public mapping.
diff --git a/doc/src/sgml/postgres-fdw.sgml b/doc/src/sgml/postgres-fdw.sgml
index c9fce77599..c46e63ae82 100644
--- a/doc/src/sgml/postgres-fdw.sgml
+++ b/doc/src/sgml/postgres-fdw.sgml
@@ -144,6 +144,15 @@
        connection setting.
       </para>
      </listitem>
+     <listitem>
+      <para>
+       <literal>krb_client_keyfile</literal> location of keytab
+       file on fdw server host. Only the first principal in this keytab can
+       be selected. It will only be cached in backend memory to prevent other
+       users from using the ticket.
+       <literal>gssencmode</literal> same to "gssencmode" libpq option.
+      </para>
+     </listitem>
     </itemizedlist>
    </para>
 

Reply via email to