From d02c6060dce1c4f2dbb6dd69fd0bc95da39b7dac Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Tue, 29 Jun 2021 10:48:20 -0700
Subject: [PATCH v3 3/4] Add default role for network security operations

Reducing the number of administrative tasks that require superuser
privileges by adding a new pg_network_security role and
delegating to it the authority to create, alter and drop foreign
servers and foreign data wrappers.
---
 contrib/dblink/dblink.c                       | 13 +++---
 contrib/dblink/expected/dblink.out            |  2 +-
 contrib/file_fdw/output/file_fdw.source       |  2 +-
 contrib/postgres_fdw/connection.c             | 20 +++++----
 .../postgres_fdw/expected/postgres_fdw.out    | 20 ++++-----
 contrib/postgres_fdw/option.c                 | 16 ++++---
 doc/src/sgml/user-manag.sgml                  |  6 +++
 src/backend/catalog/aclchk.c                  | 45 ++++++++++++++-----
 src/backend/commands/foreigncmds.c            | 40 +++++++++++------
 src/include/catalog/pg_authid.dat             |  5 +++
 src/test/regress/expected/foreign_data.out    | 18 ++++----
 src/test/regress/expected/privileges.out      | 18 +++++++-
 src/test/regress/sql/privileges.sql           | 17 ++++++-
 13 files changed, 153 insertions(+), 69 deletions(-)

diff --git a/contrib/dblink/dblink.c b/contrib/dblink/dblink.c
index 3a0beaa88e..2c4a065240 100644
--- a/contrib/dblink/dblink.c
+++ b/contrib/dblink/dblink.c
@@ -39,6 +39,7 @@
 #include "access/reloptions.h"
 #include "access/table.h"
 #include "catalog/namespace.h"
+#include "catalog/pg_authid.h"
 #include "catalog/pg_foreign_data_wrapper.h"
 #include "catalog/pg_foreign_server.h"
 #include "catalog/pg_type.h"
@@ -352,7 +353,7 @@ dblink_connect(PG_FUNCTION_ARGS)
 				 errdetail_internal("%s", msg)));
 	}
 
-	/* check password actually used if not superuser */
+	/* check password actually used if not superuser nor pg_network_security */
 	dblink_security_check(conn, rconn);
 
 	/* attempt to set client encoding to match server encoding, if needed */
@@ -2666,7 +2667,8 @@ deleteConnection(const char *name)
 static void
 dblink_security_check(PGconn *conn, remoteConn *rconn)
 {
-	if (!superuser())
+	if (!superuser() &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_NETWORK_SECURITY))
 	{
 		if (!PQconnectionUsedPassword(conn))
 		{
@@ -2678,7 +2680,7 @@ 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."),
+					 errdetail("Non-superuser other than pg_network_security cannot connect if the server does not request a password."),
 					 errhint("Target server's authentication method must be changed.")));
 		}
 	}
@@ -2693,7 +2695,8 @@ dblink_security_check(PGconn *conn, remoteConn *rconn)
 static void
 dblink_connstr_check(const char *connstr)
 {
-	if (!superuser())
+	if (!superuser() &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_NETWORK_SECURITY))
 	{
 		PQconninfoOption *options;
 		PQconninfoOption *option;
@@ -2720,7 +2723,7 @@ dblink_connstr_check(const char *connstr)
 			ereport(ERROR,
 					(errcode(ERRCODE_S_R_E_PROHIBITED_SQL_STATEMENT_ATTEMPTED),
 					 errmsg("password is required"),
-					 errdetail("Non-superusers must provide a password in the connection string.")));
+					 errdetail("Non-superusers other than pg_network_security must provide a password in the connection string.")));
 	}
 }
 
diff --git a/contrib/dblink/expected/dblink.out b/contrib/dblink/expected/dblink.out
index 6516d4f131..d575891ad1 100644
--- a/contrib/dblink/expected/dblink.out
+++ b/contrib/dblink/expected/dblink.out
@@ -887,7 +887,7 @@ SET SESSION AUTHORIZATION regress_dblink_user;
 -- should fail
 SELECT dblink_connect('myconn', 'fdtest');
 ERROR:  password is required
-DETAIL:  Non-superusers must provide a password in the connection string.
+DETAIL:  Non-superusers other than pg_network_security must provide a password in the connection string.
 -- should succeed
 SELECT dblink_connect_u('myconn', 'fdtest');
  dblink_connect_u 
diff --git a/contrib/file_fdw/output/file_fdw.source b/contrib/file_fdw/output/file_fdw.source
index 52b4d5f1df..49e2a9e45f 100644
--- a/contrib/file_fdw/output/file_fdw.source
+++ b/contrib/file_fdw/output/file_fdw.source
@@ -17,7 +17,7 @@ CREATE SERVER file_server FOREIGN DATA WRAPPER file_fdw;
 SET ROLE regress_file_fdw_user;
 CREATE FOREIGN DATA WRAPPER file_fdw2 HANDLER file_fdw_handler VALIDATOR file_fdw_validator;   -- ERROR
 ERROR:  permission denied to create foreign-data wrapper "file_fdw2"
-HINT:  Must be superuser to create a foreign-data wrapper.
+HINT:  Must be superuser or member of pg_network_security to create a foreign-data wrapper.
 CREATE SERVER file_server2 FOREIGN DATA WRAPPER file_fdw;   -- ERROR
 ERROR:  permission denied for foreign-data wrapper file_fdw
 CREATE USER MAPPING FOR regress_file_fdw_user SERVER file_server;   -- ERROR
diff --git a/contrib/postgres_fdw/connection.c b/contrib/postgres_fdw/connection.c
index 82aa14a65d..4f0da2b13d 100644
--- a/contrib/postgres_fdw/connection.c
+++ b/contrib/postgres_fdw/connection.c
@@ -14,6 +14,7 @@
 
 #include "access/htup_details.h"
 #include "access/xact.h"
+#include "catalog/pg_authid.h"
 #include "catalog/pg_user_mapping.h"
 #include "commands/defrem.h"
 #include "funcapi.h"
@@ -23,6 +24,7 @@
 #include "postgres_fdw.h"
 #include "storage/fd.h"
 #include "storage/latch.h"
+#include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/datetime.h"
 #include "utils/hsearch.h"
@@ -425,11 +427,12 @@ connect_pg_server(ForeignServer *server, UserMapping *user)
 		 * check_conn_params.
 		 */
 		if (!superuser_arg(user->userid) && UserMappingPasswordRequired(user) &&
+			!has_privs_of_role(user->userid, ROLE_PG_NETWORK_SECURITY) &&
 			!PQconnectionUsedPassword(conn))
 			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."),
+					 errdetail("Non-superuser other than pg_network_security cannot connect if the server does not request a password."),
 					 errhint("Target server's authentication method must be changed or password_required=false set in the user mapping attributes.")));
 
 		/* Prepare new session for use */
@@ -488,11 +491,11 @@ UserMappingPasswordRequired(UserMapping *user)
 }
 
 /*
- * For non-superusers, insist that the connstr specify a password.  This
- * prevents a password from being picked up from .pgpass, a service file, the
- * environment, etc.  We don't want the postgres user's passwords,
- * certificates, etc to be accessible to non-superusers.  (See also
- * dblink_connstr_check in contrib/dblink.)
+ * For non-superusers other than pg_network_security, insist that the connstr
+ * specify a password.  This prevents a password from being picked up from
+ * .pgpass, a service file, the environment, etc.  We don't want the postgres
+ * user's passwords, certificates, etc to be accessible to non-superusers.
+ * (See also dblink_connstr_check in contrib/dblink.)
  */
 static void
 check_conn_params(const char **keywords, const char **values, UserMapping *user)
@@ -500,7 +503,8 @@ check_conn_params(const char **keywords, const char **values, UserMapping *user)
 	int			i;
 
 	/* no check required if superuser */
-	if (superuser_arg(user->userid))
+	if (superuser_arg(user->userid) ||
+		has_privs_of_role(user->userid, ROLE_PG_NETWORK_SECURITY))
 		return;
 
 	/* ok if params contain a non-empty password */
@@ -517,7 +521,7 @@ check_conn_params(const char **keywords, const char **values, UserMapping *user)
 	ereport(ERROR,
 			(errcode(ERRCODE_S_R_E_PROHIBITED_SQL_STATEMENT_ATTEMPTED),
 			 errmsg("password is required"),
-			 errdetail("Non-superusers must provide a password in the user mapping.")));
+			 errdetail("Non-superusers other than pg_network_security must provide a password in the user mapping.")));
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 31b5de91ad..0c7f28c487 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -9227,7 +9227,7 @@ CREATE FOREIGN TABLE ft1_nopw (
 ) SERVER loopback_nopw OPTIONS (schema_name 'public', table_name 'ft1');
 SELECT 1 FROM ft1_nopw LIMIT 1;
 ERROR:  password is required
-DETAIL:  Non-superusers must provide a password in the user mapping.
+DETAIL:  Non-superusers other than pg_network_security must provide a password in the user mapping.
 -- If we add a password to the connstr it'll fail, because we don't allow passwords
 -- in connstrs only in user mappings.
 DO $d$
@@ -9246,15 +9246,15 @@ PL/pgSQL function inline_code_block line 3 at EXECUTE
 ALTER USER MAPPING FOR CURRENT_USER SERVER loopback_nopw OPTIONS (ADD password 'dummypw');
 SELECT 1 FROM ft1_nopw LIMIT 1;
 ERROR:  password is required
-DETAIL:  Non-superuser cannot connect if the server does not request a password.
+DETAIL:  Non-superuser other than pg_network_security cannot connect if the server does not request a password.
 HINT:  Target server's authentication method must be changed or password_required=false set in the user mapping attributes.
 -- Unpriv user cannot make the mapping passwordless
 ALTER USER MAPPING FOR CURRENT_USER SERVER loopback_nopw OPTIONS (ADD password_required 'false');
-ERROR:  password_required=false is superuser-only
-HINT:  User mappings with the password_required option set to false may only be created or modified by the superuser
+ERROR:  password_required=false requires superuser or pg_network_security privilege
+HINT:  User mappings with the password_required option set to false may only be created or modified by the superuser or pg_network_security
 SELECT 1 FROM ft1_nopw LIMIT 1;
 ERROR:  password is required
-DETAIL:  Non-superuser cannot connect if the server does not request a password.
+DETAIL:  Non-superuser other than pg_network_security cannot connect if the server does not request a password.
 HINT:  Target server's authentication method must be changed or password_required=false set in the user mapping attributes.
 RESET ROLE;
 -- But the superuser can
@@ -9271,11 +9271,11 @@ SELECT 1 FROM ft1_nopw LIMIT 1;
 -- first set password_required so we see the right error messages
 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');
-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
+ERROR:  sslcert and sslkey require superuser or pg_network_security privilege
+HINT:  User mappings with the sslcert or sslkey options set may only be created or modified by the superuser or pg_network_security
 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
+ERROR:  sslcert and sslkey require superuser or pg_network_security privilege
+HINT:  User mappings with the sslcert or sslkey options set may only be created or modified by the superuser or pg_network_security
 -- 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;
@@ -9283,7 +9283,7 @@ DROP USER MAPPING FOR CURRENT_USER SERVER loopback_nopw;
 -- lacks password_required=false
 SELECT 1 FROM ft1_nopw LIMIT 1;
 ERROR:  password is required
-DETAIL:  Non-superusers must provide a password in the user mapping.
+DETAIL:  Non-superusers other than pg_network_security must provide a password in the user mapping.
 RESET ROLE;
 -- The user mapping for public is passwordless and lacks the password_required=false
 -- mapping option, but will work because the current user is a superuser.
diff --git a/contrib/postgres_fdw/option.c b/contrib/postgres_fdw/option.c
index 672b55a808..825c37463b 100644
--- a/contrib/postgres_fdw/option.c
+++ b/contrib/postgres_fdw/option.c
@@ -13,12 +13,14 @@
 #include "postgres.h"
 
 #include "access/reloptions.h"
+#include "catalog/pg_authid_d.h"
 #include "catalog/pg_foreign_server.h"
 #include "catalog/pg_foreign_table.h"
 #include "catalog/pg_user_mapping.h"
 #include "commands/defrem.h"
 #include "commands/extension.h"
 #include "postgres_fdw.h"
+#include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/varlena.h"
 
@@ -167,21 +169,23 @@ postgres_fdw_validator(PG_FUNCTION_ARGS)
 			 * a choice since we can't see the old mapping when validating an
 			 * alter.
 			 */
-			if (!superuser() && !pw_required)
+			if (!superuser() && !pw_required &&
+				!has_privs_of_role(GetUserId(), ROLE_PG_NETWORK_SECURITY))
 				ereport(ERROR,
 						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-						 errmsg("password_required=false is superuser-only"),
-						 errhint("User mappings with the password_required option set to false may only be created or modified by the superuser")));
+						 errmsg("password_required=false requires superuser or pg_network_security privilege"),
+						 errhint("User mappings with the password_required option set to false may only be created or modified by the superuser or pg_network_security")));
 		}
 		else if (strcmp(def->defname, "sslcert") == 0 ||
 				 strcmp(def->defname, "sslkey") == 0)
 		{
 			/* similarly for sslcert / sslkey on user mapping */
-			if (catalog == UserMappingRelationId && !superuser())
+			if (catalog == UserMappingRelationId && !superuser() &&
+				!has_privs_of_role(GetUserId(), ROLE_PG_NETWORK_SECURITY))
 				ereport(ERROR,
 						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-						 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")));
+						 errmsg("sslcert and sslkey require superuser or pg_network_security privilege"),
+						 errhint("User mappings with the sslcert or sslkey options set may only be created or modified by the superuser or pg_network_security")));
 		}
 	}
 
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
index 103c3ed21c..e2d99fc07c 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -593,6 +593,12 @@ DROP ROLE doomed_role;
        event triggers and tablespaces.
        </entry>
       </row>
+      <row>
+       <entry>pg_network_security</entry>
+       <entry>Allow creating, altering and dropping foreign servers and foreign
+       data wrappers and connecting to foreign servers without a password.
+       </entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 50bafae58e..1316a6e36c 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -4362,8 +4362,9 @@ pg_foreign_data_wrapper_aclmask(Oid fdw_oid, Oid roleid,
 
 	Form_pg_foreign_data_wrapper fdwForm;
 
-	/* Bypass permission checks for superusers */
-	if (superuser_arg(roleid))
+	/* Bypass permission checks for superusers and pg_network_security */
+	if (superuser_arg(roleid) ||
+		has_privs_of_role(roleid, ROLE_PG_NETWORK_SECURITY))
 		return mask;
 
 	/*
@@ -4424,8 +4425,12 @@ pg_foreign_server_aclmask(Oid srv_oid, Oid roleid,
 
 	Form_pg_foreign_server srvForm;
 
-	/* Bypass permission checks for superusers */
-	if (superuser_arg(roleid))
+	/*
+	 * Bypass permission checks for superusers and members of the
+	 * pg_network_security role
+	 */
+	if (superuser_arg(roleid) ||
+		has_privs_of_role(roleid, ROLE_PG_NETWORK_SECURITY))
 		return mask;
 
 	/*
@@ -5170,8 +5175,12 @@ pg_foreign_data_wrapper_ownercheck(Oid srv_oid, Oid roleid)
 	HeapTuple	tuple;
 	Oid			ownerId;
 
-	/* Superusers bypass all permission checking. */
-	if (superuser_arg(roleid))
+	/*
+	 * Superusers and members of the pg_network_security role bypass all
+	 * permission checking.
+	 */
+	if (superuser_arg(roleid) ||
+		has_privs_of_role(roleid, ROLE_PG_NETWORK_SECURITY))
 		return true;
 
 	tuple = SearchSysCache1(FOREIGNDATAWRAPPEROID, ObjectIdGetDatum(srv_oid));
@@ -5197,8 +5206,12 @@ pg_foreign_server_ownercheck(Oid srv_oid, Oid roleid)
 	HeapTuple	tuple;
 	Oid			ownerId;
 
-	/* Superusers bypass all permission checking. */
-	if (superuser_arg(roleid))
+	/*
+	 * Superusers and members of the pg_network_security role bypass all
+	 * permission checking.
+	 */
+	if (superuser_arg(roleid) ||
+		has_privs_of_role(roleid, ROLE_PG_NETWORK_SECURITY))
 		return true;
 
 	tuple = SearchSysCache1(FOREIGNSERVEROID, ObjectIdGetDatum(srv_oid));
@@ -5372,8 +5385,12 @@ pg_publication_ownercheck(Oid pub_oid, Oid roleid)
 	HeapTuple	tuple;
 	Oid			ownerId;
 
-	/* Superusers bypass all permission checking. */
-	if (superuser_arg(roleid))
+	/*
+	 * Superusers and members of the pg_network_security role bypass all
+	 * permission checking.
+	 */
+	if (superuser_arg(roleid) ||
+		has_privs_of_role(GetUserId(), ROLE_PG_NETWORK_SECURITY))
 		return true;
 
 	tuple = SearchSysCache1(PUBLICATIONOID, ObjectIdGetDatum(pub_oid));
@@ -5398,8 +5415,12 @@ pg_subscription_ownercheck(Oid sub_oid, Oid roleid)
 	HeapTuple	tuple;
 	Oid			ownerId;
 
-	/* Superusers bypass all permission checking. */
-	if (superuser_arg(roleid))
+	/*
+	 * Superusers and members of the pg_network_security role bypass all
+	 * permission checking.
+	 */
+	if (superuser_arg(roleid) ||
+		has_privs_of_role(GetUserId(), ROLE_PG_NETWORK_SECURITY))
 		return true;
 
 	tuple = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(sub_oid));
diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c
index bc36311d38..711472e972 100644
--- a/src/backend/commands/foreigncmds.c
+++ b/src/backend/commands/foreigncmds.c
@@ -21,6 +21,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_authid_d.h"
 #include "catalog/pg_foreign_data_wrapper.h"
 #include "catalog/pg_foreign_server.h"
 #include "catalog/pg_foreign_table.h"
@@ -214,21 +215,29 @@ AlterForeignDataWrapperOwner_internal(Relation rel, HeapTuple tup, Oid newOwnerI
 
 	form = (Form_pg_foreign_data_wrapper) GETSTRUCT(tup);
 
-	/* Must be a superuser to change a FDW owner */
-	if (!superuser())
+	/*
+	 * Must be a superuser or a member of the pg_network_security role to
+	 * change a FDW owner
+	 */
+	if (!superuser() &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_NETWORK_SECURITY))
 		ereport(ERROR,
 				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 				 errmsg("permission denied to change owner of foreign-data wrapper \"%s\"",
 						NameStr(form->fdwname)),
-				 errhint("Must be superuser to change owner of a foreign-data wrapper.")));
+				 errhint("Must be superuser or member of pg_network_security role to change owner of a foreign-data wrapper.")));
 
-	/* New owner must also be a superuser */
-	if (!superuser_arg(newOwnerId))
+	/*
+	 * New owner must also be a superuser or a member of the
+	 * pg_network_security role
+	 */
+	if (!superuser_arg(newOwnerId) &&
+		!has_privs_of_role(newOwnerId, ROLE_PG_NETWORK_SECURITY))
 		ereport(ERROR,
 				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 				 errmsg("permission denied to change owner of foreign-data wrapper \"%s\"",
 						NameStr(form->fdwname)),
-				 errhint("The owner of a foreign-data wrapper must be a superuser.")));
+				 errhint("The owner of a foreign-data wrapper must be a superuser or member of pg_network_security role.")));
 
 	if (form->fdwowner != newOwnerId)
 	{
@@ -357,8 +366,9 @@ AlterForeignServerOwner_internal(Relation rel, HeapTuple tup, Oid newOwnerId)
 
 			srvId = form->oid;
 
-			/* Must be owner */
-			if (!pg_foreign_server_ownercheck(srvId, GetUserId()))
+			/* Must be owner or a member of the pg_network_security role */
+			if (!pg_foreign_server_ownercheck(srvId, GetUserId()) &&
+				!has_privs_of_role(GetUserId(), ROLE_PG_NETWORK_SECURITY))
 				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_FOREIGN_SERVER,
 							   NameStr(form->srvname));
 
@@ -577,13 +587,14 @@ CreateForeignDataWrapper(CreateFdwStmt *stmt)
 
 	rel = table_open(ForeignDataWrapperRelationId, RowExclusiveLock);
 
-	/* Must be super user */
-	if (!superuser())
+	/* Must be super user or a member of the pg_network_security role */
+	if (!superuser() &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_NETWORK_SECURITY))
 		ereport(ERROR,
 				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 				 errmsg("permission denied to create foreign-data wrapper \"%s\"",
 						stmt->fdwname),
-				 errhint("Must be superuser to create a foreign-data wrapper.")));
+				 errhint("Must be superuser or member of pg_network_security to create a foreign-data wrapper.")));
 
 	/* For now the owner cannot be specified on create. Use effective user ID. */
 	ownerId = GetUserId();
@@ -694,13 +705,14 @@ AlterForeignDataWrapper(AlterFdwStmt *stmt)
 
 	rel = table_open(ForeignDataWrapperRelationId, RowExclusiveLock);
 
-	/* Must be super user */
-	if (!superuser())
+	/* Must be super user or a member of the pg_network_security role */
+	if (!superuser() &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_NETWORK_SECURITY))
 		ereport(ERROR,
 				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 				 errmsg("permission denied to alter foreign-data wrapper \"%s\"",
 						stmt->fdwname),
-				 errhint("Must be superuser to alter a foreign-data wrapper.")));
+				 errhint("Must be superuser or member of pg_network_security to alter a foreign-data wrapper.")));
 
 	tp = SearchSysCacheCopy1(FOREIGNDATAWRAPPERNAME,
 							 CStringGetDatum(stmt->fdwname));
diff --git a/src/include/catalog/pg_authid.dat b/src/include/catalog/pg_authid.dat
index 33d5b637a1..6fff059699 100644
--- a/src/include/catalog/pg_authid.dat
+++ b/src/include/catalog/pg_authid.dat
@@ -89,5 +89,10 @@
   rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
   rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
   rolpassword => '_null_', rolvaliduntil => '_null_' },
+{ oid => '9278', oid_symbol => 'ROLE_PG_NETWORK_SECURITY',
+  rolname => 'pg_network_security', rolsuper => 'f', rolinherit => 't',
+  rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
+  rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
+  rolpassword => '_null_', rolvaliduntil => '_null_' },
 
 ]
diff --git a/src/test/regress/expected/foreign_data.out b/src/test/regress/expected/foreign_data.out
index 5385f98a0f..a4f7ee9af4 100644
--- a/src/test/regress/expected/foreign_data.out
+++ b/src/test/regress/expected/foreign_data.out
@@ -77,7 +77,7 @@ DROP FOREIGN DATA WRAPPER foo;
 SET ROLE regress_test_role;
 CREATE FOREIGN DATA WRAPPER foo; -- ERROR
 ERROR:  permission denied to create foreign-data wrapper "foo"
-HINT:  Must be superuser to create a foreign-data wrapper.
+HINT:  Must be superuser or member of pg_network_security to create a foreign-data wrapper.
 RESET ROLE;
 CREATE FOREIGN DATA WRAPPER foo VALIDATOR postgresql_fdw_validator;
 \dew+
@@ -154,7 +154,7 @@ ERROR:  option "b" provided more than once
 SET ROLE regress_test_role;
 ALTER FOREIGN DATA WRAPPER foo OPTIONS (ADD d '5');         -- ERROR
 ERROR:  permission denied to alter foreign-data wrapper "foo"
-HINT:  Must be superuser to alter a foreign-data wrapper.
+HINT:  Must be superuser or member of pg_network_security to alter a foreign-data wrapper.
 SET ROLE regress_test_role_super;
 ALTER FOREIGN DATA WRAPPER foo OPTIONS (ADD d '5');
 \dew+
@@ -168,13 +168,13 @@ ALTER FOREIGN DATA WRAPPER foo OPTIONS (ADD d '5');
 
 ALTER FOREIGN DATA WRAPPER foo OWNER TO regress_test_role;  -- ERROR
 ERROR:  permission denied to change owner of foreign-data wrapper "foo"
-HINT:  The owner of a foreign-data wrapper must be a superuser.
+HINT:  The owner of a foreign-data wrapper must be a superuser or member of pg_network_security role.
 ALTER FOREIGN DATA WRAPPER foo OWNER TO regress_test_role_super;
 ALTER ROLE regress_test_role_super NOSUPERUSER;
 SET ROLE regress_test_role_super;
 ALTER FOREIGN DATA WRAPPER foo OPTIONS (ADD e '6');         -- ERROR
 ERROR:  permission denied to alter foreign-data wrapper "foo"
-HINT:  Must be superuser to alter a foreign-data wrapper.
+HINT:  Must be superuser or member of pg_network_security to alter a foreign-data wrapper.
 RESET ROLE;
 \dew+
                                                         List of foreign-data wrappers
@@ -1185,13 +1185,13 @@ WARNING:  changing the foreign-data wrapper validator can cause the options for
 SET ROLE regress_unprivileged_role;
 CREATE FOREIGN DATA WRAPPER foobar;                             -- ERROR
 ERROR:  permission denied to create foreign-data wrapper "foobar"
-HINT:  Must be superuser to create a foreign-data wrapper.
+HINT:  Must be superuser or member of pg_network_security to create a foreign-data wrapper.
 ALTER FOREIGN DATA WRAPPER foo OPTIONS (gotcha 'true');         -- ERROR
 ERROR:  permission denied to alter foreign-data wrapper "foo"
-HINT:  Must be superuser to alter a foreign-data wrapper.
+HINT:  Must be superuser or member of pg_network_security to alter a foreign-data wrapper.
 ALTER FOREIGN DATA WRAPPER foo OWNER TO regress_unprivileged_role; -- ERROR
 ERROR:  permission denied to change owner of foreign-data wrapper "foo"
-HINT:  Must be superuser to change owner of a foreign-data wrapper.
+HINT:  Must be superuser or member of pg_network_security role to change owner of a foreign-data wrapper.
 DROP FOREIGN DATA WRAPPER foo;                                  -- ERROR
 ERROR:  must be owner of foreign-data wrapper foo
 GRANT USAGE ON FOREIGN DATA WRAPPER foo TO regress_test_role;   -- ERROR
@@ -1218,10 +1218,10 @@ GRANT USAGE ON FOREIGN DATA WRAPPER foo TO regress_unprivileged_role WITH GRANT
 SET ROLE regress_unprivileged_role;
 CREATE FOREIGN DATA WRAPPER foobar;                             -- ERROR
 ERROR:  permission denied to create foreign-data wrapper "foobar"
-HINT:  Must be superuser to create a foreign-data wrapper.
+HINT:  Must be superuser or member of pg_network_security to create a foreign-data wrapper.
 ALTER FOREIGN DATA WRAPPER foo OPTIONS (gotcha 'true');         -- ERROR
 ERROR:  permission denied to alter foreign-data wrapper "foo"
-HINT:  Must be superuser to alter a foreign-data wrapper.
+HINT:  Must be superuser or member of pg_network_security to alter a foreign-data wrapper.
 DROP FOREIGN DATA WRAPPER foo;                                  -- ERROR
 ERROR:  must be owner of foreign-data wrapper foo
 GRANT USAGE ON FOREIGN DATA WRAPPER postgresql TO regress_test_role; -- WARNING
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 9664276359..adb6813194 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -14,6 +14,7 @@ DROP ROLE IF EXISTS regress_priv_user5;
 DROP ROLE IF EXISTS regress_priv_user6;
 DROP ROLE IF EXISTS regress_priv_user7;
 DROP ROLE IF EXISTS regress_priv_user8;
+DROP ROLE IF EXISTS regress_priv_user9;
 SELECT lo_unlink(oid) FROM pg_largeobject_metadata WHERE oid >= 1000 AND oid < 3000 ORDER BY oid;
  lo_unlink 
 -----------
@@ -31,6 +32,7 @@ ERROR:  role "regress_priv_user5" already exists
 CREATE USER regress_priv_user6;
 CREATE USER regress_priv_user7;
 CREATE USER regress_priv_user8;
+CREATE USER regress_priv_user9;
 GRANT pg_read_all_data TO regress_priv_user6;
 GRANT pg_write_all_data TO regress_priv_user7;
 CREATE GROUP regress_priv_group1;
@@ -1849,6 +1851,17 @@ DROP EVENT TRIGGER user8_event_trigger_start;
 DROP EVENT TRIGGER user8_event_trigger_finish;
 DROP FUNCTION user8_event_trigger;
 RESET SESSION AUTHORIZATION;
+-- test pg_network_security authority to create foreign servers
+-- and foreign data wrappers
+GRANT pg_network_security TO regress_priv_user9;
+SET SESSION AUTHORIZATION regress_priv_user9;
+CREATE FOREIGN DATA WRAPPER user9_fdw VALIDATOR postgresql_fdw_validator;
+COMMENT ON FOREIGN DATA WRAPPER user9_fdw IS 'test comment';
+CREATE SERVER user9_server FOREIGN DATA WRAPPER user9_fdw;
+COMMENT ON SERVER user9_server IS 'test comment';
+DROP SERVER user9_server;
+DROP FOREIGN DATA WRAPPER user9_fdw;
+RESET SESSION AUTHORIZATION;
 -- test default ACLs
 \c -
 CREATE SCHEMA testns;
@@ -2349,8 +2362,9 @@ DROP USER regress_priv_user5;
 DROP USER regress_priv_user6;
 DROP USER regress_priv_user7;
 DROP USER regress_priv_user8;
-DROP USER regress_priv_user9; -- does not exist
-ERROR:  role "regress_priv_user9" does not exist
+DROP USER regress_priv_user9;
+DROP USER regress_priv_user10; -- does not exist
+ERROR:  role "regress_priv_user10" does not exist
 -- permissions with LOCK TABLE
 CREATE USER regress_locktable_user;
 CREATE TABLE lock_table (a int);
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index aea13448bc..3bef0b6b80 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -18,6 +18,7 @@ DROP ROLE IF EXISTS regress_priv_user5;
 DROP ROLE IF EXISTS regress_priv_user6;
 DROP ROLE IF EXISTS regress_priv_user7;
 DROP ROLE IF EXISTS regress_priv_user8;
+DROP ROLE IF EXISTS regress_priv_user9;
 
 SELECT lo_unlink(oid) FROM pg_largeobject_metadata WHERE oid >= 1000 AND oid < 3000 ORDER BY oid;
 
@@ -34,6 +35,7 @@ CREATE USER regress_priv_user5;	-- duplicate
 CREATE USER regress_priv_user6;
 CREATE USER regress_priv_user7;
 CREATE USER regress_priv_user8;
+CREATE USER regress_priv_user9;
 
 GRANT pg_read_all_data TO regress_priv_user6;
 GRANT pg_write_all_data TO regress_priv_user7;
@@ -1113,6 +1115,18 @@ DROP EVENT TRIGGER user8_event_trigger_finish;
 DROP FUNCTION user8_event_trigger;
 RESET SESSION AUTHORIZATION;
 
+-- test pg_network_security authority to create foreign servers
+-- and foreign data wrappers
+GRANT pg_network_security TO regress_priv_user9;
+SET SESSION AUTHORIZATION regress_priv_user9;
+CREATE FOREIGN DATA WRAPPER user9_fdw VALIDATOR postgresql_fdw_validator;
+COMMENT ON FOREIGN DATA WRAPPER user9_fdw IS 'test comment';
+CREATE SERVER user9_server FOREIGN DATA WRAPPER user9_fdw;
+COMMENT ON SERVER user9_server IS 'test comment';
+DROP SERVER user9_server;
+DROP FOREIGN DATA WRAPPER user9_fdw;
+RESET SESSION AUTHORIZATION;
+
 -- test default ACLs
 \c -
 
@@ -1413,7 +1427,8 @@ DROP USER regress_priv_user5;
 DROP USER regress_priv_user6;
 DROP USER regress_priv_user7;
 DROP USER regress_priv_user8;
-DROP USER regress_priv_user9; -- does not exist
+DROP USER regress_priv_user9;
+DROP USER regress_priv_user10; -- does not exist
 
 
 -- permissions with LOCK TABLE
-- 
2.21.1 (Apple Git-122.3)

