From 9c830e9dd22485f8f8990ae811875a4971552133 Mon Sep 17 00:00:00 2001
From: Peifeng Qiu <peifeng.qiu@openpie.com>
Date: Thu, 27 Mar 2025 16:01:23 +0800
Subject: [PATCH] Extensible user mapping handler

Add the following syntax:
CREATE USER MAPPING FOR user_name SERVER server_name
[ USING user_mapping_handler ] [ OPTIONS ( ... ) ]

When "user_mapping_handler" is present, options of a user mapping
is retrieved through this function.

The extension "dummy_um" is a proof of concept for this. A basic
test using postgres_fdw is also included.
---
 contrib/dummy_um/Makefile             | 11 ++++++++
 contrib/dummy_um/dummy_um--1.0.sql    |  4 +++
 contrib/dummy_um/dummy_um.c           | 23 +++++++++++++++++
 contrib/dummy_um/dummy_um.control     |  4 +++
 contrib/dummy_um/expected/basic.out   | 35 +++++++++++++++++++++++++
 contrib/dummy_um/sql/basic.sql        | 37 +++++++++++++++++++++++++++
 src/backend/commands/foreigncmds.c    | 16 +++++++++++-
 src/backend/foreign/foreign.c         | 13 ++++++++--
 src/backend/parser/gram.y             | 12 +++++++--
 src/include/catalog/pg_user_mapping.h |  1 +
 src/include/nodes/parsenodes.h        |  1 +
 11 files changed, 152 insertions(+), 5 deletions(-)
 create mode 100644 contrib/dummy_um/Makefile
 create mode 100644 contrib/dummy_um/dummy_um--1.0.sql
 create mode 100644 contrib/dummy_um/dummy_um.c
 create mode 100644 contrib/dummy_um/dummy_um.control
 create mode 100644 contrib/dummy_um/expected/basic.out
 create mode 100644 contrib/dummy_um/sql/basic.sql

diff --git a/contrib/dummy_um/Makefile b/contrib/dummy_um/Makefile
new file mode 100644
index 00000000000..808935e82ac
--- /dev/null
+++ b/contrib/dummy_um/Makefile
@@ -0,0 +1,11 @@
+# Makefile for the dummy_um PostgreSQL extension
+
+MODULES = dummy_um
+EXTENSION = dummy_um
+DATA = dummy_um--1.0.sql
+PG_CONFIG = pg_config
+PGXS = $(shell $(PG_CONFIG) --pgxs)
+REGRESS = basic
+include $(PGXS)
+
+# Additional build instructions can be added here if necessary.
\ No newline at end of file
diff --git a/contrib/dummy_um/dummy_um--1.0.sql b/contrib/dummy_um/dummy_um--1.0.sql
new file mode 100644
index 00000000000..aed8f7e9a80
--- /dev/null
+++ b/contrib/dummy_um/dummy_um--1.0.sql
@@ -0,0 +1,4 @@
+CREATE FUNCTION dummy_user_mapping_handler(um internal)
+RETURNS internal
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
\ No newline at end of file
diff --git a/contrib/dummy_um/dummy_um.c b/contrib/dummy_um/dummy_um.c
new file mode 100644
index 00000000000..79036a1f865
--- /dev/null
+++ b/contrib/dummy_um/dummy_um.c
@@ -0,0 +1,23 @@
+#include "postgres.h"
+#include "fmgr.h"
+#include "foreign/foreign.h"
+#include "nodes/makefuncs.h"
+
+PG_MODULE_MAGIC;
+
+PG_FUNCTION_INFO_V1(dummy_user_mapping_handler);
+
+Datum
+dummy_user_mapping_handler(PG_FUNCTION_ARGS)
+{
+	UserMapping *um = (UserMapping *)PG_GETARG_POINTER(0);
+	List *options = NIL;
+
+	/* handler can use the options from catalog */
+	(void)um->options;
+
+	options = lappend(options, makeDefElem("user", (Node *)makeString("dummy_test"), -1));
+	options = lappend(options, makeDefElem("password", (Node *)makeString("123"), -1));
+
+	PG_RETURN_POINTER(options);
+}
\ No newline at end of file
diff --git a/contrib/dummy_um/dummy_um.control b/contrib/dummy_um/dummy_um.control
new file mode 100644
index 00000000000..6f90bc27942
--- /dev/null
+++ b/contrib/dummy_um/dummy_um.control
@@ -0,0 +1,4 @@
+# dummy_um.control file contents
+default_version = '1.0'
+module_pathname = '$libdir/dummy_um'
+relocatable = true
diff --git a/contrib/dummy_um/expected/basic.out b/contrib/dummy_um/expected/basic.out
new file mode 100644
index 00000000000..143807b9ccc
--- /dev/null
+++ b/contrib/dummy_um/expected/basic.out
@@ -0,0 +1,35 @@
+CREATE EXTENSION dummy_um;
+CREATE EXTENSION postgres_fdw;
+CREATE USER dummy_test LOGIN PASSWORD '1234';
+CREATE SCHEMA AUTHORIZATION dummy_test;
+CREATE TABLE dummy_test.dummy_table (
+    id SERIAL PRIMARY KEY,
+    name VARCHAR(255) NOT NULL
+);
+INSERT INTO dummy_test.dummy_table (name) VALUES ('dummy');
+GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA dummy_test TO dummy_test;
+DO $d$
+    BEGIN
+        EXECUTE $$CREATE SERVER loopback FOREIGN DATA WRAPPER postgres_fdw
+            OPTIONS (dbname '$$||current_database()||$$',
+                     host '127.0.0.1',
+                     port '$$||current_setting('port')||$$'
+            )$$;
+    END;
+$d$;
+CREATE USER MAPPING FOR CURRENT_USER SERVER loopback
+USING dummy_user_mapping_handler
+OPTIONS (id 'xxx');
+CREATE FOREIGN TABLE dummy_table_fdw (
+    id int NOT NULL,
+    name VARCHAR(255) NOT NULL
+) SERVER loopback OPTIONS (schema_name 'dummy_test', table_name 'dummy_table');
+SELECT * FROM dummy_table_fdw;
+ id | name  
+----+-------
+  1 | dummy
+(1 row)
+
+DROP SCHEMA dummy_test CASCADE;
+NOTICE:  drop cascades to table dummy_test.dummy_table
+DROP USER dummy_test;
diff --git a/contrib/dummy_um/sql/basic.sql b/contrib/dummy_um/sql/basic.sql
new file mode 100644
index 00000000000..cc1cd729ccd
--- /dev/null
+++ b/contrib/dummy_um/sql/basic.sql
@@ -0,0 +1,37 @@
+CREATE EXTENSION dummy_um;
+CREATE EXTENSION postgres_fdw;
+CREATE USER dummy_test LOGIN PASSWORD '1234';
+CREATE SCHEMA AUTHORIZATION dummy_test;
+
+CREATE TABLE dummy_test.dummy_table (
+    id SERIAL PRIMARY KEY,
+    name VARCHAR(255) NOT NULL
+);
+
+INSERT INTO dummy_test.dummy_table (name) VALUES ('dummy');
+GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA dummy_test TO dummy_test;
+
+
+DO $d$
+    BEGIN
+        EXECUTE $$CREATE SERVER loopback FOREIGN DATA WRAPPER postgres_fdw
+            OPTIONS (dbname '$$||current_database()||$$',
+                     host '127.0.0.1',
+                     port '$$||current_setting('port')||$$'
+            )$$;
+    END;
+$d$;
+
+CREATE USER MAPPING FOR CURRENT_USER SERVER loopback
+USING dummy_user_mapping_handler
+OPTIONS (id 'xxx');
+
+CREATE FOREIGN TABLE dummy_table_fdw (
+    id int NOT NULL,
+    name VARCHAR(255) NOT NULL
+) SERVER loopback OPTIONS (schema_name 'dummy_test', table_name 'dummy_table');
+
+SELECT * FROM dummy_table_fdw;
+
+DROP SCHEMA dummy_test CASCADE;
+DROP USER dummy_test;
\ No newline at end of file
diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c
index c14e038d54f..25d7bc24646 100644
--- a/src/backend/commands/foreigncmds.c
+++ b/src/backend/commands/foreigncmds.c
@@ -1111,6 +1111,8 @@ CreateUserMapping(CreateUserMappingStmt *stmt)
 	ForeignServer *srv;
 	ForeignDataWrapper *fdw;
 	RoleSpec   *role = (RoleSpec *) stmt->user;
+	Oid			umHandler;
+	Oid			fdwvalidator;
 
 	rel = table_open(UserMappingRelationId, RowExclusiveLock);
 
@@ -1157,6 +1159,17 @@ CreateUserMapping(CreateUserMappingStmt *stmt)
 	}
 
 	fdw = GetForeignDataWrapper(srv->fdwid);
+	if (stmt->um_handler)
+	{
+		umHandler = DatumGetObjectId(DirectFunctionCall1(regprocin,
+			CStringGetDatum(stmt->um_handler)));
+		fdwvalidator = InvalidOid;
+	}
+	else
+	{
+		umHandler = InvalidOid;
+		fdwvalidator = fdw->fdwvalidator;
+	}
 
 	/*
 	 * Insert tuple into pg_user_mapping.
@@ -1169,12 +1182,13 @@ CreateUserMapping(CreateUserMappingStmt *stmt)
 	values[Anum_pg_user_mapping_oid - 1] = ObjectIdGetDatum(umId);
 	values[Anum_pg_user_mapping_umuser - 1] = ObjectIdGetDatum(useId);
 	values[Anum_pg_user_mapping_umserver - 1] = ObjectIdGetDatum(srv->serverid);
+	values[Anum_pg_user_mapping_umhandler - 1] = ObjectIdGetDatum(umHandler);
 
 	/* Add user options */
 	useoptions = transformGenericOptions(UserMappingRelationId,
 										 PointerGetDatum(NULL),
 										 stmt->options,
-										 fdw->fdwvalidator);
+										 fdwvalidator);
 
 	if (PointerIsValid(DatumGetPointer(useoptions)))
 		values[Anum_pg_user_mapping_umoptions - 1] = useoptions;
diff --git a/src/backend/foreign/foreign.c b/src/backend/foreign/foreign.c
index f0835fc3070..2e066efea24 100644
--- a/src/backend/foreign/foreign.c
+++ b/src/backend/foreign/foreign.c
@@ -203,6 +203,7 @@ GetUserMapping(Oid userid, Oid serverid)
 	HeapTuple	tp;
 	bool		isnull;
 	UserMapping *um;
+	Form_pg_user_mapping form_um;
 
 	tp = SearchSysCache2(USERMAPPINGUSERSERVER,
 						 ObjectIdGetDatum(userid),
@@ -225,12 +226,14 @@ GetUserMapping(Oid userid, Oid serverid)
 				 errmsg("user mapping not found for user \"%s\", server \"%s\"",
 						MappingUserName(userid), server->servername)));
 	}
-
+	form_um = (Form_pg_user_mapping) GETSTRUCT(tp);
 	um = (UserMapping *) palloc(sizeof(UserMapping));
-	um->umid = ((Form_pg_user_mapping) GETSTRUCT(tp))->oid;
+	um->umid = (form_um)->oid;
 	um->userid = userid;
 	um->serverid = serverid;
 
+
+
 	/* Extract the umoptions */
 	datum = SysCacheGetAttr(USERMAPPINGUSERSERVER,
 							tp,
@@ -241,6 +244,12 @@ GetUserMapping(Oid userid, Oid serverid)
 	else
 		um->options = untransformRelOptions(datum);
 
+	if (OidIsValid(form_um->umhandler))
+	{
+		/* custom user mapping handler exist, override options */
+		Datum result = OidFunctionCall1(form_um->umhandler, PointerGetDatum(um));
+		um->options = (List *)result;
+	}
 	ReleaseSysCache(tp);
 
 	return um;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 271ae26cbaf..cf130ed6f66 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -382,6 +382,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				access_method_clause attr_name
 				table_access_method_clause name cursor_name file_name
 				cluster_index_specification
+				user_mapping_handler
 
 %type <list>	func_name handler_name qual_Op qual_all_Op subquery_Op
 				opt_inline_handler opt_validator validator_clause
@@ -5812,13 +5813,14 @@ import_qualification:
  *
  *****************************************************************************/
 
-CreateUserMappingStmt: CREATE USER MAPPING FOR auth_ident SERVER name create_generic_options
+CreateUserMappingStmt: CREATE USER MAPPING FOR auth_ident SERVER name user_mapping_handler create_generic_options
 				{
 					CreateUserMappingStmt *n = makeNode(CreateUserMappingStmt);
 
 					n->user = $5;
 					n->servername = $7;
-					n->options = $8;
+					n->um_handler = $8;
+					n->options = $9;
 					n->if_not_exists = false;
 					$$ = (Node *) n;
 				}
@@ -5839,6 +5841,12 @@ auth_ident: RoleSpec			{ $$ = $1; }
 			| USER				{ $$ = makeRoleSpec(ROLESPEC_CURRENT_USER, @1); }
 		;
 
+/* Custom user mapping handler */
+user_mapping_handler:
+			USING name								{ $$ = $2; }
+			| /*EMPTY*/								{ $$ = NULL; }
+		;
+
 /*****************************************************************************
  *
  *		QUERY :
diff --git a/src/include/catalog/pg_user_mapping.h b/src/include/catalog/pg_user_mapping.h
index 7a0465c4d97..158c088ee90 100644
--- a/src/include/catalog/pg_user_mapping.h
+++ b/src/include/catalog/pg_user_mapping.h
@@ -34,6 +34,7 @@ CATALOG(pg_user_mapping,1418,UserMappingRelationId)
 													 * wanted */
 	Oid			umserver BKI_LOOKUP(pg_foreign_server); /* server of this
 														 * mapping */
+	Oid			umhandler BKI_LOOKUP_OPT(pg_proc);	/* Id of the handler */
 
 #ifdef CATALOG_VARLEN			/* variable-length fields start here */
 	text		umoptions[1];	/* user mapping options */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index df331b1c0d9..563c00670c8 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3001,6 +3001,7 @@ typedef struct CreateUserMappingStmt
 	NodeTag		type;
 	RoleSpec   *user;			/* user role */
 	char	   *servername;		/* server name */
+	char	   *um_handler;		/* custom handler name */
 	bool		if_not_exists;	/* just do nothing if it already exists? */
 	List	   *options;		/* generic options to server */
 } CreateUserMappingStmt;
-- 
2.34.1

