On login trigger: take three

Started by Konstantin Knizhnikover 5 years ago187 messages
#1Konstantin Knizhnik
k.knizhnik@postgrespro.ru
1 attachment(s)

Hi hackers,

Recently I have asked once again by one of our customers about login
trigger in postgres. People are migrating to Postgres from Oracle and 
looking for Postgres analog of this Oracle feature.
This topic is not new:

/messages/by-id/1570308356720-0.post@n3.nabble.com
/messages/by-id/OSAPR01MB507373499CCCEA00EAE79875FE2D0@OSAPR01MB5073.jpnprd01.prod.outlook.com

end even session connect/disconnect hooks were sometimes committed (but
then reverted).
As far as I understand most of the concerns were related with disconnect
hook.
Performing some action on session disconnect is actually much more
complicated than on login.
But customers are not needed it, unlike actions performed at session start.

I wonder if we are really going to make some steps in this directions?
The discussion above was finished with "We haven't rejected the concept
altogether, AFAICT"

I have tried to resurrect this patch and implement on-connect trigger on
top of it.
The syntax is almost the same as proposed by Takayuki:

CREATE EVENT TRIGGER mytrigger
AFTER CONNECTION ON mydatabase
EXECUTE {PROCEDURE | FUNCTION} myproc();

I have replaced CONNECT with CONNECTION because last keyword is already
recognized by Postgres and
make ON clause mandatory to avoid shift-reduce conflicts.

Actually specifying database name is redundant, because we can define
on-connect trigger only for self database (just because triggers and
functions are local to the database).
It may be considered as argument against handling session start using
trigger. But it seems to be the most natural mechanism for users.

On connect trigger can be dropped almost in the same way as normal (on
relation) trigger, but with specifying name of the database instead of
relation name:

DROP TRIGGER mytrigger ON mydatabase;

It is possible to define arbitrary number of on-connect triggers with
different names.

I attached my prototype implementation of this feature.
I just to be sure first that this feature will be interested to community.
If so, I will continue work in it and prepare new version of the patch
for the commitfest.

Example

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

Attachments:

on_login_trigger.patchtext/x-patch; name=on_login_trigger.patchDownload
diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml
index 289dd1d..b3c1fc2 100644
--- a/doc/src/sgml/ref/create_trigger.sgml
+++ b/doc/src/sgml/ref/create_trigger.sgml
@@ -27,7 +27,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE [ CONSTRAINT ] TRIGGER <replaceable class="parameter">name</replaceable> { BEFORE | AFTER | INSTEAD OF } { <replaceable class="parameter">event</replaceable> [ OR ... ] }
-    ON <replaceable class="parameter">table_name</replaceable>
+    ON { <replaceable class="parameter">table_name</replaceable> | <replaceable class="parameter">database_name</replaceable> }
     [ FROM <replaceable class="parameter">referenced_table_name</replaceable> ]
     [ NOT DEFERRABLE | [ DEFERRABLE ] [ INITIALLY IMMEDIATE | INITIALLY DEFERRED ] ]
     [ REFERENCING { { OLD | NEW } TABLE [ AS ] <replaceable class="parameter">transition_relation_name</replaceable> } [ ... ] ]
@@ -41,6 +41,7 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="parameter">name</replaceable>
     UPDATE [ OF <replaceable class="parameter">column_name</replaceable> [, ... ] ]
     DELETE
     TRUNCATE
+    CONNECTION
 </synopsis>
  </refsynopsisdiv>
 
diff --git a/doc/src/sgml/trigger.sgml b/doc/src/sgml/trigger.sgml
index 4a0e746..22caf4c 100644
--- a/doc/src/sgml/trigger.sgml
+++ b/doc/src/sgml/trigger.sgml
@@ -71,6 +71,23 @@
    </para>
 
    <para>
+     You can also define on connection trigger:
+     <programlisting>
+       CREATE EVENT TRIGGER mytrigger
+       AFTER CONNECTION ON mydatabase
+       EXECUTE {PROCEDURE | FUNCTION} myproc();
+     </programlisting>
+     On connection triggers can only be defined for self database.
+     It can be only <literal>AFTER</literal> trigger,
+     and may not contain <literal>WHERE</literal> clause or list of columns.
+     Any number of triggers with unique names can be defined for the database.
+     On connection triggers can be dropped by specifying name of the database:
+     <programlisting>
+       DROP TRIGGER mytrigger ON mydatabase;
+     </programlisting>
+   </para>
+
+   <para>
     The trigger function must be defined before the trigger itself can be
     created.  The trigger function must be declared as a
     function taking no arguments and returning type <literal>trigger</literal>.
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 6dfe1be..da57951 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -1433,11 +1433,23 @@ get_object_address_relobject(ObjectType objtype, List *object,
 
 	/* Extract relation name and open relation. */
 	relname = list_truncate(list_copy(object), nnames - 1);
-	relation = table_openrv_extended(makeRangeVarFromNameList(relname),
-									 AccessShareLock,
-									 missing_ok);
 
-	reloid = relation ? RelationGetRelid(relation) : InvalidOid;
+	address.objectId = InvalidOid;
+	if (objtype == OBJECT_TRIGGER && list_length(relname) == 1)
+	{
+		reloid = get_database_oid(strVal(linitial(relname)), true);
+		if (OidIsValid(reloid))
+			address.objectId = get_trigger_oid(reloid, depname, true);
+	}
+
+	if (!OidIsValid(address.objectId))
+	{
+		relation = table_openrv_extended(makeRangeVarFromNameList(relname),
+										 AccessShareLock,
+										 missing_ok);
+
+		reloid = relation ? RelationGetRelid(relation) : InvalidOid;
+	}
 
 	switch (objtype)
 	{
@@ -1449,8 +1461,11 @@ get_object_address_relobject(ObjectType objtype, List *object,
 			break;
 		case OBJECT_TRIGGER:
 			address.classId = TriggerRelationId;
-			address.objectId = relation ?
-				get_trigger_oid(reloid, depname, missing_ok) : InvalidOid;
+			if (!OidIsValid(address.objectId))
+			{
+				address.objectId = relation ?
+					get_trigger_oid(reloid, depname, missing_ok) : InvalidOid;
+			}
 			address.objectSubId = 0;
 			break;
 		case OBJECT_TABCONSTRAINT:
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index 81f0380..f4f4825 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -104,10 +104,13 @@ RemoveObjects(DropStmt *stmt)
 
 		/* Check permissions. */
 		namespaceId = get_object_namespace(&address);
-		if (!OidIsValid(namespaceId) ||
-			!pg_namespace_ownercheck(namespaceId, GetUserId()))
+		if ((relation != NULL || stmt->removeType != OBJECT_TRIGGER) &&
+			(!OidIsValid(namespaceId) ||
+			 !pg_namespace_ownercheck(namespaceId, GetUserId())))
+		{
 			check_object_ownership(GetUserId(), stmt->removeType, address,
 								   object, relation);
+		}
 
 		/*
 		 * Make note if a temporary namespace has been accessed in this
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 672fccf..25bc132 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -167,7 +167,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 	char	   *qual;
 	Datum		values[Natts_pg_trigger];
 	bool		nulls[Natts_pg_trigger];
-	Relation	rel;
+	Relation	rel = NULL;
 	AclResult	aclresult;
 	Relation	tgrel;
 	SysScanDesc tgscan;
@@ -179,6 +179,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 	char		internaltrigname[NAMEDATALEN];
 	char	   *trigname;
 	Oid			constrrelid = InvalidOid;
+	Oid         targetid = InvalidOid;
 	ObjectAddress myself,
 				referenced;
 	char	   *oldtablename = NULL;
@@ -188,119 +189,141 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 	if (OidIsValid(relOid))
 		rel = table_open(relOid, ShareRowExclusiveLock);
 	else
-		rel = table_openrv(stmt->relation, ShareRowExclusiveLock);
-
-	/*
-	 * Triggers must be on tables or views, and there are additional
-	 * relation-type-specific restrictions.
-	 */
-	if (rel->rd_rel->relkind == RELKIND_RELATION)
 	{
-		/* Tables can't have INSTEAD OF triggers */
-		if (stmt->timing != TRIGGER_TYPE_BEFORE &&
-			stmt->timing != TRIGGER_TYPE_AFTER)
-			ereport(ERROR,
-					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-					 errmsg("\"%s\" is a table",
-							RelationGetRelationName(rel)),
-					 errdetail("Tables cannot have INSTEAD OF triggers.")));
+		if (TRIGGER_FOR_CONNECT(stmt->events))
+		{
+			if (stmt->row)
+				elog(ERROR, "ON CONNECTION trigger can not have FOR EACH ROW clause");
+			if (stmt->transitionRels != NIL)
+				elog(ERROR, "ON CONNECTION trigger can not have FROM clause");
+			if (stmt->whenClause)
+				elog(ERROR, "ON CONNECTION trigger can not have WHEN clause");
+			if (stmt->timing != TRIGGER_TYPE_AFTER)
+				elog(ERROR, "ON CONNECTION trigger can be only AFTER");
+			targetid = get_database_oid(stmt->relation->relname, false);
+			if (targetid != MyDatabaseId)
+				elog(ERROR, "ON CONNECTION trigger can be created only for self database");
+		}
+		else
+			rel = table_openrv(stmt->relation, ShareRowExclusiveLock);
 	}
-	else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
-	{
-		/* Partitioned tables can't have INSTEAD OF triggers */
-		if (stmt->timing != TRIGGER_TYPE_BEFORE &&
-			stmt->timing != TRIGGER_TYPE_AFTER)
-			ereport(ERROR,
-					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-					 errmsg("\"%s\" is a table",
-							RelationGetRelationName(rel)),
-					 errdetail("Tables cannot have INSTEAD OF triggers.")));
 
+	if (rel != NULL)
+	{
+		if (TRIGGER_FOR_CONNECT(stmt->events))
+			elog(ERROR, "ON CONNECTION trigger should be declared for database, not for relations");
+		targetid = RelationGetRelid(rel);
 		/*
-		 * FOR EACH ROW triggers have further restrictions
+		 * Triggers must be on tables or views, and there are additional
+		 * relation-type-specific restrictions.
 		 */
-		if (stmt->row)
+		if (rel->rd_rel->relkind == RELKIND_RELATION)
 		{
+			/* Tables can't have INSTEAD OF triggers */
+			if (stmt->timing != TRIGGER_TYPE_BEFORE &&
+				stmt->timing != TRIGGER_TYPE_AFTER)
+				ereport(ERROR,
+						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+						 errmsg("\"%s\" is a table",
+								RelationGetRelationName(rel)),
+						 errdetail("Tables cannot have INSTEAD OF triggers.")));
+		}
+		else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+		{
+			/* Partitioned tables can't have INSTEAD OF triggers */
+			if (stmt->timing != TRIGGER_TYPE_BEFORE &&
+				stmt->timing != TRIGGER_TYPE_AFTER)
+				ereport(ERROR,
+						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+						 errmsg("\"%s\" is a table",
+								RelationGetRelationName(rel)),
+						 errdetail("Tables cannot have INSTEAD OF triggers.")));
+
 			/*
-			 * Disallow use of transition tables.
-			 *
-			 * Note that we have another restriction about transition tables
-			 * in partitions; search for 'has_superclass' below for an
-			 * explanation.  The check here is just to protect from the fact
-			 * that if we allowed it here, the creation would succeed for a
-			 * partitioned table with no partitions, but would be blocked by
-			 * the other restriction when the first partition was created,
-			 * which is very unfriendly behavior.
+			 * FOR EACH ROW triggers have further restrictions
 			 */
-			if (stmt->transitionRels != NIL)
+			if (stmt->row)
+			{
+				/*
+				 * Disallow use of transition tables.
+				 *
+				 * Note that we have another restriction about transition tables
+				 * in partitions; search for 'has_superclass' below for an
+				 * explanation.  The check here is just to protect from the fact
+				 * that if we allowed it here, the creation would succeed for a
+				 * partitioned table with no partitions, but would be blocked by
+				 * the other restriction when the first partition was created,
+				 * which is very unfriendly behavior.
+				 */
+				if (stmt->transitionRels != NIL)
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("\"%s\" is a partitioned table",
+									RelationGetRelationName(rel)),
+							 errdetail("Triggers on partitioned tables cannot have transition tables.")));
+			}
+		}
+		else if (rel->rd_rel->relkind == RELKIND_VIEW)
+		{
+			/*
+			 * Views can have INSTEAD OF triggers (which we check below are
+			 * row-level), or statement-level BEFORE/AFTER triggers.
+			 */
+			if (stmt->timing != TRIGGER_TYPE_INSTEAD && stmt->row)
 				ereport(ERROR,
-						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-						 errmsg("\"%s\" is a partitioned table",
+						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+						 errmsg("\"%s\" is a view",
 								RelationGetRelationName(rel)),
-						 errdetail("Triggers on partitioned tables cannot have transition tables.")));
+						 errdetail("Views cannot have row-level BEFORE or AFTER triggers.")));
+			/* Disallow TRUNCATE triggers on VIEWs */
+			if (TRIGGER_FOR_TRUNCATE(stmt->events))
+				ereport(ERROR,
+						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+						 errmsg("\"%s\" is a view",
+								RelationGetRelationName(rel)),
+						 errdetail("Views cannot have TRUNCATE triggers.")));
 		}
-	}
-	else if (rel->rd_rel->relkind == RELKIND_VIEW)
-	{
-		/*
-		 * Views can have INSTEAD OF triggers (which we check below are
-		 * row-level), or statement-level BEFORE/AFTER triggers.
-		 */
-		if (stmt->timing != TRIGGER_TYPE_INSTEAD && stmt->row)
-			ereport(ERROR,
-					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-					 errmsg("\"%s\" is a view",
-							RelationGetRelationName(rel)),
-					 errdetail("Views cannot have row-level BEFORE or AFTER triggers.")));
-		/* Disallow TRUNCATE triggers on VIEWs */
-		if (TRIGGER_FOR_TRUNCATE(stmt->events))
-			ereport(ERROR,
-					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-					 errmsg("\"%s\" is a view",
-							RelationGetRelationName(rel)),
-					 errdetail("Views cannot have TRUNCATE triggers.")));
-	}
-	else if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
-	{
-		if (stmt->timing != TRIGGER_TYPE_BEFORE &&
-			stmt->timing != TRIGGER_TYPE_AFTER)
-			ereport(ERROR,
-					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-					 errmsg("\"%s\" is a foreign table",
-							RelationGetRelationName(rel)),
-					 errdetail("Foreign tables cannot have INSTEAD OF triggers.")));
+		else if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+		{
+			if (stmt->timing != TRIGGER_TYPE_BEFORE &&
+				stmt->timing != TRIGGER_TYPE_AFTER)
+				ereport(ERROR,
+						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+						 errmsg("\"%s\" is a foreign table",
+								RelationGetRelationName(rel)),
+						 errdetail("Foreign tables cannot have INSTEAD OF triggers.")));
 
-		if (TRIGGER_FOR_TRUNCATE(stmt->events))
+			if (TRIGGER_FOR_TRUNCATE(stmt->events))
+				ereport(ERROR,
+						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+						 errmsg("\"%s\" is a foreign table",
+								RelationGetRelationName(rel)),
+						 errdetail("Foreign tables cannot have TRUNCATE triggers.")));
+
+			/*
+			 * We disallow constraint triggers to protect the assumption that
+			 * triggers on FKs can't be deferred.  See notes with AfterTriggers
+			 * data structures, below.
+			 */
+			if (stmt->isconstraint)
+				ereport(ERROR,
+						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+						 errmsg("\"%s\" is a foreign table",
+								RelationGetRelationName(rel)),
+						 errdetail("Foreign tables cannot have constraint triggers.")));
+		}
+		else
 			ereport(ERROR,
 					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-					 errmsg("\"%s\" is a foreign table",
-							RelationGetRelationName(rel)),
-					 errdetail("Foreign tables cannot have TRUNCATE triggers.")));
+					 errmsg("\"%s\" is not a table or view",
+							RelationGetRelationName(rel))));
 
-		/*
-		 * We disallow constraint triggers to protect the assumption that
-		 * triggers on FKs can't be deferred.  See notes with AfterTriggers
-		 * data structures, below.
-		 */
-		if (stmt->isconstraint)
+		if (!allowSystemTableMods && IsSystemRelation(rel))
 			ereport(ERROR,
-					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-					 errmsg("\"%s\" is a foreign table",
-							RelationGetRelationName(rel)),
-					 errdetail("Foreign tables cannot have constraint triggers.")));
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					 errmsg("permission denied: \"%s\" is a system catalog",
+							RelationGetRelationName(rel))));
 	}
-	else
-		ereport(ERROR,
-				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-				 errmsg("\"%s\" is not a table or view",
-						RelationGetRelationName(rel))));
-
-	if (!allowSystemTableMods && IsSystemRelation(rel))
-		ereport(ERROR,
-				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-				 errmsg("permission denied: \"%s\" is a system catalog",
-						RelationGetRelationName(rel))));
-
 	if (stmt->isconstraint)
 	{
 		/*
@@ -321,9 +344,9 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 	}
 
 	/* permission checks */
-	if (!isInternal)
+	if (!isInternal && rel != NULL)
 	{
-		aclresult = pg_class_aclcheck(RelationGetRelid(rel), GetUserId(),
+		aclresult = pg_class_aclcheck(targetid, GetUserId(),
 									  ACL_TRIGGER);
 		if (aclresult != ACLCHECK_OK)
 			aclcheck_error(aclresult, get_relkind_objtype(rel->rd_rel->relkind),
@@ -348,7 +371,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 	partition_recurse = !isInternal && stmt->row &&
 		rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE;
 	if (partition_recurse)
-		list_free(find_all_inheritors(RelationGetRelid(rel),
+		list_free(find_all_inheritors(targetid,
 									  ShareRowExclusiveLock, NULL));
 
 	/* Compute tgtype */
@@ -696,13 +719,13 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 		/* Internal callers should have made their own constraints */
 		Assert(!isInternal);
 		constraintOid = CreateConstraintEntry(stmt->trigname,
-											  RelationGetNamespace(rel),
+											  rel != NULL ? RelationGetNamespace(rel) : InvalidOid,
 											  CONSTRAINT_TRIGGER,
 											  stmt->deferrable,
 											  stmt->initdeferred,
 											  true,
 											  InvalidOid,	/* no parent */
-											  RelationGetRelid(rel),
+											  targetid,
 											  NULL, /* no conkey */
 											  0,
 											  0,
@@ -767,7 +790,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 		ScanKeyInit(&key,
 					Anum_pg_trigger_tgrelid,
 					BTEqualStrategyNumber, F_OIDEQ,
-					ObjectIdGetDatum(RelationGetRelid(rel)));
+					ObjectIdGetDatum(targetid));
 		tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
 									NULL, 1, &key);
 		while (HeapTupleIsValid(tuple = systable_getnext(tgscan)))
@@ -775,10 +798,22 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 			Form_pg_trigger pg_trigger = (Form_pg_trigger) GETSTRUCT(tuple);
 
 			if (namestrcmp(&(pg_trigger->tgname), trigname) == 0)
-				ereport(ERROR,
-						(errcode(ERRCODE_DUPLICATE_OBJECT),
-						 errmsg("trigger \"%s\" for relation \"%s\" already exists",
-								trigname, RelationGetRelationName(rel))));
+			{
+				if (rel != NULL)
+				{
+					ereport(ERROR,
+							(errcode(ERRCODE_DUPLICATE_OBJECT),
+							 errmsg("trigger \"%s\" for relation \"%s\" already exists",
+									trigname, RelationGetRelationName(rel))));
+				}
+				else
+				{
+					ereport(ERROR,
+							(errcode(ERRCODE_DUPLICATE_OBJECT),
+							 errmsg("trigger \"%s\" for database \"%s\" already exists",
+									trigname, stmt->relation->relname)));
+				}
+			}
 		}
 		systable_endscan(tgscan);
 	}
@@ -794,7 +829,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 	memset(nulls, false, sizeof(nulls));
 
 	values[Anum_pg_trigger_oid - 1] = ObjectIdGetDatum(trigoid);
-	values[Anum_pg_trigger_tgrelid - 1] = ObjectIdGetDatum(RelationGetRelid(rel));
+	values[Anum_pg_trigger_tgrelid - 1] = ObjectIdGetDatum(targetid);
 	values[Anum_pg_trigger_tgparentid - 1] = ObjectIdGetDatum(parentTriggerOid);
 	values[Anum_pg_trigger_tgname - 1] = DirectFunctionCall1(namein,
 															 CStringGetDatum(trigname));
@@ -927,30 +962,31 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 	if (newtablename)
 		pfree(DatumGetPointer(values[Anum_pg_trigger_tgnewtable - 1]));
 
-	/*
-	 * Update relation's pg_class entry; if necessary; and if not, send an SI
-	 * message to make other backends (and this one) rebuild relcache entries.
-	 */
-	pgrel = table_open(RelationRelationId, RowExclusiveLock);
-	tuple = SearchSysCacheCopy1(RELOID,
-								ObjectIdGetDatum(RelationGetRelid(rel)));
-	if (!HeapTupleIsValid(tuple))
-		elog(ERROR, "cache lookup failed for relation %u",
-			 RelationGetRelid(rel));
-	if (!((Form_pg_class) GETSTRUCT(tuple))->relhastriggers)
+	if (rel != NULL)
 	{
-		((Form_pg_class) GETSTRUCT(tuple))->relhastriggers = true;
-
-		CatalogTupleUpdate(pgrel, &tuple->t_self, tuple);
+		/*
+		 * Update relation's pg_class entry; if necessary; and if not, send an SI
+		 * message to make other backends (and this one) rebuild relcache entries.
+		 */
+		pgrel = table_open(RelationRelationId, RowExclusiveLock);
+		tuple = SearchSysCacheCopy1(RELOID,
+									ObjectIdGetDatum(targetid));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for relation %u", targetid);
+		if (!((Form_pg_class) GETSTRUCT(tuple))->relhastriggers)
+		{
+			((Form_pg_class) GETSTRUCT(tuple))->relhastriggers = true;
 
-		CommandCounterIncrement();
-	}
-	else
-		CacheInvalidateRelcacheByTuple(tuple);
+			CatalogTupleUpdate(pgrel, &tuple->t_self, tuple);
 
-	heap_freetuple(tuple);
-	table_close(pgrel, RowExclusiveLock);
+			CommandCounterIncrement();
+		}
+		else
+			CacheInvalidateRelcacheByTuple(tuple);
 
+		heap_freetuple(tuple);
+		table_close(pgrel, RowExclusiveLock);
+	}
 	/*
 	 * Record dependencies for trigger.  Always place a normal dependency on
 	 * the function.
@@ -977,7 +1013,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 		referenced.objectSubId = 0;
 		recordDependencyOn(&myself, &referenced, DEPENDENCY_INTERNAL);
 	}
-	else
+	else if (rel != NULL)
 	{
 		/*
 		 * User CREATE TRIGGER, so place dependencies.  We make trigger be
@@ -985,7 +1021,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 		 * dropped.  (Auto drop is compatible with our pre-7.3 behavior.)
 		 */
 		referenced.classId = RelationRelationId;
-		referenced.objectId = RelationGetRelid(rel);
+		referenced.objectId = targetid;
 		referenced.objectSubId = 0;
 		recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO);
 
@@ -1018,7 +1054,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 		{
 			ObjectAddressSet(referenced, TriggerRelationId, parentTriggerOid);
 			recordDependencyOn(&myself, &referenced, DEPENDENCY_PARTITION_PRI);
-			ObjectAddressSet(referenced, RelationRelationId, RelationGetRelid(rel));
+			ObjectAddressSet(referenced, RelationRelationId, targetid);
 			recordDependencyOn(&myself, &referenced, DEPENDENCY_PARTITION_SEC);
 		}
 	}
@@ -1029,7 +1065,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 		int			i;
 
 		referenced.classId = RelationRelationId;
-		referenced.objectId = RelationGetRelid(rel);
+		referenced.objectId = targetid;
 		for (i = 0; i < ncolumns; i++)
 		{
 			referenced.objectSubId = columns[i];
@@ -1148,7 +1184,8 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 	}
 
 	/* Keep lock on target rel until end of xact */
-	table_close(rel, NoLock);
+	if (rel != NULL)
+		table_close(rel, NoLock);
 
 	return myself;
 }
@@ -1165,7 +1202,8 @@ RemoveTriggerById(Oid trigOid)
 	ScanKeyData skey[1];
 	HeapTuple	tup;
 	Oid			relid;
-	Relation	rel;
+	Relation	rel = NULL;
+	Form_pg_trigger pg_trigger;
 
 	tgrel = table_open(TriggerRelationId, RowExclusiveLock);
 
@@ -1184,28 +1222,32 @@ RemoveTriggerById(Oid trigOid)
 	if (!HeapTupleIsValid(tup))
 		elog(ERROR, "could not find tuple for trigger %u", trigOid);
 
-	/*
-	 * Open and exclusive-lock the relation the trigger belongs to.
-	 */
-	relid = ((Form_pg_trigger) GETSTRUCT(tup))->tgrelid;
+	pg_trigger = (Form_pg_trigger) GETSTRUCT(tup);
 
-	rel = table_open(relid, AccessExclusiveLock);
+	if (!(pg_trigger->tgtype & TRIGGER_TYPE_CONNECT))
+	{
+		/*
+		 * Open and exclusive-lock the relation the trigger belongs to.
+		 */
+		relid = ((Form_pg_trigger) GETSTRUCT(tup))->tgrelid;
 
-	if (rel->rd_rel->relkind != RELKIND_RELATION &&
-		rel->rd_rel->relkind != RELKIND_VIEW &&
-		rel->rd_rel->relkind != RELKIND_FOREIGN_TABLE &&
-		rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
-		ereport(ERROR,
-				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-				 errmsg("\"%s\" is not a table, view, or foreign table",
-						RelationGetRelationName(rel))));
+		rel = table_open(relid, AccessExclusiveLock);
 
-	if (!allowSystemTableMods && IsSystemRelation(rel))
-		ereport(ERROR,
-				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-				 errmsg("permission denied: \"%s\" is a system catalog",
-						RelationGetRelationName(rel))));
+		if (rel->rd_rel->relkind != RELKIND_RELATION &&
+			rel->rd_rel->relkind != RELKIND_VIEW &&
+			rel->rd_rel->relkind != RELKIND_FOREIGN_TABLE &&
+			rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("\"%s\" is not a table, view, or foreign table",
+							RelationGetRelationName(rel))));
 
+		if (!allowSystemTableMods && IsSystemRelation(rel))
+			ereport(ERROR,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					 errmsg("permission denied: \"%s\" is a system catalog",
+							RelationGetRelationName(rel))));
+	}
 	/*
 	 * Delete the pg_trigger tuple.
 	 */
@@ -1214,19 +1256,22 @@ RemoveTriggerById(Oid trigOid)
 	systable_endscan(tgscan);
 	table_close(tgrel, RowExclusiveLock);
 
-	/*
-	 * We do not bother to try to determine whether any other triggers remain,
-	 * which would be needed in order to decide whether it's safe to clear the
-	 * relation's relhastriggers.  (In any case, there might be a concurrent
-	 * process adding new triggers.)  Instead, just force a relcache inval to
-	 * make other backends (and this one too!) rebuild their relcache entries.
-	 * There's no great harm in leaving relhastriggers true even if there are
-	 * no triggers left.
-	 */
-	CacheInvalidateRelcache(rel);
+	if (rel != NULL)
+	{
+		/*
+		 * We do not bother to try to determine whether any other triggers remain,
+		 * which would be needed in order to decide whether it's safe to clear the
+		 * relation's relhastriggers.  (In any case, there might be a concurrent
+		 * process adding new triggers.)  Instead, just force a relcache inval to
+		 * make other backends (and this one too!) rebuild their relcache entries.
+		 * There's no great harm in leaving relhastriggers true even if there are
+		 * no triggers left.
+		 */
+		CacheInvalidateRelcache(rel);
 
-	/* Keep lock on trigger's rel until end of xact */
-	table_close(rel, NoLock);
+		/* Keep lock on trigger's rel until end of xact */
+		table_close(rel, NoLock);
+	}
 }
 
 /*
@@ -5812,3 +5857,102 @@ pg_trigger_depth(PG_FUNCTION_ARGS)
 {
 	PG_RETURN_INT32(MyTriggerDepth);
 }
+
+void
+standard_session_start_hook(void)
+{
+	TriggerData LocTriggerData;
+	SysScanDesc tgscan;
+	ScanKeyData skey;
+	Relation tgrel;
+	Trigger trigger;
+	FmgrInfo finfo;
+	HeapTuple htup;
+
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+
+	tgrel = table_open(TriggerRelationId, AccessShareLock);
+
+	ScanKeyInit(&skey,
+				Anum_pg_trigger_tgrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(MyDatabaseId));
+
+	memset(&LocTriggerData, 0, sizeof(LocTriggerData));
+	LocTriggerData.type = T_TriggerData;
+	LocTriggerData.tg_event = TRIGGER_EVENT_AFTER;
+	LocTriggerData.tg_trigger = &trigger;
+
+	tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
+							  NULL, 1, &skey);
+	while (HeapTupleIsValid(htup = systable_getnext(tgscan)))
+	{
+		Form_pg_trigger pg_trigger = (Form_pg_trigger) GETSTRUCT(htup);
+		Assert(pg_trigger->tgtype == (TRIGGER_TYPE_AFTER|TRIGGER_TYPE_CONNECT));
+		trigger.tgoid = pg_trigger->oid;			/* OID of trigger (pg_trigger row) */
+		/* Remaining fields are copied from pg_trigger, see pg_trigger.h */
+		trigger.tgname = pg_trigger->tgname.data;
+		trigger.tgfoid = pg_trigger->tgfoid;
+		trigger.tgtype = pg_trigger->tgtype;
+		trigger.tgenabled = pg_trigger->tgenabled;
+		trigger.tgisinternal = pg_trigger->tgisinternal;
+		trigger.tgconstrrelid = pg_trigger->tgconstrrelid;
+		trigger.tgconstrindid = pg_trigger->tgconstrindid;
+		trigger.tgconstraint = pg_trigger->tgconstraint;
+		trigger.tgdeferrable = pg_trigger->tgdeferrable;
+		trigger.tginitdeferred = pg_trigger->tginitdeferred;
+		trigger.tgnargs = pg_trigger->tgnargs;
+		trigger.tgnattr = pg_trigger->tgattr.dim1;
+		if (trigger.tgnattr > 0)
+		{
+			trigger.tgattr = (int16 *) palloc(trigger.tgnattr * sizeof(int16));
+			memcpy(trigger.tgattr, &(pg_trigger->tgattr.values),
+				   trigger.tgnattr * sizeof(int16));
+		}
+		else
+			trigger.tgattr = NULL;
+
+		if (trigger.tgnargs > 0)
+		{
+			bytea	   *val;
+			char	   *p;
+			bool        isnull;
+
+			val = DatumGetByteaPP(fastgetattr(htup,
+											  Anum_pg_trigger_tgargs,
+											  tgrel->rd_att, &isnull));
+			if (isnull)
+				elog(ERROR, "tgargs is null in trigger \"%s\"", trigger.tgname);
+			p = (char *) VARDATA_ANY(val);
+			trigger.tgargs = (char **) palloc(trigger.tgnargs * sizeof(char *));
+			for (int i = 0; i < trigger.tgnargs; i++)
+			{
+				trigger.tgargs[i] = pstrdup(p);
+				p += strlen(p) + 1;
+			}
+		}
+		else
+			trigger.tgargs = NULL;
+
+		trigger.tgqual = NULL;
+		trigger.tgoldtable = NULL;
+		trigger.tgnewtable = NULL;
+
+		memset(&finfo, 0, sizeof(finfo));
+
+		/*
+		 * Call the trigger and throw away any possibly returned updated tuple.
+		 * (Don't let ExecCallTriggerFunc measure EXPLAIN time.)
+		 */
+		ExecCallTriggerFunc(&LocTriggerData,
+							0,
+							&finfo,
+							NULL,
+							CurrentMemoryContext);
+	}
+	systable_endscan(tgscan);
+	table_close(tgrel, AccessShareLock);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index dbb47d4..bf7a639 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -5301,6 +5301,8 @@ TriggerOneEvent:
 				{ $$ = list_make2(makeInteger(TRIGGER_TYPE_UPDATE), $3); }
 			| TRUNCATE
 				{ $$ = list_make2(makeInteger(TRIGGER_TYPE_TRUNCATE), NIL); }
+			| CONNECTION
+				{ $$ = list_make2(makeInteger(TRIGGER_TYPE_CONNECT), NIL); }
 		;
 
 TriggerReferencing:
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index c9424f1..af38640 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -101,8 +101,6 @@ int			max_stack_depth = 100;
 /* wait N seconds to allow attach from a debugger */
 int			PostAuthDelay = 0;
 
-
-
 /* ----------------
  *		private variables
  * ----------------
@@ -167,6 +165,9 @@ static ProcSignalReason RecoveryConflictReason;
 static MemoryContext row_description_context = NULL;
 static StringInfoData row_description_buf;
 
+/* Hook for plugins to get control at start of session */
+session_start_hook_type session_start_hook = standard_session_start_hook;
+
 /* ----------------------------------------------------------------
  *		decls for routines only used in this file
  * ----------------------------------------------------------------
@@ -4017,6 +4018,11 @@ PostgresMain(int argc, char *argv[],
 	if (!IsUnderPostmaster)
 		PgStartTime = GetCurrentTimestamp();
 
+	if (session_start_hook)
+	{
+		(*session_start_hook) ();
+	}
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
@@ -4779,3 +4785,4 @@ disable_statement_timeout(void)
 	if (get_timeout_active(STATEMENT_TIMEOUT))
 		disable_timeout(STATEMENT_TIMEOUT, false);
 }
+
diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h
index bd30607..c76fdff 100644
--- a/src/include/tcop/tcopprot.h
+++ b/src/include/tcop/tcopprot.h
@@ -30,6 +30,12 @@ extern PGDLLIMPORT const char *debug_query_string;
 extern int	max_stack_depth;
 extern int	PostAuthDelay;
 
+/* Hook for plugins to get control at start and end of session */
+typedef void (*session_start_hook_type) (void);
+
+extern PGDLLIMPORT session_start_hook_type session_start_hook;
+extern void standard_session_start_hook(void);
+
 /* GUC-configurable parameters */
 
 typedef enum
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index d4a3d58..6e97028 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -904,9 +904,9 @@ plpgsql_exec_trigger(PLpgSQL_function *func,
 	PLpgSQL_execstate estate;
 	ErrorContextCallback plerrcontext;
 	int			rc;
-	TupleDesc	tupdesc;
-	PLpgSQL_rec *rec_new,
-			   *rec_old;
+	TupleDesc	tupdesc = NULL;
+	PLpgSQL_rec *rec_new = NULL,
+		*rec_old = NULL;
 	HeapTuple	rettup;
 
 	/*
@@ -929,26 +929,28 @@ plpgsql_exec_trigger(PLpgSQL_function *func,
 	estate.err_text = gettext_noop("during initialization of execution state");
 	copy_plpgsql_datums(&estate, func);
 
-	/*
-	 * Put the OLD and NEW tuples into record variables
-	 *
-	 * We set up expanded records for both variables even though only one may
-	 * have a value.  This allows record references to succeed in functions
-	 * that are used for multiple trigger types.  For example, we might have a
-	 * test like "if (TG_OP = 'INSERT' and NEW.foo = 'xyz')", which should
-	 * work regardless of the current trigger type.  If a value is actually
-	 * fetched from an unsupplied tuple, it will read as NULL.
-	 */
-	tupdesc = RelationGetDescr(trigdata->tg_relation);
-
-	rec_new = (PLpgSQL_rec *) (estate.datums[func->new_varno]);
-	rec_old = (PLpgSQL_rec *) (estate.datums[func->old_varno]);
+	if (trigdata->tg_relation)
+	{
+		/*
+		 * Put the OLD and NEW tuples into record variables
+		 *
+		 * We set up expanded records for both variables even though only one may
+		 * have a value.  This allows record references to succeed in functions
+		 * that are used for multiple trigger types.  For example, we might have a
+		 * test like "if (TG_OP = 'INSERT' and NEW.foo = 'xyz')", which should
+		 * work regardless of the current trigger type.  If a value is actually
+		 * fetched from an unsupplied tuple, it will read as NULL.
+		 */
+		tupdesc = RelationGetDescr(trigdata->tg_relation);
 
-	rec_new->erh = make_expanded_record_from_tupdesc(tupdesc,
-													 estate.datum_context);
-	rec_old->erh = make_expanded_record_from_exprecord(rec_new->erh,
-													   estate.datum_context);
+		rec_new = (PLpgSQL_rec *) (estate.datums[func->new_varno]);
+		rec_old = (PLpgSQL_rec *) (estate.datums[func->old_varno]);
 
+		rec_new->erh = make_expanded_record_from_tupdesc(tupdesc,
+														 estate.datum_context);
+		rec_old->erh = make_expanded_record_from_exprecord(rec_new->erh,
+														   estate.datum_context);
+	}
 	if (!TRIGGER_FIRED_FOR_ROW(trigdata->tg_event))
 	{
 		/*
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 5e76b3a..ae47a85 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -3043,6 +3043,36 @@ drop table self_ref;
 drop function dump_insert();
 drop function dump_update();
 drop function dump_delete();
+-- on connect trigger
+create table connects(id serial, who text);
+create function on_login_proc() returns trigger as $$
+begin
+  insert into connects (who) values (current_user);
+  raise notice 'You are welcome!';
+  return null;
+end;
+$$ language plpgsql;
+create trigger on_login_trigger after connection on regression execute procedure on_login_proc();
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id |   who    
+----+----------
+  1 | knizhnik
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id |   who    
+----+----------
+  1 | knizhnik
+  2 | knizhnik
+(2 rows)
+
+drop trigger on_login_trigger on regression;
+drop function on_login_proc();
+drop table connects;
 -- Leave around some objects for other tests
 create table trigger_parted (a int primary key) partition by list (a);
 create function trigger_parted_trigfunc() returns trigger language plpgsql as
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index e228d0a..551e592 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -2286,6 +2286,24 @@ drop function dump_insert();
 drop function dump_update();
 drop function dump_delete();
 
+-- on connect trigger
+create table connects(id serial, who text);
+create function on_login_proc() returns trigger as $$
+begin
+  insert into connects (who) values (current_user);
+  raise notice 'You are welcome!';
+  return null;
+end;
+$$ language plpgsql;
+create trigger on_login_trigger after connection on regression execute procedure on_login_proc();
+\c
+select * from connects;
+\c
+select * from connects;
+drop trigger on_login_trigger on regression;
+drop function on_login_proc();
+drop table connects;
+
 -- Leave around some objects for other tests
 create table trigger_parted (a int primary key) partition by list (a);
 create function trigger_parted_trigfunc() returns trigger language plpgsql as
#2Pavel Stehule
pavel.stehule@gmail.com
In reply to: Konstantin Knizhnik (#1)
Re: On login trigger: take three

Hi

čt 3. 9. 2020 v 15:43 odesílatel Konstantin Knizhnik <
k.knizhnik@postgrespro.ru> napsal:

Hi hackers,

Recently I have asked once again by one of our customers about login
trigger in postgres. People are migrating to Postgres from Oracle and
looking for Postgres analog of this Oracle feature.
This topic is not new:

/messages/by-id/1570308356720-0.post@n3.nabble.com

/messages/by-id/OSAPR01MB507373499CCCEA00EAE79875FE2D0@OSAPR01MB5073.jpnprd01.prod.outlook.com

end even session connect/disconnect hooks were sometimes committed (but
then reverted).
As far as I understand most of the concerns were related with disconnect
hook.
Performing some action on session disconnect is actually much more
complicated than on login.
But customers are not needed it, unlike actions performed at session start.

I wonder if we are really going to make some steps in this directions?
The discussion above was finished with "We haven't rejected the concept
altogether, AFAICT"

I have tried to resurrect this patch and implement on-connect trigger on
top of it.
The syntax is almost the same as proposed by Takayuki:

CREATE EVENT TRIGGER mytrigger
AFTER CONNECTION ON mydatabase
EXECUTE {PROCEDURE | FUNCTION} myproc();

I have replaced CONNECT with CONNECTION because last keyword is already
recognized by Postgres and
make ON clause mandatory to avoid shift-reduce conflicts.

Actually specifying database name is redundant, because we can define
on-connect trigger only for self database (just because triggers and
functions are local to the database).
It may be considered as argument against handling session start using
trigger. But it seems to be the most natural mechanism for users.

On connect trigger can be dropped almost in the same way as normal (on
relation) trigger, but with specifying name of the database instead of
relation name:

DROP TRIGGER mytrigger ON mydatabase;

It is possible to define arbitrary number of on-connect triggers with
different names.

I attached my prototype implementation of this feature.
I just to be sure first that this feature will be interested to community.
If so, I will continue work in it and prepare new version of the patch
for the commitfest.

I have a customer that requires this feature too. Now it uses a solution
based on dll session autoloading. Native solution can be great.

+1

Pavel

Example

Show quoted text

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#3tsunakawa.takay@fujitsu.com
tsunakawa.takay@fujitsu.com
In reply to: Konstantin Knizhnik (#1)
RE: On login trigger: take three

From: Konstantin Knizhnik <k.knizhnik@postgrespro.ru>

Recently I have asked once again by one of our customers about login trigger in
postgres. People are migrating to Postgres from Oracle and looking for Postgres
analog of this Oracle feature.
This topic is not new:

I attached my prototype implementation of this feature.
I just to be sure first that this feature will be interested to community.
If so, I will continue work in it and prepare new version of the patch for the
commitfest.

Thanks a lot for taking on this! +10

It may be considered as argument against handling session start using trigger.
But it seems to be the most natural mechanism for users.

Yeah, it's natural, just like the Unix shells run some shell scripts in the home directory.

Regards
Takayuki Tsunakawa

#4Konstantin Knizhnik
k.knizhnik@postgrespro.ru
In reply to: tsunakawa.takay@fujitsu.com (#3)
1 attachment(s)
Re: On login trigger: take three

Sorry, attached version of the patch is missing changes in one file.
Here is it correct patch.

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

Attachments:

on_login_trigger-2.patchtext/x-patch; name=on_login_trigger-2.patchDownload
diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml
index 289dd1d..b3c1fc2 100644
--- a/doc/src/sgml/ref/create_trigger.sgml
+++ b/doc/src/sgml/ref/create_trigger.sgml
@@ -27,7 +27,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE [ CONSTRAINT ] TRIGGER <replaceable class="parameter">name</replaceable> { BEFORE | AFTER | INSTEAD OF } { <replaceable class="parameter">event</replaceable> [ OR ... ] }
-    ON <replaceable class="parameter">table_name</replaceable>
+    ON { <replaceable class="parameter">table_name</replaceable> | <replaceable class="parameter">database_name</replaceable> }
     [ FROM <replaceable class="parameter">referenced_table_name</replaceable> ]
     [ NOT DEFERRABLE | [ DEFERRABLE ] [ INITIALLY IMMEDIATE | INITIALLY DEFERRED ] ]
     [ REFERENCING { { OLD | NEW } TABLE [ AS ] <replaceable class="parameter">transition_relation_name</replaceable> } [ ... ] ]
@@ -41,6 +41,7 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="parameter">name</replaceable>
     UPDATE [ OF <replaceable class="parameter">column_name</replaceable> [, ... ] ]
     DELETE
     TRUNCATE
+    CONNECTION
 </synopsis>
  </refsynopsisdiv>
 
diff --git a/doc/src/sgml/trigger.sgml b/doc/src/sgml/trigger.sgml
index 4a0e746..22caf4c 100644
--- a/doc/src/sgml/trigger.sgml
+++ b/doc/src/sgml/trigger.sgml
@@ -71,6 +71,23 @@
    </para>
 
    <para>
+     You can also define on connection trigger:
+     <programlisting>
+       CREATE EVENT TRIGGER mytrigger
+       AFTER CONNECTION ON mydatabase
+       EXECUTE {PROCEDURE | FUNCTION} myproc();
+     </programlisting>
+     On connection triggers can only be defined for self database.
+     It can be only <literal>AFTER</literal> trigger,
+     and may not contain <literal>WHERE</literal> clause or list of columns.
+     Any number of triggers with unique names can be defined for the database.
+     On connection triggers can be dropped by specifying name of the database:
+     <programlisting>
+       DROP TRIGGER mytrigger ON mydatabase;
+     </programlisting>
+   </para>
+
+   <para>
     The trigger function must be defined before the trigger itself can be
     created.  The trigger function must be declared as a
     function taking no arguments and returning type <literal>trigger</literal>.
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 6dfe1be..da57951 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -1433,11 +1433,23 @@ get_object_address_relobject(ObjectType objtype, List *object,
 
 	/* Extract relation name and open relation. */
 	relname = list_truncate(list_copy(object), nnames - 1);
-	relation = table_openrv_extended(makeRangeVarFromNameList(relname),
-									 AccessShareLock,
-									 missing_ok);
 
-	reloid = relation ? RelationGetRelid(relation) : InvalidOid;
+	address.objectId = InvalidOid;
+	if (objtype == OBJECT_TRIGGER && list_length(relname) == 1)
+	{
+		reloid = get_database_oid(strVal(linitial(relname)), true);
+		if (OidIsValid(reloid))
+			address.objectId = get_trigger_oid(reloid, depname, true);
+	}
+
+	if (!OidIsValid(address.objectId))
+	{
+		relation = table_openrv_extended(makeRangeVarFromNameList(relname),
+										 AccessShareLock,
+										 missing_ok);
+
+		reloid = relation ? RelationGetRelid(relation) : InvalidOid;
+	}
 
 	switch (objtype)
 	{
@@ -1449,8 +1461,11 @@ get_object_address_relobject(ObjectType objtype, List *object,
 			break;
 		case OBJECT_TRIGGER:
 			address.classId = TriggerRelationId;
-			address.objectId = relation ?
-				get_trigger_oid(reloid, depname, missing_ok) : InvalidOid;
+			if (!OidIsValid(address.objectId))
+			{
+				address.objectId = relation ?
+					get_trigger_oid(reloid, depname, missing_ok) : InvalidOid;
+			}
 			address.objectSubId = 0;
 			break;
 		case OBJECT_TABCONSTRAINT:
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index 81f0380..f4f4825 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -104,10 +104,13 @@ RemoveObjects(DropStmt *stmt)
 
 		/* Check permissions. */
 		namespaceId = get_object_namespace(&address);
-		if (!OidIsValid(namespaceId) ||
-			!pg_namespace_ownercheck(namespaceId, GetUserId()))
+		if ((relation != NULL || stmt->removeType != OBJECT_TRIGGER) &&
+			(!OidIsValid(namespaceId) ||
+			 !pg_namespace_ownercheck(namespaceId, GetUserId())))
+		{
 			check_object_ownership(GetUserId(), stmt->removeType, address,
 								   object, relation);
+		}
 
 		/*
 		 * Make note if a temporary namespace has been accessed in this
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 672fccf..25bc132 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -167,7 +167,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 	char	   *qual;
 	Datum		values[Natts_pg_trigger];
 	bool		nulls[Natts_pg_trigger];
-	Relation	rel;
+	Relation	rel = NULL;
 	AclResult	aclresult;
 	Relation	tgrel;
 	SysScanDesc tgscan;
@@ -179,6 +179,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 	char		internaltrigname[NAMEDATALEN];
 	char	   *trigname;
 	Oid			constrrelid = InvalidOid;
+	Oid         targetid = InvalidOid;
 	ObjectAddress myself,
 				referenced;
 	char	   *oldtablename = NULL;
@@ -188,119 +189,141 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 	if (OidIsValid(relOid))
 		rel = table_open(relOid, ShareRowExclusiveLock);
 	else
-		rel = table_openrv(stmt->relation, ShareRowExclusiveLock);
-
-	/*
-	 * Triggers must be on tables or views, and there are additional
-	 * relation-type-specific restrictions.
-	 */
-	if (rel->rd_rel->relkind == RELKIND_RELATION)
 	{
-		/* Tables can't have INSTEAD OF triggers */
-		if (stmt->timing != TRIGGER_TYPE_BEFORE &&
-			stmt->timing != TRIGGER_TYPE_AFTER)
-			ereport(ERROR,
-					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-					 errmsg("\"%s\" is a table",
-							RelationGetRelationName(rel)),
-					 errdetail("Tables cannot have INSTEAD OF triggers.")));
+		if (TRIGGER_FOR_CONNECT(stmt->events))
+		{
+			if (stmt->row)
+				elog(ERROR, "ON CONNECTION trigger can not have FOR EACH ROW clause");
+			if (stmt->transitionRels != NIL)
+				elog(ERROR, "ON CONNECTION trigger can not have FROM clause");
+			if (stmt->whenClause)
+				elog(ERROR, "ON CONNECTION trigger can not have WHEN clause");
+			if (stmt->timing != TRIGGER_TYPE_AFTER)
+				elog(ERROR, "ON CONNECTION trigger can be only AFTER");
+			targetid = get_database_oid(stmt->relation->relname, false);
+			if (targetid != MyDatabaseId)
+				elog(ERROR, "ON CONNECTION trigger can be created only for self database");
+		}
+		else
+			rel = table_openrv(stmt->relation, ShareRowExclusiveLock);
 	}
-	else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
-	{
-		/* Partitioned tables can't have INSTEAD OF triggers */
-		if (stmt->timing != TRIGGER_TYPE_BEFORE &&
-			stmt->timing != TRIGGER_TYPE_AFTER)
-			ereport(ERROR,
-					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-					 errmsg("\"%s\" is a table",
-							RelationGetRelationName(rel)),
-					 errdetail("Tables cannot have INSTEAD OF triggers.")));
 
+	if (rel != NULL)
+	{
+		if (TRIGGER_FOR_CONNECT(stmt->events))
+			elog(ERROR, "ON CONNECTION trigger should be declared for database, not for relations");
+		targetid = RelationGetRelid(rel);
 		/*
-		 * FOR EACH ROW triggers have further restrictions
+		 * Triggers must be on tables or views, and there are additional
+		 * relation-type-specific restrictions.
 		 */
-		if (stmt->row)
+		if (rel->rd_rel->relkind == RELKIND_RELATION)
 		{
+			/* Tables can't have INSTEAD OF triggers */
+			if (stmt->timing != TRIGGER_TYPE_BEFORE &&
+				stmt->timing != TRIGGER_TYPE_AFTER)
+				ereport(ERROR,
+						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+						 errmsg("\"%s\" is a table",
+								RelationGetRelationName(rel)),
+						 errdetail("Tables cannot have INSTEAD OF triggers.")));
+		}
+		else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+		{
+			/* Partitioned tables can't have INSTEAD OF triggers */
+			if (stmt->timing != TRIGGER_TYPE_BEFORE &&
+				stmt->timing != TRIGGER_TYPE_AFTER)
+				ereport(ERROR,
+						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+						 errmsg("\"%s\" is a table",
+								RelationGetRelationName(rel)),
+						 errdetail("Tables cannot have INSTEAD OF triggers.")));
+
 			/*
-			 * Disallow use of transition tables.
-			 *
-			 * Note that we have another restriction about transition tables
-			 * in partitions; search for 'has_superclass' below for an
-			 * explanation.  The check here is just to protect from the fact
-			 * that if we allowed it here, the creation would succeed for a
-			 * partitioned table with no partitions, but would be blocked by
-			 * the other restriction when the first partition was created,
-			 * which is very unfriendly behavior.
+			 * FOR EACH ROW triggers have further restrictions
 			 */
-			if (stmt->transitionRels != NIL)
+			if (stmt->row)
+			{
+				/*
+				 * Disallow use of transition tables.
+				 *
+				 * Note that we have another restriction about transition tables
+				 * in partitions; search for 'has_superclass' below for an
+				 * explanation.  The check here is just to protect from the fact
+				 * that if we allowed it here, the creation would succeed for a
+				 * partitioned table with no partitions, but would be blocked by
+				 * the other restriction when the first partition was created,
+				 * which is very unfriendly behavior.
+				 */
+				if (stmt->transitionRels != NIL)
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("\"%s\" is a partitioned table",
+									RelationGetRelationName(rel)),
+							 errdetail("Triggers on partitioned tables cannot have transition tables.")));
+			}
+		}
+		else if (rel->rd_rel->relkind == RELKIND_VIEW)
+		{
+			/*
+			 * Views can have INSTEAD OF triggers (which we check below are
+			 * row-level), or statement-level BEFORE/AFTER triggers.
+			 */
+			if (stmt->timing != TRIGGER_TYPE_INSTEAD && stmt->row)
 				ereport(ERROR,
-						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-						 errmsg("\"%s\" is a partitioned table",
+						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+						 errmsg("\"%s\" is a view",
 								RelationGetRelationName(rel)),
-						 errdetail("Triggers on partitioned tables cannot have transition tables.")));
+						 errdetail("Views cannot have row-level BEFORE or AFTER triggers.")));
+			/* Disallow TRUNCATE triggers on VIEWs */
+			if (TRIGGER_FOR_TRUNCATE(stmt->events))
+				ereport(ERROR,
+						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+						 errmsg("\"%s\" is a view",
+								RelationGetRelationName(rel)),
+						 errdetail("Views cannot have TRUNCATE triggers.")));
 		}
-	}
-	else if (rel->rd_rel->relkind == RELKIND_VIEW)
-	{
-		/*
-		 * Views can have INSTEAD OF triggers (which we check below are
-		 * row-level), or statement-level BEFORE/AFTER triggers.
-		 */
-		if (stmt->timing != TRIGGER_TYPE_INSTEAD && stmt->row)
-			ereport(ERROR,
-					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-					 errmsg("\"%s\" is a view",
-							RelationGetRelationName(rel)),
-					 errdetail("Views cannot have row-level BEFORE or AFTER triggers.")));
-		/* Disallow TRUNCATE triggers on VIEWs */
-		if (TRIGGER_FOR_TRUNCATE(stmt->events))
-			ereport(ERROR,
-					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-					 errmsg("\"%s\" is a view",
-							RelationGetRelationName(rel)),
-					 errdetail("Views cannot have TRUNCATE triggers.")));
-	}
-	else if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
-	{
-		if (stmt->timing != TRIGGER_TYPE_BEFORE &&
-			stmt->timing != TRIGGER_TYPE_AFTER)
-			ereport(ERROR,
-					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-					 errmsg("\"%s\" is a foreign table",
-							RelationGetRelationName(rel)),
-					 errdetail("Foreign tables cannot have INSTEAD OF triggers.")));
+		else if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+		{
+			if (stmt->timing != TRIGGER_TYPE_BEFORE &&
+				stmt->timing != TRIGGER_TYPE_AFTER)
+				ereport(ERROR,
+						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+						 errmsg("\"%s\" is a foreign table",
+								RelationGetRelationName(rel)),
+						 errdetail("Foreign tables cannot have INSTEAD OF triggers.")));
 
-		if (TRIGGER_FOR_TRUNCATE(stmt->events))
+			if (TRIGGER_FOR_TRUNCATE(stmt->events))
+				ereport(ERROR,
+						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+						 errmsg("\"%s\" is a foreign table",
+								RelationGetRelationName(rel)),
+						 errdetail("Foreign tables cannot have TRUNCATE triggers.")));
+
+			/*
+			 * We disallow constraint triggers to protect the assumption that
+			 * triggers on FKs can't be deferred.  See notes with AfterTriggers
+			 * data structures, below.
+			 */
+			if (stmt->isconstraint)
+				ereport(ERROR,
+						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+						 errmsg("\"%s\" is a foreign table",
+								RelationGetRelationName(rel)),
+						 errdetail("Foreign tables cannot have constraint triggers.")));
+		}
+		else
 			ereport(ERROR,
 					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-					 errmsg("\"%s\" is a foreign table",
-							RelationGetRelationName(rel)),
-					 errdetail("Foreign tables cannot have TRUNCATE triggers.")));
+					 errmsg("\"%s\" is not a table or view",
+							RelationGetRelationName(rel))));
 
-		/*
-		 * We disallow constraint triggers to protect the assumption that
-		 * triggers on FKs can't be deferred.  See notes with AfterTriggers
-		 * data structures, below.
-		 */
-		if (stmt->isconstraint)
+		if (!allowSystemTableMods && IsSystemRelation(rel))
 			ereport(ERROR,
-					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-					 errmsg("\"%s\" is a foreign table",
-							RelationGetRelationName(rel)),
-					 errdetail("Foreign tables cannot have constraint triggers.")));
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					 errmsg("permission denied: \"%s\" is a system catalog",
+							RelationGetRelationName(rel))));
 	}
-	else
-		ereport(ERROR,
-				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-				 errmsg("\"%s\" is not a table or view",
-						RelationGetRelationName(rel))));
-
-	if (!allowSystemTableMods && IsSystemRelation(rel))
-		ereport(ERROR,
-				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-				 errmsg("permission denied: \"%s\" is a system catalog",
-						RelationGetRelationName(rel))));
-
 	if (stmt->isconstraint)
 	{
 		/*
@@ -321,9 +344,9 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 	}
 
 	/* permission checks */
-	if (!isInternal)
+	if (!isInternal && rel != NULL)
 	{
-		aclresult = pg_class_aclcheck(RelationGetRelid(rel), GetUserId(),
+		aclresult = pg_class_aclcheck(targetid, GetUserId(),
 									  ACL_TRIGGER);
 		if (aclresult != ACLCHECK_OK)
 			aclcheck_error(aclresult, get_relkind_objtype(rel->rd_rel->relkind),
@@ -348,7 +371,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 	partition_recurse = !isInternal && stmt->row &&
 		rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE;
 	if (partition_recurse)
-		list_free(find_all_inheritors(RelationGetRelid(rel),
+		list_free(find_all_inheritors(targetid,
 									  ShareRowExclusiveLock, NULL));
 
 	/* Compute tgtype */
@@ -696,13 +719,13 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 		/* Internal callers should have made their own constraints */
 		Assert(!isInternal);
 		constraintOid = CreateConstraintEntry(stmt->trigname,
-											  RelationGetNamespace(rel),
+											  rel != NULL ? RelationGetNamespace(rel) : InvalidOid,
 											  CONSTRAINT_TRIGGER,
 											  stmt->deferrable,
 											  stmt->initdeferred,
 											  true,
 											  InvalidOid,	/* no parent */
-											  RelationGetRelid(rel),
+											  targetid,
 											  NULL, /* no conkey */
 											  0,
 											  0,
@@ -767,7 +790,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 		ScanKeyInit(&key,
 					Anum_pg_trigger_tgrelid,
 					BTEqualStrategyNumber, F_OIDEQ,
-					ObjectIdGetDatum(RelationGetRelid(rel)));
+					ObjectIdGetDatum(targetid));
 		tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
 									NULL, 1, &key);
 		while (HeapTupleIsValid(tuple = systable_getnext(tgscan)))
@@ -775,10 +798,22 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 			Form_pg_trigger pg_trigger = (Form_pg_trigger) GETSTRUCT(tuple);
 
 			if (namestrcmp(&(pg_trigger->tgname), trigname) == 0)
-				ereport(ERROR,
-						(errcode(ERRCODE_DUPLICATE_OBJECT),
-						 errmsg("trigger \"%s\" for relation \"%s\" already exists",
-								trigname, RelationGetRelationName(rel))));
+			{
+				if (rel != NULL)
+				{
+					ereport(ERROR,
+							(errcode(ERRCODE_DUPLICATE_OBJECT),
+							 errmsg("trigger \"%s\" for relation \"%s\" already exists",
+									trigname, RelationGetRelationName(rel))));
+				}
+				else
+				{
+					ereport(ERROR,
+							(errcode(ERRCODE_DUPLICATE_OBJECT),
+							 errmsg("trigger \"%s\" for database \"%s\" already exists",
+									trigname, stmt->relation->relname)));
+				}
+			}
 		}
 		systable_endscan(tgscan);
 	}
@@ -794,7 +829,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 	memset(nulls, false, sizeof(nulls));
 
 	values[Anum_pg_trigger_oid - 1] = ObjectIdGetDatum(trigoid);
-	values[Anum_pg_trigger_tgrelid - 1] = ObjectIdGetDatum(RelationGetRelid(rel));
+	values[Anum_pg_trigger_tgrelid - 1] = ObjectIdGetDatum(targetid);
 	values[Anum_pg_trigger_tgparentid - 1] = ObjectIdGetDatum(parentTriggerOid);
 	values[Anum_pg_trigger_tgname - 1] = DirectFunctionCall1(namein,
 															 CStringGetDatum(trigname));
@@ -927,30 +962,31 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 	if (newtablename)
 		pfree(DatumGetPointer(values[Anum_pg_trigger_tgnewtable - 1]));
 
-	/*
-	 * Update relation's pg_class entry; if necessary; and if not, send an SI
-	 * message to make other backends (and this one) rebuild relcache entries.
-	 */
-	pgrel = table_open(RelationRelationId, RowExclusiveLock);
-	tuple = SearchSysCacheCopy1(RELOID,
-								ObjectIdGetDatum(RelationGetRelid(rel)));
-	if (!HeapTupleIsValid(tuple))
-		elog(ERROR, "cache lookup failed for relation %u",
-			 RelationGetRelid(rel));
-	if (!((Form_pg_class) GETSTRUCT(tuple))->relhastriggers)
+	if (rel != NULL)
 	{
-		((Form_pg_class) GETSTRUCT(tuple))->relhastriggers = true;
-
-		CatalogTupleUpdate(pgrel, &tuple->t_self, tuple);
+		/*
+		 * Update relation's pg_class entry; if necessary; and if not, send an SI
+		 * message to make other backends (and this one) rebuild relcache entries.
+		 */
+		pgrel = table_open(RelationRelationId, RowExclusiveLock);
+		tuple = SearchSysCacheCopy1(RELOID,
+									ObjectIdGetDatum(targetid));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for relation %u", targetid);
+		if (!((Form_pg_class) GETSTRUCT(tuple))->relhastriggers)
+		{
+			((Form_pg_class) GETSTRUCT(tuple))->relhastriggers = true;
 
-		CommandCounterIncrement();
-	}
-	else
-		CacheInvalidateRelcacheByTuple(tuple);
+			CatalogTupleUpdate(pgrel, &tuple->t_self, tuple);
 
-	heap_freetuple(tuple);
-	table_close(pgrel, RowExclusiveLock);
+			CommandCounterIncrement();
+		}
+		else
+			CacheInvalidateRelcacheByTuple(tuple);
 
+		heap_freetuple(tuple);
+		table_close(pgrel, RowExclusiveLock);
+	}
 	/*
 	 * Record dependencies for trigger.  Always place a normal dependency on
 	 * the function.
@@ -977,7 +1013,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 		referenced.objectSubId = 0;
 		recordDependencyOn(&myself, &referenced, DEPENDENCY_INTERNAL);
 	}
-	else
+	else if (rel != NULL)
 	{
 		/*
 		 * User CREATE TRIGGER, so place dependencies.  We make trigger be
@@ -985,7 +1021,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 		 * dropped.  (Auto drop is compatible with our pre-7.3 behavior.)
 		 */
 		referenced.classId = RelationRelationId;
-		referenced.objectId = RelationGetRelid(rel);
+		referenced.objectId = targetid;
 		referenced.objectSubId = 0;
 		recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO);
 
@@ -1018,7 +1054,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 		{
 			ObjectAddressSet(referenced, TriggerRelationId, parentTriggerOid);
 			recordDependencyOn(&myself, &referenced, DEPENDENCY_PARTITION_PRI);
-			ObjectAddressSet(referenced, RelationRelationId, RelationGetRelid(rel));
+			ObjectAddressSet(referenced, RelationRelationId, targetid);
 			recordDependencyOn(&myself, &referenced, DEPENDENCY_PARTITION_SEC);
 		}
 	}
@@ -1029,7 +1065,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 		int			i;
 
 		referenced.classId = RelationRelationId;
-		referenced.objectId = RelationGetRelid(rel);
+		referenced.objectId = targetid;
 		for (i = 0; i < ncolumns; i++)
 		{
 			referenced.objectSubId = columns[i];
@@ -1148,7 +1184,8 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 	}
 
 	/* Keep lock on target rel until end of xact */
-	table_close(rel, NoLock);
+	if (rel != NULL)
+		table_close(rel, NoLock);
 
 	return myself;
 }
@@ -1165,7 +1202,8 @@ RemoveTriggerById(Oid trigOid)
 	ScanKeyData skey[1];
 	HeapTuple	tup;
 	Oid			relid;
-	Relation	rel;
+	Relation	rel = NULL;
+	Form_pg_trigger pg_trigger;
 
 	tgrel = table_open(TriggerRelationId, RowExclusiveLock);
 
@@ -1184,28 +1222,32 @@ RemoveTriggerById(Oid trigOid)
 	if (!HeapTupleIsValid(tup))
 		elog(ERROR, "could not find tuple for trigger %u", trigOid);
 
-	/*
-	 * Open and exclusive-lock the relation the trigger belongs to.
-	 */
-	relid = ((Form_pg_trigger) GETSTRUCT(tup))->tgrelid;
+	pg_trigger = (Form_pg_trigger) GETSTRUCT(tup);
 
-	rel = table_open(relid, AccessExclusiveLock);
+	if (!(pg_trigger->tgtype & TRIGGER_TYPE_CONNECT))
+	{
+		/*
+		 * Open and exclusive-lock the relation the trigger belongs to.
+		 */
+		relid = ((Form_pg_trigger) GETSTRUCT(tup))->tgrelid;
 
-	if (rel->rd_rel->relkind != RELKIND_RELATION &&
-		rel->rd_rel->relkind != RELKIND_VIEW &&
-		rel->rd_rel->relkind != RELKIND_FOREIGN_TABLE &&
-		rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
-		ereport(ERROR,
-				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-				 errmsg("\"%s\" is not a table, view, or foreign table",
-						RelationGetRelationName(rel))));
+		rel = table_open(relid, AccessExclusiveLock);
 
-	if (!allowSystemTableMods && IsSystemRelation(rel))
-		ereport(ERROR,
-				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-				 errmsg("permission denied: \"%s\" is a system catalog",
-						RelationGetRelationName(rel))));
+		if (rel->rd_rel->relkind != RELKIND_RELATION &&
+			rel->rd_rel->relkind != RELKIND_VIEW &&
+			rel->rd_rel->relkind != RELKIND_FOREIGN_TABLE &&
+			rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("\"%s\" is not a table, view, or foreign table",
+							RelationGetRelationName(rel))));
 
+		if (!allowSystemTableMods && IsSystemRelation(rel))
+			ereport(ERROR,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					 errmsg("permission denied: \"%s\" is a system catalog",
+							RelationGetRelationName(rel))));
+	}
 	/*
 	 * Delete the pg_trigger tuple.
 	 */
@@ -1214,19 +1256,22 @@ RemoveTriggerById(Oid trigOid)
 	systable_endscan(tgscan);
 	table_close(tgrel, RowExclusiveLock);
 
-	/*
-	 * We do not bother to try to determine whether any other triggers remain,
-	 * which would be needed in order to decide whether it's safe to clear the
-	 * relation's relhastriggers.  (In any case, there might be a concurrent
-	 * process adding new triggers.)  Instead, just force a relcache inval to
-	 * make other backends (and this one too!) rebuild their relcache entries.
-	 * There's no great harm in leaving relhastriggers true even if there are
-	 * no triggers left.
-	 */
-	CacheInvalidateRelcache(rel);
+	if (rel != NULL)
+	{
+		/*
+		 * We do not bother to try to determine whether any other triggers remain,
+		 * which would be needed in order to decide whether it's safe to clear the
+		 * relation's relhastriggers.  (In any case, there might be a concurrent
+		 * process adding new triggers.)  Instead, just force a relcache inval to
+		 * make other backends (and this one too!) rebuild their relcache entries.
+		 * There's no great harm in leaving relhastriggers true even if there are
+		 * no triggers left.
+		 */
+		CacheInvalidateRelcache(rel);
 
-	/* Keep lock on trigger's rel until end of xact */
-	table_close(rel, NoLock);
+		/* Keep lock on trigger's rel until end of xact */
+		table_close(rel, NoLock);
+	}
 }
 
 /*
@@ -5812,3 +5857,102 @@ pg_trigger_depth(PG_FUNCTION_ARGS)
 {
 	PG_RETURN_INT32(MyTriggerDepth);
 }
+
+void
+standard_session_start_hook(void)
+{
+	TriggerData LocTriggerData;
+	SysScanDesc tgscan;
+	ScanKeyData skey;
+	Relation tgrel;
+	Trigger trigger;
+	FmgrInfo finfo;
+	HeapTuple htup;
+
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+
+	tgrel = table_open(TriggerRelationId, AccessShareLock);
+
+	ScanKeyInit(&skey,
+				Anum_pg_trigger_tgrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(MyDatabaseId));
+
+	memset(&LocTriggerData, 0, sizeof(LocTriggerData));
+	LocTriggerData.type = T_TriggerData;
+	LocTriggerData.tg_event = TRIGGER_EVENT_AFTER;
+	LocTriggerData.tg_trigger = &trigger;
+
+	tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
+							  NULL, 1, &skey);
+	while (HeapTupleIsValid(htup = systable_getnext(tgscan)))
+	{
+		Form_pg_trigger pg_trigger = (Form_pg_trigger) GETSTRUCT(htup);
+		Assert(pg_trigger->tgtype == (TRIGGER_TYPE_AFTER|TRIGGER_TYPE_CONNECT));
+		trigger.tgoid = pg_trigger->oid;			/* OID of trigger (pg_trigger row) */
+		/* Remaining fields are copied from pg_trigger, see pg_trigger.h */
+		trigger.tgname = pg_trigger->tgname.data;
+		trigger.tgfoid = pg_trigger->tgfoid;
+		trigger.tgtype = pg_trigger->tgtype;
+		trigger.tgenabled = pg_trigger->tgenabled;
+		trigger.tgisinternal = pg_trigger->tgisinternal;
+		trigger.tgconstrrelid = pg_trigger->tgconstrrelid;
+		trigger.tgconstrindid = pg_trigger->tgconstrindid;
+		trigger.tgconstraint = pg_trigger->tgconstraint;
+		trigger.tgdeferrable = pg_trigger->tgdeferrable;
+		trigger.tginitdeferred = pg_trigger->tginitdeferred;
+		trigger.tgnargs = pg_trigger->tgnargs;
+		trigger.tgnattr = pg_trigger->tgattr.dim1;
+		if (trigger.tgnattr > 0)
+		{
+			trigger.tgattr = (int16 *) palloc(trigger.tgnattr * sizeof(int16));
+			memcpy(trigger.tgattr, &(pg_trigger->tgattr.values),
+				   trigger.tgnattr * sizeof(int16));
+		}
+		else
+			trigger.tgattr = NULL;
+
+		if (trigger.tgnargs > 0)
+		{
+			bytea	   *val;
+			char	   *p;
+			bool        isnull;
+
+			val = DatumGetByteaPP(fastgetattr(htup,
+											  Anum_pg_trigger_tgargs,
+											  tgrel->rd_att, &isnull));
+			if (isnull)
+				elog(ERROR, "tgargs is null in trigger \"%s\"", trigger.tgname);
+			p = (char *) VARDATA_ANY(val);
+			trigger.tgargs = (char **) palloc(trigger.tgnargs * sizeof(char *));
+			for (int i = 0; i < trigger.tgnargs; i++)
+			{
+				trigger.tgargs[i] = pstrdup(p);
+				p += strlen(p) + 1;
+			}
+		}
+		else
+			trigger.tgargs = NULL;
+
+		trigger.tgqual = NULL;
+		trigger.tgoldtable = NULL;
+		trigger.tgnewtable = NULL;
+
+		memset(&finfo, 0, sizeof(finfo));
+
+		/*
+		 * Call the trigger and throw away any possibly returned updated tuple.
+		 * (Don't let ExecCallTriggerFunc measure EXPLAIN time.)
+		 */
+		ExecCallTriggerFunc(&LocTriggerData,
+							0,
+							&finfo,
+							NULL,
+							CurrentMemoryContext);
+	}
+	systable_endscan(tgscan);
+	table_close(tgrel, AccessShareLock);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index dbb47d4..bf7a639 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -5301,6 +5301,8 @@ TriggerOneEvent:
 				{ $$ = list_make2(makeInteger(TRIGGER_TYPE_UPDATE), $3); }
 			| TRUNCATE
 				{ $$ = list_make2(makeInteger(TRIGGER_TYPE_TRUNCATE), NIL); }
+			| CONNECTION
+				{ $$ = list_make2(makeInteger(TRIGGER_TYPE_CONNECT), NIL); }
 		;
 
 TriggerReferencing:
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index c9424f1..af38640 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -101,8 +101,6 @@ int			max_stack_depth = 100;
 /* wait N seconds to allow attach from a debugger */
 int			PostAuthDelay = 0;
 
-
-
 /* ----------------
  *		private variables
  * ----------------
@@ -167,6 +165,9 @@ static ProcSignalReason RecoveryConflictReason;
 static MemoryContext row_description_context = NULL;
 static StringInfoData row_description_buf;
 
+/* Hook for plugins to get control at start of session */
+session_start_hook_type session_start_hook = standard_session_start_hook;
+
 /* ----------------------------------------------------------------
  *		decls for routines only used in this file
  * ----------------------------------------------------------------
@@ -4017,6 +4018,11 @@ PostgresMain(int argc, char *argv[],
 	if (!IsUnderPostmaster)
 		PgStartTime = GetCurrentTimestamp();
 
+	if (session_start_hook)
+	{
+		(*session_start_hook) ();
+	}
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
@@ -4779,3 +4785,4 @@ disable_statement_timeout(void)
 	if (get_timeout_active(STATEMENT_TIMEOUT))
 		disable_timeout(STATEMENT_TIMEOUT, false);
 }
+
diff --git a/src/include/catalog/pg_trigger.h b/src/include/catalog/pg_trigger.h
index fa5761b..2dce3a7 100644
--- a/src/include/catalog/pg_trigger.h
+++ b/src/include/catalog/pg_trigger.h
@@ -82,6 +82,7 @@ typedef FormData_pg_trigger *Form_pg_trigger;
 #define TRIGGER_TYPE_UPDATE				(1 << 4)
 #define TRIGGER_TYPE_TRUNCATE			(1 << 5)
 #define TRIGGER_TYPE_INSTEAD			(1 << 6)
+#define TRIGGER_TYPE_CONNECT			(1 << 7)
 
 #define TRIGGER_TYPE_LEVEL_MASK			(TRIGGER_TYPE_ROW)
 #define TRIGGER_TYPE_STATEMENT			0
@@ -115,6 +116,7 @@ typedef FormData_pg_trigger *Form_pg_trigger;
 #define TRIGGER_FOR_DELETE(type)		((type) & TRIGGER_TYPE_DELETE)
 #define TRIGGER_FOR_UPDATE(type)		((type) & TRIGGER_TYPE_UPDATE)
 #define TRIGGER_FOR_TRUNCATE(type)		((type) & TRIGGER_TYPE_TRUNCATE)
+#define TRIGGER_FOR_CONNECT(type)		((type) & TRIGGER_TYPE_CONNECT)
 
 /*
  * Efficient macro for checking if tgtype matches a particular level
diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h
index bd30607..c76fdff 100644
--- a/src/include/tcop/tcopprot.h
+++ b/src/include/tcop/tcopprot.h
@@ -30,6 +30,12 @@ extern PGDLLIMPORT const char *debug_query_string;
 extern int	max_stack_depth;
 extern int	PostAuthDelay;
 
+/* Hook for plugins to get control at start and end of session */
+typedef void (*session_start_hook_type) (void);
+
+extern PGDLLIMPORT session_start_hook_type session_start_hook;
+extern void standard_session_start_hook(void);
+
 /* GUC-configurable parameters */
 
 typedef enum
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index d4a3d58..6e97028 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -904,9 +904,9 @@ plpgsql_exec_trigger(PLpgSQL_function *func,
 	PLpgSQL_execstate estate;
 	ErrorContextCallback plerrcontext;
 	int			rc;
-	TupleDesc	tupdesc;
-	PLpgSQL_rec *rec_new,
-			   *rec_old;
+	TupleDesc	tupdesc = NULL;
+	PLpgSQL_rec *rec_new = NULL,
+		*rec_old = NULL;
 	HeapTuple	rettup;
 
 	/*
@@ -929,26 +929,28 @@ plpgsql_exec_trigger(PLpgSQL_function *func,
 	estate.err_text = gettext_noop("during initialization of execution state");
 	copy_plpgsql_datums(&estate, func);
 
-	/*
-	 * Put the OLD and NEW tuples into record variables
-	 *
-	 * We set up expanded records for both variables even though only one may
-	 * have a value.  This allows record references to succeed in functions
-	 * that are used for multiple trigger types.  For example, we might have a
-	 * test like "if (TG_OP = 'INSERT' and NEW.foo = 'xyz')", which should
-	 * work regardless of the current trigger type.  If a value is actually
-	 * fetched from an unsupplied tuple, it will read as NULL.
-	 */
-	tupdesc = RelationGetDescr(trigdata->tg_relation);
-
-	rec_new = (PLpgSQL_rec *) (estate.datums[func->new_varno]);
-	rec_old = (PLpgSQL_rec *) (estate.datums[func->old_varno]);
+	if (trigdata->tg_relation)
+	{
+		/*
+		 * Put the OLD and NEW tuples into record variables
+		 *
+		 * We set up expanded records for both variables even though only one may
+		 * have a value.  This allows record references to succeed in functions
+		 * that are used for multiple trigger types.  For example, we might have a
+		 * test like "if (TG_OP = 'INSERT' and NEW.foo = 'xyz')", which should
+		 * work regardless of the current trigger type.  If a value is actually
+		 * fetched from an unsupplied tuple, it will read as NULL.
+		 */
+		tupdesc = RelationGetDescr(trigdata->tg_relation);
 
-	rec_new->erh = make_expanded_record_from_tupdesc(tupdesc,
-													 estate.datum_context);
-	rec_old->erh = make_expanded_record_from_exprecord(rec_new->erh,
-													   estate.datum_context);
+		rec_new = (PLpgSQL_rec *) (estate.datums[func->new_varno]);
+		rec_old = (PLpgSQL_rec *) (estate.datums[func->old_varno]);
 
+		rec_new->erh = make_expanded_record_from_tupdesc(tupdesc,
+														 estate.datum_context);
+		rec_old->erh = make_expanded_record_from_exprecord(rec_new->erh,
+														   estate.datum_context);
+	}
 	if (!TRIGGER_FIRED_FOR_ROW(trigdata->tg_event))
 	{
 		/*
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 5e76b3a..ae47a85 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -3043,6 +3043,36 @@ drop table self_ref;
 drop function dump_insert();
 drop function dump_update();
 drop function dump_delete();
+-- on connect trigger
+create table connects(id serial, who text);
+create function on_login_proc() returns trigger as $$
+begin
+  insert into connects (who) values (current_user);
+  raise notice 'You are welcome!';
+  return null;
+end;
+$$ language plpgsql;
+create trigger on_login_trigger after connection on regression execute procedure on_login_proc();
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id |   who    
+----+----------
+  1 | knizhnik
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id |   who    
+----+----------
+  1 | knizhnik
+  2 | knizhnik
+(2 rows)
+
+drop trigger on_login_trigger on regression;
+drop function on_login_proc();
+drop table connects;
 -- Leave around some objects for other tests
 create table trigger_parted (a int primary key) partition by list (a);
 create function trigger_parted_trigfunc() returns trigger language plpgsql as
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index e228d0a..551e592 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -2286,6 +2286,24 @@ drop function dump_insert();
 drop function dump_update();
 drop function dump_delete();
 
+-- on connect trigger
+create table connects(id serial, who text);
+create function on_login_proc() returns trigger as $$
+begin
+  insert into connects (who) values (current_user);
+  raise notice 'You are welcome!';
+  return null;
+end;
+$$ language plpgsql;
+create trigger on_login_trigger after connection on regression execute procedure on_login_proc();
+\c
+select * from connects;
+\c
+select * from connects;
+drop trigger on_login_trigger on regression;
+drop function on_login_proc();
+drop table connects;
+
 -- Leave around some objects for other tests
 create table trigger_parted (a int primary key) partition by list (a);
 create function trigger_parted_trigfunc() returns trigger language plpgsql as
#5Konstantin Knizhnik
k.knizhnik@postgrespro.ru
In reply to: Pavel Stehule (#2)
1 attachment(s)
Re: On login trigger: take three

On 03.09.2020 17:18, Pavel Stehule wrote:

Hi

čt 3. 9. 2020 v 15:43 odesílatel Konstantin Knizhnik
<k.knizhnik@postgrespro.ru <mailto:k.knizhnik@postgrespro.ru>> napsal:

Hi hackers,

Recently I have asked once again by one of our customers about login
trigger in postgres. People are migrating to Postgres from Oracle and
looking for Postgres analog of this Oracle feature.
This topic is not new:

/messages/by-id/1570308356720-0.post@n3.nabble.com
/messages/by-id/OSAPR01MB507373499CCCEA00EAE79875FE2D0@OSAPR01MB5073.jpnprd01.prod.outlook.com

end even session connect/disconnect hooks were sometimes committed
(but
then reverted).
As far as I understand most of the concerns were related with
disconnect
hook.
Performing some action on session disconnect is actually much more
complicated than on login.
But customers are not needed it, unlike actions performed at
session start.

I wonder if we are really going to make some steps in this directions?
The discussion above was finished with "We haven't rejected the
concept
altogether, AFAICT"

I have tried to resurrect this patch and implement on-connect
trigger on
top of it.
The syntax is almost the same as proposed by Takayuki:

CREATE EVENT TRIGGER mytrigger
AFTER CONNECTION ON mydatabase
EXECUTE {PROCEDURE | FUNCTION} myproc();

I have replaced CONNECT with CONNECTION because last keyword is
already
recognized by Postgres and
make ON clause mandatory to avoid shift-reduce conflicts.

Actually specifying database name is redundant, because we can define
on-connect trigger only for self database (just because triggers and
functions are local to the database).
It may be considered as argument against handling session start using
trigger. But it seems to be the most natural mechanism for users.

On connect trigger can be dropped almost in the same way as normal
(on
relation) trigger, but with specifying name of the database
instead of
relation name:

DROP TRIGGER mytrigger ON mydatabase;

It is possible to define arbitrary number of on-connect triggers with
different names.

I attached my prototype implementation of this feature.
I just to be sure first that this feature will be interested to
community.
If so, I will continue work in it and prepare new version of the
patch
for the commitfest.

I have a customer that requires this feature too. Now it uses a
solution based on dll session autoloading.  Native solution can be great.

+1

I realized that on connect trigger should be implemented as EVENT TRIGGER.
So I have reimplemented my patch using event trigger and use
session_start even name to make it more consistent with other events.
Now on login triggers can be created in this way:

create table connects(id serial, who text);
create function on_login_proc() returns event_trigger as $$
begin
  insert into connects (who) values (current_user());
  raise notice 'You are welcome!';
end;
$$ language plpgsql;
create event trigger on_login_trigger on session_start execute procedure
on_login_proc();
alter event trigger on_login_trigger enable always;

Attachments:

on_connect_event_trigger-6.patchtext/x-patch; name=on_connect_event_trigger-6.patchDownload
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 60366a9..13a23b0 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>session_start</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -36,6 +37,10 @@
    </para>
 
    <para>
+     The <literal>session_start</literal> event occurs on backend start when connection with user was established.
+   </para>
+
+   <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
      <literal>SECURITY LABEL</literal>,
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 7844880..a341fee 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -48,6 +48,8 @@
 #include "utils/rel.h"
 #include "utils/syscache.h"
 
+bool disable_session_start_trigger;
+
 typedef struct EventTriggerQueryState
 {
 	/* memory context for this state's objects */
@@ -130,6 +132,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "session_start") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -562,6 +565,9 @@ EventTriggerCommonSetup(Node *parsetree,
 	ListCell   *lc;
 	List	   *runlist = NIL;
 
+	/* Get the command tag. */
+	tag = parsetree ? CreateCommandTag(parsetree) : CMDTAG_CONNECT;
+
 	/*
 	 * We want the list of command tags for which this procedure is actually
 	 * invoked to match up exactly with the list that CREATE EVENT TRIGGER
@@ -577,22 +583,18 @@ EventTriggerCommonSetup(Node *parsetree,
 	 * relevant command tag.
 	 */
 #ifdef USE_ASSERT_CHECKING
+	if (event == EVT_DDLCommandStart ||
+		event == EVT_DDLCommandEnd ||
+		event == EVT_SQLDrop ||
+		event == EVT_Connect)
 	{
-		CommandTag	dbgtag;
-
-		dbgtag = CreateCommandTag(parsetree);
-		if (event == EVT_DDLCommandStart ||
-			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
-		{
-			if (!command_tag_event_trigger_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
-		else if (event == EVT_TableRewrite)
-		{
-			if (!command_tag_table_rewrite_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
+		if (!command_tag_event_trigger_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
+	}
+	else if (event == EVT_TableRewrite)
+	{
+		if (!command_tag_table_rewrite_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
 	}
 #endif
 
@@ -601,9 +603,6 @@ EventTriggerCommonSetup(Node *parsetree,
 	if (cachelist == NIL)
 		return NIL;
 
-	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
-
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
 	 * memory context.  Once we start running the command triggers, or indeed
@@ -800,6 +799,66 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Fire connect triggers.
+ */
+void
+EventTriggerOnConnect(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId) || disable_session_start_trigger)
+		return;
+
+	StartTransactionCommand();
+
+    runlist = EventTriggerCommonSetup(NULL,
+									  EVT_Connect, "connect",
+									  &trigdata);
+
+	if (runlist != NIL)
+	{
+		MemoryContext old_context = CurrentMemoryContext;
+		bool is_superuser = superuser();
+		/*
+		 * Make sure anything the main command did will be visible to the event
+		 * triggers.
+		 */
+		CommandCounterIncrement();
+
+		/* Run the triggers. */
+		PG_TRY();
+		{
+			EventTriggerInvoke(runlist, &trigdata);
+			list_free(runlist);
+		}
+		PG_CATCH();
+		{
+			ErrorData* error;
+			/*
+			 * Try to ignore error for superuser to make it possible to login even in case of errors
+			 * during trigger execution
+			 */
+			if (!is_superuser)
+				PG_RE_THROW();
+
+			MemoryContextSwitchTo(old_context);
+			error = CopyErrorData();
+			FlushErrorState();
+			elog(NOTICE, "start_session trigger failed with message %s", error->message);
+			AbortCurrentTransaction();
+			return;
+		}
+		PG_END_TRY();
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index c9424f1..008e574 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -42,6 +42,7 @@
 #include "catalog/pg_type.h"
 #include "commands/async.h"
 #include "commands/prepare.h"
+#include "commands/event_trigger.h"
 #include "executor/spi.h"
 #include "jit/jit.h"
 #include "libpq/libpq.h"
@@ -167,6 +168,9 @@ static ProcSignalReason RecoveryConflictReason;
 static MemoryContext row_description_context = NULL;
 static StringInfoData row_description_buf;
 
+/* Hook for plugins to get control at start of session */
+session_start_hook_type session_start_hook = EventTriggerOnConnect;
+
 /* ----------------------------------------------------------------
  *		decls for routines only used in this file
  * ----------------------------------------------------------------
@@ -4017,6 +4021,11 @@ PostgresMain(int argc, char *argv[],
 	if (!IsUnderPostmaster)
 		PgStartTime = GetCurrentTimestamp();
 
+	if (session_start_hook)
+	{
+		(*session_start_hook) ();
+	}
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 73d091d..6eaac13 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -169,6 +169,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "session_start") == 0)
+			event = EVT_Connect;
 		else
 			continue;
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 73518d9..09e3e82 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -43,6 +43,7 @@
 #include "commands/async.h"
 #include "commands/prepare.h"
 #include "commands/trigger.h"
+#include "commands/event_trigger.h"
 #include "commands/user.h"
 #include "commands/vacuum.h"
 #include "commands/variable.h"
@@ -928,6 +929,16 @@ static const unit_conversion time_unit_conversion_table[] =
 static struct config_bool ConfigureNamesBool[] =
 {
 	{
+		{"disable_session_start_trigger", PGC_SUSET, DEVELOPER_OPTIONS,
+			gettext_noop("Disable on session_start event trigger."),
+			gettext_noop("In case of errors in ON session_start EVENT TRIGGER procedure this GUC can be used to disable trigger activation and provide access to the database."),
+			GUC_EXPLAIN
+		},
+		&disable_session_start_trigger,
+		false,
+		NULL, NULL, NULL
+	},
+	{
 		{"enable_seqscan", PGC_USERSET, QUERY_TUNING_METHOD,
 			gettext_noop("Enables the planner's use of sequential-scan plans."),
 			NULL,
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 407fd6a..61c096d 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -21,6 +21,8 @@
 #include "tcop/deparse_utility.h"
 #include "utils/aclchk_internal.h"
 
+extern bool disable_session_start_trigger; /* GUC */
+
 typedef struct EventTriggerData
 {
 	NodeTag		type;
@@ -53,6 +55,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnConnect(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 8ef0f55..cba70b3 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -80,6 +80,7 @@ PG_CMDTAG(CMDTAG_CLUSTER, "CLUSTER", false, false, false)
 PG_CMDTAG(CMDTAG_COMMENT, "COMMENT", true, false, false)
 PG_CMDTAG(CMDTAG_COMMIT, "COMMIT", false, false, false)
 PG_CMDTAG(CMDTAG_COMMIT_PREPARED, "COMMIT PREPARED", false, false, false)
+PG_CMDTAG(CMDTAG_CONNECT, "CONNECT", true, false, false)
 PG_CMDTAG(CMDTAG_COPY, "COPY", false, false, true)
 PG_CMDTAG(CMDTAG_COPY_FROM, "COPY FROM", false, false, false)
 PG_CMDTAG(CMDTAG_CREATE_ACCESS_METHOD, "CREATE ACCESS METHOD", true, false, false)
diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h
index bd30607..be71020 100644
--- a/src/include/tcop/tcopprot.h
+++ b/src/include/tcop/tcopprot.h
@@ -30,6 +30,11 @@ extern PGDLLIMPORT const char *debug_query_string;
 extern int	max_stack_depth;
 extern int	PostAuthDelay;
 
+/* Hook for plugins to get control at start and end of session */
+typedef void (*session_start_hook_type) (void);
+
+extern PGDLLIMPORT session_start_hook_type session_start_hook;
+
 /* GUC-configurable parameters */
 
 typedef enum
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index bb1e39e..ab7e8c3 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Connect,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/000_session_start_trigger.pl b/src/test/recovery/t/000_session_start_trigger.pl
new file mode 100644
index 0000000..fa82e0a
--- /dev/null
+++ b/src/test/recovery/t/000_session_start_trigger.pl
@@ -0,0 +1,69 @@
+use strict;
+use warnings;
+use PostgresNode;
+use TestLib;
+use Test::More;
+if (!$use_unix_sockets)
+{
+	plan skip_all =>
+	  "authentication tests cannot run without Unix-domain sockets";
+}
+else
+{
+	plan tests => 5;
+}
+
+# Initialize master node
+my $node = get_new_node('master');
+$node->init;
+$node->start;
+$node->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+CREATE ROLE regress_hacker LOGIN PASSWORD 'pass';
+
+CREATE TABLE connects(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO connects (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON session_start EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+}
+);
+my $res;
+
+$res = $node->safe_psql('postgres', "SELECT 1");
+
+$res = $node->safe_psql('postgres', "SELECT 1",
+  extra_params => [ '-U', 'regress_user', '-w' ]);
+
+my ($ret, $stdout, $stderr) = $node->psql('postgres', "SELECT 1",
+  extra_params => [ '-U', 'regress_hacker', '-w' ]);
+ok( $ret != 0 && $stderr =~ /You are not welcome!/ );
+
+$res = $node->safe_psql('postgres', "SELECT COUNT(1) FROM connects WHERE who = 'regress_user'");
+ok($res == 1);
+
+my $tempdir = TestLib::tempdir;
+command_ok(
+  [ "pg_dumpall", '-p', $node->port, '-c', "--file=$tempdir/regression_dump.sql", ],
+  "dumpall");
+# my $dump_contents = slurp_file("$tempdir/regression_dump.sql");
+# print($dump_contents);
+
+my $node1 = get_new_node('secondary');
+$node1->init;
+$node1->start;
+command_ok(["psql", '-p', $node1->port, '-b', '-f', "$tempdir/regression_dump.sql" ] );
+$res = $node1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$res = $node1->safe_psql('postgres', "SELECT COUNT(1) FROM connects WHERE who = 'regress_user'");
+ok($res == 2);
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 9e31a53..5e9dc5f 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -43,6 +43,27 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+
+CREATE TABLE connects(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO connects (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON session_start EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 $node_primary->wait_for_catchup($node_standby_1, 'replay',
 	$node_primary->lsn('insert'));
@@ -266,6 +287,9 @@ sub replay_check
 
 replay_check();
 
+$node_standby_1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$node_standby_2->safe_psql('postgres', "SELECT 2", extra_params => [ '-U', 'regress_user', '-w' ]);
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index bdd0ffc..fc18e6f 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -536,3 +536,33 @@ NOTICE:  DROP POLICY - ddl_command_end
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- On session start triggers
+create table connects(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into connects (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on session_start execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id | who  
+----+------
+  1 | I am
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id | who  
+----+------
+  1 | I am
+  2 | I am
+(2 rows)
+
+drop event trigger on_login_trigger;
+drop function on_login_proc();
+drop table connects;
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 18b2a26..8c2ad19 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -429,3 +429,22 @@ DROP POLICY p2 ON event_trigger_test;
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- On session start triggers
+create table connects(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into connects (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on session_start execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+select * from connects;
+\c
+select * from connects;
+drop event trigger on_login_trigger;
+drop function on_login_proc();
+drop table connects;
+
#6Pavel Stehule
pavel.stehule@gmail.com
In reply to: Konstantin Knizhnik (#5)
Re: On login trigger: take three

Hi

I am checking last patch, and there are notices

1. disable_session_start_trigger should be SU_BACKEND instead SUSET

2. The documentation should be enhanced - there is not any note about
behave when there are unhandled exceptions, about motivation for this event
trigger

3. regress tests should be enhanced - the cases with exceptions are not
tested

4. This trigger is not executed again after RESET ALL or DISCARD ALL - it
can be a problem if somebody wants to use this trigger for initialisation
of some session objects with some pooling solutions.

5. The handling errors don't work well for canceling. If event trigger
waits for some event, then cancel disallow connect although connected user
is superuser

CREATE OR REPLACE FUNCTION on_login_proc2() RETURNS EVENT_TRIGGER AS $$
begin perform pg_sleep(10000); raise notice '%', fx1(100);raise notice
'kuku kuku'; end $$ language plpgsql;

probably nobody will use pg_sleep in this routine, but there can be wait on
some locks ...

Regards

Pavel

#7Konstantin Knizhnik
k.knizhnik@postgrespro.ru
In reply to: Pavel Stehule (#6)
1 attachment(s)
Re: On login trigger: take three

On 14.09.2020 12:44, Pavel Stehule wrote:

Hi

I am checking last patch, and there are notices

1. disable_session_start_trigger should be SU_BACKEND instead SUSET

2. The documentation should be enhanced - there is not any note about
behave when there are unhandled exceptions, about motivation for this
event trigger

3. regress tests should be enhanced - the cases with exceptions are
not tested

4. This trigger is not executed again after RESET ALL or DISCARD ALL -
it can be a problem if somebody wants to use this trigger for
initialisation of some session objects with some pooling solutions.

5. The handling errors don't work well for canceling. If event trigger
waits for some event, then cancel disallow connect although connected
user is superuser

CREATE OR REPLACE FUNCTION on_login_proc2() RETURNS EVENT_TRIGGER AS
$$ begin perform pg_sleep(10000); raise notice '%', fx1(100);raise
notice 'kuku kuku'; end  $$ language plpgsql;

probably nobody will use pg_sleep in this routine, but there can be
wait on some locks ...

Regards

Pavel

Hi
Thank you very much for looking at my patch for connection triggers.
I have fixed 1-3 issues in the attached patch.
Concerning 4 and 5 I have some doubts:

4. Should I add some extra GUC to allow firing of session_start trigger
in case of  RESET ALL or DISCARD ALL ?
Looks like such behavior contradicts with event name "session_start"...
And do we really need it? If some pooler is using RESET ALL/DISCARD ALL
to emulate session semantic then  most likely it provides way to define
custom actions which
should be perform for session initialization. As far as I know, for
example pgbouncer allows do define own on-connect hooks.

5. I do not quite understand your concern. If I define  trigger
procedure which is  blocked (for example as in your example), then I can
use pg_cancel_backend to interrupt execution of login trigger and
superuser can login. What should be changed here?

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

Attachments:

on_connect_event_trigger-7.patchtext/x-patch; name=on_connect_event_trigger-7.patchDownload
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 60366a9..f7c3c14 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,12 +28,31 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>session_start</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
      and <literal>sql_drop</literal>.
      Support for additional events may be added in future releases.
    </para>
+<literal>table_rewrite</literal>
+   <para>
+     The <literal>session_start</literal> event occurs on backend start when connection with user was established.
+     As far as bug in trigger procedure can prevent normal login to the system, there are two mechanisms
+     for preventing it:
+     <itemizedlist>
+      <listitem>
+       <para>
+         GUC <literal>disable_session_start_trigger</literal> which makes it possible to disable firing triggers on session startup.
+       </para>
+      <listitem>
+       <para>
+         Ignoring errors in trigger procedure for superuser. Error message is delivered to client as <literal>NOTICE</literal>
+         in this case
+       </para>
+      </listitem>
+     <itemizedlist>
+   </para>
 
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 7844880..a341fee 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -48,6 +48,8 @@
 #include "utils/rel.h"
 #include "utils/syscache.h"
 
+bool disable_session_start_trigger;
+
 typedef struct EventTriggerQueryState
 {
 	/* memory context for this state's objects */
@@ -130,6 +132,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "session_start") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -562,6 +565,9 @@ EventTriggerCommonSetup(Node *parsetree,
 	ListCell   *lc;
 	List	   *runlist = NIL;
 
+	/* Get the command tag. */
+	tag = parsetree ? CreateCommandTag(parsetree) : CMDTAG_CONNECT;
+
 	/*
 	 * We want the list of command tags for which this procedure is actually
 	 * invoked to match up exactly with the list that CREATE EVENT TRIGGER
@@ -577,22 +583,18 @@ EventTriggerCommonSetup(Node *parsetree,
 	 * relevant command tag.
 	 */
 #ifdef USE_ASSERT_CHECKING
+	if (event == EVT_DDLCommandStart ||
+		event == EVT_DDLCommandEnd ||
+		event == EVT_SQLDrop ||
+		event == EVT_Connect)
 	{
-		CommandTag	dbgtag;
-
-		dbgtag = CreateCommandTag(parsetree);
-		if (event == EVT_DDLCommandStart ||
-			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
-		{
-			if (!command_tag_event_trigger_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
-		else if (event == EVT_TableRewrite)
-		{
-			if (!command_tag_table_rewrite_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
+		if (!command_tag_event_trigger_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
+	}
+	else if (event == EVT_TableRewrite)
+	{
+		if (!command_tag_table_rewrite_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
 	}
 #endif
 
@@ -601,9 +603,6 @@ EventTriggerCommonSetup(Node *parsetree,
 	if (cachelist == NIL)
 		return NIL;
 
-	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
-
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
 	 * memory context.  Once we start running the command triggers, or indeed
@@ -800,6 +799,66 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Fire connect triggers.
+ */
+void
+EventTriggerOnConnect(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId) || disable_session_start_trigger)
+		return;
+
+	StartTransactionCommand();
+
+    runlist = EventTriggerCommonSetup(NULL,
+									  EVT_Connect, "connect",
+									  &trigdata);
+
+	if (runlist != NIL)
+	{
+		MemoryContext old_context = CurrentMemoryContext;
+		bool is_superuser = superuser();
+		/*
+		 * Make sure anything the main command did will be visible to the event
+		 * triggers.
+		 */
+		CommandCounterIncrement();
+
+		/* Run the triggers. */
+		PG_TRY();
+		{
+			EventTriggerInvoke(runlist, &trigdata);
+			list_free(runlist);
+		}
+		PG_CATCH();
+		{
+			ErrorData* error;
+			/*
+			 * Try to ignore error for superuser to make it possible to login even in case of errors
+			 * during trigger execution
+			 */
+			if (!is_superuser)
+				PG_RE_THROW();
+
+			MemoryContextSwitchTo(old_context);
+			error = CopyErrorData();
+			FlushErrorState();
+			elog(NOTICE, "start_session trigger failed with message %s", error->message);
+			AbortCurrentTransaction();
+			return;
+		}
+		PG_END_TRY();
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index c9424f1..008e574 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -42,6 +42,7 @@
 #include "catalog/pg_type.h"
 #include "commands/async.h"
 #include "commands/prepare.h"
+#include "commands/event_trigger.h"
 #include "executor/spi.h"
 #include "jit/jit.h"
 #include "libpq/libpq.h"
@@ -167,6 +168,9 @@ static ProcSignalReason RecoveryConflictReason;
 static MemoryContext row_description_context = NULL;
 static StringInfoData row_description_buf;
 
+/* Hook for plugins to get control at start of session */
+session_start_hook_type session_start_hook = EventTriggerOnConnect;
+
 /* ----------------------------------------------------------------
  *		decls for routines only used in this file
  * ----------------------------------------------------------------
@@ -4017,6 +4021,11 @@ PostgresMain(int argc, char *argv[],
 	if (!IsUnderPostmaster)
 		PgStartTime = GetCurrentTimestamp();
 
+	if (session_start_hook)
+	{
+		(*session_start_hook) ();
+	}
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 73d091d..6eaac13 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -169,6 +169,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "session_start") == 0)
+			event = EVT_Connect;
 		else
 			continue;
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 73518d9..5ced24f 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -43,6 +43,7 @@
 #include "commands/async.h"
 #include "commands/prepare.h"
 #include "commands/trigger.h"
+#include "commands/event_trigger.h"
 #include "commands/user.h"
 #include "commands/vacuum.h"
 #include "commands/variable.h"
@@ -928,6 +929,16 @@ static const unit_conversion time_unit_conversion_table[] =
 static struct config_bool ConfigureNamesBool[] =
 {
 	{
+		{"disable_session_start_trigger", PGC_SU_BACKEND, DEVELOPER_OPTIONS,
+			gettext_noop("Disable on session_start event trigger."),
+			gettext_noop("In case of errors in ON session_start EVENT TRIGGER procedure this GUC can be used to disable trigger activation and provide access to the database."),
+			GUC_EXPLAIN
+		},
+		&disable_session_start_trigger,
+		false,
+		NULL, NULL, NULL
+	},
+	{
 		{"enable_seqscan", PGC_USERSET, QUERY_TUNING_METHOD,
 			gettext_noop("Enables the planner's use of sequential-scan plans."),
 			NULL,
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 407fd6a..61c096d 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -21,6 +21,8 @@
 #include "tcop/deparse_utility.h"
 #include "utils/aclchk_internal.h"
 
+extern bool disable_session_start_trigger; /* GUC */
+
 typedef struct EventTriggerData
 {
 	NodeTag		type;
@@ -53,6 +55,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnConnect(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 8ef0f55..cba70b3 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -80,6 +80,7 @@ PG_CMDTAG(CMDTAG_CLUSTER, "CLUSTER", false, false, false)
 PG_CMDTAG(CMDTAG_COMMENT, "COMMENT", true, false, false)
 PG_CMDTAG(CMDTAG_COMMIT, "COMMIT", false, false, false)
 PG_CMDTAG(CMDTAG_COMMIT_PREPARED, "COMMIT PREPARED", false, false, false)
+PG_CMDTAG(CMDTAG_CONNECT, "CONNECT", true, false, false)
 PG_CMDTAG(CMDTAG_COPY, "COPY", false, false, true)
 PG_CMDTAG(CMDTAG_COPY_FROM, "COPY FROM", false, false, false)
 PG_CMDTAG(CMDTAG_CREATE_ACCESS_METHOD, "CREATE ACCESS METHOD", true, false, false)
diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h
index bd30607..be71020 100644
--- a/src/include/tcop/tcopprot.h
+++ b/src/include/tcop/tcopprot.h
@@ -30,6 +30,11 @@ extern PGDLLIMPORT const char *debug_query_string;
 extern int	max_stack_depth;
 extern int	PostAuthDelay;
 
+/* Hook for plugins to get control at start and end of session */
+typedef void (*session_start_hook_type) (void);
+
+extern PGDLLIMPORT session_start_hook_type session_start_hook;
+
 /* GUC-configurable parameters */
 
 typedef enum
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index bb1e39e..ab7e8c3 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Connect,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/000_session_start_trigger.pl b/src/test/recovery/t/000_session_start_trigger.pl
new file mode 100644
index 0000000..fa82e0a
--- /dev/null
+++ b/src/test/recovery/t/000_session_start_trigger.pl
@@ -0,0 +1,69 @@
+use strict;
+use warnings;
+use PostgresNode;
+use TestLib;
+use Test::More;
+if (!$use_unix_sockets)
+{
+	plan skip_all =>
+	  "authentication tests cannot run without Unix-domain sockets";
+}
+else
+{
+	plan tests => 5;
+}
+
+# Initialize master node
+my $node = get_new_node('master');
+$node->init;
+$node->start;
+$node->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+CREATE ROLE regress_hacker LOGIN PASSWORD 'pass';
+
+CREATE TABLE connects(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO connects (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON session_start EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+}
+);
+my $res;
+
+$res = $node->safe_psql('postgres', "SELECT 1");
+
+$res = $node->safe_psql('postgres', "SELECT 1",
+  extra_params => [ '-U', 'regress_user', '-w' ]);
+
+my ($ret, $stdout, $stderr) = $node->psql('postgres', "SELECT 1",
+  extra_params => [ '-U', 'regress_hacker', '-w' ]);
+ok( $ret != 0 && $stderr =~ /You are not welcome!/ );
+
+$res = $node->safe_psql('postgres', "SELECT COUNT(1) FROM connects WHERE who = 'regress_user'");
+ok($res == 1);
+
+my $tempdir = TestLib::tempdir;
+command_ok(
+  [ "pg_dumpall", '-p', $node->port, '-c', "--file=$tempdir/regression_dump.sql", ],
+  "dumpall");
+# my $dump_contents = slurp_file("$tempdir/regression_dump.sql");
+# print($dump_contents);
+
+my $node1 = get_new_node('secondary');
+$node1->init;
+$node1->start;
+command_ok(["psql", '-p', $node1->port, '-b', '-f', "$tempdir/regression_dump.sql" ] );
+$res = $node1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$res = $node1->safe_psql('postgres', "SELECT COUNT(1) FROM connects WHERE who = 'regress_user'");
+ok($res == 2);
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 9e31a53..5e9dc5f 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -43,6 +43,27 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+
+CREATE TABLE connects(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO connects (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON session_start EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 $node_primary->wait_for_catchup($node_standby_1, 'replay',
 	$node_primary->lsn('insert'));
@@ -266,6 +287,9 @@ sub replay_check
 
 replay_check();
 
+$node_standby_1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$node_standby_2->safe_psql('postgres', "SELECT 2", extra_params => [ '-U', 'regress_user', '-w' ]);
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index bdd0ffc..e0b5196 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -536,3 +536,40 @@ NOTICE:  DROP POLICY - ddl_command_end
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- On session start triggers
+create table connects(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into connects (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on session_start execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id | who  
+----+------
+  1 | I am
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id | who  
+----+------
+  1 | I am
+  2 | I am
+(2 rows)
+
+-- Test handing exeptions in session_start trigger
+drop table connects;
+-- superuser should ignore error
+\c
+NOTICE:  start_session trigger failed with message relation "connects" does not exist
+-- suppress troigger firing
+\c "dbname=regression options='-c disable_session_start_trigger=true'"
+-- Cleanup
+drop event trigger on_login_trigger;
+drop function on_login_proc();
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 18b2a26..82dacdd 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -429,3 +429,31 @@ DROP POLICY p2 ON event_trigger_test;
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- On session start triggers
+create table connects(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into connects (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on session_start execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+select * from connects;
+\c
+select * from connects;
+
+-- Test handing exeptions in session_start trigger
+
+drop table connects;
+-- superuser should ignore error
+\c
+-- suppress troigger firing
+\c "dbname=regression options='-c disable_session_start_trigger=true'"
+
+
+-- Cleanup
+drop event trigger on_login_trigger;
+drop function on_login_proc();
#8Pavel Stehule
pavel.stehule@gmail.com
In reply to: Konstantin Knizhnik (#7)
Re: On login trigger: take three

po 14. 9. 2020 v 16:12 odesílatel Konstantin Knizhnik <
k.knizhnik@postgrespro.ru> napsal:

On 14.09.2020 12:44, Pavel Stehule wrote:

Hi

I am checking last patch, and there are notices

1. disable_session_start_trigger should be SU_BACKEND instead SUSET

2. The documentation should be enhanced - there is not any note about
behave when there are unhandled exceptions, about motivation for this
event trigger

3. regress tests should be enhanced - the cases with exceptions are
not tested

4. This trigger is not executed again after RESET ALL or DISCARD ALL -
it can be a problem if somebody wants to use this trigger for
initialisation of some session objects with some pooling solutions.

5. The handling errors don't work well for canceling. If event trigger
waits for some event, then cancel disallow connect although connected
user is superuser

CREATE OR REPLACE FUNCTION on_login_proc2() RETURNS EVENT_TRIGGER AS
$$ begin perform pg_sleep(10000); raise notice '%', fx1(100);raise
notice 'kuku kuku'; end $$ language plpgsql;

probably nobody will use pg_sleep in this routine, but there can be
wait on some locks ...

Regards

Pavel

Hi
Thank you very much for looking at my patch for connection triggers.
I have fixed 1-3 issues in the attached patch.
Concerning 4 and 5 I have some doubts:

4. Should I add some extra GUC to allow firing of session_start trigger
in case of RESET ALL or DISCARD ALL ?
Looks like such behavior contradicts with event name "session_start"...
And do we really need it? If some pooler is using RESET ALL/DISCARD ALL
to emulate session semantic then most likely it provides way to define
custom actions which
should be perform for session initialization. As far as I know, for
example pgbouncer allows do define own on-connect hooks.

If we introduce buildin session trigger , we should to define what is the
session. Your design is much more related to the process than to session.
So the correct name should be "process_start" trigger, or some should be
different. I think there are two different events - process_start, and
session_start, and there should be two different event triggers. Maybe the
name "session_start" is just ambiguous and should be used with a different
name.

5. I do not quite understand your concern. If I define trigger
procedure which is blocked (for example as in your example), then I can
use pg_cancel_backend to interrupt execution of login trigger and
superuser can login. What should be changed here?

You cannot run pg_cancel_backend, because you cannot open a new session.
There is a cycle.

Regards

Pavel

Show quoted text

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#9Konstantin Knizhnik
k.knizhnik@postgrespro.ru
In reply to: Pavel Stehule (#8)
Re: On login trigger: take three

On 14.09.2020 17:34, Pavel Stehule wrote:

If we introduce buildin session trigger , we should to define what is
the session. Your design is much more related to the process than to
session. So the correct name should be "process_start" trigger, or
some should be different. I think there are two different events -
process_start, and session_start, and there should be two different
event triggers. Maybe the name "session_start" is just ambiguous and
should be used with a different name.

I agree.
I can rename trigger to backend_start or process_start or whatever else.

5. I do not quite understand your concern. If I define trigger
procedure which is  blocked (for example as in your example), then
I can
use pg_cancel_backend to interrupt execution of login trigger and
superuser can login. What should be changed here?

You cannot run pg_cancel_backend, because you cannot open a new
session. There is a cycle.

It is always possible to login by disabling startup triggers using
disable_session_start_trigger GUC:

psql "dbname=postgres options='-c disable_session_start_trigger=true'"

#10Pavel Stehule
pavel.stehule@gmail.com
In reply to: Konstantin Knizhnik (#9)
Re: On login trigger: take three

po 14. 9. 2020 v 17:53 odesílatel Konstantin Knizhnik <
k.knizhnik@postgrespro.ru> napsal:

On 14.09.2020 17:34, Pavel Stehule wrote:

If we introduce buildin session trigger , we should to define what is the
session. Your design is much more related to the process than to session.
So the correct name should be "process_start" trigger, or some should be
different. I think there are two different events - process_start, and
session_start, and there should be two different event triggers. Maybe the
name "session_start" is just ambiguous and should be used with a different
name.

I agree.
I can rename trigger to backend_start or process_start or whatever else.

Creating a good name can be hard - it is not called for any process - so
maybe "user_backend_start" ?

5. I do not quite understand your concern. If I define trigger
procedure which is blocked (for example as in your example), then I can
use pg_cancel_backend to interrupt execution of login trigger and
superuser can login. What should be changed here?

You cannot run pg_cancel_backend, because you cannot open a new session.
There is a cycle.

It is always possible to login by disabling startup triggers using
disable_session_start_trigger GUC:

psql "dbname=postgres options='-c disable_session_start_trigger=true'"

sure, I know. Just this behavior can be a very unpleasant surprise, and my
question is if it can be fixed. Creating custom libpq variables can be the
stop for people that use pgAdmin.

#11Greg Nancarrow
gregn4422@gmail.com
In reply to: Pavel Stehule (#10)
1 attachment(s)
Re: On login trigger: take three

On Tue, Sep 15, 2020 at 2:12 AM Pavel Stehule <pavel.stehule@gmail.com> wrote:

It is always possible to login by disabling startup triggers using disable_session_start_trigger GUC:

psql "dbname=postgres options='-c disable_session_start_trigger=true'"

sure, I know. Just this behavior can be a very unpleasant surprise, and my question is if it can be fixed. Creating custom libpq variables can be the stop for people that use pgAdmin.

Hi,

I thought in the case of using pgAdmin (assuming you can connect as
superuser to a database, say the default "postgres" maintenance
database, that doesn't have an EVENT TRIGGER defined for the
session_start event) you could issue the query "ALTER SYSTEM SET
disable_session_start_trigger TO true;" and then reload the
configuration?

Anyway, I am wondering if this patch is still being actively developed/improved?

Regarding the last-posted patch, I'd like to give some feedback. I
found that the documentation part wouldn't build because of errors in
the SGML tags. There are some grammatical errors too, and some minor
inconsistencies with the current documentation, and some descriptions
could be improved. I think that a colon separator should be added to
the NOTICE message for superuser, so it's clear exactly where the text
of the underlying error message starts. Also, I think that
"client_connection" is perhaps a better and more intuitive event name
than "session_start", or the suggested "user_backend_start".
I've therefore attached an updated patch with these suggested minor
improvements, please take a look and see what you think (please
compare with the original patch).

Regards,
Greg Nancarrow
Fujitsu Australia

Attachments:

on_connect_event_trigger_WITH_SUGGESTED_UPDATES.patchapplication/octet-stream; name=on_connect_event_trigger_WITH_SUGGESTED_UPDATES.patchDownload
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 60366a9..00f69b8 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>client_connection</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -36,6 +37,29 @@
    </para>
 
    <para>
+     The <literal>client_connection</literal> event occurs when a client connection
+     to the server is established.
+     There are two mechanisms for dealing with any bugs in a trigger procedure for
+     this event which might prevent successful login to the system:
+     <itemizedlist>
+      <listitem>
+       <para>
+         The configuration parameter <literal>disable_client_connection_trigger</literal>
+         makes it possible to disable firing the <literal>client_connection</literal>
+         trigger when a client connects.
+       </para>
+      </listitem>
+      <listitem>
+       <para>
+         Errors in the <literal>client_connection</literal> trigger procedure are
+         ignored for superuser. An error message is delivered to the client as
+         <literal>NOTICE</literal> in this case.
+       </para>
+      </listitem>
+     </itemizedlist>
+   </para>
+
+   <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
      <literal>SECURITY LABEL</literal>,
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 3ffba4e..c798791 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -48,6 +48,8 @@
 #include "utils/rel.h"
 #include "utils/syscache.h"
 
+bool disable_client_connection_trigger;
+
 typedef struct EventTriggerQueryState
 {
 	/* memory context for this state's objects */
@@ -130,6 +132,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "client_connection") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -562,6 +565,9 @@ EventTriggerCommonSetup(Node *parsetree,
 	ListCell   *lc;
 	List	   *runlist = NIL;
 
+	/* Get the command tag. */
+	tag = parsetree ? CreateCommandTag(parsetree) : CMDTAG_CONNECT;
+
 	/*
 	 * We want the list of command tags for which this procedure is actually
 	 * invoked to match up exactly with the list that CREATE EVENT TRIGGER
@@ -577,22 +583,18 @@ EventTriggerCommonSetup(Node *parsetree,
 	 * relevant command tag.
 	 */
 #ifdef USE_ASSERT_CHECKING
+	if (event == EVT_DDLCommandStart ||
+		event == EVT_DDLCommandEnd ||
+		event == EVT_SQLDrop ||
+		event == EVT_Connect)
 	{
-		CommandTag	dbgtag;
-
-		dbgtag = CreateCommandTag(parsetree);
-		if (event == EVT_DDLCommandStart ||
-			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
-		{
-			if (!command_tag_event_trigger_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
-		else if (event == EVT_TableRewrite)
-		{
-			if (!command_tag_table_rewrite_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
+		if (!command_tag_event_trigger_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
+	}
+	else if (event == EVT_TableRewrite)
+	{
+		if (!command_tag_table_rewrite_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
 	}
 #endif
 
@@ -601,9 +603,6 @@ EventTriggerCommonSetup(Node *parsetree,
 	if (cachelist == NIL)
 		return NIL;
 
-	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
-
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
 	 * memory context.  Once we start running the command triggers, or indeed
@@ -800,6 +799,66 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Fire connect triggers.
+ */
+void
+EventTriggerOnConnect(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId) || disable_client_connection_trigger)
+		return;
+
+	StartTransactionCommand();
+
+	runlist = EventTriggerCommonSetup(NULL,
+									  EVT_Connect, "connect",
+									  &trigdata);
+
+	if (runlist != NIL)
+	{
+		MemoryContext old_context = CurrentMemoryContext;
+		bool is_superuser = superuser();
+		/*
+		 * Make sure anything the main command did will be visible to the event
+		 * triggers.
+		 */
+		CommandCounterIncrement();
+
+		/* Run the triggers. */
+		PG_TRY();
+		{
+			EventTriggerInvoke(runlist, &trigdata);
+			list_free(runlist);
+		}
+		PG_CATCH();
+		{
+			ErrorData* error;
+			/*
+			 * Try to ignore error for superuser to make it possible to login even in case of errors
+			 * during trigger execution
+			 */
+			if (!is_superuser)
+				PG_RE_THROW();
+
+			MemoryContextSwitchTo(old_context);
+			error = CopyErrorData();
+			FlushErrorState();
+			elog(NOTICE, "client_connection trigger failed with message: %s", error->message);
+			AbortCurrentTransaction();
+			return;
+		}
+		PG_END_TRY();
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 3679799..ce9b98e 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -42,6 +42,7 @@
 #include "catalog/pg_type.h"
 #include "commands/async.h"
 #include "commands/prepare.h"
+#include "commands/event_trigger.h"
 #include "executor/spi.h"
 #include "jit/jit.h"
 #include "libpq/libpq.h"
@@ -167,6 +168,9 @@ static ProcSignalReason RecoveryConflictReason;
 static MemoryContext row_description_context = NULL;
 static StringInfoData row_description_buf;
 
+/* Hook for plugins to get control at start of session */
+client_connection_hook_type client_connection_hook = EventTriggerOnConnect;
+
 /* ----------------------------------------------------------------
  *		decls for routines only used in this file
  * ----------------------------------------------------------------
@@ -4012,6 +4016,11 @@ PostgresMain(int argc, char *argv[],
 	if (!IsUnderPostmaster)
 		PgStartTime = GetCurrentTimestamp();
 
+	if (client_connection_hook)
+	{
+		(*client_connection_hook) ();
+	}
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 0427795..c621b8f 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -168,6 +168,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "client_connection") == 0)
+			event = EVT_Connect;
 		else
 			continue;
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 02d2d26..6ba394b 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -43,6 +43,7 @@
 #include "commands/async.h"
 #include "commands/prepare.h"
 #include "commands/trigger.h"
+#include "commands/event_trigger.h"
 #include "commands/user.h"
 #include "commands/vacuum.h"
 #include "commands/variable.h"
@@ -928,6 +929,16 @@ static const unit_conversion time_unit_conversion_table[] =
 static struct config_bool ConfigureNamesBool[] =
 {
 	{
+		{"disable_client_connection_trigger", PGC_SU_BACKEND, DEVELOPER_OPTIONS,
+			gettext_noop("Disables the client_connection event trigger."),
+			gettext_noop("In case of errors in the ON client_connection EVENT TRIGGER procedure, this parameter can be used to disable trigger activation and provide access to the database."),
+			GUC_EXPLAIN
+		},
+		&disable_client_connection_trigger,
+		false,
+		NULL, NULL, NULL
+	},
+	{
 		{"enable_seqscan", PGC_USERSET, QUERY_TUNING_METHOD,
 			gettext_noop("Enables the planner's use of sequential-scan plans."),
 			NULL,
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 407fd6a..d5e86af 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -21,6 +21,8 @@
 #include "tcop/deparse_utility.h"
 #include "utils/aclchk_internal.h"
 
+extern bool disable_client_connection_trigger; /* GUC */
+
 typedef struct EventTriggerData
 {
 	NodeTag		type;
@@ -53,6 +55,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnConnect(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index be94852..988aa39 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -80,6 +80,7 @@ PG_CMDTAG(CMDTAG_CLUSTER, "CLUSTER", false, false, false)
 PG_CMDTAG(CMDTAG_COMMENT, "COMMENT", true, false, false)
 PG_CMDTAG(CMDTAG_COMMIT, "COMMIT", false, false, false)
 PG_CMDTAG(CMDTAG_COMMIT_PREPARED, "COMMIT PREPARED", false, false, false)
+PG_CMDTAG(CMDTAG_CONNECT, "CONNECT", true, false, false)
 PG_CMDTAG(CMDTAG_COPY, "COPY", false, false, true)
 PG_CMDTAG(CMDTAG_COPY_FROM, "COPY FROM", false, false, false)
 PG_CMDTAG(CMDTAG_CREATE_ACCESS_METHOD, "CREATE ACCESS METHOD", true, false, false)
diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h
index bd30607..90376b2 100644
--- a/src/include/tcop/tcopprot.h
+++ b/src/include/tcop/tcopprot.h
@@ -30,6 +30,11 @@ extern PGDLLIMPORT const char *debug_query_string;
 extern int	max_stack_depth;
 extern int	PostAuthDelay;
 
+/* Hook for plugins to get control at start and end of session */
+typedef void (*client_connection_hook_type) (void);
+
+extern PGDLLIMPORT client_connection_hook_type client_connection_hook;
+
 /* GUC-configurable parameters */
 
 typedef enum
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index bb1e39e..ab7e8c3 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Connect,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/000_client_connection_trigger.pl b/src/test/recovery/t/000_client_connection_trigger.pl
new file mode 100644
index 0000000..3dcd475
--- /dev/null
+++ b/src/test/recovery/t/000_client_connection_trigger.pl
@@ -0,0 +1,69 @@
+use strict;
+use warnings;
+use PostgresNode;
+use TestLib;
+use Test::More;
+if (!$use_unix_sockets)
+{
+	plan skip_all =>
+	  "authentication tests cannot run without Unix-domain sockets";
+}
+else
+{
+	plan tests => 5;
+}
+
+# Initialize master node
+my $node = get_new_node('master');
+$node->init;
+$node->start;
+$node->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+CREATE ROLE regress_hacker LOGIN PASSWORD 'pass';
+
+CREATE TABLE connects(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO connects (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON client_connection EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+}
+);
+my $res;
+
+$res = $node->safe_psql('postgres', "SELECT 1");
+
+$res = $node->safe_psql('postgres', "SELECT 1",
+  extra_params => [ '-U', 'regress_user', '-w' ]);
+
+my ($ret, $stdout, $stderr) = $node->psql('postgres', "SELECT 1",
+  extra_params => [ '-U', 'regress_hacker', '-w' ]);
+ok( $ret != 0 && $stderr =~ /You are not welcome!/ );
+
+$res = $node->safe_psql('postgres', "SELECT COUNT(1) FROM connects WHERE who = 'regress_user'");
+ok($res == 1);
+
+my $tempdir = TestLib::tempdir;
+command_ok(
+  [ "pg_dumpall", '-p', $node->port, '-c', "--file=$tempdir/regression_dump.sql", ],
+  "dumpall");
+# my $dump_contents = slurp_file("$tempdir/regression_dump.sql");
+# print($dump_contents);
+
+my $node1 = get_new_node('secondary');
+$node1->init;
+$node1->start;
+command_ok(["psql", '-p', $node1->port, '-b', '-f', "$tempdir/regression_dump.sql" ] );
+$res = $node1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$res = $node1->safe_psql('postgres', "SELECT COUNT(1) FROM connects WHERE who = 'regress_user'");
+ok($res == 2);
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 9e31a53..b4a21fb 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -43,6 +43,27 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+
+CREATE TABLE connects(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO connects (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON client_connection EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 $node_primary->wait_for_catchup($node_standby_1, 'replay',
 	$node_primary->lsn('insert'));
@@ -266,6 +287,9 @@ sub replay_check
 
 replay_check();
 
+$node_standby_1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$node_standby_2->safe_psql('postgres', "SELECT 2", extra_params => [ '-U', 'regress_user', '-w' ]);
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index bdd0ffc..3ecbd6a 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -536,3 +536,40 @@ NOTICE:  DROP POLICY - ddl_command_end
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- On session start triggers
+create table connects(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into connects (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on client_connection execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id | who  
+----+------
+  1 | I am
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id | who  
+----+------
+  1 | I am
+  2 | I am
+(2 rows)
+
+-- Test handing exeptions in client_connection trigger
+drop table connects;
+-- superuser should ignore error
+\c
+NOTICE:  client_connection trigger failed with message: relation "connects" does not exist
+-- suppress trigger firing
+\c "dbname=regression options='-c disable_client_connection_trigger=true'"
+-- Cleanup
+drop event trigger on_login_trigger;
+drop function on_login_proc();
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 18b2a26..a1c5cf2 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -429,3 +429,31 @@ DROP POLICY p2 ON event_trigger_test;
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- On session start triggers
+create table connects(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into connects (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on client_connection execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+select * from connects;
+\c
+select * from connects;
+
+-- Test handing exeptions in client_connection trigger
+
+drop table connects;
+-- superuser should ignore error
+\c
+-- suppress trigger firing
+\c "dbname=regression options='-c disable_client_connection_trigger=true'"
+
+
+-- Cleanup
+drop event trigger on_login_trigger;
+drop function on_login_proc();
#12Konstantin Knizhnik
k.knizhnik@postgrespro.ru
In reply to: Greg Nancarrow (#11)
Re: On login trigger: take three

On 04.12.2020 3:50, Greg Nancarrow wrote:

On Tue, Sep 15, 2020 at 2:12 AM Pavel Stehule <pavel.stehule@gmail.com> wrote:

It is always possible to login by disabling startup triggers using disable_session_start_trigger GUC:

psql "dbname=postgres options='-c disable_session_start_trigger=true'"

sure, I know. Just this behavior can be a very unpleasant surprise, and my question is if it can be fixed. Creating custom libpq variables can be the stop for people that use pgAdmin.

Hi,

I thought in the case of using pgAdmin (assuming you can connect as
superuser to a database, say the default "postgres" maintenance
database, that doesn't have an EVENT TRIGGER defined for the
session_start event) you could issue the query "ALTER SYSTEM SET
disable_session_start_trigger TO true;" and then reload the
configuration?

As far as I understand Pavel concern was about the case when superuser
defines wrong login trigger which prevents login to the system
all user including himself. Right now solution of this problem is to
include "options='-c disable_session_start_trigger=true'" in connection
string.
I do not know if it can be done with pgAdmin.

Anyway, I am wondering if this patch is still being actively developed/improved?

I do not know what else has to be improved.
If you, Pavel or anybody else have some suggestions: please let me know.

Regarding the last-posted patch, I'd like to give some feedback. I
found that the documentation part wouldn't build because of errors in
the SGML tags. There are some grammatical errors too, and some minor
inconsistencies with the current documentation, and some descriptions
could be improved. I think that a colon separator should be added to
the NOTICE message for superuser, so it's clear exactly where the text
of the underlying error message starts. Also, I think that
"client_connection" is perhaps a better and more intuitive event name
than "session_start", or the suggested "user_backend_start".
I've therefore attached an updated patch with these suggested minor
improvements, please take a look and see what you think (please
compare with the original patch).

Thank you very much for detecting the problems and much more thanks for
fixing them and providing your version of the patch.
I have nothing against renaming "session_start" to "client_connection".

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#13Greg Nancarrow
gregn4422@gmail.com
In reply to: Konstantin Knizhnik (#12)
Re: On login trigger: take three

On Fri, Dec 4, 2020 at 9:05 PM Konstantin Knizhnik
<k.knizhnik@postgrespro.ru> wrote:

As far as I understand Pavel concern was about the case when superuser
defines wrong login trigger which prevents login to the system
all user including himself. Right now solution of this problem is to
include "options='-c disable_session_start_trigger=true'" in connection
string.
I do not know if it can be done with pgAdmin.

As an event trigger is tied to a particular database, and a GUC is
global to the cluster, as long as there is one database in the cluster
for which an event trigger for the "client_connection" event is NOT
defined (say the default "postgres" maintenance database), then the
superuser can always connect to that database, issue "ALTER SYSTEM SET
disable_client_connection_trigger TO true" and reload the
configuration. I tested this with pgAdmin4 and it worked fine for me,
to allow login to a database for which login was previously prevented
due to a badly-defined logon trigger.

Pavel, is this an acceptable solution or do you still see problems
with this approach?

Regards,
Greg Nancarrow
Fujitsu Australia

#14Pavel Stehule
pavel.stehule@gmail.com
In reply to: Greg Nancarrow (#13)
Re: On login trigger: take three

út 8. 12. 2020 v 1:17 odesílatel Greg Nancarrow <gregn4422@gmail.com>
napsal:

On Fri, Dec 4, 2020 at 9:05 PM Konstantin Knizhnik
<k.knizhnik@postgrespro.ru> wrote:

As far as I understand Pavel concern was about the case when superuser
defines wrong login trigger which prevents login to the system
all user including himself. Right now solution of this problem is to
include "options='-c disable_session_start_trigger=true'" in connection
string.
I do not know if it can be done with pgAdmin.

As an event trigger is tied to a particular database, and a GUC is
global to the cluster, as long as there is one database in the cluster
for which an event trigger for the "client_connection" event is NOT
defined (say the default "postgres" maintenance database), then the
superuser can always connect to that database, issue "ALTER SYSTEM SET
disable_client_connection_trigger TO true" and reload the
configuration. I tested this with pgAdmin4 and it worked fine for me,
to allow login to a database for which login was previously prevented
due to a badly-defined logon trigger.

yes, it can work .. Maybe for this operation only database owner rights
should be necessary. The super user is maybe too strong.

There are two maybe generic questions?

1. Maybe we can introduce more generic GUC for all event triggers like
disable_event_triggers? This GUC can be checked only by the database owner
or super user. It can be an alternative ALTER TABLE DISABLE TRIGGER ALL. It
can be protection against necessity to restart to single mode to repair the
event trigger. I think so more generic solution is better than special
disable_client_connection_trigger GUC.

2. I have no objection against client_connection. It is probably better for
the mentioned purpose - possibility to block connection to database. Can be
interesting, and I am not sure how much work it is to introduce the second
event - session_start. This event should be started after connecting - so
the exception there doesn't block connect, and should be started also after
the new statement "DISCARD SESSION", that will be started automatically
after DISCARD ALL. This feature should not be implemented in first step,
but it can be a plan for support pooled connections

Regards

Pavel

Show quoted text

Pavel, is this an acceptable solution or do you still see problems
with this approach?

Regards,
Greg Nancarrow
Fujitsu Australia

#15Greg Nancarrow
gregn4422@gmail.com
In reply to: Pavel Stehule (#14)
2 attachment(s)
Re: On login trigger: take three

On Tue, Dec 8, 2020 at 3:26 PM Pavel Stehule <pavel.stehule@gmail.com> wrote:

There are two maybe generic questions?

1. Maybe we can introduce more generic GUC for all event triggers like disable_event_triggers? This GUC can be checked only by the database owner or super user. It can be an alternative ALTER TABLE DISABLE TRIGGER ALL. It can be protection against necessity to restart to single mode to repair the event trigger. I think so more generic solution is better than special disable_client_connection_trigger GUC.

2. I have no objection against client_connection. It is probably better for the mentioned purpose - possibility to block connection to database. Can be interesting, and I am not sure how much work it is to introduce the second event - session_start. This event should be started after connecting - so the exception there doesn't block connect, and should be started also after the new statement "DISCARD SESSION", that will be started automatically after DISCARD ALL. This feature should not be implemented in first step, but it can be a plan for support pooled connections

I've created a separate patch to address question (1), rather than
include it in the main patch, which I've adjusted accordingly. I'll
leave question (2) until another time, as you suggest.
See the attached patches.

Regards,
Greg Nancarrow
Fujitsu Australia

Attachments:

v1-0001-Add-new-config-parameter-disable_event_triggers.patchapplication/octet-stream; name=v1-0001-Add-new-config-parameter-disable_event_triggers.patchDownload
From 1e3e01f0e10cb88041b5e94351ddee122321195b Mon Sep 17 00:00:00 2001
From: Greg Nancarrow <gregn4422@gmail.com>
Date: Wed, 9 Dec 2020 20:57:45 +1100
Subject: [PATCH v1 1/2] Add new configuration parameter
 "disable_event_triggers".

If an erroneous event trigger disables the database, this new GUC may be used as
an alternative to using single-user mode, to disable all event triggers for
those with superuser privileges or for those who own the database to which an
event trigger belongs.

Discussion: https://www.postgresql.org/message-id/flat/0d46d29f-4558-3af9-9c85-7774e14a7709%40postgrespro.ru
---
 doc/src/sgml/ref/create_event_trigger.sgml |  9 ++++++++-
 src/backend/commands/event_trigger.c       | 19 +++++++++++++++++++
 src/backend/utils/misc/guc.c               | 11 +++++++++++
 src/include/commands/event_trigger.h       |  2 ++
 4 files changed, 40 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/ref/create_event_trigger.sgml b/doc/src/sgml/ref/create_event_trigger.sgml
index becd31b..09ed331 100644
--- a/doc/src/sgml/ref/create_event_trigger.sgml
+++ b/doc/src/sgml/ref/create_event_trigger.sgml
@@ -123,7 +123,14 @@ CREATE EVENT TRIGGER <replaceable class="parameter">name</replaceable>
    Event triggers are disabled in single-user mode (see <xref
    linkend="app-postgres"/>).  If an erroneous event trigger disables the
    database so much that you can't even drop the trigger, restart in
-   single-user mode and you'll be able to do that.
+   single-user mode and you'll be able to do that. As an alternative to
+   single-user mode, the configuration parameter
+   <literal>disable_event_triggers</literal> may be set to
+   <literal>true</literal>, to disable all event triggers for those with
+   superuser privileges or for those who own the database to which an
+   event trigger belongs. This global event trigger disabling mechanism is
+   completely separate to the individual enable/disable event trigger
+   mechanism provided by the <literal>ALTER EVENT TRIGGER</literal> command.
   </para>
  </refsect1>
 
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 3ffba4e..d0dd4aa 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -48,6 +48,8 @@
 #include "utils/rel.h"
 #include "utils/syscache.h"
 
+bool disable_event_triggers;
+
 typedef struct EventTriggerQueryState
 {
 	/* memory context for this state's objects */
@@ -100,6 +102,7 @@ static void validate_table_rewrite_tags(const char *filtervar, List *taglist);
 static void EventTriggerInvoke(List *fn_oid_list, EventTriggerData *trigdata);
 static const char *stringify_grant_objtype(ObjectType objtype);
 static const char *stringify_adefprivs_objtype(ObjectType objtype);
+static inline bool EventTriggersDisabled(void);
 
 /*
  * Create an event trigger.
@@ -596,6 +599,9 @@ EventTriggerCommonSetup(Node *parsetree,
 	}
 #endif
 
+	if (EventTriggersDisabled())
+		return NIL;
+
 	/* Use cache to find triggers for this event; fast exit if none. */
 	cachelist = EventCacheLookup(event);
 	if (cachelist == NIL)
@@ -635,6 +641,19 @@ EventTriggerCommonSetup(Node *parsetree,
 }
 
 /*
+ * Indicates whether all event triggers are currently disabled
+ * for the current user.
+ * Event triggers are disabled when configuration parameter
+ * "disable_event_triggers" is true, and the current user
+ * is the database owner or has superuser privileges.
+ */
+static inline bool
+EventTriggersDisabled(void)
+{
+	return (disable_event_triggers && pg_database_ownercheck(MyDatabaseId, GetUserId()));
+}
+
+/*
  * Fire ddl_command_start triggers.
  */
 void
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index dabcbb0..ac247be 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -43,6 +43,7 @@
 #include "commands/async.h"
 #include "commands/prepare.h"
 #include "commands/trigger.h"
+#include "commands/event_trigger.h"
 #include "commands/user.h"
 #include "commands/vacuum.h"
 #include "commands/variable.h"
@@ -928,6 +929,16 @@ static const unit_conversion time_unit_conversion_table[] =
 static struct config_bool ConfigureNamesBool[] =
 {
 	{
+		{"disable_event_triggers", PGC_SU_BACKEND, DEVELOPER_OPTIONS,
+			gettext_noop("Disables all event triggers."),
+			gettext_noop("In case of errors in an EVENT TRIGGER procedure, this parameter can be used to disable trigger activation and provide access to the database."),
+			GUC_EXPLAIN
+		},
+		&disable_event_triggers,
+		false,
+		NULL, NULL, NULL
+	},
+	{
 		{"enable_seqscan", PGC_USERSET, QUERY_TUNING_METHOD,
 			gettext_noop("Enables the planner's use of sequential-scan plans."),
 			NULL,
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 407fd6a..c4940b8 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -21,6 +21,8 @@
 #include "tcop/deparse_utility.h"
 #include "utils/aclchk_internal.h"
 
+extern bool disable_event_triggers; /* GUC */
+
 typedef struct EventTriggerData
 {
 	NodeTag		type;
-- 
1.8.3.1

v1-0002-Add-new-client_connection-event-supporting-a-logon-trigger.patchapplication/octet-stream; name=v1-0002-Add-new-client_connection-event-supporting-a-logon-trigger.patchDownload
From 6c78002e920b603028dd898d1311233ed7702829 Mon Sep 17 00:00:00 2001
From: Greg Nancarrow <gregn4422@gmail.com>
Date: Wed, 9 Dec 2020 22:47:36 +1100
Subject: [PATCH v1 2/2] Add a new "client_connection" event, supporting a
 "logon trigger".

The client_connection event occurs when a client connection to the server is
established.

Discussion: https://www.postgresql.org/message-id/flat/0d46d29f-4558-3af9-9c85-7774e14a7709%40postgrespro.ru
---
 doc/src/sgml/event-trigger.sgml                    |  29 ++++++
 src/backend/commands/event_trigger.c               | 101 +++++++++++++++++----
 src/backend/tcop/postgres.c                        |   9 ++
 src/backend/utils/cache/evtcache.c                 |   2 +
 src/include/commands/event_trigger.h               |   1 +
 src/include/tcop/cmdtaglist.h                      |   1 +
 src/include/tcop/tcopprot.h                        |   5 +
 src/include/utils/evtcache.h                       |   3 +-
 .../recovery/t/000_client_connection_trigger.pl    |  69 ++++++++++++++
 src/test/recovery/t/001_stream_rep.pl              |  24 +++++
 src/test/regress/expected/event_trigger.out        |  37 ++++++++
 src/test/regress/sql/event_trigger.sql             |  28 ++++++
 12 files changed, 290 insertions(+), 19 deletions(-)
 create mode 100644 src/test/recovery/t/000_client_connection_trigger.pl

diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 60366a9..bd593dd 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>client_connection</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -36,6 +37,34 @@
    </para>
 
    <para>
+     The <literal>client_connection</literal> event occurs when a client connection
+     to the server is established.
+     There are two mechanisms for dealing with any bugs in a trigger procedure for
+     this event which might prevent successful login to the system:
+     <itemizedlist>
+      <listitem>
+       <para>
+         Event triggers may be temporarily disabled in order to allow login to the
+         system so that the trigger procedure can be corrected.
+         The configuration parameter <literal>disable_event_triggers</literal>
+         makes it possible to disable firing the <literal>client_connection</literal>
+         trigger when a client connects, provided that the user is the owner of the
+         database to which the event trigger belongs, or has superuser privileges.
+         This may be preferable to the alternative of restarting in single-user
+         mode, in which event triggers are disabled.
+       </para>
+      </listitem>
+      <listitem>
+       <para>
+         Errors in the <literal>client_connection</literal> trigger procedure are
+         ignored for superuser. An error message is delivered to the client as
+         <literal>NOTICE</literal> in this case.
+       </para>
+      </listitem>
+     </itemizedlist>
+   </para>
+
+   <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
      <literal>SECURITY LABEL</literal>,
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index d0dd4aa..71d2e3a 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -133,6 +133,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "client_connection") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -565,6 +566,9 @@ EventTriggerCommonSetup(Node *parsetree,
 	ListCell   *lc;
 	List	   *runlist = NIL;
 
+	/* Get the command tag. */
+	tag = parsetree ? CreateCommandTag(parsetree) : CMDTAG_CONNECT;
+
 	/*
 	 * We want the list of command tags for which this procedure is actually
 	 * invoked to match up exactly with the list that CREATE EVENT TRIGGER
@@ -580,22 +584,18 @@ EventTriggerCommonSetup(Node *parsetree,
 	 * relevant command tag.
 	 */
 #ifdef USE_ASSERT_CHECKING
+	if (event == EVT_DDLCommandStart ||
+		event == EVT_DDLCommandEnd ||
+		event == EVT_SQLDrop ||
+		event == EVT_Connect)
 	{
-		CommandTag	dbgtag;
-
-		dbgtag = CreateCommandTag(parsetree);
-		if (event == EVT_DDLCommandStart ||
-			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
-		{
-			if (!command_tag_event_trigger_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
-		else if (event == EVT_TableRewrite)
-		{
-			if (!command_tag_table_rewrite_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
+		if (!command_tag_event_trigger_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
+	}
+	else if (event == EVT_TableRewrite)
+	{
+		if (!command_tag_table_rewrite_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
 	}
 #endif
 
@@ -607,9 +607,6 @@ EventTriggerCommonSetup(Node *parsetree,
 	if (cachelist == NIL)
 		return NIL;
 
-	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
-
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
 	 * memory context.  Once we start running the command triggers, or indeed
@@ -819,6 +816,74 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Fire connect triggers.
+ */
+void
+EventTriggerOnConnect(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId))
+		return;
+
+	/*
+	 * If all event triggers are disabled, an empty transaction can be
+	 * avoided by checking here.
+	 */
+	if (EventTriggersDisabled())
+		return;
+
+	StartTransactionCommand();
+
+	runlist = EventTriggerCommonSetup(NULL,
+									  EVT_Connect, "connect",
+									  &trigdata);
+
+	if (runlist != NIL)
+	{
+		MemoryContext old_context = CurrentMemoryContext;
+		bool is_superuser = superuser();
+
+		/*
+		 * Make sure anything the main command did will be visible to the event
+		 * triggers.
+		 */
+		CommandCounterIncrement();
+
+		/* Run the triggers. */
+		PG_TRY();
+		{
+			EventTriggerInvoke(runlist, &trigdata);
+			list_free(runlist);
+		}
+		PG_CATCH();
+		{
+			ErrorData* error;
+			/*
+			 * Try to ignore error for superuser to make it possible to login even in case of errors
+			 * during trigger execution
+			 */
+			if (!is_superuser)
+				PG_RE_THROW();
+
+			MemoryContextSwitchTo(old_context);
+			error = CopyErrorData();
+			FlushErrorState();
+			elog(NOTICE, "client_connection trigger failed with message: %s", error->message);
+			AbortCurrentTransaction();
+			return;
+		}
+		PG_END_TRY();
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 3679799..57dd4b6 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -42,6 +42,7 @@
 #include "catalog/pg_type.h"
 #include "commands/async.h"
 #include "commands/prepare.h"
+#include "commands/event_trigger.h"
 #include "executor/spi.h"
 #include "jit/jit.h"
 #include "libpq/libpq.h"
@@ -167,6 +168,9 @@ static ProcSignalReason RecoveryConflictReason;
 static MemoryContext row_description_context = NULL;
 static StringInfoData row_description_buf;
 
+/* Hook for plugins to get control at start of client connection */
+client_connection_hook_type client_connection_hook = EventTriggerOnConnect;
+
 /* ----------------------------------------------------------------
  *		decls for routines only used in this file
  * ----------------------------------------------------------------
@@ -4012,6 +4016,11 @@ PostgresMain(int argc, char *argv[],
 	if (!IsUnderPostmaster)
 		PgStartTime = GetCurrentTimestamp();
 
+	if (client_connection_hook)
+	{
+		(*client_connection_hook) ();
+	}
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 0427795..c621b8f 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -168,6 +168,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "client_connection") == 0)
+			event = EVT_Connect;
 		else
 			continue;
 
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index c4940b8..1d5e66d 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -55,6 +55,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnConnect(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index be94852..988aa39 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -80,6 +80,7 @@ PG_CMDTAG(CMDTAG_CLUSTER, "CLUSTER", false, false, false)
 PG_CMDTAG(CMDTAG_COMMENT, "COMMENT", true, false, false)
 PG_CMDTAG(CMDTAG_COMMIT, "COMMIT", false, false, false)
 PG_CMDTAG(CMDTAG_COMMIT_PREPARED, "COMMIT PREPARED", false, false, false)
+PG_CMDTAG(CMDTAG_CONNECT, "CONNECT", true, false, false)
 PG_CMDTAG(CMDTAG_COPY, "COPY", false, false, true)
 PG_CMDTAG(CMDTAG_COPY_FROM, "COPY FROM", false, false, false)
 PG_CMDTAG(CMDTAG_CREATE_ACCESS_METHOD, "CREATE ACCESS METHOD", true, false, false)
diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h
index bd30607..f4c2c24 100644
--- a/src/include/tcop/tcopprot.h
+++ b/src/include/tcop/tcopprot.h
@@ -30,6 +30,11 @@ extern PGDLLIMPORT const char *debug_query_string;
 extern int	max_stack_depth;
 extern int	PostAuthDelay;
 
+/* Hook for plugins to get control at time of client cnnection */
+typedef void (*client_connection_hook_type) (void);
+
+extern PGDLLIMPORT client_connection_hook_type client_connection_hook;
+
 /* GUC-configurable parameters */
 
 typedef enum
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index bb1e39e..ab7e8c3 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Connect,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/000_client_connection_trigger.pl b/src/test/recovery/t/000_client_connection_trigger.pl
new file mode 100644
index 0000000..3dcd475
--- /dev/null
+++ b/src/test/recovery/t/000_client_connection_trigger.pl
@@ -0,0 +1,69 @@
+use strict;
+use warnings;
+use PostgresNode;
+use TestLib;
+use Test::More;
+if (!$use_unix_sockets)
+{
+	plan skip_all =>
+	  "authentication tests cannot run without Unix-domain sockets";
+}
+else
+{
+	plan tests => 5;
+}
+
+# Initialize master node
+my $node = get_new_node('master');
+$node->init;
+$node->start;
+$node->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+CREATE ROLE regress_hacker LOGIN PASSWORD 'pass';
+
+CREATE TABLE connects(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO connects (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON client_connection EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+}
+);
+my $res;
+
+$res = $node->safe_psql('postgres', "SELECT 1");
+
+$res = $node->safe_psql('postgres', "SELECT 1",
+  extra_params => [ '-U', 'regress_user', '-w' ]);
+
+my ($ret, $stdout, $stderr) = $node->psql('postgres', "SELECT 1",
+  extra_params => [ '-U', 'regress_hacker', '-w' ]);
+ok( $ret != 0 && $stderr =~ /You are not welcome!/ );
+
+$res = $node->safe_psql('postgres', "SELECT COUNT(1) FROM connects WHERE who = 'regress_user'");
+ok($res == 1);
+
+my $tempdir = TestLib::tempdir;
+command_ok(
+  [ "pg_dumpall", '-p', $node->port, '-c', "--file=$tempdir/regression_dump.sql", ],
+  "dumpall");
+# my $dump_contents = slurp_file("$tempdir/regression_dump.sql");
+# print($dump_contents);
+
+my $node1 = get_new_node('secondary');
+$node1->init;
+$node1->start;
+command_ok(["psql", '-p', $node1->port, '-b', '-f', "$tempdir/regression_dump.sql" ] );
+$res = $node1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$res = $node1->safe_psql('postgres', "SELECT COUNT(1) FROM connects WHERE who = 'regress_user'");
+ok($res == 2);
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 9e31a53..b4a21fb 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -43,6 +43,27 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+
+CREATE TABLE connects(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO connects (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON client_connection EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 $node_primary->wait_for_catchup($node_standby_1, 'replay',
 	$node_primary->lsn('insert'));
@@ -266,6 +287,9 @@ sub replay_check
 
 replay_check();
 
+$node_standby_1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$node_standby_2->safe_psql('postgres', "SELECT 2", extra_params => [ '-U', 'regress_user', '-w' ]);
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index bdd0ffc..d1639bc 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -536,3 +536,40 @@ NOTICE:  DROP POLICY - ddl_command_end
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- On client connection triggers
+create table connects(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into connects (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on client_connection execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id | who  
+----+------
+  1 | I am
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id | who  
+----+------
+  1 | I am
+  2 | I am
+(2 rows)
+
+-- Test handing exeptions in client_connection trigger
+drop table connects;
+-- superuser should ignore error
+\c
+NOTICE:  client_connection trigger failed with message: relation "connects" does not exist
+-- suppress trigger firing
+\c "dbname=regression options='-c disable_event_triggers=true'"
+-- Cleanup
+drop event trigger on_login_trigger;
+drop function on_login_proc();
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 18b2a26..69e62b0 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -429,3 +429,31 @@ DROP POLICY p2 ON event_trigger_test;
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- On client connection triggers
+create table connects(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into connects (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on client_connection execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+select * from connects;
+\c
+select * from connects;
+
+-- Test handing exeptions in client_connection trigger
+
+drop table connects;
+-- superuser should ignore error
+\c
+-- suppress trigger firing
+\c "dbname=regression options='-c disable_event_triggers=true'"
+
+
+-- Cleanup
+drop event trigger on_login_trigger;
+drop function on_login_proc();
-- 
1.8.3.1

#16Pavel Stehule
pavel.stehule@gmail.com
In reply to: Greg Nancarrow (#15)
Re: On login trigger: take three

st 9. 12. 2020 v 13:17 odesílatel Greg Nancarrow <gregn4422@gmail.com>
napsal:

On Tue, Dec 8, 2020 at 3:26 PM Pavel Stehule <pavel.stehule@gmail.com>
wrote:

There are two maybe generic questions?

1. Maybe we can introduce more generic GUC for all event triggers like

disable_event_triggers? This GUC can be checked only by the database owner
or super user. It can be an alternative ALTER TABLE DISABLE TRIGGER ALL. It
can be protection against necessity to restart to single mode to repair the
event trigger. I think so more generic solution is better than special
disable_client_connection_trigger GUC.

2. I have no objection against client_connection. It is probably better

for the mentioned purpose - possibility to block connection to database.
Can be interesting, and I am not sure how much work it is to introduce the
second event - session_start. This event should be started after connecting
- so the exception there doesn't block connect, and should be started also
after the new statement "DISCARD SESSION", that will be started
automatically after DISCARD ALL. This feature should not be implemented in
first step, but it can be a plan for support pooled connections

PGC_SU_BACKEND is too strong, there should be PGC_BACKEND if this option
can be used by database owner

Pavel

Show quoted text

I've created a separate patch to address question (1), rather than
include it in the main patch, which I've adjusted accordingly. I'll
leave question (2) until another time, as you suggest.
See the attached patches.

Regards,
Greg Nancarrow
Fujitsu Australia

#17Pavel Stehule
pavel.stehule@gmail.com
In reply to: Greg Nancarrow (#15)
Re: On login trigger: take three

Hi

st 9. 12. 2020 v 13:17 odesílatel Greg Nancarrow <gregn4422@gmail.com>
napsal:

On Tue, Dec 8, 2020 at 3:26 PM Pavel Stehule <pavel.stehule@gmail.com>
wrote:

There are two maybe generic questions?

1. Maybe we can introduce more generic GUC for all event triggers like

disable_event_triggers? This GUC can be checked only by the database owner
or super user. It can be an alternative ALTER TABLE DISABLE TRIGGER ALL. It
can be protection against necessity to restart to single mode to repair the
event trigger. I think so more generic solution is better than special
disable_client_connection_trigger GUC.

2. I have no objection against client_connection. It is probably better

for the mentioned purpose - possibility to block connection to database.
Can be interesting, and I am not sure how much work it is to introduce the
second event - session_start. This event should be started after connecting
- so the exception there doesn't block connect, and should be started also
after the new statement "DISCARD SESSION", that will be started
automatically after DISCARD ALL. This feature should not be implemented in
first step, but it can be a plan for support pooled connections

I've created a separate patch to address question (1), rather than
include it in the main patch, which I've adjusted accordingly. I'll
leave question (2) until another time, as you suggest.
See the attached patches.

I see two possible questions?

1. when you introduce this event, then the new hook is useless ?

2. what is a performance impact for users that want not to use this
feature. What is a overhead of EventTriggerOnConnect and is possible to
skip this step if database has not any event trigger

Pavel

Show quoted text

Regards,
Greg Nancarrow
Fujitsu Australia

#18Konstantin Knizhnik
k.knizhnik@postgrespro.ru
In reply to: Pavel Stehule (#17)
Re: On login trigger: take three

On 09.12.2020 15:34, Pavel Stehule wrote:

Hi

st 9. 12. 2020 v 13:17 odesílatel Greg Nancarrow <gregn4422@gmail.com
<mailto:gregn4422@gmail.com>> napsal:

On Tue, Dec 8, 2020 at 3:26 PM Pavel Stehule
<pavel.stehule@gmail.com <mailto:pavel.stehule@gmail.com>> wrote:

There are two maybe generic questions?

1. Maybe we can introduce more generic GUC for all event

triggers like disable_event_triggers? This GUC can be checked only
by the database owner or super user. It can be an alternative
ALTER TABLE DISABLE TRIGGER ALL. It can be protection against
necessity to restart to single mode to repair the event trigger. I
think so more generic solution is better than special
disable_client_connection_trigger GUC.

2. I have no objection against client_connection. It is probably

better for the mentioned purpose - possibility to block connection
to database. Can be interesting, and I am not sure how much work
it is to introduce the second event - session_start. This event
should be started after connecting - so the exception there
doesn't block connect, and should be started also after the new
statement "DISCARD SESSION", that will be started automatically
after DISCARD ALL.  This feature should not be implemented in
first step, but it can be a plan for support pooled connections

I've created a separate patch to address question (1), rather than
include it in the main patch, which I've adjusted accordingly. I'll
leave question (2) until another time, as you suggest.
See the attached patches.

I see two possible questions?

1. when you introduce this event, then the new hook is useless ?

2. what is a performance impact for users that want not to use this
feature. What is a overhead of EventTriggerOnConnect and is possible
to skip this step if database has not any event trigger

As far as I understand this are questions to me rather than to Greg.
1. Do you mean client_connection_hook? It is used to implement this new
event type. It can be also used for other purposes.
2. Connection overhead is quite large. Supporting on connect hook
requires traversal of event trigger relation. But this overhead is
negligible comparing with overhead of establishing connection. In any
case I did the following test (with local connection):

pgbench -C -S -T 100 -P 1 -M prepared postgres

without this patch:
tps = 424.287889 (including connections establishing)
tps = 952.911068 (excluding connections establishing)

with this patch (but without any connection trigger defined):
tps = 434.642947 (including connections establishing)
tps = 995.525383 (excluding connections establishing)

As you can see - there is almost now different (patched version is even
faster, but it seems to be just "white noise".

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#19Pavel Stehule
pavel.stehule@gmail.com
In reply to: Konstantin Knizhnik (#18)
3 attachment(s)
Re: On login trigger: take three

st 9. 12. 2020 v 14:28 odesílatel Konstantin Knizhnik <
k.knizhnik@postgrespro.ru> napsal:

On 09.12.2020 15:34, Pavel Stehule wrote:

Hi

st 9. 12. 2020 v 13:17 odesílatel Greg Nancarrow <gregn4422@gmail.com>
napsal:

On Tue, Dec 8, 2020 at 3:26 PM Pavel Stehule <pavel.stehule@gmail.com>
wrote:

There are two maybe generic questions?

1. Maybe we can introduce more generic GUC for all event triggers like

disable_event_triggers? This GUC can be checked only by the database owner
or super user. It can be an alternative ALTER TABLE DISABLE TRIGGER ALL. It
can be protection against necessity to restart to single mode to repair the
event trigger. I think so more generic solution is better than special
disable_client_connection_trigger GUC.

2. I have no objection against client_connection. It is probably better

for the mentioned purpose - possibility to block connection to database.
Can be interesting, and I am not sure how much work it is to introduce the
second event - session_start. This event should be started after connecting
- so the exception there doesn't block connect, and should be started also
after the new statement "DISCARD SESSION", that will be started
automatically after DISCARD ALL. This feature should not be implemented in
first step, but it can be a plan for support pooled connections

I've created a separate patch to address question (1), rather than
include it in the main patch, which I've adjusted accordingly. I'll
leave question (2) until another time, as you suggest.
See the attached patches.

I see two possible questions?

1. when you introduce this event, then the new hook is useless ?

2. what is a performance impact for users that want not to use this
feature. What is a overhead of EventTriggerOnConnect and is possible to
skip this step if database has not any event trigger

As far as I understand this are questions to me rather than to Greg.
1. Do you mean client_connection_hook? It is used to implement this new
event type. It can be also used for other purposes.

ok. I don't like it, but there are redundant hooks (against event triggers)
already.

2. Connection overhead is quite large. Supporting on connect hook requires

traversal of event trigger relation. But this overhead is negligible
comparing with overhead of establishing connection. In any case I did the
following test (with local connection):

pgbench -C -S -T 100 -P 1 -M prepared postgres

without this patch:
tps = 424.287889 (including connections establishing)
tps = 952.911068 (excluding connections establishing)

with this patch (but without any connection trigger defined):
tps = 434.642947 (including connections establishing)
tps = 995.525383 (excluding connections establishing)

As you can see - there is almost now different (patched version is even
faster, but it seems to be just "white noise".

This is not the worst case probably. In this patch the
StartTransactionCommand is executed on every connection, although it is not
necessary - and for most use cases it will not be used.

I did more tests - see attachments and I see a 5% slowdown - I don't think
so it is acceptable for this case. This feature is nice, and for some users
important, but only really few users can use it.

┌────────────────┬─────────┬────────────┬─────────────┐
│ test │ WITH LT │ LT ENABLED │ LT DISABLED │
╞════════════════╪═════════╪════════════╪═════════════╡
│ ro_constant_10 │ 539 │ 877 │ 905 │
│ ro_index_10 │ 562 │ 808 │ 855 │
│ ro_constant_50 │ 527 │ 843 │ 863 │
│ ro_index_50 │ 544 │ 731 │ 742 │
└────────────────┴─────────┴────────────┴─────────────┘
(4 rows)

I tested a performance of trigger (results of first column in table):

CREATE OR REPLACE FUNCTION public.foo()
RETURNS event_trigger
LANGUAGE plpgsql
AS $function$
begin
if not pg_has_role(session_user, 'postgres', 'member') then
raise exception 'you are not super user';
end if;
end;
$function$;

There is an available snapshot in InitPostgres, and then there is possible
to check if for the current database some connect event trigger exists.This
can reduce an overhead of this patch, when there are no logon triggers.

I think so implemented and used names are ok, but for this feature the
performance impact should be really very minimal.

There is other small issue - missing tab-complete support for CREATE
TRIGGER statement in psql

Regards

Pavel

Show quoted text

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

Attachments:

test-ro-2.sqlapplication/sql; name=test-ro-2.sqlDownload
dataapplication/octet-stream; name=dataDownload
test-ro.sqlapplication/sql; name=test-ro.sqlDownload
#20Konstantin Knizhnik
k.knizhnik@postgrespro.ru
In reply to: Pavel Stehule (#19)
Re: On login trigger: take three

On 10.12.2020 10:45, Pavel Stehule wrote:

st 9. 12. 2020 v 14:28 odesílatel Konstantin Knizhnik
<k.knizhnik@postgrespro.ru <mailto:k.knizhnik@postgrespro.ru>> napsal:

On 09.12.2020 15:34, Pavel Stehule wrote:

Hi

st 9. 12. 2020 v 13:17 odesílatel Greg Nancarrow
<gregn4422@gmail.com <mailto:gregn4422@gmail.com>> napsal:

On Tue, Dec 8, 2020 at 3:26 PM Pavel Stehule
<pavel.stehule@gmail.com <mailto:pavel.stehule@gmail.com>> wrote:

There are two maybe generic questions?

1. Maybe we can introduce more generic GUC for all event

triggers like disable_event_triggers? This GUC can be checked
only by the database owner or super user. It can be an
alternative ALTER TABLE DISABLE TRIGGER ALL. It can be
protection against necessity to restart to single mode to
repair the event trigger. I think so more generic solution is
better than special disable_client_connection_trigger GUC.

2. I have no objection against client_connection. It is

probably better for the mentioned purpose - possibility to
block connection to database. Can be interesting, and I am
not sure how much work it is to introduce the second event -
session_start. This event should be started after connecting
- so the exception there doesn't block connect, and should be
started also after the new statement "DISCARD SESSION", that
will be started automatically after DISCARD ALL. This feature
should not be implemented in first step, but it can be a plan
for support pooled connections

I've created a separate patch to address question (1), rather
than
include it in the main patch, which I've adjusted
accordingly. I'll
leave question (2) until another time, as you suggest.
See the attached patches.

I see two possible questions?

1. when you introduce this event, then the new hook is useless ?

2. what is a performance impact for users that want not to use
this feature. What is a overhead of EventTriggerOnConnect and is
possible to skip this step if database has not any event trigger

As far as I understand this are questions to me rather than to Greg.
1. Do you mean client_connection_hook? It is used to implement
this new event type. It can be also used for other purposes.

ok. I don't like it, but there are redundant hooks (against event
triggers) already.

2. Connection overhead is quite large. Supporting on connect hook
requires traversal of event trigger relation. But this overhead is
negligible comparing with overhead of establishing connection. In
any case I did the following test (with local connection):

pgbench -C -S -T 100 -P 1 -M prepared postgres

without this patch:
tps = 424.287889 (including connections establishing)
tps = 952.911068 (excluding connections establishing)

with this patch (but without any connection trigger defined):
tps = 434.642947 (including connections establishing)
tps = 995.525383 (excluding connections establishing)

As you can see - there is almost now different (patched version is
even faster, but it seems to be just "white noise".

This is not the worst case probably. In this patch the
StartTransactionCommand is executed on every connection, although it
is not necessary - and for most use cases it will not be used.

I did more tests - see attachments and I see a 5% slowdown - I don't
think so it is acceptable for this case. This feature is nice, and for
some users important, but only really few users can use it.

┌────────────────┬─────────┬────────────┬─────────────┐
│      test      │ WITH LT │ LT ENABLED │ LT DISABLED │
╞════════════════╪═════════╪════════════╪═════════════╡
│ ro_constant_10 │     539 │        877 │         905 │
│ ro_index_10    │     562 │        808 │         855 │
│ ro_constant_50 │     527 │        843 │         863 │
│ ro_index_50    │     544 │        731 │         742 │
└────────────────┴─────────┴────────────┴─────────────┘
(4 rows)

I tested a performance of trigger (results of first column in table):

CREATE OR REPLACE FUNCTION public.foo()
 RETURNS event_trigger
 LANGUAGE plpgsql
AS $function$
begin
  if not pg_has_role(session_user, 'postgres', 'member') then
    raise exception 'you are not super user';
  end if;
end;
$function$;

There is an available snapshot in InitPostgres, and then there is
possible to check if for the current database some connect event
trigger exists.This can reduce an overhead of this patch, when there
are no logon triggers.

I think so implemented and used names are ok, but for this feature the
performance impact should be really very minimal.

There is other small issue - missing tab-complete support for CREATE
TRIGGER statement in psql

Regards

Pavel

Unfortunately I was not able to reproduce your results.
I just tried the case "select 1" because for this trivial query the
overhead of session hooks should be the largest.
In my case variation of results was large enough.
Did you try to run test multiple times?

pgbench -j 2 -c 10 --connect -f test-ro.sql -T 60 -n postgres

disable_event_triggers = off
tps = 1812.250463 (including connections establishing)
tps = 2256.285712 (excluding connections establishing)

tps = 1838.107242 (including connections establishing)
tps = 2288.507668 (excluding connections establishing)

tps = 1830.879302 (including connections establishing)
tps = 2279.302553 (excluding connections establishing)

disable_event_triggers = on
tps = 1858.328717 (including connections establishing)
tps = 2313.661689 (excluding connections establishing)

tps = 1832.932960 (including connections establishing)
tps = 2282.074346 (excluding connections establishing)

tps = 1868.908521 (including connections establishing)
tps = 2326.559150 (excluding connections establishing)

I tried to increase run time to 1000 seconds.
Results are the following:

disable_event_triggers = off
tps = 1813.587876 (including connections establishing)
tps = 2257.844703 (excluding connections establishing)

disable_event_triggers = on
tps = 1838.107242 (including connections establishing)
tps = 2288.507668 (excluding connections establishing)

So the difference on this extreme case is 1%.

I tried your suggestion and move client_connection_hook invocation to
postinit.c
It slightly improve performance:

tps = 1828.446959 (including connections establishing)
tps = 2276.142972 (excluding connections establishing)

So result is somewhere in the middle, because it allows to eliminate
overhead of
starting transaction or taking snapshots but not of lookup through
pg_event_trigger table (although it it empty).

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#21Pavel Stehule
pavel.stehule@gmail.com
In reply to: Konstantin Knizhnik (#20)
Re: On login trigger: take three

čt 10. 12. 2020 v 14:03 odesílatel Konstantin Knizhnik <
k.knizhnik@postgrespro.ru> napsal:

On 10.12.2020 10:45, Pavel Stehule wrote:

st 9. 12. 2020 v 14:28 odesílatel Konstantin Knizhnik <
k.knizhnik@postgrespro.ru> napsal:

On 09.12.2020 15:34, Pavel Stehule wrote:

Hi

st 9. 12. 2020 v 13:17 odesílatel Greg Nancarrow <gregn4422@gmail.com>
napsal:

On Tue, Dec 8, 2020 at 3:26 PM Pavel Stehule <pavel.stehule@gmail.com>
wrote:

There are two maybe generic questions?

1. Maybe we can introduce more generic GUC for all event triggers like

disable_event_triggers? This GUC can be checked only by the database owner
or super user. It can be an alternative ALTER TABLE DISABLE TRIGGER ALL. It
can be protection against necessity to restart to single mode to repair the
event trigger. I think so more generic solution is better than special
disable_client_connection_trigger GUC.

2. I have no objection against client_connection. It is probably

better for the mentioned purpose - possibility to block connection to
database. Can be interesting, and I am not sure how much work it is to
introduce the second event - session_start. This event should be started
after connecting - so the exception there doesn't block connect, and should
be started also after the new statement "DISCARD SESSION", that will be
started automatically after DISCARD ALL. This feature should not be
implemented in first step, but it can be a plan for support pooled
connections

I've created a separate patch to address question (1), rather than
include it in the main patch, which I've adjusted accordingly. I'll
leave question (2) until another time, as you suggest.
See the attached patches.

I see two possible questions?

1. when you introduce this event, then the new hook is useless ?

2. what is a performance impact for users that want not to use this
feature. What is a overhead of EventTriggerOnConnect and is possible to
skip this step if database has not any event trigger

As far as I understand this are questions to me rather than to Greg.
1. Do you mean client_connection_hook? It is used to implement this new
event type. It can be also used for other purposes.

ok. I don't like it, but there are redundant hooks (against event
triggers) already.

2. Connection overhead is quite large. Supporting on connect hook requires

traversal of event trigger relation. But this overhead is negligible
comparing with overhead of establishing connection. In any case I did the
following test (with local connection):

pgbench -C -S -T 100 -P 1 -M prepared postgres

without this patch:
tps = 424.287889 (including connections establishing)
tps = 952.911068 (excluding connections establishing)

with this patch (but without any connection trigger defined):
tps = 434.642947 (including connections establishing)
tps = 995.525383 (excluding connections establishing)

As you can see - there is almost now different (patched version is even
faster, but it seems to be just "white noise".

This is not the worst case probably. In this patch the
StartTransactionCommand is executed on every connection, although it is not
necessary - and for most use cases it will not be used.

I did more tests - see attachments and I see a 5% slowdown - I don't think
so it is acceptable for this case. This feature is nice, and for some users
important, but only really few users can use it.

┌────────────────┬─────────┬────────────┬─────────────┐
│ test │ WITH LT │ LT ENABLED │ LT DISABLED │
╞════════════════╪═════════╪════════════╪═════════════╡
│ ro_constant_10 │ 539 │ 877 │ 905 │
│ ro_index_10 │ 562 │ 808 │ 855 │
│ ro_constant_50 │ 527 │ 843 │ 863 │
│ ro_index_50 │ 544 │ 731 │ 742 │
└────────────────┴─────────┴────────────┴─────────────┘
(4 rows)

I tested a performance of trigger (results of first column in table):

CREATE OR REPLACE FUNCTION public.foo()
RETURNS event_trigger
LANGUAGE plpgsql
AS $function$
begin
if not pg_has_role(session_user, 'postgres', 'member') then
raise exception 'you are not super user';
end if;
end;
$function$;

There is an available snapshot in InitPostgres, and then there is possible
to check if for the current database some connect event trigger exists.This
can reduce an overhead of this patch, when there are no logon triggers.

I think so implemented and used names are ok, but for this feature the
performance impact should be really very minimal.

There is other small issue - missing tab-complete support for CREATE
TRIGGER statement in psql

Regards

Pavel

Unfortunately I was not able to reproduce your results.
I just tried the case "select 1" because for this trivial query the
overhead of session hooks should be the largest.
In my case variation of results was large enough.
Did you try to run test multiple times?

pgbench -j 2 -c 10 --connect -f test-ro.sql -T 60 -n postgres

disable_event_triggers = off
tps = 1812.250463 (including connections establishing)
tps = 2256.285712 (excluding connections establishing)

tps = 1838.107242 (including connections establishing)
tps = 2288.507668 (excluding connections establishing)

tps = 1830.879302 (including connections establishing)
tps = 2279.302553 (excluding connections establishing)

disable_event_triggers = on
tps = 1858.328717 (including connections establishing)
tps = 2313.661689 (excluding connections establishing)

tps = 1832.932960 (including connections establishing)
tps = 2282.074346 (excluding connections establishing)

tps = 1868.908521 (including connections establishing)
tps = 2326.559150 (excluding connections establishing)

I tried to increase run time to 1000 seconds.
Results are the following:

disable_event_triggers = off
tps = 1813.587876 (including connections establishing)
tps = 2257.844703 (excluding connections establishing)

disable_event_triggers = on
tps = 1838.107242 (including connections establishing)
tps = 2288.507668 (excluding connections establishing)

So the difference on this extreme case is 1%.

you have a stronger CPU (2x), and probably you are hitting different
bottleneck or maybe some other is wrong. I can recheck it and I can attach
profiles.

I tried your suggestion and move client_connection_hook invocation to
postinit.c
It slightly improve performance:

tps = 1828.446959 (including connections establishing)
tps = 2276.142972 (excluding connections establishing)

So result is somewhere in the middle, because it allows to eliminate
overhead of
starting transaction or taking snapshots but not of lookup through
pg_event_trigger table (although it it empty).

My idea was a little bit different. Inside postinit initialize some global
variables with info if there are event triggers or not. And later you can
use this variable to start transactions and other things.

There will be two access to pg_event_trigger, but it can eliminate useless
and probably more expensive start_transaction and end_transaction.

Regards

Pavel

Show quoted text

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#22Konstantin Knizhnik
k.knizhnik@postgrespro.ru
In reply to: Pavel Stehule (#21)
Re: On login trigger: take three

On 10.12.2020 18:12, Pavel Stehule wrote:

My idea was a little bit different. Inside postinit initialize some
global variables with info if there are event triggers or not. And
later you can use this variable to start transactions and  other things.

There will be two access to pg_event_trigger, but it can eliminate
useless and probably more expensive start_transaction and end_transaction.

Do you mean some variable in shared memory or GUCs?
It was my first idea - to use some flag in shared memory to make it
possible fast check that there are not event triggers.
But this flag should be sent per database. May be I missed something,
but there is no any per-database shared memory  data structure in Postgres.
Certainly it is possible to organize some hash db->event_info, but it
makes this patch several times more complex.

From my point of view it is better to have separate GUC disabling just
client connection events and switch it on by default.
So only those who need this events with switch it on, other users will
not pay any extra cost for it.

#23Pavel Stehule
pavel.stehule@gmail.com
In reply to: Konstantin Knizhnik (#22)
Re: On login trigger: take three

čt 10. 12. 2020 v 16:48 odesílatel Konstantin Knizhnik <
k.knizhnik@postgrespro.ru> napsal:

On 10.12.2020 18:12, Pavel Stehule wrote:

My idea was a little bit different. Inside postinit initialize some global
variables with info if there are event triggers or not. And later you can
use this variable to start transactions and other things.

There will be two access to pg_event_trigger, but it can eliminate useless
and probably more expensive start_transaction and end_transaction.

Do you mean some variable in shared memory or GUCs?
It was my first idea - to use some flag in shared memory to make it
possible fast check that there are not event triggers.
But this flag should be sent per database. May be I missed something, but
there is no any per-database shared memory data structure in Postgres.
Certainly it is possible to organize some hash db->event_info, but it
makes this patch several times more complex.

My idea was a little bit different - just inside process initialization,
checking existence of event triggers, and later when it is necessary to
start a transaction for trigger execution. This should to reduce useless
empty transaction,

I am sure this is a problem on computers with slower CPU s, although I have
I7, but because this feature is usually unused, then the performance impact
should be minimal every time.

From my point of view it is better to have separate GUC disabling just
client connection events and switch it on by default.
So only those who need this events with switch it on, other users will not
pay any extra cost for it.

It can work, but this design is not user friendly. The significant
bottleneck should be forking new processes, and check of content some
usually very small tables should be invisible. So if there is a possible
way to implement some feature that can be enabled by default, then we
should go this way. It can be pretty unfriendly if somebody writes a logon
trigger and it will not work by default without any warning.

When I tested last patch I found a problem (I have disabled assertions due
performance testing)

create role xx login;
alter system set disable_event_triggers to on; -- better should be positive
form - enable_event_triggers to on, off
select pg_reload_conf();

psql -U xx

Thread 2.1 "postmaster" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7f2bff438ec0 (LWP 827497)]
ResourceOwnerEnlargeCatCacheRefs (owner=0x0) at resowner.c:1025
1025 ResourceArrayEnlarge(&(owner->catrefarr));
(gdb) bt
#0 ResourceOwnerEnlargeCatCacheRefs (owner=0x0) at resowner.c:1025
#1 0x00000000008a70f8 in SearchCatCacheInternal (cache=<optimized out>,
nkeys=nkeys@entry=1, v1=v1@entry=13836, v2=v2@entry=0,
v3=v3@entry=0, v4=v4@entry=0) at catcache.c:1273
#2 0x00000000008a7575 in SearchCatCache1 (cache=<optimized out>,
v1=v1@entry=13836) at catcache.c:1167
#3 0x00000000008b7f80 in SearchSysCache1 (cacheId=cacheId@entry=21,
key1=key1@entry=13836) at syscache.c:1122
#4 0x00000000005939cd in pg_database_ownercheck (db_oid=13836,
roleid=16387) at aclchk.c:5114
#5 0x0000000000605b42 in EventTriggersDisabled () at event_trigger.c:650
#6 EventTriggerOnConnect () at event_trigger.c:839
#7 0x00000000007b46d7 in PostgresMain (argc=argc@entry=1,
argv=argv@entry=0x7ffdd6256080,
dbname=<optimized out>,
username=0xf82508 "tom") at postgres.c:4021
#8 0x0000000000741afd in BackendRun (port=<optimized out>, port=<optimized
out>) at postmaster.c:4490
#9 BackendStartup (port=<optimized out>) at postmaster.c:4212
#10 ServerLoop () at postmaster.c:1729
#11 0x0000000000742881 in PostmasterMain (argc=argc@entry=3,
argv=argv@entry=0xf44d00)
at postmaster.c:1401
#12 0x00000000004f1ff7 in main (argc=3, argv=0xf44d00) at main.c:209

I checked profiles, and although there is significant slowdown when there
is one login trigger, it was not related to plpgsql.

There is big overhead of

8,98% postmaster postgres [.] guc_name_compare

Maybe EventTriggerInvoke is not well optimized for very frequent execution.
Overhead of plpgsql is less than 0.1%, although there is significant
slowdown (when a very simple logon trigger is executed).

Regards

Pavel

#24Pavel Stehule
pavel.stehule@gmail.com
In reply to: Pavel Stehule (#23)
Re: On login trigger: take three

čt 10. 12. 2020 v 19:09 odesílatel Pavel Stehule <pavel.stehule@gmail.com>
napsal:

čt 10. 12. 2020 v 16:48 odesílatel Konstantin Knizhnik <
k.knizhnik@postgrespro.ru> napsal:

On 10.12.2020 18:12, Pavel Stehule wrote:

My idea was a little bit different. Inside postinit initialize some
global variables with info if there are event triggers or not. And later
you can use this variable to start transactions and other things.

There will be two access to pg_event_trigger, but it can eliminate
useless and probably more expensive start_transaction and end_transaction.

Do you mean some variable in shared memory or GUCs?
It was my first idea - to use some flag in shared memory to make it
possible fast check that there are not event triggers.
But this flag should be sent per database. May be I missed something, but
there is no any per-database shared memory data structure in Postgres.
Certainly it is possible to organize some hash db->event_info, but it
makes this patch several times more complex.

My idea was a little bit different - just inside process initialization,
checking existence of event triggers, and later when it is necessary to
start a transaction for trigger execution. This should to reduce useless
empty transaction,

but this information can be accessed via shared memory if it is necessary.
Probably it doesn't need a special hash table. Maybe a special flag per
process.

Show quoted text

I am sure this is a problem on computers with slower CPU s, although I
have I7, but because this feature is usually unused, then the performance
impact should be minimal every time.

From my point of view it is better to have separate GUC disabling just
client connection events and switch it on by default.
So only those who need this events with switch it on, other users will
not pay any extra cost for it.

It can work, but this design is not user friendly. The significant
bottleneck should be forking new processes, and check of content some
usually very small tables should be invisible. So if there is a possible
way to implement some feature that can be enabled by default, then we
should go this way. It can be pretty unfriendly if somebody writes a logon
trigger and it will not work by default without any warning.

When I tested last patch I found a problem (I have disabled assertions due
performance testing)

create role xx login;
alter system set disable_event_triggers to on; -- better should be
positive form - enable_event_triggers to on, off
select pg_reload_conf();

psql -U xx

Thread 2.1 "postmaster" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7f2bff438ec0 (LWP 827497)]
ResourceOwnerEnlargeCatCacheRefs (owner=0x0) at resowner.c:1025
1025 ResourceArrayEnlarge(&(owner->catrefarr));
(gdb) bt
#0 ResourceOwnerEnlargeCatCacheRefs (owner=0x0) at resowner.c:1025
#1 0x00000000008a70f8 in SearchCatCacheInternal (cache=<optimized out>,
nkeys=nkeys@entry=1, v1=v1@entry=13836, v2=v2@entry=0,
v3=v3@entry=0, v4=v4@entry=0) at catcache.c:1273
#2 0x00000000008a7575 in SearchCatCache1 (cache=<optimized out>,
v1=v1@entry=13836) at catcache.c:1167
#3 0x00000000008b7f80 in SearchSysCache1 (cacheId=cacheId@entry=21,
key1=key1@entry=13836) at syscache.c:1122
#4 0x00000000005939cd in pg_database_ownercheck (db_oid=13836,
roleid=16387) at aclchk.c:5114
#5 0x0000000000605b42 in EventTriggersDisabled () at event_trigger.c:650
#6 EventTriggerOnConnect () at event_trigger.c:839
#7 0x00000000007b46d7 in PostgresMain (argc=argc@entry=1, argv=argv@entry=0x7ffdd6256080,
dbname=<optimized out>,
username=0xf82508 "tom") at postgres.c:4021
#8 0x0000000000741afd in BackendRun (port=<optimized out>,
port=<optimized out>) at postmaster.c:4490
#9 BackendStartup (port=<optimized out>) at postmaster.c:4212
#10 ServerLoop () at postmaster.c:1729
#11 0x0000000000742881 in PostmasterMain (argc=argc@entry=3,
argv=argv@entry=0xf44d00) at postmaster.c:1401
#12 0x00000000004f1ff7 in main (argc=3, argv=0xf44d00) at main.c:209

I checked profiles, and although there is significant slowdown when there
is one login trigger, it was not related to plpgsql.

There is big overhead of

8,98% postmaster postgres [.] guc_name_compare

Maybe EventTriggerInvoke is not well optimized for very frequent
execution. Overhead of plpgsql is less than 0.1%, although there is
significant slowdown (when a very simple logon trigger is executed).

Regards

Pavel

#25Konstantin Knizhnik
k.knizhnik@postgrespro.ru
In reply to: Pavel Stehule (#23)
Re: On login trigger: take three

On 10.12.2020 21:09, Pavel Stehule wrote:

čt 10. 12. 2020 v 16:48 odesílatel Konstantin Knizhnik
<k.knizhnik@postgrespro.ru <mailto:k.knizhnik@postgrespro.ru>> napsal:

On 10.12.2020 18:12, Pavel Stehule wrote:

My idea was a little bit different. Inside postinit initialize
some global variables with info if there are event triggers or
not. And later you can use this variable to start transactions
and  other things.

There will be two access to pg_event_trigger, but it can
eliminate useless and probably more expensive start_transaction
and end_transaction.

Do you mean some variable in shared memory or GUCs?
It was my first idea - to use some flag in shared memory to make
it possible fast check that there are not event triggers.
But this flag should be sent per database. May be I missed
something, but there is no any per-database shared memory data
structure in Postgres.
Certainly it is possible to organize some hash db->event_info, but
it makes this patch several times more complex.

My idea was a little bit different - just inside process
initialization, checking existence of event triggers, and later when
it is necessary to start a transaction for trigger execution. This
should to reduce useless empty transaction,

I am sure this is a problem on computers with slower CPU s, although I
have I7, but because this feature is usually unused, then the
performance impact should be minimal every time.

Sorry, may be I missed something. But now I completely confused with
your idea.
Right now processing of login hooks is done in PostgresMain.
In the previous mail you have suggested to did it in InitPostgres which
is invoked from PostgresMain.
So what is the difference (except there open transaction in InitPostgres)?

So what the problem we are trying to solve now?
As far as I understand, your concern is about extra overhead during
backend startup needed to check if there on-login triggers defined.
We are not speaking about trigger execution. We mostly worry about
applications which are not using triggers at all. But still have to pay
some small extra overhead at startup.
This overhead seems to be negligible (1% for dummy connection doing just
"select 1"). But taken in account that 99.9999% applications will not
use connection triggers,
even very small overhead seems to be not good.

But what can we do?
We do not want to scan pg_event_trigger table on backend startup. But
how else we can skip this check, taken in account that
1. Such trigger can be registered at any moment of time
2. Triggers are registered per database, so we can not have just global
flag,signaling lack of event triggers.

The only solution I see at this moment is <db,has_event_triggers> hash
in shared memory.
But it seems to be overkill from my point of view.

This is why I suggested to have disable_login_event_triggers GUC set to
true by default.

From my point of view it is better to have separate GUC disabling
just client connection events and switch it on by default.
So only those who need this events with switch it on, other users
will not pay any extra cost for it.

It can work, but this design is not user friendly.  The significant
bottleneck should be forking new processes, and check of content some
usually very small tables should be invisible. So if there is a
possible way to implement some feature that can be enabled by default,
then we should go this way. It can be pretty unfriendly if somebody
writes a logon trigger and it will not work by default without any
warning.

Please notice that event triggers are disabled by default.
You need to call "alter event trigger xxx enable always".
May be instead of GUCs we should somehow use ALTER mechanism?
But I do not understand why it is better and how it will help to solve
this problem (elimination of exrta overhead when there are not triggers).

When I tested last patch I found a problem (I have disabled assertions
due performance testing)

create role xx login;
alter system set disable_event_triggers to on; -- better should be
positive form - enable_event_triggers to on, off
select pg_reload_conf();

psql -U xx

Thread 2.1 "postmaster" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7f2bff438ec0 (LWP 827497)]
ResourceOwnerEnlargeCatCacheRefs (owner=0x0) at resowner.c:1025
1025 ResourceArrayEnlarge(&(owner->catrefarr));
(gdb) bt
#0  ResourceOwnerEnlargeCatCacheRefs (owner=0x0) at resowner.c:1025
#1  0x00000000008a70f8 in SearchCatCacheInternal (cache=<optimized
out>, nkeys=nkeys@entry=1, v1=v1@entry=13836, v2=v2@entry=0,
    v3=v3@entry=0, v4=v4@entry=0) at catcache.c:1273
#2  0x00000000008a7575 in SearchCatCache1 (cache=<optimized out>,
v1=v1@entry=13836) at catcache.c:1167
#3  0x00000000008b7f80 in SearchSysCache1 (cacheId=cacheId@entry=21,
key1=key1@entry=13836) at syscache.c:1122
#4  0x00000000005939cd in pg_database_ownercheck (db_oid=13836,
roleid=16387) at aclchk.c:5114
#5  0x0000000000605b42 in EventTriggersDisabled () at event_trigger.c:650
#6  EventTriggerOnConnect () at event_trigger.c:839
#7  0x00000000007b46d7 in PostgresMain (argc=argc@entry=1,
argv=argv@entry=0x7ffdd6256080, dbname=<optimized out>,
    username=0xf82508 "tom") at postgres.c:4021
#8  0x0000000000741afd in BackendRun (port=<optimized out>,
port=<optimized out>) at postmaster.c:4490
#9  BackendStartup (port=<optimized out>) at postmaster.c:4212
#10 ServerLoop () at postmaster.c:1729
#11 0x0000000000742881 in PostmasterMain (argc=argc@entry=3,
argv=argv@entry=0xf44d00) at postmaster.c:1401
#12 0x00000000004f1ff7 in main (argc=3, argv=0xf44d00) at main.c:209

pg_database_ownercheck can not be called outside transaction.
I can split replace call of EventTriggersDisabled in
EventTriggerOnConnect with two separate checks:
disable_event_triggers - before start of transaction
and pg_database_ownercheck(MyDatabaseId, GetUserId()) inside transaction.

But I wonder if we need to check database ownership in this place at all?
May be just allow to alter disable_event_triggers for superusers?

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#26Konstantin Knizhnik
k.knizhnik@postgrespro.ru
In reply to: Pavel Stehule (#16)
Re: On login trigger: take three

On 09.12.2020 15:24, Pavel Stehule wrote:

st 9. 12. 2020 v 13:17 odesílatel Greg Nancarrow <gregn4422@gmail.com
<mailto:gregn4422@gmail.com>> napsal:

On Tue, Dec 8, 2020 at 3:26 PM Pavel Stehule
<pavel.stehule@gmail.com <mailto:pavel.stehule@gmail.com>> wrote:

There are two maybe generic questions?

1. Maybe we can introduce more generic GUC for all event

triggers like disable_event_triggers? This GUC can be checked only
by the database owner or super user. It can be an alternative
ALTER TABLE DISABLE TRIGGER ALL. It can be protection against
necessity to restart to single mode to repair the event trigger. I
think so more generic solution is better than special
disable_client_connection_trigger GUC.

2. I have no objection against client_connection. It is probably

better for the mentioned purpose - possibility to block connection
to database. Can be interesting, and I am not sure how much work
it is to introduce the second event - session_start. This event
should be started after connecting - so the exception there
doesn't block connect, and should be started also after the new
statement "DISCARD SESSION", that will be started automatically
after DISCARD ALL.  This feature should not be implemented in
first step, but it can be a plan for support pooled connections

PGC_SU_BACKEND is too strong, there should be PGC_BACKEND if this
option can be used by database owner

Pavel

I've created a separate patch to address question (1), rather than
include it in the main patch, which I've adjusted accordingly. I'll
leave question (2) until another time, as you suggest.
See the attached patches.

Regards,
Greg Nancarrow
Fujitsu Australia

It seems to me that current implementation of EventTriggersDisabled:

 /*
 * Indicates whether all event triggers are currently disabled
 * for the current user.
 * Event triggers are disabled when configuration parameter
 * "disable_event_triggers" is true, and the current user
 * is the database owner or has superuser privileges.
 */
static inline bool
EventTriggersDisabled(void)
{
    return (disable_event_triggers &&
pg_database_ownercheck(MyDatabaseId, GetUserId()));
}

is not correct. It makes it not possible to superuser to disable
triggers for all users.
Also GUCs are not associated with any database. So I do not understand
why  this check of database ownership is relevant at all?

What kind of protection violation we want to prevent?

It seems to be obvious that normal user should not be able to prevent
trigger execution because this triggers may be used to enforce some
security policies.
If trigger was created by user itself, then it can drop or disable it
using ALTER statement. GUC is not needed for it.

So I think that EventTriggersDisabled function is not needed and can be
replaced just with disable_event_triggers GUC check.
And this option should be defined with PGC_SU_BACKEND

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#27Pavel Stehule
pavel.stehule@gmail.com
In reply to: Konstantin Knizhnik (#26)
Re: On login trigger: take three

pá 11. 12. 2020 v 16:07 odesílatel Konstantin Knizhnik <
k.knizhnik@postgrespro.ru> napsal:

On 09.12.2020 15:24, Pavel Stehule wrote:

st 9. 12. 2020 v 13:17 odesílatel Greg Nancarrow <gregn4422@gmail.com>
napsal:

On Tue, Dec 8, 2020 at 3:26 PM Pavel Stehule <pavel.stehule@gmail.com>
wrote:

There are two maybe generic questions?

1. Maybe we can introduce more generic GUC for all event triggers like

disable_event_triggers? This GUC can be checked only by the database owner
or super user. It can be an alternative ALTER TABLE DISABLE TRIGGER ALL. It
can be protection against necessity to restart to single mode to repair the
event trigger. I think so more generic solution is better than special
disable_client_connection_trigger GUC.

2. I have no objection against client_connection. It is probably better

for the mentioned purpose - possibility to block connection to database.
Can be interesting, and I am not sure how much work it is to introduce the
second event - session_start. This event should be started after connecting
- so the exception there doesn't block connect, and should be started also
after the new statement "DISCARD SESSION", that will be started
automatically after DISCARD ALL. This feature should not be implemented in
first step, but it can be a plan for support pooled connections

PGC_SU_BACKEND is too strong, there should be PGC_BACKEND if this option
can be used by database owner

Pavel

I've created a separate patch to address question (1), rather than
include it in the main patch, which I've adjusted accordingly. I'll
leave question (2) until another time, as you suggest.
See the attached patches.

Regards,
Greg Nancarrow
Fujitsu Australia

It seems to me that current implementation of EventTriggersDisabled:

/*
* Indicates whether all event triggers are currently disabled
* for the current user.
* Event triggers are disabled when configuration parameter
* "disable_event_triggers" is true, and the current user
* is the database owner or has superuser privileges.
*/
static inline bool
EventTriggersDisabled(void)
{
return (disable_event_triggers && pg_database_ownercheck(MyDatabaseId,
GetUserId()));
}

is not correct. It makes it not possible to superuser to disable triggers
for all users.

pg_database_ownercheck returns true for superuser always.

Also GUCs are not associated with any database. So I do not understand why

this check of database ownership is relevant at all?

What kind of protection violation we want to prevent?

It seems to be obvious that normal user should not be able to prevent
trigger execution because this triggers may be used to enforce some
security policies.
If trigger was created by user itself, then it can drop or disable it
using ALTER statement. GUC is not needed for it.

when you cannot connect to the database, then you cannot do ALTER. In DBaaS
environments lot of users has not superuser rights.

Show quoted text

So I think that EventTriggersDisabled function is not needed and can be
replaced just with disable_event_triggers GUC check.
And this option should be defined with PGC_SU_BACKEND

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#28Konstantin Knizhnik
k.knizhnik@postgrespro.ru
In reply to: Pavel Stehule (#27)
Re: On login trigger: take three

On 11.12.2020 18:40, Pavel Stehule wrote:

is not correct. It makes it not possible to superuser to disable
triggers for all users.

pg_database_ownercheck returns true for superuser always.

Sorry, but I consider different case: when normal user is connected to
the database.
In this case pg_database_ownercheck returns false and trigger is not
disabled, isn't it?

Also GUCs are not associated with any database. So I do not
understand why  this check of database ownership is relevant at all?

What kind of protection violation we want to prevent?

It seems to be obvious that normal user should not be able to
prevent trigger execution because this triggers may be used to
enforce some security policies.
If trigger was created by user itself, then it can drop or disable
it using ALTER statement. GUC is not needed for it.

when you cannot connect to the database, then you cannot do ALTER. In
DBaaS environments lot of users has not superuser rights.

But only superusers can set login triggers, right?
So only superuser can make a mistake in this trigger. But he have enough
rights to recover this error. Normal users are not able to define on
connection triggers and
should not have rights to disable them.

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#29Pavel Stehule
pavel.stehule@gmail.com
In reply to: Konstantin Knizhnik (#28)
Re: On login trigger: take three

pá 11. 12. 2020 v 17:05 odesílatel Konstantin Knizhnik <
k.knizhnik@postgrespro.ru> napsal:

On 11.12.2020 18:40, Pavel Stehule wrote:

is not correct. It makes it not possible to superuser to disable triggers

for all users.

pg_database_ownercheck returns true for superuser always.

Sorry, but I consider different case: when normal user is connected to the
database.
In this case pg_database_ownercheck returns false and trigger is not
disabled, isn't it?

My idea was to reduce necessary rights to database owners. But you have a
true, so only superuser can create event trigger, so this feature cannot be
used in DBaaS environments, and then my original idea was wrong.

Also GUCs are not associated with any database. So I do not understand

why this check of database ownership is relevant at all?

What kind of protection violation we want to prevent?

It seems to be obvious that normal user should not be able to prevent
trigger execution because this triggers may be used to enforce some
security policies.
If trigger was created by user itself, then it can drop or disable it
using ALTER statement. GUC is not needed for it.

when you cannot connect to the database, then you cannot do ALTER. In
DBaaS environments lot of users has not superuser rights.

But only superusers can set login triggers, right?
So only superuser can make a mistake in this trigger. But he have enough
rights to recover this error. Normal users are not able to define on
connection triggers and
should not have rights to disable them.

yes, it is true

Pavel

Show quoted text

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#30Konstantin Knizhnik
k.knizhnik@postgrespro.ru
In reply to: Pavel Stehule (#29)
Re: On login trigger: take three

On 11.12.2020 19:27, Pavel Stehule wrote:

pá 11. 12. 2020 v 17:05 odesílatel Konstantin Knizhnik
<k.knizhnik@postgrespro.ru <mailto:k.knizhnik@postgrespro.ru>> napsal:

On 11.12.2020 18:40, Pavel Stehule wrote:

is not correct. It makes it not possible to superuser to
disable triggers for all users.

pg_database_ownercheck returns true for superuser always.

Sorry, but I consider different case: when normal user is
connected to the database.
In this case pg_database_ownercheck returns false and trigger is
not disabled, isn't it?

My idea was to reduce necessary rights to database owners.  But you
have a true, so only superuser can create event trigger, so this
feature cannot be used in DBaaS environments, and then my original
idea was wrong.

Also GUCs are not associated with any database. So I do not
understand why  this check of database ownership is relevant
at all?

What kind of protection violation we want to prevent?

It seems to be obvious that normal user should not be able to
prevent trigger execution because this triggers may be used
to enforce some security policies.
If trigger was created by user itself, then it can drop or
disable it using ALTER statement. GUC is not needed for it.

when you cannot connect to the database, then you cannot do
ALTER. In DBaaS environments lot of users has not superuser rights.

But only superusers can set login triggers, right?
So only superuser can make a mistake in this trigger. But he have
enough rights to recover this error. Normal users are not able to
define on connection triggers and
should not have rights to disable them.

yes, it is true

Pavel

--
Konstantin Knizhnik
Postgres Professional:http://www.postgrespro.com
The Russian Postgres Company

So what's next?
I see three options:

1. Do not introduce GUC for disabling all event triggers (especially
taken in account that them are  disabled by default).
Return back to the patch
on_connect_event_trigger_WITH_SUGGESTED_UPDATES.patch with
"disable_client_connection_trigger"
and make it true by default (to eliminate any overhead for users which
are not using on logintriggers).

2. Have two GUCS: "disable_client_connection_trigger" and
"disable_event_triggers".

3. Implement some mechanism for caching presence of event triggers in
shared memory.

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#31Pavel Stehule
pavel.stehule@gmail.com
In reply to: Konstantin Knizhnik (#30)
Re: On login trigger: take three

út 15. 12. 2020 v 14:12 odesílatel Konstantin Knizhnik <
k.knizhnik@postgrespro.ru> napsal:

On 11.12.2020 19:27, Pavel Stehule wrote:

pá 11. 12. 2020 v 17:05 odesílatel Konstantin Knizhnik <
k.knizhnik@postgrespro.ru> napsal:

On 11.12.2020 18:40, Pavel Stehule wrote:

is not correct. It makes it not possible to superuser to disable triggers

for all users.

pg_database_ownercheck returns true for superuser always.

Sorry, but I consider different case: when normal user is connected to
the database.
In this case pg_database_ownercheck returns false and trigger is not
disabled, isn't it?

My idea was to reduce necessary rights to database owners. But you have a
true, so only superuser can create event trigger, so this feature cannot be
used in DBaaS environments, and then my original idea was wrong.

Also GUCs are not associated with any database. So I do not understand

why this check of database ownership is relevant at all?

What kind of protection violation we want to prevent?

It seems to be obvious that normal user should not be able to prevent
trigger execution because this triggers may be used to enforce some
security policies.
If trigger was created by user itself, then it can drop or disable it
using ALTER statement. GUC is not needed for it.

when you cannot connect to the database, then you cannot do ALTER. In
DBaaS environments lot of users has not superuser rights.

But only superusers can set login triggers, right?
So only superuser can make a mistake in this trigger. But he have enough
rights to recover this error. Normal users are not able to define on
connection triggers and
should not have rights to disable them.

yes, it is true

Pavel

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

So what's next?
I see three options:

1. Do not introduce GUC for disabling all event triggers (especially taken
in account that them are disabled by default).
Return back to the patch
on_connect_event_trigger_WITH_SUGGESTED_UPDATES.patch with
"disable_client_connection_trigger"
and make it true by default (to eliminate any overhead for users which are
not using on logintriggers).

2. Have two GUCS: "disable_client_connection_trigger" and
"disable_event_triggers".

3. Implement some mechanism for caching presence of event triggers in
shared memory.

@3 is the best design (the things should work well by default), @2 is a
little bit chaotic and @1 looks like a workaround.

Regards

Pavel

Show quoted text

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#32Konstantin Knizhnik
k.knizhnik@postgrespro.ru
In reply to: Pavel Stehule (#31)
Re: On login trigger: take three

On 15.12.2020 16:18, Pavel Stehule wrote:

út 15. 12. 2020 v 14:12 odesílatel Konstantin Knizhnik
<k.knizhnik@postgrespro.ru <mailto:k.knizhnik@postgrespro.ru>> napsal:

On 11.12.2020 19:27, Pavel Stehule wrote:

pá 11. 12. 2020 v 17:05 odesílatel Konstantin Knizhnik
<k.knizhnik@postgrespro.ru <mailto:k.knizhnik@postgrespro.ru>>
napsal:

On 11.12.2020 18:40, Pavel Stehule wrote:

is not correct. It makes it not possible to superuser to
disable triggers for all users.

pg_database_ownercheck returns true for superuser always.

Sorry, but I consider different case: when normal user is
connected to the database.
In this case pg_database_ownercheck returns false and trigger
is not disabled, isn't it?

My idea was to reduce necessary rights to database owners.  But
you have a true, so only superuser can create event trigger, so
this feature cannot be used in DBaaS environments, and then my
original idea was wrong.

Also GUCs are not associated with any database. So I do
not understand why  this check of database ownership is
relevant at all?

What kind of protection violation we want to prevent?

It seems to be obvious that normal user should not be
able to prevent trigger execution because this triggers
may be used to enforce some security policies.
If trigger was created by user itself, then it can drop
or disable it using ALTER statement. GUC is not needed
for it.

when you cannot connect to the database, then you cannot do
ALTER. In DBaaS environments lot of users has not superuser
rights.

But only superusers can set login triggers, right?
So only superuser can make a mistake in this trigger. But he
have enough rights to recover this error. Normal users are
not able to define on connection triggers and
should not have rights to disable them.

yes, it is true

Pavel

--
Konstantin Knizhnik
Postgres Professional:http://www.postgrespro.com
The Russian Postgres Company

So what's next?
I see three options:

1. Do not introduce GUC for disabling all event triggers
(especially taken in account that them are  disabled by default).
Return back to the patch
on_connect_event_trigger_WITH_SUGGESTED_UPDATES.patch with
"disable_client_connection_trigger"
and make it true by default (to eliminate any overhead for users
which are not using on logintriggers).

2. Have two GUCS: "disable_client_connection_trigger" and
"disable_event_triggers".

3. Implement some mechanism for caching presence of event triggers
in shared memory.

@3 is the best design (the things should work well by default), @2 is
a little bit chaotic and @1 looks like a workaround.

Please notice that we still need GUC to disable on-login triggers: to
make it possible for superuser who did mistake and defined incorrect
on-login trigger to
login to the system.
Do we need GUC to disable all other event triggers? May be I am wrong,
but I do not see much need in such GUC: error in any of such event
triggers is non fatal
and can be easily reverted.
So the only question is whether "disable_client_connection_trigger"
should be true by default or not...

I agree with you that @2 is a little bit chaotic and @1 looks like a
workaround.
But from my point of view @3 is not the best solution but overkill:
maintaining yet another shared hash just to save few milliseconds on
login seems to be too high price.
Actually there are many things which are loaded by new backend from the
database on start: for example - catalog.
This is why launch of new backend is an expensive operation.
Certainly if we execute "select 1", then system catalog is not needed...
But does anybody start new backend just to execute "select 1" and exit?

Regards

Pavel

--
Konstantin Knizhnik
Postgres Professional:http://www.postgrespro.com
The Russian Postgres Company

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#33Pavel Stehule
pavel.stehule@gmail.com
In reply to: Konstantin Knizhnik (#32)
Re: On login trigger: take three

út 15. 12. 2020 v 15:06 odesílatel Konstantin Knizhnik <
k.knizhnik@postgrespro.ru> napsal:

On 15.12.2020 16:18, Pavel Stehule wrote:

út 15. 12. 2020 v 14:12 odesílatel Konstantin Knizhnik <
k.knizhnik@postgrespro.ru> napsal:

On 11.12.2020 19:27, Pavel Stehule wrote:

pá 11. 12. 2020 v 17:05 odesílatel Konstantin Knizhnik <
k.knizhnik@postgrespro.ru> napsal:

On 11.12.2020 18:40, Pavel Stehule wrote:

is not correct. It makes it not possible to superuser to disable

triggers for all users.

pg_database_ownercheck returns true for superuser always.

Sorry, but I consider different case: when normal user is connected to
the database.
In this case pg_database_ownercheck returns false and trigger is not
disabled, isn't it?

My idea was to reduce necessary rights to database owners. But you have
a true, so only superuser can create event trigger, so this feature cannot
be used in DBaaS environments, and then my original idea was wrong.

Also GUCs are not associated with any database. So I do not understand

why this check of database ownership is relevant at all?

What kind of protection violation we want to prevent?

It seems to be obvious that normal user should not be able to prevent
trigger execution because this triggers may be used to enforce some
security policies.
If trigger was created by user itself, then it can drop or disable it
using ALTER statement. GUC is not needed for it.

when you cannot connect to the database, then you cannot do ALTER. In
DBaaS environments lot of users has not superuser rights.

But only superusers can set login triggers, right?
So only superuser can make a mistake in this trigger. But he have enough
rights to recover this error. Normal users are not able to define on
connection triggers and
should not have rights to disable them.

yes, it is true

Pavel

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

So what's next?
I see three options:

1. Do not introduce GUC for disabling all event triggers (especially
taken in account that them are disabled by default).
Return back to the patch
on_connect_event_trigger_WITH_SUGGESTED_UPDATES.patch with
"disable_client_connection_trigger"
and make it true by default (to eliminate any overhead for users which
are not using on logintriggers).

2. Have two GUCS: "disable_client_connection_trigger" and
"disable_event_triggers".

3. Implement some mechanism for caching presence of event triggers in
shared memory.

@3 is the best design (the things should work well by default), @2 is a
little bit chaotic and @1 looks like a workaround.

Please notice that we still need GUC to disable on-login triggers: to make
it possible for superuser who did mistake and defined incorrect on-login
trigger to
login to the system.
Do we need GUC to disable all other event triggers? May be I am wrong, but
I do not see much need in such GUC: error in any of such event triggers is
non fatal
and can be easily reverted.
So the only question is whether "disable_client_connection_trigger" should
be true by default or not...

I agree with you that @2 is a little bit chaotic and @1 looks like a
workaround.
But from my point of view @3 is not the best solution but overkill:
maintaining yet another shared hash just to save few milliseconds on login
seems to be too high price.
Actually there are many things which are loaded by new backend from the
database on start: for example - catalog.
This is why launch of new backend is an expensive operation.
Certainly if we execute "select 1", then system catalog is not needed...
But does anybody start new backend just to execute "select 1" and exit?

This is the worst case but tested on a not too bad CPU (i7). In virtual
environments with overloaded CPU the effect can be worse.

Show quoted text

Regards

Pavel

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#34Pavel Stehule
pavel.stehule@gmail.com
In reply to: Konstantin Knizhnik (#32)
Re: On login trigger: take three

út 15. 12. 2020 v 15:06 odesílatel Konstantin Knizhnik <
k.knizhnik@postgrespro.ru> napsal:

On 15.12.2020 16:18, Pavel Stehule wrote:

út 15. 12. 2020 v 14:12 odesílatel Konstantin Knizhnik <
k.knizhnik@postgrespro.ru> napsal:

On 11.12.2020 19:27, Pavel Stehule wrote:

pá 11. 12. 2020 v 17:05 odesílatel Konstantin Knizhnik <
k.knizhnik@postgrespro.ru> napsal:

On 11.12.2020 18:40, Pavel Stehule wrote:

is not correct. It makes it not possible to superuser to disable

triggers for all users.

pg_database_ownercheck returns true for superuser always.

Sorry, but I consider different case: when normal user is connected to
the database.
In this case pg_database_ownercheck returns false and trigger is not
disabled, isn't it?

My idea was to reduce necessary rights to database owners. But you have
a true, so only superuser can create event trigger, so this feature cannot
be used in DBaaS environments, and then my original idea was wrong.

Also GUCs are not associated with any database. So I do not understand

why this check of database ownership is relevant at all?

What kind of protection violation we want to prevent?

It seems to be obvious that normal user should not be able to prevent
trigger execution because this triggers may be used to enforce some
security policies.
If trigger was created by user itself, then it can drop or disable it
using ALTER statement. GUC is not needed for it.

when you cannot connect to the database, then you cannot do ALTER. In
DBaaS environments lot of users has not superuser rights.

But only superusers can set login triggers, right?
So only superuser can make a mistake in this trigger. But he have enough
rights to recover this error. Normal users are not able to define on
connection triggers and
should not have rights to disable them.

yes, it is true

Pavel

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

So what's next?
I see three options:

1. Do not introduce GUC for disabling all event triggers (especially
taken in account that them are disabled by default).
Return back to the patch
on_connect_event_trigger_WITH_SUGGESTED_UPDATES.patch with
"disable_client_connection_trigger"
and make it true by default (to eliminate any overhead for users which
are not using on logintriggers).

2. Have two GUCS: "disable_client_connection_trigger" and
"disable_event_triggers".

3. Implement some mechanism for caching presence of event triggers in
shared memory.

@3 is the best design (the things should work well by default), @2 is a
little bit chaotic and @1 looks like a workaround.

Please notice that we still need GUC to disable on-login triggers: to make
it possible for superuser who did mistake and defined incorrect on-login
trigger to
login to the system.
Do we need GUC to disable all other event triggers? May be I am wrong, but
I do not see much need in such GUC: error in any of such event triggers is
non fatal
and can be easily reverted.
So the only question is whether "disable_client_connection_trigger" should
be true by default or not...

I agree with you that @2 is a little bit chaotic and @1 looks like a
workaround.
But from my point of view @3 is not the best solution but overkill:
maintaining yet another shared hash just to save few milliseconds on login
seems to be too high price.
Actually there are many things which are loaded by new backend from the
database on start: for example - catalog.
This is why launch of new backend is an expensive operation.
Certainly if we execute "select 1", then system catalog is not needed...
But does anybody start new backend just to execute "select 1" and exit?

I understand so the implementation of a new shared cache can be a lot of
work. The best way is enhancing pg_database about one column with
information about the login triggers (dathaslogontriggers). In init time
these data are in syscache, and can be easily checked. Some like
pg_attribute have an atthasdef column. Same fields has pg_class -
relhasrules, relhastriggers, ... Then the overhead of this design should be
really zero.

What do you think about it?

Pavel

Show quoted text

Regards

Pavel

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#35Konstantin Knizhnik
k.knizhnik@postgrespro.ru
In reply to: Pavel Stehule (#34)
Re: On login trigger: take three

On 15.12.2020 18:25, Pavel Stehule wrote:

út 15. 12. 2020 v 15:06 odesílatel Konstantin Knizhnik
<k.knizhnik@postgrespro.ru <mailto:k.knizhnik@postgrespro.ru>> napsal:

On 15.12.2020 16:18, Pavel Stehule wrote:

út 15. 12. 2020 v 14:12 odesílatel Konstantin Knizhnik
<k.knizhnik@postgrespro.ru <mailto:k.knizhnik@postgrespro.ru>>
napsal:

On 11.12.2020 19:27, Pavel Stehule wrote:

pá 11. 12. 2020 v 17:05 odesílatel Konstantin Knizhnik
<k.knizhnik@postgrespro.ru
<mailto:k.knizhnik@postgrespro.ru>> napsal:

On 11.12.2020 18:40, Pavel Stehule wrote:

is not correct. It makes it not possible to
superuser to disable triggers for all users.

pg_database_ownercheck returns true for superuser always.

Sorry, but I consider different case: when normal user
is connected to the database.
In this case pg_database_ownercheck returns false and
trigger is not disabled, isn't it?

My idea was to reduce necessary rights to database owners. 
But you have a true, so only superuser can create event
trigger, so this feature cannot be used in DBaaS
environments, and then my original idea was wrong.

Also GUCs are not associated with any database. So
I do not understand why  this check of database
ownership is relevant at all?

What kind of protection violation we want to prevent?

It seems to be obvious that normal user should not
be able to prevent trigger execution because this
triggers may be used to enforce some security policies.
If trigger was created by user itself, then it can
drop or disable it using ALTER statement. GUC is
not needed for it.

when you cannot connect to the database, then you
cannot do ALTER. In DBaaS environments lot of users has
not superuser rights.

But only superusers can set login triggers, right?
So only superuser can make a mistake in this trigger.
But he have enough rights to recover this error. Normal
users are not able to define on connection triggers and
should not have rights to disable them.

yes, it is true

Pavel

--
Konstantin Knizhnik
Postgres Professional:http://www.postgrespro.com
The Russian Postgres Company

So what's next?
I see three options:

1. Do not introduce GUC for disabling all event triggers
(especially taken in account that them are  disabled by default).
Return back to the patch
on_connect_event_trigger_WITH_SUGGESTED_UPDATES.patch with
"disable_client_connection_trigger"
and make it true by default (to eliminate any overhead for
users which are not using on logintriggers).

2. Have two GUCS: "disable_client_connection_trigger" and
"disable_event_triggers".

3. Implement some mechanism for caching presence of event
triggers in shared memory.

@3 is the best design (the things should work well by default),
@2 is a little bit chaotic and @1 looks like a workaround.

Please notice that we still need GUC to disable on-login triggers:
to make it possible for superuser who did mistake and defined
incorrect on-login trigger to
login to the system.
Do we need GUC to disable all other event triggers? May be I am
wrong, but I do not see much need in such GUC: error in any of
such event triggers is non fatal
and can be easily reverted.
So the only question is whether
"disable_client_connection_trigger" should be true by default or
not...

I agree with you that @2 is a little bit chaotic and @1 looks like
a workaround.
But from my point of view @3 is not the best solution but
overkill: maintaining yet another shared hash just to save few
milliseconds on login seems to be too high price.
Actually there are many things which are loaded by new backend
from the database on start: for example - catalog.
This is why launch of new backend is an expensive operation.
Certainly if we execute "select 1", then system catalog is not
needed...
But does anybody start new backend just to execute "select 1" and
exit?

I understand so the implementation of a new shared cache can be a lot
of work. The best way is enhancing pg_database about one column with
information about the login triggers (dathaslogontriggers). In init
time these data are in syscache, and can be easily checked. Some like
pg_attribute have an atthasdef column.  Same fields has pg_class -
relhasrules, relhastriggers, ... Then the overhead of this design
should be really zero.

What do you think about it?

I like this approach more than implementation of shared hash.
But still I have some concerns:

1. pg_database table format has to be changed. Certainly it is not
something  completely unacceptable. But IMHO we should try to avoid
modification of such commonly used catalog tables as much as possible.

2. It is not so easy to maintain this flag. There can be multiple
on-login triggers defined. If such trigger is dropped, we can not just
clear this flag.
We should check if other triggers exist. Now assume that there are two
triggers and two concurrent transactions dropping each one.
According to their snapshots them do not see changes made by other
transaction. So them remove both triggers but didn't clear the flag.
Certainly we can use counter instead of flag. But I am not sure that
their will be not other problems with maintaining counter.

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#36Pavel Stehule
pavel.stehule@gmail.com
In reply to: Konstantin Knizhnik (#35)
Re: On login trigger: take three

Please notice that we still need GUC to disable on-login triggers: to make

it possible for superuser who did mistake and defined incorrect on-login
trigger to
login to the system.
Do we need GUC to disable all other event triggers? May be I am wrong,
but I do not see much need in such GUC: error in any of such event triggers
is non fatal
and can be easily reverted.
So the only question is whether "disable_client_connection_trigger"
should be true by default or not...

I agree with you that @2 is a little bit chaotic and @1 looks like a
workaround.
But from my point of view @3 is not the best solution but overkill:
maintaining yet another shared hash just to save few milliseconds on login
seems to be too high price.
Actually there are many things which are loaded by new backend from the
database on start: for example - catalog.
This is why launch of new backend is an expensive operation.
Certainly if we execute "select 1", then system catalog is not needed...
But does anybody start new backend just to execute "select 1" and exit?

I understand so the implementation of a new shared cache can be a lot of
work. The best way is enhancing pg_database about one column with
information about the login triggers (dathaslogontriggers). In init time
these data are in syscache, and can be easily checked. Some like
pg_attribute have an atthasdef column. Same fields has pg_class -
relhasrules, relhastriggers, ... Then the overhead of this design should be
really zero.

What do you think about it?

I like this approach more than implementation of shared hash.
But still I have some concerns:

1. pg_database table format has to be changed. Certainly it is not
something completely unacceptable. But IMHO we should try to avoid
modification of such commonly used catalog tables as much as possible.

yes, I know. Unfortunately I don't see any other and correct solution.
There should be more wide discussion before this work about this topic. On
second hand, this change should not break anything (if this new field will
be placed on as the last field). The logon trigger really looks like a
database trigger - so I think so this semantic is correct. I have no idea
if it is acceptable for committers :-/. I hope so.

The fact that the existence of a logon trigger can be visible from a shared
database object can be an interesting feature too.

2. It is not so easy to maintain this flag. There can be multiple on-login
triggers defined. If such trigger is dropped, we can not just clear this
flag.
We should check if other triggers exist. Now assume that there are two
triggers and two concurrent transactions dropping each one.
According to their snapshots them do not see changes made by other
transaction. So them remove both triggers but didn't clear the flag.
Certainly we can use counter instead of flag. But I am not sure that their
will be not other problems with maintaining counter.

I don't think it is necessary. My opinion is not too strong, but if
pg_class doesn't need to hold a number of triggers, then I think so
pg_database doesn't need to hold this number too.

Show quoted text

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#37Konstantin Knizhnik
k.knizhnik@postgrespro.ru
In reply to: Pavel Stehule (#36)
1 attachment(s)
Re: On login trigger: take three

On 15.12.2020 20:16, Pavel Stehule wrote:

Please notice that we still need GUC to disable on-login
triggers: to make it possible for superuser who did mistake
and defined incorrect on-login trigger to
login to the system.
Do we need GUC to disable all other event triggers? May be I
am wrong, but I do not see much need in such GUC: error in
any of such event triggers is non fatal
and can be easily reverted.
So the only question is whether
"disable_client_connection_trigger" should be true by default
or not...

I agree with you that @2 is a little bit chaotic and @1 looks
like a workaround.
But from my point of view @3 is not the best solution but
overkill: maintaining yet another shared hash just to save
few milliseconds on login seems to be too high price.
Actually there are many things which are loaded by new
backend from the database on start: for example - catalog.
This is why launch of new backend is an expensive operation.
Certainly if we execute "select 1", then system catalog is
not needed...
But does anybody start new backend just to execute "select 1"
and exit?

I understand so the implementation of a new shared cache can be a
lot of work. The best way is enhancing pg_database about one
column with information about the login triggers
(dathaslogontriggers). In init time these data are in syscache,
and can be easily checked. Some like pg_attribute have an
atthasdef column.  Same fields has pg_class - relhasrules,
relhastriggers, ... Then the overhead of this design should be
really zero.

What do you think about it?

I like this approach more than implementation of shared hash.
But still I have some concerns:

1. pg_database table format has to be changed. Certainly it is not
something  completely unacceptable. But IMHO we should try to avoid
modification of such commonly used catalog tables as much as possible.

yes, I know. Unfortunately I  don't see any other and correct
solution. There should be more wide discussion before this work about
this topic. On second hand, this change should not break anything (if
this new field will be placed on as the last field). The logon trigger
really looks like a database trigger - so I think so this semantic is
correct. I have no idea if it is acceptable for committers :-/. I hope
so.

The fact that the existence of a logon trigger can be visible from a
shared database object can be an interesting feature too.

2. It is not so easy to maintain this flag. There can be multiple
on-login triggers defined. If such trigger is dropped, we can not
just clear this flag.
We should check if other triggers exist. Now assume that there are
two triggers and two concurrent transactions dropping each one.
According to their snapshots them do not see changes made by other
transaction. So them remove both triggers but didn't clear the flag.
Certainly we can use counter instead of flag. But I am not sure
that their will be not other problems with maintaining counter.

I don't think it is necessary.  My opinion is not too strong, but if
pg_class doesn't need to hold a number of triggers, then I think so
pg_database doesn't need to hold this number too.

Attached please find new versoin of the patch based on
on_connect_event_trigger_WITH_SUGGESTED_UPDATES.patch
So there is still only  "disable_client_connection_trigger" GUC? because
we need possibility to disable client connect triggers and there is no
such need for other event types.

As you suggested  have added "dathaslogontriggers" flag which is set
when client connection trigger is created.
This flag is never cleaned (to avoid visibility issues mentioned in my
previous mail).

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

Attachments:

on_connect_event_trigger-8.patchtext/x-patch; name=on_connect_event_trigger-8.patchDownload
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 60366a9..00f69b8 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>client_connection</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -36,6 +37,29 @@
    </para>
 
    <para>
+     The <literal>client_connection</literal> event occurs when a client connection
+     to the server is established.
+     There are two mechanisms for dealing with any bugs in a trigger procedure for
+     this event which might prevent successful login to the system:
+     <itemizedlist>
+      <listitem>
+       <para>
+         The configuration parameter <literal>disable_client_connection_trigger</literal>
+         makes it possible to disable firing the <literal>client_connection</literal>
+         trigger when a client connects.
+       </para>
+      </listitem>
+      <listitem>
+       <para>
+         Errors in the <literal>client_connection</literal> trigger procedure are
+         ignored for superuser. An error message is delivered to the client as
+         <literal>NOTICE</literal> in this case.
+       </para>
+      </listitem>
+     </itemizedlist>
+   </para>
+
+   <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
      <literal>SECURITY LABEL</literal>,
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index f27c3fe..8646db7 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -560,6 +560,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	new_record[Anum_pg_database_datctype - 1] =
 		DirectFunctionCall1(namein, CStringGetDatum(dbctype));
 	new_record[Anum_pg_database_datistemplate - 1] = BoolGetDatum(dbistemplate);
+	new_record[Anum_pg_database_dathaslogontriggers - 1] = BoolGetDatum(false);
 	new_record[Anum_pg_database_datallowconn - 1] = BoolGetDatum(dballowconnections);
 	new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 	new_record[Anum_pg_database_datlastsysoid - 1] = ObjectIdGetDatum(src_lastsysoid);
@@ -1627,7 +1628,7 @@ AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel)
 		new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 		new_record_repl[Anum_pg_database_datconnlimit - 1] = true;
 	}
-
+	new_record[Anum_pg_database_dathaslogontriggers - 1] = BoolGetDatum(datform->dathaslogontriggers);
 	newtuple = heap_modify_tuple(tuple, RelationGetDescr(rel), new_record,
 								 new_record_nulls, new_record_repl);
 	CatalogTupleUpdate(rel, &tuple->t_self, newtuple);
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 3ffba4e..39b42d6 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -48,6 +49,8 @@
 #include "utils/rel.h"
 #include "utils/syscache.h"
 
+bool disable_client_connection_trigger;
+
 typedef struct EventTriggerQueryState
 {
 	/* memory context for this state's objects */
@@ -130,6 +133,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "client_connection") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -293,6 +297,23 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
 	CatalogTupleInsert(tgrel, tuple);
 	heap_freetuple(tuple);
 
+	if (strcmp(eventname, "client_connection") == 0)
+	{
+		Form_pg_database db;
+		Relation pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+		/* Set dathaslogontriggers flag in pg_database */
+		tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+		db = (Form_pg_database) GETSTRUCT(tuple);
+		if (!db->dathaslogontriggers)
+		{
+			db->dathaslogontriggers= true;
+			CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+		}
+		table_close(pg_db, RowExclusiveLock);
+	}
+
 	/* Depend on owner. */
 	recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
 
@@ -562,6 +583,9 @@ EventTriggerCommonSetup(Node *parsetree,
 	ListCell   *lc;
 	List	   *runlist = NIL;
 
+	/* Get the command tag. */
+	tag = parsetree ? CreateCommandTag(parsetree) : CMDTAG_CONNECT;
+
 	/*
 	 * We want the list of command tags for which this procedure is actually
 	 * invoked to match up exactly with the list that CREATE EVENT TRIGGER
@@ -577,22 +601,18 @@ EventTriggerCommonSetup(Node *parsetree,
 	 * relevant command tag.
 	 */
 #ifdef USE_ASSERT_CHECKING
+	if (event == EVT_DDLCommandStart ||
+		event == EVT_DDLCommandEnd ||
+		event == EVT_SQLDrop ||
+		event == EVT_Connect)
 	{
-		CommandTag	dbgtag;
-
-		dbgtag = CreateCommandTag(parsetree);
-		if (event == EVT_DDLCommandStart ||
-			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
-		{
-			if (!command_tag_event_trigger_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
-		else if (event == EVT_TableRewrite)
-		{
-			if (!command_tag_table_rewrite_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
+		if (!command_tag_event_trigger_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
+	}
+	else if (event == EVT_TableRewrite)
+	{
+		if (!command_tag_table_rewrite_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
 	}
 #endif
 
@@ -601,9 +621,6 @@ EventTriggerCommonSetup(Node *parsetree,
 	if (cachelist == NIL)
 		return NIL;
 
-	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
-
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
 	 * memory context.  Once we start running the command triggers, or indeed
@@ -800,6 +817,85 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+static bool
+DatabaseHasLogonTriggers(void)
+{
+	bool has_logon_triggers;
+	HeapTuple tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+	has_logon_triggers = ((Form_pg_database) GETSTRUCT(tuple))->dathaslogontriggers;
+	ReleaseSysCache(tuple);
+	return has_logon_triggers;
+}
+
+/*
+ * Fire connect triggers.
+ */
+void
+EventTriggerOnConnect(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster
+		|| !OidIsValid(MyDatabaseId)
+		|| disable_client_connection_trigger)
+		return;
+
+	StartTransactionCommand();
+
+	if (DatabaseHasLogonTriggers())
+	{
+		runlist = EventTriggerCommonSetup(NULL,
+										  EVT_Connect, "connect",
+										  &trigdata);
+
+		if (runlist != NIL)
+		{
+			MemoryContext old_context = CurrentMemoryContext;
+			bool is_superuser = superuser();
+			/*
+			 * Make sure anything the main command did will be visible to the event
+			 * triggers.
+			 */
+			CommandCounterIncrement();
+
+			/* Run the triggers. */
+			PG_TRY();
+			{
+				EventTriggerInvoke(runlist, &trigdata);
+				list_free(runlist);
+			}
+			PG_CATCH();
+			{
+				ErrorData* error;
+				/*
+				 * Try to ignore error for superuser to make it possible to login even in case of errors
+				 * during trigger execution
+				 */
+				if (!is_superuser)
+					PG_RE_THROW();
+
+				MemoryContextSwitchTo(old_context);
+				error = CopyErrorData();
+				FlushErrorState();
+				elog(NOTICE, "client_connection trigger failed with message: %s", error->message);
+				AbortCurrentTransaction();
+				return;
+			}
+			PG_END_TRY();
+		}
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 3679799..ce9b98e 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -42,6 +42,7 @@
 #include "catalog/pg_type.h"
 #include "commands/async.h"
 #include "commands/prepare.h"
+#include "commands/event_trigger.h"
 #include "executor/spi.h"
 #include "jit/jit.h"
 #include "libpq/libpq.h"
@@ -167,6 +168,9 @@ static ProcSignalReason RecoveryConflictReason;
 static MemoryContext row_description_context = NULL;
 static StringInfoData row_description_buf;
 
+/* Hook for plugins to get control at start of session */
+client_connection_hook_type client_connection_hook = EventTriggerOnConnect;
+
 /* ----------------------------------------------------------------
  *		decls for routines only used in this file
  * ----------------------------------------------------------------
@@ -4012,6 +4016,11 @@ PostgresMain(int argc, char *argv[],
 	if (!IsUnderPostmaster)
 		PgStartTime = GetCurrentTimestamp();
 
+	if (client_connection_hook)
+	{
+		(*client_connection_hook) ();
+	}
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 0427795..c621b8f 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -168,6 +168,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "client_connection") == 0)
+			event = EVT_Connect;
 		else
 			continue;
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 245a347..5c7e339 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -43,6 +43,7 @@
 #include "commands/async.h"
 #include "commands/prepare.h"
 #include "commands/trigger.h"
+#include "commands/event_trigger.h"
 #include "commands/user.h"
 #include "commands/vacuum.h"
 #include "commands/variable.h"
@@ -928,6 +929,16 @@ static const unit_conversion time_unit_conversion_table[] =
 static struct config_bool ConfigureNamesBool[] =
 {
 	{
+		{"disable_client_connection_trigger", PGC_SU_BACKEND, DEVELOPER_OPTIONS,
+			gettext_noop("Disables the client_connection event trigger."),
+			gettext_noop("In case of errors in the ON client_connection EVENT TRIGGER procedure, this parameter can be used to disable trigger activation and provide access to the database."),
+			GUC_EXPLAIN
+		},
+		&disable_client_connection_trigger,
+		false,
+		NULL, NULL, NULL
+	},
+	{
 		{"enable_seqscan", PGC_USERSET, QUERY_TUNING_METHOD,
 			gettext_noop("Enables the planner's use of sequential-scan plans."),
 			NULL,
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index dc1d41d..a9a7c1f 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -2726,6 +2726,7 @@ dumpDatabase(Archive *fout)
 				i_datacl,
 				i_rdatacl,
 				i_datistemplate,
+				i_dathaslogontriggers,
 				i_datconnlimit,
 				i_tablespace;
 	CatalogId	dbCatId;
@@ -2738,6 +2739,7 @@ dumpDatabase(Archive *fout)
 			   *datacl,
 			   *rdatacl,
 			   *datistemplate,
+			   *dathaslogontriggers,
 			   *datconnlimit,
 			   *tablespace;
 	uint32		frozenxid,
@@ -2756,7 +2758,7 @@ dumpDatabase(Archive *fout)
 	 * (pg_init_privs) are not supported on databases, so this logic cannot
 	 * make use of buildACLQueries().
 	 */
-	if (fout->remoteVersion >= 90600)
+	if (fout->remoteVersion >= 140000)
 	{
 		appendPQExpBuffer(dbQry, "SELECT tableoid, oid, datname, "
 						  "(%s datdba) AS dba, "
@@ -2782,7 +2784,41 @@ dumpDatabase(Archive *fout)
 						  "       AS permp(orig_acl) "
 						  "     WHERE acl = orig_acl)) AS rdatacls) "
 						  " AS rdatacl, "
-						  "datistemplate, datconnlimit, "
+						  "datistemplate, datconnlimit, dathaslogontriggers, "
+						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
+						  "shobj_description(oid, 'pg_database') AS description "
+
+						  "FROM pg_database "
+						  "WHERE datname = current_database()",
+						  username_subquery);
+	}
+	else if (fout->remoteVersion >= 90600)
+	{
+		appendPQExpBuffer(dbQry, "SELECT tableoid, oid, datname, "
+						  "(%s datdba) AS dba, "
+						  "pg_encoding_to_char(encoding) AS encoding, "
+						  "datcollate, datctype, datfrozenxid, datminmxid, "
+						  "(SELECT array_agg(acl ORDER BY row_n) FROM "
+						  "  (SELECT acl, row_n FROM "
+						  "     unnest(coalesce(datacl,acldefault('d',datdba))) "
+						  "     WITH ORDINALITY AS perm(acl,row_n) "
+						  "   WHERE NOT EXISTS ( "
+						  "     SELECT 1 "
+						  "     FROM unnest(acldefault('d',datdba)) "
+						  "       AS init(init_acl) "
+						  "     WHERE acl = init_acl)) AS datacls) "
+						  " AS datacl, "
+						  "(SELECT array_agg(acl ORDER BY row_n) FROM "
+						  "  (SELECT acl, row_n FROM "
+						  "     unnest(acldefault('d',datdba)) "
+						  "     WITH ORDINALITY AS initp(acl,row_n) "
+						  "   WHERE NOT EXISTS ( "
+						  "     SELECT 1 "
+						  "     FROM unnest(coalesce(datacl,acldefault('d',datdba))) "
+						  "       AS permp(orig_acl) "
+						  "     WHERE acl = orig_acl)) AS rdatacls) "
+						  " AS rdatacl, "
+						  "datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2796,7 +2832,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "datcollate, datctype, datfrozenxid, datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, dathaslogontriggers"
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2810,7 +2846,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "datcollate, datctype, datfrozenxid, 0 AS datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2824,7 +2860,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "NULL AS datcollate, NULL AS datctype, datfrozenxid, 0 AS datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2839,7 +2875,7 @@ dumpDatabase(Archive *fout)
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "NULL AS datcollate, NULL AS datctype, datfrozenxid, 0 AS datminmxid, "
 						  "datacl, '' as rdatacl, datistemplate, "
-						  "-1 as datconnlimit, "
+						  "-1 as datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace "
 						  "FROM pg_database "
 						  "WHERE datname = current_database()",
@@ -2861,6 +2897,7 @@ dumpDatabase(Archive *fout)
 	i_rdatacl = PQfnumber(res, "rdatacl");
 	i_datistemplate = PQfnumber(res, "datistemplate");
 	i_datconnlimit = PQfnumber(res, "datconnlimit");
+	i_dathaslogontriggers = PQfnumber(res, "dathaslogontriggers");
 	i_tablespace = PQfnumber(res, "tablespace");
 
 	dbCatId.tableoid = atooid(PQgetvalue(res, 0, i_tableoid));
@@ -2875,6 +2912,7 @@ dumpDatabase(Archive *fout)
 	datacl = PQgetvalue(res, 0, i_datacl);
 	rdatacl = PQgetvalue(res, 0, i_rdatacl);
 	datistemplate = PQgetvalue(res, 0, i_datistemplate);
+	dathaslogontriggers = PQgetvalue(res, 0, i_dathaslogontriggers);
 	datconnlimit = PQgetvalue(res, 0, i_datconnlimit);
 	tablespace = PQgetvalue(res, 0, i_tablespace);
 
@@ -3048,6 +3086,14 @@ dumpDatabase(Archive *fout)
 		appendPQExpBufferStr(delQry, ";\n");
 	}
 
+	if (strcmp(dathaslogontriggers, "t") == 0)
+	{
+		appendPQExpBufferStr(creaQry, "UPDATE pg_catalog.pg_database "
+							 "SET dathaslogontriggers = true WHERE datname = ");
+		appendStringLiteralAH(creaQry, datname, fout);
+		appendPQExpBufferStr(creaQry, ";\n");
+	}
+
 	/* Add database-specific SET options */
 	dumpDatabaseConfig(fout, creaQry, datname, dbCatId.oid);
 
diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat
index 21cd660..4c10575 100644
--- a/src/include/catalog/pg_database.dat
+++ b/src/include/catalog/pg_database.dat
@@ -15,7 +15,7 @@
 { oid => '1', oid_symbol => 'TemplateDbOid',
   descr => 'default template for new databases',
   datname => 'template1', encoding => 'ENCODING', datcollate => 'LC_COLLATE',
-  datctype => 'LC_CTYPE', datistemplate => 't', datallowconn => 't',
+  datctype => 'LC_CTYPE', datistemplate => 't', dathaslogontriggers => 'f', datallowconn => 't',
   datconnlimit => '-1', datlastsysoid => '0', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datacl => '_null_' },
 
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index 47bcf40..7dc15a5 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -52,6 +52,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
 	/* new connections allowed? */
 	bool		datallowconn;
 
+	/* database has on-login triggers */
+	bool		dathaslogontriggers;
+
 	/* max connections allowed (-1=no limit) */
 	int32		datconnlimit;
 
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 407fd6a..d5e86af 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -21,6 +21,8 @@
 #include "tcop/deparse_utility.h"
 #include "utils/aclchk_internal.h"
 
+extern bool disable_client_connection_trigger; /* GUC */
+
 typedef struct EventTriggerData
 {
 	NodeTag		type;
@@ -53,6 +55,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnConnect(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index be94852..988aa39 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -80,6 +80,7 @@ PG_CMDTAG(CMDTAG_CLUSTER, "CLUSTER", false, false, false)
 PG_CMDTAG(CMDTAG_COMMENT, "COMMENT", true, false, false)
 PG_CMDTAG(CMDTAG_COMMIT, "COMMIT", false, false, false)
 PG_CMDTAG(CMDTAG_COMMIT_PREPARED, "COMMIT PREPARED", false, false, false)
+PG_CMDTAG(CMDTAG_CONNECT, "CONNECT", true, false, false)
 PG_CMDTAG(CMDTAG_COPY, "COPY", false, false, true)
 PG_CMDTAG(CMDTAG_COPY_FROM, "COPY FROM", false, false, false)
 PG_CMDTAG(CMDTAG_CREATE_ACCESS_METHOD, "CREATE ACCESS METHOD", true, false, false)
diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h
index bd30607..90376b2 100644
--- a/src/include/tcop/tcopprot.h
+++ b/src/include/tcop/tcopprot.h
@@ -30,6 +30,11 @@ extern PGDLLIMPORT const char *debug_query_string;
 extern int	max_stack_depth;
 extern int	PostAuthDelay;
 
+/* Hook for plugins to get control at start and end of session */
+typedef void (*client_connection_hook_type) (void);
+
+extern PGDLLIMPORT client_connection_hook_type client_connection_hook;
+
 /* GUC-configurable parameters */
 
 typedef enum
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index bb1e39e..ab7e8c3 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Connect,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/000_client_connection_trigger.pl b/src/test/recovery/t/000_client_connection_trigger.pl
new file mode 100644
index 0000000..3dcd475
--- /dev/null
+++ b/src/test/recovery/t/000_client_connection_trigger.pl
@@ -0,0 +1,69 @@
+use strict;
+use warnings;
+use PostgresNode;
+use TestLib;
+use Test::More;
+if (!$use_unix_sockets)
+{
+	plan skip_all =>
+	  "authentication tests cannot run without Unix-domain sockets";
+}
+else
+{
+	plan tests => 5;
+}
+
+# Initialize master node
+my $node = get_new_node('master');
+$node->init;
+$node->start;
+$node->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+CREATE ROLE regress_hacker LOGIN PASSWORD 'pass';
+
+CREATE TABLE connects(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO connects (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON client_connection EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+}
+);
+my $res;
+
+$res = $node->safe_psql('postgres', "SELECT 1");
+
+$res = $node->safe_psql('postgres', "SELECT 1",
+  extra_params => [ '-U', 'regress_user', '-w' ]);
+
+my ($ret, $stdout, $stderr) = $node->psql('postgres', "SELECT 1",
+  extra_params => [ '-U', 'regress_hacker', '-w' ]);
+ok( $ret != 0 && $stderr =~ /You are not welcome!/ );
+
+$res = $node->safe_psql('postgres', "SELECT COUNT(1) FROM connects WHERE who = 'regress_user'");
+ok($res == 1);
+
+my $tempdir = TestLib::tempdir;
+command_ok(
+  [ "pg_dumpall", '-p', $node->port, '-c', "--file=$tempdir/regression_dump.sql", ],
+  "dumpall");
+# my $dump_contents = slurp_file("$tempdir/regression_dump.sql");
+# print($dump_contents);
+
+my $node1 = get_new_node('secondary');
+$node1->init;
+$node1->start;
+command_ok(["psql", '-p', $node1->port, '-b', '-f', "$tempdir/regression_dump.sql" ] );
+$res = $node1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$res = $node1->safe_psql('postgres', "SELECT COUNT(1) FROM connects WHERE who = 'regress_user'");
+ok($res == 2);
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 9e31a53..b4a21fb 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -43,6 +43,27 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+
+CREATE TABLE connects(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO connects (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON client_connection EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 $node_primary->wait_for_catchup($node_standby_1, 'replay',
 	$node_primary->lsn('insert'));
@@ -266,6 +287,9 @@ sub replay_check
 
 replay_check();
 
+$node_standby_1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$node_standby_2->safe_psql('postgres', "SELECT 2", extra_params => [ '-U', 'regress_user', '-w' ]);
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index bdd0ffc..3ecbd6a 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -536,3 +536,40 @@ NOTICE:  DROP POLICY - ddl_command_end
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- On session start triggers
+create table connects(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into connects (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on client_connection execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id | who  
+----+------
+  1 | I am
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id | who  
+----+------
+  1 | I am
+  2 | I am
+(2 rows)
+
+-- Test handing exeptions in client_connection trigger
+drop table connects;
+-- superuser should ignore error
+\c
+NOTICE:  client_connection trigger failed with message: relation "connects" does not exist
+-- suppress trigger firing
+\c "dbname=regression options='-c disable_client_connection_trigger=true'"
+-- Cleanup
+drop event trigger on_login_trigger;
+drop function on_login_proc();
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 18b2a26..a1c5cf2 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -429,3 +429,31 @@ DROP POLICY p2 ON event_trigger_test;
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- On session start triggers
+create table connects(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into connects (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on client_connection execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+select * from connects;
+\c
+select * from connects;
+
+-- Test handing exeptions in client_connection trigger
+
+drop table connects;
+-- superuser should ignore error
+\c
+-- suppress trigger firing
+\c "dbname=regression options='-c disable_client_connection_trigger=true'"
+
+
+-- Cleanup
+drop event trigger on_login_trigger;
+drop function on_login_proc();
#38Pavel Stehule
pavel.stehule@gmail.com
In reply to: Konstantin Knizhnik (#37)
Re: On login trigger: take three

Attached please find new versoin of the patch based on
on_connect_event_trigger_WITH_SUGGESTED_UPDATES.patch

So there is still only "disable_client_connection_trigger" GUC? because
we need possibility to disable client connect triggers and there is no such
need for other event types.

As you suggested have added "dathaslogontriggers" flag which is set when
client connection trigger is created.
This flag is never cleaned (to avoid visibility issues mentioned in my
previous mail).

This is much better - I don't see any slowdown when logon trigger is not
defined

I did some benchmarks and looks so starting language handler is relatively
expensive - it is about 25% and starting handler like event trigger has
about 35%. But this problem can be solved later and elsewhere.

I prefer the inverse form of disable_connection_trigger. Almost all GUC are
in positive form - so enable_connection_triggger is better

I don't think so current handling dathaslogontriggers is good for
production usage. The protection against race condition can be solved by
lock on pg_event_trigger

Regards

Pavel

Show quoted text

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#39Zhihong Yu
zyu@yugabyte.com
In reply to: Pavel Stehule (#38)
Re: On login trigger: take three

Hi,
For EventTriggerOnConnect():

+           PG_CATCH();
+           {
...
+               AbortCurrentTransaction();
+               return;

Should runlist be freed in the catch block ?

+ gettext_noop("In case of errors in the ON client_connection
EVENT TRIGGER procedure, this parameter can be used to disable trigger
activation and provide access to the database."),

I think the text should be on two lines (current line too long).

Cheers

#40Pavel Stehule
pavel.stehule@gmail.com
In reply to: Pavel Stehule (#38)
Re: On login trigger: take three

st 16. 12. 2020 v 20:38 odesílatel Pavel Stehule <pavel.stehule@gmail.com>
napsal:

Attached please find new versoin of the patch based on
on_connect_event_trigger_WITH_SUGGESTED_UPDATES.patch

So there is still only "disable_client_connection_trigger" GUC? because
we need possibility to disable client connect triggers and there is no such
need for other event types.

As you suggested have added "dathaslogontriggers" flag which is set when
client connection trigger is created.
This flag is never cleaned (to avoid visibility issues mentioned in my
previous mail).

This is much better - I don't see any slowdown when logon trigger is not
defined

I did some benchmarks and looks so starting language handler is relatively
expensive - it is about 25% and starting handler like event trigger has
about 35%. But this problem can be solved later and elsewhere.

I prefer the inverse form of disable_connection_trigger. Almost all GUC
are in positive form - so enable_connection_triggger is better

I don't think so current handling dathaslogontriggers is good for
production usage. The protection against race condition can be solved by
lock on pg_event_trigger

I thought about it, and probably the counter of connect triggers will be
better there. The implementation will be simpler and more robust.

Pavel

Show quoted text

Regards

Pavel

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#41Konstantin Knizhnik
k.knizhnik@postgrespro.ru
In reply to: Pavel Stehule (#40)
1 attachment(s)
Re: On login trigger: take three

On 17.12.2020 9:31, Pavel Stehule wrote:

st 16. 12. 2020 v 20:38 odesílatel Pavel Stehule
<pavel.stehule@gmail.com <mailto:pavel.stehule@gmail.com>> napsal:

Attached please find new versoin of the patch based on
on_connect_event_trigger_WITH_SUGGESTED_UPDATES.patch

So there is still only "disable_client_connection_trigger"
GUC? because we need possibility to disable client connect
triggers and there is no such need for other event types.

As you suggested  have added "dathaslogontriggers" flag which
is set when client connection trigger is created.
This flag is never cleaned (to avoid visibility issues
mentioned in my previous mail).

This is much better - I don't see any slowdown when logon trigger
is not defined

I did some benchmarks and looks so starting language handler is
relatively expensive - it is about 25% and starting handler like
event trigger has about 35%. But this problem can be solved later
and elsewhere.

I prefer the inverse form of disable_connection_trigger. Almost
all GUC are in positive form - so enable_connection_triggger is better

I don't think so current handling dathaslogontriggers is good for
production usage. The protection against race condition can be
solved by lock on pg_event_trigger

I thought about it, and probably the counter of connect triggers will
be better there. The implementation will be simpler and more robust.

I prefer to implement different approach: unset dathaslogontriggers flag
in event trigger itself when no triggers are returned by
EventTriggerCommonSetup.
I am using double checking to prevent race condition.
The main reason for this approach is that dropping of triggers is not
done in event_trigger.c: it is done by generic code for all resources.
It seems to be there is no right place where decrementing of trigger
counters can be inserted.
Also I prefer to keep all logon-trigger specific code in one file.

Also, as you suggested, I renamed disable_connection_trigger to
enable_connection_trigger.

New version of the patch is attached.

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

Attachments:

on_connect_event_trigger-9.patchtext/x-patch; name=on_connect_event_trigger-9.patchDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index f810789..45d30b3 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -996,6 +996,23 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-enable-client-connection-trigger" xreflabel="enable_client_connection_trigger">
+      <term><varname>enable_client_connection_trigger</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>enable_client_connection_trigger</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Enables firing the <literal>client_connection</literal>
+        trigger when a client connects. This parameter is switched on by default.
+        Errors in trigger code can prevent user to login to the system.
+        I this case disabling this parameter in connection string can solve the problem:
+        <literal>psql "dbname=postgres options='-c enable_client_connection_trigger=false'". 
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
      </sect2>
 
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 60366a9..ae40a8e 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>client_connection</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -36,6 +37,29 @@
    </para>
 
    <para>
+     The <literal>client_connection</literal> event occurs when a client connection
+     to the server is established.
+     There are two mechanisms for dealing with any bugs in a trigger procedure for
+     this event which might prevent successful login to the system:
+     <itemizedlist>
+      <listitem>
+       <para>
+         The configuration parameter <literal>enable_client_connection_trigger</literal>
+         makes it possible to disable firing the <literal>client_connection</literal>
+         trigger when a client connects.
+       </para>
+      </listitem>
+      <listitem>
+       <para>
+         Errors in the <literal>client_connection</literal> trigger procedure are
+         ignored for superuser. An error message is delivered to the client as
+         <literal>NOTICE</literal> in this case.
+       </para>
+      </listitem>
+     </itemizedlist>
+   </para>
+
+   <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
      <literal>SECURITY LABEL</literal>,
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index f27c3fe..8646db7 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -560,6 +560,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	new_record[Anum_pg_database_datctype - 1] =
 		DirectFunctionCall1(namein, CStringGetDatum(dbctype));
 	new_record[Anum_pg_database_datistemplate - 1] = BoolGetDatum(dbistemplate);
+	new_record[Anum_pg_database_dathaslogontriggers - 1] = BoolGetDatum(false);
 	new_record[Anum_pg_database_datallowconn - 1] = BoolGetDatum(dballowconnections);
 	new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 	new_record[Anum_pg_database_datlastsysoid - 1] = ObjectIdGetDatum(src_lastsysoid);
@@ -1627,7 +1628,7 @@ AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel)
 		new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 		new_record_repl[Anum_pg_database_datconnlimit - 1] = true;
 	}
-
+	new_record[Anum_pg_database_dathaslogontriggers - 1] = BoolGetDatum(datform->dathaslogontriggers);
 	newtuple = heap_modify_tuple(tuple, RelationGetDescr(rel), new_record,
 								 new_record_nulls, new_record_repl);
 	CatalogTupleUpdate(rel, &tuple->t_self, newtuple);
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 3ffba4e..855b656 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -43,11 +44,14 @@
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
 #include "utils/syscache.h"
 
+bool enable_client_connection_trigger;
+
 typedef struct EventTriggerQueryState
 {
 	/* memory context for this state's objects */
@@ -130,6 +134,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "client_connection") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -293,6 +298,23 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
 	CatalogTupleInsert(tgrel, tuple);
 	heap_freetuple(tuple);
 
+	if (strcmp(eventname, "client_connection") == 0)
+	{
+		Form_pg_database db;
+		Relation pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+		/* Set dathaslogontriggers flag in pg_database */
+		tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+		db = (Form_pg_database) GETSTRUCT(tuple);
+		if (!db->dathaslogontriggers)
+		{
+			db->dathaslogontriggers = true;
+			CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+		}
+		table_close(pg_db, RowExclusiveLock);
+	}
+
 	/* Depend on owner. */
 	recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
 
@@ -562,6 +584,9 @@ EventTriggerCommonSetup(Node *parsetree,
 	ListCell   *lc;
 	List	   *runlist = NIL;
 
+	/* Get the command tag. */
+	tag = parsetree ? CreateCommandTag(parsetree) : CMDTAG_CONNECT;
+
 	/*
 	 * We want the list of command tags for which this procedure is actually
 	 * invoked to match up exactly with the list that CREATE EVENT TRIGGER
@@ -577,22 +602,18 @@ EventTriggerCommonSetup(Node *parsetree,
 	 * relevant command tag.
 	 */
 #ifdef USE_ASSERT_CHECKING
+	if (event == EVT_DDLCommandStart ||
+		event == EVT_DDLCommandEnd ||
+		event == EVT_SQLDrop ||
+		event == EVT_Connect)
 	{
-		CommandTag	dbgtag;
-
-		dbgtag = CreateCommandTag(parsetree);
-		if (event == EVT_DDLCommandStart ||
-			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
-		{
-			if (!command_tag_event_trigger_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
-		else if (event == EVT_TableRewrite)
-		{
-			if (!command_tag_table_rewrite_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
+		if (!command_tag_event_trigger_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
+	}
+	else if (event == EVT_TableRewrite)
+	{
+		if (!command_tag_table_rewrite_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
 	}
 #endif
 
@@ -601,9 +622,6 @@ EventTriggerCommonSetup(Node *parsetree,
 	if (cachelist == NIL)
 		return NIL;
 
-	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
-
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
 	 * memory context.  Once we start running the command triggers, or indeed
@@ -800,6 +818,117 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+static bool
+DatabaseHasLogonTriggers(void)
+{
+	bool has_logon_triggers;
+	HeapTuple tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+	has_logon_triggers = ((Form_pg_database) GETSTRUCT(tuple))->dathaslogontriggers;
+	ReleaseSysCache(tuple);
+	return has_logon_triggers;
+}
+
+/*
+ * Fire connect triggers.
+ */
+void
+EventTriggerOnConnect(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster
+		|| !OidIsValid(MyDatabaseId)
+		|| !enable_client_connection_trigger)
+		return;
+
+	StartTransactionCommand();
+
+	if (DatabaseHasLogonTriggers())
+	{
+		runlist = EventTriggerCommonSetup(NULL,
+										  EVT_Connect, "connect",
+										  &trigdata);
+
+		if (runlist != NIL)
+		{
+			MemoryContext old_context = CurrentMemoryContext;
+			bool is_superuser = superuser();
+			/*
+			 * Make sure anything the main command did will be visible to the event
+			 * triggers.
+			 */
+			CommandCounterIncrement();
+
+			/* Run the triggers. */
+			PG_TRY();
+			{
+				EventTriggerInvoke(runlist, &trigdata);
+				list_free(runlist);
+			}
+			PG_CATCH();
+			{
+				ErrorData* error;
+				/*
+				 * Try to ignore error for superuser to make it possible to login even in case of errors
+				 * during trigger execution
+				 */
+				if (!is_superuser)
+					PG_RE_THROW();
+
+				MemoryContextSwitchTo(old_context);
+				error = CopyErrorData();
+				FlushErrorState();
+				elog(NOTICE, "client_connection trigger failed with message: %s", error->message);
+				AbortCurrentTransaction();
+				return;
+			}
+			PG_END_TRY();
+		}
+		else
+		{
+			/* Runtlist is empty: clear dathaslogontriggers flag
+			 */
+			Relation pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+			HeapTuple tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+			Form_pg_database db;
+
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+			db = (Form_pg_database) GETSTRUCT(tuple);
+			if (db->dathaslogontriggers)
+			{
+				db->dathaslogontriggers = false;
+				CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				/*
+				 * There can be race condition: event trigger may be added after we have scanned
+				 * pg_event_trigger table. Repeat this test nuder  pg_database table lock.
+				 */
+				AcceptInvalidationMessages();
+				runlist = EventTriggerCommonSetup(NULL,
+												  EVT_Connect, "connect",
+												  &trigdata);
+				if (runlist != NULL) /* if list is not empty, then restore the flag */
+				{
+					db->dathaslogontriggers = true;
+					CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				}
+			}
+			table_close(pg_db, RowExclusiveLock);
+		}
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 3679799..ce9b98e 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -42,6 +42,7 @@
 #include "catalog/pg_type.h"
 #include "commands/async.h"
 #include "commands/prepare.h"
+#include "commands/event_trigger.h"
 #include "executor/spi.h"
 #include "jit/jit.h"
 #include "libpq/libpq.h"
@@ -167,6 +168,9 @@ static ProcSignalReason RecoveryConflictReason;
 static MemoryContext row_description_context = NULL;
 static StringInfoData row_description_buf;
 
+/* Hook for plugins to get control at start of session */
+client_connection_hook_type client_connection_hook = EventTriggerOnConnect;
+
 /* ----------------------------------------------------------------
  *		decls for routines only used in this file
  * ----------------------------------------------------------------
@@ -4012,6 +4016,11 @@ PostgresMain(int argc, char *argv[],
 	if (!IsUnderPostmaster)
 		PgStartTime = GetCurrentTimestamp();
 
+	if (client_connection_hook)
+	{
+		(*client_connection_hook) ();
+	}
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 0427795..c621b8f 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -168,6 +168,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "client_connection") == 0)
+			event = EVT_Connect;
 		else
 			continue;
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 245a347..e42fc0e 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -43,6 +43,7 @@
 #include "commands/async.h"
 #include "commands/prepare.h"
 #include "commands/trigger.h"
+#include "commands/event_trigger.h"
 #include "commands/user.h"
 #include "commands/vacuum.h"
 #include "commands/variable.h"
@@ -928,6 +929,18 @@ static const unit_conversion time_unit_conversion_table[] =
 static struct config_bool ConfigureNamesBool[] =
 {
 	{
+		{"enable_client_connection_trigger", PGC_SU_BACKEND, DEVELOPER_OPTIONS,
+			gettext_noop("Enables the client_connection event trigger."),
+			gettext_noop("In case of errors in the ON client_connection EVENT TRIGGER procedure, "
+						 "this parameter can be used to disable trigger activation "
+						 "and provide access to the database."),
+			GUC_EXPLAIN
+		},
+		&enable_client_connection_trigger,
+		true,
+		NULL, NULL, NULL
+	},
+	{
 		{"enable_seqscan", PGC_USERSET, QUERY_TUNING_METHOD,
 			gettext_noop("Enables the planner's use of sequential-scan plans."),
 			NULL,
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index dc1d41d..a9a7c1f 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -2726,6 +2726,7 @@ dumpDatabase(Archive *fout)
 				i_datacl,
 				i_rdatacl,
 				i_datistemplate,
+				i_dathaslogontriggers,
 				i_datconnlimit,
 				i_tablespace;
 	CatalogId	dbCatId;
@@ -2738,6 +2739,7 @@ dumpDatabase(Archive *fout)
 			   *datacl,
 			   *rdatacl,
 			   *datistemplate,
+			   *dathaslogontriggers,
 			   *datconnlimit,
 			   *tablespace;
 	uint32		frozenxid,
@@ -2756,7 +2758,7 @@ dumpDatabase(Archive *fout)
 	 * (pg_init_privs) are not supported on databases, so this logic cannot
 	 * make use of buildACLQueries().
 	 */
-	if (fout->remoteVersion >= 90600)
+	if (fout->remoteVersion >= 140000)
 	{
 		appendPQExpBuffer(dbQry, "SELECT tableoid, oid, datname, "
 						  "(%s datdba) AS dba, "
@@ -2782,7 +2784,41 @@ dumpDatabase(Archive *fout)
 						  "       AS permp(orig_acl) "
 						  "     WHERE acl = orig_acl)) AS rdatacls) "
 						  " AS rdatacl, "
-						  "datistemplate, datconnlimit, "
+						  "datistemplate, datconnlimit, dathaslogontriggers, "
+						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
+						  "shobj_description(oid, 'pg_database') AS description "
+
+						  "FROM pg_database "
+						  "WHERE datname = current_database()",
+						  username_subquery);
+	}
+	else if (fout->remoteVersion >= 90600)
+	{
+		appendPQExpBuffer(dbQry, "SELECT tableoid, oid, datname, "
+						  "(%s datdba) AS dba, "
+						  "pg_encoding_to_char(encoding) AS encoding, "
+						  "datcollate, datctype, datfrozenxid, datminmxid, "
+						  "(SELECT array_agg(acl ORDER BY row_n) FROM "
+						  "  (SELECT acl, row_n FROM "
+						  "     unnest(coalesce(datacl,acldefault('d',datdba))) "
+						  "     WITH ORDINALITY AS perm(acl,row_n) "
+						  "   WHERE NOT EXISTS ( "
+						  "     SELECT 1 "
+						  "     FROM unnest(acldefault('d',datdba)) "
+						  "       AS init(init_acl) "
+						  "     WHERE acl = init_acl)) AS datacls) "
+						  " AS datacl, "
+						  "(SELECT array_agg(acl ORDER BY row_n) FROM "
+						  "  (SELECT acl, row_n FROM "
+						  "     unnest(acldefault('d',datdba)) "
+						  "     WITH ORDINALITY AS initp(acl,row_n) "
+						  "   WHERE NOT EXISTS ( "
+						  "     SELECT 1 "
+						  "     FROM unnest(coalesce(datacl,acldefault('d',datdba))) "
+						  "       AS permp(orig_acl) "
+						  "     WHERE acl = orig_acl)) AS rdatacls) "
+						  " AS rdatacl, "
+						  "datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2796,7 +2832,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "datcollate, datctype, datfrozenxid, datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, dathaslogontriggers"
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2810,7 +2846,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "datcollate, datctype, datfrozenxid, 0 AS datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2824,7 +2860,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "NULL AS datcollate, NULL AS datctype, datfrozenxid, 0 AS datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2839,7 +2875,7 @@ dumpDatabase(Archive *fout)
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "NULL AS datcollate, NULL AS datctype, datfrozenxid, 0 AS datminmxid, "
 						  "datacl, '' as rdatacl, datistemplate, "
-						  "-1 as datconnlimit, "
+						  "-1 as datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace "
 						  "FROM pg_database "
 						  "WHERE datname = current_database()",
@@ -2861,6 +2897,7 @@ dumpDatabase(Archive *fout)
 	i_rdatacl = PQfnumber(res, "rdatacl");
 	i_datistemplate = PQfnumber(res, "datistemplate");
 	i_datconnlimit = PQfnumber(res, "datconnlimit");
+	i_dathaslogontriggers = PQfnumber(res, "dathaslogontriggers");
 	i_tablespace = PQfnumber(res, "tablespace");
 
 	dbCatId.tableoid = atooid(PQgetvalue(res, 0, i_tableoid));
@@ -2875,6 +2912,7 @@ dumpDatabase(Archive *fout)
 	datacl = PQgetvalue(res, 0, i_datacl);
 	rdatacl = PQgetvalue(res, 0, i_rdatacl);
 	datistemplate = PQgetvalue(res, 0, i_datistemplate);
+	dathaslogontriggers = PQgetvalue(res, 0, i_dathaslogontriggers);
 	datconnlimit = PQgetvalue(res, 0, i_datconnlimit);
 	tablespace = PQgetvalue(res, 0, i_tablespace);
 
@@ -3048,6 +3086,14 @@ dumpDatabase(Archive *fout)
 		appendPQExpBufferStr(delQry, ";\n");
 	}
 
+	if (strcmp(dathaslogontriggers, "t") == 0)
+	{
+		appendPQExpBufferStr(creaQry, "UPDATE pg_catalog.pg_database "
+							 "SET dathaslogontriggers = true WHERE datname = ");
+		appendStringLiteralAH(creaQry, datname, fout);
+		appendPQExpBufferStr(creaQry, ";\n");
+	}
+
 	/* Add database-specific SET options */
 	dumpDatabaseConfig(fout, creaQry, datname, dbCatId.oid);
 
diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat
index 21cd660..4c10575 100644
--- a/src/include/catalog/pg_database.dat
+++ b/src/include/catalog/pg_database.dat
@@ -15,7 +15,7 @@
 { oid => '1', oid_symbol => 'TemplateDbOid',
   descr => 'default template for new databases',
   datname => 'template1', encoding => 'ENCODING', datcollate => 'LC_COLLATE',
-  datctype => 'LC_CTYPE', datistemplate => 't', datallowconn => 't',
+  datctype => 'LC_CTYPE', datistemplate => 't', dathaslogontriggers => 'f', datallowconn => 't',
   datconnlimit => '-1', datlastsysoid => '0', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datacl => '_null_' },
 
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index 47bcf40..7dc15a5 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -52,6 +52,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
 	/* new connections allowed? */
 	bool		datallowconn;
 
+	/* database has on-login triggers */
+	bool		dathaslogontriggers;
+
 	/* max connections allowed (-1=no limit) */
 	int32		datconnlimit;
 
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 407fd6a..47d481e 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -21,6 +21,8 @@
 #include "tcop/deparse_utility.h"
 #include "utils/aclchk_internal.h"
 
+extern bool enable_client_connection_trigger; /* GUC */
+
 typedef struct EventTriggerData
 {
 	NodeTag		type;
@@ -53,6 +55,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnConnect(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index be94852..988aa39 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -80,6 +80,7 @@ PG_CMDTAG(CMDTAG_CLUSTER, "CLUSTER", false, false, false)
 PG_CMDTAG(CMDTAG_COMMENT, "COMMENT", true, false, false)
 PG_CMDTAG(CMDTAG_COMMIT, "COMMIT", false, false, false)
 PG_CMDTAG(CMDTAG_COMMIT_PREPARED, "COMMIT PREPARED", false, false, false)
+PG_CMDTAG(CMDTAG_CONNECT, "CONNECT", true, false, false)
 PG_CMDTAG(CMDTAG_COPY, "COPY", false, false, true)
 PG_CMDTAG(CMDTAG_COPY_FROM, "COPY FROM", false, false, false)
 PG_CMDTAG(CMDTAG_CREATE_ACCESS_METHOD, "CREATE ACCESS METHOD", true, false, false)
diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h
index bd30607..90376b2 100644
--- a/src/include/tcop/tcopprot.h
+++ b/src/include/tcop/tcopprot.h
@@ -30,6 +30,11 @@ extern PGDLLIMPORT const char *debug_query_string;
 extern int	max_stack_depth;
 extern int	PostAuthDelay;
 
+/* Hook for plugins to get control at start and end of session */
+typedef void (*client_connection_hook_type) (void);
+
+extern PGDLLIMPORT client_connection_hook_type client_connection_hook;
+
 /* GUC-configurable parameters */
 
 typedef enum
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index bb1e39e..ab7e8c3 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Connect,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/000_client_connection_trigger.pl b/src/test/recovery/t/000_client_connection_trigger.pl
new file mode 100644
index 0000000..3dcd475
--- /dev/null
+++ b/src/test/recovery/t/000_client_connection_trigger.pl
@@ -0,0 +1,69 @@
+use strict;
+use warnings;
+use PostgresNode;
+use TestLib;
+use Test::More;
+if (!$use_unix_sockets)
+{
+	plan skip_all =>
+	  "authentication tests cannot run without Unix-domain sockets";
+}
+else
+{
+	plan tests => 5;
+}
+
+# Initialize master node
+my $node = get_new_node('master');
+$node->init;
+$node->start;
+$node->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+CREATE ROLE regress_hacker LOGIN PASSWORD 'pass';
+
+CREATE TABLE connects(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO connects (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON client_connection EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+}
+);
+my $res;
+
+$res = $node->safe_psql('postgres', "SELECT 1");
+
+$res = $node->safe_psql('postgres', "SELECT 1",
+  extra_params => [ '-U', 'regress_user', '-w' ]);
+
+my ($ret, $stdout, $stderr) = $node->psql('postgres', "SELECT 1",
+  extra_params => [ '-U', 'regress_hacker', '-w' ]);
+ok( $ret != 0 && $stderr =~ /You are not welcome!/ );
+
+$res = $node->safe_psql('postgres', "SELECT COUNT(1) FROM connects WHERE who = 'regress_user'");
+ok($res == 1);
+
+my $tempdir = TestLib::tempdir;
+command_ok(
+  [ "pg_dumpall", '-p', $node->port, '-c', "--file=$tempdir/regression_dump.sql", ],
+  "dumpall");
+# my $dump_contents = slurp_file("$tempdir/regression_dump.sql");
+# print($dump_contents);
+
+my $node1 = get_new_node('secondary');
+$node1->init;
+$node1->start;
+command_ok(["psql", '-p', $node1->port, '-b', '-f', "$tempdir/regression_dump.sql" ] );
+$res = $node1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$res = $node1->safe_psql('postgres', "SELECT COUNT(1) FROM connects WHERE who = 'regress_user'");
+ok($res == 2);
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 9e31a53..b4a21fb 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -43,6 +43,27 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+
+CREATE TABLE connects(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO connects (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON client_connection EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 $node_primary->wait_for_catchup($node_standby_1, 'replay',
 	$node_primary->lsn('insert'));
@@ -266,6 +287,9 @@ sub replay_check
 
 replay_check();
 
+$node_standby_1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$node_standby_2->safe_psql('postgres', "SELECT 2", extra_params => [ '-U', 'regress_user', '-w' ]);
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index bdd0ffc..3ecbd6a 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -536,3 +536,40 @@ NOTICE:  DROP POLICY - ddl_command_end
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- On session start triggers
+create table connects(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into connects (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on client_connection execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id | who  
+----+------
+  1 | I am
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id | who  
+----+------
+  1 | I am
+  2 | I am
+(2 rows)
+
+-- Test handing exeptions in client_connection trigger
+drop table connects;
+-- superuser should ignore error
+\c
+NOTICE:  client_connection trigger failed with message: relation "connects" does not exist
+-- suppress trigger firing
+\c "dbname=regression options='-c disable_client_connection_trigger=true'"
+-- Cleanup
+drop event trigger on_login_trigger;
+drop function on_login_proc();
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 18b2a26..a1c5cf2 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -429,3 +429,31 @@ DROP POLICY p2 ON event_trigger_test;
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- On session start triggers
+create table connects(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into connects (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on client_connection execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+select * from connects;
+\c
+select * from connects;
+
+-- Test handing exeptions in client_connection trigger
+
+drop table connects;
+-- superuser should ignore error
+\c
+-- suppress trigger firing
+\c "dbname=regression options='-c disable_client_connection_trigger=true'"
+
+
+-- Cleanup
+drop event trigger on_login_trigger;
+drop function on_login_proc();
#42Konstantin Knizhnik
k.knizhnik@postgrespro.ru
In reply to: Zhihong Yu (#39)
Re: On login trigger: take three

Hi,

On 17.12.2020 3:31, Zhihong Yu wrote:

Hi,
For EventTriggerOnConnect():

+           PG_CATCH();
+           {
...
+               AbortCurrentTransaction();
+               return;

Should runlist be freed in the catch block ?

No need: it is allocated in transaction memory context and removed on
transaction abort.

+           gettext_noop("In case of errors in the ON
client_connection EVENT TRIGGER procedure, this parameter can be used
to disable trigger activation and provide access to the database."),

I think the text should be on two lines (current line too long).

Thank you, fixed.

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#43Pavel Stehule
pavel.stehule@gmail.com
In reply to: Konstantin Knizhnik (#41)
Re: On login trigger: take three

čt 17. 12. 2020 v 14:04 odesílatel Konstantin Knizhnik <
k.knizhnik@postgrespro.ru> napsal:

On 17.12.2020 9:31, Pavel Stehule wrote:

st 16. 12. 2020 v 20:38 odesílatel Pavel Stehule <pavel.stehule@gmail.com>
napsal:

Attached please find new versoin of the patch based on
on_connect_event_trigger_WITH_SUGGESTED_UPDATES.patch

So there is still only "disable_client_connection_trigger" GUC? because
we need possibility to disable client connect triggers and there is no such
need for other event types.

As you suggested have added "dathaslogontriggers" flag which is set
when client connection trigger is created.
This flag is never cleaned (to avoid visibility issues mentioned in my
previous mail).

This is much better - I don't see any slowdown when logon trigger is not
defined

I did some benchmarks and looks so starting language handler is
relatively expensive - it is about 25% and starting handler like event
trigger has about 35%. But this problem can be solved later and elsewhere.

I prefer the inverse form of disable_connection_trigger. Almost all GUC
are in positive form - so enable_connection_triggger is better

I don't think so current handling dathaslogontriggers is good for
production usage. The protection against race condition can be solved by
lock on pg_event_trigger

I thought about it, and probably the counter of connect triggers will be
better there. The implementation will be simpler and more robust.

I prefer to implement different approach: unset dathaslogontriggers flag
in event trigger itself when no triggers are returned by
EventTriggerCommonSetup.
I am using double checking to prevent race condition.
The main reason for this approach is that dropping of triggers is not done
in event_trigger.c: it is done by generic code for all resources.
It seems to be there is no right place where decrementing of trigger
counters can be inserted.
Also I prefer to keep all logon-trigger specific code in one file.

Also, as you suggested, I renamed disable_connection_trigger to
enable_connection_trigger.

New version of the patch is attached.

yes, it can work

Regards

Pavel

Show quoted text

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#44Pavel Stehule
pavel.stehule@gmail.com
In reply to: Pavel Stehule (#43)
Re: On login trigger: take three

čt 17. 12. 2020 v 19:30 odesílatel Pavel Stehule <pavel.stehule@gmail.com>
napsal:

čt 17. 12. 2020 v 14:04 odesílatel Konstantin Knizhnik <
k.knizhnik@postgrespro.ru> napsal:

On 17.12.2020 9:31, Pavel Stehule wrote:

st 16. 12. 2020 v 20:38 odesílatel Pavel Stehule <pavel.stehule@gmail.com>
napsal:

Attached please find new versoin of the patch based on
on_connect_event_trigger_WITH_SUGGESTED_UPDATES.patch

So there is still only "disable_client_connection_trigger" GUC?
because we need possibility to disable client connect triggers and there is
no such need for other event types.

As you suggested have added "dathaslogontriggers" flag which is set
when client connection trigger is created.
This flag is never cleaned (to avoid visibility issues mentioned in my
previous mail).

This is much better - I don't see any slowdown when logon trigger is not
defined

I did some benchmarks and looks so starting language handler is
relatively expensive - it is about 25% and starting handler like event
trigger has about 35%. But this problem can be solved later and elsewhere.

I prefer the inverse form of disable_connection_trigger. Almost all GUC
are in positive form - so enable_connection_triggger is better

I don't think so current handling dathaslogontriggers is good for
production usage. The protection against race condition can be solved by
lock on pg_event_trigger

I thought about it, and probably the counter of connect triggers will be
better there. The implementation will be simpler and more robust.

I prefer to implement different approach: unset dathaslogontriggers flag
in event trigger itself when no triggers are returned by
EventTriggerCommonSetup.
I am using double checking to prevent race condition.
The main reason for this approach is that dropping of triggers is not
done in event_trigger.c: it is done by generic code for all resources.
It seems to be there is no right place where decrementing of trigger
counters can be inserted.
Also I prefer to keep all logon-trigger specific code in one file.

Also, as you suggested, I renamed disable_connection_trigger to
enable_connection_trigger.

New version of the patch is attached.

yes, it can work

for complete review the new field in pg_doc should be documented

Regards

Pavel

Show quoted text

Regards

Pavel

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#45Konstantin Knizhnik
k.knizhnik@postgrespro.ru
In reply to: Pavel Stehule (#44)
1 attachment(s)
Re: On login trigger: take three

On 20.12.2020 10:04, Pavel Stehule wrote:

čt 17. 12. 2020 v 19:30 odesílatel Pavel Stehule
<pavel.stehule@gmail.com <mailto:pavel.stehule@gmail.com>> napsal:

čt 17. 12. 2020 v 14:04 odesílatel Konstantin Knizhnik
<k.knizhnik@postgrespro.ru <mailto:k.knizhnik@postgrespro.ru>> napsal:

On 17.12.2020 9:31, Pavel Stehule wrote:

st 16. 12. 2020 v 20:38 odesílatel Pavel Stehule
<pavel.stehule@gmail.com <mailto:pavel.stehule@gmail.com>>
napsal:

Attached please find new versoin of the patch based on
on_connect_event_trigger_WITH_SUGGESTED_UPDATES.patch

So there is still only
"disable_client_connection_trigger" GUC? because we
need possibility to disable client connect triggers
and there is no such need for other event types.

As you suggested  have added "dathaslogontriggers"
flag which is set when client connection trigger is
created.
This flag is never cleaned (to avoid visibility
issues mentioned in my previous mail).

This is much better - I don't see any slowdown when logon
trigger is not defined

I did some benchmarks and looks so starting language
handler is relatively expensive - it is about 25% and
starting handler like event trigger has about 35%. But
this problem can be solved later and elsewhere.

I prefer the inverse form of disable_connection_trigger.
Almost all GUC are in positive form - so
enable_connection_triggger is better

I don't think so current handling dathaslogontriggers is
good for production usage. The protection against race
condition can be solved by lock on pg_event_trigger

I thought about it, and probably the counter of connect
triggers will be better there. The implementation will be
simpler and more robust.

I prefer to implement different approach: unset
dathaslogontriggers  flag in event trigger itself when no
triggers are returned by EventTriggerCommonSetup.
I am using double checking to prevent race condition.
The main reason for this approach is that dropping of triggers
is not done in event_trigger.c: it is done by generic code for
all resources.
It seems to be there is no right place where decrementing of
trigger counters can be inserted.
Also I prefer to keep all logon-trigger specific code in one file.

Also, as you suggested, I renamed disable_connection_trigger
to enable_connection_trigger.

New version of the patch is attached.

yes, it can work

for complete review the new field in pg_doc should be documented

Done.
Also I fixed some examples in documentation which involves pg_database.

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

Attachments:

on_connect_event_trigger-10.patchtext/x-patch; name=on_connect_event_trigger-10.patchDownload
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 5698413..010bff5 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2946,6 +2946,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
 
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>dathaslogontriggers</structfield> <type>bool</type>
+      </para>
+      <para>
+        Indicates that there are client connection triggers defined for this database.
+        This flag is used to avoid extra lookup of pg_event_trigger table on each backend startup.
+        This flag is used internally by Postgres and should not be manually changed by DBA or application.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>datconnlimit</structfield> <type>int4</type>
       </para>
       <para>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index f810789..45d30b3 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -996,6 +996,23 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-enable-client-connection-trigger" xreflabel="enable_client_connection_trigger">
+      <term><varname>enable_client_connection_trigger</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>enable_client_connection_trigger</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Enables firing the <literal>client_connection</literal>
+        trigger when a client connects. This parameter is switched on by default.
+        Errors in trigger code can prevent user to login to the system.
+        I this case disabling this parameter in connection string can solve the problem:
+        <literal>psql "dbname=postgres options='-c enable_client_connection_trigger=false'". 
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
      </sect2>
 
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index 14dcbdb..965e497 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -4673,6 +4673,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = t (type: 1)
 datallowconn = t (type: 1)
+dathaslogontriggers = f (type: 1)
 datconnlimit = -1 (type: 5)
 datlastsysoid = 11510 (type: 1)
 datfrozenxid = 379 (type: 1)
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 60366a9..ae40a8e 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>client_connection</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -36,6 +37,29 @@
    </para>
 
    <para>
+     The <literal>client_connection</literal> event occurs when a client connection
+     to the server is established.
+     There are two mechanisms for dealing with any bugs in a trigger procedure for
+     this event which might prevent successful login to the system:
+     <itemizedlist>
+      <listitem>
+       <para>
+         The configuration parameter <literal>enable_client_connection_trigger</literal>
+         makes it possible to disable firing the <literal>client_connection</literal>
+         trigger when a client connects.
+       </para>
+      </listitem>
+      <listitem>
+       <para>
+         Errors in the <literal>client_connection</literal> trigger procedure are
+         ignored for superuser. An error message is delivered to the client as
+         <literal>NOTICE</literal> in this case.
+       </para>
+      </listitem>
+     </itemizedlist>
+   </para>
+
+   <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
      <literal>SECURITY LABEL</literal>,
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index f27c3fe..8646db7 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -560,6 +560,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	new_record[Anum_pg_database_datctype - 1] =
 		DirectFunctionCall1(namein, CStringGetDatum(dbctype));
 	new_record[Anum_pg_database_datistemplate - 1] = BoolGetDatum(dbistemplate);
+	new_record[Anum_pg_database_dathaslogontriggers - 1] = BoolGetDatum(false);
 	new_record[Anum_pg_database_datallowconn - 1] = BoolGetDatum(dballowconnections);
 	new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 	new_record[Anum_pg_database_datlastsysoid - 1] = ObjectIdGetDatum(src_lastsysoid);
@@ -1627,7 +1628,7 @@ AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel)
 		new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 		new_record_repl[Anum_pg_database_datconnlimit - 1] = true;
 	}
-
+	new_record[Anum_pg_database_dathaslogontriggers - 1] = BoolGetDatum(datform->dathaslogontriggers);
 	newtuple = heap_modify_tuple(tuple, RelationGetDescr(rel), new_record,
 								 new_record_nulls, new_record_repl);
 	CatalogTupleUpdate(rel, &tuple->t_self, newtuple);
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 3ffba4e..855b656 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -43,11 +44,14 @@
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
 #include "utils/syscache.h"
 
+bool enable_client_connection_trigger;
+
 typedef struct EventTriggerQueryState
 {
 	/* memory context for this state's objects */
@@ -130,6 +134,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "client_connection") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -293,6 +298,23 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
 	CatalogTupleInsert(tgrel, tuple);
 	heap_freetuple(tuple);
 
+	if (strcmp(eventname, "client_connection") == 0)
+	{
+		Form_pg_database db;
+		Relation pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+		/* Set dathaslogontriggers flag in pg_database */
+		tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+		db = (Form_pg_database) GETSTRUCT(tuple);
+		if (!db->dathaslogontriggers)
+		{
+			db->dathaslogontriggers = true;
+			CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+		}
+		table_close(pg_db, RowExclusiveLock);
+	}
+
 	/* Depend on owner. */
 	recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
 
@@ -562,6 +584,9 @@ EventTriggerCommonSetup(Node *parsetree,
 	ListCell   *lc;
 	List	   *runlist = NIL;
 
+	/* Get the command tag. */
+	tag = parsetree ? CreateCommandTag(parsetree) : CMDTAG_CONNECT;
+
 	/*
 	 * We want the list of command tags for which this procedure is actually
 	 * invoked to match up exactly with the list that CREATE EVENT TRIGGER
@@ -577,22 +602,18 @@ EventTriggerCommonSetup(Node *parsetree,
 	 * relevant command tag.
 	 */
 #ifdef USE_ASSERT_CHECKING
+	if (event == EVT_DDLCommandStart ||
+		event == EVT_DDLCommandEnd ||
+		event == EVT_SQLDrop ||
+		event == EVT_Connect)
 	{
-		CommandTag	dbgtag;
-
-		dbgtag = CreateCommandTag(parsetree);
-		if (event == EVT_DDLCommandStart ||
-			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
-		{
-			if (!command_tag_event_trigger_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
-		else if (event == EVT_TableRewrite)
-		{
-			if (!command_tag_table_rewrite_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
+		if (!command_tag_event_trigger_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
+	}
+	else if (event == EVT_TableRewrite)
+	{
+		if (!command_tag_table_rewrite_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
 	}
 #endif
 
@@ -601,9 +622,6 @@ EventTriggerCommonSetup(Node *parsetree,
 	if (cachelist == NIL)
 		return NIL;
 
-	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
-
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
 	 * memory context.  Once we start running the command triggers, or indeed
@@ -800,6 +818,117 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+static bool
+DatabaseHasLogonTriggers(void)
+{
+	bool has_logon_triggers;
+	HeapTuple tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+	has_logon_triggers = ((Form_pg_database) GETSTRUCT(tuple))->dathaslogontriggers;
+	ReleaseSysCache(tuple);
+	return has_logon_triggers;
+}
+
+/*
+ * Fire connect triggers.
+ */
+void
+EventTriggerOnConnect(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster
+		|| !OidIsValid(MyDatabaseId)
+		|| !enable_client_connection_trigger)
+		return;
+
+	StartTransactionCommand();
+
+	if (DatabaseHasLogonTriggers())
+	{
+		runlist = EventTriggerCommonSetup(NULL,
+										  EVT_Connect, "connect",
+										  &trigdata);
+
+		if (runlist != NIL)
+		{
+			MemoryContext old_context = CurrentMemoryContext;
+			bool is_superuser = superuser();
+			/*
+			 * Make sure anything the main command did will be visible to the event
+			 * triggers.
+			 */
+			CommandCounterIncrement();
+
+			/* Run the triggers. */
+			PG_TRY();
+			{
+				EventTriggerInvoke(runlist, &trigdata);
+				list_free(runlist);
+			}
+			PG_CATCH();
+			{
+				ErrorData* error;
+				/*
+				 * Try to ignore error for superuser to make it possible to login even in case of errors
+				 * during trigger execution
+				 */
+				if (!is_superuser)
+					PG_RE_THROW();
+
+				MemoryContextSwitchTo(old_context);
+				error = CopyErrorData();
+				FlushErrorState();
+				elog(NOTICE, "client_connection trigger failed with message: %s", error->message);
+				AbortCurrentTransaction();
+				return;
+			}
+			PG_END_TRY();
+		}
+		else
+		{
+			/* Runtlist is empty: clear dathaslogontriggers flag
+			 */
+			Relation pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+			HeapTuple tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+			Form_pg_database db;
+
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+			db = (Form_pg_database) GETSTRUCT(tuple);
+			if (db->dathaslogontriggers)
+			{
+				db->dathaslogontriggers = false;
+				CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				/*
+				 * There can be race condition: event trigger may be added after we have scanned
+				 * pg_event_trigger table. Repeat this test nuder  pg_database table lock.
+				 */
+				AcceptInvalidationMessages();
+				runlist = EventTriggerCommonSetup(NULL,
+												  EVT_Connect, "connect",
+												  &trigdata);
+				if (runlist != NULL) /* if list is not empty, then restore the flag */
+				{
+					db->dathaslogontriggers = true;
+					CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				}
+			}
+			table_close(pg_db, RowExclusiveLock);
+		}
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 3679799..ce9b98e 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -42,6 +42,7 @@
 #include "catalog/pg_type.h"
 #include "commands/async.h"
 #include "commands/prepare.h"
+#include "commands/event_trigger.h"
 #include "executor/spi.h"
 #include "jit/jit.h"
 #include "libpq/libpq.h"
@@ -167,6 +168,9 @@ static ProcSignalReason RecoveryConflictReason;
 static MemoryContext row_description_context = NULL;
 static StringInfoData row_description_buf;
 
+/* Hook for plugins to get control at start of session */
+client_connection_hook_type client_connection_hook = EventTriggerOnConnect;
+
 /* ----------------------------------------------------------------
  *		decls for routines only used in this file
  * ----------------------------------------------------------------
@@ -4012,6 +4016,11 @@ PostgresMain(int argc, char *argv[],
 	if (!IsUnderPostmaster)
 		PgStartTime = GetCurrentTimestamp();
 
+	if (client_connection_hook)
+	{
+		(*client_connection_hook) ();
+	}
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 0427795..c621b8f 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -168,6 +168,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "client_connection") == 0)
+			event = EVT_Connect;
 		else
 			continue;
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 245a347..e42fc0e 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -43,6 +43,7 @@
 #include "commands/async.h"
 #include "commands/prepare.h"
 #include "commands/trigger.h"
+#include "commands/event_trigger.h"
 #include "commands/user.h"
 #include "commands/vacuum.h"
 #include "commands/variable.h"
@@ -928,6 +929,18 @@ static const unit_conversion time_unit_conversion_table[] =
 static struct config_bool ConfigureNamesBool[] =
 {
 	{
+		{"enable_client_connection_trigger", PGC_SU_BACKEND, DEVELOPER_OPTIONS,
+			gettext_noop("Enables the client_connection event trigger."),
+			gettext_noop("In case of errors in the ON client_connection EVENT TRIGGER procedure, "
+						 "this parameter can be used to disable trigger activation "
+						 "and provide access to the database."),
+			GUC_EXPLAIN
+		},
+		&enable_client_connection_trigger,
+		true,
+		NULL, NULL, NULL
+	},
+	{
 		{"enable_seqscan", PGC_USERSET, QUERY_TUNING_METHOD,
 			gettext_noop("Enables the planner's use of sequential-scan plans."),
 			NULL,
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index dc1d41d..d286401 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -2726,6 +2726,7 @@ dumpDatabase(Archive *fout)
 				i_datacl,
 				i_rdatacl,
 				i_datistemplate,
+				i_dathaslogontriggers,
 				i_datconnlimit,
 				i_tablespace;
 	CatalogId	dbCatId;
@@ -2738,6 +2739,7 @@ dumpDatabase(Archive *fout)
 			   *datacl,
 			   *rdatacl,
 			   *datistemplate,
+			   *dathaslogontriggers,
 			   *datconnlimit,
 			   *tablespace;
 	uint32		frozenxid,
@@ -2756,7 +2758,7 @@ dumpDatabase(Archive *fout)
 	 * (pg_init_privs) are not supported on databases, so this logic cannot
 	 * make use of buildACLQueries().
 	 */
-	if (fout->remoteVersion >= 90600)
+	if (fout->remoteVersion >= 140000)
 	{
 		appendPQExpBuffer(dbQry, "SELECT tableoid, oid, datname, "
 						  "(%s datdba) AS dba, "
@@ -2782,7 +2784,41 @@ dumpDatabase(Archive *fout)
 						  "       AS permp(orig_acl) "
 						  "     WHERE acl = orig_acl)) AS rdatacls) "
 						  " AS rdatacl, "
-						  "datistemplate, datconnlimit, "
+						  "datistemplate, datconnlimit, dathaslogontriggers, "
+						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
+						  "shobj_description(oid, 'pg_database') AS description "
+
+						  "FROM pg_database "
+						  "WHERE datname = current_database()",
+						  username_subquery);
+	}
+	else if (fout->remoteVersion >= 90600)
+	{
+		appendPQExpBuffer(dbQry, "SELECT tableoid, oid, datname, "
+						  "(%s datdba) AS dba, "
+						  "pg_encoding_to_char(encoding) AS encoding, "
+						  "datcollate, datctype, datfrozenxid, datminmxid, "
+						  "(SELECT array_agg(acl ORDER BY row_n) FROM "
+						  "  (SELECT acl, row_n FROM "
+						  "     unnest(coalesce(datacl,acldefault('d',datdba))) "
+						  "     WITH ORDINALITY AS perm(acl,row_n) "
+						  "   WHERE NOT EXISTS ( "
+						  "     SELECT 1 "
+						  "     FROM unnest(acldefault('d',datdba)) "
+						  "       AS init(init_acl) "
+						  "     WHERE acl = init_acl)) AS datacls) "
+						  " AS datacl, "
+						  "(SELECT array_agg(acl ORDER BY row_n) FROM "
+						  "  (SELECT acl, row_n FROM "
+						  "     unnest(acldefault('d',datdba)) "
+						  "     WITH ORDINALITY AS initp(acl,row_n) "
+						  "   WHERE NOT EXISTS ( "
+						  "     SELECT 1 "
+						  "     FROM unnest(coalesce(datacl,acldefault('d',datdba))) "
+						  "       AS permp(orig_acl) "
+						  "     WHERE acl = orig_acl)) AS rdatacls) "
+						  " AS rdatacl, "
+						  "datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2796,7 +2832,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "datcollate, datctype, datfrozenxid, datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogontriggers"
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2810,7 +2846,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "datcollate, datctype, datfrozenxid, 0 AS datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2824,7 +2860,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "NULL AS datcollate, NULL AS datctype, datfrozenxid, 0 AS datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2839,7 +2875,7 @@ dumpDatabase(Archive *fout)
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "NULL AS datcollate, NULL AS datctype, datfrozenxid, 0 AS datminmxid, "
 						  "datacl, '' as rdatacl, datistemplate, "
-						  "-1 as datconnlimit, "
+						  "-1 as datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace "
 						  "FROM pg_database "
 						  "WHERE datname = current_database()",
@@ -2861,6 +2897,7 @@ dumpDatabase(Archive *fout)
 	i_rdatacl = PQfnumber(res, "rdatacl");
 	i_datistemplate = PQfnumber(res, "datistemplate");
 	i_datconnlimit = PQfnumber(res, "datconnlimit");
+	i_dathaslogontriggers = PQfnumber(res, "dathaslogontriggers");
 	i_tablespace = PQfnumber(res, "tablespace");
 
 	dbCatId.tableoid = atooid(PQgetvalue(res, 0, i_tableoid));
@@ -2875,6 +2912,7 @@ dumpDatabase(Archive *fout)
 	datacl = PQgetvalue(res, 0, i_datacl);
 	rdatacl = PQgetvalue(res, 0, i_rdatacl);
 	datistemplate = PQgetvalue(res, 0, i_datistemplate);
+	dathaslogontriggers = PQgetvalue(res, 0, i_dathaslogontriggers);
 	datconnlimit = PQgetvalue(res, 0, i_datconnlimit);
 	tablespace = PQgetvalue(res, 0, i_tablespace);
 
@@ -3048,6 +3086,14 @@ dumpDatabase(Archive *fout)
 		appendPQExpBufferStr(delQry, ";\n");
 	}
 
+	if (strcmp(dathaslogontriggers, "t") == 0)
+	{
+		appendPQExpBufferStr(creaQry, "UPDATE pg_catalog.pg_database "
+							 "SET dathaslogontriggers = true WHERE datname = ");
+		appendStringLiteralAH(creaQry, datname, fout);
+		appendPQExpBufferStr(creaQry, ";\n");
+	}
+
 	/* Add database-specific SET options */
 	dumpDatabaseConfig(fout, creaQry, datname, dbCatId.oid);
 
diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat
index 21cd660..4c10575 100644
--- a/src/include/catalog/pg_database.dat
+++ b/src/include/catalog/pg_database.dat
@@ -15,7 +15,7 @@
 { oid => '1', oid_symbol => 'TemplateDbOid',
   descr => 'default template for new databases',
   datname => 'template1', encoding => 'ENCODING', datcollate => 'LC_COLLATE',
-  datctype => 'LC_CTYPE', datistemplate => 't', datallowconn => 't',
+  datctype => 'LC_CTYPE', datistemplate => 't', dathaslogontriggers => 'f', datallowconn => 't',
   datconnlimit => '-1', datlastsysoid => '0', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datacl => '_null_' },
 
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index 47bcf40..7dc15a5 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -52,6 +52,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
 	/* new connections allowed? */
 	bool		datallowconn;
 
+	/* database has on-login triggers */
+	bool		dathaslogontriggers;
+
 	/* max connections allowed (-1=no limit) */
 	int32		datconnlimit;
 
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 407fd6a..47d481e 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -21,6 +21,8 @@
 #include "tcop/deparse_utility.h"
 #include "utils/aclchk_internal.h"
 
+extern bool enable_client_connection_trigger; /* GUC */
+
 typedef struct EventTriggerData
 {
 	NodeTag		type;
@@ -53,6 +55,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnConnect(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index be94852..988aa39 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -80,6 +80,7 @@ PG_CMDTAG(CMDTAG_CLUSTER, "CLUSTER", false, false, false)
 PG_CMDTAG(CMDTAG_COMMENT, "COMMENT", true, false, false)
 PG_CMDTAG(CMDTAG_COMMIT, "COMMIT", false, false, false)
 PG_CMDTAG(CMDTAG_COMMIT_PREPARED, "COMMIT PREPARED", false, false, false)
+PG_CMDTAG(CMDTAG_CONNECT, "CONNECT", true, false, false)
 PG_CMDTAG(CMDTAG_COPY, "COPY", false, false, true)
 PG_CMDTAG(CMDTAG_COPY_FROM, "COPY FROM", false, false, false)
 PG_CMDTAG(CMDTAG_CREATE_ACCESS_METHOD, "CREATE ACCESS METHOD", true, false, false)
diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h
index bd30607..90376b2 100644
--- a/src/include/tcop/tcopprot.h
+++ b/src/include/tcop/tcopprot.h
@@ -30,6 +30,11 @@ extern PGDLLIMPORT const char *debug_query_string;
 extern int	max_stack_depth;
 extern int	PostAuthDelay;
 
+/* Hook for plugins to get control at start and end of session */
+typedef void (*client_connection_hook_type) (void);
+
+extern PGDLLIMPORT client_connection_hook_type client_connection_hook;
+
 /* GUC-configurable parameters */
 
 typedef enum
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index bb1e39e..ab7e8c3 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Connect,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/000_client_connection_trigger.pl b/src/test/recovery/t/000_client_connection_trigger.pl
new file mode 100644
index 0000000..3dcd475
--- /dev/null
+++ b/src/test/recovery/t/000_client_connection_trigger.pl
@@ -0,0 +1,69 @@
+use strict;
+use warnings;
+use PostgresNode;
+use TestLib;
+use Test::More;
+if (!$use_unix_sockets)
+{
+	plan skip_all =>
+	  "authentication tests cannot run without Unix-domain sockets";
+}
+else
+{
+	plan tests => 5;
+}
+
+# Initialize master node
+my $node = get_new_node('master');
+$node->init;
+$node->start;
+$node->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+CREATE ROLE regress_hacker LOGIN PASSWORD 'pass';
+
+CREATE TABLE connects(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO connects (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON client_connection EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+}
+);
+my $res;
+
+$res = $node->safe_psql('postgres', "SELECT 1");
+
+$res = $node->safe_psql('postgres', "SELECT 1",
+  extra_params => [ '-U', 'regress_user', '-w' ]);
+
+my ($ret, $stdout, $stderr) = $node->psql('postgres', "SELECT 1",
+  extra_params => [ '-U', 'regress_hacker', '-w' ]);
+ok( $ret != 0 && $stderr =~ /You are not welcome!/ );
+
+$res = $node->safe_psql('postgres', "SELECT COUNT(1) FROM connects WHERE who = 'regress_user'");
+ok($res == 1);
+
+my $tempdir = TestLib::tempdir;
+command_ok(
+  [ "pg_dumpall", '-p', $node->port, '-c', "--file=$tempdir/regression_dump.sql", ],
+  "dumpall");
+# my $dump_contents = slurp_file("$tempdir/regression_dump.sql");
+# print($dump_contents);
+
+my $node1 = get_new_node('secondary');
+$node1->init;
+$node1->start;
+command_ok(["psql", '-p', $node1->port, '-b', '-f', "$tempdir/regression_dump.sql" ] );
+$res = $node1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$res = $node1->safe_psql('postgres', "SELECT COUNT(1) FROM connects WHERE who = 'regress_user'");
+ok($res == 2);
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 9e31a53..b4a21fb 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -43,6 +43,27 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+
+CREATE TABLE connects(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO connects (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON client_connection EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 $node_primary->wait_for_catchup($node_standby_1, 'replay',
 	$node_primary->lsn('insert'));
@@ -266,6 +287,9 @@ sub replay_check
 
 replay_check();
 
+$node_standby_1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$node_standby_2->safe_psql('postgres', "SELECT 2", extra_params => [ '-U', 'regress_user', '-w' ]);
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index bdd0ffc..3ecbd6a 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -536,3 +536,40 @@ NOTICE:  DROP POLICY - ddl_command_end
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- On session start triggers
+create table connects(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into connects (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on client_connection execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id | who  
+----+------
+  1 | I am
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id | who  
+----+------
+  1 | I am
+  2 | I am
+(2 rows)
+
+-- Test handing exeptions in client_connection trigger
+drop table connects;
+-- superuser should ignore error
+\c
+NOTICE:  client_connection trigger failed with message: relation "connects" does not exist
+-- suppress trigger firing
+\c "dbname=regression options='-c disable_client_connection_trigger=true'"
+-- Cleanup
+drop event trigger on_login_trigger;
+drop function on_login_proc();
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 18b2a26..a1c5cf2 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -429,3 +429,31 @@ DROP POLICY p2 ON event_trigger_test;
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- On session start triggers
+create table connects(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into connects (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on client_connection execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+select * from connects;
+\c
+select * from connects;
+
+-- Test handing exeptions in client_connection trigger
+
+drop table connects;
+-- superuser should ignore error
+\c
+-- suppress trigger firing
+\c "dbname=regression options='-c disable_client_connection_trigger=true'"
+
+
+-- Cleanup
+drop event trigger on_login_trigger;
+drop function on_login_proc();
#46Pavel Stehule
pavel.stehule@gmail.com
In reply to: Konstantin Knizhnik (#45)
Re: On login trigger: take three

Hi

po 21. 12. 2020 v 11:06 odesílatel Konstantin Knizhnik <
k.knizhnik@postgrespro.ru> napsal:

On 20.12.2020 10:04, Pavel Stehule wrote:

čt 17. 12. 2020 v 19:30 odesílatel Pavel Stehule <pavel.stehule@gmail.com>
napsal:

čt 17. 12. 2020 v 14:04 odesílatel Konstantin Knizhnik <
k.knizhnik@postgrespro.ru> napsal:

On 17.12.2020 9:31, Pavel Stehule wrote:

st 16. 12. 2020 v 20:38 odesílatel Pavel Stehule <
pavel.stehule@gmail.com> napsal:

Attached please find new versoin of the patch based on
on_connect_event_trigger_WITH_SUGGESTED_UPDATES.patch

So there is still only "disable_client_connection_trigger" GUC?
because we need possibility to disable client connect triggers and there is
no such need for other event types.

As you suggested have added "dathaslogontriggers" flag which is set
when client connection trigger is created.
This flag is never cleaned (to avoid visibility issues mentioned in my
previous mail).

This is much better - I don't see any slowdown when logon trigger is
not defined

I did some benchmarks and looks so starting language handler is
relatively expensive - it is about 25% and starting handler like event
trigger has about 35%. But this problem can be solved later and elsewhere.

I prefer the inverse form of disable_connection_trigger. Almost all GUC
are in positive form - so enable_connection_triggger is better

I don't think so current handling dathaslogontriggers is good for
production usage. The protection against race condition can be solved by
lock on pg_event_trigger

I thought about it, and probably the counter of connect triggers will be
better there. The implementation will be simpler and more robust.

I prefer to implement different approach: unset dathaslogontriggers
flag in event trigger itself when no triggers are returned by
EventTriggerCommonSetup.
I am using double checking to prevent race condition.
The main reason for this approach is that dropping of triggers is not
done in event_trigger.c: it is done by generic code for all resources.
It seems to be there is no right place where decrementing of trigger
counters can be inserted.
Also I prefer to keep all logon-trigger specific code in one file.

Also, as you suggested, I renamed disable_connection_trigger to
enable_connection_trigger.

New version of the patch is attached.

yes, it can work

for complete review the new field in pg_doc should be documented

Done.
Also I fixed some examples in documentation which involves pg_database.

regress tests fails

sysviews ... FAILED 112 ms
test event_trigger ... FAILED (test process exited with exit
code 2) 447 ms
test fast_default ... FAILED 392 ms
test stats ... FAILED 626 ms
============== shutting down postmaster ==============

Regards

Pavel

Show quoted text

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#47Konstantin Knizhnik
k.knizhnik@postgrespro.ru
In reply to: Pavel Stehule (#46)
1 attachment(s)
Re: On login trigger: take three

On 22.12.2020 12:25, Pavel Stehule wrote:

regress tests fails

     sysviews                     ... FAILED      112 ms
test event_trigger                ... FAILED (test process exited with
exit code 2)      447 ms
test fast_default                 ... FAILED      392 ms
test stats                        ... FAILED      626 ms
============== shutting down postmaster ==============

Sorry, fixed.

--

Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

Attachments:

on_connect_event_trigger-11.patchtext/x-patch; name=on_connect_event_trigger-11.patchDownload
diff --git a/doc/src/sgml/bki.sgml b/doc/src/sgml/bki.sgml
index 036a72c..bb62f25 100644
--- a/doc/src/sgml/bki.sgml
+++ b/doc/src/sgml/bki.sgml
@@ -182,7 +182,7 @@
 { oid =&gt; '1', oid_symbol =&gt; 'TemplateDbOid',
   descr =&gt; 'database\'s default template',
   datname =&gt; 'template1', encoding =&gt; 'ENCODING', datcollate =&gt; 'LC_COLLATE',
-  datctype =&gt; 'LC_CTYPE', datistemplate =&gt; 't', datallowconn =&gt; 't',
+  datctype =&gt; 'LC_CTYPE', datistemplate =&gt; 't', datallowconn =&gt; 't', dathaslogontriggers =&gt; 'f',
   datconnlimit =&gt; '-1', datlastsysoid =&gt; '0', datfrozenxid =&gt; '0',
   datminmxid =&gt; '1', dattablespace =&gt; 'pg_default', datacl =&gt; '_null_' },
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 5698413..010bff5 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2946,6 +2946,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
 
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>dathaslogontriggers</structfield> <type>bool</type>
+      </para>
+      <para>
+        Indicates that there are client connection triggers defined for this database.
+        This flag is used to avoid extra lookup of pg_event_trigger table on each backend startup.
+        This flag is used internally by Postgres and should not be manually changed by DBA or application.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>datconnlimit</structfield> <type>int4</type>
       </para>
       <para>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index f810789..45d30b3 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -996,6 +996,23 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-enable-client-connection-trigger" xreflabel="enable_client_connection_trigger">
+      <term><varname>enable_client_connection_trigger</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>enable_client_connection_trigger</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Enables firing the <literal>client_connection</literal>
+        trigger when a client connects. This parameter is switched on by default.
+        Errors in trigger code can prevent user to login to the system.
+        I this case disabling this parameter in connection string can solve the problem:
+        <literal>psql "dbname=postgres options='-c enable_client_connection_trigger=false'". 
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
      </sect2>
 
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index 14dcbdb..383f782 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -4673,6 +4673,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = t (type: 1)
 datallowconn = t (type: 1)
+dathaslogontriggers = f (type: 1)
 datconnlimit = -1 (type: 5)
 datlastsysoid = 11510 (type: 1)
 datfrozenxid = 379 (type: 1)
@@ -4698,6 +4699,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = f (type: 1)
 datallowconn = t (type: 1)
+dathaslogontriggers = f (type: 1)
 datconnlimit = -1 (type: 5)
 datlastsysoid = 11510 (type: 1)
 datfrozenxid = 379 (type: 1)
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 60366a9..ae40a8e 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>client_connection</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -36,6 +37,29 @@
    </para>
 
    <para>
+     The <literal>client_connection</literal> event occurs when a client connection
+     to the server is established.
+     There are two mechanisms for dealing with any bugs in a trigger procedure for
+     this event which might prevent successful login to the system:
+     <itemizedlist>
+      <listitem>
+       <para>
+         The configuration parameter <literal>enable_client_connection_trigger</literal>
+         makes it possible to disable firing the <literal>client_connection</literal>
+         trigger when a client connects.
+       </para>
+      </listitem>
+      <listitem>
+       <para>
+         Errors in the <literal>client_connection</literal> trigger procedure are
+         ignored for superuser. An error message is delivered to the client as
+         <literal>NOTICE</literal> in this case.
+       </para>
+      </listitem>
+     </itemizedlist>
+   </para>
+
+   <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
      <literal>SECURITY LABEL</literal>,
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index f27c3fe..8646db7 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -560,6 +560,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	new_record[Anum_pg_database_datctype - 1] =
 		DirectFunctionCall1(namein, CStringGetDatum(dbctype));
 	new_record[Anum_pg_database_datistemplate - 1] = BoolGetDatum(dbistemplate);
+	new_record[Anum_pg_database_dathaslogontriggers - 1] = BoolGetDatum(false);
 	new_record[Anum_pg_database_datallowconn - 1] = BoolGetDatum(dballowconnections);
 	new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 	new_record[Anum_pg_database_datlastsysoid - 1] = ObjectIdGetDatum(src_lastsysoid);
@@ -1627,7 +1628,7 @@ AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel)
 		new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 		new_record_repl[Anum_pg_database_datconnlimit - 1] = true;
 	}
-
+	new_record[Anum_pg_database_dathaslogontriggers - 1] = BoolGetDatum(datform->dathaslogontriggers);
 	newtuple = heap_modify_tuple(tuple, RelationGetDescr(rel), new_record,
 								 new_record_nulls, new_record_repl);
 	CatalogTupleUpdate(rel, &tuple->t_self, newtuple);
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 3ffba4e..855b656 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -43,11 +44,14 @@
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
 #include "utils/syscache.h"
 
+bool enable_client_connection_trigger;
+
 typedef struct EventTriggerQueryState
 {
 	/* memory context for this state's objects */
@@ -130,6 +134,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "client_connection") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -293,6 +298,23 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
 	CatalogTupleInsert(tgrel, tuple);
 	heap_freetuple(tuple);
 
+	if (strcmp(eventname, "client_connection") == 0)
+	{
+		Form_pg_database db;
+		Relation pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+		/* Set dathaslogontriggers flag in pg_database */
+		tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+		db = (Form_pg_database) GETSTRUCT(tuple);
+		if (!db->dathaslogontriggers)
+		{
+			db->dathaslogontriggers = true;
+			CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+		}
+		table_close(pg_db, RowExclusiveLock);
+	}
+
 	/* Depend on owner. */
 	recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
 
@@ -562,6 +584,9 @@ EventTriggerCommonSetup(Node *parsetree,
 	ListCell   *lc;
 	List	   *runlist = NIL;
 
+	/* Get the command tag. */
+	tag = parsetree ? CreateCommandTag(parsetree) : CMDTAG_CONNECT;
+
 	/*
 	 * We want the list of command tags for which this procedure is actually
 	 * invoked to match up exactly with the list that CREATE EVENT TRIGGER
@@ -577,22 +602,18 @@ EventTriggerCommonSetup(Node *parsetree,
 	 * relevant command tag.
 	 */
 #ifdef USE_ASSERT_CHECKING
+	if (event == EVT_DDLCommandStart ||
+		event == EVT_DDLCommandEnd ||
+		event == EVT_SQLDrop ||
+		event == EVT_Connect)
 	{
-		CommandTag	dbgtag;
-
-		dbgtag = CreateCommandTag(parsetree);
-		if (event == EVT_DDLCommandStart ||
-			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
-		{
-			if (!command_tag_event_trigger_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
-		else if (event == EVT_TableRewrite)
-		{
-			if (!command_tag_table_rewrite_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
+		if (!command_tag_event_trigger_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
+	}
+	else if (event == EVT_TableRewrite)
+	{
+		if (!command_tag_table_rewrite_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
 	}
 #endif
 
@@ -601,9 +622,6 @@ EventTriggerCommonSetup(Node *parsetree,
 	if (cachelist == NIL)
 		return NIL;
 
-	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
-
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
 	 * memory context.  Once we start running the command triggers, or indeed
@@ -800,6 +818,117 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+static bool
+DatabaseHasLogonTriggers(void)
+{
+	bool has_logon_triggers;
+	HeapTuple tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+	has_logon_triggers = ((Form_pg_database) GETSTRUCT(tuple))->dathaslogontriggers;
+	ReleaseSysCache(tuple);
+	return has_logon_triggers;
+}
+
+/*
+ * Fire connect triggers.
+ */
+void
+EventTriggerOnConnect(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster
+		|| !OidIsValid(MyDatabaseId)
+		|| !enable_client_connection_trigger)
+		return;
+
+	StartTransactionCommand();
+
+	if (DatabaseHasLogonTriggers())
+	{
+		runlist = EventTriggerCommonSetup(NULL,
+										  EVT_Connect, "connect",
+										  &trigdata);
+
+		if (runlist != NIL)
+		{
+			MemoryContext old_context = CurrentMemoryContext;
+			bool is_superuser = superuser();
+			/*
+			 * Make sure anything the main command did will be visible to the event
+			 * triggers.
+			 */
+			CommandCounterIncrement();
+
+			/* Run the triggers. */
+			PG_TRY();
+			{
+				EventTriggerInvoke(runlist, &trigdata);
+				list_free(runlist);
+			}
+			PG_CATCH();
+			{
+				ErrorData* error;
+				/*
+				 * Try to ignore error for superuser to make it possible to login even in case of errors
+				 * during trigger execution
+				 */
+				if (!is_superuser)
+					PG_RE_THROW();
+
+				MemoryContextSwitchTo(old_context);
+				error = CopyErrorData();
+				FlushErrorState();
+				elog(NOTICE, "client_connection trigger failed with message: %s", error->message);
+				AbortCurrentTransaction();
+				return;
+			}
+			PG_END_TRY();
+		}
+		else
+		{
+			/* Runtlist is empty: clear dathaslogontriggers flag
+			 */
+			Relation pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+			HeapTuple tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+			Form_pg_database db;
+
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+			db = (Form_pg_database) GETSTRUCT(tuple);
+			if (db->dathaslogontriggers)
+			{
+				db->dathaslogontriggers = false;
+				CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				/*
+				 * There can be race condition: event trigger may be added after we have scanned
+				 * pg_event_trigger table. Repeat this test nuder  pg_database table lock.
+				 */
+				AcceptInvalidationMessages();
+				runlist = EventTriggerCommonSetup(NULL,
+												  EVT_Connect, "connect",
+												  &trigdata);
+				if (runlist != NULL) /* if list is not empty, then restore the flag */
+				{
+					db->dathaslogontriggers = true;
+					CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				}
+			}
+			table_close(pg_db, RowExclusiveLock);
+		}
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 3679799..ce9b98e 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -42,6 +42,7 @@
 #include "catalog/pg_type.h"
 #include "commands/async.h"
 #include "commands/prepare.h"
+#include "commands/event_trigger.h"
 #include "executor/spi.h"
 #include "jit/jit.h"
 #include "libpq/libpq.h"
@@ -167,6 +168,9 @@ static ProcSignalReason RecoveryConflictReason;
 static MemoryContext row_description_context = NULL;
 static StringInfoData row_description_buf;
 
+/* Hook for plugins to get control at start of session */
+client_connection_hook_type client_connection_hook = EventTriggerOnConnect;
+
 /* ----------------------------------------------------------------
  *		decls for routines only used in this file
  * ----------------------------------------------------------------
@@ -4012,6 +4016,11 @@ PostgresMain(int argc, char *argv[],
 	if (!IsUnderPostmaster)
 		PgStartTime = GetCurrentTimestamp();
 
+	if (client_connection_hook)
+	{
+		(*client_connection_hook) ();
+	}
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 0427795..c621b8f 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -168,6 +168,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "client_connection") == 0)
+			event = EVT_Connect;
 		else
 			continue;
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 245a347..e42fc0e 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -43,6 +43,7 @@
 #include "commands/async.h"
 #include "commands/prepare.h"
 #include "commands/trigger.h"
+#include "commands/event_trigger.h"
 #include "commands/user.h"
 #include "commands/vacuum.h"
 #include "commands/variable.h"
@@ -928,6 +929,18 @@ static const unit_conversion time_unit_conversion_table[] =
 static struct config_bool ConfigureNamesBool[] =
 {
 	{
+		{"enable_client_connection_trigger", PGC_SU_BACKEND, DEVELOPER_OPTIONS,
+			gettext_noop("Enables the client_connection event trigger."),
+			gettext_noop("In case of errors in the ON client_connection EVENT TRIGGER procedure, "
+						 "this parameter can be used to disable trigger activation "
+						 "and provide access to the database."),
+			GUC_EXPLAIN
+		},
+		&enable_client_connection_trigger,
+		true,
+		NULL, NULL, NULL
+	},
+	{
 		{"enable_seqscan", PGC_USERSET, QUERY_TUNING_METHOD,
 			gettext_noop("Enables the planner's use of sequential-scan plans."),
 			NULL,
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index dc1d41d..d286401 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -2726,6 +2726,7 @@ dumpDatabase(Archive *fout)
 				i_datacl,
 				i_rdatacl,
 				i_datistemplate,
+				i_dathaslogontriggers,
 				i_datconnlimit,
 				i_tablespace;
 	CatalogId	dbCatId;
@@ -2738,6 +2739,7 @@ dumpDatabase(Archive *fout)
 			   *datacl,
 			   *rdatacl,
 			   *datistemplate,
+			   *dathaslogontriggers,
 			   *datconnlimit,
 			   *tablespace;
 	uint32		frozenxid,
@@ -2756,7 +2758,7 @@ dumpDatabase(Archive *fout)
 	 * (pg_init_privs) are not supported on databases, so this logic cannot
 	 * make use of buildACLQueries().
 	 */
-	if (fout->remoteVersion >= 90600)
+	if (fout->remoteVersion >= 140000)
 	{
 		appendPQExpBuffer(dbQry, "SELECT tableoid, oid, datname, "
 						  "(%s datdba) AS dba, "
@@ -2782,7 +2784,41 @@ dumpDatabase(Archive *fout)
 						  "       AS permp(orig_acl) "
 						  "     WHERE acl = orig_acl)) AS rdatacls) "
 						  " AS rdatacl, "
-						  "datistemplate, datconnlimit, "
+						  "datistemplate, datconnlimit, dathaslogontriggers, "
+						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
+						  "shobj_description(oid, 'pg_database') AS description "
+
+						  "FROM pg_database "
+						  "WHERE datname = current_database()",
+						  username_subquery);
+	}
+	else if (fout->remoteVersion >= 90600)
+	{
+		appendPQExpBuffer(dbQry, "SELECT tableoid, oid, datname, "
+						  "(%s datdba) AS dba, "
+						  "pg_encoding_to_char(encoding) AS encoding, "
+						  "datcollate, datctype, datfrozenxid, datminmxid, "
+						  "(SELECT array_agg(acl ORDER BY row_n) FROM "
+						  "  (SELECT acl, row_n FROM "
+						  "     unnest(coalesce(datacl,acldefault('d',datdba))) "
+						  "     WITH ORDINALITY AS perm(acl,row_n) "
+						  "   WHERE NOT EXISTS ( "
+						  "     SELECT 1 "
+						  "     FROM unnest(acldefault('d',datdba)) "
+						  "       AS init(init_acl) "
+						  "     WHERE acl = init_acl)) AS datacls) "
+						  " AS datacl, "
+						  "(SELECT array_agg(acl ORDER BY row_n) FROM "
+						  "  (SELECT acl, row_n FROM "
+						  "     unnest(acldefault('d',datdba)) "
+						  "     WITH ORDINALITY AS initp(acl,row_n) "
+						  "   WHERE NOT EXISTS ( "
+						  "     SELECT 1 "
+						  "     FROM unnest(coalesce(datacl,acldefault('d',datdba))) "
+						  "       AS permp(orig_acl) "
+						  "     WHERE acl = orig_acl)) AS rdatacls) "
+						  " AS rdatacl, "
+						  "datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2796,7 +2832,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "datcollate, datctype, datfrozenxid, datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogontriggers"
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2810,7 +2846,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "datcollate, datctype, datfrozenxid, 0 AS datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2824,7 +2860,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "NULL AS datcollate, NULL AS datctype, datfrozenxid, 0 AS datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2839,7 +2875,7 @@ dumpDatabase(Archive *fout)
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "NULL AS datcollate, NULL AS datctype, datfrozenxid, 0 AS datminmxid, "
 						  "datacl, '' as rdatacl, datistemplate, "
-						  "-1 as datconnlimit, "
+						  "-1 as datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace "
 						  "FROM pg_database "
 						  "WHERE datname = current_database()",
@@ -2861,6 +2897,7 @@ dumpDatabase(Archive *fout)
 	i_rdatacl = PQfnumber(res, "rdatacl");
 	i_datistemplate = PQfnumber(res, "datistemplate");
 	i_datconnlimit = PQfnumber(res, "datconnlimit");
+	i_dathaslogontriggers = PQfnumber(res, "dathaslogontriggers");
 	i_tablespace = PQfnumber(res, "tablespace");
 
 	dbCatId.tableoid = atooid(PQgetvalue(res, 0, i_tableoid));
@@ -2875,6 +2912,7 @@ dumpDatabase(Archive *fout)
 	datacl = PQgetvalue(res, 0, i_datacl);
 	rdatacl = PQgetvalue(res, 0, i_rdatacl);
 	datistemplate = PQgetvalue(res, 0, i_datistemplate);
+	dathaslogontriggers = PQgetvalue(res, 0, i_dathaslogontriggers);
 	datconnlimit = PQgetvalue(res, 0, i_datconnlimit);
 	tablespace = PQgetvalue(res, 0, i_tablespace);
 
@@ -3048,6 +3086,14 @@ dumpDatabase(Archive *fout)
 		appendPQExpBufferStr(delQry, ";\n");
 	}
 
+	if (strcmp(dathaslogontriggers, "t") == 0)
+	{
+		appendPQExpBufferStr(creaQry, "UPDATE pg_catalog.pg_database "
+							 "SET dathaslogontriggers = true WHERE datname = ");
+		appendStringLiteralAH(creaQry, datname, fout);
+		appendPQExpBufferStr(creaQry, ";\n");
+	}
+
 	/* Add database-specific SET options */
 	dumpDatabaseConfig(fout, creaQry, datname, dbCatId.oid);
 
diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat
index 21cd660..4c10575 100644
--- a/src/include/catalog/pg_database.dat
+++ b/src/include/catalog/pg_database.dat
@@ -15,7 +15,7 @@
 { oid => '1', oid_symbol => 'TemplateDbOid',
   descr => 'default template for new databases',
   datname => 'template1', encoding => 'ENCODING', datcollate => 'LC_COLLATE',
-  datctype => 'LC_CTYPE', datistemplate => 't', datallowconn => 't',
+  datctype => 'LC_CTYPE', datistemplate => 't', dathaslogontriggers => 'f', datallowconn => 't',
   datconnlimit => '-1', datlastsysoid => '0', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datacl => '_null_' },
 
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index 47bcf40..7dc15a5 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -52,6 +52,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
 	/* new connections allowed? */
 	bool		datallowconn;
 
+	/* database has on-login triggers */
+	bool		dathaslogontriggers;
+
 	/* max connections allowed (-1=no limit) */
 	int32		datconnlimit;
 
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 407fd6a..47d481e 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -21,6 +21,8 @@
 #include "tcop/deparse_utility.h"
 #include "utils/aclchk_internal.h"
 
+extern bool enable_client_connection_trigger; /* GUC */
+
 typedef struct EventTriggerData
 {
 	NodeTag		type;
@@ -53,6 +55,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnConnect(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index be94852..988aa39 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -80,6 +80,7 @@ PG_CMDTAG(CMDTAG_CLUSTER, "CLUSTER", false, false, false)
 PG_CMDTAG(CMDTAG_COMMENT, "COMMENT", true, false, false)
 PG_CMDTAG(CMDTAG_COMMIT, "COMMIT", false, false, false)
 PG_CMDTAG(CMDTAG_COMMIT_PREPARED, "COMMIT PREPARED", false, false, false)
+PG_CMDTAG(CMDTAG_CONNECT, "CONNECT", true, false, false)
 PG_CMDTAG(CMDTAG_COPY, "COPY", false, false, true)
 PG_CMDTAG(CMDTAG_COPY_FROM, "COPY FROM", false, false, false)
 PG_CMDTAG(CMDTAG_CREATE_ACCESS_METHOD, "CREATE ACCESS METHOD", true, false, false)
diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h
index bd30607..90376b2 100644
--- a/src/include/tcop/tcopprot.h
+++ b/src/include/tcop/tcopprot.h
@@ -30,6 +30,11 @@ extern PGDLLIMPORT const char *debug_query_string;
 extern int	max_stack_depth;
 extern int	PostAuthDelay;
 
+/* Hook for plugins to get control at start and end of session */
+typedef void (*client_connection_hook_type) (void);
+
+extern PGDLLIMPORT client_connection_hook_type client_connection_hook;
+
 /* GUC-configurable parameters */
 
 typedef enum
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index bb1e39e..ab7e8c3 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Connect,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/000_client_connection_trigger.pl b/src/test/recovery/t/000_client_connection_trigger.pl
new file mode 100644
index 0000000..3dcd475
--- /dev/null
+++ b/src/test/recovery/t/000_client_connection_trigger.pl
@@ -0,0 +1,69 @@
+use strict;
+use warnings;
+use PostgresNode;
+use TestLib;
+use Test::More;
+if (!$use_unix_sockets)
+{
+	plan skip_all =>
+	  "authentication tests cannot run without Unix-domain sockets";
+}
+else
+{
+	plan tests => 5;
+}
+
+# Initialize master node
+my $node = get_new_node('master');
+$node->init;
+$node->start;
+$node->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+CREATE ROLE regress_hacker LOGIN PASSWORD 'pass';
+
+CREATE TABLE connects(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO connects (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON client_connection EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+}
+);
+my $res;
+
+$res = $node->safe_psql('postgres', "SELECT 1");
+
+$res = $node->safe_psql('postgres', "SELECT 1",
+  extra_params => [ '-U', 'regress_user', '-w' ]);
+
+my ($ret, $stdout, $stderr) = $node->psql('postgres', "SELECT 1",
+  extra_params => [ '-U', 'regress_hacker', '-w' ]);
+ok( $ret != 0 && $stderr =~ /You are not welcome!/ );
+
+$res = $node->safe_psql('postgres', "SELECT COUNT(1) FROM connects WHERE who = 'regress_user'");
+ok($res == 1);
+
+my $tempdir = TestLib::tempdir;
+command_ok(
+  [ "pg_dumpall", '-p', $node->port, '-c', "--file=$tempdir/regression_dump.sql", ],
+  "dumpall");
+# my $dump_contents = slurp_file("$tempdir/regression_dump.sql");
+# print($dump_contents);
+
+my $node1 = get_new_node('secondary');
+$node1->init;
+$node1->start;
+command_ok(["psql", '-p', $node1->port, '-b', '-f', "$tempdir/regression_dump.sql" ] );
+$res = $node1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$res = $node1->safe_psql('postgres', "SELECT COUNT(1) FROM connects WHERE who = 'regress_user'");
+ok($res == 2);
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 9e31a53..b4a21fb 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -43,6 +43,27 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+
+CREATE TABLE connects(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO connects (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON client_connection EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 $node_primary->wait_for_catchup($node_standby_1, 'replay',
 	$node_primary->lsn('insert'));
@@ -266,6 +287,9 @@ sub replay_check
 
 replay_check();
 
+$node_standby_1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$node_standby_2->safe_psql('postgres', "SELECT 2", extra_params => [ '-U', 'regress_user', '-w' ]);
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index bdd0ffc..bdfa3ee 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -536,3 +536,40 @@ NOTICE:  DROP POLICY - ddl_command_end
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- On session start triggers
+create table connects(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into connects (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on client_connection execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id | who  
+----+------
+  1 | I am
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id | who  
+----+------
+  1 | I am
+  2 | I am
+(2 rows)
+
+-- Test handing exeptions in client_connection trigger
+drop table connects;
+-- superuser should ignore error
+\c
+NOTICE:  client_connection trigger failed with message: relation "connects" does not exist
+-- suppress trigger firing
+\c "dbname=regression options='-c enable_client_connection_trigger=false'"
+-- Cleanup
+drop event trigger on_login_trigger;
+drop function on_login_proc();
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 81bdacf..88a5102 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -86,27 +86,28 @@ select count(*) = 1 as ok from pg_stat_wal;
 -- This is to record the prevailing planner enable_foo settings during
 -- a regression test run.
 select name, setting from pg_settings where name like 'enable%';
-              name              | setting 
---------------------------------+---------
- enable_bitmapscan              | on
- enable_gathermerge             | on
- enable_hashagg                 | on
- enable_hashjoin                | on
- enable_incremental_sort        | on
- enable_indexonlyscan           | on
- enable_indexscan               | on
- enable_material                | on
- enable_mergejoin               | on
- enable_nestloop                | on
- enable_parallel_append         | on
- enable_parallel_hash           | on
- enable_partition_pruning       | on
- enable_partitionwise_aggregate | off
- enable_partitionwise_join      | off
- enable_seqscan                 | on
- enable_sort                    | on
- enable_tidscan                 | on
-(18 rows)
+               name               | setting 
+----------------------------------+---------
+ enable_bitmapscan                | on
+ enable_client_connection_trigger | on
+ enable_gathermerge               | on
+ enable_hashagg                   | on
+ enable_hashjoin                  | on
+ enable_incremental_sort          | on
+ enable_indexonlyscan             | on
+ enable_indexscan                 | on
+ enable_material                  | on
+ enable_mergejoin                 | on
+ enable_nestloop                  | on
+ enable_parallel_append           | on
+ enable_parallel_hash             | on
+ enable_partition_pruning         | on
+ enable_partitionwise_aggregate   | off
+ enable_partitionwise_join        | off
+ enable_seqscan                   | on
+ enable_sort                      | on
+ enable_tidscan                   | on
+(19 rows)
 
 -- Test that the pg_timezone_names and pg_timezone_abbrevs views are
 -- more-or-less working.  We can't test their contents in any great detail
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 18b2a26..abece80 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -429,3 +429,31 @@ DROP POLICY p2 ON event_trigger_test;
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- On session start triggers
+create table connects(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into connects (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on client_connection execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+select * from connects;
+\c
+select * from connects;
+
+-- Test handing exeptions in client_connection trigger
+
+drop table connects;
+-- superuser should ignore error
+\c
+-- suppress trigger firing
+\c "dbname=regression options='-c enable_client_connection_trigger=false'"
+
+
+-- Cleanup
+drop event trigger on_login_trigger;
+drop function on_login_proc();
#48Pavel Stehule
pavel.stehule@gmail.com
In reply to: Konstantin Knizhnik (#47)
Re: On login trigger: take three

út 22. 12. 2020 v 12:42 odesílatel Konstantin Knizhnik <
k.knizhnik@postgrespro.ru> napsal:

On 22.12.2020 12:25, Pavel Stehule wrote:

regress tests fails

sysviews ... FAILED 112 ms
test event_trigger ... FAILED (test process exited with
exit code 2) 447 ms
test fast_default ... FAILED 392 ms
test stats ... FAILED 626 ms
============== shutting down postmaster ==============

Sorry, fixed.

no problem

I had to fix doc

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 6ff783273f..7aded1848f 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1008,7 +1008,7 @@ include_dir 'conf.d'
         trigger when a client connects. This parameter is switched on by
default.
         Errors in trigger code can prevent user to login to the system.
         I this case disabling this parameter in connection string can
solve the problem:
-        <literal>psql "dbname=postgres options='-c
enable_client_connection_trigger=false'".
+        <literal>psql "dbname=postgres options='-c
enable_client_connection_trigger=false'"</literal>.
        </para>
       </listitem>
      </varlistentry>

I am thinking again about enable_client_connection_trigger, and although it
can look useless (because error is ignored for superuser), it can be useful
for some debugging and administration purposes. Probably we don't want to
start the client_connection trigger from backup tools, maybe from some
monitoring tools. Maybe the possibility to set this GUC can be dedicated to
some special role (like pg_signal_backend).

+     <varlistentry id="guc-enable-client-connection-trigger"
xreflabel="enable_client_connection_trigger">
+      <term><varname>enable_client_connection_trigger</varname>
(<type>boolean</type>)
+      <indexterm>
+       <primary><varname>enable_client_connection_trigger</varname>
configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Enables firing the <literal>client_connection</literal>
+        trigger when a client connects. This parameter is switched on by
default.
+        Errors in trigger code can prevent user to login to the system.
+        I this case disabling this parameter in connection string can
solve the problem:
+        <literal>psql "dbname=postgres options='-c
enable_client_connection_trigger=false'"</literal>.
+       </para>
+      </listitem>
+     </varlistentry>

There should be note, so only superuser can change this value

There is should be tab-complete support

diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 3a43c09bf6..08f00d8fc4 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -2970,7 +2970,8 @@ psql_completion(const char *text, int start, int end)
        COMPLETE_WITH("ON");
    /* Complete CREATE EVENT TRIGGER <name> ON with event_type */
    else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-       COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop");
+       COMPLETE_WITH("ddl_command_start", "ddl_command_end",
+                     "client_connection", "sql_drop");

/*
* Complete CREATE EVENT TRIGGER <name> ON <event_type>. EXECUTE
FUNCTION

Regards

Pavel

Show quoted text

--

Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#49Konstantin Knizhnik
k.knizhnik@postgrespro.ru
In reply to: Pavel Stehule (#48)
1 attachment(s)
Re: On login trigger: take three

On 22.12.2020 21:19, Pavel Stehule wrote:

út 22. 12. 2020 v 12:42 odesílatel Konstantin Knizhnik
<k.knizhnik@postgrespro.ru <mailto:k.knizhnik@postgrespro.ru>> napsal:

On 22.12.2020 12:25, Pavel Stehule wrote:

regress tests fails

     sysviews                     ... FAILED    112 ms
test event_trigger                ... FAILED (test process exited
with exit code 2)      447 ms
test fast_default                 ... FAILED  392 ms
test stats                        ... FAILED  626 ms
============== shutting down postmaster     ==============

Sorry, fixed.

no problem

I had to fix doc

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 6ff783273f..7aded1848f 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1008,7 +1008,7 @@ include_dir 'conf.d'
         trigger when a client connects. This parameter is switched on 
by default.
         Errors in trigger code can prevent user to login to the system.
         I this case disabling this parameter in connection string can 
solve the problem:
-        <literal>psql "dbname=postgres options='-c 
enable_client_connection_trigger=false'".
+        <literal>psql "dbname=postgres options='-c 
enable_client_connection_trigger=false'"</literal>.
        </para>
       </listitem>
      </varlistentry>

I am thinking again about enable_client_connection_trigger, and
although it can look useless (because error is ignored for superuser),
it can be useful for some debugging and administration purposes.
Probably we don't want to start the client_connection trigger from
backup tools, maybe from some monitoring tools. Maybe the possibility
to set this GUC can be dedicated to some special role (like
pg_signal_backend).

+     <varlistentry id="guc-enable-client-connection-trigger" 
xreflabel="enable_client_connection_trigger">
+  <term><varname>enable_client_connection_trigger</varname> 
(<type>boolean</type>)
+      <indexterm>
+ <primary><varname>enable_client_connection_trigger</varname> 
configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Enables firing the <literal>client_connection</literal>
+        trigger when a client connects. This parameter is switched on 
by default.
+        Errors in trigger code can prevent user to login to the system.
+        I this case disabling this parameter in connection string can 
solve the problem:
+        <literal>psql "dbname=postgres options='-c 
enable_client_connection_trigger=false'"</literal>.
+       </para>
+      </listitem>
+     </varlistentry>

There should be note, so only superuser can change this value

There is should be tab-complete support

diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 3a43c09bf6..08f00d8fc4 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -2970,7 +2970,8 @@ psql_completion(const char *text, int start, int 
end)
        COMPLETE_WITH("ON");
    /* Complete CREATE EVENT TRIGGER <name> ON with event_type */
    else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-       COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop");
+       COMPLETE_WITH("ddl_command_start", "ddl_command_end",
+                     "client_connection", "sql_drop");

    /*
     * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE
FUNCTION

Thank you.
I have applied all your fixes in on_connect_event_trigger-12.patch.

Concerning enable_client_connection_trigger GUC, I think that it is
really useful: it is the fastest and simplest way to disable login
triggers in case
of some problems with them (not only for superuser itself, but for all
users). Yes, it can be also done using "ALTER EVENT TRIGGER DISABLE".
But assume that you have a lot of databases with different login
policies enforced by on-login event triggers. And you want temporary
disable them all, for example for testing purposes.
In this case GUC is most convenient way to do it.

Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

Attachments:

on_connect_event_trigger-12.patchtext/x-patch; name=on_connect_event_trigger-12.patchDownload
diff --git a/doc/src/sgml/bki.sgml b/doc/src/sgml/bki.sgml
index 036a72c..bb62f25 100644
--- a/doc/src/sgml/bki.sgml
+++ b/doc/src/sgml/bki.sgml
@@ -182,7 +182,7 @@
 { oid =&gt; '1', oid_symbol =&gt; 'TemplateDbOid',
   descr =&gt; 'database\'s default template',
   datname =&gt; 'template1', encoding =&gt; 'ENCODING', datcollate =&gt; 'LC_COLLATE',
-  datctype =&gt; 'LC_CTYPE', datistemplate =&gt; 't', datallowconn =&gt; 't',
+  datctype =&gt; 'LC_CTYPE', datistemplate =&gt; 't', datallowconn =&gt; 't', dathaslogontriggers =&gt; 'f',
   datconnlimit =&gt; '-1', datlastsysoid =&gt; '0', datfrozenxid =&gt; '0',
   datminmxid =&gt; '1', dattablespace =&gt; 'pg_default', datacl =&gt; '_null_' },
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 5698413..010bff5 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2946,6 +2946,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
 
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>dathaslogontriggers</structfield> <type>bool</type>
+      </para>
+      <para>
+        Indicates that there are client connection triggers defined for this database.
+        This flag is used to avoid extra lookup of pg_event_trigger table on each backend startup.
+        This flag is used internally by Postgres and should not be manually changed by DBA or application.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>datconnlimit</structfield> <type>int4</type>
       </para>
       <para>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index f810789..8861f1b 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1,4 +1,4 @@
-<!-- doc/src/sgml/config.sgml -->
+\<!-- doc/src/sgml/config.sgml -->
 
 <chapter id="runtime-config">
   <title>Server Configuration</title>
@@ -996,6 +996,24 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-enable-client-connection-trigger" xreflabel="enable_client_connection_trigger">
+      <term><varname>enable_client_connection_trigger</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>enable_client_connection_trigger</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Enables firing the <literal>client_connection</literal>
+        trigger when a client connects. This parameter is switched on by default.
+        Errors in trigger code can prevent user to login to the system.
+        In this case disabling this parameter in connection string can solve the problem:
+        <literal>psql "dbname=postgres options='-c enable_client_connection_trigger=false'".</literal>
+        Only superuser can change this variable.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
      </sect2>
 
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index 14dcbdb..383f782 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -4673,6 +4673,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = t (type: 1)
 datallowconn = t (type: 1)
+dathaslogontriggers = f (type: 1)
 datconnlimit = -1 (type: 5)
 datlastsysoid = 11510 (type: 1)
 datfrozenxid = 379 (type: 1)
@@ -4698,6 +4699,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = f (type: 1)
 datallowconn = t (type: 1)
+dathaslogontriggers = f (type: 1)
 datconnlimit = -1 (type: 5)
 datlastsysoid = 11510 (type: 1)
 datfrozenxid = 379 (type: 1)
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 60366a9..ae40a8e 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>client_connection</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -36,6 +37,29 @@
    </para>
 
    <para>
+     The <literal>client_connection</literal> event occurs when a client connection
+     to the server is established.
+     There are two mechanisms for dealing with any bugs in a trigger procedure for
+     this event which might prevent successful login to the system:
+     <itemizedlist>
+      <listitem>
+       <para>
+         The configuration parameter <literal>enable_client_connection_trigger</literal>
+         makes it possible to disable firing the <literal>client_connection</literal>
+         trigger when a client connects.
+       </para>
+      </listitem>
+      <listitem>
+       <para>
+         Errors in the <literal>client_connection</literal> trigger procedure are
+         ignored for superuser. An error message is delivered to the client as
+         <literal>NOTICE</literal> in this case.
+       </para>
+      </listitem>
+     </itemizedlist>
+   </para>
+
+   <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
      <literal>SECURITY LABEL</literal>,
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index f27c3fe..8646db7 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -560,6 +560,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	new_record[Anum_pg_database_datctype - 1] =
 		DirectFunctionCall1(namein, CStringGetDatum(dbctype));
 	new_record[Anum_pg_database_datistemplate - 1] = BoolGetDatum(dbistemplate);
+	new_record[Anum_pg_database_dathaslogontriggers - 1] = BoolGetDatum(false);
 	new_record[Anum_pg_database_datallowconn - 1] = BoolGetDatum(dballowconnections);
 	new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 	new_record[Anum_pg_database_datlastsysoid - 1] = ObjectIdGetDatum(src_lastsysoid);
@@ -1627,7 +1628,7 @@ AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel)
 		new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 		new_record_repl[Anum_pg_database_datconnlimit - 1] = true;
 	}
-
+	new_record[Anum_pg_database_dathaslogontriggers - 1] = BoolGetDatum(datform->dathaslogontriggers);
 	newtuple = heap_modify_tuple(tuple, RelationGetDescr(rel), new_record,
 								 new_record_nulls, new_record_repl);
 	CatalogTupleUpdate(rel, &tuple->t_self, newtuple);
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 3ffba4e..855b656 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -43,11 +44,14 @@
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
 #include "utils/syscache.h"
 
+bool enable_client_connection_trigger;
+
 typedef struct EventTriggerQueryState
 {
 	/* memory context for this state's objects */
@@ -130,6 +134,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "client_connection") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -293,6 +298,23 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
 	CatalogTupleInsert(tgrel, tuple);
 	heap_freetuple(tuple);
 
+	if (strcmp(eventname, "client_connection") == 0)
+	{
+		Form_pg_database db;
+		Relation pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+		/* Set dathaslogontriggers flag in pg_database */
+		tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+		db = (Form_pg_database) GETSTRUCT(tuple);
+		if (!db->dathaslogontriggers)
+		{
+			db->dathaslogontriggers = true;
+			CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+		}
+		table_close(pg_db, RowExclusiveLock);
+	}
+
 	/* Depend on owner. */
 	recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
 
@@ -562,6 +584,9 @@ EventTriggerCommonSetup(Node *parsetree,
 	ListCell   *lc;
 	List	   *runlist = NIL;
 
+	/* Get the command tag. */
+	tag = parsetree ? CreateCommandTag(parsetree) : CMDTAG_CONNECT;
+
 	/*
 	 * We want the list of command tags for which this procedure is actually
 	 * invoked to match up exactly with the list that CREATE EVENT TRIGGER
@@ -577,22 +602,18 @@ EventTriggerCommonSetup(Node *parsetree,
 	 * relevant command tag.
 	 */
 #ifdef USE_ASSERT_CHECKING
+	if (event == EVT_DDLCommandStart ||
+		event == EVT_DDLCommandEnd ||
+		event == EVT_SQLDrop ||
+		event == EVT_Connect)
 	{
-		CommandTag	dbgtag;
-
-		dbgtag = CreateCommandTag(parsetree);
-		if (event == EVT_DDLCommandStart ||
-			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
-		{
-			if (!command_tag_event_trigger_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
-		else if (event == EVT_TableRewrite)
-		{
-			if (!command_tag_table_rewrite_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
+		if (!command_tag_event_trigger_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
+	}
+	else if (event == EVT_TableRewrite)
+	{
+		if (!command_tag_table_rewrite_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
 	}
 #endif
 
@@ -601,9 +622,6 @@ EventTriggerCommonSetup(Node *parsetree,
 	if (cachelist == NIL)
 		return NIL;
 
-	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
-
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
 	 * memory context.  Once we start running the command triggers, or indeed
@@ -800,6 +818,117 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+static bool
+DatabaseHasLogonTriggers(void)
+{
+	bool has_logon_triggers;
+	HeapTuple tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+	has_logon_triggers = ((Form_pg_database) GETSTRUCT(tuple))->dathaslogontriggers;
+	ReleaseSysCache(tuple);
+	return has_logon_triggers;
+}
+
+/*
+ * Fire connect triggers.
+ */
+void
+EventTriggerOnConnect(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster
+		|| !OidIsValid(MyDatabaseId)
+		|| !enable_client_connection_trigger)
+		return;
+
+	StartTransactionCommand();
+
+	if (DatabaseHasLogonTriggers())
+	{
+		runlist = EventTriggerCommonSetup(NULL,
+										  EVT_Connect, "connect",
+										  &trigdata);
+
+		if (runlist != NIL)
+		{
+			MemoryContext old_context = CurrentMemoryContext;
+			bool is_superuser = superuser();
+			/*
+			 * Make sure anything the main command did will be visible to the event
+			 * triggers.
+			 */
+			CommandCounterIncrement();
+
+			/* Run the triggers. */
+			PG_TRY();
+			{
+				EventTriggerInvoke(runlist, &trigdata);
+				list_free(runlist);
+			}
+			PG_CATCH();
+			{
+				ErrorData* error;
+				/*
+				 * Try to ignore error for superuser to make it possible to login even in case of errors
+				 * during trigger execution
+				 */
+				if (!is_superuser)
+					PG_RE_THROW();
+
+				MemoryContextSwitchTo(old_context);
+				error = CopyErrorData();
+				FlushErrorState();
+				elog(NOTICE, "client_connection trigger failed with message: %s", error->message);
+				AbortCurrentTransaction();
+				return;
+			}
+			PG_END_TRY();
+		}
+		else
+		{
+			/* Runtlist is empty: clear dathaslogontriggers flag
+			 */
+			Relation pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+			HeapTuple tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+			Form_pg_database db;
+
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+			db = (Form_pg_database) GETSTRUCT(tuple);
+			if (db->dathaslogontriggers)
+			{
+				db->dathaslogontriggers = false;
+				CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				/*
+				 * There can be race condition: event trigger may be added after we have scanned
+				 * pg_event_trigger table. Repeat this test nuder  pg_database table lock.
+				 */
+				AcceptInvalidationMessages();
+				runlist = EventTriggerCommonSetup(NULL,
+												  EVT_Connect, "connect",
+												  &trigdata);
+				if (runlist != NULL) /* if list is not empty, then restore the flag */
+				{
+					db->dathaslogontriggers = true;
+					CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				}
+			}
+			table_close(pg_db, RowExclusiveLock);
+		}
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 3679799..ce9b98e 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -42,6 +42,7 @@
 #include "catalog/pg_type.h"
 #include "commands/async.h"
 #include "commands/prepare.h"
+#include "commands/event_trigger.h"
 #include "executor/spi.h"
 #include "jit/jit.h"
 #include "libpq/libpq.h"
@@ -167,6 +168,9 @@ static ProcSignalReason RecoveryConflictReason;
 static MemoryContext row_description_context = NULL;
 static StringInfoData row_description_buf;
 
+/* Hook for plugins to get control at start of session */
+client_connection_hook_type client_connection_hook = EventTriggerOnConnect;
+
 /* ----------------------------------------------------------------
  *		decls for routines only used in this file
  * ----------------------------------------------------------------
@@ -4012,6 +4016,11 @@ PostgresMain(int argc, char *argv[],
 	if (!IsUnderPostmaster)
 		PgStartTime = GetCurrentTimestamp();
 
+	if (client_connection_hook)
+	{
+		(*client_connection_hook) ();
+	}
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 0427795..c621b8f 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -168,6 +168,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "client_connection") == 0)
+			event = EVT_Connect;
 		else
 			continue;
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 245a347..e42fc0e 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -43,6 +43,7 @@
 #include "commands/async.h"
 #include "commands/prepare.h"
 #include "commands/trigger.h"
+#include "commands/event_trigger.h"
 #include "commands/user.h"
 #include "commands/vacuum.h"
 #include "commands/variable.h"
@@ -928,6 +929,18 @@ static const unit_conversion time_unit_conversion_table[] =
 static struct config_bool ConfigureNamesBool[] =
 {
 	{
+		{"enable_client_connection_trigger", PGC_SU_BACKEND, DEVELOPER_OPTIONS,
+			gettext_noop("Enables the client_connection event trigger."),
+			gettext_noop("In case of errors in the ON client_connection EVENT TRIGGER procedure, "
+						 "this parameter can be used to disable trigger activation "
+						 "and provide access to the database."),
+			GUC_EXPLAIN
+		},
+		&enable_client_connection_trigger,
+		true,
+		NULL, NULL, NULL
+	},
+	{
 		{"enable_seqscan", PGC_USERSET, QUERY_TUNING_METHOD,
 			gettext_noop("Enables the planner's use of sequential-scan plans."),
 			NULL,
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index dc1d41d..d286401 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -2726,6 +2726,7 @@ dumpDatabase(Archive *fout)
 				i_datacl,
 				i_rdatacl,
 				i_datistemplate,
+				i_dathaslogontriggers,
 				i_datconnlimit,
 				i_tablespace;
 	CatalogId	dbCatId;
@@ -2738,6 +2739,7 @@ dumpDatabase(Archive *fout)
 			   *datacl,
 			   *rdatacl,
 			   *datistemplate,
+			   *dathaslogontriggers,
 			   *datconnlimit,
 			   *tablespace;
 	uint32		frozenxid,
@@ -2756,7 +2758,7 @@ dumpDatabase(Archive *fout)
 	 * (pg_init_privs) are not supported on databases, so this logic cannot
 	 * make use of buildACLQueries().
 	 */
-	if (fout->remoteVersion >= 90600)
+	if (fout->remoteVersion >= 140000)
 	{
 		appendPQExpBuffer(dbQry, "SELECT tableoid, oid, datname, "
 						  "(%s datdba) AS dba, "
@@ -2782,7 +2784,41 @@ dumpDatabase(Archive *fout)
 						  "       AS permp(orig_acl) "
 						  "     WHERE acl = orig_acl)) AS rdatacls) "
 						  " AS rdatacl, "
-						  "datistemplate, datconnlimit, "
+						  "datistemplate, datconnlimit, dathaslogontriggers, "
+						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
+						  "shobj_description(oid, 'pg_database') AS description "
+
+						  "FROM pg_database "
+						  "WHERE datname = current_database()",
+						  username_subquery);
+	}
+	else if (fout->remoteVersion >= 90600)
+	{
+		appendPQExpBuffer(dbQry, "SELECT tableoid, oid, datname, "
+						  "(%s datdba) AS dba, "
+						  "pg_encoding_to_char(encoding) AS encoding, "
+						  "datcollate, datctype, datfrozenxid, datminmxid, "
+						  "(SELECT array_agg(acl ORDER BY row_n) FROM "
+						  "  (SELECT acl, row_n FROM "
+						  "     unnest(coalesce(datacl,acldefault('d',datdba))) "
+						  "     WITH ORDINALITY AS perm(acl,row_n) "
+						  "   WHERE NOT EXISTS ( "
+						  "     SELECT 1 "
+						  "     FROM unnest(acldefault('d',datdba)) "
+						  "       AS init(init_acl) "
+						  "     WHERE acl = init_acl)) AS datacls) "
+						  " AS datacl, "
+						  "(SELECT array_agg(acl ORDER BY row_n) FROM "
+						  "  (SELECT acl, row_n FROM "
+						  "     unnest(acldefault('d',datdba)) "
+						  "     WITH ORDINALITY AS initp(acl,row_n) "
+						  "   WHERE NOT EXISTS ( "
+						  "     SELECT 1 "
+						  "     FROM unnest(coalesce(datacl,acldefault('d',datdba))) "
+						  "       AS permp(orig_acl) "
+						  "     WHERE acl = orig_acl)) AS rdatacls) "
+						  " AS rdatacl, "
+						  "datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2796,7 +2832,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "datcollate, datctype, datfrozenxid, datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogontriggers"
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2810,7 +2846,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "datcollate, datctype, datfrozenxid, 0 AS datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2824,7 +2860,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "NULL AS datcollate, NULL AS datctype, datfrozenxid, 0 AS datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2839,7 +2875,7 @@ dumpDatabase(Archive *fout)
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "NULL AS datcollate, NULL AS datctype, datfrozenxid, 0 AS datminmxid, "
 						  "datacl, '' as rdatacl, datistemplate, "
-						  "-1 as datconnlimit, "
+						  "-1 as datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace "
 						  "FROM pg_database "
 						  "WHERE datname = current_database()",
@@ -2861,6 +2897,7 @@ dumpDatabase(Archive *fout)
 	i_rdatacl = PQfnumber(res, "rdatacl");
 	i_datistemplate = PQfnumber(res, "datistemplate");
 	i_datconnlimit = PQfnumber(res, "datconnlimit");
+	i_dathaslogontriggers = PQfnumber(res, "dathaslogontriggers");
 	i_tablespace = PQfnumber(res, "tablespace");
 
 	dbCatId.tableoid = atooid(PQgetvalue(res, 0, i_tableoid));
@@ -2875,6 +2912,7 @@ dumpDatabase(Archive *fout)
 	datacl = PQgetvalue(res, 0, i_datacl);
 	rdatacl = PQgetvalue(res, 0, i_rdatacl);
 	datistemplate = PQgetvalue(res, 0, i_datistemplate);
+	dathaslogontriggers = PQgetvalue(res, 0, i_dathaslogontriggers);
 	datconnlimit = PQgetvalue(res, 0, i_datconnlimit);
 	tablespace = PQgetvalue(res, 0, i_tablespace);
 
@@ -3048,6 +3086,14 @@ dumpDatabase(Archive *fout)
 		appendPQExpBufferStr(delQry, ";\n");
 	}
 
+	if (strcmp(dathaslogontriggers, "t") == 0)
+	{
+		appendPQExpBufferStr(creaQry, "UPDATE pg_catalog.pg_database "
+							 "SET dathaslogontriggers = true WHERE datname = ");
+		appendStringLiteralAH(creaQry, datname, fout);
+		appendPQExpBufferStr(creaQry, ";\n");
+	}
+
 	/* Add database-specific SET options */
 	dumpDatabaseConfig(fout, creaQry, datname, dbCatId.oid);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 8afc780..81c5fc8 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -2958,7 +2958,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end",
+					  "client_connection", "sql_drop");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat
index 21cd660..4c10575 100644
--- a/src/include/catalog/pg_database.dat
+++ b/src/include/catalog/pg_database.dat
@@ -15,7 +15,7 @@
 { oid => '1', oid_symbol => 'TemplateDbOid',
   descr => 'default template for new databases',
   datname => 'template1', encoding => 'ENCODING', datcollate => 'LC_COLLATE',
-  datctype => 'LC_CTYPE', datistemplate => 't', datallowconn => 't',
+  datctype => 'LC_CTYPE', datistemplate => 't', dathaslogontriggers => 'f', datallowconn => 't',
   datconnlimit => '-1', datlastsysoid => '0', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datacl => '_null_' },
 
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index 47bcf40..7dc15a5 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -52,6 +52,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
 	/* new connections allowed? */
 	bool		datallowconn;
 
+	/* database has on-login triggers */
+	bool		dathaslogontriggers;
+
 	/* max connections allowed (-1=no limit) */
 	int32		datconnlimit;
 
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 407fd6a..47d481e 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -21,6 +21,8 @@
 #include "tcop/deparse_utility.h"
 #include "utils/aclchk_internal.h"
 
+extern bool enable_client_connection_trigger; /* GUC */
+
 typedef struct EventTriggerData
 {
 	NodeTag		type;
@@ -53,6 +55,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnConnect(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index be94852..988aa39 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -80,6 +80,7 @@ PG_CMDTAG(CMDTAG_CLUSTER, "CLUSTER", false, false, false)
 PG_CMDTAG(CMDTAG_COMMENT, "COMMENT", true, false, false)
 PG_CMDTAG(CMDTAG_COMMIT, "COMMIT", false, false, false)
 PG_CMDTAG(CMDTAG_COMMIT_PREPARED, "COMMIT PREPARED", false, false, false)
+PG_CMDTAG(CMDTAG_CONNECT, "CONNECT", true, false, false)
 PG_CMDTAG(CMDTAG_COPY, "COPY", false, false, true)
 PG_CMDTAG(CMDTAG_COPY_FROM, "COPY FROM", false, false, false)
 PG_CMDTAG(CMDTAG_CREATE_ACCESS_METHOD, "CREATE ACCESS METHOD", true, false, false)
diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h
index bd30607..90376b2 100644
--- a/src/include/tcop/tcopprot.h
+++ b/src/include/tcop/tcopprot.h
@@ -30,6 +30,11 @@ extern PGDLLIMPORT const char *debug_query_string;
 extern int	max_stack_depth;
 extern int	PostAuthDelay;
 
+/* Hook for plugins to get control at start and end of session */
+typedef void (*client_connection_hook_type) (void);
+
+extern PGDLLIMPORT client_connection_hook_type client_connection_hook;
+
 /* GUC-configurable parameters */
 
 typedef enum
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index bb1e39e..ab7e8c3 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Connect,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/000_client_connection_trigger.pl b/src/test/recovery/t/000_client_connection_trigger.pl
new file mode 100644
index 0000000..3dcd475
--- /dev/null
+++ b/src/test/recovery/t/000_client_connection_trigger.pl
@@ -0,0 +1,69 @@
+use strict;
+use warnings;
+use PostgresNode;
+use TestLib;
+use Test::More;
+if (!$use_unix_sockets)
+{
+	plan skip_all =>
+	  "authentication tests cannot run without Unix-domain sockets";
+}
+else
+{
+	plan tests => 5;
+}
+
+# Initialize master node
+my $node = get_new_node('master');
+$node->init;
+$node->start;
+$node->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+CREATE ROLE regress_hacker LOGIN PASSWORD 'pass';
+
+CREATE TABLE connects(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO connects (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON client_connection EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+}
+);
+my $res;
+
+$res = $node->safe_psql('postgres', "SELECT 1");
+
+$res = $node->safe_psql('postgres', "SELECT 1",
+  extra_params => [ '-U', 'regress_user', '-w' ]);
+
+my ($ret, $stdout, $stderr) = $node->psql('postgres', "SELECT 1",
+  extra_params => [ '-U', 'regress_hacker', '-w' ]);
+ok( $ret != 0 && $stderr =~ /You are not welcome!/ );
+
+$res = $node->safe_psql('postgres', "SELECT COUNT(1) FROM connects WHERE who = 'regress_user'");
+ok($res == 1);
+
+my $tempdir = TestLib::tempdir;
+command_ok(
+  [ "pg_dumpall", '-p', $node->port, '-c', "--file=$tempdir/regression_dump.sql", ],
+  "dumpall");
+# my $dump_contents = slurp_file("$tempdir/regression_dump.sql");
+# print($dump_contents);
+
+my $node1 = get_new_node('secondary');
+$node1->init;
+$node1->start;
+command_ok(["psql", '-p', $node1->port, '-b', '-f', "$tempdir/regression_dump.sql" ] );
+$res = $node1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$res = $node1->safe_psql('postgres', "SELECT COUNT(1) FROM connects WHERE who = 'regress_user'");
+ok($res == 2);
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 9e31a53..b4a21fb 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -43,6 +43,27 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+
+CREATE TABLE connects(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO connects (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON client_connection EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 $node_primary->wait_for_catchup($node_standby_1, 'replay',
 	$node_primary->lsn('insert'));
@@ -266,6 +287,9 @@ sub replay_check
 
 replay_check();
 
+$node_standby_1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$node_standby_2->safe_psql('postgres', "SELECT 2", extra_params => [ '-U', 'regress_user', '-w' ]);
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index bdd0ffc..bdfa3ee 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -536,3 +536,40 @@ NOTICE:  DROP POLICY - ddl_command_end
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- On session start triggers
+create table connects(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into connects (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on client_connection execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id | who  
+----+------
+  1 | I am
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id | who  
+----+------
+  1 | I am
+  2 | I am
+(2 rows)
+
+-- Test handing exeptions in client_connection trigger
+drop table connects;
+-- superuser should ignore error
+\c
+NOTICE:  client_connection trigger failed with message: relation "connects" does not exist
+-- suppress trigger firing
+\c "dbname=regression options='-c enable_client_connection_trigger=false'"
+-- Cleanup
+drop event trigger on_login_trigger;
+drop function on_login_proc();
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 81bdacf..88a5102 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -86,27 +86,28 @@ select count(*) = 1 as ok from pg_stat_wal;
 -- This is to record the prevailing planner enable_foo settings during
 -- a regression test run.
 select name, setting from pg_settings where name like 'enable%';
-              name              | setting 
---------------------------------+---------
- enable_bitmapscan              | on
- enable_gathermerge             | on
- enable_hashagg                 | on
- enable_hashjoin                | on
- enable_incremental_sort        | on
- enable_indexonlyscan           | on
- enable_indexscan               | on
- enable_material                | on
- enable_mergejoin               | on
- enable_nestloop                | on
- enable_parallel_append         | on
- enable_parallel_hash           | on
- enable_partition_pruning       | on
- enable_partitionwise_aggregate | off
- enable_partitionwise_join      | off
- enable_seqscan                 | on
- enable_sort                    | on
- enable_tidscan                 | on
-(18 rows)
+               name               | setting 
+----------------------------------+---------
+ enable_bitmapscan                | on
+ enable_client_connection_trigger | on
+ enable_gathermerge               | on
+ enable_hashagg                   | on
+ enable_hashjoin                  | on
+ enable_incremental_sort          | on
+ enable_indexonlyscan             | on
+ enable_indexscan                 | on
+ enable_material                  | on
+ enable_mergejoin                 | on
+ enable_nestloop                  | on
+ enable_parallel_append           | on
+ enable_parallel_hash             | on
+ enable_partition_pruning         | on
+ enable_partitionwise_aggregate   | off
+ enable_partitionwise_join        | off
+ enable_seqscan                   | on
+ enable_sort                      | on
+ enable_tidscan                   | on
+(19 rows)
 
 -- Test that the pg_timezone_names and pg_timezone_abbrevs views are
 -- more-or-less working.  We can't test their contents in any great detail
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 18b2a26..abece80 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -429,3 +429,31 @@ DROP POLICY p2 ON event_trigger_test;
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- On session start triggers
+create table connects(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into connects (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on client_connection execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+select * from connects;
+\c
+select * from connects;
+
+-- Test handing exeptions in client_connection trigger
+
+drop table connects;
+-- superuser should ignore error
+\c
+-- suppress trigger firing
+\c "dbname=regression options='-c enable_client_connection_trigger=false'"
+
+
+-- Cleanup
+drop event trigger on_login_trigger;
+drop function on_login_proc();
#50Pavel Stehule
pavel.stehule@gmail.com
In reply to: Konstantin Knizhnik (#49)
1 attachment(s)
Re: On login trigger: take three

Hi

Thank you.

I have applied all your fixes in on_connect_event_trigger-12.patch.

Concerning enable_client_connection_trigger GUC, I think that it is really
useful: it is the fastest and simplest way to disable login triggers in case
of some problems with them (not only for superuser itself, but for all
users). Yes, it can be also done using "ALTER EVENT TRIGGER DISABLE".
But assume that you have a lot of databases with different login policies
enforced by on-login event triggers. And you want temporary disable them
all, for example for testing purposes.
In this case GUC is most convenient way to do it.

There was typo in patch

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index f810789..8861f1b 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1,4 +1,4 @@
-<!-- doc/src/sgml/config.sgml -->
+\<!-- doc/src/sgml/config.sgml -->

I have not any objections against functionality or design. I tested the
performance, and there are no negative impacts when this feature is not
used. There is significant overhead related to plpgsql runtime
initialization, but when this trigger will be used, then probably some
other PLpgSQL procedures and functions will be used too, and then this
overhead can be ignored.

* make without warnings
* make check-world passed
* doc build passed

Possible ToDo:

The documentation can contain a note so usage connect triggers in
environments with short life sessions and very short fast queries without
usage PLpgSQL functions or procedures can have negative impact on
performance due overhead of initialization of PLpgSQL engine.

I'll mark this patch as ready for committers

Regards

Pavel

Show quoted text

Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

Attachments:

on_connect_event_trigger-13.patchtext/x-patch; charset=US-ASCII; name=on_connect_event_trigger-13.patchDownload
diff --git a/doc/src/sgml/bki.sgml b/doc/src/sgml/bki.sgml
index 036a72c81e..bb62f25b2a 100644
--- a/doc/src/sgml/bki.sgml
+++ b/doc/src/sgml/bki.sgml
@@ -182,7 +182,7 @@
 { oid =&gt; '1', oid_symbol =&gt; 'TemplateDbOid',
   descr =&gt; 'database\'s default template',
   datname =&gt; 'template1', encoding =&gt; 'ENCODING', datcollate =&gt; 'LC_COLLATE',
-  datctype =&gt; 'LC_CTYPE', datistemplate =&gt; 't', datallowconn =&gt; 't',
+  datctype =&gt; 'LC_CTYPE', datistemplate =&gt; 't', datallowconn =&gt; 't', dathaslogontriggers =&gt; 'f',
   datconnlimit =&gt; '-1', datlastsysoid =&gt; '0', datfrozenxid =&gt; '0',
   datminmxid =&gt; '1', dattablespace =&gt; 'pg_default', datacl =&gt; '_null_' },
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 3a2266526c..8100ff761a 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2944,6 +2944,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>dathaslogontriggers</structfield> <type>bool</type>
+      </para>
+      <para>
+        Indicates that there are client connection triggers defined for this database.
+        This flag is used to avoid extra lookup of pg_event_trigger table on each backend startup.
+        This flag is used internally by Postgres and should not be manually changed by DBA or application.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>datconnlimit</structfield> <type>int4</type>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 4b60382778..c1914c5ceb 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -996,6 +996,24 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-enable-client-connection-trigger" xreflabel="enable_client_connection_trigger">
+      <term><varname>enable_client_connection_trigger</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>enable_client_connection_trigger</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Enables firing the <literal>client_connection</literal>
+        trigger when a client connects. This parameter is switched on by default.
+        Errors in trigger code can prevent user to login to the system.
+        In this case disabling this parameter in connection string can solve the problem:
+        <literal>psql "dbname=postgres options='-c enable_client_connection_trigger=false'".</literal>
+        Only superuser can change this variable.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
      </sect2>
 
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index 0fef9bfcbe..1ecb8c1f45 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -4673,6 +4673,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = t (type: 1)
 datallowconn = t (type: 1)
+dathaslogontriggers = f (type: 1)
 datconnlimit = -1 (type: 5)
 datlastsysoid = 11510 (type: 1)
 datfrozenxid = 379 (type: 1)
@@ -4698,6 +4699,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = f (type: 1)
 datallowconn = t (type: 1)
+dathaslogontriggers = f (type: 1)
 datconnlimit = -1 (type: 5)
 datlastsysoid = 11510 (type: 1)
 datfrozenxid = 379 (type: 1)
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 60366a950e..ae40a8e1a2 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>client_connection</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,29 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>client_connection</literal> event occurs when a client connection
+     to the server is established.
+     There are two mechanisms for dealing with any bugs in a trigger procedure for
+     this event which might prevent successful login to the system:
+     <itemizedlist>
+      <listitem>
+       <para>
+         The configuration parameter <literal>enable_client_connection_trigger</literal>
+         makes it possible to disable firing the <literal>client_connection</literal>
+         trigger when a client connects.
+       </para>
+      </listitem>
+      <listitem>
+       <para>
+         Errors in the <literal>client_connection</literal> trigger procedure are
+         ignored for superuser. An error message is delivered to the client as
+         <literal>NOTICE</literal> in this case.
+       </para>
+      </listitem>
+     </itemizedlist>
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index f27c3fe8c1..8646db7d3d 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -560,6 +560,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	new_record[Anum_pg_database_datctype - 1] =
 		DirectFunctionCall1(namein, CStringGetDatum(dbctype));
 	new_record[Anum_pg_database_datistemplate - 1] = BoolGetDatum(dbistemplate);
+	new_record[Anum_pg_database_dathaslogontriggers - 1] = BoolGetDatum(false);
 	new_record[Anum_pg_database_datallowconn - 1] = BoolGetDatum(dballowconnections);
 	new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 	new_record[Anum_pg_database_datlastsysoid - 1] = ObjectIdGetDatum(src_lastsysoid);
@@ -1627,7 +1628,7 @@ AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel)
 		new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 		new_record_repl[Anum_pg_database_datconnlimit - 1] = true;
 	}
-
+	new_record[Anum_pg_database_dathaslogontriggers - 1] = BoolGetDatum(datform->dathaslogontriggers);
 	newtuple = heap_modify_tuple(tuple, RelationGetDescr(rel), new_record,
 								 new_record_nulls, new_record_repl);
 	CatalogTupleUpdate(rel, &tuple->t_self, newtuple);
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 3ffba4e63e..855b65644f 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -43,11 +44,14 @@
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
 #include "utils/syscache.h"
 
+bool enable_client_connection_trigger;
+
 typedef struct EventTriggerQueryState
 {
 	/* memory context for this state's objects */
@@ -130,6 +134,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "client_connection") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -293,6 +298,23 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
 	CatalogTupleInsert(tgrel, tuple);
 	heap_freetuple(tuple);
 
+	if (strcmp(eventname, "client_connection") == 0)
+	{
+		Form_pg_database db;
+		Relation pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+		/* Set dathaslogontriggers flag in pg_database */
+		tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+		db = (Form_pg_database) GETSTRUCT(tuple);
+		if (!db->dathaslogontriggers)
+		{
+			db->dathaslogontriggers = true;
+			CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+		}
+		table_close(pg_db, RowExclusiveLock);
+	}
+
 	/* Depend on owner. */
 	recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
 
@@ -562,6 +584,9 @@ EventTriggerCommonSetup(Node *parsetree,
 	ListCell   *lc;
 	List	   *runlist = NIL;
 
+	/* Get the command tag. */
+	tag = parsetree ? CreateCommandTag(parsetree) : CMDTAG_CONNECT;
+
 	/*
 	 * We want the list of command tags for which this procedure is actually
 	 * invoked to match up exactly with the list that CREATE EVENT TRIGGER
@@ -577,22 +602,18 @@ EventTriggerCommonSetup(Node *parsetree,
 	 * relevant command tag.
 	 */
 #ifdef USE_ASSERT_CHECKING
+	if (event == EVT_DDLCommandStart ||
+		event == EVT_DDLCommandEnd ||
+		event == EVT_SQLDrop ||
+		event == EVT_Connect)
 	{
-		CommandTag	dbgtag;
-
-		dbgtag = CreateCommandTag(parsetree);
-		if (event == EVT_DDLCommandStart ||
-			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
-		{
-			if (!command_tag_event_trigger_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
-		else if (event == EVT_TableRewrite)
-		{
-			if (!command_tag_table_rewrite_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
+		if (!command_tag_event_trigger_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
+	}
+	else if (event == EVT_TableRewrite)
+	{
+		if (!command_tag_table_rewrite_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
 	}
 #endif
 
@@ -601,9 +622,6 @@ EventTriggerCommonSetup(Node *parsetree,
 	if (cachelist == NIL)
 		return NIL;
 
-	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
-
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
 	 * memory context.  Once we start running the command triggers, or indeed
@@ -800,6 +818,117 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+static bool
+DatabaseHasLogonTriggers(void)
+{
+	bool has_logon_triggers;
+	HeapTuple tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+	has_logon_triggers = ((Form_pg_database) GETSTRUCT(tuple))->dathaslogontriggers;
+	ReleaseSysCache(tuple);
+	return has_logon_triggers;
+}
+
+/*
+ * Fire connect triggers.
+ */
+void
+EventTriggerOnConnect(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster
+		|| !OidIsValid(MyDatabaseId)
+		|| !enable_client_connection_trigger)
+		return;
+
+	StartTransactionCommand();
+
+	if (DatabaseHasLogonTriggers())
+	{
+		runlist = EventTriggerCommonSetup(NULL,
+										  EVT_Connect, "connect",
+										  &trigdata);
+
+		if (runlist != NIL)
+		{
+			MemoryContext old_context = CurrentMemoryContext;
+			bool is_superuser = superuser();
+			/*
+			 * Make sure anything the main command did will be visible to the event
+			 * triggers.
+			 */
+			CommandCounterIncrement();
+
+			/* Run the triggers. */
+			PG_TRY();
+			{
+				EventTriggerInvoke(runlist, &trigdata);
+				list_free(runlist);
+			}
+			PG_CATCH();
+			{
+				ErrorData* error;
+				/*
+				 * Try to ignore error for superuser to make it possible to login even in case of errors
+				 * during trigger execution
+				 */
+				if (!is_superuser)
+					PG_RE_THROW();
+
+				MemoryContextSwitchTo(old_context);
+				error = CopyErrorData();
+				FlushErrorState();
+				elog(NOTICE, "client_connection trigger failed with message: %s", error->message);
+				AbortCurrentTransaction();
+				return;
+			}
+			PG_END_TRY();
+		}
+		else
+		{
+			/* Runtlist is empty: clear dathaslogontriggers flag
+			 */
+			Relation pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+			HeapTuple tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+			Form_pg_database db;
+
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+			db = (Form_pg_database) GETSTRUCT(tuple);
+			if (db->dathaslogontriggers)
+			{
+				db->dathaslogontriggers = false;
+				CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				/*
+				 * There can be race condition: event trigger may be added after we have scanned
+				 * pg_event_trigger table. Repeat this test nuder  pg_database table lock.
+				 */
+				AcceptInvalidationMessages();
+				runlist = EventTriggerCommonSetup(NULL,
+												  EVT_Connect, "connect",
+												  &trigdata);
+				if (runlist != NULL) /* if list is not empty, then restore the flag */
+				{
+					db->dathaslogontriggers = true;
+					CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				}
+			}
+			table_close(pg_db, RowExclusiveLock);
+		}
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index d35c5020ea..4e12cc958b 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -42,6 +42,7 @@
 #include "catalog/pg_type.h"
 #include "commands/async.h"
 #include "commands/prepare.h"
+#include "commands/event_trigger.h"
 #include "executor/spi.h"
 #include "jit/jit.h"
 #include "libpq/libpq.h"
@@ -168,6 +169,9 @@ static ProcSignalReason RecoveryConflictReason;
 static MemoryContext row_description_context = NULL;
 static StringInfoData row_description_buf;
 
+/* Hook for plugins to get control at start of session */
+client_connection_hook_type client_connection_hook = EventTriggerOnConnect;
+
 /* ----------------------------------------------------------------
  *		decls for routines only used in this file
  * ----------------------------------------------------------------
@@ -4031,6 +4035,11 @@ PostgresMain(int argc, char *argv[],
 	if (!IsUnderPostmaster)
 		PgStartTime = GetCurrentTimestamp();
 
+	if (client_connection_hook)
+	{
+		(*client_connection_hook) ();
+	}
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 0877bc7e0e..288d6ae829 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "client_connection") == 0)
+			event = EVT_Connect;
 		else
 			continue;
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 878fcc2236..347e1cc6c8 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -43,6 +43,7 @@
 #include "commands/async.h"
 #include "commands/prepare.h"
 #include "commands/trigger.h"
+#include "commands/event_trigger.h"
 #include "commands/user.h"
 #include "commands/vacuum.h"
 #include "commands/variable.h"
@@ -927,6 +928,18 @@ static const unit_conversion time_unit_conversion_table[] =
 
 static struct config_bool ConfigureNamesBool[] =
 {
+	{
+		{"enable_client_connection_trigger", PGC_SU_BACKEND, DEVELOPER_OPTIONS,
+			gettext_noop("Enables the client_connection event trigger."),
+			gettext_noop("In case of errors in the ON client_connection EVENT TRIGGER procedure, "
+						 "this parameter can be used to disable trigger activation "
+						 "and provide access to the database."),
+			GUC_EXPLAIN
+		},
+		&enable_client_connection_trigger,
+		true,
+		NULL, NULL, NULL
+	},
 	{
 		{"enable_seqscan", PGC_USERSET, QUERY_TUNING_METHOD,
 			gettext_noop("Enables the planner's use of sequential-scan plans."),
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 1ab98a2286..f6606b40ea 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -2727,6 +2727,7 @@ dumpDatabase(Archive *fout)
 				i_datacl,
 				i_rdatacl,
 				i_datistemplate,
+				i_dathaslogontriggers,
 				i_datconnlimit,
 				i_tablespace;
 	CatalogId	dbCatId;
@@ -2739,6 +2740,7 @@ dumpDatabase(Archive *fout)
 			   *datacl,
 			   *rdatacl,
 			   *datistemplate,
+			   *dathaslogontriggers,
 			   *datconnlimit,
 			   *tablespace;
 	uint32		frozenxid,
@@ -2757,7 +2759,7 @@ dumpDatabase(Archive *fout)
 	 * (pg_init_privs) are not supported on databases, so this logic cannot
 	 * make use of buildACLQueries().
 	 */
-	if (fout->remoteVersion >= 90600)
+	if (fout->remoteVersion >= 140000)
 	{
 		appendPQExpBuffer(dbQry, "SELECT tableoid, oid, datname, "
 						  "(%s datdba) AS dba, "
@@ -2783,7 +2785,41 @@ dumpDatabase(Archive *fout)
 						  "       AS permp(orig_acl) "
 						  "     WHERE acl = orig_acl)) AS rdatacls) "
 						  " AS rdatacl, "
-						  "datistemplate, datconnlimit, "
+						  "datistemplate, datconnlimit, dathaslogontriggers, "
+						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
+						  "shobj_description(oid, 'pg_database') AS description "
+
+						  "FROM pg_database "
+						  "WHERE datname = current_database()",
+						  username_subquery);
+	}
+	else if (fout->remoteVersion >= 90600)
+	{
+		appendPQExpBuffer(dbQry, "SELECT tableoid, oid, datname, "
+						  "(%s datdba) AS dba, "
+						  "pg_encoding_to_char(encoding) AS encoding, "
+						  "datcollate, datctype, datfrozenxid, datminmxid, "
+						  "(SELECT array_agg(acl ORDER BY row_n) FROM "
+						  "  (SELECT acl, row_n FROM "
+						  "     unnest(coalesce(datacl,acldefault('d',datdba))) "
+						  "     WITH ORDINALITY AS perm(acl,row_n) "
+						  "   WHERE NOT EXISTS ( "
+						  "     SELECT 1 "
+						  "     FROM unnest(acldefault('d',datdba)) "
+						  "       AS init(init_acl) "
+						  "     WHERE acl = init_acl)) AS datacls) "
+						  " AS datacl, "
+						  "(SELECT array_agg(acl ORDER BY row_n) FROM "
+						  "  (SELECT acl, row_n FROM "
+						  "     unnest(acldefault('d',datdba)) "
+						  "     WITH ORDINALITY AS initp(acl,row_n) "
+						  "   WHERE NOT EXISTS ( "
+						  "     SELECT 1 "
+						  "     FROM unnest(coalesce(datacl,acldefault('d',datdba))) "
+						  "       AS permp(orig_acl) "
+						  "     WHERE acl = orig_acl)) AS rdatacls) "
+						  " AS rdatacl, "
+						  "datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2797,7 +2833,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "datcollate, datctype, datfrozenxid, datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogontriggers"
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2811,7 +2847,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "datcollate, datctype, datfrozenxid, 0 AS datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2825,7 +2861,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "NULL AS datcollate, NULL AS datctype, datfrozenxid, 0 AS datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2840,7 +2876,7 @@ dumpDatabase(Archive *fout)
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "NULL AS datcollate, NULL AS datctype, datfrozenxid, 0 AS datminmxid, "
 						  "datacl, '' as rdatacl, datistemplate, "
-						  "-1 as datconnlimit, "
+						  "-1 as datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace "
 						  "FROM pg_database "
 						  "WHERE datname = current_database()",
@@ -2862,6 +2898,7 @@ dumpDatabase(Archive *fout)
 	i_rdatacl = PQfnumber(res, "rdatacl");
 	i_datistemplate = PQfnumber(res, "datistemplate");
 	i_datconnlimit = PQfnumber(res, "datconnlimit");
+	i_dathaslogontriggers = PQfnumber(res, "dathaslogontriggers");
 	i_tablespace = PQfnumber(res, "tablespace");
 
 	dbCatId.tableoid = atooid(PQgetvalue(res, 0, i_tableoid));
@@ -2876,6 +2913,7 @@ dumpDatabase(Archive *fout)
 	datacl = PQgetvalue(res, 0, i_datacl);
 	rdatacl = PQgetvalue(res, 0, i_rdatacl);
 	datistemplate = PQgetvalue(res, 0, i_datistemplate);
+	dathaslogontriggers = PQgetvalue(res, 0, i_dathaslogontriggers);
 	datconnlimit = PQgetvalue(res, 0, i_datconnlimit);
 	tablespace = PQgetvalue(res, 0, i_tablespace);
 
@@ -3049,6 +3087,14 @@ dumpDatabase(Archive *fout)
 		appendPQExpBufferStr(delQry, ";\n");
 	}
 
+	if (strcmp(dathaslogontriggers, "t") == 0)
+	{
+		appendPQExpBufferStr(creaQry, "UPDATE pg_catalog.pg_database "
+							 "SET dathaslogontriggers = true WHERE datname = ");
+		appendStringLiteralAH(creaQry, datname, fout);
+		appendPQExpBufferStr(creaQry, ";\n");
+	}
+
 	/* Add database-specific SET options */
 	dumpDatabaseConfig(fout, creaQry, datname, dbCatId.oid);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 3a43c09bf6..08f00d8fc4 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -2970,7 +2970,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end",
+					  "client_connection", "sql_drop");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat
index 21cd6604f3..4c10575a31 100644
--- a/src/include/catalog/pg_database.dat
+++ b/src/include/catalog/pg_database.dat
@@ -15,7 +15,7 @@
 { oid => '1', oid_symbol => 'TemplateDbOid',
   descr => 'default template for new databases',
   datname => 'template1', encoding => 'ENCODING', datcollate => 'LC_COLLATE',
-  datctype => 'LC_CTYPE', datistemplate => 't', datallowconn => 't',
+  datctype => 'LC_CTYPE', datistemplate => 't', dathaslogontriggers => 'f', datallowconn => 't',
   datconnlimit => '-1', datlastsysoid => '0', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datacl => '_null_' },
 
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index 47bcf40346..7dc15a5ff1 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -52,6 +52,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
 	/* new connections allowed? */
 	bool		datallowconn;
 
+	/* database has on-login triggers */
+	bool		dathaslogontriggers;
+
 	/* max connections allowed (-1=no limit) */
 	int32		datconnlimit;
 
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 407fd6a978..47d481e56c 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -21,6 +21,8 @@
 #include "tcop/deparse_utility.h"
 #include "utils/aclchk_internal.h"
 
+extern bool enable_client_connection_trigger; /* GUC */
+
 typedef struct EventTriggerData
 {
 	NodeTag		type;
@@ -53,6 +55,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnConnect(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index be94852bbd..988aa39f4b 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -80,6 +80,7 @@ PG_CMDTAG(CMDTAG_CLUSTER, "CLUSTER", false, false, false)
 PG_CMDTAG(CMDTAG_COMMENT, "COMMENT", true, false, false)
 PG_CMDTAG(CMDTAG_COMMIT, "COMMIT", false, false, false)
 PG_CMDTAG(CMDTAG_COMMIT_PREPARED, "COMMIT PREPARED", false, false, false)
+PG_CMDTAG(CMDTAG_CONNECT, "CONNECT", true, false, false)
 PG_CMDTAG(CMDTAG_COPY, "COPY", false, false, true)
 PG_CMDTAG(CMDTAG_COPY_FROM, "COPY FROM", false, false, false)
 PG_CMDTAG(CMDTAG_CREATE_ACCESS_METHOD, "CREATE ACCESS METHOD", true, false, false)
diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h
index bd30607b07..90376b22c7 100644
--- a/src/include/tcop/tcopprot.h
+++ b/src/include/tcop/tcopprot.h
@@ -30,6 +30,11 @@ extern PGDLLIMPORT const char *debug_query_string;
 extern int	max_stack_depth;
 extern int	PostAuthDelay;
 
+/* Hook for plugins to get control at start and end of session */
+typedef void (*client_connection_hook_type) (void);
+
+extern PGDLLIMPORT client_connection_hook_type client_connection_hook;
+
 /* GUC-configurable parameters */
 
 typedef enum
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index bb1e39eb64..ab7e8c39b8 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Connect,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/000_client_connection_trigger.pl b/src/test/recovery/t/000_client_connection_trigger.pl
new file mode 100644
index 0000000000..3dcd475f72
--- /dev/null
+++ b/src/test/recovery/t/000_client_connection_trigger.pl
@@ -0,0 +1,69 @@
+use strict;
+use warnings;
+use PostgresNode;
+use TestLib;
+use Test::More;
+if (!$use_unix_sockets)
+{
+	plan skip_all =>
+	  "authentication tests cannot run without Unix-domain sockets";
+}
+else
+{
+	plan tests => 5;
+}
+
+# Initialize master node
+my $node = get_new_node('master');
+$node->init;
+$node->start;
+$node->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+CREATE ROLE regress_hacker LOGIN PASSWORD 'pass';
+
+CREATE TABLE connects(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO connects (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON client_connection EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+}
+);
+my $res;
+
+$res = $node->safe_psql('postgres', "SELECT 1");
+
+$res = $node->safe_psql('postgres', "SELECT 1",
+  extra_params => [ '-U', 'regress_user', '-w' ]);
+
+my ($ret, $stdout, $stderr) = $node->psql('postgres', "SELECT 1",
+  extra_params => [ '-U', 'regress_hacker', '-w' ]);
+ok( $ret != 0 && $stderr =~ /You are not welcome!/ );
+
+$res = $node->safe_psql('postgres', "SELECT COUNT(1) FROM connects WHERE who = 'regress_user'");
+ok($res == 1);
+
+my $tempdir = TestLib::tempdir;
+command_ok(
+  [ "pg_dumpall", '-p', $node->port, '-c', "--file=$tempdir/regression_dump.sql", ],
+  "dumpall");
+# my $dump_contents = slurp_file("$tempdir/regression_dump.sql");
+# print($dump_contents);
+
+my $node1 = get_new_node('secondary');
+$node1->init;
+$node1->start;
+command_ok(["psql", '-p', $node1->port, '-b', '-f', "$tempdir/regression_dump.sql" ] );
+$res = $node1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$res = $node1->safe_psql('postgres', "SELECT COUNT(1) FROM connects WHERE who = 'regress_user'");
+ok($res == 2);
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 9e31a53de7..b4a21fbc2a 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -43,6 +43,27 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+
+CREATE TABLE connects(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO connects (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON client_connection EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 $node_primary->wait_for_catchup($node_standby_1, 'replay',
 	$node_primary->lsn('insert'));
@@ -266,6 +287,9 @@ sub replay_check
 
 replay_check();
 
+$node_standby_1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$node_standby_2->safe_psql('postgres', "SELECT 2", extra_params => [ '-U', 'regress_user', '-w' ]);
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index bdd0ffcdaf..bdfa3eefb8 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -536,3 +536,40 @@ NOTICE:  DROP POLICY - ddl_command_end
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- On session start triggers
+create table connects(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into connects (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on client_connection execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id | who  
+----+------
+  1 | I am
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id | who  
+----+------
+  1 | I am
+  2 | I am
+(2 rows)
+
+-- Test handing exeptions in client_connection trigger
+drop table connects;
+-- superuser should ignore error
+\c
+NOTICE:  client_connection trigger failed with message: relation "connects" does not exist
+-- suppress trigger firing
+\c "dbname=regression options='-c enable_client_connection_trigger=false'"
+-- Cleanup
+drop event trigger on_login_trigger;
+drop function on_login_proc();
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 81bdacf59d..88a51022fb 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -86,27 +86,28 @@ select count(*) = 1 as ok from pg_stat_wal;
 -- This is to record the prevailing planner enable_foo settings during
 -- a regression test run.
 select name, setting from pg_settings where name like 'enable%';
-              name              | setting 
---------------------------------+---------
- enable_bitmapscan              | on
- enable_gathermerge             | on
- enable_hashagg                 | on
- enable_hashjoin                | on
- enable_incremental_sort        | on
- enable_indexonlyscan           | on
- enable_indexscan               | on
- enable_material                | on
- enable_mergejoin               | on
- enable_nestloop                | on
- enable_parallel_append         | on
- enable_parallel_hash           | on
- enable_partition_pruning       | on
- enable_partitionwise_aggregate | off
- enable_partitionwise_join      | off
- enable_seqscan                 | on
- enable_sort                    | on
- enable_tidscan                 | on
-(18 rows)
+               name               | setting 
+----------------------------------+---------
+ enable_bitmapscan                | on
+ enable_client_connection_trigger | on
+ enable_gathermerge               | on
+ enable_hashagg                   | on
+ enable_hashjoin                  | on
+ enable_incremental_sort          | on
+ enable_indexonlyscan             | on
+ enable_indexscan                 | on
+ enable_material                  | on
+ enable_mergejoin                 | on
+ enable_nestloop                  | on
+ enable_parallel_append           | on
+ enable_parallel_hash             | on
+ enable_partition_pruning         | on
+ enable_partitionwise_aggregate   | off
+ enable_partitionwise_join        | off
+ enable_seqscan                   | on
+ enable_sort                      | on
+ enable_tidscan                   | on
+(19 rows)
 
 -- Test that the pg_timezone_names and pg_timezone_abbrevs views are
 -- more-or-less working.  We can't test their contents in any great detail
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 18b2a267cb..abece80426 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -429,3 +429,31 @@ DROP POLICY p2 ON event_trigger_test;
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- On session start triggers
+create table connects(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into connects (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on client_connection execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+select * from connects;
+\c
+select * from connects;
+
+-- Test handing exeptions in client_connection trigger
+
+drop table connects;
+-- superuser should ignore error
+\c
+-- suppress trigger firing
+\c "dbname=regression options='-c enable_client_connection_trigger=false'"
+
+
+-- Cleanup
+drop event trigger on_login_trigger;
+drop function on_login_proc();
#51Pavel Stehule
pavel.stehule@gmail.com
In reply to: Pavel Stehule (#50)
Re: On login trigger: take three

so 26. 12. 2020 v 8:00 odesílatel Pavel Stehule <pavel.stehule@gmail.com>
napsal:

Hi

Thank you.

I have applied all your fixes in on_connect_event_trigger-12.patch.

Concerning enable_client_connection_trigger GUC, I think that it is
really useful: it is the fastest and simplest way to disable login triggers
in case
of some problems with them (not only for superuser itself, but for all
users). Yes, it can be also done using "ALTER EVENT TRIGGER DISABLE".
But assume that you have a lot of databases with different login policies
enforced by on-login event triggers. And you want temporary disable them
all, for example for testing purposes.
In this case GUC is most convenient way to do it.

There was typo in patch

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index f810789..8861f1b 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1,4 +1,4 @@
-<!-- doc/src/sgml/config.sgml -->
+\<!-- doc/src/sgml/config.sgml -->

I have not any objections against functionality or design. I tested the
performance, and there are no negative impacts when this feature is not
used. There is significant overhead related to plpgsql runtime
initialization, but when this trigger will be used, then probably some
other PLpgSQL procedures and functions will be used too, and then this
overhead can be ignored.

* make without warnings
* make check-world passed
* doc build passed

Possible ToDo:

The documentation can contain a note so usage connect triggers in
environments with short life sessions and very short fast queries without
usage PLpgSQL functions or procedures can have negative impact on
performance due overhead of initialization of PLpgSQL engine.

I'll mark this patch as ready for committers

looks so this patch has not entry in commitfestapp 2021-01

Regards

Pavel

Show quoted text

Regards

Pavel

Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#52Masahiko Sawada
sawada.mshk@gmail.com
In reply to: Pavel Stehule (#51)
Re: On login trigger: take three

On Sat, Dec 26, 2020 at 4:04 PM Pavel Stehule <pavel.stehule@gmail.com> wrote:

so 26. 12. 2020 v 8:00 odesílatel Pavel Stehule <pavel.stehule@gmail.com> napsal:

Hi

Thank you.
I have applied all your fixes in on_connect_event_trigger-12.patch.

Concerning enable_client_connection_trigger GUC, I think that it is really useful: it is the fastest and simplest way to disable login triggers in case
of some problems with them (not only for superuser itself, but for all users). Yes, it can be also done using "ALTER EVENT TRIGGER DISABLE".
But assume that you have a lot of databases with different login policies enforced by on-login event triggers. And you want temporary disable them all, for example for testing purposes.
In this case GUC is most convenient way to do it.

There was typo in patch

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index f810789..8861f1b 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1,4 +1,4 @@
-<!-- doc/src/sgml/config.sgml -->
+\<!-- doc/src/sgml/config.sgml -->

I have not any objections against functionality or design. I tested the performance, and there are no negative impacts when this feature is not used. There is significant overhead related to plpgsql runtime initialization, but when this trigger will be used, then probably some other PLpgSQL procedures and functions will be used too, and then this overhead can be ignored.

* make without warnings
* make check-world passed
* doc build passed

Possible ToDo:

The documentation can contain a note so usage connect triggers in environments with short life sessions and very short fast queries without usage PLpgSQL functions or procedures can have negative impact on performance due overhead of initialization of PLpgSQL engine.

I'll mark this patch as ready for committers

looks so this patch has not entry in commitfestapp 2021-01

Yeah, please register this patch before the next CommitFest[1]https://commitfest.postgresql.org/31/ starts,
2021-01-01 AoE[2]https://en.wikipedia.org/wiki/Anywhere_on_Earth.

Regards,

[1]: https://commitfest.postgresql.org/31/
[2]: https://en.wikipedia.org/wiki/Anywhere_on_Earth

--
Masahiko Sawada
EnterpriseDB: https://www.enterprisedb.com/

#53Amit Kapila
amit.kapila16@gmail.com
In reply to: Masahiko Sawada (#52)
Re: On login trigger: take three

On Mon, Dec 28, 2020 at 5:46 PM Masahiko Sawada <sawada.mshk@gmail.com> wrote:

On Sat, Dec 26, 2020 at 4:04 PM Pavel Stehule <pavel.stehule@gmail.com> wrote:

so 26. 12. 2020 v 8:00 odesílatel Pavel Stehule <pavel.stehule@gmail.com> napsal:

Hi

Thank you.
I have applied all your fixes in on_connect_event_trigger-12.patch.

Concerning enable_client_connection_trigger GUC, I think that it is really useful: it is the fastest and simplest way to disable login triggers in case
of some problems with them (not only for superuser itself, but for all users). Yes, it can be also done using "ALTER EVENT TRIGGER DISABLE".
But assume that you have a lot of databases with different login policies enforced by on-login event triggers. And you want temporary disable them all, for example for testing purposes.
In this case GUC is most convenient way to do it.

There was typo in patch

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index f810789..8861f1b 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1,4 +1,4 @@
-<!-- doc/src/sgml/config.sgml -->
+\<!-- doc/src/sgml/config.sgml -->

I have not any objections against functionality or design. I tested the performance, and there are no negative impacts when this feature is not used. There is significant overhead related to plpgsql runtime initialization, but when this trigger will be used, then probably some other PLpgSQL procedures and functions will be used too, and then this overhead can be ignored.

* make without warnings
* make check-world passed
* doc build passed

Possible ToDo:

The documentation can contain a note so usage connect triggers in environments with short life sessions and very short fast queries without usage PLpgSQL functions or procedures can have negative impact on performance due overhead of initialization of PLpgSQL engine.

I'll mark this patch as ready for committers

looks so this patch has not entry in commitfestapp 2021-01

Yeah, please register this patch before the next CommitFest[1] starts,
2021-01-01 AoE[2].

Konstantin, did you register this patch in any CF? Even though the
reviewer seems to be happy with the patch, I am afraid that we might
lose track of this unless we register it.

--
With Regards,
Amit Kapila.

#54Amit Kapila
amit.kapila16@gmail.com
In reply to: Amit Kapila (#53)
Re: On login trigger: take three

On Thu, Jan 28, 2021 at 8:17 AM Amit Kapila <amit.kapila16@gmail.com> wrote:

On Mon, Dec 28, 2020 at 5:46 PM Masahiko Sawada <sawada.mshk@gmail.com> wrote:

On Sat, Dec 26, 2020 at 4:04 PM Pavel Stehule <pavel.stehule@gmail.com> wrote:

so 26. 12. 2020 v 8:00 odesílatel Pavel Stehule <pavel.stehule@gmail.com> napsal:

Hi

Thank you.
I have applied all your fixes in on_connect_event_trigger-12.patch.

Concerning enable_client_connection_trigger GUC, I think that it is really useful: it is the fastest and simplest way to disable login triggers in case
of some problems with them (not only for superuser itself, but for all users). Yes, it can be also done using "ALTER EVENT TRIGGER DISABLE".
But assume that you have a lot of databases with different login policies enforced by on-login event triggers. And you want temporary disable them all, for example for testing purposes.
In this case GUC is most convenient way to do it.

There was typo in patch

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index f810789..8861f1b 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1,4 +1,4 @@
-<!-- doc/src/sgml/config.sgml -->
+\<!-- doc/src/sgml/config.sgml -->

I have not any objections against functionality or design. I tested the performance, and there are no negative impacts when this feature is not used. There is significant overhead related to plpgsql runtime initialization, but when this trigger will be used, then probably some other PLpgSQL procedures and functions will be used too, and then this overhead can be ignored.

* make without warnings
* make check-world passed
* doc build passed

Possible ToDo:

The documentation can contain a note so usage connect triggers in environments with short life sessions and very short fast queries without usage PLpgSQL functions or procedures can have negative impact on performance due overhead of initialization of PLpgSQL engine.

I'll mark this patch as ready for committers

looks so this patch has not entry in commitfestapp 2021-01

Yeah, please register this patch before the next CommitFest[1] starts,
2021-01-01 AoE[2].

Konstantin, did you register this patch in any CF? Even though the
reviewer seems to be happy with the patch, I am afraid that we might
lose track of this unless we register it.

I see the CF entry (https://commitfest.postgresql.org/31/2900/) for
this work. Sorry for the noise.

--
With Regards,
Amit Kapila.

#55Konstantin Knizhnik
k.knizhnik@postgrespro.ru
In reply to: Amit Kapila (#53)
Re: On login trigger: take three

On 28.01.2021 5:47, Amit Kapila wrote:

On Mon, Dec 28, 2020 at 5:46 PM Masahiko Sawada <sawada.mshk@gmail.com> wrote:

On Sat, Dec 26, 2020 at 4:04 PM Pavel Stehule <pavel.stehule@gmail.com> wrote:

so 26. 12. 2020 v 8:00 odesílatel Pavel Stehule <pavel.stehule@gmail.com> napsal:

Hi

Thank you.
I have applied all your fixes in on_connect_event_trigger-12.patch.

Concerning enable_client_connection_trigger GUC, I think that it is really useful: it is the fastest and simplest way to disable login triggers in case
of some problems with them (not only for superuser itself, but for all users). Yes, it can be also done using "ALTER EVENT TRIGGER DISABLE".
But assume that you have a lot of databases with different login policies enforced by on-login event triggers. And you want temporary disable them all, for example for testing purposes.
In this case GUC is most convenient way to do it.

There was typo in patch

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index f810789..8861f1b 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1,4 +1,4 @@
-<!-- doc/src/sgml/config.sgml -->
+\<!-- doc/src/sgml/config.sgml -->

I have not any objections against functionality or design. I tested the performance, and there are no negative impacts when this feature is not used. There is significant overhead related to plpgsql runtime initialization, but when this trigger will be used, then probably some other PLpgSQL procedures and functions will be used too, and then this overhead can be ignored.

* make without warnings
* make check-world passed
* doc build passed

Possible ToDo:

The documentation can contain a note so usage connect triggers in environments with short life sessions and very short fast queries without usage PLpgSQL functions or procedures can have negative impact on performance due overhead of initialization of PLpgSQL engine.

I'll mark this patch as ready for committers

looks so this patch has not entry in commitfestapp 2021-01

Yeah, please register this patch before the next CommitFest[1] starts,
2021-01-01 AoE[2].

Konstantin, did you register this patch in any CF? Even though the
reviewer seems to be happy with the patch, I am afraid that we might
lose track of this unless we register it.

Yes, certainly:
https://commitfest.postgresql.org/31/2900/

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

In reply to: Konstantin Knizhnik (#55)
1 attachment(s)
Re[2]: On login trigger: take three

Hi,

Thank you, Konstantin, for this very good feature with numerous use cases.
Please find the modified patch attached.

I’ve added the ‘enable_client_connection_trigger’ GUC to the sample config file and also an additional example page to the docs.
Check world has passed and it is ready for committer.
 

Четверг, 28 января 2021, 12:04 +03:00 от Konstantin Knizhnik <k.knizhnik@postgrespro.ru>:
 

On 28.01.2021 5:47, Amit Kapila wrote:

On Mon, Dec 28, 2020 at 5:46 PM Masahiko Sawada < sawada.mshk@gmail.com > wrote:

On Sat, Dec 26, 2020 at 4:04 PM Pavel Stehule < pavel.stehule@gmail.com > wrote:

so 26. 12. 2020 v 8:00 odesílatel Pavel Stehule < pavel.stehule@gmail.com > napsal:

Hi

Thank you.
I have applied all your fixes in on_connect_event_trigger-12.patch.

Concerning enable_client_connection_trigger GUC, I think that it is really useful: it is the fastest and simplest way to disable login triggers in case
of some problems with them (not only for superuser itself, but for all users). Yes, it can be also done using "ALTER EVENT TRIGGER DISABLE".
But assume that you have a lot of databases with different login policies enforced by on-login event triggers. And you want temporary disable them all, for example for testing purposes.
In this case GUC is most convenient way to do it.

There was typo in patch

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index f810789..8861f1b 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1,4 +1,4 @@
-<!-- doc/src/sgml/config.sgml -->
+\<!-- doc/src/sgml/config.sgml -->

I have not any objections against functionality or design. I tested the performance, and there are no negative impacts when this feature is not used. There is significant overhead related to plpgsql runtime initialization, but when this trigger will be used, then probably some other PLpgSQL procedures and functions will be used too, and then this overhead can be ignored.

* make without warnings
* make check-world passed
* doc build passed

Possible ToDo:

The documentation can contain a note so usage connect triggers in environments with short life sessions and very short fast queries without usage PLpgSQL functions or procedures can have negative impact on performance due overhead of initialization of PLpgSQL engine.

I'll mark this patch as ready for committers

looks so this patch has not entry in commitfestapp 2021-01

Yeah, please register this patch before the next CommitFest[1] starts,
2021-01-01 AoE[2].

Konstantin, did you register this patch in any CF? Even though the
reviewer seems to be happy with the patch, I am afraid that we might
lose track of this unless we register it.
Yes, certainly:

https://commitfest.postgresql.org/31/2900/

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

 

 
 
 
 

Attachments:

on_connect_event_trigger-13a.patchapplication/octet-stream; name="=?UTF-8?B?b25fY29ubmVjdF9ldmVudF90cmlnZ2VyLTEzYS5wYXRjaA==?="Download
diff --git a/doc/src/sgml/bki.sgml b/doc/src/sgml/bki.sgml
index db4801c246..76ebb84735 100644
--- a/doc/src/sgml/bki.sgml
+++ b/doc/src/sgml/bki.sgml
@@ -185,7 +185,7 @@
 { oid =&gt; '1', oid_symbol =&gt; 'TemplateDbOid',
   descr =&gt; 'database\'s default template',
   datname =&gt; 'template1', encoding =&gt; 'ENCODING', datcollate =&gt; 'LC_COLLATE',
-  datctype =&gt; 'LC_CTYPE', datistemplate =&gt; 't', datallowconn =&gt; 't',
+  datctype =&gt; 'LC_CTYPE', datistemplate =&gt; 't', datallowconn =&gt; 't', dathaslogontriggers =&gt; 'f',
   datconnlimit =&gt; '-1', datlastsysoid =&gt; '0', datfrozenxid =&gt; '0',
   datminmxid =&gt; '1', dattablespace =&gt; 'pg_default', datacl =&gt; '_null_' },
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 612cc69802..1ed1ffb8d7 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2945,6 +2945,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>dathaslogontriggers</structfield> <type>bool</type>
+      </para>
+      <para>
+        Indicates that there are client connection triggers defined for this database.
+        This flag is used to avoid extra lookup of pg_event_trigger table on each backend startup.
+        This flag is used internally by Postgres and should not be manually changed by DBA or application.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>datconnlimit</structfield> <type>int4</type>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index dd2778611f..960b628176 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -976,6 +976,24 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-enable-client-connection-trigger" xreflabel="enable_client_connection_trigger">
+      <term><varname>enable_client_connection_trigger</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>enable_client_connection_trigger</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Enables firing the <literal>client_connection</literal>
+        trigger when a client connects. This parameter is switched on by default.
+        Errors in trigger code can prevent user to login to the system.
+        In this case disabling this parameter in connection string can solve the problem:
+        <literal>psql "dbname=postgres options='-c enable_client_connection_trigger=false'".</literal>
+        Only superuser can change this variable.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
      </sect2>
 
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index 7266e229a4..752ffc2357 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -4663,6 +4663,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = t (type: 1)
 datallowconn = t (type: 1)
+dathaslogontriggers = f (type: 1)
 datconnlimit = -1 (type: 5)
 datlastsysoid = 11510 (type: 1)
 datfrozenxid = 379 (type: 1)
@@ -4688,6 +4689,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = f (type: 1)
 datallowconn = t (type: 1)
+dathaslogontriggers = f (type: 1)
 datconnlimit = -1 (type: 5)
 datlastsysoid = 11510 (type: 1)
 datfrozenxid = 379 (type: 1)
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 60366a950e..6f99909a74 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>client_connection</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,29 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>client_connection</literal> event occurs when a client connection
+     to the server is established.
+     There are two mechanisms for dealing with any bugs in a trigger procedure for
+     this event which might prevent successful login to the system:
+     <itemizedlist>
+      <listitem>
+       <para>
+         The configuration parameter <literal>enable_client_connection_trigger</literal>
+         makes it possible to disable firing the <literal>client_connection</literal>
+         trigger when a client connects.
+       </para>
+      </listitem>
+      <listitem>
+       <para>
+         Errors in the <literal>client_connection</literal> trigger procedure are
+         ignored for superuser. An error message is delivered to the client as
+         <literal>NOTICE</literal> in this case.
+       </para>
+      </listitem>
+     </itemizedlist>
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1140,7 +1164,7 @@ typedef struct EventTriggerData
   </sect1>
 
   <sect1 id="event-trigger-example">
-   <title>A Complete Event Trigger Example</title>
+   <title>A C language Event Trigger Example</title>
 
    <para>
     Here is a very simple example of an event trigger function written in C.
@@ -1280,6 +1304,64 @@ $$;
 CREATE EVENT TRIGGER no_rewrite_allowed
                   ON table_rewrite
    EXECUTE FUNCTION no_rewrite();
+</programlisting>
+   </para>
+ </sect1>
+
+  <sect1 id="event-trigger-database-client-connection-example">
+   <title>A Database Client Connection Event Trigger Example</title>
+
+   <para>
+    The event trigger on the <literal>client_connection</literal> event
+    can be useful for client connections logging,
+    for verifying the connection and assigning roles according to current circumstances,
+    or for some session data initialization.
+   </para>
+
+   <para>
+    The following example demonstrates these options.
+<programlisting>
+
+-- create test tables and roles
+CREATE TABLE user_sessions_log (
+	"user" text,
+    "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+ RETURNS event_trigger SECURITY DEFINER
+ LANGUAGE plpgsql AS
+$$
+DECLARE
+	hour integer = EXTRACT('hour' FROM current_time);
+BEGIN
+-- 1) Assign some roles
+    IF hour BETWEEN 8 AND 20 THEN           -- at daytime grant the day_worker role
+        EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+        EXECUTE 'GRANT    day_worker TO '   || quote_ident(session_user);
+    ELSIF hour BETWEEN 2 AND 4 THEN
+        RAISE EXCEPTION 'Login forbidden';  -- do not allow to connect these hours
+    ELSE                                    -- at other time grant the night_worker role
+        EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+        EXECUTE 'GRANT  night_worker TO ' || quote_ident(session_user);
+    END IF;
+
+-- 2) Initialize some user session data
+   CREATE TEMP TABLE session_storage (x float, y integer);
+
+-- 3) Log the connection time
+   INSERT INTO user_sessions_log VALUES (session_user, current_timestamp);
+	
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+   ON client_connection
+   EXECUTE FUNCTION init_session();
 </programlisting>
    </para>
  </sect1>
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index f27c3fe8c1..8646db7d3d 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -560,6 +560,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	new_record[Anum_pg_database_datctype - 1] =
 		DirectFunctionCall1(namein, CStringGetDatum(dbctype));
 	new_record[Anum_pg_database_datistemplate - 1] = BoolGetDatum(dbistemplate);
+	new_record[Anum_pg_database_dathaslogontriggers - 1] = BoolGetDatum(false);
 	new_record[Anum_pg_database_datallowconn - 1] = BoolGetDatum(dballowconnections);
 	new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 	new_record[Anum_pg_database_datlastsysoid - 1] = ObjectIdGetDatum(src_lastsysoid);
@@ -1627,7 +1628,7 @@ AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel)
 		new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 		new_record_repl[Anum_pg_database_datconnlimit - 1] = true;
 	}
-
+	new_record[Anum_pg_database_dathaslogontriggers - 1] = BoolGetDatum(datform->dathaslogontriggers);
 	newtuple = heap_modify_tuple(tuple, RelationGetDescr(rel), new_record,
 								 new_record_nulls, new_record_repl);
 	CatalogTupleUpdate(rel, &tuple->t_self, newtuple);
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index b6894cbf76..d3e3eafddd 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -43,11 +44,14 @@
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
 #include "utils/syscache.h"
 
+bool enable_client_connection_trigger;
+
 typedef struct EventTriggerQueryState
 {
 	/* memory context for this state's objects */
@@ -130,6 +134,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "client_connection") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -293,6 +298,23 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
 	CatalogTupleInsert(tgrel, tuple);
 	heap_freetuple(tuple);
 
+	if (strcmp(eventname, "client_connection") == 0)
+	{
+		Form_pg_database db;
+		Relation pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+		/* Set dathaslogontriggers flag in pg_database */
+		tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+		db = (Form_pg_database) GETSTRUCT(tuple);
+		if (!db->dathaslogontriggers)
+		{
+			db->dathaslogontriggers = true;
+			CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+		}
+		table_close(pg_db, RowExclusiveLock);
+	}
+
 	/* Depend on owner. */
 	recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
 
@@ -584,6 +606,9 @@ EventTriggerCommonSetup(Node *parsetree,
 	ListCell   *lc;
 	List	   *runlist = NIL;
 
+	/* Get the command tag. */
+	tag = parsetree ? CreateCommandTag(parsetree) : CMDTAG_CONNECT;
+
 	/*
 	 * We want the list of command tags for which this procedure is actually
 	 * invoked to match up exactly with the list that CREATE EVENT TRIGGER
@@ -599,22 +624,18 @@ EventTriggerCommonSetup(Node *parsetree,
 	 * relevant command tag.
 	 */
 #ifdef USE_ASSERT_CHECKING
+	if (event == EVT_DDLCommandStart ||
+		event == EVT_DDLCommandEnd ||
+		event == EVT_SQLDrop ||
+		event == EVT_Connect)
 	{
-		CommandTag	dbgtag;
-
-		dbgtag = CreateCommandTag(parsetree);
-		if (event == EVT_DDLCommandStart ||
-			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
-		{
-			if (!command_tag_event_trigger_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
-		else if (event == EVT_TableRewrite)
-		{
-			if (!command_tag_table_rewrite_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
+		if (!command_tag_event_trigger_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
+	}
+	else if (event == EVT_TableRewrite)
+	{
+		if (!command_tag_table_rewrite_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
 	}
 #endif
 
@@ -623,9 +644,6 @@ EventTriggerCommonSetup(Node *parsetree,
 	if (cachelist == NIL)
 		return NIL;
 
-	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
-
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
 	 * memory context.  Once we start running the command triggers, or indeed
@@ -822,6 +840,117 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+static bool
+DatabaseHasLogonTriggers(void)
+{
+	bool has_logon_triggers;
+	HeapTuple tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+	has_logon_triggers = ((Form_pg_database) GETSTRUCT(tuple))->dathaslogontriggers;
+	ReleaseSysCache(tuple);
+	return has_logon_triggers;
+}
+
+/*
+ * Fire connect triggers.
+ */
+void
+EventTriggerOnConnect(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster
+		|| !OidIsValid(MyDatabaseId)
+		|| !enable_client_connection_trigger)
+		return;
+
+	StartTransactionCommand();
+
+	if (DatabaseHasLogonTriggers())
+	{
+		runlist = EventTriggerCommonSetup(NULL,
+										  EVT_Connect, "connect",
+										  &trigdata);
+
+		if (runlist != NIL)
+		{
+			MemoryContext old_context = CurrentMemoryContext;
+			bool is_superuser = superuser();
+			/*
+			 * Make sure anything the main command did will be visible to the event
+			 * triggers.
+			 */
+			CommandCounterIncrement();
+
+			/* Run the triggers. */
+			PG_TRY();
+			{
+				EventTriggerInvoke(runlist, &trigdata);
+				list_free(runlist);
+			}
+			PG_CATCH();
+			{
+				ErrorData* error;
+				/*
+				 * Try to ignore error for superuser to make it possible to login even in case of errors
+				 * during trigger execution
+				 */
+				if (!is_superuser)
+					PG_RE_THROW();
+
+				MemoryContextSwitchTo(old_context);
+				error = CopyErrorData();
+				FlushErrorState();
+				elog(NOTICE, "client_connection trigger failed with message: %s", error->message);
+				AbortCurrentTransaction();
+				return;
+			}
+			PG_END_TRY();
+		}
+		else
+		{
+			/* Runtlist is empty: clear dathaslogontriggers flag
+			 */
+			Relation pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+			HeapTuple tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+			Form_pg_database db;
+
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+			db = (Form_pg_database) GETSTRUCT(tuple);
+			if (db->dathaslogontriggers)
+			{
+				db->dathaslogontriggers = false;
+				CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				/*
+				 * There can be race condition: event trigger may be added after we have scanned
+				 * pg_event_trigger table. Repeat this test nuder  pg_database table lock.
+				 */
+				AcceptInvalidationMessages();
+				runlist = EventTriggerCommonSetup(NULL,
+												  EVT_Connect, "connect",
+												  &trigdata);
+				if (runlist != NULL) /* if list is not empty, then restore the flag */
+				{
+					db->dathaslogontriggers = true;
+					CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				}
+			}
+			table_close(pg_db, RowExclusiveLock);
+		}
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 174c72a14b..60954939c0 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -42,6 +42,7 @@
 #include "catalog/pg_type.h"
 #include "commands/async.h"
 #include "commands/prepare.h"
+#include "commands/event_trigger.h"
 #include "executor/spi.h"
 #include "jit/jit.h"
 #include "libpq/libpq.h"
@@ -167,6 +168,9 @@ static ProcSignalReason RecoveryConflictReason;
 static MemoryContext row_description_context = NULL;
 static StringInfoData row_description_buf;
 
+/* Hook for plugins to get control at start of session */
+client_connection_hook_type client_connection_hook = EventTriggerOnConnect;
+
 /* ----------------------------------------------------------------
  *		decls for routines only used in this file
  * ----------------------------------------------------------------
@@ -4029,6 +4033,11 @@ PostgresMain(int argc, char *argv[],
 	if (!IsUnderPostmaster)
 		PgStartTime = GetCurrentTimestamp();
 
+	if (client_connection_hook)
+	{
+		(*client_connection_hook) ();
+	}
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 73d091d1f6..d134aac7d4 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -169,6 +169,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "client_connection") == 0)
+			event = EVT_Connect;
 		else
 			continue;
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 974235d705..a37c9a45a2 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -40,6 +40,7 @@
 #include "commands/async.h"
 #include "commands/prepare.h"
 #include "commands/trigger.h"
+#include "commands/event_trigger.h"
 #include "commands/user.h"
 #include "commands/vacuum.h"
 #include "commands/variable.h"
@@ -931,6 +932,18 @@ static const unit_conversion time_unit_conversion_table[] =
 
 static struct config_bool ConfigureNamesBool[] =
 {
+	{
+		{"enable_client_connection_trigger", PGC_SU_BACKEND, DEVELOPER_OPTIONS,
+			gettext_noop("Enables the client_connection event trigger."),
+			gettext_noop("In case of errors in the ON client_connection EVENT TRIGGER procedure, "
+						 "this parameter can be used to disable trigger activation "
+						 "and provide access to the database."),
+			GUC_EXPLAIN
+		},
+		&enable_client_connection_trigger,
+		true,
+		NULL, NULL, NULL
+	},
 	{
 		{"enable_seqscan", PGC_USERSET, QUERY_TUNING_METHOD,
 			gettext_noop("Enables the planner's use of sequential-scan plans."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 2663bccfda..92707ed703 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -72,6 +72,7 @@
 					# (change requires restart)
 #bonjour_name = ''			# defaults to the computer name
 					# (change requires restart)
+#enable_client_connection_trigger = true	# enables firing the client_connection trigger when a client connect
 
 # - TCP settings -
 # see "man tcp" for details
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index f8d99b7722..29a9f08ec9 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -2718,6 +2718,7 @@ dumpDatabase(Archive *fout)
 				i_datacl,
 				i_rdatacl,
 				i_datistemplate,
+				i_dathaslogontriggers,
 				i_datconnlimit,
 				i_tablespace;
 	CatalogId	dbCatId;
@@ -2730,6 +2731,7 @@ dumpDatabase(Archive *fout)
 			   *datacl,
 			   *rdatacl,
 			   *datistemplate,
+			   *dathaslogontriggers,
 			   *datconnlimit,
 			   *tablespace;
 	uint32		frozenxid,
@@ -2748,7 +2750,7 @@ dumpDatabase(Archive *fout)
 	 * (pg_init_privs) are not supported on databases, so this logic cannot
 	 * make use of buildACLQueries().
 	 */
-	if (fout->remoteVersion >= 90600)
+	if (fout->remoteVersion >= 140000)
 	{
 		appendPQExpBuffer(dbQry, "SELECT tableoid, oid, datname, "
 						  "(%s datdba) AS dba, "
@@ -2774,7 +2776,41 @@ dumpDatabase(Archive *fout)
 						  "       AS permp(orig_acl) "
 						  "     WHERE acl = orig_acl)) AS rdatacls) "
 						  " AS rdatacl, "
-						  "datistemplate, datconnlimit, "
+						  "datistemplate, datconnlimit, dathaslogontriggers, "
+						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
+						  "shobj_description(oid, 'pg_database') AS description "
+
+						  "FROM pg_database "
+						  "WHERE datname = current_database()",
+						  username_subquery);
+	}
+	else if (fout->remoteVersion >= 90600)
+	{
+		appendPQExpBuffer(dbQry, "SELECT tableoid, oid, datname, "
+						  "(%s datdba) AS dba, "
+						  "pg_encoding_to_char(encoding) AS encoding, "
+						  "datcollate, datctype, datfrozenxid, datminmxid, "
+						  "(SELECT array_agg(acl ORDER BY row_n) FROM "
+						  "  (SELECT acl, row_n FROM "
+						  "     unnest(coalesce(datacl,acldefault('d',datdba))) "
+						  "     WITH ORDINALITY AS perm(acl,row_n) "
+						  "   WHERE NOT EXISTS ( "
+						  "     SELECT 1 "
+						  "     FROM unnest(acldefault('d',datdba)) "
+						  "       AS init(init_acl) "
+						  "     WHERE acl = init_acl)) AS datacls) "
+						  " AS datacl, "
+						  "(SELECT array_agg(acl ORDER BY row_n) FROM "
+						  "  (SELECT acl, row_n FROM "
+						  "     unnest(acldefault('d',datdba)) "
+						  "     WITH ORDINALITY AS initp(acl,row_n) "
+						  "   WHERE NOT EXISTS ( "
+						  "     SELECT 1 "
+						  "     FROM unnest(coalesce(datacl,acldefault('d',datdba))) "
+						  "       AS permp(orig_acl) "
+						  "     WHERE acl = orig_acl)) AS rdatacls) "
+						  " AS rdatacl, "
+						  "datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2788,7 +2824,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "datcollate, datctype, datfrozenxid, datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogontriggers"
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2802,7 +2838,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "datcollate, datctype, datfrozenxid, 0 AS datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2816,7 +2852,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "NULL AS datcollate, NULL AS datctype, datfrozenxid, 0 AS datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2831,7 +2867,7 @@ dumpDatabase(Archive *fout)
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "NULL AS datcollate, NULL AS datctype, datfrozenxid, 0 AS datminmxid, "
 						  "datacl, '' as rdatacl, datistemplate, "
-						  "-1 as datconnlimit, "
+						  "-1 as datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace "
 						  "FROM pg_database "
 						  "WHERE datname = current_database()",
@@ -2853,6 +2889,7 @@ dumpDatabase(Archive *fout)
 	i_rdatacl = PQfnumber(res, "rdatacl");
 	i_datistemplate = PQfnumber(res, "datistemplate");
 	i_datconnlimit = PQfnumber(res, "datconnlimit");
+	i_dathaslogontriggers = PQfnumber(res, "dathaslogontriggers");
 	i_tablespace = PQfnumber(res, "tablespace");
 
 	dbCatId.tableoid = atooid(PQgetvalue(res, 0, i_tableoid));
@@ -2867,6 +2904,7 @@ dumpDatabase(Archive *fout)
 	datacl = PQgetvalue(res, 0, i_datacl);
 	rdatacl = PQgetvalue(res, 0, i_rdatacl);
 	datistemplate = PQgetvalue(res, 0, i_datistemplate);
+	dathaslogontriggers = PQgetvalue(res, 0, i_dathaslogontriggers);
 	datconnlimit = PQgetvalue(res, 0, i_datconnlimit);
 	tablespace = PQgetvalue(res, 0, i_tablespace);
 
@@ -3040,6 +3078,14 @@ dumpDatabase(Archive *fout)
 		appendPQExpBufferStr(delQry, ";\n");
 	}
 
+	if (strcmp(dathaslogontriggers, "t") == 0)
+	{
+		appendPQExpBufferStr(creaQry, "UPDATE pg_catalog.pg_database "
+							 "SET dathaslogontriggers = true WHERE datname = ");
+		appendStringLiteralAH(creaQry, datname, fout);
+		appendPQExpBufferStr(creaQry, ";\n");
+	}
+
 	/* Add database-specific SET options */
 	dumpDatabaseConfig(fout, creaQry, datname, dbCatId.oid);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index eb018854a5..c1c0cd6753 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -2871,7 +2871,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end",
+					  "client_connection", "sql_drop");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat
index 21cd6604f3..4c10575a31 100644
--- a/src/include/catalog/pg_database.dat
+++ b/src/include/catalog/pg_database.dat
@@ -15,7 +15,7 @@
 { oid => '1', oid_symbol => 'TemplateDbOid',
   descr => 'default template for new databases',
   datname => 'template1', encoding => 'ENCODING', datcollate => 'LC_COLLATE',
-  datctype => 'LC_CTYPE', datistemplate => 't', datallowconn => 't',
+  datctype => 'LC_CTYPE', datistemplate => 't', dathaslogontriggers => 'f', datallowconn => 't',
   datconnlimit => '-1', datlastsysoid => '0', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datacl => '_null_' },
 
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index f623ee81b7..f60af37089 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -52,6 +52,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
 	/* new connections allowed? */
 	bool		datallowconn;
 
+	/* database has on-login triggers */
+	bool		dathaslogontriggers;
+
 	/* max connections allowed (-1=no limit) */
 	int32		datconnlimit;
 
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 28b352051b..b00bf66a98 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -21,6 +21,8 @@
 #include "tcop/deparse_utility.h"
 #include "utils/aclchk_internal.h"
 
+extern bool enable_client_connection_trigger; /* GUC */
+
 typedef struct EventTriggerData
 {
 	NodeTag		type;
@@ -54,6 +56,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnConnect(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index be94852bbd..988aa39f4b 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -80,6 +80,7 @@ PG_CMDTAG(CMDTAG_CLUSTER, "CLUSTER", false, false, false)
 PG_CMDTAG(CMDTAG_COMMENT, "COMMENT", true, false, false)
 PG_CMDTAG(CMDTAG_COMMIT, "COMMIT", false, false, false)
 PG_CMDTAG(CMDTAG_COMMIT_PREPARED, "COMMIT PREPARED", false, false, false)
+PG_CMDTAG(CMDTAG_CONNECT, "CONNECT", true, false, false)
 PG_CMDTAG(CMDTAG_COPY, "COPY", false, false, true)
 PG_CMDTAG(CMDTAG_COPY_FROM, "COPY FROM", false, false, false)
 PG_CMDTAG(CMDTAG_CREATE_ACCESS_METHOD, "CREATE ACCESS METHOD", true, false, false)
diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h
index bd30607b07..90376b22c7 100644
--- a/src/include/tcop/tcopprot.h
+++ b/src/include/tcop/tcopprot.h
@@ -30,6 +30,11 @@ extern PGDLLIMPORT const char *debug_query_string;
 extern int	max_stack_depth;
 extern int	PostAuthDelay;
 
+/* Hook for plugins to get control at start and end of session */
+typedef void (*client_connection_hook_type) (void);
+
+extern PGDLLIMPORT client_connection_hook_type client_connection_hook;
+
 /* GUC-configurable parameters */
 
 typedef enum
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index bb1e39eb64..ab7e8c39b8 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Connect,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 778f11b28b..652995b021 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -43,6 +43,27 @@ $node_standby_2->start;
 $node_master->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+
+CREATE TABLE connects(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO connects (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON client_connection EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 $node_master->wait_for_catchup($node_standby_1, 'replay',
 	$node_master->lsn('insert'));
@@ -266,6 +287,9 @@ sub replay_check
 
 replay_check();
 
+$node_standby_1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$node_standby_2->safe_psql('postgres', "SELECT 2", extra_params => [ '-U', 'regress_user', '-w' ]);
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index bdd0ffcdaf..bdfa3eefb8 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -536,3 +536,40 @@ NOTICE:  DROP POLICY - ddl_command_end
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- On session start triggers
+create table connects(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into connects (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on client_connection execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id | who  
+----+------
+  1 | I am
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id | who  
+----+------
+  1 | I am
+  2 | I am
+(2 rows)
+
+-- Test handing exeptions in client_connection trigger
+drop table connects;
+-- superuser should ignore error
+\c
+NOTICE:  client_connection trigger failed with message: relation "connects" does not exist
+-- suppress trigger firing
+\c "dbname=regression options='-c enable_client_connection_trigger=false'"
+-- Cleanup
+drop event trigger on_login_trigger;
+drop function on_login_proc();
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index c3b988597c..dc672ab1db 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -77,27 +77,28 @@ select count(*) = 0 as ok from pg_stat_wal_receiver;
 -- This is to record the prevailing planner enable_foo settings during
 -- a regression test run.
 select name, setting from pg_settings where name like 'enable%';
-              name              | setting 
---------------------------------+---------
- enable_bitmapscan              | on
- enable_gathermerge             | on
- enable_hashagg                 | on
- enable_hashjoin                | on
- enable_incremental_sort        | on
- enable_indexonlyscan           | on
- enable_indexscan               | on
- enable_material                | on
- enable_mergejoin               | on
- enable_nestloop                | on
- enable_parallel_append         | on
- enable_parallel_hash           | on
- enable_partition_pruning       | on
- enable_partitionwise_aggregate | off
- enable_partitionwise_join      | off
- enable_seqscan                 | on
- enable_sort                    | on
- enable_tidscan                 | on
-(18 rows)
+               name               | setting 
+----------------------------------+---------
+ enable_bitmapscan                | on
+ enable_client_connection_trigger | on
+ enable_gathermerge               | on
+ enable_hashagg                   | on
+ enable_hashjoin                  | on
+ enable_incremental_sort          | on
+ enable_indexonlyscan             | on
+ enable_indexscan                 | on
+ enable_material                  | on
+ enable_mergejoin                 | on
+ enable_nestloop                  | on
+ enable_parallel_append           | on
+ enable_parallel_hash             | on
+ enable_partition_pruning         | on
+ enable_partitionwise_aggregate   | off
+ enable_partitionwise_join        | off
+ enable_seqscan                   | on
+ enable_sort                      | on
+ enable_tidscan                   | on
+(19 rows)
 
 -- Test that the pg_timezone_names and pg_timezone_abbrevs views are
 -- more-or-less working.  We can't test their contents in any great detail
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 18b2a267cb..abece80426 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -429,3 +429,31 @@ DROP POLICY p2 ON event_trigger_test;
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- On session start triggers
+create table connects(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into connects (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on client_connection execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+select * from connects;
+\c
+select * from connects;
+
+-- Test handing exeptions in client_connection trigger
+
+drop table connects;
+-- superuser should ignore error
+\c
+-- suppress trigger firing
+\c "dbname=regression options='-c enable_client_connection_trigger=false'"
+
+
+-- Cleanup
+drop event trigger on_login_trigger;
+drop function on_login_proc();
In reply to: Ivan Panchenko (#56)
1 attachment(s)
Re[3]: On login trigger: take three

Hi,
I have upgraded the patch for the 14th version.

Вторник, 16 марта 2021, 14:32 +03:00 от Ivan Panchenko <wao@mail.ru>:
 
Hi,

Thank you, Konstantin, for this very good feature with numerous use cases.
Please find the modified patch attached.

I’ve added the ‘enable_client_connection_trigger’ GUC to the sample config file and also an additional example page to the docs.
Check world has passed and it is ready for committer.
 

Четверг, 28 января 2021, 12:04 +03:00 от Konstantin Knizhnik < k.knizhnik@postgrespro.ru >:
 

On 28.01.2021 5:47, Amit Kapila wrote:

On Mon, Dec 28, 2020 at 5:46 PM Masahiko Sawada < sawada.mshk@gmail.com > wrote:

On Sat, Dec 26, 2020 at 4:04 PM Pavel Stehule < pavel.stehule@gmail.com > wrote:

so 26. 12. 2020 v 8:00 odesílatel Pavel Stehule < pavel.stehule@gmail.com > napsal:

Hi

Thank you.
I have applied all your fixes in on_connect_event_trigger-12.patch.

Concerning enable_client_connection_trigger GUC, I think that it is really useful: it is the fastest and simplest way to disable login triggers in case
of some problems with them (not only for superuser itself, but for all users). Yes, it can be also done using "ALTER EVENT TRIGGER DISABLE".
But assume that you have a lot of databases with different login policies enforced by on-login event triggers. And you want temporary disable them all, for example for testing purposes.
In this case GUC is most convenient way to do it.

There was typo in patch

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index f810789..8861f1b 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1,4 +1,4 @@
-<!-- doc/src/sgml/config.sgml -->
+\<!-- doc/src/sgml/config.sgml -->

I have not any objections against functionality or design. I tested the performance, and there are no negative impacts when this feature is not used. There is significant overhead related to plpgsql runtime initialization, but when this trigger will be used, then probably some other PLpgSQL procedures and functions will be used too, and then this overhead can be ignored.

* make without warnings
* make check-world passed
* doc build passed

Possible ToDo:

The documentation can contain a note so usage connect triggers in environments with short life sessions and very short fast queries without usage PLpgSQL functions or procedures can have negative impact on performance due overhead of initialization of PLpgSQL engine.

I'll mark this patch as ready for committers

looks so this patch has not entry in commitfestapp 2021-01

Yeah, please register this patch before the next CommitFest[1] starts,
2021-01-01 AoE[2].

Konstantin, did you register this patch in any CF? Even though the
reviewer seems to be happy with the patch, I am afraid that we might
lose track of this unless we register it.
Yes, certainly:

https://commitfest.postgresql.org/31/2900/

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

 

 
 
 
 

 
 
 
 

Attachments:

on_connect_event_trigger-14a.patchtext/x-diff; name="=?UTF-8?B?b25fY29ubmVjdF9ldmVudF90cmlnZ2VyLTE0YS5wYXRjaA==?="Download
diff --git a/doc/src/sgml/bki.sgml b/doc/src/sgml/bki.sgml
index b33e59d5e4..2bb5804e76 100644
--- a/doc/src/sgml/bki.sgml
+++ b/doc/src/sgml/bki.sgml
@@ -182,7 +182,7 @@
 { oid =&gt; '1', oid_symbol =&gt; 'TemplateDbOid',
   descr =&gt; 'database\'s default template',
   datname =&gt; 'template1', encoding =&gt; 'ENCODING', datcollate =&gt; 'LC_COLLATE',
-  datctype =&gt; 'LC_CTYPE', datistemplate =&gt; 't', datallowconn =&gt; 't',
+  datctype =&gt; 'LC_CTYPE', datistemplate =&gt; 't', datallowconn =&gt; 't', dathaslogontriggers =&gt; 'f',
   datconnlimit =&gt; '-1', datlastsysoid =&gt; '0', datfrozenxid =&gt; '0',
   datminmxid =&gt; '1', dattablespace =&gt; 'pg_default', datacl =&gt; '_null_' },
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 6d06ad22b9..7e0113f1e8 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2968,6 +2968,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>dathaslogontriggers</structfield> <type>bool</type>
+      </para>
+      <para>
+        Indicates that there are client connection triggers defined for this database.
+        This flag is used to avoid extra lookup of pg_event_trigger table on each backend startup.
+        This flag is used internally by Postgres and should not be manually changed by DBA or application.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>datconnlimit</structfield> <type>int4</type>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 7e32b0686c..d6d9b3eb34 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1035,6 +1035,24 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-enable-client-connection-trigger" xreflabel="enable_client_connection_trigger">
+      <term><varname>enable_client_connection_trigger</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>enable_client_connection_trigger</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Enables firing the <literal>client_connection</literal>
+        trigger when a client connects. This parameter is switched on by default.
+        Errors in trigger code can prevent user to login to the system.
+        In this case disabling this parameter in connection string can solve the problem:
+        <literal>psql "dbname=postgres options='-c enable_client_connection_trigger=false'".</literal>
+        Only superuser can change this variable.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
      </sect2>
 
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index 86c078e17d..c3176e763f 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -4731,6 +4731,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = t (type: 1)
 datallowconn = t (type: 1)
+dathaslogontriggers = f (type: 1)
 datconnlimit = -1 (type: 5)
 datlastsysoid = 11510 (type: 1)
 datfrozenxid = 379 (type: 1)
@@ -4756,6 +4757,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = f (type: 1)
 datallowconn = t (type: 1)
+dathaslogontriggers = f (type: 1)
 datconnlimit = -1 (type: 5)
 datlastsysoid = 11510 (type: 1)
 datfrozenxid = 379 (type: 1)
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 60366a950e..6f99909a74 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>client_connection</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,29 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>client_connection</literal> event occurs when a client connection
+     to the server is established.
+     There are two mechanisms for dealing with any bugs in a trigger procedure for
+     this event which might prevent successful login to the system:
+     <itemizedlist>
+      <listitem>
+       <para>
+         The configuration parameter <literal>enable_client_connection_trigger</literal>
+         makes it possible to disable firing the <literal>client_connection</literal>
+         trigger when a client connects.
+       </para>
+      </listitem>
+      <listitem>
+       <para>
+         Errors in the <literal>client_connection</literal> trigger procedure are
+         ignored for superuser. An error message is delivered to the client as
+         <literal>NOTICE</literal> in this case.
+       </para>
+      </listitem>
+     </itemizedlist>
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1140,7 +1164,7 @@ typedef struct EventTriggerData
   </sect1>
 
   <sect1 id="event-trigger-example">
-   <title>A Complete Event Trigger Example</title>
+   <title>A C language Event Trigger Example</title>
 
    <para>
     Here is a very simple example of an event trigger function written in C.
@@ -1280,6 +1304,64 @@ $$;
 CREATE EVENT TRIGGER no_rewrite_allowed
                   ON table_rewrite
    EXECUTE FUNCTION no_rewrite();
+</programlisting>
+   </para>
+ </sect1>
+
+  <sect1 id="event-trigger-database-client-connection-example">
+   <title>A Database Client Connection Event Trigger Example</title>
+
+   <para>
+    The event trigger on the <literal>client_connection</literal> event
+    can be useful for client connections logging,
+    for verifying the connection and assigning roles according to current circumstances,
+    or for some session data initialization.
+   </para>
+
+   <para>
+    The following example demonstrates these options.
+<programlisting>
+
+-- create test tables and roles
+CREATE TABLE user_sessions_log (
+	"user" text,
+    "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+ RETURNS event_trigger SECURITY DEFINER
+ LANGUAGE plpgsql AS
+$$
+DECLARE
+	hour integer = EXTRACT('hour' FROM current_time);
+BEGIN
+-- 1) Assign some roles
+    IF hour BETWEEN 8 AND 20 THEN           -- at daytime grant the day_worker role
+        EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+        EXECUTE 'GRANT    day_worker TO '   || quote_ident(session_user);
+    ELSIF hour BETWEEN 2 AND 4 THEN
+        RAISE EXCEPTION 'Login forbidden';  -- do not allow to connect these hours
+    ELSE                                    -- at other time grant the night_worker role
+        EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+        EXECUTE 'GRANT  night_worker TO ' || quote_ident(session_user);
+    END IF;
+
+-- 2) Initialize some user session data
+   CREATE TEMP TABLE session_storage (x float, y integer);
+
+-- 3) Log the connection time
+   INSERT INTO user_sessions_log VALUES (session_user, current_timestamp);
+	
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+   ON client_connection
+   EXECUTE FUNCTION init_session();
 </programlisting>
    </para>
  </sect1>
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 2b159b60eb..6ed0df3d15 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -560,6 +560,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	new_record[Anum_pg_database_datctype - 1] =
 		DirectFunctionCall1(namein, CStringGetDatum(dbctype));
 	new_record[Anum_pg_database_datistemplate - 1] = BoolGetDatum(dbistemplate);
+	new_record[Anum_pg_database_dathaslogontriggers - 1] = BoolGetDatum(false);
 	new_record[Anum_pg_database_datallowconn - 1] = BoolGetDatum(dballowconnections);
 	new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 	new_record[Anum_pg_database_datlastsysoid - 1] = ObjectIdGetDatum(src_lastsysoid);
@@ -1627,7 +1628,7 @@ AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel)
 		new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 		new_record_repl[Anum_pg_database_datconnlimit - 1] = true;
 	}
-
+	new_record[Anum_pg_database_dathaslogontriggers - 1] = BoolGetDatum(datform->dathaslogontriggers);
 	newtuple = heap_modify_tuple(tuple, RelationGetDescr(rel), new_record,
 								 new_record_nulls, new_record_repl);
 	CatalogTupleUpdate(rel, &tuple->t_self, newtuple);
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 5bde507c75..65d467b8b8 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -43,11 +44,14 @@
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
 #include "utils/syscache.h"
 
+bool enable_client_connection_trigger;
+
 typedef struct EventTriggerQueryState
 {
 	/* memory context for this state's objects */
@@ -130,6 +134,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "client_connection") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -293,6 +298,23 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
 	CatalogTupleInsert(tgrel, tuple);
 	heap_freetuple(tuple);
 
+	if (strcmp(eventname, "client_connection") == 0)
+	{
+		Form_pg_database db;
+		Relation pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+		/* Set dathaslogontriggers flag in pg_database */
+		tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+		db = (Form_pg_database) GETSTRUCT(tuple);
+		if (!db->dathaslogontriggers)
+		{
+			db->dathaslogontriggers = true;
+			CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+		}
+		table_close(pg_db, RowExclusiveLock);
+	}
+
 	/* Depend on owner. */
 	recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
 
@@ -562,6 +584,9 @@ EventTriggerCommonSetup(Node *parsetree,
 	ListCell   *lc;
 	List	   *runlist = NIL;
 
+	/* Get the command tag. */
+	tag = parsetree ? CreateCommandTag(parsetree) : CMDTAG_CONNECT;
+
 	/*
 	 * We want the list of command tags for which this procedure is actually
 	 * invoked to match up exactly with the list that CREATE EVENT TRIGGER
@@ -577,22 +602,18 @@ EventTriggerCommonSetup(Node *parsetree,
 	 * relevant command tag.
 	 */
 #ifdef USE_ASSERT_CHECKING
+	if (event == EVT_DDLCommandStart ||
+		event == EVT_DDLCommandEnd ||
+		event == EVT_SQLDrop ||
+		event == EVT_Connect)
 	{
-		CommandTag	dbgtag;
-
-		dbgtag = CreateCommandTag(parsetree);
-		if (event == EVT_DDLCommandStart ||
-			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
-		{
-			if (!command_tag_event_trigger_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
-		else if (event == EVT_TableRewrite)
-		{
-			if (!command_tag_table_rewrite_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
+		if (!command_tag_event_trigger_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
+	}
+	else if (event == EVT_TableRewrite)
+	{
+		if (!command_tag_table_rewrite_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
 	}
 #endif
 
@@ -601,9 +622,6 @@ EventTriggerCommonSetup(Node *parsetree,
 	if (cachelist == NIL)
 		return NIL;
 
-	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
-
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
 	 * memory context.  Once we start running the command triggers, or indeed
@@ -800,6 +818,117 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+static bool
+DatabaseHasLogonTriggers(void)
+{
+	bool has_logon_triggers;
+	HeapTuple tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+	has_logon_triggers = ((Form_pg_database) GETSTRUCT(tuple))->dathaslogontriggers;
+	ReleaseSysCache(tuple);
+	return has_logon_triggers;
+}
+
+/*
+ * Fire connect triggers.
+ */
+void
+EventTriggerOnConnect(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster
+		|| !OidIsValid(MyDatabaseId)
+		|| !enable_client_connection_trigger)
+		return;
+
+	StartTransactionCommand();
+
+	if (DatabaseHasLogonTriggers())
+	{
+		runlist = EventTriggerCommonSetup(NULL,
+										  EVT_Connect, "connect",
+										  &trigdata);
+
+		if (runlist != NIL)
+		{
+			MemoryContext old_context = CurrentMemoryContext;
+			bool is_superuser = superuser();
+			/*
+			 * Make sure anything the main command did will be visible to the event
+			 * triggers.
+			 */
+			CommandCounterIncrement();
+
+			/* Run the triggers. */
+			PG_TRY();
+			{
+				EventTriggerInvoke(runlist, &trigdata);
+				list_free(runlist);
+			}
+			PG_CATCH();
+			{
+				ErrorData* error;
+				/*
+				 * Try to ignore error for superuser to make it possible to login even in case of errors
+				 * during trigger execution
+				 */
+				if (!is_superuser)
+					PG_RE_THROW();
+
+				MemoryContextSwitchTo(old_context);
+				error = CopyErrorData();
+				FlushErrorState();
+				elog(NOTICE, "client_connection trigger failed with message: %s", error->message);
+				AbortCurrentTransaction();
+				return;
+			}
+			PG_END_TRY();
+		}
+		else
+		{
+			/* Runtlist is empty: clear dathaslogontriggers flag
+			 */
+			Relation pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+			HeapTuple tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+			Form_pg_database db;
+
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+			db = (Form_pg_database) GETSTRUCT(tuple);
+			if (db->dathaslogontriggers)
+			{
+				db->dathaslogontriggers = false;
+				CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				/*
+				 * There can be race condition: event trigger may be added after we have scanned
+				 * pg_event_trigger table. Repeat this test nuder  pg_database table lock.
+				 */
+				AcceptInvalidationMessages();
+				runlist = EventTriggerCommonSetup(NULL,
+												  EVT_Connect, "connect",
+												  &trigdata);
+				if (runlist != NULL) /* if list is not empty, then restore the flag */
+				{
+					db->dathaslogontriggers = true;
+					CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				}
+			}
+			table_close(pg_db, RowExclusiveLock);
+		}
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 8cea10c901..e9f6da3501 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -42,6 +42,7 @@
 #include "catalog/pg_type.h"
 #include "commands/async.h"
 #include "commands/prepare.h"
+#include "commands/event_trigger.h"
 #include "executor/spi.h"
 #include "jit/jit.h"
 #include "libpq/libpq.h"
@@ -182,6 +183,9 @@ static ProcSignalReason RecoveryConflictReason;
 static MemoryContext row_description_context = NULL;
 static StringInfoData row_description_buf;
 
+/* Hook for plugins to get control at start of session */
+client_connection_hook_type client_connection_hook = EventTriggerOnConnect;
+
 /* ----------------------------------------------------------------
  *		decls for routines only used in this file
  * ----------------------------------------------------------------
@@ -4165,6 +4169,11 @@ PostgresMain(int argc, char *argv[],
 	if (!IsUnderPostmaster)
 		PgStartTime = GetCurrentTimestamp();
 
+	if (client_connection_hook)
+	{
+		(*client_connection_hook) ();
+	}
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 460b720a65..d0093191b8 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "client_connection") == 0)
+			event = EVT_Connect;
 		else
 			continue;
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index ee731044b6..875a00d049 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -47,6 +47,7 @@
 #include "commands/async.h"
 #include "commands/prepare.h"
 #include "commands/trigger.h"
+#include "commands/event_trigger.h"
 #include "commands/user.h"
 #include "commands/vacuum.h"
 #include "commands/variable.h"
@@ -952,6 +953,18 @@ static const unit_conversion time_unit_conversion_table[] =
 
 static struct config_bool ConfigureNamesBool[] =
 {
+	{
+		{"enable_client_connection_trigger", PGC_SU_BACKEND, DEVELOPER_OPTIONS,
+			gettext_noop("Enables the client_connection event trigger."),
+			gettext_noop("In case of errors in the ON client_connection EVENT TRIGGER procedure, "
+						 "this parameter can be used to disable trigger activation "
+						 "and provide access to the database."),
+			GUC_EXPLAIN
+		},
+		&enable_client_connection_trigger,
+		true,
+		NULL, NULL, NULL
+	},
 	{
 		{"enable_seqscan", PGC_USERSET, QUERY_TUNING_METHOD,
 			gettext_noop("Enables the planner's use of sequential-scan plans."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 6e36e4c2ef..55ad7e35d0 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -72,6 +72,7 @@
 					# (change requires restart)
 #bonjour_name = ''			# defaults to the computer name
 					# (change requires restart)
+#enable_client_connection_trigger = true	# enables firing the client_connection trigger when a client connect
 
 # - TCP settings -
 # see "man tcp" for details
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 339c393718..faec0dbfe1 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -2807,6 +2807,7 @@ dumpDatabase(Archive *fout)
 				i_datacl,
 				i_rdatacl,
 				i_datistemplate,
+				i_dathaslogontriggers,
 				i_datconnlimit,
 				i_tablespace;
 	CatalogId	dbCatId;
@@ -2819,6 +2820,7 @@ dumpDatabase(Archive *fout)
 			   *datacl,
 			   *rdatacl,
 			   *datistemplate,
+			   *dathaslogontriggers,
 			   *datconnlimit,
 			   *tablespace;
 	uint32		frozenxid,
@@ -2837,7 +2839,7 @@ dumpDatabase(Archive *fout)
 	 * (pg_init_privs) are not supported on databases, so this logic cannot
 	 * make use of buildACLQueries().
 	 */
-	if (fout->remoteVersion >= 90600)
+	if (fout->remoteVersion >= 140000)
 	{
 		appendPQExpBuffer(dbQry, "SELECT tableoid, oid, datname, "
 						  "(%s datdba) AS dba, "
@@ -2863,7 +2865,41 @@ dumpDatabase(Archive *fout)
 						  "       AS permp(orig_acl) "
 						  "     WHERE acl = orig_acl)) AS rdatacls) "
 						  " AS rdatacl, "
-						  "datistemplate, datconnlimit, "
+						  "datistemplate, datconnlimit, dathaslogontriggers, "
+						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
+						  "shobj_description(oid, 'pg_database') AS description "
+
+						  "FROM pg_database "
+						  "WHERE datname = current_database()",
+						  username_subquery);
+	}
+	else if (fout->remoteVersion >= 90600)
+	{
+		appendPQExpBuffer(dbQry, "SELECT tableoid, oid, datname, "
+						  "(%s datdba) AS dba, "
+						  "pg_encoding_to_char(encoding) AS encoding, "
+						  "datcollate, datctype, datfrozenxid, datminmxid, "
+						  "(SELECT array_agg(acl ORDER BY row_n) FROM "
+						  "  (SELECT acl, row_n FROM "
+						  "     unnest(coalesce(datacl,acldefault('d',datdba))) "
+						  "     WITH ORDINALITY AS perm(acl,row_n) "
+						  "   WHERE NOT EXISTS ( "
+						  "     SELECT 1 "
+						  "     FROM unnest(acldefault('d',datdba)) "
+						  "       AS init(init_acl) "
+						  "     WHERE acl = init_acl)) AS datacls) "
+						  " AS datacl, "
+						  "(SELECT array_agg(acl ORDER BY row_n) FROM "
+						  "  (SELECT acl, row_n FROM "
+						  "     unnest(acldefault('d',datdba)) "
+						  "     WITH ORDINALITY AS initp(acl,row_n) "
+						  "   WHERE NOT EXISTS ( "
+						  "     SELECT 1 "
+						  "     FROM unnest(coalesce(datacl,acldefault('d',datdba))) "
+						  "       AS permp(orig_acl) "
+						  "     WHERE acl = orig_acl)) AS rdatacls) "
+						  " AS rdatacl, "
+						  "datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2877,7 +2913,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "datcollate, datctype, datfrozenxid, datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogontriggers"
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2891,7 +2927,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "datcollate, datctype, datfrozenxid, 0 AS datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2905,7 +2941,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "NULL AS datcollate, NULL AS datctype, datfrozenxid, 0 AS datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2920,7 +2956,7 @@ dumpDatabase(Archive *fout)
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "NULL AS datcollate, NULL AS datctype, datfrozenxid, 0 AS datminmxid, "
 						  "datacl, '' as rdatacl, datistemplate, "
-						  "-1 as datconnlimit, "
+						  "-1 as datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace "
 						  "FROM pg_database "
 						  "WHERE datname = current_database()",
@@ -2942,6 +2978,7 @@ dumpDatabase(Archive *fout)
 	i_rdatacl = PQfnumber(res, "rdatacl");
 	i_datistemplate = PQfnumber(res, "datistemplate");
 	i_datconnlimit = PQfnumber(res, "datconnlimit");
+	i_dathaslogontriggers = PQfnumber(res, "dathaslogontriggers");
 	i_tablespace = PQfnumber(res, "tablespace");
 
 	dbCatId.tableoid = atooid(PQgetvalue(res, 0, i_tableoid));
@@ -2956,6 +2993,7 @@ dumpDatabase(Archive *fout)
 	datacl = PQgetvalue(res, 0, i_datacl);
 	rdatacl = PQgetvalue(res, 0, i_rdatacl);
 	datistemplate = PQgetvalue(res, 0, i_datistemplate);
+	dathaslogontriggers = PQgetvalue(res, 0, i_dathaslogontriggers);
 	datconnlimit = PQgetvalue(res, 0, i_datconnlimit);
 	tablespace = PQgetvalue(res, 0, i_tablespace);
 
@@ -3129,6 +3167,14 @@ dumpDatabase(Archive *fout)
 		appendPQExpBufferStr(delQry, ";\n");
 	}
 
+	if (strcmp(dathaslogontriggers, "t") == 0)
+	{
+		appendPQExpBufferStr(creaQry, "UPDATE pg_catalog.pg_database "
+							 "SET dathaslogontriggers = true WHERE datname = ");
+		appendStringLiteralAH(creaQry, datname, fout);
+		appendPQExpBufferStr(creaQry, ";\n");
+	}
+
 	/* Add database-specific SET options */
 	dumpDatabaseConfig(fout, creaQry, datname, dbCatId.oid);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 6598c5369a..15992c22b0 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3008,7 +3008,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end",
+					  "client_connection", "sql_drop");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat
index b8aa1364a0..b89906e8ce 100644
--- a/src/include/catalog/pg_database.dat
+++ b/src/include/catalog/pg_database.dat
@@ -15,7 +15,7 @@
 { oid => '1', oid_symbol => 'TemplateDbOid',
   descr => 'default template for new databases',
   datname => 'template1', encoding => 'ENCODING', datcollate => 'LC_COLLATE',
-  datctype => 'LC_CTYPE', datistemplate => 't', datallowconn => 't',
+  datctype => 'LC_CTYPE', datistemplate => 't', dathaslogontriggers => 'f', datallowconn => 't',
   datconnlimit => '-1', datlastsysoid => '0', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datacl => '_null_' },
 
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index d3de45821c..b30574f79d 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -52,6 +52,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
 	/* new connections allowed? */
 	bool		datallowconn;
 
+	/* database has on-login triggers */
+	bool		dathaslogontriggers;
+
 	/* max connections allowed (-1=no limit) */
 	int32		datconnlimit;
 
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index c11bf2d781..e70d68b0f3 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -21,6 +21,8 @@
 #include "tcop/deparse_utility.h"
 #include "utils/aclchk_internal.h"
 
+extern bool enable_client_connection_trigger; /* GUC */
+
 typedef struct EventTriggerData
 {
 	NodeTag		type;
@@ -53,6 +55,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnConnect(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 9ba24d4ca9..0e64f90103 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -80,6 +80,7 @@ PG_CMDTAG(CMDTAG_CLUSTER, "CLUSTER", false, false, false)
 PG_CMDTAG(CMDTAG_COMMENT, "COMMENT", true, false, false)
 PG_CMDTAG(CMDTAG_COMMIT, "COMMIT", false, false, false)
 PG_CMDTAG(CMDTAG_COMMIT_PREPARED, "COMMIT PREPARED", false, false, false)
+PG_CMDTAG(CMDTAG_CONNECT, "CONNECT", true, false, false)
 PG_CMDTAG(CMDTAG_COPY, "COPY", false, false, true)
 PG_CMDTAG(CMDTAG_COPY_FROM, "COPY FROM", false, false, false)
 PG_CMDTAG(CMDTAG_CREATE_ACCESS_METHOD, "CREATE ACCESS METHOD", true, false, false)
diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h
index 968345404e..159fe5d939 100644
--- a/src/include/tcop/tcopprot.h
+++ b/src/include/tcop/tcopprot.h
@@ -31,6 +31,11 @@ extern int	max_stack_depth;
 extern int	PostAuthDelay;
 extern int	client_connection_check_interval;
 
+/* Hook for plugins to get control at start and end of session */
+typedef void (*client_connection_hook_type) (void);
+
+extern PGDLLIMPORT client_connection_hook_type client_connection_hook;
+
 /* GUC-configurable parameters */
 
 typedef enum
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index 58ddb71cb1..2440b408d1 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Connect,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index df6fdc20d1..87736d82c7 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,27 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+
+CREATE TABLE connects(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO connects (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON client_connection EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 $node_primary->wait_for_catchup($node_standby_1, 'replay',
 	$node_primary->lsn('insert'));
@@ -339,6 +360,9 @@ sub replay_check
 
 replay_check();
 
+$node_standby_1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$node_standby_2->safe_psql('postgres', "SELECT 2", extra_params => [ '-U', 'regress_user', '-w' ]);
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 369f3d7d84..49e0da7c9b 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -553,3 +553,40 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- On session start triggers
+create table connects(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into connects (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on client_connection execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id | who  
+----+------
+  1 | I am
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id | who  
+----+------
+  1 | I am
+  2 | I am
+(2 rows)
+
+-- Test handing exeptions in client_connection trigger
+drop table connects;
+-- superuser should ignore error
+\c
+NOTICE:  client_connection trigger failed with message: relation "connects" does not exist
+-- suppress trigger firing
+\c "dbname=regression options='-c enable_client_connection_trigger=false'"
+-- Cleanup
+drop event trigger on_login_trigger;
+drop function on_login_proc();
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 0bb558d93c..b395bc7a88 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -93,29 +93,30 @@ select count(*) = 0 as ok from pg_stat_wal_receiver;
 -- This is to record the prevailing planner enable_foo settings during
 -- a regression test run.
 select name, setting from pg_settings where name like 'enable%';
-              name              | setting 
---------------------------------+---------
- enable_async_append            | on
- enable_bitmapscan              | on
- enable_gathermerge             | on
- enable_hashagg                 | on
- enable_hashjoin                | on
- enable_incremental_sort        | on
- enable_indexonlyscan           | on
- enable_indexscan               | on
- enable_material                | on
- enable_mergejoin               | on
- enable_nestloop                | on
- enable_parallel_append         | on
- enable_parallel_hash           | on
- enable_partition_pruning       | on
- enable_partitionwise_aggregate | off
- enable_partitionwise_join      | off
- enable_resultcache             | on
- enable_seqscan                 | on
- enable_sort                    | on
- enable_tidscan                 | on
-(20 rows)
+               name               | setting 
+----------------------------------+---------
+ enable_async_append              | on
+ enable_bitmapscan                | on
+ enable_client_connection_trigger | on
+ enable_gathermerge               | on
+ enable_hashagg                   | on
+ enable_hashjoin                  | on
+ enable_incremental_sort          | on
+ enable_indexonlyscan             | on
+ enable_indexscan                 | on
+ enable_material                  | on
+ enable_mergejoin                 | on
+ enable_nestloop                  | on
+ enable_parallel_append           | on
+ enable_parallel_hash             | on
+ enable_partition_pruning         | on
+ enable_partitionwise_aggregate   | off
+ enable_partitionwise_join        | off
+ enable_resultcache               | on
+ enable_seqscan                   | on
+ enable_sort                      | on
+ enable_tidscan                   | on
+(21 rows)
 
 -- Test that the pg_timezone_names and pg_timezone_abbrevs views are
 -- more-or-less working.  We can't test their contents in any great detail
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index e79c5f0b5d..a16ba8455e 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -440,3 +440,31 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- On session start triggers
+create table connects(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into connects (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on client_connection execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+select * from connects;
+\c
+select * from connects;
+
+-- Test handing exeptions in client_connection trigger
+
+drop table connects;
+-- superuser should ignore error
+\c
+-- suppress trigger firing
+\c "dbname=regression options='-c enable_client_connection_trigger=false'"
+
+
+-- Cleanup
+drop event trigger on_login_trigger;
+drop function on_login_proc();
#58Greg Nancarrow
gregn4422@gmail.com
In reply to: Ivan Panchenko (#57)
Re: Re[3]: On login trigger: take three

On Thu, May 20, 2021 at 2:45 PM Ivan Panchenko <wao@mail.ru> wrote:

I have upgraded the patch for the 14th version.

I have some feedback on the patch:

(1) The patch adds 3 whitespace errors ("git apply <patch-file>"
reports 3 warnings)

(2) doc/src/sgml/catalogs.sgml

CURRENTLY:
This flag is used to avoid extra lookup of pg_event_trigger table on
each backend startup.

SUGGEST:
This flag is used to avoid extra lookups on the pg_event_trigger table
during each backend startup.

(3) doc/src/sgml/config.sgml

CURRENTLY:
Errors in trigger code can prevent user to login to the system.In this
case disabling this parameter in connection string can solve the
problem:

SUGGEST:
Errors in the trigger code can prevent a user from logging in to the
system. In this case, the parameter can be disabled in the connection
string, to allow the user to login:

(4) doc/src/sgml/event-trigger.sgml

(i)

CURRENTLY:
An event trigger fires whenever the event with which it is associated
occurs in the database in which it is defined. Currently, the only

SUGGEST:
An event trigger fires whenever an associated event occurs in the
database in which it is defined. Currently, the only

(ii)

CURRENTLY:
can be useful for client connections logging,

SUGGEST:
can be useful for logging client connections,

(5) src/backend/commands/event_trigger.c

(i) There are two instances of code blocks like:

xxxx = table_open(...);
tuple = SearchSysCacheCopy1(...);
table_close(...);

These should end with: "heap_freetuple(tuple);"

(ii) Typo "nuder" and grammar:

CURRENTLY:
There can be race condition: event trigger may be added after we have
scanned pg_event_trigger table. Repeat this test nuder pg_database
table lock.

SUGGEST:
There can be a race condition: the event trigger may be added after we
have scanned the pg_event_trigger table. Repeat this test under the
pg_database table lock.

(6) src/backend/utils/misc/postgresql.conf.sample

CURRENTLY:
+#enable_client_connection_trigger = true # enables firing the
client_connection trigger when a client connect

SUGGEST:
+#enable_client_connection_trigger = true # enables firing the
client_connection trigger when a client connects

Regards,
Greg Nancarrow
Fujitsu Australia

#59Greg Nancarrow
gregn4422@gmail.com
In reply to: Greg Nancarrow (#58)
1 attachment(s)
Re: Re[3]: On login trigger: take three

On Fri, May 21, 2021 at 2:46 PM Greg Nancarrow <gregn4422@gmail.com> wrote:

On Thu, May 20, 2021 at 2:45 PM Ivan Panchenko <wao@mail.ru> wrote:

I have upgraded the patch for the 14th version.

I have some feedback on the patch:

I've attached an updated version of the patch.
I've applied my review comments and done a bit more tidying up of
documentation and comments.
Also, I found that the previously-posted patch was broken by
snapshot-handling changes in commit 84f5c290 (with patch applied,
resulting in a coredump during regression tests) so I've added a fix
for that too.

Regards,
Greg Nancarrow
Fujitsu Australia

Attachments:

v15-0001-on_client_connect_event_trigger.patchapplication/octet-stream; name=v15-0001-on_client_connect_event_trigger.patchDownload
From 4ed175e6465a65a9817462d0bb2470aadb7af817 Mon Sep 17 00:00:00 2001
From: Greg Nancarrow <gregn4422@gmail.com>
Date: Thu, 3 Jun 2021 12:36:29 +1000
Subject: [PATCH] Add a new "client_connection" event and client connection
 trigger support.

The client_connection event occurs when a client connection to the server is
established. A new boolean GUC "enable_client_connection_trigger" is addded,
that enables firing of client_connection event triggers (switched on by
default). This parameter may be used (by superuser only) to disable
client_connection triggers, if any error in trigger code prevents a user from
logging in to the system. Errors in a client_connection trigger procedure are
ignored for superuser. An error message is delivered to the client as a NOTICE
in this case.

Author: Konstantin Knizhnik
Discussion: https://www.postgresql.org/message-id/flat/0d46d29f-4558-3af9-9c85-7774e14a7709%40postgrespro.ru
---
 doc/src/sgml/bki.sgml                         |   2 +-
 doc/src/sgml/catalogs.sgml                    |  11 ++
 doc/src/sgml/config.sgml                      |  19 ++
 doc/src/sgml/ecpg.sgml                        |   2 +
 doc/src/sgml/event-trigger.sgml               |  84 ++++++++-
 src/backend/commands/dbcommands.c             |   3 +-
 src/backend/commands/event_trigger.c          | 176 ++++++++++++++++--
 src/backend/tcop/postgres.c                   |   9 +
 src/backend/utils/cache/evtcache.c            |   2 +
 src/backend/utils/misc/guc.c                  |  13 ++
 src/backend/utils/misc/postgresql.conf.sample |   2 +
 src/bin/pg_dump/pg_dump.c                     |  58 +++++-
 src/bin/psql/tab-complete.c                   |   3 +-
 src/include/catalog/pg_database.dat           |   2 +-
 src/include/catalog/pg_database.h             |   3 +
 src/include/commands/event_trigger.h          |   3 +
 src/include/tcop/cmdtaglist.h                 |   1 +
 src/include/tcop/tcopprot.h                   |   5 +
 src/include/utils/evtcache.h                  |   3 +-
 src/test/recovery/t/001_stream_rep.pl         |  24 +++
 src/test/regress/expected/event_trigger.out   |  37 ++++
 src/test/regress/expected/sysviews.out        |  47 ++---
 src/test/regress/sql/event_trigger.sql        |  28 +++
 23 files changed, 484 insertions(+), 53 deletions(-)

diff --git a/doc/src/sgml/bki.sgml b/doc/src/sgml/bki.sgml
index db1b3d5e9a..e9751d013e 100644
--- a/doc/src/sgml/bki.sgml
+++ b/doc/src/sgml/bki.sgml
@@ -182,7 +182,7 @@
 { oid =&gt; '1', oid_symbol =&gt; 'TemplateDbOid',
   descr =&gt; 'database\'s default template',
   datname =&gt; 'template1', encoding =&gt; 'ENCODING', datcollate =&gt; 'LC_COLLATE',
-  datctype =&gt; 'LC_CTYPE', datistemplate =&gt; 't', datallowconn =&gt; 't',
+  datctype =&gt; 'LC_CTYPE', datistemplate =&gt; 't', datallowconn =&gt; 't', dathaslogontriggers =&gt; 'f',
   datconnlimit =&gt; '-1', datlastsysoid =&gt; '0', datfrozenxid =&gt; '0',
   datminmxid =&gt; '1', dattablespace =&gt; 'pg_default', datacl =&gt; '_null_' },
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 16493209c6..09ec48eaad 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2972,6 +2972,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>dathaslogontriggers</structfield> <type>bool</type>
+      </para>
+      <para>
+        Indicates that there are client connection triggers defined for this database.
+        This flag is used to avoid extra lookups on the pg_event_trigger table during each backend startup.
+        This flag is used internally by Postgres and should not be manually changed by DBA or application.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>datconnlimit</structfield> <type>int4</type>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index d8c0fd3315..fd9897789d 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1035,6 +1035,25 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-enable-client-connection-trigger" xreflabel="enable_client_connection_trigger">
+      <term><varname>enable_client_connection_trigger</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>enable_client_connection_trigger</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Enables firing the <literal>client_connection</literal>
+        trigger when a client connects. This parameter is switched on by
+        default. Errors in the trigger code can prevent the user from logging in
+        to the system. In this case, disabling this parameter in the connection
+        string can allow login so that the problem can then be resolved:
+        <literal>psql "dbname=postgres options='-c enable_client_connection_trigger=off'".</literal>
+        Only superuser can change this parameter.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
      </sect2>
 
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index 86c078e17d..c3176e763f 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -4731,6 +4731,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = t (type: 1)
 datallowconn = t (type: 1)
+dathaslogontriggers = f (type: 1)
 datconnlimit = -1 (type: 5)
 datlastsysoid = 11510 (type: 1)
 datfrozenxid = 379 (type: 1)
@@ -4756,6 +4757,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = f (type: 1)
 datallowconn = t (type: 1)
+dathaslogontriggers = f (type: 1)
 datconnlimit = -1 (type: 5)
 datlastsysoid = 11510 (type: 1)
 datfrozenxid = 379 (type: 1)
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 60366a950e..5263e2706d 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>client_connection</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,29 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>client_connection</literal> event occurs when a client connection
+     to the server is established.
+     There are two mechanisms for dealing with any bugs in a trigger procedure for
+     this event which might prevent successful login to the system:
+     <itemizedlist>
+      <listitem>
+       <para>
+         The configuration parameter <literal>enable_client_connection_trigger</literal>
+         makes it possible to disable firing the <literal>client_connection</literal>
+         trigger when a client connects.
+       </para>
+      </listitem>
+      <listitem>
+       <para>
+         Errors in the <literal>client_connection</literal> trigger procedure are
+         ignored for superuser. An error message is delivered to the client as a
+         <literal>NOTICE</literal> in this case.
+       </para>
+      </listitem>
+     </itemizedlist>
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1140,7 +1164,7 @@ typedef struct EventTriggerData
   </sect1>
 
   <sect1 id="event-trigger-example">
-   <title>A Complete Event Trigger Example</title>
+   <title>A C language Event Trigger Example</title>
 
    <para>
     Here is a very simple example of an event trigger function written in C.
@@ -1280,6 +1304,64 @@ $$;
 CREATE EVENT TRIGGER no_rewrite_allowed
                   ON table_rewrite
    EXECUTE FUNCTION no_rewrite();
+</programlisting>
+   </para>
+ </sect1>
+
+  <sect1 id="event-trigger-database-client-connection-example">
+   <title>A Database Client Connection Event Trigger Example</title>
+
+   <para>
+    The event trigger on the <literal>client_connection</literal> event can be
+    useful for logging client connections, for verifying the connection and
+    assigning roles according to current circumstances, or for some session data
+    initialization.
+   </para>
+
+   <para>
+    The following example demonstrates these options.
+<programlisting>
+
+-- create test tables and roles
+CREATE TABLE user_sessions_log (
+    "user" text,
+    "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+ RETURNS event_trigger SECURITY DEFINER
+ LANGUAGE plpgsql AS
+$$
+DECLARE
+    hour integer = EXTRACT('hour' FROM current_time);
+BEGIN
+-- 1) Assign some roles
+    IF hour BETWEEN 8 AND 20 THEN           -- at daytime grant the day_worker role
+        EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+        EXECUTE 'GRANT    day_worker TO '   || quote_ident(session_user);
+    ELSIF hour BETWEEN 2 AND 4 THEN
+        RAISE EXCEPTION 'Login forbidden';  -- do not allow to connect these hours
+    ELSE                                    -- at other time grant the night_worker role
+        EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+        EXECUTE 'GRANT  night_worker TO ' || quote_ident(session_user);
+    END IF;
+
+-- 2) Initialize some user session data
+   CREATE TEMP TABLE session_storage (x float, y integer);
+
+-- 3) Log the connection time
+   INSERT INTO user_sessions_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+   ON client_connection
+   EXECUTE FUNCTION init_session();
 </programlisting>
    </para>
  </sect1>
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 2b159b60eb..6ed0df3d15 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -560,6 +560,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	new_record[Anum_pg_database_datctype - 1] =
 		DirectFunctionCall1(namein, CStringGetDatum(dbctype));
 	new_record[Anum_pg_database_datistemplate - 1] = BoolGetDatum(dbistemplate);
+	new_record[Anum_pg_database_dathaslogontriggers - 1] = BoolGetDatum(false);
 	new_record[Anum_pg_database_datallowconn - 1] = BoolGetDatum(dballowconnections);
 	new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 	new_record[Anum_pg_database_datlastsysoid - 1] = ObjectIdGetDatum(src_lastsysoid);
@@ -1627,7 +1628,7 @@ AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel)
 		new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 		new_record_repl[Anum_pg_database_datconnlimit - 1] = true;
 	}
-
+	new_record[Anum_pg_database_dathaslogontriggers - 1] = BoolGetDatum(datform->dathaslogontriggers);
 	newtuple = heap_modify_tuple(tuple, RelationGetDescr(rel), new_record,
 								 new_record_nulls, new_record_repl);
 	CatalogTupleUpdate(rel, &tuple->t_self, newtuple);
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 5bde507c75..73b663a0cb 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -43,11 +44,15 @@
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
+bool enable_client_connection_trigger;
+
 typedef struct EventTriggerQueryState
 {
 	/* memory context for this state's objects */
@@ -130,6 +135,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "client_connection") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -293,6 +299,24 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
 	CatalogTupleInsert(tgrel, tuple);
 	heap_freetuple(tuple);
 
+	if (strcmp(eventname, "client_connection") == 0)
+	{
+		Form_pg_database db;
+		Relation pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+		/* Set dathaslogontriggers flag in pg_database */
+		tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+		db = (Form_pg_database) GETSTRUCT(tuple);
+		if (!db->dathaslogontriggers)
+		{
+			db->dathaslogontriggers = true;
+			CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+		}
+		table_close(pg_db, RowExclusiveLock);
+		heap_freetuple(tuple);
+	}
+
 	/* Depend on owner. */
 	recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
 
@@ -562,6 +586,9 @@ EventTriggerCommonSetup(Node *parsetree,
 	ListCell   *lc;
 	List	   *runlist = NIL;
 
+	/* Get the command tag. */
+	tag = parsetree ? CreateCommandTag(parsetree) : CMDTAG_CONNECT;
+
 	/*
 	 * We want the list of command tags for which this procedure is actually
 	 * invoked to match up exactly with the list that CREATE EVENT TRIGGER
@@ -577,22 +604,18 @@ EventTriggerCommonSetup(Node *parsetree,
 	 * relevant command tag.
 	 */
 #ifdef USE_ASSERT_CHECKING
+	if (event == EVT_DDLCommandStart ||
+		event == EVT_DDLCommandEnd ||
+		event == EVT_SQLDrop ||
+		event == EVT_Connect)
 	{
-		CommandTag	dbgtag;
-
-		dbgtag = CreateCommandTag(parsetree);
-		if (event == EVT_DDLCommandStart ||
-			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
-		{
-			if (!command_tag_event_trigger_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
-		else if (event == EVT_TableRewrite)
-		{
-			if (!command_tag_table_rewrite_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
+		if (!command_tag_event_trigger_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
+	}
+	else if (event == EVT_TableRewrite)
+	{
+		if (!command_tag_table_rewrite_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
 	}
 #endif
 
@@ -601,9 +624,6 @@ EventTriggerCommonSetup(Node *parsetree,
 	if (cachelist == NIL)
 		return NIL;
 
-	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
-
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
 	 * memory context.  Once we start running the command triggers, or indeed
@@ -800,6 +820,126 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+static bool
+DatabaseHasLogonTriggers(void)
+{
+	bool has_logon_triggers;
+	HeapTuple tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+	has_logon_triggers = ((Form_pg_database) GETSTRUCT(tuple))->dathaslogontriggers;
+	ReleaseSysCache(tuple);
+	return has_logon_triggers;
+}
+
+/*
+ * Fire connect triggers.
+ */
+void
+EventTriggerOnConnect(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster
+		|| !OidIsValid(MyDatabaseId)
+		|| !enable_client_connection_trigger)
+		return;
+
+	StartTransactionCommand();
+
+	if (DatabaseHasLogonTriggers())
+	{
+		runlist = EventTriggerCommonSetup(NULL,
+										  EVT_Connect, "connect",
+										  &trigdata);
+
+		if (runlist != NIL)
+		{
+			MemoryContext old_context = CurrentMemoryContext;
+			bool is_superuser = superuser();
+			/*
+			 * Make sure anything the main command did will be visible to the event
+			 * triggers.
+			 */
+			CommandCounterIncrement();
+
+			/*
+			 * Event trigger execution may require an active snapshot.
+			 */
+			PushActiveSnapshot(GetTransactionSnapshot());
+
+			/* Run the triggers. */
+			PG_TRY();
+			{
+				EventTriggerInvoke(runlist, &trigdata);
+				list_free(runlist);
+			}
+			PG_CATCH();
+			{
+				ErrorData* error;
+				/*
+				 * Try to ignore error for superuser to make it possible to login even in case of errors
+				 * during trigger execution
+				 */
+				if (!is_superuser)
+					PG_RE_THROW();
+
+				MemoryContextSwitchTo(old_context);
+				error = CopyErrorData();
+				FlushErrorState();
+				elog(NOTICE, "client_connection trigger failed with message: %s", error->message);
+				AbortCurrentTransaction();
+				return;
+			}
+			PG_END_TRY();
+
+			PopActiveSnapshot();
+		}
+		else
+		{
+			/* Runtlist is empty: clear dathaslogontriggers flag
+			 */
+			Relation pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+			HeapTuple tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+			Form_pg_database db;
+
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+			db = (Form_pg_database) GETSTRUCT(tuple);
+			if (db->dathaslogontriggers)
+			{
+				db->dathaslogontriggers = false;
+				CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				/*
+				 * There can be a race condition: the event trigger may be added
+				 * after we have scanned the pg_event_trigger table. Repeat this
+				 * test under the pg_database table lock.
+				 */
+				AcceptInvalidationMessages();
+				runlist = EventTriggerCommonSetup(NULL,
+												  EVT_Connect, "connect",
+												  &trigdata);
+				if (runlist != NULL) /* if list is not empty, then restore the flag */
+				{
+					db->dathaslogontriggers = true;
+					CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				}
+			}
+			table_close(pg_db, RowExclusiveLock);
+			heap_freetuple(tuple);
+		}
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 8cea10c901..e9f6da3501 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -42,6 +42,7 @@
 #include "catalog/pg_type.h"
 #include "commands/async.h"
 #include "commands/prepare.h"
+#include "commands/event_trigger.h"
 #include "executor/spi.h"
 #include "jit/jit.h"
 #include "libpq/libpq.h"
@@ -182,6 +183,9 @@ static ProcSignalReason RecoveryConflictReason;
 static MemoryContext row_description_context = NULL;
 static StringInfoData row_description_buf;
 
+/* Hook for plugins to get control at start of session */
+client_connection_hook_type client_connection_hook = EventTriggerOnConnect;
+
 /* ----------------------------------------------------------------
  *		decls for routines only used in this file
  * ----------------------------------------------------------------
@@ -4165,6 +4169,11 @@ PostgresMain(int argc, char *argv[],
 	if (!IsUnderPostmaster)
 		PgStartTime = GetCurrentTimestamp();
 
+	if (client_connection_hook)
+	{
+		(*client_connection_hook) ();
+	}
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 460b720a65..d0093191b8 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "client_connection") == 0)
+			event = EVT_Connect;
 		else
 			continue;
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 68b62d523d..51e320f80d 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -47,6 +47,7 @@
 #include "commands/async.h"
 #include "commands/prepare.h"
 #include "commands/trigger.h"
+#include "commands/event_trigger.h"
 #include "commands/user.h"
 #include "commands/vacuum.h"
 #include "commands/variable.h"
@@ -952,6 +953,18 @@ static const unit_conversion time_unit_conversion_table[] =
 
 static struct config_bool ConfigureNamesBool[] =
 {
+	{
+		{"enable_client_connection_trigger", PGC_SU_BACKEND, DEVELOPER_OPTIONS,
+			gettext_noop("Enables the client_connection event trigger."),
+			gettext_noop("In case of errors in the ON client_connection EVENT TRIGGER procedure, "
+						 "this parameter can be used to disable trigger activation "
+						 "and provide access to the database."),
+			GUC_EXPLAIN
+		},
+		&enable_client_connection_trigger,
+		true,
+		NULL, NULL, NULL
+	},
 	{
 		{"enable_seqscan", PGC_USERSET, QUERY_TUNING_METHOD,
 			gettext_noop("Enables the planner's use of sequential-scan plans."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index ddbb6dc2be..54ea3827d6 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -72,6 +72,8 @@
 					# (change requires restart)
 #bonjour_name = ''			# defaults to the computer name
 					# (change requires restart)
+#enable_client_connection_trigger = on	# enables firing the client_connection
+					# trigger when a client connects
 
 # - TCP settings -
 # see "man tcp" for details
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 8f53cc7c3b..02d6aebb80 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -2804,6 +2804,7 @@ dumpDatabase(Archive *fout)
 				i_datacl,
 				i_rdatacl,
 				i_datistemplate,
+				i_dathaslogontriggers,
 				i_datconnlimit,
 				i_tablespace;
 	CatalogId	dbCatId;
@@ -2816,6 +2817,7 @@ dumpDatabase(Archive *fout)
 			   *datacl,
 			   *rdatacl,
 			   *datistemplate,
+			   *dathaslogontriggers,
 			   *datconnlimit,
 			   *tablespace;
 	uint32		frozenxid,
@@ -2834,7 +2836,7 @@ dumpDatabase(Archive *fout)
 	 * (pg_init_privs) are not supported on databases, so this logic cannot
 	 * make use of buildACLQueries().
 	 */
-	if (fout->remoteVersion >= 90600)
+	if (fout->remoteVersion >= 140000)
 	{
 		appendPQExpBuffer(dbQry, "SELECT tableoid, oid, datname, "
 						  "(%s datdba) AS dba, "
@@ -2860,7 +2862,41 @@ dumpDatabase(Archive *fout)
 						  "       AS permp(orig_acl) "
 						  "     WHERE acl = orig_acl)) AS rdatacls) "
 						  " AS rdatacl, "
-						  "datistemplate, datconnlimit, "
+						  "datistemplate, datconnlimit, dathaslogontriggers, "
+						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
+						  "shobj_description(oid, 'pg_database') AS description "
+
+						  "FROM pg_database "
+						  "WHERE datname = current_database()",
+						  username_subquery);
+	}
+	else if (fout->remoteVersion >= 90600)
+	{
+		appendPQExpBuffer(dbQry, "SELECT tableoid, oid, datname, "
+						  "(%s datdba) AS dba, "
+						  "pg_encoding_to_char(encoding) AS encoding, "
+						  "datcollate, datctype, datfrozenxid, datminmxid, "
+						  "(SELECT array_agg(acl ORDER BY row_n) FROM "
+						  "  (SELECT acl, row_n FROM "
+						  "     unnest(coalesce(datacl,acldefault('d',datdba))) "
+						  "     WITH ORDINALITY AS perm(acl,row_n) "
+						  "   WHERE NOT EXISTS ( "
+						  "     SELECT 1 "
+						  "     FROM unnest(acldefault('d',datdba)) "
+						  "       AS init(init_acl) "
+						  "     WHERE acl = init_acl)) AS datacls) "
+						  " AS datacl, "
+						  "(SELECT array_agg(acl ORDER BY row_n) FROM "
+						  "  (SELECT acl, row_n FROM "
+						  "     unnest(acldefault('d',datdba)) "
+						  "     WITH ORDINALITY AS initp(acl,row_n) "
+						  "   WHERE NOT EXISTS ( "
+						  "     SELECT 1 "
+						  "     FROM unnest(coalesce(datacl,acldefault('d',datdba))) "
+						  "       AS permp(orig_acl) "
+						  "     WHERE acl = orig_acl)) AS rdatacls) "
+						  " AS rdatacl, "
+						  "datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2874,7 +2910,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "datcollate, datctype, datfrozenxid, datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogontriggers"
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2888,7 +2924,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "datcollate, datctype, datfrozenxid, 0 AS datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2902,7 +2938,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "NULL AS datcollate, NULL AS datctype, datfrozenxid, 0 AS datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2917,7 +2953,7 @@ dumpDatabase(Archive *fout)
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "NULL AS datcollate, NULL AS datctype, datfrozenxid, 0 AS datminmxid, "
 						  "datacl, '' as rdatacl, datistemplate, "
-						  "-1 as datconnlimit, "
+						  "-1 as datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace "
 						  "FROM pg_database "
 						  "WHERE datname = current_database()",
@@ -2939,6 +2975,7 @@ dumpDatabase(Archive *fout)
 	i_rdatacl = PQfnumber(res, "rdatacl");
 	i_datistemplate = PQfnumber(res, "datistemplate");
 	i_datconnlimit = PQfnumber(res, "datconnlimit");
+	i_dathaslogontriggers = PQfnumber(res, "dathaslogontriggers");
 	i_tablespace = PQfnumber(res, "tablespace");
 
 	dbCatId.tableoid = atooid(PQgetvalue(res, 0, i_tableoid));
@@ -2953,6 +2990,7 @@ dumpDatabase(Archive *fout)
 	datacl = PQgetvalue(res, 0, i_datacl);
 	rdatacl = PQgetvalue(res, 0, i_rdatacl);
 	datistemplate = PQgetvalue(res, 0, i_datistemplate);
+	dathaslogontriggers = PQgetvalue(res, 0, i_dathaslogontriggers);
 	datconnlimit = PQgetvalue(res, 0, i_datconnlimit);
 	tablespace = PQgetvalue(res, 0, i_tablespace);
 
@@ -3126,6 +3164,14 @@ dumpDatabase(Archive *fout)
 		appendPQExpBufferStr(delQry, ";\n");
 	}
 
+	if (strcmp(dathaslogontriggers, "t") == 0)
+	{
+		appendPQExpBufferStr(creaQry, "UPDATE pg_catalog.pg_database "
+							 "SET dathaslogontriggers = true WHERE datname = ");
+		appendStringLiteralAH(creaQry, datname, fout);
+		appendPQExpBufferStr(creaQry, ";\n");
+	}
+
 	/* Add database-specific SET options */
 	dumpDatabaseConfig(fout, creaQry, datname, dbCatId.oid);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 109b22acb6..2c5c2acf1d 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3009,7 +3009,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end",
+					  "client_connection", "sql_drop");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat
index b8aa1364a0..b89906e8ce 100644
--- a/src/include/catalog/pg_database.dat
+++ b/src/include/catalog/pg_database.dat
@@ -15,7 +15,7 @@
 { oid => '1', oid_symbol => 'TemplateDbOid',
   descr => 'default template for new databases',
   datname => 'template1', encoding => 'ENCODING', datcollate => 'LC_COLLATE',
-  datctype => 'LC_CTYPE', datistemplate => 't', datallowconn => 't',
+  datctype => 'LC_CTYPE', datistemplate => 't', dathaslogontriggers => 'f', datallowconn => 't',
   datconnlimit => '-1', datlastsysoid => '0', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datacl => '_null_' },
 
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index d3de45821c..b30574f79d 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -52,6 +52,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
 	/* new connections allowed? */
 	bool		datallowconn;
 
+	/* database has on-login triggers */
+	bool		dathaslogontriggers;
+
 	/* max connections allowed (-1=no limit) */
 	int32		datconnlimit;
 
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index c11bf2d781..e70d68b0f3 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -21,6 +21,8 @@
 #include "tcop/deparse_utility.h"
 #include "utils/aclchk_internal.h"
 
+extern bool enable_client_connection_trigger; /* GUC */
+
 typedef struct EventTriggerData
 {
 	NodeTag		type;
@@ -53,6 +55,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnConnect(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 9ba24d4ca9..0e64f90103 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -80,6 +80,7 @@ PG_CMDTAG(CMDTAG_CLUSTER, "CLUSTER", false, false, false)
 PG_CMDTAG(CMDTAG_COMMENT, "COMMENT", true, false, false)
 PG_CMDTAG(CMDTAG_COMMIT, "COMMIT", false, false, false)
 PG_CMDTAG(CMDTAG_COMMIT_PREPARED, "COMMIT PREPARED", false, false, false)
+PG_CMDTAG(CMDTAG_CONNECT, "CONNECT", true, false, false)
 PG_CMDTAG(CMDTAG_COPY, "COPY", false, false, true)
 PG_CMDTAG(CMDTAG_COPY_FROM, "COPY FROM", false, false, false)
 PG_CMDTAG(CMDTAG_CREATE_ACCESS_METHOD, "CREATE ACCESS METHOD", true, false, false)
diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h
index 968345404e..159fe5d939 100644
--- a/src/include/tcop/tcopprot.h
+++ b/src/include/tcop/tcopprot.h
@@ -31,6 +31,11 @@ extern int	max_stack_depth;
 extern int	PostAuthDelay;
 extern int	client_connection_check_interval;
 
+/* Hook for plugins to get control at start and end of session */
+typedef void (*client_connection_hook_type) (void);
+
+extern PGDLLIMPORT client_connection_hook_type client_connection_hook;
+
 /* GUC-configurable parameters */
 
 typedef enum
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index 58ddb71cb1..2440b408d1 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Connect,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index df6fdc20d1..87736d82c7 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,27 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+
+CREATE TABLE connects(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO connects (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON client_connection EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 $node_primary->wait_for_catchup($node_standby_1, 'replay',
 	$node_primary->lsn('insert'));
@@ -339,6 +360,9 @@ sub replay_check
 
 replay_check();
 
+$node_standby_1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$node_standby_2->safe_psql('postgres', "SELECT 2", extra_params => [ '-U', 'regress_user', '-w' ]);
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 369f3d7d84..49e0da7c9b 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -553,3 +553,40 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- On session start triggers
+create table connects(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into connects (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on client_connection execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id | who  
+----+------
+  1 | I am
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id | who  
+----+------
+  1 | I am
+  2 | I am
+(2 rows)
+
+-- Test handing exeptions in client_connection trigger
+drop table connects;
+-- superuser should ignore error
+\c
+NOTICE:  client_connection trigger failed with message: relation "connects" does not exist
+-- suppress trigger firing
+\c "dbname=regression options='-c enable_client_connection_trigger=false'"
+-- Cleanup
+drop event trigger on_login_trigger;
+drop function on_login_proc();
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 0bb558d93c..b395bc7a88 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -93,29 +93,30 @@ select count(*) = 0 as ok from pg_stat_wal_receiver;
 -- This is to record the prevailing planner enable_foo settings during
 -- a regression test run.
 select name, setting from pg_settings where name like 'enable%';
-              name              | setting 
---------------------------------+---------
- enable_async_append            | on
- enable_bitmapscan              | on
- enable_gathermerge             | on
- enable_hashagg                 | on
- enable_hashjoin                | on
- enable_incremental_sort        | on
- enable_indexonlyscan           | on
- enable_indexscan               | on
- enable_material                | on
- enable_mergejoin               | on
- enable_nestloop                | on
- enable_parallel_append         | on
- enable_parallel_hash           | on
- enable_partition_pruning       | on
- enable_partitionwise_aggregate | off
- enable_partitionwise_join      | off
- enable_resultcache             | on
- enable_seqscan                 | on
- enable_sort                    | on
- enable_tidscan                 | on
-(20 rows)
+               name               | setting 
+----------------------------------+---------
+ enable_async_append              | on
+ enable_bitmapscan                | on
+ enable_client_connection_trigger | on
+ enable_gathermerge               | on
+ enable_hashagg                   | on
+ enable_hashjoin                  | on
+ enable_incremental_sort          | on
+ enable_indexonlyscan             | on
+ enable_indexscan                 | on
+ enable_material                  | on
+ enable_mergejoin                 | on
+ enable_nestloop                  | on
+ enable_parallel_append           | on
+ enable_parallel_hash             | on
+ enable_partition_pruning         | on
+ enable_partitionwise_aggregate   | off
+ enable_partitionwise_join        | off
+ enable_resultcache               | on
+ enable_seqscan                   | on
+ enable_sort                      | on
+ enable_tidscan                   | on
+(21 rows)
 
 -- Test that the pg_timezone_names and pg_timezone_abbrevs views are
 -- more-or-less working.  We can't test their contents in any great detail
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index e79c5f0b5d..a16ba8455e 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -440,3 +440,31 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- On session start triggers
+create table connects(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into connects (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on client_connection execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+select * from connects;
+\c
+select * from connects;
+
+-- Test handing exeptions in client_connection trigger
+
+drop table connects;
+-- superuser should ignore error
+\c
+-- suppress trigger firing
+\c "dbname=regression options='-c enable_client_connection_trigger=false'"
+
+
+-- Cleanup
+drop event trigger on_login_trigger;
+drop function on_login_proc();
-- 
2.27.0

#60vignesh C
vignesh21@gmail.com
In reply to: Greg Nancarrow (#59)
Re: Re[3]: On login trigger: take three

On Thu, Jun 3, 2021 at 8:36 AM Greg Nancarrow <gregn4422@gmail.com> wrote:

On Fri, May 21, 2021 at 2:46 PM Greg Nancarrow <gregn4422@gmail.com> wrote:

On Thu, May 20, 2021 at 2:45 PM Ivan Panchenko <wao@mail.ru> wrote:

I have upgraded the patch for the 14th version.

I have some feedback on the patch:

I've attached an updated version of the patch.
I've applied my review comments and done a bit more tidying up of
documentation and comments.
Also, I found that the previously-posted patch was broken by
snapshot-handling changes in commit 84f5c290 (with patch applied,
resulting in a coredump during regression tests) so I've added a fix
for that too.

CFBot shows the following failure:
# poll_query_until timed out executing this query:
# SELECT '0/3046250' <= replay_lsn AND state = 'streaming' FROM
pg_catalog.pg_stat_replication WHERE application_name = 'standby_1';
# expecting this output:
# t
# last actual query output:
# t
# with stderr:
# NOTICE: You are welcome!
# Looks like your test exited with 29 before it could output anything.
t/001_stream_rep.pl ..................
Dubious, test returned 29 (wstat 7424, 0x1d00)

Regards,
Vignesh

#61Greg Nancarrow
gregn4422@gmail.com
In reply to: vignesh C (#60)
1 attachment(s)
Re: Re[3]: On login trigger: take three

On Sun, Jul 4, 2021 at 1:21 PM vignesh C <vignesh21@gmail.com> wrote:

CFBot shows the following failure:
# poll_query_until timed out executing this query:
# SELECT '0/3046250' <= replay_lsn AND state = 'streaming' FROM
pg_catalog.pg_stat_replication WHERE application_name = 'standby_1';
# expecting this output:
# t
# last actual query output:
# t
# with stderr:
# NOTICE: You are welcome!
# Looks like your test exited with 29 before it could output anything.
t/001_stream_rep.pl ..................
Dubious, test returned 29 (wstat 7424, 0x1d00)

Thanks.
I found that the patch was broken by commit f452aaf7d (the part
"adjust poll_query_until to insist on empty stderr as well as a stdout
match").
So I had to remove a "RAISE NOTICE" (which was just an informational
message) from the login trigger function, to satisfy the new
poll_query_until expectations.
Also, I updated a PG14 version check (now must check PG15 version).

Regards,
Greg Nancarrow
Fujitsu Australia

Attachments:

v16-0001-on_client_connect_event_trigger.patchapplication/octet-stream; name=v16-0001-on_client_connect_event_trigger.patchDownload
From a9c56042baee37aad501b9998d1d262cb4528314 Mon Sep 17 00:00:00 2001
From: Greg Nancarrow <gregn4422@gmail.com>
Date: Wed, 7 Jul 2021 10:17:57 +1000
Subject: [PATCH] Add a new "client_connection" event and client connection
 trigger support.

The client_connection event occurs when a client connection to the server is
established. A new boolean GUC "enable_client_connection_trigger" is addded,
that enables firing of client_connection event triggers (switched on by
default). This parameter may be used (by superuser only) to disable
client_connection triggers, if any error in trigger code prevents a user from
logging in to the system. Errors in a client_connection trigger procedure are
ignored for superuser. An error message is delivered to the client as a NOTICE
in this case.

Author: Konstantin Knizhnik
Discussion: https://www.postgresql.org/message-id/flat/0d46d29f-4558-3af9-9c85-7774e14a7709%40postgrespro.ru
---
 doc/src/sgml/bki.sgml                         |   2 +-
 doc/src/sgml/catalogs.sgml                    |  11 ++
 doc/src/sgml/config.sgml                      |  19 ++
 doc/src/sgml/ecpg.sgml                        |   2 +
 doc/src/sgml/event-trigger.sgml               |  84 ++++++++-
 src/backend/commands/dbcommands.c             |   3 +-
 src/backend/commands/event_trigger.c          | 176 ++++++++++++++++--
 src/backend/tcop/postgres.c                   |   9 +
 src/backend/utils/cache/evtcache.c            |   2 +
 src/backend/utils/misc/guc.c                  |  13 ++
 src/backend/utils/misc/postgresql.conf.sample |   2 +
 src/bin/pg_dump/pg_dump.c                     |  58 +++++-
 src/bin/psql/tab-complete.c                   |   3 +-
 src/include/catalog/pg_database.dat           |   2 +-
 src/include/catalog/pg_database.h             |   3 +
 src/include/commands/event_trigger.h          |   3 +
 src/include/tcop/cmdtaglist.h                 |   1 +
 src/include/tcop/tcopprot.h                   |   5 +
 src/include/utils/evtcache.h                  |   3 +-
 src/test/recovery/t/001_stream_rep.pl         |  23 +++
 src/test/regress/expected/event_trigger.out   |  37 ++++
 src/test/regress/expected/sysviews.out        |  47 ++---
 src/test/regress/sql/event_trigger.sql        |  28 +++
 23 files changed, 483 insertions(+), 53 deletions(-)

diff --git a/doc/src/sgml/bki.sgml b/doc/src/sgml/bki.sgml
index db1b3d5e9a..e9751d013e 100644
--- a/doc/src/sgml/bki.sgml
+++ b/doc/src/sgml/bki.sgml
@@ -182,7 +182,7 @@
 { oid =&gt; '1', oid_symbol =&gt; 'TemplateDbOid',
   descr =&gt; 'database\'s default template',
   datname =&gt; 'template1', encoding =&gt; 'ENCODING', datcollate =&gt; 'LC_COLLATE',
-  datctype =&gt; 'LC_CTYPE', datistemplate =&gt; 't', datallowconn =&gt; 't',
+  datctype =&gt; 'LC_CTYPE', datistemplate =&gt; 't', datallowconn =&gt; 't', dathaslogontriggers =&gt; 'f',
   datconnlimit =&gt; '-1', datlastsysoid =&gt; '0', datfrozenxid =&gt; '0',
   datminmxid =&gt; '1', dattablespace =&gt; 'pg_default', datacl =&gt; '_null_' },
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index f517a7d4af..efc9e406cc 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2972,6 +2972,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>dathaslogontriggers</structfield> <type>bool</type>
+      </para>
+      <para>
+        Indicates that there are client connection triggers defined for this database.
+        This flag is used to avoid extra lookups on the pg_event_trigger table during each backend startup.
+        This flag is used internally by Postgres and should not be manually changed by DBA or application.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>datconnlimit</structfield> <type>int4</type>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 381d8636ab..3fc4a71d04 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1035,6 +1035,25 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-enable-client-connection-trigger" xreflabel="enable_client_connection_trigger">
+      <term><varname>enable_client_connection_trigger</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>enable_client_connection_trigger</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Enables firing the <literal>client_connection</literal>
+        trigger when a client connects. This parameter is switched on by
+        default. Errors in the trigger code can prevent the user from logging in
+        to the system. In this case, disabling this parameter in the connection
+        string can allow login so that the problem can then be resolved:
+        <literal>psql "dbname=postgres options='-c enable_client_connection_trigger=off'".</literal>
+        Only superuser can change this parameter.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
      </sect2>
 
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index 9d5505cb84..49fbf2566c 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -4731,6 +4731,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = t (type: 1)
 datallowconn = t (type: 1)
+dathaslogontriggers = f (type: 1)
 datconnlimit = -1 (type: 5)
 datlastsysoid = 11510 (type: 1)
 datfrozenxid = 379 (type: 1)
@@ -4756,6 +4757,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = f (type: 1)
 datallowconn = t (type: 1)
+dathaslogontriggers = f (type: 1)
 datconnlimit = -1 (type: 5)
 datlastsysoid = 11510 (type: 1)
 datfrozenxid = 379 (type: 1)
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 60366a950e..5263e2706d 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>client_connection</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,29 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>client_connection</literal> event occurs when a client connection
+     to the server is established.
+     There are two mechanisms for dealing with any bugs in a trigger procedure for
+     this event which might prevent successful login to the system:
+     <itemizedlist>
+      <listitem>
+       <para>
+         The configuration parameter <literal>enable_client_connection_trigger</literal>
+         makes it possible to disable firing the <literal>client_connection</literal>
+         trigger when a client connects.
+       </para>
+      </listitem>
+      <listitem>
+       <para>
+         Errors in the <literal>client_connection</literal> trigger procedure are
+         ignored for superuser. An error message is delivered to the client as a
+         <literal>NOTICE</literal> in this case.
+       </para>
+      </listitem>
+     </itemizedlist>
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1140,7 +1164,7 @@ typedef struct EventTriggerData
   </sect1>
 
   <sect1 id="event-trigger-example">
-   <title>A Complete Event Trigger Example</title>
+   <title>A C language Event Trigger Example</title>
 
    <para>
     Here is a very simple example of an event trigger function written in C.
@@ -1280,6 +1304,64 @@ $$;
 CREATE EVENT TRIGGER no_rewrite_allowed
                   ON table_rewrite
    EXECUTE FUNCTION no_rewrite();
+</programlisting>
+   </para>
+ </sect1>
+
+  <sect1 id="event-trigger-database-client-connection-example">
+   <title>A Database Client Connection Event Trigger Example</title>
+
+   <para>
+    The event trigger on the <literal>client_connection</literal> event can be
+    useful for logging client connections, for verifying the connection and
+    assigning roles according to current circumstances, or for some session data
+    initialization.
+   </para>
+
+   <para>
+    The following example demonstrates these options.
+<programlisting>
+
+-- create test tables and roles
+CREATE TABLE user_sessions_log (
+    "user" text,
+    "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+ RETURNS event_trigger SECURITY DEFINER
+ LANGUAGE plpgsql AS
+$$
+DECLARE
+    hour integer = EXTRACT('hour' FROM current_time);
+BEGIN
+-- 1) Assign some roles
+    IF hour BETWEEN 8 AND 20 THEN           -- at daytime grant the day_worker role
+        EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+        EXECUTE 'GRANT    day_worker TO '   || quote_ident(session_user);
+    ELSIF hour BETWEEN 2 AND 4 THEN
+        RAISE EXCEPTION 'Login forbidden';  -- do not allow to connect these hours
+    ELSE                                    -- at other time grant the night_worker role
+        EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+        EXECUTE 'GRANT  night_worker TO ' || quote_ident(session_user);
+    END IF;
+
+-- 2) Initialize some user session data
+   CREATE TEMP TABLE session_storage (x float, y integer);
+
+-- 3) Log the connection time
+   INSERT INTO user_sessions_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+   ON client_connection
+   EXECUTE FUNCTION init_session();
 </programlisting>
    </para>
  </sect1>
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 2b159b60eb..6ed0df3d15 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -560,6 +560,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	new_record[Anum_pg_database_datctype - 1] =
 		DirectFunctionCall1(namein, CStringGetDatum(dbctype));
 	new_record[Anum_pg_database_datistemplate - 1] = BoolGetDatum(dbistemplate);
+	new_record[Anum_pg_database_dathaslogontriggers - 1] = BoolGetDatum(false);
 	new_record[Anum_pg_database_datallowconn - 1] = BoolGetDatum(dballowconnections);
 	new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 	new_record[Anum_pg_database_datlastsysoid - 1] = ObjectIdGetDatum(src_lastsysoid);
@@ -1627,7 +1628,7 @@ AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel)
 		new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 		new_record_repl[Anum_pg_database_datconnlimit - 1] = true;
 	}
-
+	new_record[Anum_pg_database_dathaslogontriggers - 1] = BoolGetDatum(datform->dathaslogontriggers);
 	newtuple = heap_modify_tuple(tuple, RelationGetDescr(rel), new_record,
 								 new_record_nulls, new_record_repl);
 	CatalogTupleUpdate(rel, &tuple->t_self, newtuple);
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 9c31c9e763..27bfb8339f 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -43,11 +44,15 @@
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
+bool enable_client_connection_trigger;
+
 typedef struct EventTriggerQueryState
 {
 	/* memory context for this state's objects */
@@ -130,6 +135,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "client_connection") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -293,6 +299,24 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
 	CatalogTupleInsert(tgrel, tuple);
 	heap_freetuple(tuple);
 
+	if (strcmp(eventname, "client_connection") == 0)
+	{
+		Form_pg_database db;
+		Relation pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+		/* Set dathaslogontriggers flag in pg_database */
+		tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+		db = (Form_pg_database) GETSTRUCT(tuple);
+		if (!db->dathaslogontriggers)
+		{
+			db->dathaslogontriggers = true;
+			CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+		}
+		table_close(pg_db, RowExclusiveLock);
+		heap_freetuple(tuple);
+	}
+
 	/* Depend on owner. */
 	recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
 
@@ -562,6 +586,9 @@ EventTriggerCommonSetup(Node *parsetree,
 	ListCell   *lc;
 	List	   *runlist = NIL;
 
+	/* Get the command tag. */
+	tag = parsetree ? CreateCommandTag(parsetree) : CMDTAG_CONNECT;
+
 	/*
 	 * We want the list of command tags for which this procedure is actually
 	 * invoked to match up exactly with the list that CREATE EVENT TRIGGER
@@ -577,22 +604,18 @@ EventTriggerCommonSetup(Node *parsetree,
 	 * relevant command tag.
 	 */
 #ifdef USE_ASSERT_CHECKING
+	if (event == EVT_DDLCommandStart ||
+		event == EVT_DDLCommandEnd ||
+		event == EVT_SQLDrop ||
+		event == EVT_Connect)
 	{
-		CommandTag	dbgtag;
-
-		dbgtag = CreateCommandTag(parsetree);
-		if (event == EVT_DDLCommandStart ||
-			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
-		{
-			if (!command_tag_event_trigger_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
-		else if (event == EVT_TableRewrite)
-		{
-			if (!command_tag_table_rewrite_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
+		if (!command_tag_event_trigger_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
+	}
+	else if (event == EVT_TableRewrite)
+	{
+		if (!command_tag_table_rewrite_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
 	}
 #endif
 
@@ -601,9 +624,6 @@ EventTriggerCommonSetup(Node *parsetree,
 	if (cachelist == NIL)
 		return NIL;
 
-	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
-
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
 	 * memory context.  Once we start running the command triggers, or indeed
@@ -800,6 +820,126 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+static bool
+DatabaseHasLogonTriggers(void)
+{
+	bool has_logon_triggers;
+	HeapTuple tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+	has_logon_triggers = ((Form_pg_database) GETSTRUCT(tuple))->dathaslogontriggers;
+	ReleaseSysCache(tuple);
+	return has_logon_triggers;
+}
+
+/*
+ * Fire connect triggers.
+ */
+void
+EventTriggerOnConnect(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster
+		|| !OidIsValid(MyDatabaseId)
+		|| !enable_client_connection_trigger)
+		return;
+
+	StartTransactionCommand();
+
+	if (DatabaseHasLogonTriggers())
+	{
+		runlist = EventTriggerCommonSetup(NULL,
+										  EVT_Connect, "connect",
+										  &trigdata);
+
+		if (runlist != NIL)
+		{
+			MemoryContext old_context = CurrentMemoryContext;
+			bool is_superuser = superuser();
+			/*
+			 * Make sure anything the main command did will be visible to the event
+			 * triggers.
+			 */
+			CommandCounterIncrement();
+
+			/*
+			 * Event trigger execution may require an active snapshot.
+			 */
+			PushActiveSnapshot(GetTransactionSnapshot());
+
+			/* Run the triggers. */
+			PG_TRY();
+			{
+				EventTriggerInvoke(runlist, &trigdata);
+				list_free(runlist);
+			}
+			PG_CATCH();
+			{
+				ErrorData* error;
+				/*
+				 * Try to ignore error for superuser to make it possible to login even in case of errors
+				 * during trigger execution
+				 */
+				if (!is_superuser)
+					PG_RE_THROW();
+
+				MemoryContextSwitchTo(old_context);
+				error = CopyErrorData();
+				FlushErrorState();
+				elog(NOTICE, "client_connection trigger failed with message: %s", error->message);
+				AbortCurrentTransaction();
+				return;
+			}
+			PG_END_TRY();
+
+			PopActiveSnapshot();
+		}
+		else
+		{
+			/* Runtlist is empty: clear dathaslogontriggers flag
+			 */
+			Relation pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+			HeapTuple tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+			Form_pg_database db;
+
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+			db = (Form_pg_database) GETSTRUCT(tuple);
+			if (db->dathaslogontriggers)
+			{
+				db->dathaslogontriggers = false;
+				CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				/*
+				 * There can be a race condition: the event trigger may be added
+				 * after we have scanned the pg_event_trigger table. Repeat this
+				 * test under the pg_database table lock.
+				 */
+				AcceptInvalidationMessages();
+				runlist = EventTriggerCommonSetup(NULL,
+												  EVT_Connect, "connect",
+												  &trigdata);
+				if (runlist != NULL) /* if list is not empty, then restore the flag */
+				{
+					db->dathaslogontriggers = true;
+					CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				}
+			}
+			table_close(pg_db, RowExclusiveLock);
+			heap_freetuple(tuple);
+		}
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 8cea10c901..e9f6da3501 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -42,6 +42,7 @@
 #include "catalog/pg_type.h"
 #include "commands/async.h"
 #include "commands/prepare.h"
+#include "commands/event_trigger.h"
 #include "executor/spi.h"
 #include "jit/jit.h"
 #include "libpq/libpq.h"
@@ -182,6 +183,9 @@ static ProcSignalReason RecoveryConflictReason;
 static MemoryContext row_description_context = NULL;
 static StringInfoData row_description_buf;
 
+/* Hook for plugins to get control at start of session */
+client_connection_hook_type client_connection_hook = EventTriggerOnConnect;
+
 /* ----------------------------------------------------------------
  *		decls for routines only used in this file
  * ----------------------------------------------------------------
@@ -4165,6 +4169,11 @@ PostgresMain(int argc, char *argv[],
 	if (!IsUnderPostmaster)
 		PgStartTime = GetCurrentTimestamp();
 
+	if (client_connection_hook)
+	{
+		(*client_connection_hook) ();
+	}
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 460b720a65..d0093191b8 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "client_connection") == 0)
+			event = EVT_Connect;
 		else
 			continue;
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 480e8cd199..54201f01e2 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -47,6 +47,7 @@
 #include "commands/async.h"
 #include "commands/prepare.h"
 #include "commands/trigger.h"
+#include "commands/event_trigger.h"
 #include "commands/user.h"
 #include "commands/vacuum.h"
 #include "commands/variable.h"
@@ -968,6 +969,18 @@ static const unit_conversion time_unit_conversion_table[] =
 
 static struct config_bool ConfigureNamesBool[] =
 {
+	{
+		{"enable_client_connection_trigger", PGC_SU_BACKEND, DEVELOPER_OPTIONS,
+			gettext_noop("Enables the client_connection event trigger."),
+			gettext_noop("In case of errors in the ON client_connection EVENT TRIGGER procedure, "
+						 "this parameter can be used to disable trigger activation "
+						 "and provide access to the database."),
+			GUC_EXPLAIN
+		},
+		&enable_client_connection_trigger,
+		true,
+		NULL, NULL, NULL
+	},
 	{
 		{"enable_seqscan", PGC_USERSET, QUERY_TUNING_METHOD,
 			gettext_noop("Enables the planner's use of sequential-scan plans."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index b696abfe54..50669032d4 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -72,6 +72,8 @@
 					# (change requires restart)
 #bonjour_name = ''			# defaults to the computer name
 					# (change requires restart)
+#enable_client_connection_trigger = on	# enables firing the client_connection
+					# trigger when a client connects
 
 # - TCP settings -
 # see "man tcp" for details
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 321152151d..7955868e84 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -2820,6 +2820,7 @@ dumpDatabase(Archive *fout)
 				i_datacl,
 				i_rdatacl,
 				i_datistemplate,
+				i_dathaslogontriggers,
 				i_datconnlimit,
 				i_tablespace;
 	CatalogId	dbCatId;
@@ -2832,6 +2833,7 @@ dumpDatabase(Archive *fout)
 			   *datacl,
 			   *rdatacl,
 			   *datistemplate,
+			   *dathaslogontriggers,
 			   *datconnlimit,
 			   *tablespace;
 	uint32		frozenxid,
@@ -2850,7 +2852,41 @@ dumpDatabase(Archive *fout)
 	 * (pg_init_privs) are not supported on databases, so this logic cannot
 	 * make use of buildACLQueries().
 	 */
-	if (fout->remoteVersion >= 90600)
+	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBuffer(dbQry, "SELECT tableoid, oid, datname, "
+						  "(%s datdba) AS dba, "
+						  "pg_encoding_to_char(encoding) AS encoding, "
+						  "datcollate, datctype, datfrozenxid, datminmxid, "
+						  "(SELECT array_agg(acl ORDER BY row_n) FROM "
+						  "  (SELECT acl, row_n FROM "
+						  "     unnest(coalesce(datacl,acldefault('d',datdba))) "
+						  "     WITH ORDINALITY AS perm(acl,row_n) "
+						  "   WHERE NOT EXISTS ( "
+						  "     SELECT 1 "
+						  "     FROM unnest(acldefault('d',datdba)) "
+						  "       AS init(init_acl) "
+						  "     WHERE acl = init_acl)) AS datacls) "
+						  " AS datacl, "
+						  "(SELECT array_agg(acl ORDER BY row_n) FROM "
+						  "  (SELECT acl, row_n FROM "
+						  "     unnest(acldefault('d',datdba)) "
+						  "     WITH ORDINALITY AS initp(acl,row_n) "
+						  "   WHERE NOT EXISTS ( "
+						  "     SELECT 1 "
+						  "     FROM unnest(coalesce(datacl,acldefault('d',datdba))) "
+						  "       AS permp(orig_acl) "
+						  "     WHERE acl = orig_acl)) AS rdatacls) "
+						  " AS rdatacl, "
+						  "datistemplate, datconnlimit, dathaslogontriggers, "
+						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
+						  "shobj_description(oid, 'pg_database') AS description "
+
+						  "FROM pg_database "
+						  "WHERE datname = current_database()",
+						  username_subquery);
+	}
+	else if (fout->remoteVersion >= 90600)
 	{
 		appendPQExpBuffer(dbQry, "SELECT tableoid, oid, datname, "
 						  "(%s datdba) AS dba, "
@@ -2876,7 +2912,7 @@ dumpDatabase(Archive *fout)
 						  "       AS permp(orig_acl) "
 						  "     WHERE acl = orig_acl)) AS rdatacls) "
 						  " AS rdatacl, "
-						  "datistemplate, datconnlimit, "
+						  "datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2890,7 +2926,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "datcollate, datctype, datfrozenxid, datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogontriggers"
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2904,7 +2940,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "datcollate, datctype, datfrozenxid, 0 AS datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2918,7 +2954,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "NULL AS datcollate, NULL AS datctype, datfrozenxid, 0 AS datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2933,7 +2969,7 @@ dumpDatabase(Archive *fout)
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "NULL AS datcollate, NULL AS datctype, datfrozenxid, 0 AS datminmxid, "
 						  "datacl, '' as rdatacl, datistemplate, "
-						  "-1 as datconnlimit, "
+						  "-1 as datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace "
 						  "FROM pg_database "
 						  "WHERE datname = current_database()",
@@ -2955,6 +2991,7 @@ dumpDatabase(Archive *fout)
 	i_rdatacl = PQfnumber(res, "rdatacl");
 	i_datistemplate = PQfnumber(res, "datistemplate");
 	i_datconnlimit = PQfnumber(res, "datconnlimit");
+	i_dathaslogontriggers = PQfnumber(res, "dathaslogontriggers");
 	i_tablespace = PQfnumber(res, "tablespace");
 
 	dbCatId.tableoid = atooid(PQgetvalue(res, 0, i_tableoid));
@@ -2969,6 +3006,7 @@ dumpDatabase(Archive *fout)
 	datacl = PQgetvalue(res, 0, i_datacl);
 	rdatacl = PQgetvalue(res, 0, i_rdatacl);
 	datistemplate = PQgetvalue(res, 0, i_datistemplate);
+	dathaslogontriggers = PQgetvalue(res, 0, i_dathaslogontriggers);
 	datconnlimit = PQgetvalue(res, 0, i_datconnlimit);
 	tablespace = PQgetvalue(res, 0, i_tablespace);
 
@@ -3142,6 +3180,14 @@ dumpDatabase(Archive *fout)
 		appendPQExpBufferStr(delQry, ";\n");
 	}
 
+	if (strcmp(dathaslogontriggers, "t") == 0)
+	{
+		appendPQExpBufferStr(creaQry, "UPDATE pg_catalog.pg_database "
+							 "SET dathaslogontriggers = true WHERE datname = ");
+		appendStringLiteralAH(creaQry, datname, fout);
+		appendPQExpBufferStr(creaQry, ";\n");
+	}
+
 	/* Add database-specific SET options */
 	dumpDatabaseConfig(fout, creaQry, datname, dbCatId.oid);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 0ebd5aa41a..f967110f2c 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3014,7 +3014,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end",
+					  "client_connection", "sql_drop");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat
index b8aa1364a0..b89906e8ce 100644
--- a/src/include/catalog/pg_database.dat
+++ b/src/include/catalog/pg_database.dat
@@ -15,7 +15,7 @@
 { oid => '1', oid_symbol => 'TemplateDbOid',
   descr => 'default template for new databases',
   datname => 'template1', encoding => 'ENCODING', datcollate => 'LC_COLLATE',
-  datctype => 'LC_CTYPE', datistemplate => 't', datallowconn => 't',
+  datctype => 'LC_CTYPE', datistemplate => 't', dathaslogontriggers => 'f', datallowconn => 't',
   datconnlimit => '-1', datlastsysoid => '0', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datacl => '_null_' },
 
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index 43f3beb6a3..7b5b2aabb8 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -52,6 +52,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
 	/* new connections allowed? */
 	bool		datallowconn;
 
+	/* database has on-login triggers */
+	bool		dathaslogontriggers;
+
 	/* max connections allowed (-1=no limit) */
 	int32		datconnlimit;
 
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index c11bf2d781..e70d68b0f3 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -21,6 +21,8 @@
 #include "tcop/deparse_utility.h"
 #include "utils/aclchk_internal.h"
 
+extern bool enable_client_connection_trigger; /* GUC */
+
 typedef struct EventTriggerData
 {
 	NodeTag		type;
@@ -53,6 +55,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnConnect(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 9ba24d4ca9..0e64f90103 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -80,6 +80,7 @@ PG_CMDTAG(CMDTAG_CLUSTER, "CLUSTER", false, false, false)
 PG_CMDTAG(CMDTAG_COMMENT, "COMMENT", true, false, false)
 PG_CMDTAG(CMDTAG_COMMIT, "COMMIT", false, false, false)
 PG_CMDTAG(CMDTAG_COMMIT_PREPARED, "COMMIT PREPARED", false, false, false)
+PG_CMDTAG(CMDTAG_CONNECT, "CONNECT", true, false, false)
 PG_CMDTAG(CMDTAG_COPY, "COPY", false, false, true)
 PG_CMDTAG(CMDTAG_COPY_FROM, "COPY FROM", false, false, false)
 PG_CMDTAG(CMDTAG_CREATE_ACCESS_METHOD, "CREATE ACCESS METHOD", true, false, false)
diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h
index 968345404e..159fe5d939 100644
--- a/src/include/tcop/tcopprot.h
+++ b/src/include/tcop/tcopprot.h
@@ -31,6 +31,11 @@ extern int	max_stack_depth;
 extern int	PostAuthDelay;
 extern int	client_connection_check_interval;
 
+/* Hook for plugins to get control at start and end of session */
+typedef void (*client_connection_hook_type) (void);
+
+extern PGDLLIMPORT client_connection_hook_type client_connection_hook;
+
 /* GUC-configurable parameters */
 
 typedef enum
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index 58ddb71cb1..2440b408d1 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Connect,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index df6fdc20d1..510a82987e 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,26 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+
+CREATE TABLE connects(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO connects (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON client_connection EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 $node_primary->wait_for_catchup($node_standby_1, 'replay',
 	$node_primary->lsn('insert'));
@@ -339,6 +359,9 @@ sub replay_check
 
 replay_check();
 
+$node_standby_1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$node_standby_2->safe_psql('postgres', "SELECT 2", extra_params => [ '-U', 'regress_user', '-w' ]);
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 44d545de25..bd36efde00 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -604,3 +604,40 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- On session start triggers
+create table connects(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into connects (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on client_connection execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id | who  
+----+------
+  1 | I am
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id | who  
+----+------
+  1 | I am
+  2 | I am
+(2 rows)
+
+-- Test handing exeptions in client_connection trigger
+drop table connects;
+-- superuser should ignore error
+\c
+NOTICE:  client_connection trigger failed with message: relation "connects" does not exist
+-- suppress trigger firing
+\c "dbname=regression options='-c enable_client_connection_trigger=false'"
+-- Cleanup
+drop event trigger on_login_trigger;
+drop function on_login_proc();
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 0bb558d93c..b395bc7a88 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -93,29 +93,30 @@ select count(*) = 0 as ok from pg_stat_wal_receiver;
 -- This is to record the prevailing planner enable_foo settings during
 -- a regression test run.
 select name, setting from pg_settings where name like 'enable%';
-              name              | setting 
---------------------------------+---------
- enable_async_append            | on
- enable_bitmapscan              | on
- enable_gathermerge             | on
- enable_hashagg                 | on
- enable_hashjoin                | on
- enable_incremental_sort        | on
- enable_indexonlyscan           | on
- enable_indexscan               | on
- enable_material                | on
- enable_mergejoin               | on
- enable_nestloop                | on
- enable_parallel_append         | on
- enable_parallel_hash           | on
- enable_partition_pruning       | on
- enable_partitionwise_aggregate | off
- enable_partitionwise_join      | off
- enable_resultcache             | on
- enable_seqscan                 | on
- enable_sort                    | on
- enable_tidscan                 | on
-(20 rows)
+               name               | setting 
+----------------------------------+---------
+ enable_async_append              | on
+ enable_bitmapscan                | on
+ enable_client_connection_trigger | on
+ enable_gathermerge               | on
+ enable_hashagg                   | on
+ enable_hashjoin                  | on
+ enable_incremental_sort          | on
+ enable_indexonlyscan             | on
+ enable_indexscan                 | on
+ enable_material                  | on
+ enable_mergejoin                 | on
+ enable_nestloop                  | on
+ enable_parallel_append           | on
+ enable_parallel_hash             | on
+ enable_partition_pruning         | on
+ enable_partitionwise_aggregate   | off
+ enable_partitionwise_join        | off
+ enable_resultcache               | on
+ enable_seqscan                   | on
+ enable_sort                      | on
+ enable_tidscan                   | on
+(21 rows)
 
 -- Test that the pg_timezone_names and pg_timezone_abbrevs views are
 -- more-or-less working.  We can't test their contents in any great detail
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 1446cf8cc8..303ecf101a 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -462,3 +462,31 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- On session start triggers
+create table connects(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into connects (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on client_connection execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+select * from connects;
+\c
+select * from connects;
+
+-- Test handing exeptions in client_connection trigger
+
+drop table connects;
+-- superuser should ignore error
+\c
+-- suppress trigger firing
+\c "dbname=regression options='-c enable_client_connection_trigger=false'"
+
+
+-- Cleanup
+drop event trigger on_login_trigger;
+drop function on_login_proc();
-- 
2.27.0

#62Ibrar Ahmed
ibrar.ahmad@gmail.com
In reply to: Greg Nancarrow (#61)
Re: Re[3]: On login trigger: take three

On Wed, Jul 7, 2021 at 5:55 AM Greg Nancarrow <gregn4422@gmail.com> wrote:

On Sun, Jul 4, 2021 at 1:21 PM vignesh C <vignesh21@gmail.com> wrote:

CFBot shows the following failure:
# poll_query_until timed out executing this query:
# SELECT '0/3046250' <= replay_lsn AND state = 'streaming' FROM
pg_catalog.pg_stat_replication WHERE application_name = 'standby_1';
# expecting this output:
# t
# last actual query output:
# t
# with stderr:
# NOTICE: You are welcome!
# Looks like your test exited with 29 before it could output anything.
t/001_stream_rep.pl ..................
Dubious, test returned 29 (wstat 7424, 0x1d00)

Thanks.
I found that the patch was broken by commit f452aaf7d (the part
"adjust poll_query_until to insist on empty stderr as well as a stdout
match").
So I had to remove a "RAISE NOTICE" (which was just an informational
message) from the login trigger function, to satisfy the new
poll_query_until expectations.
Also, I updated a PG14 version check (now must check PG15 version).

Regards,
Greg Nancarrow
Fujitsu Australia

The patch does not apply, and rebase is required.

Hunk #1 FAILED at 93.
1 out of 1 hunk FAILED -- saving rejects to file
src/test/regress/expected/sysviews.out.rej

I am not changing the status because it is a minor change and the
patch is already "Ready for Committer".

--
Ibrar Ahmed

#63Greg Nancarrow
gregn4422@gmail.com
In reply to: Ibrar Ahmed (#62)
1 attachment(s)
Re: Re[3]: On login trigger: take three

On Mon, Jul 19, 2021 at 8:18 PM Ibrar Ahmed <ibrar.ahmad@gmail.com> wrote:

The patch does not apply, and rebase is required.

Attached a rebased patch (minor updates to the test code).

Regards,
Greg Nancarrow
Fujitsu Australia

Attachments:

v17-0001-on_client_connect_event_trigger.patchapplication/octet-stream; name=v17-0001-on_client_connect_event_trigger.patchDownload
From 62224860e45b99909ebaf7001428eacb8fd702e3 Mon Sep 17 00:00:00 2001
From: Greg Nancarrow <gregn4422@gmail.com>
Date: Mon, 19 Jul 2021 22:40:30 +1000
Subject: [PATCH v17] Add a new "client_connection" event and client connection
 trigger support.

The client_connection event occurs when a client connection to the server is
established. A new boolean GUC "enable_client_connection_trigger" is addded,
that enables firing of client_connection event triggers (switched on by
default). This parameter may be used (by superuser only) to disable
client_connection triggers, if any error in trigger code prevents a user from
logging in to the system. Errors in a client_connection trigger procedure are
ignored for superuser. An error message is delivered to the client as a NOTICE
in this case.

Author: Konstantin Knizhnik
Discussion: https://www.postgresql.org/message-id/flat/0d46d29f-4558-3af9-9c85-7774e14a7709%40postgrespro.ru
---
 doc/src/sgml/bki.sgml                         |   2 +-
 doc/src/sgml/catalogs.sgml                    |  11 ++
 doc/src/sgml/config.sgml                      |  19 ++
 doc/src/sgml/ecpg.sgml                        |   2 +
 doc/src/sgml/event-trigger.sgml               |  84 ++++++++-
 src/backend/commands/dbcommands.c             |   3 +-
 src/backend/commands/event_trigger.c          | 176 ++++++++++++++++--
 src/backend/tcop/postgres.c                   |   9 +
 src/backend/utils/cache/evtcache.c            |   2 +
 src/backend/utils/misc/guc.c                  |  13 ++
 src/backend/utils/misc/postgresql.conf.sample |   2 +
 src/bin/pg_dump/pg_dump.c                     |  58 +++++-
 src/bin/psql/tab-complete.c                   |   3 +-
 src/include/catalog/pg_database.dat           |   2 +-
 src/include/catalog/pg_database.h             |   3 +
 src/include/commands/event_trigger.h          |   3 +
 src/include/tcop/cmdtaglist.h                 |   1 +
 src/include/tcop/tcopprot.h                   |   5 +
 src/include/utils/evtcache.h                  |   3 +-
 src/test/recovery/t/001_stream_rep.pl         |  23 +++
 src/test/regress/expected/event_trigger.out   |  37 ++++
 src/test/regress/expected/sysviews.out        |  47 ++---
 src/test/regress/sql/event_trigger.sql        |  28 +++
 23 files changed, 483 insertions(+), 53 deletions(-)

diff --git a/doc/src/sgml/bki.sgml b/doc/src/sgml/bki.sgml
index f900a01b82..9387227f75 100644
--- a/doc/src/sgml/bki.sgml
+++ b/doc/src/sgml/bki.sgml
@@ -182,7 +182,7 @@
 { oid =&gt; '1', oid_symbol =&gt; 'TemplateDbOid',
   descr =&gt; 'database\'s default template',
   datname =&gt; 'template1', encoding =&gt; 'ENCODING', datcollate =&gt; 'LC_COLLATE',
-  datctype =&gt; 'LC_CTYPE', datistemplate =&gt; 't', datallowconn =&gt; 't',
+  datctype =&gt; 'LC_CTYPE', datistemplate =&gt; 't', datallowconn =&gt; 't', dathaslogontriggers =&gt; 'f',
   datconnlimit =&gt; '-1', datlastsysoid =&gt; '0', datfrozenxid =&gt; '0',
   datminmxid =&gt; '1', dattablespace =&gt; 'pg_default', datacl =&gt; '_null_' },
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 2b2c70a26e..e51e7239d1 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2972,6 +2972,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>dathaslogontriggers</structfield> <type>bool</type>
+      </para>
+      <para>
+        Indicates that there are client connection triggers defined for this database.
+        This flag is used to avoid extra lookups on the pg_event_trigger table during each backend startup.
+        This flag is used internally by Postgres and should not be manually changed by DBA or application.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>datconnlimit</structfield> <type>int4</type>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index b69ac974e3..a481d9eace 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1035,6 +1035,25 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-enable-client-connection-trigger" xreflabel="enable_client_connection_trigger">
+      <term><varname>enable_client_connection_trigger</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>enable_client_connection_trigger</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Enables firing the <literal>client_connection</literal>
+        trigger when a client connects. This parameter is switched on by
+        default. Errors in the trigger code can prevent the user from logging in
+        to the system. In this case, disabling this parameter in the connection
+        string can allow login so that the problem can then be resolved:
+        <literal>psql "dbname=postgres options='-c enable_client_connection_trigger=off'".</literal>
+        Only superuser can change this parameter.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
      </sect2>
 
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index 9d5505cb84..49fbf2566c 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -4731,6 +4731,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = t (type: 1)
 datallowconn = t (type: 1)
+dathaslogontriggers = f (type: 1)
 datconnlimit = -1 (type: 5)
 datlastsysoid = 11510 (type: 1)
 datfrozenxid = 379 (type: 1)
@@ -4756,6 +4757,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = f (type: 1)
 datallowconn = t (type: 1)
+dathaslogontriggers = f (type: 1)
 datconnlimit = -1 (type: 5)
 datlastsysoid = 11510 (type: 1)
 datfrozenxid = 379 (type: 1)
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 60366a950e..5263e2706d 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>client_connection</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,29 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>client_connection</literal> event occurs when a client connection
+     to the server is established.
+     There are two mechanisms for dealing with any bugs in a trigger procedure for
+     this event which might prevent successful login to the system:
+     <itemizedlist>
+      <listitem>
+       <para>
+         The configuration parameter <literal>enable_client_connection_trigger</literal>
+         makes it possible to disable firing the <literal>client_connection</literal>
+         trigger when a client connects.
+       </para>
+      </listitem>
+      <listitem>
+       <para>
+         Errors in the <literal>client_connection</literal> trigger procedure are
+         ignored for superuser. An error message is delivered to the client as a
+         <literal>NOTICE</literal> in this case.
+       </para>
+      </listitem>
+     </itemizedlist>
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1140,7 +1164,7 @@ typedef struct EventTriggerData
   </sect1>
 
   <sect1 id="event-trigger-example">
-   <title>A Complete Event Trigger Example</title>
+   <title>A C language Event Trigger Example</title>
 
    <para>
     Here is a very simple example of an event trigger function written in C.
@@ -1280,6 +1304,64 @@ $$;
 CREATE EVENT TRIGGER no_rewrite_allowed
                   ON table_rewrite
    EXECUTE FUNCTION no_rewrite();
+</programlisting>
+   </para>
+ </sect1>
+
+  <sect1 id="event-trigger-database-client-connection-example">
+   <title>A Database Client Connection Event Trigger Example</title>
+
+   <para>
+    The event trigger on the <literal>client_connection</literal> event can be
+    useful for logging client connections, for verifying the connection and
+    assigning roles according to current circumstances, or for some session data
+    initialization.
+   </para>
+
+   <para>
+    The following example demonstrates these options.
+<programlisting>
+
+-- create test tables and roles
+CREATE TABLE user_sessions_log (
+    "user" text,
+    "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+ RETURNS event_trigger SECURITY DEFINER
+ LANGUAGE plpgsql AS
+$$
+DECLARE
+    hour integer = EXTRACT('hour' FROM current_time);
+BEGIN
+-- 1) Assign some roles
+    IF hour BETWEEN 8 AND 20 THEN           -- at daytime grant the day_worker role
+        EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+        EXECUTE 'GRANT    day_worker TO '   || quote_ident(session_user);
+    ELSIF hour BETWEEN 2 AND 4 THEN
+        RAISE EXCEPTION 'Login forbidden';  -- do not allow to connect these hours
+    ELSE                                    -- at other time grant the night_worker role
+        EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+        EXECUTE 'GRANT  night_worker TO ' || quote_ident(session_user);
+    END IF;
+
+-- 2) Initialize some user session data
+   CREATE TEMP TABLE session_storage (x float, y integer);
+
+-- 3) Log the connection time
+   INSERT INTO user_sessions_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+   ON client_connection
+   EXECUTE FUNCTION init_session();
 </programlisting>
    </para>
  </sect1>
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 029fab48df..2cfcdee371 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -530,6 +530,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	new_record[Anum_pg_database_datctype - 1] =
 		DirectFunctionCall1(namein, CStringGetDatum(dbctype));
 	new_record[Anum_pg_database_datistemplate - 1] = BoolGetDatum(dbistemplate);
+	new_record[Anum_pg_database_dathaslogontriggers - 1] = BoolGetDatum(false);
 	new_record[Anum_pg_database_datallowconn - 1] = BoolGetDatum(dballowconnections);
 	new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 	new_record[Anum_pg_database_datlastsysoid - 1] = ObjectIdGetDatum(src_lastsysoid);
@@ -1585,7 +1586,7 @@ AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel)
 		new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 		new_record_repl[Anum_pg_database_datconnlimit - 1] = true;
 	}
-
+	new_record[Anum_pg_database_dathaslogontriggers - 1] = BoolGetDatum(datform->dathaslogontriggers);
 	newtuple = heap_modify_tuple(tuple, RelationGetDescr(rel), new_record,
 								 new_record_nulls, new_record_repl);
 	CatalogTupleUpdate(rel, &tuple->t_self, newtuple);
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 9c31c9e763..27bfb8339f 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -43,11 +44,15 @@
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
+bool enable_client_connection_trigger;
+
 typedef struct EventTriggerQueryState
 {
 	/* memory context for this state's objects */
@@ -130,6 +135,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "client_connection") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -293,6 +299,24 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
 	CatalogTupleInsert(tgrel, tuple);
 	heap_freetuple(tuple);
 
+	if (strcmp(eventname, "client_connection") == 0)
+	{
+		Form_pg_database db;
+		Relation pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+		/* Set dathaslogontriggers flag in pg_database */
+		tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+		db = (Form_pg_database) GETSTRUCT(tuple);
+		if (!db->dathaslogontriggers)
+		{
+			db->dathaslogontriggers = true;
+			CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+		}
+		table_close(pg_db, RowExclusiveLock);
+		heap_freetuple(tuple);
+	}
+
 	/* Depend on owner. */
 	recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
 
@@ -562,6 +586,9 @@ EventTriggerCommonSetup(Node *parsetree,
 	ListCell   *lc;
 	List	   *runlist = NIL;
 
+	/* Get the command tag. */
+	tag = parsetree ? CreateCommandTag(parsetree) : CMDTAG_CONNECT;
+
 	/*
 	 * We want the list of command tags for which this procedure is actually
 	 * invoked to match up exactly with the list that CREATE EVENT TRIGGER
@@ -577,22 +604,18 @@ EventTriggerCommonSetup(Node *parsetree,
 	 * relevant command tag.
 	 */
 #ifdef USE_ASSERT_CHECKING
+	if (event == EVT_DDLCommandStart ||
+		event == EVT_DDLCommandEnd ||
+		event == EVT_SQLDrop ||
+		event == EVT_Connect)
 	{
-		CommandTag	dbgtag;
-
-		dbgtag = CreateCommandTag(parsetree);
-		if (event == EVT_DDLCommandStart ||
-			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
-		{
-			if (!command_tag_event_trigger_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
-		else if (event == EVT_TableRewrite)
-		{
-			if (!command_tag_table_rewrite_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
+		if (!command_tag_event_trigger_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
+	}
+	else if (event == EVT_TableRewrite)
+	{
+		if (!command_tag_table_rewrite_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
 	}
 #endif
 
@@ -601,9 +624,6 @@ EventTriggerCommonSetup(Node *parsetree,
 	if (cachelist == NIL)
 		return NIL;
 
-	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
-
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
 	 * memory context.  Once we start running the command triggers, or indeed
@@ -800,6 +820,126 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+static bool
+DatabaseHasLogonTriggers(void)
+{
+	bool has_logon_triggers;
+	HeapTuple tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+	has_logon_triggers = ((Form_pg_database) GETSTRUCT(tuple))->dathaslogontriggers;
+	ReleaseSysCache(tuple);
+	return has_logon_triggers;
+}
+
+/*
+ * Fire connect triggers.
+ */
+void
+EventTriggerOnConnect(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster
+		|| !OidIsValid(MyDatabaseId)
+		|| !enable_client_connection_trigger)
+		return;
+
+	StartTransactionCommand();
+
+	if (DatabaseHasLogonTriggers())
+	{
+		runlist = EventTriggerCommonSetup(NULL,
+										  EVT_Connect, "connect",
+										  &trigdata);
+
+		if (runlist != NIL)
+		{
+			MemoryContext old_context = CurrentMemoryContext;
+			bool is_superuser = superuser();
+			/*
+			 * Make sure anything the main command did will be visible to the event
+			 * triggers.
+			 */
+			CommandCounterIncrement();
+
+			/*
+			 * Event trigger execution may require an active snapshot.
+			 */
+			PushActiveSnapshot(GetTransactionSnapshot());
+
+			/* Run the triggers. */
+			PG_TRY();
+			{
+				EventTriggerInvoke(runlist, &trigdata);
+				list_free(runlist);
+			}
+			PG_CATCH();
+			{
+				ErrorData* error;
+				/*
+				 * Try to ignore error for superuser to make it possible to login even in case of errors
+				 * during trigger execution
+				 */
+				if (!is_superuser)
+					PG_RE_THROW();
+
+				MemoryContextSwitchTo(old_context);
+				error = CopyErrorData();
+				FlushErrorState();
+				elog(NOTICE, "client_connection trigger failed with message: %s", error->message);
+				AbortCurrentTransaction();
+				return;
+			}
+			PG_END_TRY();
+
+			PopActiveSnapshot();
+		}
+		else
+		{
+			/* Runtlist is empty: clear dathaslogontriggers flag
+			 */
+			Relation pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+			HeapTuple tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+			Form_pg_database db;
+
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+			db = (Form_pg_database) GETSTRUCT(tuple);
+			if (db->dathaslogontriggers)
+			{
+				db->dathaslogontriggers = false;
+				CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				/*
+				 * There can be a race condition: the event trigger may be added
+				 * after we have scanned the pg_event_trigger table. Repeat this
+				 * test under the pg_database table lock.
+				 */
+				AcceptInvalidationMessages();
+				runlist = EventTriggerCommonSetup(NULL,
+												  EVT_Connect, "connect",
+												  &trigdata);
+				if (runlist != NULL) /* if list is not empty, then restore the flag */
+				{
+					db->dathaslogontriggers = true;
+					CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				}
+			}
+			table_close(pg_db, RowExclusiveLock);
+			heap_freetuple(tuple);
+		}
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 8cea10c901..e9f6da3501 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -42,6 +42,7 @@
 #include "catalog/pg_type.h"
 #include "commands/async.h"
 #include "commands/prepare.h"
+#include "commands/event_trigger.h"
 #include "executor/spi.h"
 #include "jit/jit.h"
 #include "libpq/libpq.h"
@@ -182,6 +183,9 @@ static ProcSignalReason RecoveryConflictReason;
 static MemoryContext row_description_context = NULL;
 static StringInfoData row_description_buf;
 
+/* Hook for plugins to get control at start of session */
+client_connection_hook_type client_connection_hook = EventTriggerOnConnect;
+
 /* ----------------------------------------------------------------
  *		decls for routines only used in this file
  * ----------------------------------------------------------------
@@ -4165,6 +4169,11 @@ PostgresMain(int argc, char *argv[],
 	if (!IsUnderPostmaster)
 		PgStartTime = GetCurrentTimestamp();
 
+	if (client_connection_hook)
+	{
+		(*client_connection_hook) ();
+	}
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 460b720a65..d0093191b8 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "client_connection") == 0)
+			event = EVT_Connect;
 		else
 			continue;
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index a2e0f8de7e..63cdbf5b24 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -47,6 +47,7 @@
 #include "commands/async.h"
 #include "commands/prepare.h"
 #include "commands/trigger.h"
+#include "commands/event_trigger.h"
 #include "commands/user.h"
 #include "commands/vacuum.h"
 #include "commands/variable.h"
@@ -968,6 +969,18 @@ static const unit_conversion time_unit_conversion_table[] =
 
 static struct config_bool ConfigureNamesBool[] =
 {
+	{
+		{"enable_client_connection_trigger", PGC_SU_BACKEND, DEVELOPER_OPTIONS,
+			gettext_noop("Enables the client_connection event trigger."),
+			gettext_noop("In case of errors in the ON client_connection EVENT TRIGGER procedure, "
+						 "this parameter can be used to disable trigger activation "
+						 "and provide access to the database."),
+			GUC_EXPLAIN
+		},
+		&enable_client_connection_trigger,
+		true,
+		NULL, NULL, NULL
+	},
 	{
 		{"enable_seqscan", PGC_USERSET, QUERY_TUNING_METHOD,
 			gettext_noop("Enables the planner's use of sequential-scan plans."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index ccaaf63850..5fc59a62f0 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -72,6 +72,8 @@
 					# (change requires restart)
 #bonjour_name = ''			# defaults to the computer name
 					# (change requires restart)
+#enable_client_connection_trigger = on	# enables firing the client_connection
+					# trigger when a client connects
 
 # - TCP settings -
 # see "man tcp" for details
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 34b91bb226..0050d28b70 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -2821,6 +2821,7 @@ dumpDatabase(Archive *fout)
 				i_datacl,
 				i_rdatacl,
 				i_datistemplate,
+				i_dathaslogontriggers,
 				i_datconnlimit,
 				i_tablespace;
 	CatalogId	dbCatId;
@@ -2833,6 +2834,7 @@ dumpDatabase(Archive *fout)
 			   *datacl,
 			   *rdatacl,
 			   *datistemplate,
+			   *dathaslogontriggers,
 			   *datconnlimit,
 			   *tablespace;
 	uint32		frozenxid,
@@ -2851,7 +2853,7 @@ dumpDatabase(Archive *fout)
 	 * (pg_init_privs) are not supported on databases, so this logic cannot
 	 * make use of buildACLQueries().
 	 */
-	if (fout->remoteVersion >= 90600)
+	if (fout->remoteVersion >= 150000)
 	{
 		appendPQExpBuffer(dbQry, "SELECT tableoid, oid, datname, "
 						  "(%s datdba) AS dba, "
@@ -2877,7 +2879,41 @@ dumpDatabase(Archive *fout)
 						  "       AS permp(orig_acl) "
 						  "     WHERE acl = orig_acl)) AS rdatacls) "
 						  " AS rdatacl, "
-						  "datistemplate, datconnlimit, "
+						  "datistemplate, datconnlimit, dathaslogontriggers, "
+						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
+						  "shobj_description(oid, 'pg_database') AS description "
+
+						  "FROM pg_database "
+						  "WHERE datname = current_database()",
+						  username_subquery);
+	}
+	else if (fout->remoteVersion >= 90600)
+	{
+		appendPQExpBuffer(dbQry, "SELECT tableoid, oid, datname, "
+						  "(%s datdba) AS dba, "
+						  "pg_encoding_to_char(encoding) AS encoding, "
+						  "datcollate, datctype, datfrozenxid, datminmxid, "
+						  "(SELECT array_agg(acl ORDER BY row_n) FROM "
+						  "  (SELECT acl, row_n FROM "
+						  "     unnest(coalesce(datacl,acldefault('d',datdba))) "
+						  "     WITH ORDINALITY AS perm(acl,row_n) "
+						  "   WHERE NOT EXISTS ( "
+						  "     SELECT 1 "
+						  "     FROM unnest(acldefault('d',datdba)) "
+						  "       AS init(init_acl) "
+						  "     WHERE acl = init_acl)) AS datacls) "
+						  " AS datacl, "
+						  "(SELECT array_agg(acl ORDER BY row_n) FROM "
+						  "  (SELECT acl, row_n FROM "
+						  "     unnest(acldefault('d',datdba)) "
+						  "     WITH ORDINALITY AS initp(acl,row_n) "
+						  "   WHERE NOT EXISTS ( "
+						  "     SELECT 1 "
+						  "     FROM unnest(coalesce(datacl,acldefault('d',datdba))) "
+						  "       AS permp(orig_acl) "
+						  "     WHERE acl = orig_acl)) AS rdatacls) "
+						  " AS rdatacl, "
+						  "datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2891,7 +2927,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "datcollate, datctype, datfrozenxid, datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogontriggers"
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2905,7 +2941,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "datcollate, datctype, datfrozenxid, 0 AS datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2919,7 +2955,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "NULL AS datcollate, NULL AS datctype, datfrozenxid, 0 AS datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2934,7 +2970,7 @@ dumpDatabase(Archive *fout)
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "NULL AS datcollate, NULL AS datctype, datfrozenxid, 0 AS datminmxid, "
 						  "datacl, '' as rdatacl, datistemplate, "
-						  "-1 as datconnlimit, "
+						  "-1 as datconnlimit, false as dathaslogontriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace "
 						  "FROM pg_database "
 						  "WHERE datname = current_database()",
@@ -2956,6 +2992,7 @@ dumpDatabase(Archive *fout)
 	i_rdatacl = PQfnumber(res, "rdatacl");
 	i_datistemplate = PQfnumber(res, "datistemplate");
 	i_datconnlimit = PQfnumber(res, "datconnlimit");
+	i_dathaslogontriggers = PQfnumber(res, "dathaslogontriggers");
 	i_tablespace = PQfnumber(res, "tablespace");
 
 	dbCatId.tableoid = atooid(PQgetvalue(res, 0, i_tableoid));
@@ -2970,6 +3007,7 @@ dumpDatabase(Archive *fout)
 	datacl = PQgetvalue(res, 0, i_datacl);
 	rdatacl = PQgetvalue(res, 0, i_rdatacl);
 	datistemplate = PQgetvalue(res, 0, i_datistemplate);
+	dathaslogontriggers = PQgetvalue(res, 0, i_dathaslogontriggers);
 	datconnlimit = PQgetvalue(res, 0, i_datconnlimit);
 	tablespace = PQgetvalue(res, 0, i_tablespace);
 
@@ -3143,6 +3181,14 @@ dumpDatabase(Archive *fout)
 		appendPQExpBufferStr(delQry, ";\n");
 	}
 
+	if (strcmp(dathaslogontriggers, "t") == 0)
+	{
+		appendPQExpBufferStr(creaQry, "UPDATE pg_catalog.pg_database "
+							 "SET dathaslogontriggers = true WHERE datname = ");
+		appendStringLiteralAH(creaQry, datname, fout);
+		appendPQExpBufferStr(creaQry, ";\n");
+	}
+
 	/* Add database-specific SET options */
 	dumpDatabaseConfig(fout, creaQry, datname, dbCatId.oid);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index d6bf725971..d9d93b7b59 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3014,7 +3014,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end",
+					  "client_connection", "sql_drop");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat
index b8aa1364a0..b89906e8ce 100644
--- a/src/include/catalog/pg_database.dat
+++ b/src/include/catalog/pg_database.dat
@@ -15,7 +15,7 @@
 { oid => '1', oid_symbol => 'TemplateDbOid',
   descr => 'default template for new databases',
   datname => 'template1', encoding => 'ENCODING', datcollate => 'LC_COLLATE',
-  datctype => 'LC_CTYPE', datistemplate => 't', datallowconn => 't',
+  datctype => 'LC_CTYPE', datistemplate => 't', dathaslogontriggers => 'f', datallowconn => 't',
   datconnlimit => '-1', datlastsysoid => '0', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datacl => '_null_' },
 
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index 43f3beb6a3..7b5b2aabb8 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -52,6 +52,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
 	/* new connections allowed? */
 	bool		datallowconn;
 
+	/* database has on-login triggers */
+	bool		dathaslogontriggers;
+
 	/* max connections allowed (-1=no limit) */
 	int32		datconnlimit;
 
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index c11bf2d781..e70d68b0f3 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -21,6 +21,8 @@
 #include "tcop/deparse_utility.h"
 #include "utils/aclchk_internal.h"
 
+extern bool enable_client_connection_trigger; /* GUC */
+
 typedef struct EventTriggerData
 {
 	NodeTag		type;
@@ -53,6 +55,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnConnect(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 9ba24d4ca9..0e64f90103 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -80,6 +80,7 @@ PG_CMDTAG(CMDTAG_CLUSTER, "CLUSTER", false, false, false)
 PG_CMDTAG(CMDTAG_COMMENT, "COMMENT", true, false, false)
 PG_CMDTAG(CMDTAG_COMMIT, "COMMIT", false, false, false)
 PG_CMDTAG(CMDTAG_COMMIT_PREPARED, "COMMIT PREPARED", false, false, false)
+PG_CMDTAG(CMDTAG_CONNECT, "CONNECT", true, false, false)
 PG_CMDTAG(CMDTAG_COPY, "COPY", false, false, true)
 PG_CMDTAG(CMDTAG_COPY_FROM, "COPY FROM", false, false, false)
 PG_CMDTAG(CMDTAG_CREATE_ACCESS_METHOD, "CREATE ACCESS METHOD", true, false, false)
diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h
index 968345404e..159fe5d939 100644
--- a/src/include/tcop/tcopprot.h
+++ b/src/include/tcop/tcopprot.h
@@ -31,6 +31,11 @@ extern int	max_stack_depth;
 extern int	PostAuthDelay;
 extern int	client_connection_check_interval;
 
+/* Hook for plugins to get control at start and end of session */
+typedef void (*client_connection_hook_type) (void);
+
+extern PGDLLIMPORT client_connection_hook_type client_connection_hook;
+
 /* GUC-configurable parameters */
 
 typedef enum
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index 58ddb71cb1..2440b408d1 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Connect,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index df6fdc20d1..510a82987e 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,26 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+
+CREATE TABLE connects(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO connects (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON client_connection EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 $node_primary->wait_for_catchup($node_standby_1, 'replay',
 	$node_primary->lsn('insert'));
@@ -339,6 +359,9 @@ sub replay_check
 
 replay_check();
 
+$node_standby_1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$node_standby_2->safe_psql('postgres', "SELECT 2", extra_params => [ '-U', 'regress_user', '-w' ]);
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 44d545de25..bd36efde00 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -604,3 +604,40 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- On session start triggers
+create table connects(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into connects (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on client_connection execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id | who  
+----+------
+  1 | I am
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+select * from connects;
+ id | who  
+----+------
+  1 | I am
+  2 | I am
+(2 rows)
+
+-- Test handing exeptions in client_connection trigger
+drop table connects;
+-- superuser should ignore error
+\c
+NOTICE:  client_connection trigger failed with message: relation "connects" does not exist
+-- suppress trigger firing
+\c "dbname=regression options='-c enable_client_connection_trigger=false'"
+-- Cleanup
+drop event trigger on_login_trigger;
+drop function on_login_proc();
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 6e54f3e15e..93b8698cb8 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -93,29 +93,30 @@ select count(*) = 0 as ok from pg_stat_wal_receiver;
 -- This is to record the prevailing planner enable_foo settings during
 -- a regression test run.
 select name, setting from pg_settings where name like 'enable%';
-              name              | setting 
---------------------------------+---------
- enable_async_append            | on
- enable_bitmapscan              | on
- enable_gathermerge             | on
- enable_hashagg                 | on
- enable_hashjoin                | on
- enable_incremental_sort        | on
- enable_indexonlyscan           | on
- enable_indexscan               | on
- enable_material                | on
- enable_memoize                 | on
- enable_mergejoin               | on
- enable_nestloop                | on
- enable_parallel_append         | on
- enable_parallel_hash           | on
- enable_partition_pruning       | on
- enable_partitionwise_aggregate | off
- enable_partitionwise_join      | off
- enable_seqscan                 | on
- enable_sort                    | on
- enable_tidscan                 | on
-(20 rows)
+               name               | setting 
+----------------------------------+---------
+ enable_async_append              | on
+ enable_bitmapscan                | on
+ enable_client_connection_trigger | on
+ enable_gathermerge               | on
+ enable_hashagg                   | on
+ enable_hashjoin                  | on
+ enable_incremental_sort          | on
+ enable_indexonlyscan             | on
+ enable_indexscan                 | on
+ enable_material                  | on
+ enable_memoize                   | on
+ enable_mergejoin                 | on
+ enable_nestloop                  | on
+ enable_parallel_append           | on
+ enable_parallel_hash             | on
+ enable_partition_pruning         | on
+ enable_partitionwise_aggregate   | off
+ enable_partitionwise_join        | off
+ enable_seqscan                   | on
+ enable_sort                      | on
+ enable_tidscan                   | on
+(21 rows)
 
 -- Test that the pg_timezone_names and pg_timezone_abbrevs views are
 -- more-or-less working.  We can't test their contents in any great detail
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 1446cf8cc8..303ecf101a 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -462,3 +462,31 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- On session start triggers
+create table connects(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into connects (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on client_connection execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+select * from connects;
+\c
+select * from connects;
+
+-- Test handing exeptions in client_connection trigger
+
+drop table connects;
+-- superuser should ignore error
+\c
+-- suppress trigger firing
+\c "dbname=regression options='-c enable_client_connection_trigger=false'"
+
+
+-- Cleanup
+drop event trigger on_login_trigger;
+drop function on_login_proc();
-- 
2.27.0

In reply to: Greg Nancarrow (#61)
Re[5]: On login trigger: take three

 
Hi Greg,
 

Среда, 7 июля 2021, 3:55 +03:00 от Greg Nancarrow <gregn4422@gmail.com>:
 
On Sun, Jul 4, 2021 at 1:21 PM vignesh C < vignesh21@gmail.com > wrote:

CFBot shows the following failure:
# poll_query_until timed out executing this query:
# SELECT '0/3046250' <= replay_lsn AND state = 'streaming' FROM
pg_catalog.pg_stat_replication WHERE application_name = 'standby_1';
# expecting this output:
# t
# last actual query output:
# t
# with stderr:
# NOTICE: You are welcome!
# Looks like your test exited with 29 before it could output anything.
t/001_stream_rep.pl ..................
Dubious, test returned 29 (wstat 7424, 0x1d00)

Thanks.
I found that the patch was broken by commit f452aaf7d (the part
"adjust poll_query_until to insist on empty stderr as well as a stdout
match").
So I had to remove a "RAISE NOTICE" (which was just an informational
message) from the login trigger function, to satisfy the new
poll_query_until expectations.

Does it mean that "RAISE NOTICE" should’t be used, or behaves unexpectedly in logon triggers? Should we mention this in the docs?
 
Regards,
Ivan Panchenko

Also, I updated a PG14 version check (now must check PG15 version).

Regards,
Greg Nancarrow
Fujitsu Australia

 

#65Greg Nancarrow
gregn4422@gmail.com
In reply to: Ivan Panchenko (#64)
Re: Re[5]: On login trigger: take three

On Tue, Aug 17, 2021 at 1:11 AM Ivan Panchenko <wao@mail.ru> wrote:

Does it mean that "RAISE NOTICE" should’t be used, or behaves unexpectedly in logon triggers? Should we mention this in the docs?

No I don't believe so, it was just that that part of the test
framework (sub poll_query_until) had been changed to regard anything
output to stderr as an error (so now for the test to succeed, whatever
is printed to stdout must match the expected test output, and stderr
must be empty).

Regards,
Greg Nancarrow
Fujitsu Australia

#66Daniel Gustafsson
daniel@yesql.se
In reply to: Greg Nancarrow (#63)
Re: On login trigger: take three

On 19 Jul 2021, at 15:25, Greg Nancarrow <gregn4422@gmail.com> wrote:

Attached a rebased patch (minor updates to the test code).

I took a look at this, and while I like the proposed feature I think the patch
has a bit more work required.

+    END IF;
+
+-- 2) Initialize some user session data
+   CREATE TEMP TABLE session_storage (x float, y integer);
The example in the documentation use a mix of indentations, neither of which is
the 2-space indentation used elsewhere in the docs.
+	/* Get the command tag. */
+	tag = parsetree ? CreateCommandTag(parsetree) : CMDTAG_CONNECT;
This is hardcoding knowledge that currently only this trigger wont have a
parsetree, and setting the tag accordingly.  This should instead check the
event and set CMDTAG_UNKNOWN if it isn't the expected one.

+ /* database has on-login triggers */
+ bool dathaslogontriggers;
This patch uses three different names for the same thing: client connection,
logontrigger and login trigger. Since this trigger only fires after successful
authentication it’s not accurate to name it client connection, as that implies
it running on connections rather than logins. The nomenclature used in the
tree is "login" so the patch should be adjusted everywhere to use that.

+/* Hook for plugins to get control at start of session */
+client_connection_hook_type client_connection_hook = EventTriggerOnConnect;
I don't see the reason for adding core functionality by hooks.  Having a hook
might be a useful thing on its own (to be discussed in a new thread, not hidden
here), but core functionality should not depend on it IMO.
+	{"enable_client_connection_trigger", PGC_SU_BACKEND, DEVELOPER_OPTIONS,
+		gettext_noop("Enables the client_connection event trigger."),
+		gettext_noop("In case of errors in the ON client_connection EVENT TRIGGER procedure, "
 ..and..
+	/*
+	 * Try to ignore error for superuser to make it possible to login even in case of errors
+	 * during trigger execution
+	 */
+	if (!is_superuser)
+		PG_RE_THROW();
This patch adds two ways for superusers to bypass this event trigger in case of
it being faulty, but for every other event trigger we've documented to restart
in single-user mode and fixing it there.  Why does this need to be different?
This clearly has a bigger chance of being a footgun but I don't see that as a
reason to add a GUC and a bypass that other footguns lack.

+ elog(NOTICE, "client_connection trigger failed with message: %s", error->message);
Calling elog() in a PG_CATCH() block isn't allowed is it?

+	/* Runtlist is empty: clear dathaslogontriggers flag
+	 */
s/Runtlist/Runlist/ and also commenting style.

The logic for resetting the pg_database flag when firing event trigger in case
the trigger has gone away is messy and a problem waiting to happen IMO. For
normal triggers we don't bother with that on the grounds of it being racy, and
instead perform relhastrigger removal during VACUUM. Another approach is using
a counter as propose upthread, since updating that can be made safer. The
current coding also isn't instructing concurrent backends to perform relcache
rebuild.

--
Daniel Gustafsson https://vmware.com/

#67Pavel Stehule
pavel.stehule@gmail.com
In reply to: Daniel Gustafsson (#66)
Re: On login trigger: take three

Hi

+       {"enable_client_connection_trigger", PGC_SU_BACKEND,
DEVELOPER_OPTIONS,
+               gettext_noop("Enables the client_connection event
trigger."),
+               gettext_noop("In case of errors in the ON
client_connection EVENT TRIGGER procedure, "
..and..
+       /*
+        * Try to ignore error for superuser to make it possible to login
even in case of errors
+        * during trigger execution
+        */
+       if (!is_superuser)
+               PG_RE_THROW();
This patch adds two ways for superusers to bypass this event trigger in
case of
it being faulty, but for every other event trigger we've documented to
restart
in single-user mode and fixing it there.  Why does this need to be
different?
This clearly has a bigger chance of being a footgun but I don't see that
as a
reason to add a GUC and a bypass that other footguns lack.

In the time when event triggers were introduced, managed services were not
too widely used like now. When we discussed this feature we thought about
environments when users have no superuser rights and have no possibility to
go to single mode.

Personally, I prefer to introduce some bypassing for event triggers instead
of removing bypass from login triggers.

Regards

Pavel

#68Daniel Gustafsson
daniel@yesql.se
In reply to: Pavel Stehule (#67)
Re: On login trigger: take three

On 8 Sep 2021, at 16:02, Pavel Stehule <pavel.stehule@gmail.com> wrote:

In the time when event triggers were introduced, managed services were not too widely used like now. When we discussed this feature we thought about environments when users have no superuser rights and have no possibility to go to single mode.

In situations where you don't have superuser access and cannot restart in
single user mode, none of the bypasses in this patch would help anyways.

I understand the motivation, but continuing on even in the face of an
ereport(ERROR.. ) in the hopes of being able to turn off buggy code seems
pretty unsafe at best.

--
Daniel Gustafsson https://vmware.com/

#69Pavel Stehule
pavel.stehule@gmail.com
In reply to: Daniel Gustafsson (#68)
Re: On login trigger: take three

st 8. 9. 2021 v 20:23 odesílatel Daniel Gustafsson <daniel@yesql.se> napsal:

On 8 Sep 2021, at 16:02, Pavel Stehule <pavel.stehule@gmail.com> wrote:

In the time when event triggers were introduced, managed services were

not too widely used like now. When we discussed this feature we thought
about environments when users have no superuser rights and have no
possibility to go to single mode.

In situations where you don't have superuser access and cannot restart in
single user mode, none of the bypasses in this patch would help anyways.

If I remember well, it should be possible - you can set GUC in connection
string, and this GUC is limited to the database owner.

I understand the motivation, but continuing on even in the face of an
ereport(ERROR.. ) in the hopes of being able to turn off buggy code seems
pretty unsafe at best.

I don't understand what you mean. I can disable the logon trigger by GUC. I
cannot to enable an continue after an exception.

Regards

Pavel

Show quoted text

--
Daniel Gustafsson https://vmware.com/

#70Greg Nancarrow
gregn4422@gmail.com
In reply to: Daniel Gustafsson (#66)
1 attachment(s)
Re: On login trigger: take three

On Wed, Sep 8, 2021 at 10:56 PM Daniel Gustafsson <daniel@yesql.se> wrote:

I took a look at this, and while I like the proposed feature I think the patch
has a bit more work required.

Thanks for reviewing the patch.
I am not the original patch author (who no longer seems active) but
I've been contributing a bit and keeping the patch alive because I
think it's a worthwhile feature.

+
+-- 2) Initialize some user session data
+   CREATE TEMP TABLE session_storage (x float, y integer);
The example in the documentation use a mix of indentations, neither of which is
the 2-space indentation used elsewhere in the docs.

Fixed, using 2-space indentation.
(to be honest, the indentation seems inconsistent elsewhere in the
docs e.g. I'm seeing a nearby case of 2-space on 1st indent, 6-space
on 2nd indent)

+       /* Get the command tag. */
+       tag = parsetree ? CreateCommandTag(parsetree) : CMDTAG_CONNECT;
This is hardcoding knowledge that currently only this trigger wont have a
parsetree, and setting the tag accordingly.  This should instead check the
event and set CMDTAG_UNKNOWN if it isn't the expected one.

I updated that, but maybe not exactly how you expected?

+       /* database has on-login triggers */
+       bool            dathaslogontriggers;
This patch uses three different names for the same thing: client connection,
logontrigger and login trigger.  Since this trigger only fires after successful
authentication it’s not accurate to name it client connection, as that implies
it running on connections rather than logins.  The nomenclature used in the
tree is "login" so the patch should be adjusted everywhere to use that.

Fixed.

+/* Hook for plugins to get control at start of session */
+client_connection_hook_type client_connection_hook = EventTriggerOnConnect;
I don't see the reason for adding core functionality by hooks.  Having a hook
might be a useful thing on its own (to be discussed in a new thread, not hidden
here), but core functionality should not depend on it IMO.

Fair enough, I removed the hook.

+       {"enable_client_connection_trigger", PGC_SU_BACKEND, DEVELOPER_OPTIONS,
+               gettext_noop("Enables the client_connection event trigger."),
+               gettext_noop("In case of errors in the ON client_connection EVENT TRIGGER procedure, "
..and..
+       /*
+        * Try to ignore error for superuser to make it possible to login even in case of errors
+        * during trigger execution
+        */
+       if (!is_superuser)
+               PG_RE_THROW();
This patch adds two ways for superusers to bypass this event trigger in case of
it being faulty, but for every other event trigger we've documented to restart
in single-user mode and fixing it there.  Why does this need to be different?
This clearly has a bigger chance of being a footgun but I don't see that as a
reason to add a GUC and a bypass that other footguns lack.

OK, I removed those bypasses. We'll see what others think.

+ elog(NOTICE, "client_connection trigger failed with message: %s", error->message);
Calling elog() in a PG_CATCH() block isn't allowed is it?

I believe it is allowed (e.g. there's a case in libpq), but I removed
this anyway as part of the superuser bypass.

+       /* Runtlist is empty: clear dathaslogontriggers flag
+        */
s/Runtlist/Runlist/ and also commenting style.

Fixed.

The logic for resetting the pg_database flag when firing event trigger in case
the trigger has gone away is messy and a problem waiting to happen IMO. For
normal triggers we don't bother with that on the grounds of it being racy, and
instead perform relhastrigger removal during VACUUM. Another approach is using
a counter as propose upthread, since updating that can be made safer. The
current coding also isn't instructing concurrent backends to perform relcache
rebuild.

I think there are pros and cons of each possible approach, but I think
I prefer the current way (which I have tweaked a bit) for similar
reasons to those explained by the original patch author when debating
whether to use reference-counting instead, in the discussion upthread
(e.g. it keeps it all in one place). Also, it seems to be more inline
with the documented reason why that pg_database flag was added in the
first place. I have debugged a few concurrent scenarios with the
current mechanism in place. If you still dislike the logic for
resetting the flag, please elaborate on the issues you foresee and one
of the alternative approaches can be tried.

I've attached the updated patch.

Regards,
Greg Nancarrow
Fujitsu Australia

Attachments:

v18-0001-Add-a-new-login-event-and-login-trigger-support.patchapplication/octet-stream; name=v18-0001-Add-a-new-login-event-and-login-trigger-support.patchDownload
From 19a4f4c41d439fdcd8a0a65f953882373e7f7af0 Mon Sep 17 00:00:00 2001
From: Greg Nancarrow <gregn4422@gmail.com>
Date: Wed, 15 Sep 2021 22:38:23 +1000
Subject: [PATCH v18] Add a new "login" event and login trigger support.

The login event occurs when a user logs in to the system.

Author: Konstantin Knizhnik
Discussion: https://www.postgresql.org/message-id/flat/0d46d29f-4558-3af9-9c85-7774e14a7709%40postgrespro.ru
---
 doc/src/sgml/bki.sgml                       |   2 +-
 doc/src/sgml/catalogs.sgml                  |  11 ++
 doc/src/sgml/ecpg.sgml                      |   2 +
 doc/src/sgml/event-trigger.sgml             |  70 ++++++++-
 src/backend/commands/dbcommands.c           |   3 +-
 src/backend/commands/event_trigger.c        | 159 +++++++++++++++++---
 src/backend/tcop/postgres.c                 |   3 +
 src/backend/utils/cache/evtcache.c          |   2 +
 src/bin/pg_dump/pg_dump.c                   |  58 ++++++-
 src/bin/psql/tab-complete.c                 |   3 +-
 src/include/catalog/pg_database.dat         |   2 +-
 src/include/catalog/pg_database.h           |   3 +
 src/include/commands/event_trigger.h        |   1 +
 src/include/tcop/cmdtaglist.h               |   1 +
 src/include/utils/evtcache.h                |   3 +-
 src/test/recovery/t/001_stream_rep.pl       |  23 +++
 src/test/regress/expected/event_trigger.out |  31 ++++
 src/test/regress/sql/event_trigger.sql      |  20 +++
 18 files changed, 367 insertions(+), 30 deletions(-)

diff --git a/doc/src/sgml/bki.sgml b/doc/src/sgml/bki.sgml
index f900a01b82..c312165f71 100644
--- a/doc/src/sgml/bki.sgml
+++ b/doc/src/sgml/bki.sgml
@@ -182,7 +182,7 @@
 { oid =&gt; '1', oid_symbol =&gt; 'TemplateDbOid',
   descr =&gt; 'database\'s default template',
   datname =&gt; 'template1', encoding =&gt; 'ENCODING', datcollate =&gt; 'LC_COLLATE',
-  datctype =&gt; 'LC_CTYPE', datistemplate =&gt; 't', datallowconn =&gt; 't',
+  datctype =&gt; 'LC_CTYPE', datistemplate =&gt; 't', datallowconn =&gt; 't', dathaslogintriggers =&gt; 'f',
   datconnlimit =&gt; '-1', datlastsysoid =&gt; '0', datfrozenxid =&gt; '0',
   datminmxid =&gt; '1', dattablespace =&gt; 'pg_default', datacl =&gt; '_null_' },
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 2f0def9b19..c35a70ce50 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2972,6 +2972,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>dathaslogintriggers</structfield> <type>bool</type>
+      </para>
+      <para>
+        Indicates that there are login triggers defined for this database.
+        This flag is used to avoid extra lookups on the pg_event_trigger table during each backend startup.
+        This flag is used internally by Postgres and should not be manually changed by DBA or application.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>datconnlimit</structfield> <type>int4</type>
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index 04d4a6bdb2..565f88d298 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -4731,6 +4731,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = t (type: 1)
 datallowconn = t (type: 1)
+dathaslogintriggers = f (type: 1)
 datconnlimit = -1 (type: 5)
 datlastsysoid = 11510 (type: 1)
 datfrozenxid = 379 (type: 1)
@@ -4756,6 +4757,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = f (type: 1)
 datallowconn = t (type: 1)
+dathaslogintriggers = f (type: 1)
 datconnlimit = -1 (type: 5)
 datlastsysoid = 11510 (type: 1)
 datfrozenxid = 379 (type: 1)
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 60366a950e..ec35ddfad8 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>login</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,16 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>login</literal> event occurs when a user logs in to the
+     system.
+     Any bugs in a trigger procedure for this event may prevent successful
+     login to the system. Such bugs may be fixed after first restarting the
+     system in single-user mode (as event triggers are disabled in this mode).
+     See the <xref linkend="app-postgres"/> reference page for details about
+     using single-user mode.
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1140,7 +1151,7 @@ typedef struct EventTriggerData
   </sect1>
 
   <sect1 id="event-trigger-example">
-   <title>A Complete Event Trigger Example</title>
+   <title>A C language Event Trigger Example</title>
 
    <para>
     Here is a very simple example of an event trigger function written in C.
@@ -1280,6 +1291,63 @@ $$;
 CREATE EVENT TRIGGER no_rewrite_allowed
                   ON table_rewrite
    EXECUTE FUNCTION no_rewrite();
+</programlisting>
+   </para>
+ </sect1>
+
+  <sect1 id="event-trigger-database-login-example">
+   <title>A Database Login Event Trigger Example</title>
+
+   <para>
+    The event trigger on the <literal>login</literal> event can be
+    useful for logging user logins, for verifying the connection and
+    assigning roles according to current circumstances, or for some session data
+    initialization.
+   </para>
+
+   <para>
+    The following example demonstrates these options.
+<programlisting>
+-- create test tables and roles
+CREATE TABLE user_login_log (
+  "user" text,
+  "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+  RETURNS event_trigger SECURITY DEFINER
+  LANGUAGE plpgsql AS
+$$
+DECLARE
+  hour integer = EXTRACT('hour' FROM current_time);
+BEGIN
+-- 1) Assign some roles
+IF hour BETWEEN 8 AND 20 THEN         -- at daytime grant the day_worker role
+  EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT    day_worker TO '   || quote_ident(session_user);
+ELSIF hour BETWEEN 2 AND 4 THEN
+  RAISE EXCEPTION 'Login forbidden';  -- do not allow to login these hours
+ELSE                                  -- at other time grant the night_worker role
+  EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT  night_worker TO ' || quote_ident(session_user);
+END IF;
+
+-- 2) Initialize some user session data
+CREATE TEMP TABLE session_storage (x float, y integer);
+
+-- 3) Log the connection time
+INSERT INTO user_login_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+  ON login
+  EXECUTE FUNCTION init_session();
 </programlisting>
    </para>
  </sect1>
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 029fab48df..a658d476ca 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -530,6 +530,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	new_record[Anum_pg_database_datctype - 1] =
 		DirectFunctionCall1(namein, CStringGetDatum(dbctype));
 	new_record[Anum_pg_database_datistemplate - 1] = BoolGetDatum(dbistemplate);
+	new_record[Anum_pg_database_dathaslogintriggers - 1] = BoolGetDatum(false);
 	new_record[Anum_pg_database_datallowconn - 1] = BoolGetDatum(dballowconnections);
 	new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 	new_record[Anum_pg_database_datlastsysoid - 1] = ObjectIdGetDatum(src_lastsysoid);
@@ -1585,7 +1586,7 @@ AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel)
 		new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 		new_record_repl[Anum_pg_database_datconnlimit - 1] = true;
 	}
-
+	new_record[Anum_pg_database_dathaslogintriggers - 1] = BoolGetDatum(datform->dathaslogintriggers);
 	newtuple = heap_modify_tuple(tuple, RelationGetDescr(rel), new_record,
 								 new_record_nulls, new_record_repl);
 	CatalogTupleUpdate(rel, &tuple->t_self, newtuple);
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 71612d577e..539ba2e8ad 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -43,9 +44,11 @@
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 typedef struct EventTriggerQueryState
@@ -130,6 +133,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "login") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -293,6 +297,26 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
 	CatalogTupleInsert(tgrel, tuple);
 	heap_freetuple(tuple);
 
+	if (strcmp(eventname, "login") == 0)
+	{
+		Form_pg_database db;
+		Relation pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+		/* Set dathaslogintriggers flag in pg_database */
+		tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+		db = (Form_pg_database) GETSTRUCT(tuple);
+		if (!db->dathaslogintriggers)
+		{
+			db->dathaslogintriggers = true;
+			CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+		}
+		else
+			CacheInvalidateRelcacheByTuple(tuple);
+		table_close(pg_db, RowExclusiveLock);
+		heap_freetuple(tuple);
+	}
+
 	/* Depend on owner. */
 	recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
 
@@ -562,6 +586,9 @@ EventTriggerCommonSetup(Node *parsetree,
 	ListCell   *lc;
 	List	   *runlist = NIL;
 
+	/* Get the command tag. */
+	tag = (event == EVT_Login) ? CMDTAG_LOGIN : CreateCommandTag(parsetree);
+
 	/*
 	 * We want the list of command tags for which this procedure is actually
 	 * invoked to match up exactly with the list that CREATE EVENT TRIGGER
@@ -577,22 +604,18 @@ EventTriggerCommonSetup(Node *parsetree,
 	 * relevant command tag.
 	 */
 #ifdef USE_ASSERT_CHECKING
+	if (event == EVT_DDLCommandStart ||
+		event == EVT_DDLCommandEnd ||
+		event == EVT_SQLDrop ||
+		event == EVT_Login)
 	{
-		CommandTag	dbgtag;
-
-		dbgtag = CreateCommandTag(parsetree);
-		if (event == EVT_DDLCommandStart ||
-			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
-		{
-			if (!command_tag_event_trigger_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
-		else if (event == EVT_TableRewrite)
-		{
-			if (!command_tag_table_rewrite_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
+		if (!command_tag_event_trigger_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
+	}
+	else if (event == EVT_TableRewrite)
+	{
+		if (!command_tag_table_rewrite_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
 	}
 #endif
 
@@ -601,9 +624,6 @@ EventTriggerCommonSetup(Node *parsetree,
 	if (cachelist == NIL)
 		return NIL;
 
-	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
-
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
 	 * memory context.  Once we start running the command triggers, or indeed
@@ -800,6 +820,109 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Return true if this database has login triggers, false otherwise.
+ */
+static bool
+DatabaseHasLoginTriggers(void)
+{
+	bool has_login_triggers;
+	HeapTuple tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+	has_login_triggers = ((Form_pg_database) GETSTRUCT(tuple))->dathaslogintriggers;
+	ReleaseSysCache(tuple);
+	return has_login_triggers;
+}
+
+/*
+ * Fire login triggers.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId))
+		return;
+
+	StartTransactionCommand();
+
+	if (DatabaseHasLoginTriggers())
+	{
+		runlist = EventTriggerCommonSetup(NULL,
+										  EVT_Login, "login",
+										  &trigdata);
+
+		if (runlist != NIL)
+		{
+			/*
+			 * Make sure anything the main command did will be visible to the event
+			 * triggers.
+			 */
+			CommandCounterIncrement();
+
+			/*
+			 * Event trigger execution may require an active snapshot.
+			 */
+			PushActiveSnapshot(GetTransactionSnapshot());
+
+			/* Run the triggers. */
+			EventTriggerInvoke(runlist, &trigdata);
+
+			/* Cleanup. */
+			list_free(runlist);
+
+			PopActiveSnapshot();
+		}
+		else
+		{
+			/*
+			 * Runlist is empty: clear dathaslogintriggers flag
+			 */
+			Relation pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+			HeapTuple tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+			Form_pg_database db;
+
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+			db = (Form_pg_database) GETSTRUCT(tuple);
+			if (db->dathaslogintriggers)
+			{
+				/*
+				 * There can be a race condition: a login event trigger may have
+				 * been added after the pg_event_trigger table was scanned, and
+				 * we don't want to erroneously clear the dathaslogintriggers
+				 * flag in this case. To be sure that this hasn't happened,
+				 * repeat the scan under the pg_database table lock.
+				 */
+				AcceptInvalidationMessages();
+				runlist = EventTriggerCommonSetup(NULL,
+												  EVT_Login, "login",
+												  &trigdata);
+				if (runlist == NULL) /* list is still empty, so clear the flag */
+				{
+					db->dathaslogintriggers = false;
+					CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				}
+				else
+					CacheInvalidateRelcacheByTuple(tuple);
+			}
+			table_close(pg_db, RowExclusiveLock);
+			heap_freetuple(tuple);
+		}
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 3f9ed549f9..657e200541 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -42,6 +42,7 @@
 #include "catalog/pg_type.h"
 #include "commands/async.h"
 #include "commands/prepare.h"
+#include "commands/event_trigger.h"
 #include "executor/spi.h"
 #include "jit/jit.h"
 #include "libpq/libpq.h"
@@ -4167,6 +4168,8 @@ PostgresMain(int argc, char *argv[],
 	if (!IsUnderPostmaster)
 		PgStartTime = GetCurrentTimestamp();
 
+	EventTriggerOnLogin();
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 460b720a65..b9b935f1cc 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "login") == 0)
+			event = EVT_Login;
 		else
 			continue;
 
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a485fb2d07..4e83dec4b1 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -2797,6 +2797,7 @@ dumpDatabase(Archive *fout)
 				i_datacl,
 				i_rdatacl,
 				i_datistemplate,
+				i_dathaslogintriggers,
 				i_datconnlimit,
 				i_tablespace;
 	CatalogId	dbCatId;
@@ -2809,6 +2810,7 @@ dumpDatabase(Archive *fout)
 			   *datacl,
 			   *rdatacl,
 			   *datistemplate,
+			   *dathaslogintriggers,
 			   *datconnlimit,
 			   *tablespace;
 	uint32		frozenxid,
@@ -2827,7 +2829,7 @@ dumpDatabase(Archive *fout)
 	 * (pg_init_privs) are not supported on databases, so this logic cannot
 	 * make use of buildACLQueries().
 	 */
-	if (fout->remoteVersion >= 90600)
+	if (fout->remoteVersion >= 150000)
 	{
 		appendPQExpBuffer(dbQry, "SELECT tableoid, oid, datname, "
 						  "(%s datdba) AS dba, "
@@ -2853,7 +2855,41 @@ dumpDatabase(Archive *fout)
 						  "       AS permp(orig_acl) "
 						  "     WHERE acl = orig_acl)) AS rdatacls) "
 						  " AS rdatacl, "
-						  "datistemplate, datconnlimit, "
+						  "datistemplate, datconnlimit, dathaslogintriggers, "
+						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
+						  "shobj_description(oid, 'pg_database') AS description "
+
+						  "FROM pg_database "
+						  "WHERE datname = current_database()",
+						  username_subquery);
+	}
+	else if (fout->remoteVersion >= 90600)
+	{
+		appendPQExpBuffer(dbQry, "SELECT tableoid, oid, datname, "
+						  "(%s datdba) AS dba, "
+						  "pg_encoding_to_char(encoding) AS encoding, "
+						  "datcollate, datctype, datfrozenxid, datminmxid, "
+						  "(SELECT array_agg(acl ORDER BY row_n) FROM "
+						  "  (SELECT acl, row_n FROM "
+						  "     unnest(coalesce(datacl,acldefault('d',datdba))) "
+						  "     WITH ORDINALITY AS perm(acl,row_n) "
+						  "   WHERE NOT EXISTS ( "
+						  "     SELECT 1 "
+						  "     FROM unnest(acldefault('d',datdba)) "
+						  "       AS init(init_acl) "
+						  "     WHERE acl = init_acl)) AS datacls) "
+						  " AS datacl, "
+						  "(SELECT array_agg(acl ORDER BY row_n) FROM "
+						  "  (SELECT acl, row_n FROM "
+						  "     unnest(acldefault('d',datdba)) "
+						  "     WITH ORDINALITY AS initp(acl,row_n) "
+						  "   WHERE NOT EXISTS ( "
+						  "     SELECT 1 "
+						  "     FROM unnest(coalesce(datacl,acldefault('d',datdba))) "
+						  "       AS permp(orig_acl) "
+						  "     WHERE acl = orig_acl)) AS rdatacls) "
+						  " AS rdatacl, "
+						  "datistemplate, datconnlimit, false as dathaslogintriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2867,7 +2903,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "datcollate, datctype, datfrozenxid, datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogintriggers"
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2881,7 +2917,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "datcollate, datctype, datfrozenxid, 0 AS datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogintriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2895,7 +2931,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "NULL AS datcollate, NULL AS datctype, datfrozenxid, 0 AS datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogintriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2910,7 +2946,7 @@ dumpDatabase(Archive *fout)
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "NULL AS datcollate, NULL AS datctype, datfrozenxid, 0 AS datminmxid, "
 						  "datacl, '' as rdatacl, datistemplate, "
-						  "-1 as datconnlimit, "
+						  "-1 as datconnlimit, false as dathaslogintriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace "
 						  "FROM pg_database "
 						  "WHERE datname = current_database()",
@@ -2932,6 +2968,7 @@ dumpDatabase(Archive *fout)
 	i_rdatacl = PQfnumber(res, "rdatacl");
 	i_datistemplate = PQfnumber(res, "datistemplate");
 	i_datconnlimit = PQfnumber(res, "datconnlimit");
+	i_dathaslogintriggers = PQfnumber(res, "dathaslogintriggers");
 	i_tablespace = PQfnumber(res, "tablespace");
 
 	dbCatId.tableoid = atooid(PQgetvalue(res, 0, i_tableoid));
@@ -2946,6 +2983,7 @@ dumpDatabase(Archive *fout)
 	datacl = PQgetvalue(res, 0, i_datacl);
 	rdatacl = PQgetvalue(res, 0, i_rdatacl);
 	datistemplate = PQgetvalue(res, 0, i_datistemplate);
+	dathaslogintriggers = PQgetvalue(res, 0, i_dathaslogintriggers);
 	datconnlimit = PQgetvalue(res, 0, i_datconnlimit);
 	tablespace = PQgetvalue(res, 0, i_tablespace);
 
@@ -3119,6 +3157,14 @@ dumpDatabase(Archive *fout)
 		appendPQExpBufferStr(delQry, ";\n");
 	}
 
+	if (strcmp(dathaslogintriggers, "t") == 0)
+	{
+		appendPQExpBufferStr(creaQry, "UPDATE pg_catalog.pg_database "
+							 "SET dathaslogintriggers = true WHERE datname = ");
+		appendStringLiteralAH(creaQry, datname, fout);
+		appendPQExpBufferStr(creaQry, ";\n");
+	}
+
 	/* Add database-specific SET options */
 	dumpDatabaseConfig(fout, creaQry, datname, dbCatId.oid);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 5cd5838668..70c64b6700 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3073,7 +3073,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end",
+					  "login", "sql_drop");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat
index b8aa1364a0..2b804a47c3 100644
--- a/src/include/catalog/pg_database.dat
+++ b/src/include/catalog/pg_database.dat
@@ -15,7 +15,7 @@
 { oid => '1', oid_symbol => 'TemplateDbOid',
   descr => 'default template for new databases',
   datname => 'template1', encoding => 'ENCODING', datcollate => 'LC_COLLATE',
-  datctype => 'LC_CTYPE', datistemplate => 't', datallowconn => 't',
+  datctype => 'LC_CTYPE', datistemplate => 't', dathaslogintriggers => 'f', datallowconn => 't',
   datconnlimit => '-1', datlastsysoid => '0', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datacl => '_null_' },
 
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index 43f3beb6a3..a6e0309bb3 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -52,6 +52,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
 	/* new connections allowed? */
 	bool		datallowconn;
 
+	/* database has login triggers? */
+	bool		dathaslogintriggers;
+
 	/* max connections allowed (-1=no limit) */
 	int32		datconnlimit;
 
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index e765e67fd1..a9f9954597 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -54,6 +54,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnLogin(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 9ba24d4ca9..b0e7df3d81 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
+PG_CMDTAG(CMDTAG_LOGIN, "LOGIN", true, false, false)
 PG_CMDTAG(CMDTAG_MOVE, "MOVE", false, false, true)
 PG_CMDTAG(CMDTAG_NOTIFY, "NOTIFY", false, false, false)
 PG_CMDTAG(CMDTAG_PREPARE, "PREPARE", false, false, false)
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index 58ddb71cb1..29d73afe3d 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Login,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index ac581c1c07..bdf576651f 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,26 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+
+CREATE TABLE user_logins(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO user_logins (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 $node_primary->wait_for_catchup($node_standby_1, 'replay',
 	$node_primary->lsn('insert'));
@@ -339,6 +359,9 @@ sub replay_check
 
 replay_check();
 
+$node_standby_1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$node_standby_2->safe_psql('postgres', "SELECT 2", extra_params => [ '-U', 'regress_user', '-w' ]);
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 44d545de25..7794744d6b 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -604,3 +604,34 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- On login triggers
+create table user_logins(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into user_logins (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on login execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+NOTICE:  You are welcome!
+select * from user_logins;
+ id | who  
+----+------
+  1 | I am
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+select * from user_logins;
+ id | who  
+----+------
+  1 | I am
+  2 | I am
+(2 rows)
+
+-- Cleanup
+drop table user_logins;
+drop event trigger on_login_trigger;
+drop function on_login_proc();
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 1446cf8cc8..d2ebb4b2c9 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -462,3 +462,23 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- On login triggers
+create table user_logins(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into user_logins (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on login execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+select * from user_logins;
+\c
+select * from user_logins;
+
+-- Cleanup
+drop table user_logins;
+drop event trigger on_login_trigger;
+drop function on_login_proc();
-- 
2.27.0

#71Teodor Sigaev
teodor@sigaev.ru
In reply to: Greg Nancarrow (#70)
1 attachment(s)
Re: On login trigger: take three

Hi!

Nice feature, but, sorry, I see some design problem in suggested feature. AFAIK,
there is two use cases for this feature:
1 A permission / prohibition to login some users
2 Just a logging of facts of user's login

Suggested patch proposes prohibition of login only by failing of login trigger
and it has at least two issues:
1 In case of prohibition to login, there is no clean way to store information
about unsuccessful login. Ok, it could be solved by dblink module but it seems
to scary.
2 For logging purpose failing of trigger will cause impossibility to login, it
could be workarounded by catching error in trigger function, but it's not so
obvious for DBA.

some other issues:
3 It's easy to create security hole, see attachment where non-privileged user
can close access to database even for superuser.
4 Feature is not applicable for logging unsuccessful login with wrong
username/password or not matched by pg_hba.conf. Oracle could operate with such
cases. But I don't think that this issue is a stopper for the patch.

May be, to solve that issues we could introduce return value of trigger and/or
add something like IGNORE ERROR to CREATE EVENT TRIGGER command.

On 15.09.2021 16:32, Greg Nancarrow wrote:

On Wed, Sep 8, 2021 at 10:56 PM Daniel Gustafsson <daniel@yesql.se> wrote:

I took a look at this, and while I like the proposed feature I think the patch
has a bit more work required.

Thanks for reviewing the patch.
I am not the original patch author (who no longer seems active) but
I've been contributing a bit and keeping the patch alive because I
think it's a worthwhile feature.

+
+-- 2) Initialize some user session data
+   CREATE TEMP TABLE session_storage (x float, y integer);
The example in the documentation use a mix of indentations, neither of which is
the 2-space indentation used elsewhere in the docs.

Fixed, using 2-space indentation.
(to be honest, the indentation seems inconsistent elsewhere in the
docs e.g. I'm seeing a nearby case of 2-space on 1st indent, 6-space
on 2nd indent)

+       /* Get the command tag. */
+       tag = parsetree ? CreateCommandTag(parsetree) : CMDTAG_CONNECT;
This is hardcoding knowledge that currently only this trigger wont have a
parsetree, and setting the tag accordingly.  This should instead check the
event and set CMDTAG_UNKNOWN if it isn't the expected one.

I updated that, but maybe not exactly how you expected?

+       /* database has on-login triggers */
+       bool            dathaslogontriggers;
This patch uses three different names for the same thing: client connection,
logontrigger and login trigger.  Since this trigger only fires after successful
authentication it’s not accurate to name it client connection, as that implies
it running on connections rather than logins.  The nomenclature used in the
tree is "login" so the patch should be adjusted everywhere to use that.

Fixed.

+/* Hook for plugins to get control at start of session */
+client_connection_hook_type client_connection_hook = EventTriggerOnConnect;
I don't see the reason for adding core functionality by hooks.  Having a hook
might be a useful thing on its own (to be discussed in a new thread, not hidden
here), but core functionality should not depend on it IMO.

Fair enough, I removed the hook.

+       {"enable_client_connection_trigger", PGC_SU_BACKEND, DEVELOPER_OPTIONS,
+               gettext_noop("Enables the client_connection event trigger."),
+               gettext_noop("In case of errors in the ON client_connection EVENT TRIGGER procedure, "
..and..
+       /*
+        * Try to ignore error for superuser to make it possible to login even in case of errors
+        * during trigger execution
+        */
+       if (!is_superuser)
+               PG_RE_THROW();
This patch adds two ways for superusers to bypass this event trigger in case of
it being faulty, but for every other event trigger we've documented to restart
in single-user mode and fixing it there.  Why does this need to be different?
This clearly has a bigger chance of being a footgun but I don't see that as a
reason to add a GUC and a bypass that other footguns lack.

OK, I removed those bypasses. We'll see what others think.

+ elog(NOTICE, "client_connection trigger failed with message: %s", error->message);
Calling elog() in a PG_CATCH() block isn't allowed is it?

I believe it is allowed (e.g. there's a case in libpq), but I removed
this anyway as part of the superuser bypass.

+       /* Runtlist is empty: clear dathaslogontriggers flag
+        */
s/Runtlist/Runlist/ and also commenting style.

Fixed.

The logic for resetting the pg_database flag when firing event trigger in case
the trigger has gone away is messy and a problem waiting to happen IMO. For
normal triggers we don't bother with that on the grounds of it being racy, and
instead perform relhastrigger removal during VACUUM. Another approach is using
a counter as propose upthread, since updating that can be made safer. The
current coding also isn't instructing concurrent backends to perform relcache
rebuild.

I think there are pros and cons of each possible approach, but I think
I prefer the current way (which I have tweaked a bit) for similar
reasons to those explained by the original patch author when debating
whether to use reference-counting instead, in the discussion upthread
(e.g. it keeps it all in one place). Also, it seems to be more inline
with the documented reason why that pg_database flag was added in the
first place. I have debugged a few concurrent scenarios with the
current mechanism in place. If you still dislike the logic for
resetting the flag, please elaborate on the issues you foresee and one
of the alternative approaches can be tried.

I've attached the updated patch.

Regards,
Greg Nancarrow
Fujitsu Australia

--
Teodor Sigaev E-mail: teodor@sigaev.ru
WWW: http://www.sigaev.ru/

Attachments:

2.sqlapplication/sql; name=2.sqlDownload
#72Greg Nancarrow
gregn4422@gmail.com
In reply to: Teodor Sigaev (#71)
Re: On login trigger: take three

On Wed, Sep 29, 2021 at 10:14 PM Teodor Sigaev <teodor@sigaev.ru> wrote:

Nice feature, but, sorry, I see some design problem in suggested feature. AFAIK,
there is two use cases for this feature:
1 A permission / prohibition to login some users
2 Just a logging of facts of user's login

Suggested patch proposes prohibition of login only by failing of login trigger
and it has at least two issues:
1 In case of prohibition to login, there is no clean way to store information
about unsuccessful login. Ok, it could be solved by dblink module but it seems
to scary.

It's an area that could be improved, but the patch is more intended to
allow additional actions on successful login, as described by the
following (taken from the doc updates in the patch):

+   <para>
+    The event trigger on the <literal>login</literal> event can be
+    useful for logging user logins, for verifying the connection and
+    assigning roles according to current circumstances, or for some
session data
+    initialization.
+   </para>

2 For logging purpose failing of trigger will cause impossibility to login, it
could be workarounded by catching error in trigger function, but it's not so
obvious for DBA.

If you look back on the past discussions on this thread, you'll see
that originally the patch added a GUC for the purpose of allowing
superusers to disable the event trigger, to allow login in this case.
However it was argued that there was already a documented way of
bypassing buggy event triggers (i.e. restart in single-user mode), so
that GUC was removed.
Also previously in the patch, login trigger errors for superusers were
ignored in order to allow them to login in this case, but it was
argued that it could well be unsafe to continue on after an error, so
that too was removed.
See below:

+       {"enable_client_connection_trigger", PGC_SU_BACKEND, DEVELOPER_OPTIONS,
+               gettext_noop("Enables the client_connection event trigger."),
+               gettext_noop("In case of errors in the ON client_connection EVENT TRIGGER procedure, "
..and..
+       /*
+        * Try to ignore error for superuser to make it possible to login even in case of errors
+        * during trigger execution
+        */
+       if (!is_superuser)
+               PG_RE_THROW();
This patch adds two ways for superusers to bypass this event trigger in case of
it being faulty, but for every other event trigger we've documented to restart
in single-user mode and fixing it there.  Why does this need to be different?
This clearly has a bigger chance of being a footgun but I don't see that as a
reason to add a GUC and a bypass that other footguns lack.

So I really don't think that a failing event trigger will cause an
"impossibility to login".
The patch updates the documentation to explain the use of single-user
mode to fix buggy login event triggers. See below:

+   <para>
+     The <literal>login</literal> event occurs when a user logs in to the
+     system.
+     Any bugs in a trigger procedure for this event may prevent successful
+     login to the system. Such bugs may be fixed after first restarting the
+     system in single-user mode (as event triggers are disabled in this mode).
+     See the <xref linkend="app-postgres"/> reference page for details about
+     using single-user mode.
+   </para>
+

Regards,
Greg Nancarrow
Fujitsu Australia

#73Daniel Gustafsson
daniel@yesql.se
In reply to: Greg Nancarrow (#72)
Re: On login trigger: take three

On 30 Sep 2021, at 04:15, Greg Nancarrow <gregn4422@gmail.com> wrote:

On Wed, Sep 29, 2021 at 10:14 PM Teodor Sigaev <teodor@sigaev.ru> wrote:

Nice feature, but, sorry, I see some design problem in suggested feature. AFAIK,
there is two use cases for this feature:
1 A permission / prohibition to login some users
2 Just a logging of facts of user's login

Suggested patch proposes prohibition of login only by failing of login trigger
and it has at least two issues:
1 In case of prohibition to login, there is no clean way to store information
about unsuccessful login. Ok, it could be solved by dblink module but it seems
to scary.

It's an area that could be improved, but the patch is more intended to
allow additional actions on successful login, as described by the
following (taken from the doc updates in the patch):

+   <para>
+    The event trigger on the <literal>login</literal> event can be
+    useful for logging user logins, for verifying the connection and
+    assigning roles according to current circumstances, or for some
session data
+    initialization.
+   </para>

Running user code with potential side effects on unsuccessful logins also open
up the risk for (D)DoS attacks.

--
Daniel Gustafsson https://vmware.com/

#74Greg Nancarrow
gregn4422@gmail.com
In reply to: Greg Nancarrow (#70)
1 attachment(s)
Re: On login trigger: take three

On Wed, Sep 15, 2021 at 11:32 PM Greg Nancarrow <gregn4422@gmail.com> wrote:

I've attached the updated patch.

Attached a rebased version of the patch, as it was broken by fairly
recent changes (only very minor change to the previous version).

Regards,
Greg Nancarrow
Fujitsu Australia

Attachments:

v19-0001-Add-a-new-login-event-and-login-trigger-support.patchapplication/octet-stream; name=v19-0001-Add-a-new-login-event-and-login-trigger-support.patchDownload
From 4c556bbb6eebe2bae4f14efc96f21b97ce4daf15 Mon Sep 17 00:00:00 2001
From: Greg Nancarrow <gregn4422@gmail.com>
Date: Tue, 5 Oct 2021 14:50:46 +1100
Subject: [PATCH v19] Add a new "login" event and login trigger support.

The login event occurs when a user logs in to the system.

Author: Konstantin Knizhnik
Discussion: https://www.postgresql.org/message-id/flat/0d46d29f-4558-3af9-9c85-7774e14a7709%40postgrespro.ru
---
 doc/src/sgml/bki.sgml                       |   2 +-
 doc/src/sgml/catalogs.sgml                  |  11 ++
 doc/src/sgml/ecpg.sgml                      |   2 +
 doc/src/sgml/event-trigger.sgml             |  70 ++++++++-
 src/backend/commands/dbcommands.c           |   3 +-
 src/backend/commands/event_trigger.c        | 159 +++++++++++++++++---
 src/backend/tcop/postgres.c                 |   4 +
 src/backend/utils/cache/evtcache.c          |   2 +
 src/bin/pg_dump/pg_dump.c                   |  58 ++++++-
 src/bin/psql/tab-complete.c                 |   3 +-
 src/include/catalog/pg_database.dat         |   2 +-
 src/include/catalog/pg_database.h           |   3 +
 src/include/commands/event_trigger.h        |   1 +
 src/include/tcop/cmdtaglist.h               |   1 +
 src/include/utils/evtcache.h                |   3 +-
 src/test/recovery/t/001_stream_rep.pl       |  23 +++
 src/test/regress/expected/event_trigger.out |  31 ++++
 src/test/regress/sql/event_trigger.sql      |  20 +++
 18 files changed, 368 insertions(+), 30 deletions(-)

diff --git a/doc/src/sgml/bki.sgml b/doc/src/sgml/bki.sgml
index f900a01b82..c312165f71 100644
--- a/doc/src/sgml/bki.sgml
+++ b/doc/src/sgml/bki.sgml
@@ -182,7 +182,7 @@
 { oid =&gt; '1', oid_symbol =&gt; 'TemplateDbOid',
   descr =&gt; 'database\'s default template',
   datname =&gt; 'template1', encoding =&gt; 'ENCODING', datcollate =&gt; 'LC_COLLATE',
-  datctype =&gt; 'LC_CTYPE', datistemplate =&gt; 't', datallowconn =&gt; 't',
+  datctype =&gt; 'LC_CTYPE', datistemplate =&gt; 't', datallowconn =&gt; 't', dathaslogintriggers =&gt; 'f',
   datconnlimit =&gt; '-1', datlastsysoid =&gt; '0', datfrozenxid =&gt; '0',
   datminmxid =&gt; '1', dattablespace =&gt; 'pg_default', datacl =&gt; '_null_' },
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 384e6eaa3b..e088c33f44 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2972,6 +2972,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>dathaslogintriggers</structfield> <type>bool</type>
+      </para>
+      <para>
+        Indicates that there are login triggers defined for this database.
+        This flag is used to avoid extra lookups on the pg_event_trigger table during each backend startup.
+        This flag is used internally by Postgres and should not be manually changed by DBA or application.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>datconnlimit</structfield> <type>int4</type>
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index 04d4a6bdb2..565f88d298 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -4731,6 +4731,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = t (type: 1)
 datallowconn = t (type: 1)
+dathaslogintriggers = f (type: 1)
 datconnlimit = -1 (type: 5)
 datlastsysoid = 11510 (type: 1)
 datfrozenxid = 379 (type: 1)
@@ -4756,6 +4757,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = f (type: 1)
 datallowconn = t (type: 1)
+dathaslogintriggers = f (type: 1)
 datconnlimit = -1 (type: 5)
 datlastsysoid = 11510 (type: 1)
 datfrozenxid = 379 (type: 1)
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 60366a950e..ec35ddfad8 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>login</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,16 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>login</literal> event occurs when a user logs in to the
+     system.
+     Any bugs in a trigger procedure for this event may prevent successful
+     login to the system. Such bugs may be fixed after first restarting the
+     system in single-user mode (as event triggers are disabled in this mode).
+     See the <xref linkend="app-postgres"/> reference page for details about
+     using single-user mode.
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1140,7 +1151,7 @@ typedef struct EventTriggerData
   </sect1>
 
   <sect1 id="event-trigger-example">
-   <title>A Complete Event Trigger Example</title>
+   <title>A C language Event Trigger Example</title>
 
    <para>
     Here is a very simple example of an event trigger function written in C.
@@ -1280,6 +1291,63 @@ $$;
 CREATE EVENT TRIGGER no_rewrite_allowed
                   ON table_rewrite
    EXECUTE FUNCTION no_rewrite();
+</programlisting>
+   </para>
+ </sect1>
+
+  <sect1 id="event-trigger-database-login-example">
+   <title>A Database Login Event Trigger Example</title>
+
+   <para>
+    The event trigger on the <literal>login</literal> event can be
+    useful for logging user logins, for verifying the connection and
+    assigning roles according to current circumstances, or for some session data
+    initialization.
+   </para>
+
+   <para>
+    The following example demonstrates these options.
+<programlisting>
+-- create test tables and roles
+CREATE TABLE user_login_log (
+  "user" text,
+  "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+  RETURNS event_trigger SECURITY DEFINER
+  LANGUAGE plpgsql AS
+$$
+DECLARE
+  hour integer = EXTRACT('hour' FROM current_time);
+BEGIN
+-- 1) Assign some roles
+IF hour BETWEEN 8 AND 20 THEN         -- at daytime grant the day_worker role
+  EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT    day_worker TO '   || quote_ident(session_user);
+ELSIF hour BETWEEN 2 AND 4 THEN
+  RAISE EXCEPTION 'Login forbidden';  -- do not allow to login these hours
+ELSE                                  -- at other time grant the night_worker role
+  EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT  night_worker TO ' || quote_ident(session_user);
+END IF;
+
+-- 2) Initialize some user session data
+CREATE TEMP TABLE session_storage (x float, y integer);
+
+-- 3) Log the connection time
+INSERT INTO user_login_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+  ON login
+  EXECUTE FUNCTION init_session();
 </programlisting>
    </para>
  </sect1>
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 029fab48df..a658d476ca 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -530,6 +530,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	new_record[Anum_pg_database_datctype - 1] =
 		DirectFunctionCall1(namein, CStringGetDatum(dbctype));
 	new_record[Anum_pg_database_datistemplate - 1] = BoolGetDatum(dbistemplate);
+	new_record[Anum_pg_database_dathaslogintriggers - 1] = BoolGetDatum(false);
 	new_record[Anum_pg_database_datallowconn - 1] = BoolGetDatum(dballowconnections);
 	new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 	new_record[Anum_pg_database_datlastsysoid - 1] = ObjectIdGetDatum(src_lastsysoid);
@@ -1585,7 +1586,7 @@ AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel)
 		new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 		new_record_repl[Anum_pg_database_datconnlimit - 1] = true;
 	}
-
+	new_record[Anum_pg_database_dathaslogintriggers - 1] = BoolGetDatum(datform->dathaslogintriggers);
 	newtuple = heap_modify_tuple(tuple, RelationGetDescr(rel), new_record,
 								 new_record_nulls, new_record_repl);
 	CatalogTupleUpdate(rel, &tuple->t_self, newtuple);
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 71612d577e..539ba2e8ad 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -43,9 +44,11 @@
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 typedef struct EventTriggerQueryState
@@ -130,6 +133,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "login") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -293,6 +297,26 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
 	CatalogTupleInsert(tgrel, tuple);
 	heap_freetuple(tuple);
 
+	if (strcmp(eventname, "login") == 0)
+	{
+		Form_pg_database db;
+		Relation pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+		/* Set dathaslogintriggers flag in pg_database */
+		tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+		db = (Form_pg_database) GETSTRUCT(tuple);
+		if (!db->dathaslogintriggers)
+		{
+			db->dathaslogintriggers = true;
+			CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+		}
+		else
+			CacheInvalidateRelcacheByTuple(tuple);
+		table_close(pg_db, RowExclusiveLock);
+		heap_freetuple(tuple);
+	}
+
 	/* Depend on owner. */
 	recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
 
@@ -562,6 +586,9 @@ EventTriggerCommonSetup(Node *parsetree,
 	ListCell   *lc;
 	List	   *runlist = NIL;
 
+	/* Get the command tag. */
+	tag = (event == EVT_Login) ? CMDTAG_LOGIN : CreateCommandTag(parsetree);
+
 	/*
 	 * We want the list of command tags for which this procedure is actually
 	 * invoked to match up exactly with the list that CREATE EVENT TRIGGER
@@ -577,22 +604,18 @@ EventTriggerCommonSetup(Node *parsetree,
 	 * relevant command tag.
 	 */
 #ifdef USE_ASSERT_CHECKING
+	if (event == EVT_DDLCommandStart ||
+		event == EVT_DDLCommandEnd ||
+		event == EVT_SQLDrop ||
+		event == EVT_Login)
 	{
-		CommandTag	dbgtag;
-
-		dbgtag = CreateCommandTag(parsetree);
-		if (event == EVT_DDLCommandStart ||
-			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
-		{
-			if (!command_tag_event_trigger_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
-		else if (event == EVT_TableRewrite)
-		{
-			if (!command_tag_table_rewrite_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
+		if (!command_tag_event_trigger_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
+	}
+	else if (event == EVT_TableRewrite)
+	{
+		if (!command_tag_table_rewrite_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
 	}
 #endif
 
@@ -601,9 +624,6 @@ EventTriggerCommonSetup(Node *parsetree,
 	if (cachelist == NIL)
 		return NIL;
 
-	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
-
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
 	 * memory context.  Once we start running the command triggers, or indeed
@@ -800,6 +820,109 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Return true if this database has login triggers, false otherwise.
+ */
+static bool
+DatabaseHasLoginTriggers(void)
+{
+	bool has_login_triggers;
+	HeapTuple tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+	has_login_triggers = ((Form_pg_database) GETSTRUCT(tuple))->dathaslogintriggers;
+	ReleaseSysCache(tuple);
+	return has_login_triggers;
+}
+
+/*
+ * Fire login triggers.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId))
+		return;
+
+	StartTransactionCommand();
+
+	if (DatabaseHasLoginTriggers())
+	{
+		runlist = EventTriggerCommonSetup(NULL,
+										  EVT_Login, "login",
+										  &trigdata);
+
+		if (runlist != NIL)
+		{
+			/*
+			 * Make sure anything the main command did will be visible to the event
+			 * triggers.
+			 */
+			CommandCounterIncrement();
+
+			/*
+			 * Event trigger execution may require an active snapshot.
+			 */
+			PushActiveSnapshot(GetTransactionSnapshot());
+
+			/* Run the triggers. */
+			EventTriggerInvoke(runlist, &trigdata);
+
+			/* Cleanup. */
+			list_free(runlist);
+
+			PopActiveSnapshot();
+		}
+		else
+		{
+			/*
+			 * Runlist is empty: clear dathaslogintriggers flag
+			 */
+			Relation pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+			HeapTuple tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+			Form_pg_database db;
+
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+			db = (Form_pg_database) GETSTRUCT(tuple);
+			if (db->dathaslogintriggers)
+			{
+				/*
+				 * There can be a race condition: a login event trigger may have
+				 * been added after the pg_event_trigger table was scanned, and
+				 * we don't want to erroneously clear the dathaslogintriggers
+				 * flag in this case. To be sure that this hasn't happened,
+				 * repeat the scan under the pg_database table lock.
+				 */
+				AcceptInvalidationMessages();
+				runlist = EventTriggerCommonSetup(NULL,
+												  EVT_Login, "login",
+												  &trigdata);
+				if (runlist == NULL) /* list is still empty, so clear the flag */
+				{
+					db->dathaslogintriggers = false;
+					CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				}
+				else
+					CacheInvalidateRelcacheByTuple(tuple);
+			}
+			table_close(pg_db, RowExclusiveLock);
+			heap_freetuple(tuple);
+		}
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 0775abe35d..273611caf9 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -42,6 +42,7 @@
 #include "catalog/pg_type.h"
 #include "commands/async.h"
 #include "commands/prepare.h"
+#include "commands/event_trigger.h"
 #include "executor/spi.h"
 #include "jit/jit.h"
 #include "libpq/libpq.h"
@@ -4178,6 +4179,9 @@ PostgresMain(const char *dbname, const char *username)
 	initStringInfo(&row_description_buf);
 	MemoryContextSwitchTo(TopMemoryContext);
 
+	/* Fire any defined login triggers, if appropriate */
+	EventTriggerOnLogin();
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 460b720a65..b9b935f1cc 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "login") == 0)
+			event = EVT_Login;
 		else
 			continue;
 
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a485fb2d07..4e83dec4b1 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -2797,6 +2797,7 @@ dumpDatabase(Archive *fout)
 				i_datacl,
 				i_rdatacl,
 				i_datistemplate,
+				i_dathaslogintriggers,
 				i_datconnlimit,
 				i_tablespace;
 	CatalogId	dbCatId;
@@ -2809,6 +2810,7 @@ dumpDatabase(Archive *fout)
 			   *datacl,
 			   *rdatacl,
 			   *datistemplate,
+			   *dathaslogintriggers,
 			   *datconnlimit,
 			   *tablespace;
 	uint32		frozenxid,
@@ -2827,7 +2829,7 @@ dumpDatabase(Archive *fout)
 	 * (pg_init_privs) are not supported on databases, so this logic cannot
 	 * make use of buildACLQueries().
 	 */
-	if (fout->remoteVersion >= 90600)
+	if (fout->remoteVersion >= 150000)
 	{
 		appendPQExpBuffer(dbQry, "SELECT tableoid, oid, datname, "
 						  "(%s datdba) AS dba, "
@@ -2853,7 +2855,41 @@ dumpDatabase(Archive *fout)
 						  "       AS permp(orig_acl) "
 						  "     WHERE acl = orig_acl)) AS rdatacls) "
 						  " AS rdatacl, "
-						  "datistemplate, datconnlimit, "
+						  "datistemplate, datconnlimit, dathaslogintriggers, "
+						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
+						  "shobj_description(oid, 'pg_database') AS description "
+
+						  "FROM pg_database "
+						  "WHERE datname = current_database()",
+						  username_subquery);
+	}
+	else if (fout->remoteVersion >= 90600)
+	{
+		appendPQExpBuffer(dbQry, "SELECT tableoid, oid, datname, "
+						  "(%s datdba) AS dba, "
+						  "pg_encoding_to_char(encoding) AS encoding, "
+						  "datcollate, datctype, datfrozenxid, datminmxid, "
+						  "(SELECT array_agg(acl ORDER BY row_n) FROM "
+						  "  (SELECT acl, row_n FROM "
+						  "     unnest(coalesce(datacl,acldefault('d',datdba))) "
+						  "     WITH ORDINALITY AS perm(acl,row_n) "
+						  "   WHERE NOT EXISTS ( "
+						  "     SELECT 1 "
+						  "     FROM unnest(acldefault('d',datdba)) "
+						  "       AS init(init_acl) "
+						  "     WHERE acl = init_acl)) AS datacls) "
+						  " AS datacl, "
+						  "(SELECT array_agg(acl ORDER BY row_n) FROM "
+						  "  (SELECT acl, row_n FROM "
+						  "     unnest(acldefault('d',datdba)) "
+						  "     WITH ORDINALITY AS initp(acl,row_n) "
+						  "   WHERE NOT EXISTS ( "
+						  "     SELECT 1 "
+						  "     FROM unnest(coalesce(datacl,acldefault('d',datdba))) "
+						  "       AS permp(orig_acl) "
+						  "     WHERE acl = orig_acl)) AS rdatacls) "
+						  " AS rdatacl, "
+						  "datistemplate, datconnlimit, false as dathaslogintriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2867,7 +2903,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "datcollate, datctype, datfrozenxid, datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogintriggers"
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2881,7 +2917,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "datcollate, datctype, datfrozenxid, 0 AS datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogintriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2895,7 +2931,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "NULL AS datcollate, NULL AS datctype, datfrozenxid, 0 AS datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogintriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2910,7 +2946,7 @@ dumpDatabase(Archive *fout)
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "NULL AS datcollate, NULL AS datctype, datfrozenxid, 0 AS datminmxid, "
 						  "datacl, '' as rdatacl, datistemplate, "
-						  "-1 as datconnlimit, "
+						  "-1 as datconnlimit, false as dathaslogintriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace "
 						  "FROM pg_database "
 						  "WHERE datname = current_database()",
@@ -2932,6 +2968,7 @@ dumpDatabase(Archive *fout)
 	i_rdatacl = PQfnumber(res, "rdatacl");
 	i_datistemplate = PQfnumber(res, "datistemplate");
 	i_datconnlimit = PQfnumber(res, "datconnlimit");
+	i_dathaslogintriggers = PQfnumber(res, "dathaslogintriggers");
 	i_tablespace = PQfnumber(res, "tablespace");
 
 	dbCatId.tableoid = atooid(PQgetvalue(res, 0, i_tableoid));
@@ -2946,6 +2983,7 @@ dumpDatabase(Archive *fout)
 	datacl = PQgetvalue(res, 0, i_datacl);
 	rdatacl = PQgetvalue(res, 0, i_rdatacl);
 	datistemplate = PQgetvalue(res, 0, i_datistemplate);
+	dathaslogintriggers = PQgetvalue(res, 0, i_dathaslogintriggers);
 	datconnlimit = PQgetvalue(res, 0, i_datconnlimit);
 	tablespace = PQgetvalue(res, 0, i_tablespace);
 
@@ -3119,6 +3157,14 @@ dumpDatabase(Archive *fout)
 		appendPQExpBufferStr(delQry, ";\n");
 	}
 
+	if (strcmp(dathaslogintriggers, "t") == 0)
+	{
+		appendPQExpBufferStr(creaQry, "UPDATE pg_catalog.pg_database "
+							 "SET dathaslogintriggers = true WHERE datname = ");
+		appendStringLiteralAH(creaQry, datname, fout);
+		appendPQExpBufferStr(creaQry, ";\n");
+	}
+
 	/* Add database-specific SET options */
 	dumpDatabaseConfig(fout, creaQry, datname, dbCatId.oid);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index ecae9df8ed..b7a0186dba 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3073,7 +3073,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end",
+					  "login", "sql_drop");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat
index b8aa1364a0..2b804a47c3 100644
--- a/src/include/catalog/pg_database.dat
+++ b/src/include/catalog/pg_database.dat
@@ -15,7 +15,7 @@
 { oid => '1', oid_symbol => 'TemplateDbOid',
   descr => 'default template for new databases',
   datname => 'template1', encoding => 'ENCODING', datcollate => 'LC_COLLATE',
-  datctype => 'LC_CTYPE', datistemplate => 't', datallowconn => 't',
+  datctype => 'LC_CTYPE', datistemplate => 't', dathaslogintriggers => 'f', datallowconn => 't',
   datconnlimit => '-1', datlastsysoid => '0', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datacl => '_null_' },
 
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index 43f3beb6a3..a6e0309bb3 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -52,6 +52,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
 	/* new connections allowed? */
 	bool		datallowconn;
 
+	/* database has login triggers? */
+	bool		dathaslogintriggers;
+
 	/* max connections allowed (-1=no limit) */
 	int32		datconnlimit;
 
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index e765e67fd1..a9f9954597 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -54,6 +54,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnLogin(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 9ba24d4ca9..b0e7df3d81 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
+PG_CMDTAG(CMDTAG_LOGIN, "LOGIN", true, false, false)
 PG_CMDTAG(CMDTAG_MOVE, "MOVE", false, false, true)
 PG_CMDTAG(CMDTAG_NOTIFY, "NOTIFY", false, false, false)
 PG_CMDTAG(CMDTAG_PREPARE, "PREPARE", false, false, false)
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index 58ddb71cb1..29d73afe3d 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Login,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index ac581c1c07..bdf576651f 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,26 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+
+CREATE TABLE user_logins(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO user_logins (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 $node_primary->wait_for_catchup($node_standby_1, 'replay',
 	$node_primary->lsn('insert'));
@@ -339,6 +359,9 @@ sub replay_check
 
 replay_check();
 
+$node_standby_1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$node_standby_2->safe_psql('postgres', "SELECT 2", extra_params => [ '-U', 'regress_user', '-w' ]);
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 44d545de25..7794744d6b 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -604,3 +604,34 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- On login triggers
+create table user_logins(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into user_logins (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on login execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+NOTICE:  You are welcome!
+select * from user_logins;
+ id | who  
+----+------
+  1 | I am
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+select * from user_logins;
+ id | who  
+----+------
+  1 | I am
+  2 | I am
+(2 rows)
+
+-- Cleanup
+drop table user_logins;
+drop event trigger on_login_trigger;
+drop function on_login_proc();
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 1446cf8cc8..d2ebb4b2c9 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -462,3 +462,23 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- On login triggers
+create table user_logins(id serial, who text);
+create function on_login_proc() returns event_trigger as $$
+begin
+  insert into user_logins (who) values ('I am');
+  raise notice 'You are welcome!';
+end;
+$$ language plpgsql;
+create event trigger on_login_trigger on login execute procedure on_login_proc();
+alter event trigger on_login_trigger enable always;
+\c
+select * from user_logins;
+\c
+select * from user_logins;
+
+-- Cleanup
+drop table user_logins;
+drop event trigger on_login_trigger;
+drop function on_login_proc();
-- 
2.27.0

In reply to: Teodor Sigaev (#71)
1 attachment(s)
Re[2]: On login trigger: take three

Dear colleagues, 
Please see my suggestions below and the updated patch attached.
 
 
 
 
 

Среда, 29 сентября 2021, 15:14 +03:00 от Teodor Sigaev < teodor@sigaev.ru >:
 
Hi!

Nice feature, but, sorry, I see some design problem in suggested feature. AFAIK,
there is two use cases for this feature:
1 A permission / prohibition to login some users
2 Just a logging of facts of user's login

Suggested patch proposes prohibition of login only by failing of login trigger
and it has at least two issues:
1 In case of prohibition to login, there is no clean way to store information
about unsuccessful login. Ok, it could be solved by dblink module but it seems
to scary.

This is a common problem of logging errors and unsuccessful transactions of any kind. It can be solved by:
- logging to external log storage (stupid logging to files or syslog or whatever you can imagine with PL/Perl (sorry))
- logging inside the database by db_link or through background worker (like described in https://dpavlin.wordpress.com/2017/05/09/david-rader-autonomous-transactions-using-pg_background/ )
- or by implementing autonomous transactions in PostgreSQL, which is already under development by some of my and Teodor’s colleagues.
 
So I propose not to invent another solution to this common problem here.

2 For logging purpose failing of trigger will cause impossibility to login, it
could be workarounded by catching error in trigger function, but it's not so
obvious for DBA.

If the trigger contains an error, nobody can login. The database is bricked.
Previous variant of the patch proposed to fix this with going to single-user mode.
For faster recovery I propose to have also a GUC variable to turn on/off the login triggers. It should be 'on' by default.

some other issues:
3 It's easy to create security hole, see attachment where non-privileged user
can close access to database even for superuser.

Such cases can be avoided by careful design of the login triggers and related permissions. Added such note to the documentation.

4 Feature is not applicable for logging unsuccessful login with wrong
username/password or not matched by pg_hba.conf. Oracle could operate with such
cases. But I don't think that this issue is a stopper for the patch.

Yes. Btw, note that pg_hba functionality can be implemented completely inside the login trigger :) !

May be, to solve that issues we could introduce return value of trigger and/or
add something like IGNORE ERROR to CREATE EVENT TRIGGER command.

Any solutions which make syntax more complicated can lead Postgres to become Oracle (in the worst sense). I do not like this.

A new version of the patch is attached. It applies to the current master and contains the above mentioned GUC code, relevant tests and docs.
Regards,
Ivan Panchenko

On 15.09.2021 16:32, Greg Nancarrow wrote:

On Wed, Sep 8, 2021 at 10:56 PM Daniel Gustafsson < daniel@yesql.se > wrote:

I took a look at this, and while I like the proposed feature I think the patch
has a bit more work required.

Thanks for reviewing the patch.
I am not the original patch author (who no longer seems active) but
I've been contributing a bit and keeping the patch alive because I
think it's a worthwhile feature.

+
+-- 2) Initialize some user session data
+ CREATE TEMP TABLE session_storage (x float, y integer);
The example in the documentation use a mix of indentations, neither of which is
the 2-space indentation used elsewhere in the docs.

Fixed, using 2-space indentation.
(to be honest, the indentation seems inconsistent elsewhere in the
docs e.g. I'm seeing a nearby case of 2-space on 1st indent, 6-space
on 2nd indent)

+ /* Get the command tag. */
+ tag = parsetree ? CreateCommandTag(parsetree) : CMDTAG_CONNECT;
This is hardcoding knowledge that currently only this trigger wont have a
parsetree, and setting the tag accordingly. This should instead check the
event and set CMDTAG_UNKNOWN if it isn't the expected one.

I updated that, but maybe not exactly how you expected?

+ /* database has on-login triggers */
+ bool dathaslogontriggers;
This patch uses three different names for the same thing: client connection,
logontrigger and login trigger. Since this trigger only fires after successful
authentication it’s not accurate to name it client connection, as that implies
it running on connections rather than logins. The nomenclature used in the
tree is "login" so the patch should be adjusted everywhere to use that.

Fixed.

+/* Hook for plugins to get control at start of session */
+client_connection_hook_type client_connection_hook = EventTriggerOnConnect;
I don't see the reason for adding core functionality by hooks. Having a hook
might be a useful thing on its own (to be discussed in a new thread, not hidden
here), but core functionality should not depend on it IMO.

Fair enough, I removed the hook.

+ {"enable_client_connection_trigger", PGC_SU_BACKEND, DEVELOPER_OPTIONS,
+ gettext_noop("Enables the client_connection event trigger."),
+ gettext_noop("In case of errors in the ON client_connection EVENT TRIGGER procedure, "
..and..
+ /*
+ * Try to ignore error for superuser to make it possible to login even in case of errors
+ * during trigger execution
+ */
+ if (!is_superuser)
+ PG_RE_THROW();
This patch adds two ways for superusers to bypass this event trigger in case of
it being faulty, but for every other event trigger we've documented to restart
in single-user mode and fixing it there. Why does this need to be different?
This clearly has a bigger chance of being a footgun but I don't see that as a
reason to add a GUC and a bypass that other footguns lack.

OK, I removed those bypasses. We'll see what others think.

+ elog(NOTICE, "client_connection trigger failed with message: %s", error->message);
Calling elog() in a PG_CATCH() block isn't allowed is it?

I believe it is allowed (e.g. there's a case in libpq), but I removed
this anyway as part of the superuser bypass.

+ /* Runtlist is empty: clear dathaslogontriggers flag
+ */
s/Runtlist/Runlist/ and also commenting style.

Fixed.

The logic for resetting the pg_database flag when firing event trigger in case
the trigger has gone away is messy and a problem waiting to happen IMO. For
normal triggers we don't bother with that on the grounds of it being racy, and
instead perform relhastrigger removal during VACUUM. Another approach is using
a counter as propose upthread, since updating that can be made safer. The
current coding also isn't instructing concurrent backends to perform relcache
rebuild.

I think there are pros and cons of each possible approach, but I think
I prefer the current way (which I have tweaked a bit) for similar
reasons to those explained by the original patch author when debating
whether to use reference-counting instead, in the discussion upthread
(e.g. it keeps it all in one place). Also, it seems to be more inline
with the documented reason why that pg_database flag was added in the
first place. I have debugged a few concurrent scenarios with the
current mechanism in place. If you still dislike the logic for
resetting the flag, please elaborate on the issues you foresee and one
of the alternative approaches can be tried.

I've attached the updated patch.

Regards,
Greg Nancarrow
Fujitsu Australia

--
Teodor Sigaev E-mail: teodor@sigaev.ru
                                                    WWW: http://www.sigaev.ru/

 

Attachments:

v20-0001-Add-a-new-login-event-and-login-trigger-support.patchtext/x-diff; name="=?UTF-8?B?djIwLTAwMDEtQWRkLWEtbmV3LWxvZ2luLWV2ZW50LWFuZC1sb2dpbi10cmln?= =?UTF-8?B?Z2VyLXN1cHBvcnQucGF0Y2g=?="Download
diff --git a/doc/src/sgml/bki.sgml b/doc/src/sgml/bki.sgml
index f900a01b82..c312165f71 100644
--- a/doc/src/sgml/bki.sgml
+++ b/doc/src/sgml/bki.sgml
@@ -182,7 +182,7 @@
 { oid =&gt; '1', oid_symbol =&gt; 'TemplateDbOid',
   descr =&gt; 'database\'s default template',
   datname =&gt; 'template1', encoding =&gt; 'ENCODING', datcollate =&gt; 'LC_COLLATE',
-  datctype =&gt; 'LC_CTYPE', datistemplate =&gt; 't', datallowconn =&gt; 't',
+  datctype =&gt; 'LC_CTYPE', datistemplate =&gt; 't', datallowconn =&gt; 't', dathaslogintriggers =&gt; 'f',
   datconnlimit =&gt; '-1', datlastsysoid =&gt; '0', datfrozenxid =&gt; '0',
   datminmxid =&gt; '1', dattablespace =&gt; 'pg_default', datacl =&gt; '_null_' },
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index c1d11be73f..a2d75687c9 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2979,6 +2979,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>dathaslogintriggers</structfield> <type>bool</type>
+      </para>
+      <para>
+        Indicates that there are login triggers defined for this database.
+        This flag is used to avoid extra lookups on the pg_event_trigger table during each backend startup.
+        This flag is used internally by Postgres and should not be manually changed by DBA or application.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>datconnlimit</structfield> <type>int4</type>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index de77f14573..4584c077a9 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1035,6 +1035,22 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-login-triggers" xreflabel="login_triggers">
+      <term><varname>login_triggers</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>login_triggers</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+
+      <listitem>
+       <para>
+        Enables the <xref linkend="event-trigger-login"/>. The default value is <literal>on</literal>.
+	   </para>
+		<para>
+		This parameter can only be set by superusers.
+		</para>
+      </listitem>
+     </varlistentry>
      </variablelist>
      </sect2>
 
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index 04d4a6bdb2..565f88d298 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -4731,6 +4731,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = t (type: 1)
 datallowconn = t (type: 1)
+dathaslogintriggers = f (type: 1)
 datconnlimit = -1 (type: 5)
 datlastsysoid = 11510 (type: 1)
 datfrozenxid = 379 (type: 1)
@@ -4756,6 +4757,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = f (type: 1)
 datallowconn = t (type: 1)
+dathaslogintriggers = f (type: 1)
 datconnlimit = -1 (type: 5)
 datlastsysoid = 11510 (type: 1)
 datfrozenxid = 379 (type: 1)
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 60366a950e..2ff7ce75c3 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>login</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,19 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para id="event-trigger-login" xreflabel="login event triggers">
+     The <literal>login</literal> event occurs when a user logs in to the
+     system.
+     If the trigger fails, user will not log in. The side effect of such behaviour is that a bug in a trigger procedure can prevent any user from logging to the system and bricking it.
+     If this happens, access can be restored by connecting to the system in 
+     single-user mode (as event triggers are disabled in this mode) and fixing the buggy trigger
+     or by disabling login triggers by the <xref linkend="guc-login-triggers"/> GUC variable.
+     See the <xref linkend="app-postgres"/> reference page for details about
+     using single-user mode.
+ 	 Such cases can be avoided by careful design of trigger procedures and related permissions.
+     See also <xref linkend="event-trigger-database-login-example"/>.
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1140,7 +1154,7 @@ typedef struct EventTriggerData
   </sect1>
 
   <sect1 id="event-trigger-example">
-   <title>A Complete Event Trigger Example</title>
+   <title>A C language Event Trigger Example</title>
 
    <para>
     Here is a very simple example of an event trigger function written in C.
@@ -1280,6 +1294,63 @@ $$;
 CREATE EVENT TRIGGER no_rewrite_allowed
                   ON table_rewrite
    EXECUTE FUNCTION no_rewrite();
+</programlisting>
+   </para>
+ </sect1>
+
+  <sect1 id="event-trigger-database-login-example" xreflabel="a login trigger example">
+   <title>A Database Login Event Trigger Example</title>
+
+   <para>
+    The event trigger on the <literal>login</literal> event can be
+    useful for logging user logins, for verifying the connection and
+    assigning roles according to current circumstances, or for some session data
+    initialization.
+   </para>
+
+   <para>
+    The following example demonstrates these options.
+<programlisting>
+-- create test tables and roles
+CREATE TABLE user_login_log (
+  "user" text,
+  "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+  RETURNS event_trigger SECURITY DEFINER
+  LANGUAGE plpgsql AS
+$$
+DECLARE
+  hour integer = EXTRACT('hour' FROM current_time);
+BEGIN
+-- 1) Assign some roles
+IF hour BETWEEN 8 AND 20 THEN         -- at daytime grant the day_worker role
+  EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT    day_worker TO '   || quote_ident(session_user);
+ELSIF hour BETWEEN 2 AND 4 THEN
+  RAISE EXCEPTION 'Login forbidden';  -- do not allow to login these hours
+ELSE                                  -- at other time grant the night_worker role
+  EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT  night_worker TO ' || quote_ident(session_user);
+END IF;
+
+-- 2) Initialize some user session data
+CREATE TEMP TABLE session_storage (x float, y integer);
+
+-- 3) Log the connection time
+INSERT INTO user_login_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+  ON login
+  EXECUTE FUNCTION init_session();
 </programlisting>
    </para>
  </sect1>
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 029fab48df..a658d476ca 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -530,6 +530,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	new_record[Anum_pg_database_datctype - 1] =
 		DirectFunctionCall1(namein, CStringGetDatum(dbctype));
 	new_record[Anum_pg_database_datistemplate - 1] = BoolGetDatum(dbistemplate);
+	new_record[Anum_pg_database_dathaslogintriggers - 1] = BoolGetDatum(false);
 	new_record[Anum_pg_database_datallowconn - 1] = BoolGetDatum(dballowconnections);
 	new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 	new_record[Anum_pg_database_datlastsysoid - 1] = ObjectIdGetDatum(src_lastsysoid);
@@ -1585,7 +1586,7 @@ AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel)
 		new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 		new_record_repl[Anum_pg_database_datconnlimit - 1] = true;
 	}
-
+	new_record[Anum_pg_database_dathaslogintriggers - 1] = BoolGetDatum(datform->dathaslogintriggers);
 	newtuple = heap_modify_tuple(tuple, RelationGetDescr(rel), new_record,
 								 new_record_nulls, new_record_repl);
 	CatalogTupleUpdate(rel, &tuple->t_self, newtuple);
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index df264329d8..34a95c7513 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -43,11 +44,15 @@
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
+bool login_triggers_enabled = false;
+
 typedef struct EventTriggerQueryState
 {
 	/* memory context for this state's objects */
@@ -101,6 +106,8 @@ static void EventTriggerInvoke(List *fn_oid_list, EventTriggerData *trigdata);
 static const char *stringify_grant_objtype(ObjectType objtype);
 static const char *stringify_adefprivs_objtype(ObjectType objtype);
 
+
+
 /*
  * Create an event trigger.
  */
@@ -130,6 +137,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "login") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -293,6 +301,26 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
 	CatalogTupleInsert(tgrel, tuple);
 	heap_freetuple(tuple);
 
+	if (strcmp(eventname, "login") == 0)
+	{
+		Form_pg_database db;
+		Relation pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+		/* Set dathaslogintriggers flag in pg_database */
+		tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+		db = (Form_pg_database) GETSTRUCT(tuple);
+		if (!db->dathaslogintriggers)
+		{
+			db->dathaslogintriggers = true;
+			CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+		}
+		else
+			CacheInvalidateRelcacheByTuple(tuple);
+		table_close(pg_db, RowExclusiveLock);
+		heap_freetuple(tuple);
+	}
+
 	/* Depend on owner. */
 	recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
 
@@ -562,6 +590,9 @@ EventTriggerCommonSetup(Node *parsetree,
 	ListCell   *lc;
 	List	   *runlist = NIL;
 
+	/* Get the command tag. */
+	tag = (event == EVT_Login) ? CMDTAG_LOGIN : CreateCommandTag(parsetree);
+
 	/*
 	 * We want the list of command tags for which this procedure is actually
 	 * invoked to match up exactly with the list that CREATE EVENT TRIGGER
@@ -577,22 +608,18 @@ EventTriggerCommonSetup(Node *parsetree,
 	 * relevant command tag.
 	 */
 #ifdef USE_ASSERT_CHECKING
+	if (event == EVT_DDLCommandStart ||
+		event == EVT_DDLCommandEnd ||
+		event == EVT_SQLDrop ||
+		event == EVT_Login)
 	{
-		CommandTag	dbgtag;
-
-		dbgtag = CreateCommandTag(parsetree);
-		if (event == EVT_DDLCommandStart ||
-			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
-		{
-			if (!command_tag_event_trigger_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
-		else if (event == EVT_TableRewrite)
-		{
-			if (!command_tag_table_rewrite_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
+		if (!command_tag_event_trigger_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
+	}
+	else if (event == EVT_TableRewrite)
+	{
+		if (!command_tag_table_rewrite_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
 	}
 #endif
 
@@ -601,9 +628,6 @@ EventTriggerCommonSetup(Node *parsetree,
 	if (cachelist == NIL)
 		return NIL;
 
-	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
-
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
 	 * memory context.  Once we start running the command triggers, or indeed
@@ -800,6 +824,111 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Return true if this database has login triggers, false otherwise.
+ */
+static bool
+DatabaseHasLoginTriggers(void)
+{
+	bool has_login_triggers;
+	HeapTuple tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+	has_login_triggers = ((Form_pg_database) GETSTRUCT(tuple))->dathaslogintriggers;
+	ReleaseSysCache(tuple);
+	return has_login_triggers;
+}
+
+/*
+ * Fire login triggers.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId))
+		return;
+
+	StartTransactionCommand();
+
+	if (DatabaseHasLoginTriggers())
+	{
+		runlist = EventTriggerCommonSetup(NULL,
+										  EVT_Login, "login",
+										  &trigdata);
+
+		if (runlist != NIL)
+		{
+			if(login_triggers_enabled) { 
+				/*
+				 * Make sure anything the main command did will be visible to the event
+				 * triggers.
+				 */
+				CommandCounterIncrement();
+	
+				/*
+				 * Event trigger execution may require an active snapshot.
+				 */
+				PushActiveSnapshot(GetTransactionSnapshot());
+	
+				/* Run the triggers. */
+				EventTriggerInvoke(runlist, &trigdata);
+	
+				/* Cleanup. */
+				list_free(runlist);
+	
+				PopActiveSnapshot();
+			}
+		}
+		else
+		{
+			/*
+			 * Runlist is empty: clear dathaslogintriggers flag
+			 */
+			Relation pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+			HeapTuple tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+			Form_pg_database db;
+
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+			db = (Form_pg_database) GETSTRUCT(tuple);
+			if (db->dathaslogintriggers)
+			{
+				/*
+				 * There can be a race condition: a login event trigger may have
+				 * been added after the pg_event_trigger table was scanned, and
+				 * we don't want to erroneously clear the dathaslogintriggers
+				 * flag in this case. To be sure that this hasn't happened,
+				 * repeat the scan under the pg_database table lock.
+				 */
+				AcceptInvalidationMessages();
+				runlist = EventTriggerCommonSetup(NULL,
+												  EVT_Login, "login",
+												  &trigdata);
+				if (runlist == NULL) /* list is still empty, so clear the flag */
+				{
+					db->dathaslogintriggers = false;
+					CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				}
+				else
+					CacheInvalidateRelcacheByTuple(tuple);
+			}
+			table_close(pg_db, RowExclusiveLock);
+			heap_freetuple(tuple);
+		}
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 0775abe35d..273611caf9 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -42,6 +42,7 @@
 #include "catalog/pg_type.h"
 #include "commands/async.h"
 #include "commands/prepare.h"
+#include "commands/event_trigger.h"
 #include "executor/spi.h"
 #include "jit/jit.h"
 #include "libpq/libpq.h"
@@ -4178,6 +4179,9 @@ PostgresMain(const char *dbname, const char *username)
 	initStringInfo(&row_description_buf);
 	MemoryContextSwitchTo(TopMemoryContext);
 
+	/* Fire any defined login triggers, if appropriate */
+	EventTriggerOnLogin();
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 460b720a65..b9b935f1cc 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "login") == 0)
+			event = EVT_Login;
 		else
 			continue;
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index e91d5a3cfd..a41c3cd7d5 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -129,7 +129,7 @@
  */
 #define REALTYPE_PRECISION 17
 
-/* XXX these should appear in other modules' header files */
+/* XXX these should appear in other modules or their header files */
 extern bool Log_disconnections;
 extern int	CommitDelay;
 extern int	CommitSiblings;
@@ -138,6 +138,7 @@ extern char *temp_tablespaces;
 extern bool ignore_checksum_failure;
 extern bool ignore_invalid_pages;
 extern bool synchronize_seqscans;
+extern bool login_triggers_enabled;
 
 #ifdef TRACE_SYNCSCAN
 extern bool trace_syncscan;
@@ -2119,6 +2120,15 @@ static struct config_bool ConfigureNamesBool[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"login_triggers", PGC_SIGHUP, CLIENT_CONN_OTHER,
+			gettext_noop("Enables login triggers."),
+		},
+		&login_triggers_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, false, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 1cbc9feeb6..28b8992cfc 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -89,6 +89,7 @@
 #client_connection_check_interval = 0	# time between checks for client
 					# disconnection while running queries;
 					# 0 for never
+#login_triggers = on
 
 # - Authentication -
 
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index b9635a95b6..bd7a21699a 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -2798,6 +2798,7 @@ dumpDatabase(Archive *fout)
 				i_datacl,
 				i_rdatacl,
 				i_datistemplate,
+				i_dathaslogintriggers,
 				i_datconnlimit,
 				i_tablespace;
 	CatalogId	dbCatId;
@@ -2810,6 +2811,7 @@ dumpDatabase(Archive *fout)
 			   *datacl,
 			   *rdatacl,
 			   *datistemplate,
+			   *dathaslogintriggers,
 			   *datconnlimit,
 			   *tablespace;
 	uint32		frozenxid,
@@ -2828,7 +2830,7 @@ dumpDatabase(Archive *fout)
 	 * (pg_init_privs) are not supported on databases, so this logic cannot
 	 * make use of buildACLQueries().
 	 */
-	if (fout->remoteVersion >= 90600)
+	if (fout->remoteVersion >= 150000)
 	{
 		appendPQExpBuffer(dbQry, "SELECT tableoid, oid, datname, "
 						  "(%s datdba) AS dba, "
@@ -2854,7 +2856,41 @@ dumpDatabase(Archive *fout)
 						  "       AS permp(orig_acl) "
 						  "     WHERE acl = orig_acl)) AS rdatacls) "
 						  " AS rdatacl, "
-						  "datistemplate, datconnlimit, "
+						  "datistemplate, datconnlimit, dathaslogintriggers, "
+						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
+						  "shobj_description(oid, 'pg_database') AS description "
+
+						  "FROM pg_database "
+						  "WHERE datname = current_database()",
+						  username_subquery);
+	}
+	else if (fout->remoteVersion >= 90600)
+	{
+		appendPQExpBuffer(dbQry, "SELECT tableoid, oid, datname, "
+						  "(%s datdba) AS dba, "
+						  "pg_encoding_to_char(encoding) AS encoding, "
+						  "datcollate, datctype, datfrozenxid, datminmxid, "
+						  "(SELECT array_agg(acl ORDER BY row_n) FROM "
+						  "  (SELECT acl, row_n FROM "
+						  "     unnest(coalesce(datacl,acldefault('d',datdba))) "
+						  "     WITH ORDINALITY AS perm(acl,row_n) "
+						  "   WHERE NOT EXISTS ( "
+						  "     SELECT 1 "
+						  "     FROM unnest(acldefault('d',datdba)) "
+						  "       AS init(init_acl) "
+						  "     WHERE acl = init_acl)) AS datacls) "
+						  " AS datacl, "
+						  "(SELECT array_agg(acl ORDER BY row_n) FROM "
+						  "  (SELECT acl, row_n FROM "
+						  "     unnest(acldefault('d',datdba)) "
+						  "     WITH ORDINALITY AS initp(acl,row_n) "
+						  "   WHERE NOT EXISTS ( "
+						  "     SELECT 1 "
+						  "     FROM unnest(coalesce(datacl,acldefault('d',datdba))) "
+						  "       AS permp(orig_acl) "
+						  "     WHERE acl = orig_acl)) AS rdatacls) "
+						  " AS rdatacl, "
+						  "datistemplate, datconnlimit, false as dathaslogintriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2868,7 +2904,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "datcollate, datctype, datfrozenxid, datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogintriggers"
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2882,7 +2918,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "datcollate, datctype, datfrozenxid, 0 AS datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogintriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2896,7 +2932,7 @@ dumpDatabase(Archive *fout)
 						  "(%s datdba) AS dba, "
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "NULL AS datcollate, NULL AS datctype, datfrozenxid, 0 AS datminmxid, "
-						  "datacl, '' as rdatacl, datistemplate, datconnlimit, "
+						  "datacl, '' as rdatacl, datistemplate, datconnlimit, false as dathaslogintriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
 						  "shobj_description(oid, 'pg_database') AS description "
 
@@ -2911,7 +2947,7 @@ dumpDatabase(Archive *fout)
 						  "pg_encoding_to_char(encoding) AS encoding, "
 						  "NULL AS datcollate, NULL AS datctype, datfrozenxid, 0 AS datminmxid, "
 						  "datacl, '' as rdatacl, datistemplate, "
-						  "-1 as datconnlimit, "
+						  "-1 as datconnlimit, false as dathaslogintriggers, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace "
 						  "FROM pg_database "
 						  "WHERE datname = current_database()",
@@ -2933,6 +2969,7 @@ dumpDatabase(Archive *fout)
 	i_rdatacl = PQfnumber(res, "rdatacl");
 	i_datistemplate = PQfnumber(res, "datistemplate");
 	i_datconnlimit = PQfnumber(res, "datconnlimit");
+	i_dathaslogintriggers = PQfnumber(res, "dathaslogintriggers");
 	i_tablespace = PQfnumber(res, "tablespace");
 
 	dbCatId.tableoid = atooid(PQgetvalue(res, 0, i_tableoid));
@@ -2947,6 +2984,7 @@ dumpDatabase(Archive *fout)
 	datacl = PQgetvalue(res, 0, i_datacl);
 	rdatacl = PQgetvalue(res, 0, i_rdatacl);
 	datistemplate = PQgetvalue(res, 0, i_datistemplate);
+	dathaslogintriggers = PQgetvalue(res, 0, i_dathaslogintriggers);
 	datconnlimit = PQgetvalue(res, 0, i_datconnlimit);
 	tablespace = PQgetvalue(res, 0, i_tablespace);
 
@@ -3120,6 +3158,8 @@ dumpDatabase(Archive *fout)
 		appendPQExpBufferStr(delQry, ";\n");
 	}
 
+	/* We do not restore the pg_database.dathaslogintriggers because it is set automatically on trigger creation */
+
 	/* Add database-specific SET options */
 	dumpDatabaseConfig(fout, creaQry, datname, dbCatId.oid);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 8e01f54500..17e1a058a1 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3099,7 +3099,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end",
+					  "login", "sql_drop");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat
index b8aa1364a0..2b804a47c3 100644
--- a/src/include/catalog/pg_database.dat
+++ b/src/include/catalog/pg_database.dat
@@ -15,7 +15,7 @@
 { oid => '1', oid_symbol => 'TemplateDbOid',
   descr => 'default template for new databases',
   datname => 'template1', encoding => 'ENCODING', datcollate => 'LC_COLLATE',
-  datctype => 'LC_CTYPE', datistemplate => 't', datallowconn => 't',
+  datctype => 'LC_CTYPE', datistemplate => 't', dathaslogintriggers => 'f', datallowconn => 't',
   datconnlimit => '-1', datlastsysoid => '0', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datacl => '_null_' },
 
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index 43f3beb6a3..a6e0309bb3 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -52,6 +52,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
 	/* new connections allowed? */
 	bool		datallowconn;
 
+	/* database has login triggers? */
+	bool		dathaslogintriggers;
+
 	/* max connections allowed (-1=no limit) */
 	int32		datconnlimit;
 
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index e765e67fd1..a9f9954597 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -54,6 +54,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnLogin(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 9ba24d4ca9..b0e7df3d81 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
+PG_CMDTAG(CMDTAG_LOGIN, "LOGIN", true, false, false)
 PG_CMDTAG(CMDTAG_MOVE, "MOVE", false, false, true)
 PG_CMDTAG(CMDTAG_NOTIFY, "NOTIFY", false, false, false)
 PG_CMDTAG(CMDTAG_PREPARE, "PREPARE", false, false, false)
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index 58ddb71cb1..29d73afe3d 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Login,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index c70c08e27b..59fa30a3ae 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,26 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+
+CREATE TABLE user_logins(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO user_logins (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 $node_primary->wait_for_catchup($node_standby_1, 'replay',
 	$node_primary->lsn('insert'));
@@ -371,6 +391,9 @@ sub replay_check
 
 replay_check();
 
+$node_standby_1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$node_standby_2->safe_psql('postgres', "SELECT 2", extra_params => [ '-U', 'regress_user', '-w' ]);
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 44d545de25..af3564d532 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -604,3 +604,84 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- On login triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE USER login_tester;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger SECURITY DEFINER AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON LOGIN EXECUTE PROCEDURE on_login_proc();
+ALTER  EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+NOTICE:  You are welcome!
+SELECT count(*) FROM user_logins;
+ count 
+-------
+     1
+(1 row)
+
+\set superuser :USER
+\c - login_tester
+NOTICE:  You are welcome!
+SELECT count(*) FROM user_logins;
+ count 
+-------
+     2
+(1 row)
+
+CREATE EVENT TRIGGER on_login_trigger ON LOGIN EXECUTE PROCEDURE on_login_proc(); -- non-superuser should not be able to!
+ERROR:  permission denied to create event trigger "on_login_trigger"
+HINT:  Must be superuser to create an event trigger.
+\c - :superuser
+NOTICE:  You are welcome!
+SELECT id, CASE WHEN who = :'superuser' THEN 'superuser' ELSE who END AS who FROM user_logins;
+ id |     who      
+----+--------------
+  1 | superuser
+  2 | login_tester
+  3 | superuser
+(3 rows)
+
+-- Turn off login triggers and make an erroneous one. We check that it will not be called.
+ALTER SYSTEM SET login_triggers = 'off';
+CREATE FUNCTION on_login_fail() RETURNS event_trigger SECURITY DEFINER AS $$
+BEGIN
+  RAISE EXCEPTION 'login trigger failure';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_fail ON LOGIN EXECUTE PROCEDURE on_login_fail();
+ALTER  EVENT TRIGGER on_login_fail ENABLE ALWAYS;
+SELECT pg_reload_conf();
+ pg_reload_conf 
+----------------
+ t
+(1 row)
+
+\c
+SELECT id, CASE WHEN who = :'superuser' THEN 'superuser' ELSE who END AS who FROM user_logins;
+ id |     who      
+----+--------------
+  1 | superuser
+  2 | login_tester
+  3 | superuser
+(3 rows)
+
+-- check dathaslogintriggers in system catalog
+SELECT dathaslogintriggers FROM pg_database WHERE datname= :'DBNAME';
+ dathaslogintriggers 
+---------------------
+ t
+(1 row)
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP EVENT TRIGGER on_login_fail;
+DROP FUNCTION on_login_proc();
+DROP FUNCTION on_login_fail();
+DROP USER login_tester;
+-- End
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 1446cf8cc8..b54b4d33ce 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -462,3 +462,48 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- On login triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE USER login_tester;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger SECURITY DEFINER AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON LOGIN EXECUTE PROCEDURE on_login_proc();
+ALTER  EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+SELECT count(*) FROM user_logins;
+\set superuser :USER
+\c - login_tester
+SELECT count(*) FROM user_logins;
+CREATE EVENT TRIGGER on_login_trigger ON LOGIN EXECUTE PROCEDURE on_login_proc(); -- non-superuser should not be able to!
+\c - :superuser
+SELECT id, CASE WHEN who = :'superuser' THEN 'superuser' ELSE who END AS who FROM user_logins;
+
+-- Turn off login triggers and make an erroneous one. We check that it will not be called.
+ALTER SYSTEM SET login_triggers = 'off';
+CREATE FUNCTION on_login_fail() RETURNS event_trigger SECURITY DEFINER AS $$
+BEGIN
+  RAISE EXCEPTION 'login trigger failure';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_fail ON LOGIN EXECUTE PROCEDURE on_login_fail();
+ALTER  EVENT TRIGGER on_login_fail ENABLE ALWAYS;
+SELECT pg_reload_conf();
+\c
+SELECT id, CASE WHEN who = :'superuser' THEN 'superuser' ELSE who END AS who FROM user_logins;
+-- check dathaslogintriggers in system catalog
+SELECT dathaslogintriggers FROM pg_database WHERE datname= :'DBNAME';
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP EVENT TRIGGER on_login_fail;
+DROP FUNCTION on_login_proc();
+DROP FUNCTION on_login_fail();
+DROP USER login_tester;
+-- End
+
#76Daniel Gustafsson
daniel@yesql.se
In reply to: Ivan Panchenko (#75)
Re: On login trigger: take three

On 3 Nov 2021, at 17:15, Ivan Panchenko <wao@mail.ru> wrote:
Среда, 29 сентября 2021, 15:14 +03:00 от Teodor Sigaev <teodor@sigaev.ru <x-msg://33/compose?To=teodor@sigaev.ru>>:
2 For logging purpose failing of trigger will cause impossibility to login, it
could be workarounded by catching error in trigger function, but it's not so
obvious for DBA.
If the trigger contains an error, nobody can login. The database is bricked.
Previous variant of the patch proposed to fix this with going to single-user mode.
For faster recovery I propose to have also a GUC variable to turn on/off the login triggers.
It should be 'on' by default.

As voiced earlier, I disagree with this and I dislike the idea of punching a
hole for circumventing infrastructure intended for auditing.

Use-cases for a login-trigger commonly involve audit trail logging, session
initialization etc. If the login trigger bricks the production database to the
extent that it needs to be restarted with the magic GUC, then it's highly
likely that you *don't* want regular connections to the database for the
duration of this. Any such connection won't be subject to what the trigger
does which seem counter to having the trigger in the first place. This means
that it's likely that the superuser fixing it will have to disable logins for
everyone else while fixing, and it quicly becomes messy.

With that in mind, I think single-user mode actually *helps* the users in this
case, and we avoid a hole punched which in worst case can be a vector for an
attack.

Maybe I'm overly paranoid, but adding a backdoor of sorts for a situation which
really shouldn't happen doesn't seem like a good idea.

some other issues:
3 It's easy to create security hole, see attachment where non-privileged user
can close access to database even for superuser.
Such cases can be avoided by careful design of the login triggers and related permissions. Added such note to the documentation.

If all login triggers are written carefully like that, we don't need the GUC to
disable them?

--
Daniel Gustafsson https://vmware.com/

#77Greg Nancarrow
gregn4422@gmail.com
In reply to: Daniel Gustafsson (#76)
Re: On login trigger: take three

On Thu, Nov 4, 2021 at 7:43 AM Daniel Gustafsson <daniel@yesql.se> wrote:

On 3 Nov 2021, at 17:15, Ivan Panchenko <wao@mail.ru> wrote:
Среда, 29 сентября 2021, 15:14 +03:00 от Teodor Sigaev <teodor@sigaev.ru

<x-msg://33/compose?To=teodor@sigaev.ru>>:

2 For logging purpose failing of trigger will cause impossibility to

login, it

could be workarounded by catching error in trigger function, but it's

not so

obvious for DBA.
If the trigger contains an error, nobody can login. The database is

bricked.

Previous variant of the patch proposed to fix this with going to

single-user mode.

For faster recovery I propose to have also a GUC variable to turn on/off

the login triggers.

It should be 'on' by default.

As voiced earlier, I disagree with this and I dislike the idea of punching
a
hole for circumventing infrastructure intended for auditing.

Use-cases for a login-trigger commonly involve audit trail logging, session
initialization etc. If the login trigger bricks the production database
to the
extent that it needs to be restarted with the magic GUC, then it's highly
likely that you *don't* want regular connections to the database for the
duration of this. Any such connection won't be subject to what the trigger
does which seem counter to having the trigger in the first place. This
means
that it's likely that the superuser fixing it will have to disable logins
for
everyone else while fixing, and it quicly becomes messy.

With that in mind, I think single-user mode actually *helps* the users in
this
case, and we avoid a hole punched which in worst case can be a vector for
an
attack.

Maybe I'm overly paranoid, but adding a backdoor of sorts for a situation
which
really shouldn't happen doesn't seem like a good idea.

+1
The arguments given are pretty convincing IMHO, and I agree that the
additions made in the v20 patch are not a good idea, and are not needed.

Regards,
Greg Nancarrow
Fujitsu Australia

#78Greg Nancarrow
gregn4422@gmail.com
In reply to: Greg Nancarrow (#77)
Re: On login trigger: take three

On Fri, Nov 5, 2021 at 3:03 PM Greg Nancarrow <gregn4422@gmail.com> wrote:

+1
The arguments given are pretty convincing IMHO, and I agree that the additions made in the v20 patch are not a good idea, and are not needed.

If there are no objections, I plan to reinstate the previous v19 patch
(as v21), perhaps with a few minor improvements and cleanups (e.g. SQL
capitalization) in the tests, as hinted at in the v20 patch, but no
new functionality.

Regards,
Greg Nancarrow
Fujitsu Australia

#79Daniel Gustafsson
daniel@yesql.se
In reply to: Greg Nancarrow (#78)
Re: On login trigger: take three

On 10 Nov 2021, at 08:12, Greg Nancarrow <gregn4422@gmail.com> wrote:

On Fri, Nov 5, 2021 at 3:03 PM Greg Nancarrow <gregn4422@gmail.com> wrote:

+1
The arguments given are pretty convincing IMHO, and I agree that the additions made in the v20 patch are not a good idea, and are not needed.

If there are no objections, I plan to reinstate the previous v19 patch
(as v21), perhaps with a few minor improvements and cleanups (e.g. SQL
capitalization) in the tests, as hinted at in the v20 patch, but no
new functionality.

No objections from me. Small nitpicks from the v19 patch:

+ This flag is used internally by Postgres and should not be manually changed by DBA or application.
This should be <productname>PostgreSQL</productname>.

+	 * There can be a race condition: a login event trigger may have
..
+	/* Fire any defined login triggers, if appropriate */
The patch say "login trigger" in most places, and "login event trigger" in a
few places.  We should settle for a single nomenclature, and I think "login
event trigger" is the best option.

--
Daniel Gustafsson https://vmware.com/

#80Pavel Stehule
pavel.stehule@gmail.com
In reply to: Daniel Gustafsson (#79)
Re: On login trigger: take three

st 10. 11. 2021 v 10:11 odesílatel Daniel Gustafsson <daniel@yesql.se>
napsal:

On 10 Nov 2021, at 08:12, Greg Nancarrow <gregn4422@gmail.com> wrote:

On Fri, Nov 5, 2021 at 3:03 PM Greg Nancarrow <gregn4422@gmail.com>

wrote:

+1
The arguments given are pretty convincing IMHO, and I agree that the

additions made in the v20 patch are not a good idea, and are not needed.

If there are no objections, I plan to reinstate the previous v19 patch
(as v21), perhaps with a few minor improvements and cleanups (e.g. SQL
capitalization) in the tests, as hinted at in the v20 patch, but no
new functionality.

No objections from me. Small nitpicks from the v19 patch:

ok,

Regards

Pavel

Show quoted text

+ This flag is used internally by Postgres and should not be
manually changed by DBA or application.
This should be <productname>PostgreSQL</productname>.

+        * There can be a race condition: a login event trigger may have
..
+       /* Fire any defined login triggers, if appropriate */
The patch say "login trigger" in most places, and "login event trigger" in
a
few places.  We should settle for a single nomenclature, and I think "login
event trigger" is the best option.

--
Daniel Gustafsson https://vmware.com/

#81Greg Nancarrow
gregn4422@gmail.com
In reply to: Daniel Gustafsson (#79)
1 attachment(s)
Re: On login trigger: take three

On Wed, Nov 10, 2021 at 8:11 PM Daniel Gustafsson <daniel@yesql.se> wrote:

If there are no objections, I plan to reinstate the previous v19 patch
(as v21), perhaps with a few minor improvements and cleanups (e.g. SQL
capitalization) in the tests, as hinted at in the v20 patch, but no
new functionality.

No objections from me. Small nitpicks from the v19 patch:

+ This flag is used internally by Postgres and should not be manually changed by DBA or application.
This should be <productname>PostgreSQL</productname>.

+        * There can be a race condition: a login event trigger may have
..
+       /* Fire any defined login triggers, if appropriate */
The patch say "login trigger" in most places, and "login event trigger" in a
few places.  We should settle for a single nomenclature, and I think "login
event trigger" is the best option.

I've attached an updated patch, that essentially reinstates the v19
patch, but with a few improvements such as:
- Updates to address nitpicks (Daniel Gustafsson)
- dathaslogintriggers -> dathasloginevttriggers flag rename (too
long?) and remove its restoration in pg_dump output, since it's not
needed (as in v20 patch)
- Some tidying of the updates to the event_trigger tests and
capitalization of the test SQL

Regards,
Greg Nancarrow
Fujitsu Australia

Attachments:

v21-0001-Add-a-new-login-event-and-login-event-trigger-support.patchapplication/octet-stream; name=v21-0001-Add-a-new-login-event-and-login-event-trigger-support.patchDownload
From 7a7d7387216737437421c2ee28f515930ef7aaf6 Mon Sep 17 00:00:00 2001
From: Greg Nancarrow <gregn4422@gmail.com>
Date: Thu, 11 Nov 2021 16:41:46 +1100
Subject: [PATCH v21] Add a new "login" event and login event trigger support.

The login event occurs when a user logs in to the system.

Author: Konstantin Knizhnik
Discussion: https://www.postgresql.org/message-id/flat/0d46d29f-4558-3af9-9c85-7774e14a7709%40postgrespro.ru
---
 doc/src/sgml/bki.sgml                       |   2 +-
 doc/src/sgml/catalogs.sgml                  |  11 ++
 doc/src/sgml/ecpg.sgml                      |   2 +
 doc/src/sgml/event-trigger.sgml             |  70 ++++++++-
 src/backend/commands/dbcommands.c           |   3 +-
 src/backend/commands/event_trigger.c        | 162 +++++++++++++++++---
 src/backend/tcop/postgres.c                 |   4 +
 src/backend/utils/cache/evtcache.c          |   2 +
 src/bin/pg_dump/pg_dump.c                   |   5 +
 src/bin/psql/tab-complete.c                 |   3 +-
 src/include/catalog/pg_database.dat         |   2 +-
 src/include/catalog/pg_database.h           |   3 +
 src/include/commands/event_trigger.h        |   1 +
 src/include/tcop/cmdtaglist.h               |   1 +
 src/include/utils/evtcache.h                |   3 +-
 src/test/recovery/t/001_stream_rep.pl       |  23 +++
 src/test/regress/expected/event_trigger.out |  44 ++++++
 src/test/regress/sql/event_trigger.sql      |  28 ++++
 18 files changed, 345 insertions(+), 24 deletions(-)

diff --git a/doc/src/sgml/bki.sgml b/doc/src/sgml/bki.sgml
index f900a01b82..cb9903740f 100644
--- a/doc/src/sgml/bki.sgml
+++ b/doc/src/sgml/bki.sgml
@@ -182,7 +182,7 @@
 { oid =&gt; '1', oid_symbol =&gt; 'TemplateDbOid',
   descr =&gt; 'database\'s default template',
   datname =&gt; 'template1', encoding =&gt; 'ENCODING', datcollate =&gt; 'LC_COLLATE',
-  datctype =&gt; 'LC_CTYPE', datistemplate =&gt; 't', datallowconn =&gt; 't',
+  datctype =&gt; 'LC_CTYPE', datistemplate =&gt; 't', datallowconn =&gt; 't', dathasloginevttriggers =&gt; 'f',
   datconnlimit =&gt; '-1', datlastsysoid =&gt; '0', datfrozenxid =&gt; '0',
   datminmxid =&gt; '1', dattablespace =&gt; 'pg_default', datacl =&gt; '_null_' },
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index c1d11be73f..74457f9b31 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2979,6 +2979,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>dathasloginevttriggers</structfield> <type>bool</type>
+      </para>
+      <para>
+        Indicates that there are login event triggers defined for this database.
+        This flag is used to avoid extra lookups on the pg_event_trigger table during each backend startup.
+        This flag is used internally by <productname>PostgreSQL</productname> and should not be manually changed by DBA or application.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>datconnlimit</structfield> <type>int4</type>
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index 04d4a6bdb2..81516c089c 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -4731,6 +4731,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = t (type: 1)
 datallowconn = t (type: 1)
+dathasloginevttriggers = f (type: 1)
 datconnlimit = -1 (type: 5)
 datlastsysoid = 11510 (type: 1)
 datfrozenxid = 379 (type: 1)
@@ -4756,6 +4757,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = f (type: 1)
 datallowconn = t (type: 1)
+dathasloginevttriggers = f (type: 1)
 datconnlimit = -1 (type: 5)
 datlastsysoid = 11510 (type: 1)
 datfrozenxid = 379 (type: 1)
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 60366a950e..ec35ddfad8 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>login</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,16 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>login</literal> event occurs when a user logs in to the
+     system.
+     Any bugs in a trigger procedure for this event may prevent successful
+     login to the system. Such bugs may be fixed after first restarting the
+     system in single-user mode (as event triggers are disabled in this mode).
+     See the <xref linkend="app-postgres"/> reference page for details about
+     using single-user mode.
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1140,7 +1151,7 @@ typedef struct EventTriggerData
   </sect1>
 
   <sect1 id="event-trigger-example">
-   <title>A Complete Event Trigger Example</title>
+   <title>A C language Event Trigger Example</title>
 
    <para>
     Here is a very simple example of an event trigger function written in C.
@@ -1280,6 +1291,63 @@ $$;
 CREATE EVENT TRIGGER no_rewrite_allowed
                   ON table_rewrite
    EXECUTE FUNCTION no_rewrite();
+</programlisting>
+   </para>
+ </sect1>
+
+  <sect1 id="event-trigger-database-login-example">
+   <title>A Database Login Event Trigger Example</title>
+
+   <para>
+    The event trigger on the <literal>login</literal> event can be
+    useful for logging user logins, for verifying the connection and
+    assigning roles according to current circumstances, or for some session data
+    initialization.
+   </para>
+
+   <para>
+    The following example demonstrates these options.
+<programlisting>
+-- create test tables and roles
+CREATE TABLE user_login_log (
+  "user" text,
+  "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+  RETURNS event_trigger SECURITY DEFINER
+  LANGUAGE plpgsql AS
+$$
+DECLARE
+  hour integer = EXTRACT('hour' FROM current_time);
+BEGIN
+-- 1) Assign some roles
+IF hour BETWEEN 8 AND 20 THEN         -- at daytime grant the day_worker role
+  EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT    day_worker TO '   || quote_ident(session_user);
+ELSIF hour BETWEEN 2 AND 4 THEN
+  RAISE EXCEPTION 'Login forbidden';  -- do not allow to login these hours
+ELSE                                  -- at other time grant the night_worker role
+  EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT  night_worker TO ' || quote_ident(session_user);
+END IF;
+
+-- 2) Initialize some user session data
+CREATE TEMP TABLE session_storage (x float, y integer);
+
+-- 3) Log the connection time
+INSERT INTO user_login_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+  ON login
+  EXECUTE FUNCTION init_session();
 </programlisting>
    </para>
  </sect1>
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 029fab48df..d62507370c 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -530,6 +530,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	new_record[Anum_pg_database_datctype - 1] =
 		DirectFunctionCall1(namein, CStringGetDatum(dbctype));
 	new_record[Anum_pg_database_datistemplate - 1] = BoolGetDatum(dbistemplate);
+	new_record[Anum_pg_database_dathasloginevttriggers - 1] = BoolGetDatum(false);
 	new_record[Anum_pg_database_datallowconn - 1] = BoolGetDatum(dballowconnections);
 	new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 	new_record[Anum_pg_database_datlastsysoid - 1] = ObjectIdGetDatum(src_lastsysoid);
@@ -1585,7 +1586,7 @@ AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel)
 		new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 		new_record_repl[Anum_pg_database_datconnlimit - 1] = true;
 	}
-
+	new_record[Anum_pg_database_dathasloginevttriggers - 1] = BoolGetDatum(datform->dathasloginevttriggers);
 	newtuple = heap_modify_tuple(tuple, RelationGetDescr(rel), new_record,
 								 new_record_nulls, new_record_repl);
 	CatalogTupleUpdate(rel, &tuple->t_self, newtuple);
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index df264329d8..9c48fda6bb 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -43,9 +44,11 @@
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 typedef struct EventTriggerQueryState
@@ -130,6 +133,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "login") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -293,6 +297,27 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
 	CatalogTupleInsert(tgrel, tuple);
 	heap_freetuple(tuple);
 
+	if (strcmp(eventname, "login") == 0)
+	{
+		Form_pg_database db;
+		Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+
+		/* Set dathasloginevttriggers flag in pg_database */
+		tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+		db = (Form_pg_database) GETSTRUCT(tuple);
+		if (!db->dathasloginevttriggers)
+		{
+			db->dathasloginevttriggers = true;
+			CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+		}
+		else
+			CacheInvalidateRelcacheByTuple(tuple);
+		table_close(pg_db, RowExclusiveLock);
+		heap_freetuple(tuple);
+	}
+
 	/* Depend on owner. */
 	recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
 
@@ -562,6 +587,9 @@ EventTriggerCommonSetup(Node *parsetree,
 	ListCell   *lc;
 	List	   *runlist = NIL;
 
+	/* Get the command tag. */
+	tag = (event == EVT_Login) ? CMDTAG_LOGIN : CreateCommandTag(parsetree);
+
 	/*
 	 * We want the list of command tags for which this procedure is actually
 	 * invoked to match up exactly with the list that CREATE EVENT TRIGGER
@@ -577,22 +605,18 @@ EventTriggerCommonSetup(Node *parsetree,
 	 * relevant command tag.
 	 */
 #ifdef USE_ASSERT_CHECKING
+	if (event == EVT_DDLCommandStart ||
+		event == EVT_DDLCommandEnd ||
+		event == EVT_SQLDrop ||
+		event == EVT_Login)
 	{
-		CommandTag	dbgtag;
-
-		dbgtag = CreateCommandTag(parsetree);
-		if (event == EVT_DDLCommandStart ||
-			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
-		{
-			if (!command_tag_event_trigger_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
-		else if (event == EVT_TableRewrite)
-		{
-			if (!command_tag_table_rewrite_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
+		if (!command_tag_event_trigger_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
+	}
+	else if (event == EVT_TableRewrite)
+	{
+		if (!command_tag_table_rewrite_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
 	}
 #endif
 
@@ -601,9 +625,6 @@ EventTriggerCommonSetup(Node *parsetree,
 	if (cachelist == NIL)
 		return NIL;
 
-	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
-
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
 	 * memory context.  Once we start running the command triggers, or indeed
@@ -800,6 +821,111 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Return true if this database has login event triggers, false otherwise.
+ */
+static bool
+DatabaseHasLoginEventTriggers(void)
+{
+	bool		has_login_event_triggers;
+	HeapTuple	tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+	has_login_event_triggers = ((Form_pg_database) GETSTRUCT(tuple))->dathasloginevttriggers;
+	ReleaseSysCache(tuple);
+	return has_login_event_triggers;
+}
+
+/*
+ * Fire login event triggers.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId))
+		return;
+
+	StartTransactionCommand();
+
+	if (DatabaseHasLoginEventTriggers())
+	{
+		runlist = EventTriggerCommonSetup(NULL,
+										  EVT_Login, "login",
+										  &trigdata);
+
+		if (runlist != NIL)
+		{
+			/*
+			 * Make sure anything the main command did will be visible to the
+			 * event triggers.
+			 */
+			CommandCounterIncrement();
+
+			/*
+			 * Event trigger execution may require an active snapshot.
+			 */
+			PushActiveSnapshot(GetTransactionSnapshot());
+
+			/* Run the triggers. */
+			EventTriggerInvoke(runlist, &trigdata);
+
+			/* Cleanup. */
+			list_free(runlist);
+
+			PopActiveSnapshot();
+		}
+		else
+		{
+			/*
+			 * Runlist is empty: clear dathasloginevttriggers flag
+			 */
+			Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+			HeapTuple	tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+			Form_pg_database db;
+
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+			db = (Form_pg_database) GETSTRUCT(tuple);
+			if (db->dathasloginevttriggers)
+			{
+				/*
+				 * There can be a race condition: a login event trigger may
+				 * have been added after the pg_event_trigger table was
+				 * scanned, and we don't want to erroneously clear the
+				 * dathasloginevttriggers flag in this case. To be sure that
+				 * this hasn't happened, repeat the scan under the pg_database
+				 * table lock.
+				 */
+				AcceptInvalidationMessages();
+				runlist = EventTriggerCommonSetup(NULL,
+												  EVT_Login, "login",
+												  &trigdata);
+				if (runlist == NULL)	/* list is still empty, so clear the
+										 * flag */
+				{
+					db->dathasloginevttriggers = false;
+					CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				}
+				else
+					CacheInvalidateRelcacheByTuple(tuple);
+			}
+			table_close(pg_db, RowExclusiveLock);
+			heap_freetuple(tuple);
+		}
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 0775abe35d..33391686f3 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -42,6 +42,7 @@
 #include "catalog/pg_type.h"
 #include "commands/async.h"
 #include "commands/prepare.h"
+#include "commands/event_trigger.h"
 #include "executor/spi.h"
 #include "jit/jit.h"
 #include "libpq/libpq.h"
@@ -4178,6 +4179,9 @@ PostgresMain(const char *dbname, const char *username)
 	initStringInfo(&row_description_buf);
 	MemoryContextSwitchTo(TopMemoryContext);
 
+	/* Fire any defined login event triggers, if appropriate */
+	EventTriggerOnLogin();
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 460b720a65..b9b935f1cc 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "login") == 0)
+			event = EVT_Login;
 		else
 			continue;
 
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 7e98371d25..91b3f3f016 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3120,6 +3120,11 @@ dumpDatabase(Archive *fout)
 		appendPQExpBufferStr(delQry, ";\n");
 	}
 
+	/*
+	 * We do not restore pg_database.dathasloginevttriggers because it is set
+	 * automatically on login event trigger creation.
+	 */
+
 	/* Add database-specific SET options */
 	dumpDatabaseConfig(fout, creaQry, datname, dbCatId.oid);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 4f724e4428..e5a9a80895 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3148,7 +3148,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end",
+					  "login", "sql_drop");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat
index b8aa1364a0..cfc112a709 100644
--- a/src/include/catalog/pg_database.dat
+++ b/src/include/catalog/pg_database.dat
@@ -15,7 +15,7 @@
 { oid => '1', oid_symbol => 'TemplateDbOid',
   descr => 'default template for new databases',
   datname => 'template1', encoding => 'ENCODING', datcollate => 'LC_COLLATE',
-  datctype => 'LC_CTYPE', datistemplate => 't', datallowconn => 't',
+  datctype => 'LC_CTYPE', datistemplate => 't', dathasloginevttriggers => 'f', datallowconn => 't',
   datconnlimit => '-1', datlastsysoid => '0', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datacl => '_null_' },
 
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index 43f3beb6a3..f19bbfa4c8 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -52,6 +52,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
 	/* new connections allowed? */
 	bool		datallowconn;
 
+	/* database has login event triggers? */
+	bool		dathasloginevttriggers;
+
 	/* max connections allowed (-1=no limit) */
 	int32		datconnlimit;
 
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index e765e67fd1..a9f9954597 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -54,6 +54,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnLogin(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 9ba24d4ca9..b0e7df3d81 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
+PG_CMDTAG(CMDTAG_LOGIN, "LOGIN", true, false, false)
 PG_CMDTAG(CMDTAG_MOVE, "MOVE", false, false, true)
 PG_CMDTAG(CMDTAG_NOTIFY, "NOTIFY", false, false, false)
 PG_CMDTAG(CMDTAG_PREPARE, "PREPARE", false, false, false)
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index 58ddb71cb1..29d73afe3d 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Login,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index c70c08e27b..59fa30a3ae 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,26 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+
+CREATE TABLE user_logins(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO user_logins (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 $node_primary->wait_for_catchup($node_standby_1, 'replay',
 	$node_primary->lsn('insert'));
@@ -371,6 +391,9 @@ sub replay_check
 
 replay_check();
 
+$node_standby_1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$node_standby_2->safe_psql('postgres', "SELECT 2", extra_params => [ '-U', 'regress_user', '-w' ]);
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 44d545de25..23cdd94e06 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -604,3 +604,47 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- Login event triggers
+\set test_user :USER
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE USER login_tester;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger SECURITY DEFINER AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c - login_tester
+NOTICE:  You are welcome!
+SELECT * FROM user_logins;
+ id |     who      
+----+--------------
+  1 | login_tester
+(1 row)
+
+\c - login_tester
+NOTICE:  You are welcome!
+SELECT * FROM user_logins;
+ id |     who      
+----+--------------
+  1 | login_tester
+  2 | login_tester
+(2 rows)
+
+\c - :test_user
+NOTICE:  You are welcome!
+-- Check dathasloginevttriggers in system catalog
+SELECT dathasloginevttriggers FROM pg_database WHERE datname= :'DBNAME';
+ dathasloginevttriggers 
+------------------------
+ t
+(1 row)
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
+DROP USER login_tester;
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 1446cf8cc8..6a9022c962 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -462,3 +462,31 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- Login event triggers
+\set test_user :USER
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE USER login_tester;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger SECURITY DEFINER AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c - login_tester
+SELECT * FROM user_logins;
+\c - login_tester
+SELECT * FROM user_logins;
+
+\c - :test_user
+-- Check dathasloginevttriggers in system catalog
+SELECT dathasloginevttriggers FROM pg_database WHERE datname= :'DBNAME';
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
+DROP USER login_tester;
-- 
2.27.0

#82Daniel Gustafsson
daniel@yesql.se
In reply to: Greg Nancarrow (#81)
Re: On login trigger: take three

On 11 Nov 2021, at 07:37, Greg Nancarrow <gregn4422@gmail.com> wrote:

I've attached an updated patch, that essentially reinstates the v19
patch

Thanks! I've only skimmed it so far but it looks good, will do a more thorough
review soon.

+ This flag is used to avoid extra lookups on the pg_event_trigger table during each backend startup.
This should be <structname>pg_event_trigger</structname>. Sorry, missed that
one at that last read-through.

- dathaslogintriggers -> dathasloginevttriggers flag rename (too
long?)

I'm not crazy about this name, "evt" is commonly the abbreviation of "event
trigger" (used in evtcache.c etc) so "dathasloginevt" would IMO be better.
That being said, that's still not a very readable name, maybe someone else has
an even better suggestion?

--
Daniel Gustafsson https://vmware.com/

#83Greg Nancarrow
gregn4422@gmail.com
In reply to: Daniel Gustafsson (#82)
Re: On login trigger: take three

On Thu, Nov 11, 2021 at 8:56 PM Daniel Gustafsson <daniel@yesql.se> wrote:

+ This flag is used to avoid extra lookups on the pg_event_trigger table during each backend startup.
This should be <structname>pg_event_trigger</structname>. Sorry, missed that
one at that last read-through.

Thanks, noted.

- dathaslogintriggers -> dathasloginevttriggers flag rename (too
long?)

I'm not crazy about this name, "evt" is commonly the abbreviation of "event
trigger" (used in evtcache.c etc) so "dathasloginevt" would IMO be better.
That being said, that's still not a very readable name, maybe someone else has
an even better suggestion?

Yes you're right, in this case I was wrongly treating "evt" as an
abbreviation for "event".
I agree "dathasloginevt" would be better.

Regards,
Greg Nancarrow
Fujitsu Australia

#84Greg Nancarrow
gregn4422@gmail.com
In reply to: Daniel Gustafsson (#82)
1 attachment(s)
Re: On login trigger: take three

On Thu, Nov 11, 2021 at 8:56 PM Daniel Gustafsson <daniel@yesql.se> wrote:

+ This flag is used to avoid extra lookups on the pg_event_trigger table during each backend startup.
This should be <structname>pg_event_trigger</structname>. Sorry, missed that
one at that last read-through.

Fixed.

- dathaslogintriggers -> dathasloginevttriggers flag rename (too
long?)

I'm not crazy about this name, "evt" is commonly the abbreviation of "event
trigger" (used in evtcache.c etc) so "dathasloginevt" would IMO be better.
That being said, that's still not a very readable name, maybe someone else has
an even better suggestion?

Changed to "dathasloginevt", as suggested.

I've attached an updated patch with these changes.
I also noticed one of the Windows-based cfbots was failing with an
"SSPI authentication failed for user" error, so I updated the test
code for that.

Regards,
Greg Nancarrow
Fujitsu Australia

Attachments:

v22-0001-Add-a-new-login-event-and-login-event-trigger-support.patchapplication/octet-stream; name=v22-0001-Add-a-new-login-event-and-login-event-trigger-support.patchDownload
From 64f12d254d61669455ee053bc3f3bfb9e48410a4 Mon Sep 17 00:00:00 2001
From: Greg Nancarrow <gregn4422@gmail.com>
Date: Tue, 16 Nov 2021 15:56:33 +1100
Subject: [PATCH v22] Add a new "login" event and login event trigger support.

The login event occurs when a user logs in to the system.

Author: Konstantin Knizhnik
Discussion: https://www.postgresql.org/message-id/flat/0d46d29f-4558-3af9-9c85-7774e14a7709%40postgrespro.ru
---
 doc/src/sgml/bki.sgml                       |   2 +-
 doc/src/sgml/catalogs.sgml                  |  11 ++
 doc/src/sgml/ecpg.sgml                      |   2 +
 doc/src/sgml/event-trigger.sgml             |  70 ++++++++-
 src/backend/commands/dbcommands.c           |   3 +-
 src/backend/commands/event_trigger.c        | 162 +++++++++++++++++---
 src/backend/tcop/postgres.c                 |   4 +
 src/backend/utils/cache/evtcache.c          |   2 +
 src/bin/pg_dump/pg_dump.c                   |   5 +
 src/bin/psql/tab-complete.c                 |   3 +-
 src/include/catalog/pg_database.dat         |   2 +-
 src/include/catalog/pg_database.h           |   3 +
 src/include/commands/event_trigger.h        |   1 +
 src/include/tcop/cmdtaglist.h               |   1 +
 src/include/utils/evtcache.h                |   3 +-
 src/test/recovery/t/001_stream_rep.pl       |  23 +++
 src/test/regress/expected/event_trigger.out |  38 +++++
 src/test/regress/sql/event_trigger.sql      |  24 +++
 18 files changed, 335 insertions(+), 24 deletions(-)

diff --git a/doc/src/sgml/bki.sgml b/doc/src/sgml/bki.sgml
index f900a01b82..c7627fe644 100644
--- a/doc/src/sgml/bki.sgml
+++ b/doc/src/sgml/bki.sgml
@@ -182,7 +182,7 @@
 { oid =&gt; '1', oid_symbol =&gt; 'TemplateDbOid',
   descr =&gt; 'database\'s default template',
   datname =&gt; 'template1', encoding =&gt; 'ENCODING', datcollate =&gt; 'LC_COLLATE',
-  datctype =&gt; 'LC_CTYPE', datistemplate =&gt; 't', datallowconn =&gt; 't',
+  datctype =&gt; 'LC_CTYPE', datistemplate =&gt; 't', datallowconn =&gt; 't', dathasloginevt =&gt; 'f',
   datconnlimit =&gt; '-1', datlastsysoid =&gt; '0', datfrozenxid =&gt; '0',
   datminmxid =&gt; '1', dattablespace =&gt; 'pg_default', datacl =&gt; '_null_' },
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index c1d11be73f..9e37596752 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2979,6 +2979,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>dathasloginevt</structfield> <type>bool</type>
+      </para>
+      <para>
+        Indicates that there are login event triggers defined for this database.
+        This flag is used to avoid extra lookups on the <structname>pg_event_trigger</structname> table during each backend startup.
+        This flag is used internally by <productname>PostgreSQL</productname> and should not be manually changed by DBA or application.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>datconnlimit</structfield> <type>int4</type>
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index 04d4a6bdb2..2db3cbbc11 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -4731,6 +4731,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = t (type: 1)
 datallowconn = t (type: 1)
+dathasloginevt = f (type: 1)
 datconnlimit = -1 (type: 5)
 datlastsysoid = 11510 (type: 1)
 datfrozenxid = 379 (type: 1)
@@ -4756,6 +4757,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = f (type: 1)
 datallowconn = t (type: 1)
+dathasloginevt = f (type: 1)
 datconnlimit = -1 (type: 5)
 datlastsysoid = 11510 (type: 1)
 datfrozenxid = 379 (type: 1)
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 60366a950e..ec35ddfad8 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>login</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,16 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>login</literal> event occurs when a user logs in to the
+     system.
+     Any bugs in a trigger procedure for this event may prevent successful
+     login to the system. Such bugs may be fixed after first restarting the
+     system in single-user mode (as event triggers are disabled in this mode).
+     See the <xref linkend="app-postgres"/> reference page for details about
+     using single-user mode.
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1140,7 +1151,7 @@ typedef struct EventTriggerData
   </sect1>
 
   <sect1 id="event-trigger-example">
-   <title>A Complete Event Trigger Example</title>
+   <title>A C language Event Trigger Example</title>
 
    <para>
     Here is a very simple example of an event trigger function written in C.
@@ -1280,6 +1291,63 @@ $$;
 CREATE EVENT TRIGGER no_rewrite_allowed
                   ON table_rewrite
    EXECUTE FUNCTION no_rewrite();
+</programlisting>
+   </para>
+ </sect1>
+
+  <sect1 id="event-trigger-database-login-example">
+   <title>A Database Login Event Trigger Example</title>
+
+   <para>
+    The event trigger on the <literal>login</literal> event can be
+    useful for logging user logins, for verifying the connection and
+    assigning roles according to current circumstances, or for some session data
+    initialization.
+   </para>
+
+   <para>
+    The following example demonstrates these options.
+<programlisting>
+-- create test tables and roles
+CREATE TABLE user_login_log (
+  "user" text,
+  "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+  RETURNS event_trigger SECURITY DEFINER
+  LANGUAGE plpgsql AS
+$$
+DECLARE
+  hour integer = EXTRACT('hour' FROM current_time);
+BEGIN
+-- 1) Assign some roles
+IF hour BETWEEN 8 AND 20 THEN         -- at daytime grant the day_worker role
+  EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT    day_worker TO '   || quote_ident(session_user);
+ELSIF hour BETWEEN 2 AND 4 THEN
+  RAISE EXCEPTION 'Login forbidden';  -- do not allow to login these hours
+ELSE                                  -- at other time grant the night_worker role
+  EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT  night_worker TO ' || quote_ident(session_user);
+END IF;
+
+-- 2) Initialize some user session data
+CREATE TEMP TABLE session_storage (x float, y integer);
+
+-- 3) Log the connection time
+INSERT INTO user_login_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+  ON login
+  EXECUTE FUNCTION init_session();
 </programlisting>
    </para>
  </sect1>
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 029fab48df..7bb9b8627c 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -530,6 +530,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	new_record[Anum_pg_database_datctype - 1] =
 		DirectFunctionCall1(namein, CStringGetDatum(dbctype));
 	new_record[Anum_pg_database_datistemplate - 1] = BoolGetDatum(dbistemplate);
+	new_record[Anum_pg_database_dathasloginevt - 1] = BoolGetDatum(false);
 	new_record[Anum_pg_database_datallowconn - 1] = BoolGetDatum(dballowconnections);
 	new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 	new_record[Anum_pg_database_datlastsysoid - 1] = ObjectIdGetDatum(src_lastsysoid);
@@ -1585,7 +1586,7 @@ AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel)
 		new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 		new_record_repl[Anum_pg_database_datconnlimit - 1] = true;
 	}
-
+	new_record[Anum_pg_database_dathasloginevt - 1] = BoolGetDatum(datform->dathasloginevt);
 	newtuple = heap_modify_tuple(tuple, RelationGetDescr(rel), new_record,
 								 new_record_nulls, new_record_repl);
 	CatalogTupleUpdate(rel, &tuple->t_self, newtuple);
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index df264329d8..7a08480bf4 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -43,9 +44,11 @@
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 typedef struct EventTriggerQueryState
@@ -130,6 +133,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "login") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -293,6 +297,27 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
 	CatalogTupleInsert(tgrel, tuple);
 	heap_freetuple(tuple);
 
+	if (strcmp(eventname, "login") == 0)
+	{
+		Form_pg_database db;
+		Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+
+		/* Set dathasloginevt flag in pg_database */
+		tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+		db = (Form_pg_database) GETSTRUCT(tuple);
+		if (!db->dathasloginevt)
+		{
+			db->dathasloginevt = true;
+			CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+		}
+		else
+			CacheInvalidateRelcacheByTuple(tuple);
+		table_close(pg_db, RowExclusiveLock);
+		heap_freetuple(tuple);
+	}
+
 	/* Depend on owner. */
 	recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
 
@@ -562,6 +587,9 @@ EventTriggerCommonSetup(Node *parsetree,
 	ListCell   *lc;
 	List	   *runlist = NIL;
 
+	/* Get the command tag. */
+	tag = (event == EVT_Login) ? CMDTAG_LOGIN : CreateCommandTag(parsetree);
+
 	/*
 	 * We want the list of command tags for which this procedure is actually
 	 * invoked to match up exactly with the list that CREATE EVENT TRIGGER
@@ -577,22 +605,18 @@ EventTriggerCommonSetup(Node *parsetree,
 	 * relevant command tag.
 	 */
 #ifdef USE_ASSERT_CHECKING
+	if (event == EVT_DDLCommandStart ||
+		event == EVT_DDLCommandEnd ||
+		event == EVT_SQLDrop ||
+		event == EVT_Login)
 	{
-		CommandTag	dbgtag;
-
-		dbgtag = CreateCommandTag(parsetree);
-		if (event == EVT_DDLCommandStart ||
-			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
-		{
-			if (!command_tag_event_trigger_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
-		else if (event == EVT_TableRewrite)
-		{
-			if (!command_tag_table_rewrite_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
+		if (!command_tag_event_trigger_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
+	}
+	else if (event == EVT_TableRewrite)
+	{
+		if (!command_tag_table_rewrite_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
 	}
 #endif
 
@@ -601,9 +625,6 @@ EventTriggerCommonSetup(Node *parsetree,
 	if (cachelist == NIL)
 		return NIL;
 
-	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
-
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
 	 * memory context.  Once we start running the command triggers, or indeed
@@ -800,6 +821,111 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Return true if this database has login event triggers, false otherwise.
+ */
+static bool
+DatabaseHasLoginEventTriggers(void)
+{
+	bool		has_login_event_triggers;
+	HeapTuple	tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+	has_login_event_triggers = ((Form_pg_database) GETSTRUCT(tuple))->dathasloginevt;
+	ReleaseSysCache(tuple);
+	return has_login_event_triggers;
+}
+
+/*
+ * Fire login event triggers.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId))
+		return;
+
+	StartTransactionCommand();
+
+	if (DatabaseHasLoginEventTriggers())
+	{
+		runlist = EventTriggerCommonSetup(NULL,
+										  EVT_Login, "login",
+										  &trigdata);
+
+		if (runlist != NIL)
+		{
+			/*
+			 * Make sure anything the main command did will be visible to the
+			 * event triggers.
+			 */
+			CommandCounterIncrement();
+
+			/*
+			 * Event trigger execution may require an active snapshot.
+			 */
+			PushActiveSnapshot(GetTransactionSnapshot());
+
+			/* Run the triggers. */
+			EventTriggerInvoke(runlist, &trigdata);
+
+			/* Cleanup. */
+			list_free(runlist);
+
+			PopActiveSnapshot();
+		}
+		else
+		{
+			/*
+			 * Runlist is empty: clear dathasloginevt flag
+			 */
+			Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+			HeapTuple	tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+			Form_pg_database db;
+
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+			db = (Form_pg_database) GETSTRUCT(tuple);
+			if (db->dathasloginevt)
+			{
+				/*
+				 * There can be a race condition: a login event trigger may
+				 * have been added after the pg_event_trigger table was
+				 * scanned, and we don't want to erroneously clear the
+				 * dathasloginevt flag in this case. To be sure that this
+				 * hasn't happened, repeat the scan under the pg_database
+				 * table lock.
+				 */
+				AcceptInvalidationMessages();
+				runlist = EventTriggerCommonSetup(NULL,
+												  EVT_Login, "login",
+												  &trigdata);
+				if (runlist == NULL)	/* list is still empty, so clear the
+										 * flag */
+				{
+					db->dathasloginevt = false;
+					CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				}
+				else
+					CacheInvalidateRelcacheByTuple(tuple);
+			}
+			table_close(pg_db, RowExclusiveLock);
+			heap_freetuple(tuple);
+		}
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 0775abe35d..33391686f3 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -42,6 +42,7 @@
 #include "catalog/pg_type.h"
 #include "commands/async.h"
 #include "commands/prepare.h"
+#include "commands/event_trigger.h"
 #include "executor/spi.h"
 #include "jit/jit.h"
 #include "libpq/libpq.h"
@@ -4178,6 +4179,9 @@ PostgresMain(const char *dbname, const char *username)
 	initStringInfo(&row_description_buf);
 	MemoryContextSwitchTo(TopMemoryContext);
 
+	/* Fire any defined login event triggers, if appropriate */
+	EventTriggerOnLogin();
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 460b720a65..b9b935f1cc 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "login") == 0)
+			event = EVT_Login;
 		else
 			continue;
 
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 7e98371d25..fc2f7cb245 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3120,6 +3120,11 @@ dumpDatabase(Archive *fout)
 		appendPQExpBufferStr(delQry, ";\n");
 	}
 
+	/*
+	 * We do not restore pg_database.dathasloginevt because it is set
+	 * automatically on login event trigger creation.
+	 */
+
 	/* Add database-specific SET options */
 	dumpDatabaseConfig(fout, creaQry, datname, dbCatId.oid);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 4f724e4428..e5a9a80895 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3148,7 +3148,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end",
+					  "login", "sql_drop");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat
index b8aa1364a0..90b7b34abb 100644
--- a/src/include/catalog/pg_database.dat
+++ b/src/include/catalog/pg_database.dat
@@ -15,7 +15,7 @@
 { oid => '1', oid_symbol => 'TemplateDbOid',
   descr => 'default template for new databases',
   datname => 'template1', encoding => 'ENCODING', datcollate => 'LC_COLLATE',
-  datctype => 'LC_CTYPE', datistemplate => 't', datallowconn => 't',
+  datctype => 'LC_CTYPE', datistemplate => 't', dathasloginevt => 'f', datallowconn => 't',
   datconnlimit => '-1', datlastsysoid => '0', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datacl => '_null_' },
 
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index 43f3beb6a3..b3eac25755 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -52,6 +52,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
 	/* new connections allowed? */
 	bool		datallowconn;
 
+	/* database has login event triggers? */
+	bool		dathasloginevt;
+
 	/* max connections allowed (-1=no limit) */
 	int32		datconnlimit;
 
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index e765e67fd1..a9f9954597 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -54,6 +54,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnLogin(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 9ba24d4ca9..b0e7df3d81 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
+PG_CMDTAG(CMDTAG_LOGIN, "LOGIN", true, false, false)
 PG_CMDTAG(CMDTAG_MOVE, "MOVE", false, false, true)
 PG_CMDTAG(CMDTAG_NOTIFY, "NOTIFY", false, false, false)
 PG_CMDTAG(CMDTAG_PREPARE, "PREPARE", false, false, false)
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index 58ddb71cb1..29d73afe3d 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Login,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index c70c08e27b..59fa30a3ae 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,26 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+
+CREATE TABLE user_logins(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO user_logins (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 $node_primary->wait_for_catchup($node_standby_1, 'replay',
 	$node_primary->lsn('insert'));
@@ -371,6 +391,9 @@ sub replay_check
 
 replay_check();
 
+$node_standby_1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$node_standby_2->safe_psql('postgres', "SELECT 2", extra_params => [ '-U', 'regress_user', '-w' ]);
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 44d545de25..fb2c799ebb 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -604,3 +604,41 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     1
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     2
+(1 row)
+
+-- Check dathasloginevt in system catalog
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+ dathasloginevt 
+----------------
+ t
+(1 row)
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 1446cf8cc8..3123cbb23d 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -462,3 +462,27 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+SELECT COUNT(*) FROM user_logins;
+\c
+SELECT COUNT(*) FROM user_logins;
+
+-- Check dathasloginevt in system catalog
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
-- 
2.27.0

#85Greg Nancarrow
gregn4422@gmail.com
In reply to: Greg Nancarrow (#84)
1 attachment(s)
Re: On login trigger: take three

On Tue, Nov 16, 2021 at 4:38 PM Greg Nancarrow <gregn4422@gmail.com> wrote:

I've attached an updated patch with these changes.

I've attached a re-based version (no functional changes from the
previous) to fix cfbot failures.

Regards,
Greg Nancarrow
Fujitsu Australia

Attachments:

v23-0001-Add-a-new-login-event-and-login-event-trigger-support.patchapplication/octet-stream; name=v23-0001-Add-a-new-login-event-and-login-event-trigger-support.patchDownload
From a492592a78904fa53837d1357236aa5a3013741e Mon Sep 17 00:00:00 2001
From: Greg Nancarrow <gregn4422@gmail.com>
Date: Mon, 6 Dec 2021 11:28:21 +1100
Subject: [PATCH v23] Add a new "login" event and login event trigger support.

The login event occurs when a user logs in to the system.

Author: Konstantin Knizhnik
Discussion: https://www.postgresql.org/message-id/flat/0d46d29f-4558-3af9-9c85-7774e14a7709%40postgrespro.ru
---
 doc/src/sgml/bki.sgml                       |   2 +-
 doc/src/sgml/catalogs.sgml                  |  11 ++
 doc/src/sgml/ecpg.sgml                      |   2 +
 doc/src/sgml/event-trigger.sgml             |  70 ++++++++-
 src/backend/commands/dbcommands.c           |   3 +-
 src/backend/commands/event_trigger.c        | 162 +++++++++++++++++---
 src/backend/tcop/postgres.c                 |   4 +
 src/backend/utils/cache/evtcache.c          |   2 +
 src/bin/pg_dump/pg_dump.c                   |   5 +
 src/bin/psql/tab-complete.c                 |   3 +-
 src/include/catalog/pg_database.dat         |   2 +-
 src/include/catalog/pg_database.h           |   3 +
 src/include/commands/event_trigger.h        |   1 +
 src/include/tcop/cmdtaglist.h               |   1 +
 src/include/utils/evtcache.h                |   3 +-
 src/test/recovery/t/001_stream_rep.pl       |  23 +++
 src/test/regress/expected/event_trigger.out |  38 +++++
 src/test/regress/sql/event_trigger.sql      |  24 +++
 18 files changed, 335 insertions(+), 24 deletions(-)

diff --git a/doc/src/sgml/bki.sgml b/doc/src/sgml/bki.sgml
index f900a01b82..c7627fe644 100644
--- a/doc/src/sgml/bki.sgml
+++ b/doc/src/sgml/bki.sgml
@@ -182,7 +182,7 @@
 { oid =&gt; '1', oid_symbol =&gt; 'TemplateDbOid',
   descr =&gt; 'database\'s default template',
   datname =&gt; 'template1', encoding =&gt; 'ENCODING', datcollate =&gt; 'LC_COLLATE',
-  datctype =&gt; 'LC_CTYPE', datistemplate =&gt; 't', datallowconn =&gt; 't',
+  datctype =&gt; 'LC_CTYPE', datistemplate =&gt; 't', datallowconn =&gt; 't', dathasloginevt =&gt; 'f',
   datconnlimit =&gt; '-1', datlastsysoid =&gt; '0', datfrozenxid =&gt; '0',
   datminmxid =&gt; '1', dattablespace =&gt; 'pg_default', datacl =&gt; '_null_' },
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index c1d11be73f..9e37596752 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2979,6 +2979,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>dathasloginevt</structfield> <type>bool</type>
+      </para>
+      <para>
+        Indicates that there are login event triggers defined for this database.
+        This flag is used to avoid extra lookups on the <structname>pg_event_trigger</structname> table during each backend startup.
+        This flag is used internally by <productname>PostgreSQL</productname> and should not be manually changed by DBA or application.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>datconnlimit</structfield> <type>int4</type>
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index 04d4a6bdb2..2db3cbbc11 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -4731,6 +4731,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = t (type: 1)
 datallowconn = t (type: 1)
+dathasloginevt = f (type: 1)
 datconnlimit = -1 (type: 5)
 datlastsysoid = 11510 (type: 1)
 datfrozenxid = 379 (type: 1)
@@ -4756,6 +4757,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = f (type: 1)
 datallowconn = t (type: 1)
+dathasloginevt = f (type: 1)
 datconnlimit = -1 (type: 5)
 datlastsysoid = 11510 (type: 1)
 datfrozenxid = 379 (type: 1)
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 60366a950e..ec35ddfad8 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>login</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,16 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>login</literal> event occurs when a user logs in to the
+     system.
+     Any bugs in a trigger procedure for this event may prevent successful
+     login to the system. Such bugs may be fixed after first restarting the
+     system in single-user mode (as event triggers are disabled in this mode).
+     See the <xref linkend="app-postgres"/> reference page for details about
+     using single-user mode.
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1140,7 +1151,7 @@ typedef struct EventTriggerData
   </sect1>
 
   <sect1 id="event-trigger-example">
-   <title>A Complete Event Trigger Example</title>
+   <title>A C language Event Trigger Example</title>
 
    <para>
     Here is a very simple example of an event trigger function written in C.
@@ -1280,6 +1291,63 @@ $$;
 CREATE EVENT TRIGGER no_rewrite_allowed
                   ON table_rewrite
    EXECUTE FUNCTION no_rewrite();
+</programlisting>
+   </para>
+ </sect1>
+
+  <sect1 id="event-trigger-database-login-example">
+   <title>A Database Login Event Trigger Example</title>
+
+   <para>
+    The event trigger on the <literal>login</literal> event can be
+    useful for logging user logins, for verifying the connection and
+    assigning roles according to current circumstances, or for some session data
+    initialization.
+   </para>
+
+   <para>
+    The following example demonstrates these options.
+<programlisting>
+-- create test tables and roles
+CREATE TABLE user_login_log (
+  "user" text,
+  "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+  RETURNS event_trigger SECURITY DEFINER
+  LANGUAGE plpgsql AS
+$$
+DECLARE
+  hour integer = EXTRACT('hour' FROM current_time);
+BEGIN
+-- 1) Assign some roles
+IF hour BETWEEN 8 AND 20 THEN         -- at daytime grant the day_worker role
+  EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT    day_worker TO '   || quote_ident(session_user);
+ELSIF hour BETWEEN 2 AND 4 THEN
+  RAISE EXCEPTION 'Login forbidden';  -- do not allow to login these hours
+ELSE                                  -- at other time grant the night_worker role
+  EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT  night_worker TO ' || quote_ident(session_user);
+END IF;
+
+-- 2) Initialize some user session data
+CREATE TEMP TABLE session_storage (x float, y integer);
+
+-- 3) Log the connection time
+INSERT INTO user_login_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+  ON login
+  EXECUTE FUNCTION init_session();
 </programlisting>
    </para>
  </sect1>
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 029fab48df..7bb9b8627c 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -530,6 +530,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	new_record[Anum_pg_database_datctype - 1] =
 		DirectFunctionCall1(namein, CStringGetDatum(dbctype));
 	new_record[Anum_pg_database_datistemplate - 1] = BoolGetDatum(dbistemplate);
+	new_record[Anum_pg_database_dathasloginevt - 1] = BoolGetDatum(false);
 	new_record[Anum_pg_database_datallowconn - 1] = BoolGetDatum(dballowconnections);
 	new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 	new_record[Anum_pg_database_datlastsysoid - 1] = ObjectIdGetDatum(src_lastsysoid);
@@ -1585,7 +1586,7 @@ AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel)
 		new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 		new_record_repl[Anum_pg_database_datconnlimit - 1] = true;
 	}
-
+	new_record[Anum_pg_database_dathasloginevt - 1] = BoolGetDatum(datform->dathasloginevt);
 	newtuple = heap_modify_tuple(tuple, RelationGetDescr(rel), new_record,
 								 new_record_nulls, new_record_repl);
 	CatalogTupleUpdate(rel, &tuple->t_self, newtuple);
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index df264329d8..7a08480bf4 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -43,9 +44,11 @@
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 typedef struct EventTriggerQueryState
@@ -130,6 +133,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "login") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -293,6 +297,27 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
 	CatalogTupleInsert(tgrel, tuple);
 	heap_freetuple(tuple);
 
+	if (strcmp(eventname, "login") == 0)
+	{
+		Form_pg_database db;
+		Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+
+		/* Set dathasloginevt flag in pg_database */
+		tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+		db = (Form_pg_database) GETSTRUCT(tuple);
+		if (!db->dathasloginevt)
+		{
+			db->dathasloginevt = true;
+			CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+		}
+		else
+			CacheInvalidateRelcacheByTuple(tuple);
+		table_close(pg_db, RowExclusiveLock);
+		heap_freetuple(tuple);
+	}
+
 	/* Depend on owner. */
 	recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
 
@@ -562,6 +587,9 @@ EventTriggerCommonSetup(Node *parsetree,
 	ListCell   *lc;
 	List	   *runlist = NIL;
 
+	/* Get the command tag. */
+	tag = (event == EVT_Login) ? CMDTAG_LOGIN : CreateCommandTag(parsetree);
+
 	/*
 	 * We want the list of command tags for which this procedure is actually
 	 * invoked to match up exactly with the list that CREATE EVENT TRIGGER
@@ -577,22 +605,18 @@ EventTriggerCommonSetup(Node *parsetree,
 	 * relevant command tag.
 	 */
 #ifdef USE_ASSERT_CHECKING
+	if (event == EVT_DDLCommandStart ||
+		event == EVT_DDLCommandEnd ||
+		event == EVT_SQLDrop ||
+		event == EVT_Login)
 	{
-		CommandTag	dbgtag;
-
-		dbgtag = CreateCommandTag(parsetree);
-		if (event == EVT_DDLCommandStart ||
-			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
-		{
-			if (!command_tag_event_trigger_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
-		else if (event == EVT_TableRewrite)
-		{
-			if (!command_tag_table_rewrite_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
+		if (!command_tag_event_trigger_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
+	}
+	else if (event == EVT_TableRewrite)
+	{
+		if (!command_tag_table_rewrite_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
 	}
 #endif
 
@@ -601,9 +625,6 @@ EventTriggerCommonSetup(Node *parsetree,
 	if (cachelist == NIL)
 		return NIL;
 
-	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
-
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
 	 * memory context.  Once we start running the command triggers, or indeed
@@ -800,6 +821,111 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Return true if this database has login event triggers, false otherwise.
+ */
+static bool
+DatabaseHasLoginEventTriggers(void)
+{
+	bool		has_login_event_triggers;
+	HeapTuple	tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+	has_login_event_triggers = ((Form_pg_database) GETSTRUCT(tuple))->dathasloginevt;
+	ReleaseSysCache(tuple);
+	return has_login_event_triggers;
+}
+
+/*
+ * Fire login event triggers.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId))
+		return;
+
+	StartTransactionCommand();
+
+	if (DatabaseHasLoginEventTriggers())
+	{
+		runlist = EventTriggerCommonSetup(NULL,
+										  EVT_Login, "login",
+										  &trigdata);
+
+		if (runlist != NIL)
+		{
+			/*
+			 * Make sure anything the main command did will be visible to the
+			 * event triggers.
+			 */
+			CommandCounterIncrement();
+
+			/*
+			 * Event trigger execution may require an active snapshot.
+			 */
+			PushActiveSnapshot(GetTransactionSnapshot());
+
+			/* Run the triggers. */
+			EventTriggerInvoke(runlist, &trigdata);
+
+			/* Cleanup. */
+			list_free(runlist);
+
+			PopActiveSnapshot();
+		}
+		else
+		{
+			/*
+			 * Runlist is empty: clear dathasloginevt flag
+			 */
+			Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+			HeapTuple	tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+			Form_pg_database db;
+
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+			db = (Form_pg_database) GETSTRUCT(tuple);
+			if (db->dathasloginevt)
+			{
+				/*
+				 * There can be a race condition: a login event trigger may
+				 * have been added after the pg_event_trigger table was
+				 * scanned, and we don't want to erroneously clear the
+				 * dathasloginevt flag in this case. To be sure that this
+				 * hasn't happened, repeat the scan under the pg_database
+				 * table lock.
+				 */
+				AcceptInvalidationMessages();
+				runlist = EventTriggerCommonSetup(NULL,
+												  EVT_Login, "login",
+												  &trigdata);
+				if (runlist == NULL)	/* list is still empty, so clear the
+										 * flag */
+				{
+					db->dathasloginevt = false;
+					CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				}
+				else
+					CacheInvalidateRelcacheByTuple(tuple);
+			}
+			table_close(pg_db, RowExclusiveLock);
+			heap_freetuple(tuple);
+		}
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 82de01cdc6..f17ecd1b36 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -41,6 +41,7 @@
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/prepare.h"
 #include "common/pg_prng.h"
 #include "executor/spi.h"
@@ -4179,6 +4180,9 @@ PostgresMain(const char *dbname, const char *username)
 	initStringInfo(&row_description_buf);
 	MemoryContextSwitchTo(TopMemoryContext);
 
+	/* Fire any defined login event triggers, if appropriate */
+	EventTriggerOnLogin();
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 460b720a65..b9b935f1cc 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "login") == 0)
+			event = EVT_Login;
 		else
 			continue;
 
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index c590003f18..b9936f8b76 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3156,6 +3156,11 @@ dumpDatabase(Archive *fout)
 		appendPQExpBufferStr(delQry, ";\n");
 	}
 
+	/*
+	 * We do not restore pg_database.dathasloginevt because it is set
+	 * automatically on login event trigger creation.
+	 */
+
 	/* Add database-specific SET options */
 	dumpDatabaseConfig(fout, creaQry, datname, dbCatId.oid);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 2f412ca3db..a3e23c9db8 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3189,7 +3189,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end",
+					  "login", "sql_drop");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat
index b8aa1364a0..90b7b34abb 100644
--- a/src/include/catalog/pg_database.dat
+++ b/src/include/catalog/pg_database.dat
@@ -15,7 +15,7 @@
 { oid => '1', oid_symbol => 'TemplateDbOid',
   descr => 'default template for new databases',
   datname => 'template1', encoding => 'ENCODING', datcollate => 'LC_COLLATE',
-  datctype => 'LC_CTYPE', datistemplate => 't', datallowconn => 't',
+  datctype => 'LC_CTYPE', datistemplate => 't', dathasloginevt => 'f', datallowconn => 't',
   datconnlimit => '-1', datlastsysoid => '0', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datacl => '_null_' },
 
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index 43f3beb6a3..b3eac25755 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -52,6 +52,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
 	/* new connections allowed? */
 	bool		datallowconn;
 
+	/* database has login event triggers? */
+	bool		dathasloginevt;
+
 	/* max connections allowed (-1=no limit) */
 	int32		datconnlimit;
 
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index e765e67fd1..a9f9954597 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -54,6 +54,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnLogin(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 9ba24d4ca9..b0e7df3d81 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
+PG_CMDTAG(CMDTAG_LOGIN, "LOGIN", true, false, false)
 PG_CMDTAG(CMDTAG_MOVE, "MOVE", false, false, true)
 PG_CMDTAG(CMDTAG_NOTIFY, "NOTIFY", false, false, false)
 PG_CMDTAG(CMDTAG_PREPARE, "PREPARE", false, false, false)
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index 58ddb71cb1..29d73afe3d 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Login,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index c70c08e27b..59fa30a3ae 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,26 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+
+CREATE TABLE user_logins(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO user_logins (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 $node_primary->wait_for_catchup($node_standby_1, 'replay',
 	$node_primary->lsn('insert'));
@@ -371,6 +391,9 @@ sub replay_check
 
 replay_check();
 
+$node_standby_1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$node_standby_2->safe_psql('postgres', "SELECT 2", extra_params => [ '-U', 'regress_user', '-w' ]);
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 44d545de25..fb2c799ebb 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -604,3 +604,41 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     1
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     2
+(1 row)
+
+-- Check dathasloginevt in system catalog
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+ dathasloginevt 
+----------------
+ t
+(1 row)
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 1446cf8cc8..3123cbb23d 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -462,3 +462,27 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+SELECT COUNT(*) FROM user_logins;
+\c
+SELECT COUNT(*) FROM user_logins;
+
+-- Check dathasloginevt in system catalog
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
-- 
2.27.0

#86Greg Nancarrow
gregn4422@gmail.com
In reply to: Greg Nancarrow (#85)
1 attachment(s)
Re: On login trigger: take three

On Mon, Dec 6, 2021 at 12:10 PM Greg Nancarrow <gregn4422@gmail.com> wrote:

I've attached a re-based version (no functional changes from the
previous) to fix cfbot failures.

I've attached a re-based version (no functional changes from the
previous) to fix cfbot failures.

Regards,
Greg Nancarrow
Fujitsu Australia

Attachments:

v24-0001-Add-a-new-login-event-and-login-event-trigger-support.patchapplication/octet-stream; name=v24-0001-Add-a-new-login-event-and-login-event-trigger-support.patchDownload
From 5a3f0d6f0a2bcb80e9e4d9d8a445f8e5456458b3 Mon Sep 17 00:00:00 2001
From: Greg Nancarrow <gregn4422@gmail.com>
Date: Mon, 24 Jan 2022 11:08:27 +1100
Subject: [PATCH v24] Add a new "login" event and login event trigger support.

The login event occurs when a user logs in to the system.

Author: Konstantin Knizhnik
Discussion: https://www.postgresql.org/message-id/flat/0d46d29f-4558-3af9-9c85-7774e14a7709%40postgrespro.ru
---
 doc/src/sgml/bki.sgml                       |   2 +-
 doc/src/sgml/catalogs.sgml                  |  11 ++
 doc/src/sgml/ecpg.sgml                      |   2 +
 doc/src/sgml/event-trigger.sgml             |  70 ++++++++-
 src/backend/commands/dbcommands.c           |   3 +-
 src/backend/commands/event_trigger.c        | 162 +++++++++++++++++---
 src/backend/tcop/postgres.c                 |   4 +
 src/backend/utils/cache/evtcache.c          |   2 +
 src/bin/pg_dump/pg_dump.c                   |   5 +
 src/bin/psql/tab-complete.c                 |   3 +-
 src/include/catalog/pg_database.dat         |   2 +-
 src/include/catalog/pg_database.h           |   3 +
 src/include/commands/event_trigger.h        |   1 +
 src/include/tcop/cmdtaglist.h               |   1 +
 src/include/utils/evtcache.h                |   3 +-
 src/test/recovery/t/001_stream_rep.pl       |  23 +++
 src/test/regress/expected/event_trigger.out |  38 +++++
 src/test/regress/sql/event_trigger.sql      |  24 +++
 18 files changed, 335 insertions(+), 24 deletions(-)

diff --git a/doc/src/sgml/bki.sgml b/doc/src/sgml/bki.sgml
index ae32bfcb7e..5d6bffe7ff 100644
--- a/doc/src/sgml/bki.sgml
+++ b/doc/src/sgml/bki.sgml
@@ -182,7 +182,7 @@
 { oid =&gt; '1', oid_symbol =&gt; 'TemplateDbOid',
   descr =&gt; 'database\'s default template',
   datname =&gt; 'template1', encoding =&gt; 'ENCODING', datcollate =&gt; 'LC_COLLATE',
-  datctype =&gt; 'LC_CTYPE', datistemplate =&gt; 't', datallowconn =&gt; 't',
+  datctype =&gt; 'LC_CTYPE', datistemplate =&gt; 't', datallowconn =&gt; 't', dathasloginevt =&gt; 'f',
   datconnlimit =&gt; '-1', datfrozenxid =&gt; '0',
   datminmxid =&gt; '1', dattablespace =&gt; 'pg_default', datacl =&gt; '_null_' },
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 1e65c426b2..cdb5252996 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2991,6 +2991,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>dathasloginevt</structfield> <type>bool</type>
+      </para>
+      <para>
+        Indicates that there are login event triggers defined for this database.
+        This flag is used to avoid extra lookups on the <structname>pg_event_trigger</structname> table during each backend startup.
+        This flag is used internally by <productname>PostgreSQL</productname> and should not be manually changed by DBA or application.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>datconnlimit</structfield> <type>int4</type>
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index cdc4761c60..197b476205 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -4731,6 +4731,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = t (type: 1)
 datallowconn = t (type: 1)
+dathasloginevt = f (type: 1)
 datconnlimit = -1 (type: 5)
 datfrozenxid = 379 (type: 1)
 dattablespace = 1663 (type: 1)
@@ -4755,6 +4756,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = f (type: 1)
 datallowconn = t (type: 1)
+dathasloginevt = f (type: 1)
 datconnlimit = -1 (type: 5)
 datfrozenxid = 379 (type: 1)
 dattablespace = 1663 (type: 1)
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 60366a950e..ec35ddfad8 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>login</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,16 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>login</literal> event occurs when a user logs in to the
+     system.
+     Any bugs in a trigger procedure for this event may prevent successful
+     login to the system. Such bugs may be fixed after first restarting the
+     system in single-user mode (as event triggers are disabled in this mode).
+     See the <xref linkend="app-postgres"/> reference page for details about
+     using single-user mode.
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1140,7 +1151,7 @@ typedef struct EventTriggerData
   </sect1>
 
   <sect1 id="event-trigger-example">
-   <title>A Complete Event Trigger Example</title>
+   <title>A C language Event Trigger Example</title>
 
    <para>
     Here is a very simple example of an event trigger function written in C.
@@ -1280,6 +1291,63 @@ $$;
 CREATE EVENT TRIGGER no_rewrite_allowed
                   ON table_rewrite
    EXECUTE FUNCTION no_rewrite();
+</programlisting>
+   </para>
+ </sect1>
+
+  <sect1 id="event-trigger-database-login-example">
+   <title>A Database Login Event Trigger Example</title>
+
+   <para>
+    The event trigger on the <literal>login</literal> event can be
+    useful for logging user logins, for verifying the connection and
+    assigning roles according to current circumstances, or for some session data
+    initialization.
+   </para>
+
+   <para>
+    The following example demonstrates these options.
+<programlisting>
+-- create test tables and roles
+CREATE TABLE user_login_log (
+  "user" text,
+  "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+  RETURNS event_trigger SECURITY DEFINER
+  LANGUAGE plpgsql AS
+$$
+DECLARE
+  hour integer = EXTRACT('hour' FROM current_time);
+BEGIN
+-- 1) Assign some roles
+IF hour BETWEEN 8 AND 20 THEN         -- at daytime grant the day_worker role
+  EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT    day_worker TO '   || quote_ident(session_user);
+ELSIF hour BETWEEN 2 AND 4 THEN
+  RAISE EXCEPTION 'Login forbidden';  -- do not allow to login these hours
+ELSE                                  -- at other time grant the night_worker role
+  EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT  night_worker TO ' || quote_ident(session_user);
+END IF;
+
+-- 2) Initialize some user session data
+CREATE TEMP TABLE session_storage (x float, y integer);
+
+-- 3) Log the connection time
+INSERT INTO user_login_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+  ON login
+  EXECUTE FUNCTION init_session();
 </programlisting>
    </para>
  </sect1>
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index da8345561d..6a743d2058 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -528,6 +528,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	new_record[Anum_pg_database_datctype - 1] =
 		DirectFunctionCall1(namein, CStringGetDatum(dbctype));
 	new_record[Anum_pg_database_datistemplate - 1] = BoolGetDatum(dbistemplate);
+	new_record[Anum_pg_database_dathasloginevt - 1] = BoolGetDatum(false);
 	new_record[Anum_pg_database_datallowconn - 1] = BoolGetDatum(dballowconnections);
 	new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 	new_record[Anum_pg_database_datfrozenxid - 1] = TransactionIdGetDatum(src_frozenxid);
@@ -1582,7 +1583,7 @@ AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel)
 		new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 		new_record_repl[Anum_pg_database_datconnlimit - 1] = true;
 	}
-
+	new_record[Anum_pg_database_dathasloginevt - 1] = BoolGetDatum(datform->dathasloginevt);
 	newtuple = heap_modify_tuple(tuple, RelationGetDescr(rel), new_record,
 								 new_record_nulls, new_record_repl);
 	CatalogTupleUpdate(rel, &tuple->t_self, newtuple);
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 93c2099735..9a5b5e98e7 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -43,9 +44,11 @@
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 typedef struct EventTriggerQueryState
@@ -130,6 +133,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "login") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -293,6 +297,27 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
 	CatalogTupleInsert(tgrel, tuple);
 	heap_freetuple(tuple);
 
+	if (strcmp(eventname, "login") == 0)
+	{
+		Form_pg_database db;
+		Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+
+		/* Set dathasloginevt flag in pg_database */
+		tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+		db = (Form_pg_database) GETSTRUCT(tuple);
+		if (!db->dathasloginevt)
+		{
+			db->dathasloginevt = true;
+			CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+		}
+		else
+			CacheInvalidateRelcacheByTuple(tuple);
+		table_close(pg_db, RowExclusiveLock);
+		heap_freetuple(tuple);
+	}
+
 	/* Depend on owner. */
 	recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
 
@@ -562,6 +587,9 @@ EventTriggerCommonSetup(Node *parsetree,
 	ListCell   *lc;
 	List	   *runlist = NIL;
 
+	/* Get the command tag. */
+	tag = (event == EVT_Login) ? CMDTAG_LOGIN : CreateCommandTag(parsetree);
+
 	/*
 	 * We want the list of command tags for which this procedure is actually
 	 * invoked to match up exactly with the list that CREATE EVENT TRIGGER
@@ -577,22 +605,18 @@ EventTriggerCommonSetup(Node *parsetree,
 	 * relevant command tag.
 	 */
 #ifdef USE_ASSERT_CHECKING
+	if (event == EVT_DDLCommandStart ||
+		event == EVT_DDLCommandEnd ||
+		event == EVT_SQLDrop ||
+		event == EVT_Login)
 	{
-		CommandTag	dbgtag;
-
-		dbgtag = CreateCommandTag(parsetree);
-		if (event == EVT_DDLCommandStart ||
-			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
-		{
-			if (!command_tag_event_trigger_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
-		else if (event == EVT_TableRewrite)
-		{
-			if (!command_tag_table_rewrite_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
+		if (!command_tag_event_trigger_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
+	}
+	else if (event == EVT_TableRewrite)
+	{
+		if (!command_tag_table_rewrite_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
 	}
 #endif
 
@@ -601,9 +625,6 @@ EventTriggerCommonSetup(Node *parsetree,
 	if (cachelist == NIL)
 		return NIL;
 
-	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
-
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
 	 * memory context.  Once we start running the command triggers, or indeed
@@ -800,6 +821,111 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Return true if this database has login event triggers, false otherwise.
+ */
+static bool
+DatabaseHasLoginEventTriggers(void)
+{
+	bool		has_login_event_triggers;
+	HeapTuple	tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+	has_login_event_triggers = ((Form_pg_database) GETSTRUCT(tuple))->dathasloginevt;
+	ReleaseSysCache(tuple);
+	return has_login_event_triggers;
+}
+
+/*
+ * Fire login event triggers.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId))
+		return;
+
+	StartTransactionCommand();
+
+	if (DatabaseHasLoginEventTriggers())
+	{
+		runlist = EventTriggerCommonSetup(NULL,
+										  EVT_Login, "login",
+										  &trigdata);
+
+		if (runlist != NIL)
+		{
+			/*
+			 * Make sure anything the main command did will be visible to the
+			 * event triggers.
+			 */
+			CommandCounterIncrement();
+
+			/*
+			 * Event trigger execution may require an active snapshot.
+			 */
+			PushActiveSnapshot(GetTransactionSnapshot());
+
+			/* Run the triggers. */
+			EventTriggerInvoke(runlist, &trigdata);
+
+			/* Cleanup. */
+			list_free(runlist);
+
+			PopActiveSnapshot();
+		}
+		else
+		{
+			/*
+			 * Runlist is empty: clear dathasloginevt flag
+			 */
+			Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+			HeapTuple	tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+			Form_pg_database db;
+
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+			db = (Form_pg_database) GETSTRUCT(tuple);
+			if (db->dathasloginevt)
+			{
+				/*
+				 * There can be a race condition: a login event trigger may
+				 * have been added after the pg_event_trigger table was
+				 * scanned, and we don't want to erroneously clear the
+				 * dathasloginevt flag in this case. To be sure that this
+				 * hasn't happened, repeat the scan under the pg_database
+				 * table lock.
+				 */
+				AcceptInvalidationMessages();
+				runlist = EventTriggerCommonSetup(NULL,
+												  EVT_Login, "login",
+												  &trigdata);
+				if (runlist == NULL)	/* list is still empty, so clear the
+										 * flag */
+				{
+					db->dathasloginevt = false;
+					CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				}
+				else
+					CacheInvalidateRelcacheByTuple(tuple);
+			}
+			table_close(pg_db, RowExclusiveLock);
+			heap_freetuple(tuple);
+		}
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index fda2e9360e..00dead5d57 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -41,6 +41,7 @@
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/prepare.h"
 #include "common/pg_prng.h"
 #include "executor/spi.h"
@@ -4179,6 +4180,9 @@ PostgresMain(const char *dbname, const char *username)
 	initStringInfo(&row_description_buf);
 	MemoryContextSwitchTo(TopMemoryContext);
 
+	/* Fire any defined login event triggers, if appropriate */
+	EventTriggerOnLogin();
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 3a9c9f0c50..43a38ae6c7 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "login") == 0)
+			event = EVT_Login;
 		else
 			continue;
 
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index f1e8b0b5c2..cc658ce454 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -2997,6 +2997,11 @@ dumpDatabase(Archive *fout)
 		appendPQExpBufferStr(delQry, ";\n");
 	}
 
+	/*
+	 * We do not restore pg_database.dathasloginevt because it is set
+	 * automatically on login event trigger creation.
+	 */
+
 	/* Add database-specific SET options */
 	dumpDatabaseConfig(fout, creaQry, datname, dbCatId.oid);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 6bd33a06cb..7e35713eb3 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3239,7 +3239,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end",
+					  "login", "sql_drop");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat
index d0b0c2d9a0..9f3dad5d2d 100644
--- a/src/include/catalog/pg_database.dat
+++ b/src/include/catalog/pg_database.dat
@@ -15,7 +15,7 @@
 { oid => '1', oid_symbol => 'TemplateDbOid',
   descr => 'default template for new databases',
   datname => 'template1', encoding => 'ENCODING', datcollate => 'LC_COLLATE',
-  datctype => 'LC_CTYPE', datistemplate => 't', datallowconn => 't',
+  datctype => 'LC_CTYPE', datistemplate => 't', dathasloginevt => 'f', datallowconn => 't',
   datconnlimit => '-1', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datacl => '_null_' },
 
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index 1ff6d3e50c..635248d32f 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -52,6 +52,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
 	/* new connections allowed? */
 	bool		datallowconn;
 
+	/* database has login event triggers? */
+	bool		dathasloginevt;
+
 	/* max connections allowed (-1=no limit) */
 	int32		datconnlimit;
 
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 10091c3aaf..c9efee59bf 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -54,6 +54,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnLogin(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 4bc7ddf410..d6e408e1ed 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
+PG_CMDTAG(CMDTAG_LOGIN, "LOGIN", true, false, false)
 PG_CMDTAG(CMDTAG_MOVE, "MOVE", false, false, true)
 PG_CMDTAG(CMDTAG_NOTIFY, "NOTIFY", false, false, false)
 PG_CMDTAG(CMDTAG_PREPARE, "PREPARE", false, false, false)
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index ddb67a68fa..a66344aacb 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Login,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index ca760c7210..b70e3ebc87 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,26 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+
+CREATE TABLE user_logins(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO user_logins (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 my $primary_lsn = $node_primary->lsn('write');
 $node_primary->wait_for_catchup($node_standby_1, 'replay', $primary_lsn);
@@ -387,6 +407,9 @@ sub replay_check
 
 replay_check();
 
+$node_standby_1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$node_standby_2->safe_psql('postgres', "SELECT 2", extra_params => [ '-U', 'regress_user', '-w' ]);
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 44d545de25..fb2c799ebb 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -604,3 +604,41 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     1
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     2
+(1 row)
+
+-- Check dathasloginevt in system catalog
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+ dathasloginevt 
+----------------
+ t
+(1 row)
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 1446cf8cc8..3123cbb23d 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -462,3 +462,27 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+SELECT COUNT(*) FROM user_logins;
+\c
+SELECT COUNT(*) FROM user_logins;
+
+-- Check dathasloginevt in system catalog
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
-- 
2.27.0

#87Greg Nancarrow
gregn4422@gmail.com
In reply to: Greg Nancarrow (#86)
1 attachment(s)
Re: On login trigger: take three

On Mon, Jan 24, 2022 at 1:59 PM Greg Nancarrow <gregn4422@gmail.com> wrote:

I've attached a re-based version (no functional changes from the
previous) to fix cfbot failures.

I've attached a re-based version (no functional changes from the
previous) to fix cfbot failures.

Regards,
Greg Nancarrow
Fujitsu Australia

Attachments:

v25-0001-Add-a-new-login-event-and-login-event-trigger-support.patchapplication/octet-stream; name=v25-0001-Add-a-new-login-event-and-login-event-trigger-support.patchDownload
From 8f50047b6a0b7debd8f5ae16a17314a35ba4cad9 Mon Sep 17 00:00:00 2001
From: Greg Nancarrow <gregn4422@gmail.com>
Date: Tue, 15 Feb 2022 20:40:42 +1100
Subject: [PATCH v25] Add a new "login" event and login event trigger support.

The login event occurs when a user logs in to the system.

Author: Konstantin Knizhnik
Discussion: https://www.postgresql.org/message-id/flat/0d46d29f-4558-3af9-9c85-7774e14a7709%40postgrespro.ru
---
 doc/src/sgml/bki.sgml                       |   2 +-
 doc/src/sgml/catalogs.sgml                  |  11 ++
 doc/src/sgml/ecpg.sgml                      |   2 +
 doc/src/sgml/event-trigger.sgml             |  70 ++++++++-
 src/backend/commands/dbcommands.c           |   2 +
 src/backend/commands/event_trigger.c        | 162 +++++++++++++++++---
 src/backend/tcop/postgres.c                 |   4 +
 src/backend/utils/cache/evtcache.c          |   2 +
 src/bin/pg_dump/pg_dump.c                   |   5 +
 src/bin/psql/tab-complete.c                 |   3 +-
 src/include/catalog/pg_database.dat         |   2 +-
 src/include/catalog/pg_database.h           |   3 +
 src/include/commands/event_trigger.h        |   1 +
 src/include/tcop/cmdtaglist.h               |   1 +
 src/include/utils/evtcache.h                |   3 +-
 src/test/recovery/t/001_stream_rep.pl       |  23 +++
 src/test/regress/expected/event_trigger.out |  38 +++++
 src/test/regress/sql/event_trigger.sql      |  24 +++
 18 files changed, 335 insertions(+), 23 deletions(-)

diff --git a/doc/src/sgml/bki.sgml b/doc/src/sgml/bki.sgml
index 33955494c6..caef1df200 100644
--- a/doc/src/sgml/bki.sgml
+++ b/doc/src/sgml/bki.sgml
@@ -183,7 +183,7 @@
 { oid => '1', oid_symbol => 'TemplateDbOid',
   descr => 'database\'s default template',
   datname => 'template1', encoding => 'ENCODING', datistemplate => 't',
-  datallowconn => 't', datconnlimit => '-1', datfrozenxid => '0',
+  datallowconn => 't', dathasloginevt => 'f', datconnlimit => '-1', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datcollate => 'LC_COLLATE',
   datctype => 'LC_CTYPE', datacl => '_null_' },
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 5a1627a394..1e34165f5a 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2973,6 +2973,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>dathasloginevt</structfield> <type>bool</type>
+      </para>
+      <para>
+        Indicates that there are login event triggers defined for this database.
+        This flag is used to avoid extra lookups on the <structname>pg_event_trigger</structname> table during each backend startup.
+        This flag is used internally by <productname>PostgreSQL</productname> and should not be manually changed by DBA or application.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>datconnlimit</structfield> <type>int4</type>
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index cdc4761c60..197b476205 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -4731,6 +4731,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = t (type: 1)
 datallowconn = t (type: 1)
+dathasloginevt = f (type: 1)
 datconnlimit = -1 (type: 5)
 datfrozenxid = 379 (type: 1)
 dattablespace = 1663 (type: 1)
@@ -4755,6 +4756,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = f (type: 1)
 datallowconn = t (type: 1)
+dathasloginevt = f (type: 1)
 datconnlimit = -1 (type: 5)
 datfrozenxid = 379 (type: 1)
 dattablespace = 1663 (type: 1)
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 60366a950e..ec35ddfad8 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>login</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,16 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>login</literal> event occurs when a user logs in to the
+     system.
+     Any bugs in a trigger procedure for this event may prevent successful
+     login to the system. Such bugs may be fixed after first restarting the
+     system in single-user mode (as event triggers are disabled in this mode).
+     See the <xref linkend="app-postgres"/> reference page for details about
+     using single-user mode.
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1140,7 +1151,7 @@ typedef struct EventTriggerData
   </sect1>
 
   <sect1 id="event-trigger-example">
-   <title>A Complete Event Trigger Example</title>
+   <title>A C language Event Trigger Example</title>
 
    <para>
     Here is a very simple example of an event trigger function written in C.
@@ -1280,6 +1291,63 @@ $$;
 CREATE EVENT TRIGGER no_rewrite_allowed
                   ON table_rewrite
    EXECUTE FUNCTION no_rewrite();
+</programlisting>
+   </para>
+ </sect1>
+
+  <sect1 id="event-trigger-database-login-example">
+   <title>A Database Login Event Trigger Example</title>
+
+   <para>
+    The event trigger on the <literal>login</literal> event can be
+    useful for logging user logins, for verifying the connection and
+    assigning roles according to current circumstances, or for some session data
+    initialization.
+   </para>
+
+   <para>
+    The following example demonstrates these options.
+<programlisting>
+-- create test tables and roles
+CREATE TABLE user_login_log (
+  "user" text,
+  "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+  RETURNS event_trigger SECURITY DEFINER
+  LANGUAGE plpgsql AS
+$$
+DECLARE
+  hour integer = EXTRACT('hour' FROM current_time);
+BEGIN
+-- 1) Assign some roles
+IF hour BETWEEN 8 AND 20 THEN         -- at daytime grant the day_worker role
+  EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT    day_worker TO '   || quote_ident(session_user);
+ELSIF hour BETWEEN 2 AND 4 THEN
+  RAISE EXCEPTION 'Login forbidden';  -- do not allow to login these hours
+ELSE                                  -- at other time grant the night_worker role
+  EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT  night_worker TO ' || quote_ident(session_user);
+END IF;
+
+-- 2) Initialize some user session data
+CREATE TEMP TABLE session_storage (x float, y integer);
+
+-- 3) Log the connection time
+INSERT INTO user_login_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+  ON login
+  EXECUTE FUNCTION init_session();
 </programlisting>
    </para>
  </sect1>
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index c37e3c9a9a..f26679a08f 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -631,6 +631,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	new_record[Anum_pg_database_encoding - 1] = Int32GetDatum(encoding);
 	new_record[Anum_pg_database_datistemplate - 1] = BoolGetDatum(dbistemplate);
 	new_record[Anum_pg_database_datallowconn - 1] = BoolGetDatum(dballowconnections);
+	new_record[Anum_pg_database_dathasloginevt - 1] = BoolGetDatum(false);
 	new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 	new_record[Anum_pg_database_datfrozenxid - 1] = TransactionIdGetDatum(src_frozenxid);
 	new_record[Anum_pg_database_datminmxid - 1] = TransactionIdGetDatum(src_minmxid);
@@ -1699,6 +1700,7 @@ AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel)
 		new_record_repl[Anum_pg_database_datconnlimit - 1] = true;
 	}
 
+	new_record[Anum_pg_database_dathasloginevt - 1] = BoolGetDatum(datform->dathasloginevt);
 	newtuple = heap_modify_tuple(tuple, RelationGetDescr(rel), new_record,
 								 new_record_nulls, new_record_repl);
 	CatalogTupleUpdate(rel, &tuple->t_self, newtuple);
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 93c2099735..9a5b5e98e7 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -43,9 +44,11 @@
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 typedef struct EventTriggerQueryState
@@ -130,6 +133,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "login") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -293,6 +297,27 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
 	CatalogTupleInsert(tgrel, tuple);
 	heap_freetuple(tuple);
 
+	if (strcmp(eventname, "login") == 0)
+	{
+		Form_pg_database db;
+		Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+
+		/* Set dathasloginevt flag in pg_database */
+		tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+		db = (Form_pg_database) GETSTRUCT(tuple);
+		if (!db->dathasloginevt)
+		{
+			db->dathasloginevt = true;
+			CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+		}
+		else
+			CacheInvalidateRelcacheByTuple(tuple);
+		table_close(pg_db, RowExclusiveLock);
+		heap_freetuple(tuple);
+	}
+
 	/* Depend on owner. */
 	recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
 
@@ -562,6 +587,9 @@ EventTriggerCommonSetup(Node *parsetree,
 	ListCell   *lc;
 	List	   *runlist = NIL;
 
+	/* Get the command tag. */
+	tag = (event == EVT_Login) ? CMDTAG_LOGIN : CreateCommandTag(parsetree);
+
 	/*
 	 * We want the list of command tags for which this procedure is actually
 	 * invoked to match up exactly with the list that CREATE EVENT TRIGGER
@@ -577,22 +605,18 @@ EventTriggerCommonSetup(Node *parsetree,
 	 * relevant command tag.
 	 */
 #ifdef USE_ASSERT_CHECKING
+	if (event == EVT_DDLCommandStart ||
+		event == EVT_DDLCommandEnd ||
+		event == EVT_SQLDrop ||
+		event == EVT_Login)
 	{
-		CommandTag	dbgtag;
-
-		dbgtag = CreateCommandTag(parsetree);
-		if (event == EVT_DDLCommandStart ||
-			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
-		{
-			if (!command_tag_event_trigger_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
-		else if (event == EVT_TableRewrite)
-		{
-			if (!command_tag_table_rewrite_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
+		if (!command_tag_event_trigger_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
+	}
+	else if (event == EVT_TableRewrite)
+	{
+		if (!command_tag_table_rewrite_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
 	}
 #endif
 
@@ -601,9 +625,6 @@ EventTriggerCommonSetup(Node *parsetree,
 	if (cachelist == NIL)
 		return NIL;
 
-	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
-
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
 	 * memory context.  Once we start running the command triggers, or indeed
@@ -800,6 +821,111 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Return true if this database has login event triggers, false otherwise.
+ */
+static bool
+DatabaseHasLoginEventTriggers(void)
+{
+	bool		has_login_event_triggers;
+	HeapTuple	tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+	has_login_event_triggers = ((Form_pg_database) GETSTRUCT(tuple))->dathasloginevt;
+	ReleaseSysCache(tuple);
+	return has_login_event_triggers;
+}
+
+/*
+ * Fire login event triggers.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId))
+		return;
+
+	StartTransactionCommand();
+
+	if (DatabaseHasLoginEventTriggers())
+	{
+		runlist = EventTriggerCommonSetup(NULL,
+										  EVT_Login, "login",
+										  &trigdata);
+
+		if (runlist != NIL)
+		{
+			/*
+			 * Make sure anything the main command did will be visible to the
+			 * event triggers.
+			 */
+			CommandCounterIncrement();
+
+			/*
+			 * Event trigger execution may require an active snapshot.
+			 */
+			PushActiveSnapshot(GetTransactionSnapshot());
+
+			/* Run the triggers. */
+			EventTriggerInvoke(runlist, &trigdata);
+
+			/* Cleanup. */
+			list_free(runlist);
+
+			PopActiveSnapshot();
+		}
+		else
+		{
+			/*
+			 * Runlist is empty: clear dathasloginevt flag
+			 */
+			Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+			HeapTuple	tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+			Form_pg_database db;
+
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+			db = (Form_pg_database) GETSTRUCT(tuple);
+			if (db->dathasloginevt)
+			{
+				/*
+				 * There can be a race condition: a login event trigger may
+				 * have been added after the pg_event_trigger table was
+				 * scanned, and we don't want to erroneously clear the
+				 * dathasloginevt flag in this case. To be sure that this
+				 * hasn't happened, repeat the scan under the pg_database
+				 * table lock.
+				 */
+				AcceptInvalidationMessages();
+				runlist = EventTriggerCommonSetup(NULL,
+												  EVT_Login, "login",
+												  &trigdata);
+				if (runlist == NULL)	/* list is still empty, so clear the
+										 * flag */
+				{
+					db->dathasloginevt = false;
+					CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				}
+				else
+					CacheInvalidateRelcacheByTuple(tuple);
+			}
+			table_close(pg_db, RowExclusiveLock);
+			heap_freetuple(tuple);
+		}
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 38d8b97894..1f9d218cd9 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -41,6 +41,7 @@
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/prepare.h"
 #include "common/pg_prng.h"
 #include "executor/spi.h"
@@ -4179,6 +4180,9 @@ PostgresMain(const char *dbname, const char *username)
 	initStringInfo(&row_description_buf);
 	MemoryContextSwitchTo(TopMemoryContext);
 
+	/* Fire any defined login event triggers, if appropriate */
+	EventTriggerOnLogin();
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 3a9c9f0c50..43a38ae6c7 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "login") == 0)
+			event = EVT_Login;
 		else
 			continue;
 
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 4485ea83b1..928539b0f3 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3026,6 +3026,11 @@ dumpDatabase(Archive *fout)
 		appendPQExpBufferStr(delQry, ";\n");
 	}
 
+	/*
+	 * We do not restore pg_database.dathasloginevt because it is set
+	 * automatically on login event trigger creation.
+	 */
+
 	/* Add database-specific SET options */
 	dumpDatabaseConfig(fout, creaQry, datname, dbCatId.oid);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 010edb685f..b14e20b414 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3329,7 +3329,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end",
+					  "login", "sql_drop");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat
index e7e42d6023..468d8453a5 100644
--- a/src/include/catalog/pg_database.dat
+++ b/src/include/catalog/pg_database.dat
@@ -15,7 +15,7 @@
 { oid => '1', oid_symbol => 'TemplateDbOid',
   descr => 'default template for new databases',
   datname => 'template1', encoding => 'ENCODING', datistemplate => 't',
-  datallowconn => 't', datconnlimit => '-1', datfrozenxid => '0',
+  datallowconn => 't', dathasloginevt => 'f', datconnlimit => '-1', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datcollate => 'LC_COLLATE',
   datctype => 'LC_CTYPE', datacl => '_null_' },
 
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index 76adbd4aad..4dd40bf693 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -46,6 +46,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
 	/* new connections allowed? */
 	bool		datallowconn;
 
+	/* database has login event triggers? */
+	bool		dathasloginevt;
+
 	/* max connections allowed (-1=no limit) */
 	int32		datconnlimit;
 
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 10091c3aaf..c9efee59bf 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -54,6 +54,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnLogin(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 4bc7ddf410..d6e408e1ed 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
+PG_CMDTAG(CMDTAG_LOGIN, "LOGIN", true, false, false)
 PG_CMDTAG(CMDTAG_MOVE, "MOVE", false, false, true)
 PG_CMDTAG(CMDTAG_NOTIFY, "NOTIFY", false, false, false)
 PG_CMDTAG(CMDTAG_PREPARE, "PREPARE", false, false, false)
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index ddb67a68fa..a66344aacb 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Login,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 583ee87da8..94d69039c4 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,26 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+
+CREATE TABLE user_logins(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO user_logins (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 my $primary_lsn = $node_primary->lsn('write');
 $node_primary->wait_for_catchup($node_standby_1, 'replay', $primary_lsn);
@@ -387,6 +407,9 @@ sub replay_check
 
 replay_check();
 
+$node_standby_1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$node_standby_2->safe_psql('postgres', "SELECT 2", extra_params => [ '-U', 'regress_user', '-w' ]);
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 44d545de25..fb2c799ebb 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -604,3 +604,41 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     1
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     2
+(1 row)
+
+-- Check dathasloginevt in system catalog
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+ dathasloginevt 
+----------------
+ t
+(1 row)
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 1446cf8cc8..3123cbb23d 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -462,3 +462,27 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+SELECT COUNT(*) FROM user_logins;
+\c
+SELECT COUNT(*) FROM user_logins;
+
+-- Check dathasloginevt in system catalog
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
-- 
2.27.0

#88Daniel Gustafsson
daniel@yesql.se
In reply to: Greg Nancarrow (#87)
Re: On login trigger: take three

On 15 Feb 2022, at 11:07, Greg Nancarrow <gregn4422@gmail.com> wrote:

I've attached a re-based version (no functional changes from the
previous) to fix cfbot failures.

Thanks for adopting this patch, I took another look at it today and have some
comments.

+ This flag is used internally by <productname>PostgreSQL</productname> and should not be manually changed by DBA or application.
I think we should reword this to something like "This flag is used internally
by <productname>PostgreSQL</productname> and should not be altered or relied
upon for monitoring". We really don't want anyone touching or interpreting
this value since there is no guarantee that it will be accurate.

+ new_record[Anum_pg_database_dathasloginevt - 1] = BoolGetDatum(datform->dathasloginevt);
Since the corresponding new_record_repl valus is false, this won't do anything
and can be removed. We will use the old value anyways.

+ if (strcmp(eventname, "login") == 0)
I think this block warrants a comment on why it only applies to login triggers.

+		db = (Form_pg_database) GETSTRUCT(tuple);
+		if (!db->dathasloginevt)
+		{
+			db->dathasloginevt = true;
+			CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+		}
+		else
+			CacheInvalidateRelcacheByTuple(tuple);
This needs a CommandCounterIncrement inside the if () { .. } block right?
+	/* Get the command tag. */
+	tag = (event == EVT_Login) ? CMDTAG_LOGIN : CreateCommandTag(parsetree);
To match project style I think this should be an if-then-else block.  Also,
while it's tempting to move this before the assertion block in order to reuse
it there, it does mean that we are calling this in a hot codepath before we
know if it's required.  I think we should restore the previous programming with
a separate CreateCommandTag call inside the assertion block and move this one
back underneath the fast-path exit.
+					CacheInvalidateRelcacheByTuple(tuple);
+			}
+			table_close(pg_db, RowExclusiveLock);
+			heap_freetuple(tuple);
+		}
+	}
+	CommitTransactionCommand();
Since we are commiting the transaction just after closing the table, is the
relcache invalidation going to achieve much?  I guess registering the event
doesn't hurt much?
+		/*
+		 * There can be a race condition: a login event trigger may
+		 * have been added after the pg_event_trigger table was
+		 * scanned, and we don't want to erroneously clear the
+		 * dathasloginevt flag in this case. To be sure that this
+		 * hasn't happened, repeat the scan under the pg_database
+		 * table lock.
+		 */
+		AcceptInvalidationMessages();
+		runlist = EventTriggerCommonSetup(NULL,
+						  EVT_Login, "login",
+						  &trigdata);
It seems conceptually wrong to allocate this in the TopTransactionContext when
we only generate the runlist to throw it away.  At the very least I think we
should free the returned list.

+ if (runlist == NULL) /* list is still empty, so clear the
This should check for NIL and not NULL.

Have you done any benchmarking on this patch? With this version I see a small
slowdown on connection time of ~1.5% using pgbench for the case where there are
no login event triggers defined. With a login event trigger calling an empty
plpgsql function there is a ~30% slowdown (corresponding to ~1ms on my system).
When there is a login event trigger defined all bets are off however, since the
function called can be arbitrarily complex and it's up to the user to measure
and decide - but the bare overhead we impose is still of interest of course.

--
Daniel Gustafsson https://vmware.com/

#89Robert Haas
robertmhaas@gmail.com
In reply to: Greg Nancarrow (#87)
Re: On login trigger: take three

On Tue, Feb 15, 2022 at 5:07 AM Greg Nancarrow <gregn4422@gmail.com> wrote:

I've attached a re-based version (no functional changes from the
previous) to fix cfbot failures.

I tried this:

rhaas=# create function on_login_proc() returns event_trigger as
$$begin perform pg_sleep(10000000); end$$ language plpgsql;
CREATE FUNCTION
rhaas=# create event trigger on_login_trigger on login execute
procedure on_login_proc();

When I then attempt to connect via psql, it hangs, as expected. When I
press ^C, psql exits, but the backend process is not cancelled and
just keeps chugging along in the background. The good news is that if
I connect to another database, I can cancel all of the hung sessions
using pg_cancel_backend(), and all of those processes then promptly
exit, and presumably I could accomplish the same thing by sending them
SIGINT directly. But it's still not great behavior. It would be easy
to use up a pile of connection slots inadvertently and have to go to
some trouble to get access to the server again. Since this is a psql
behavior and not a server behavior, one could argue that it's
unrelated to this patch, but in practice this patch seems to increase
the chances of people running into problems quite a lot.

--
Robert Haas
EDB: http://www.enterprisedb.com

#90Andres Freund
andres@anarazel.de
In reply to: Greg Nancarrow (#87)
Re: On login trigger: take three

Hi,

On 2022-02-15 21:07:15 +1100, Greg Nancarrow wrote:

Subject: [PATCH v25] Add a new "login" event and login event trigger support.

The login event occurs when a user logs in to the system.

I think this needs a HUGE warning in the docs about most event triggers
needing to check pg_is_in_recovery() or breaking hot standby. And certainly
the example given needs to include an pg_is_in_recovery() check.

+   <para>
+     The <literal>login</literal> event occurs when a user logs in to the
+     system.
+     Any bugs in a trigger procedure for this event may prevent successful
+     login to the system. Such bugs may be fixed after first restarting the
+     system in single-user mode (as event triggers are disabled in this mode).
+     See the <xref linkend="app-postgres"/> reference page for details about
+     using single-user mode.
+   </para>

I'm strongly against adding any new dependencies on single user mode.

A saner approach might be a superuser-only GUC that can be set as part of the
connection data (e.g. PGOPTIONS='-c ignore_login_event_trigger=true').

@@ -293,6 +297,27 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
CatalogTupleInsert(tgrel, tuple);
heap_freetuple(tuple);

+	if (strcmp(eventname, "login") == 0)
+	{
+		Form_pg_database db;
+		Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+
+		/* Set dathasloginevt flag in pg_database */
+		tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+		db = (Form_pg_database) GETSTRUCT(tuple);
+		if (!db->dathasloginevt)
+		{
+			db->dathasloginevt = true;
+			CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+		}
+		else
+			CacheInvalidateRelcacheByTuple(tuple);
+		table_close(pg_db, RowExclusiveLock);
+		heap_freetuple(tuple);
+	}
+
/* Depend on owner. */
recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);

Maybe I am confused, but isn't that CacheInvalidateRelcacheByTuple call
*entirely* bogus? CacheInvalidateRelcacheByTuple() expects a pg_class tuple,
but you're passing in a pg_database tuple? And what is relcache invalidation
even supposed to achieve here?

I think this should mention that ->dathasloginevt is unset on login when
appropriate.

+/*
+ * Fire login event triggers.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId))
+		return;
+
+	StartTransactionCommand();
+
+	if (DatabaseHasLoginEventTriggers())
+	{
+		runlist = EventTriggerCommonSetup(NULL,
+										  EVT_Login, "login",
+										  &trigdata);
+
+		if (runlist != NIL)
+		{
+			/*
+			 * Make sure anything the main command did will be visible to the
+			 * event triggers.
+			 */
+			CommandCounterIncrement();

"Main command"?

It's not clear to my why a CommandCounterIncrement() could be needed here -
which previous writes do you need to make visible?

+			/*
+			 * Runlist is empty: clear dathasloginevt flag
+			 */
+			Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+			HeapTuple	tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+			Form_pg_database db;
+
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+			db = (Form_pg_database) GETSTRUCT(tuple);
+			if (db->dathasloginevt)
+			{
+				/*
+				 * There can be a race condition: a login event trigger may
+				 * have been added after the pg_event_trigger table was
+				 * scanned, and we don't want to erroneously clear the
+				 * dathasloginevt flag in this case. To be sure that this
+				 * hasn't happened, repeat the scan under the pg_database
+				 * table lock.
+				 */
+				AcceptInvalidationMessages();
+				runlist = EventTriggerCommonSetup(NULL,
+												  EVT_Login, "login",
+												  &trigdata);

This doesn't work. RowExclusiveLock doesn't conflict with another
RowExclusiveLock.

What is the AcceptInvalidationMessages() intending to do here?

+				if (runlist == NULL)	/* list is still empty, so clear the
+										 * flag */
+				{
+					db->dathasloginevt = false;
+					CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				}
+				else
+					CacheInvalidateRelcacheByTuple(tuple);

Copy of the bogus relcache stuff.

Greetings,

Andres Freund

#91Daniel Gustafsson
daniel@yesql.se
In reply to: Andres Freund (#90)
Re: On login trigger: take three

On 12 Mar 2022, at 03:46, Andres Freund <andres@anarazel.de> wrote:

+   <para>
+     The <literal>login</literal> event occurs when a user logs in to the
+     system.
+     Any bugs in a trigger procedure for this event may prevent successful
+     login to the system. Such bugs may be fixed after first restarting the
+     system in single-user mode (as event triggers are disabled in this mode).
+     See the <xref linkend="app-postgres"/> reference page for details about
+     using single-user mode.
+   </para>

I'm strongly against adding any new dependencies on single user mode.

A saner approach might be a superuser-only GUC that can be set as part of the
connection data (e.g. PGOPTIONS='-c ignore_login_event_trigger=true').

This, and similar approaches, has been discussed in this thread. I'm not a fan
of holes punched with GUC's like this, but if you plan on removing single-user
mode (as I recall seeing in an initdb thread) altogether then that kills the
discussion. So.

Since we already recommend single-user mode to handle broken event triggers, we
should IMO do something to cover all of them rather than risk ending up with
one disabling GUC per each event type. Right now we have this on the CREATE
EVENT TRIGGER reference page:

"Event triggers are disabled in single-user mode (see postgres). If an
erroneous event trigger disables the database so much that you can't even
drop the trigger, restart in single-user mode and you'll be able to do
that."

Something like a '-c ignore_event_trigger=<event|all>' GUC perhaps?

--
Daniel Gustafsson https://vmware.com/

#92Andres Freund
andres@anarazel.de
In reply to: Daniel Gustafsson (#91)
Re: On login trigger: take three

Hi,

On 2022-03-13 23:31:03 +0100, Daniel Gustafsson wrote:

On 12 Mar 2022, at 03:46, Andres Freund <andres@anarazel.de> wrote:

+   <para>
+     The <literal>login</literal> event occurs when a user logs in to the
+     system.
+     Any bugs in a trigger procedure for this event may prevent successful
+     login to the system. Such bugs may be fixed after first restarting the
+     system in single-user mode (as event triggers are disabled in this mode).
+     See the <xref linkend="app-postgres"/> reference page for details about
+     using single-user mode.
+   </para>

I'm strongly against adding any new dependencies on single user mode.

A saner approach might be a superuser-only GUC that can be set as part of the
connection data (e.g. PGOPTIONS='-c ignore_login_event_trigger=true').

This, and similar approaches, has been discussed in this thread. I'm not a fan
of holes punched with GUC's like this, but if you plan on removing single-user
mode (as I recall seeing in an initdb thread) altogether then that kills the
discussion. So.

Even if we end up not removing single user mode, I think it's not OK to add
new failure modes that require single user mode to resolve after not-absurd
operations (I'm ok with needing single user mode if somebody does delete from
pg_authid). It's too hard to reach for most.

We already have GUCs like row_security, so it doesn't seem insane to add one
that disables login event triggers. What is the danger that you see?

Since we already recommend single-user mode to handle broken event triggers, we
should IMO do something to cover all of them rather than risk ending up with
one disabling GUC per each event type. Right now we have this on the CREATE
EVENT TRIGGER reference page:

IMO the other types of event triggers make it a heck of a lot harder to get
yourself into a situation that you can't get out of...

"Event triggers are disabled in single-user mode (see postgres). If an
erroneous event trigger disables the database so much that you can't even
drop the trigger, restart in single-user mode and you'll be able to do
that."
Something like a '-c ignore_event_trigger=<event|all>' GUC perhaps?

Did you mean login instead of event?

Something like it would work for me. It probably should be
GUC_DISALLOW_IN_FILE?

Greetings,

Andres Freund

#93Tom Lane
tgl@sss.pgh.pa.us
In reply to: Andres Freund (#92)
Re: On login trigger: take three

Andres Freund <andres@anarazel.de> writes:

On 2022-03-13 23:31:03 +0100, Daniel Gustafsson wrote:

Something like a '-c ignore_event_trigger=<event|all>' GUC perhaps?

Did you mean login instead of event?

Something like it would work for me. It probably should be
GUC_DISALLOW_IN_FILE?

Why? Inserting such a setting in postgresql.conf and restarting
would be the first thing I'd think of if I needed to get out
of a problem. The only other way is to set it on the postmaster
command line, which is going to be awkward-to-impossible with
most system-provided start scripts.

regards, tom lane

#94Andres Freund
andres@anarazel.de
In reply to: Tom Lane (#93)
Re: On login trigger: take three

Hi,

On 2022-03-13 19:57:08 -0400, Tom Lane wrote:

Andres Freund <andres@anarazel.de> writes:

On 2022-03-13 23:31:03 +0100, Daniel Gustafsson wrote:

Something like a '-c ignore_event_trigger=<event|all>' GUC perhaps?

Did you mean login instead of event?

Something like it would work for me. It probably should be
GUC_DISALLOW_IN_FILE?

Why? Inserting such a setting in postgresql.conf and restarting
would be the first thing I'd think of if I needed to get out
of a problem. The only other way is to set it on the postmaster
command line, which is going to be awkward-to-impossible with
most system-provided start scripts.

I was thinking that the way to use it would be to specify it as a client
option. Like PGOPTIONS='-c ignore_event_trigger=login' psql.

Greetings,

Andres Freund

#95Tom Lane
tgl@sss.pgh.pa.us
In reply to: Andres Freund (#94)
Re: On login trigger: take three

Andres Freund <andres@anarazel.de> writes:

I was thinking that the way to use it would be to specify it as a client
option. Like PGOPTIONS='-c ignore_event_trigger=login' psql.

Ugh ... that would allow people (at least superusers) to bypass
the login trigger at will, which seems to me to break a lot of
the use-cases for the feature. I supposed we'd want this to be a
PGC_POSTMASTER setting for security reasons.

regards, tom lane

#96Andres Freund
andres@anarazel.de
In reply to: Tom Lane (#95)
Re: On login trigger: take three

On 2022-03-13 20:35:44 -0400, Tom Lane wrote:

Andres Freund <andres@anarazel.de> writes:

I was thinking that the way to use it would be to specify it as a client
option. Like PGOPTIONS='-c ignore_event_trigger=login' psql.

Ugh ... that would allow people (at least superusers) to bypass
the login trigger at will, which seems to me to break a lot of
the use-cases for the feature. I supposed we'd want this to be a
PGC_POSTMASTER setting for security reasons.

Shrug. This doesn't seem to add actual security to me.

#97Daniel Gustafsson
daniel@yesql.se
In reply to: Andres Freund (#92)
Re: On login trigger: take three

On 14 Mar 2022, at 00:33, Andres Freund <andres@anarazel.de> wrote:

We already have GUCs like row_security, so it doesn't seem insane to add one
that disables login event triggers. What is the danger that you see?

My fear is that GUCs like that end up as permanent fixtures in scripts etc
after having been used temporary, and then X timeunits later someone realize
that the backup has never actually really worked due to a subtle issue, or
something else unpleasant.

The row_security GUC is kind of different IMO, as it's required for pg_dump
(though it can be used in the same way as the above).

--
Daniel Gustafsson https://vmware.com/

#98Robert Haas
robertmhaas@gmail.com
In reply to: Andres Freund (#92)
Re: On login trigger: take three

On Sun, Mar 13, 2022 at 7:34 PM Andres Freund <andres@anarazel.de> wrote:

IMO the other types of event triggers make it a heck of a lot harder to get
yourself into a situation that you can't get out of...

In particular, unless something has changed since I committed this
stuff originally, there's no existing type of event trigger than can
prevent the superuser from logging in and running DROP EVENT TRIGGER
-- or a SELECT on the system catalogs to find out what to drop. That
was very much a deliberate decision on my part.

I think it's fine to require dropping to single-user mode as a way of
recovering from extreme situations where, for example, there are
corrupted database files. If we don't need it even then, cool, but if
we do, I'm not sad. But all we're talking about here is somebody maybe
running a command that perhaps they should not have run. Having to
take the whole system down to recover from that seems excessively
painful.

--
Robert Haas
EDB: http://www.enterprisedb.com

#99Daniel Gustafsson
daniel@yesql.se
In reply to: Andres Freund (#94)
3 attachment(s)
Re: On login trigger: take three

On 14 Mar 2022, at 01:08, Andres Freund <andres@anarazel.de> wrote:

I was thinking that the way to use it would be to specify it as a client
option. Like PGOPTIONS='-c ignore_event_trigger=login' psql.

Attached is a quick PoC/sketch of such a patch, where 0001 adds a guc, 0002 is
the previously posted v25 login event trigger patch, and 0003 adapts is to
allow ignoring it (0002/0003 split only for illustrative purposes of course).
Is this along the lines of what you were thinking?

--
Daniel Gustafsson https://vmware.com/

Attachments:

v26-0003-Add-IGNORE_EVENT_TRIGGER_LOGIN.patchapplication/octet-stream; name=v26-0003-Add-IGNORE_EVENT_TRIGGER_LOGIN.patch; x-unix-mode=0644Download
From b5cef56f15aec91ead15f8f67dc639cdaf49908e Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <daniel@yesql.se>
Date: Mon, 14 Mar 2022 14:40:04 +0100
Subject: [PATCH v26 3/3] Add IGNORE_EVENT_TRIGGER_LOGIN

---
 doc/src/sgml/event-trigger.sgml      |  6 ++++--
 src/backend/commands/event_trigger.c | 10 +++++++---
 src/backend/utils/misc/guc.c         |  1 +
 src/include/commands/event_trigger.h |  3 ++-
 4 files changed, 14 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 1d10b98b76..1577426dc3 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -40,8 +40,10 @@
      The <literal>login</literal> event occurs when a user logs in to the
      system.
      Any bugs in a trigger procedure for this event may prevent successful
-     login to the system. Such bugs may be fixed after first restarting the
-     system in single-user mode (as event triggers are disabled in this mode).
+     login to the system. Such bugs may be fixed after first reconnecting to
+     the system with event triggers disabled (see <xref="guc-ignore-event-trigger"/>)
+     or using restarting the system in single-user mode (as event triggers are
+     disabled in this mode).
      See the <xref linkend="app-postgres"/> reference page for details about
      using single-user mode.
    </para>
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index ef09080a74..ca9b7c2efd 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -858,7 +858,8 @@ EventTriggerOnLogin(void)
 	 * See EventTriggerDDLCommandStart for a discussion about why event
 	 * triggers are disabled in single user mode.
 	 */
-	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId))
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId)
+		|| ignore_event_trigger_check(EVT_Login))
 		return;
 
 	StartTransactionCommand();
@@ -2312,8 +2313,7 @@ stringify_adefprivs_objtype(ObjectType objtype)
 
 /*
  * Checks whether the specified event is ignored by the ignore_event_trigger
- * GUC or not. Currently, the GUC only supports ignoreing all or nothing but
- * that will most likely change so the function takes an event to aid that.
+ * GUC or not.
  */
 static bool
 ignore_event_trigger_check(EventTriggerEvent event)
@@ -2323,6 +2323,10 @@ ignore_event_trigger_check(EventTriggerEvent event)
 	if (ignore_event_trigger == IGNORE_EVENT_TRIGGER_ALL)
 		return true;
 
+	if (ignore_event_trigger == IGNORE_EVENT_TRIGGER_LOGIN
+		&& event == EVT_Login)
+		return true;
+
 	/* IGNORE_EVENT_TRIGGER_NONE */
 	return false;
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 088c4690d9..c5d53386e7 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -569,6 +569,7 @@ static const struct config_enum_entry wal_compression_options[] = {
 static const struct config_enum_entry ignore_event_trigger_options[] = {
 	{"none", IGNORE_EVENT_TRIGGER_NONE, false},
 	{"all", IGNORE_EVENT_TRIGGER_ALL, false},
+	{"login", IGNORE_EVENT_TRIGGER_LOGIN, false},
 	{NULL, 0, false}
 };
 
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 4d9e7dba94..2400cc7d7a 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -32,7 +32,8 @@ typedef struct EventTriggerData
 typedef enum ignore_event_trigger_events
 {
 	IGNORE_EVENT_TRIGGER_NONE,
-	IGNORE_EVENT_TRIGGER_ALL
+	IGNORE_EVENT_TRIGGER_ALL,
+	IGNORE_EVENT_TRIGGER_LOGIN
 } IgnoreEventTriggersEvents;
 
 extern int ignore_event_trigger;
-- 
2.24.3 (Apple Git-128)

v26-0002-v25-of-event-trigger-patch.patchapplication/octet-stream; name=v26-0002-v25-of-event-trigger-patch.patch; x-unix-mode=0644Download
From 75d820f4af69234cac51527da72f12e117dbf3e2 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <daniel@yesql.se>
Date: Mon, 14 Mar 2022 14:27:10 +0100
Subject: [PATCH v26 2/3] v25 of event trigger patch

---
 doc/src/sgml/bki.sgml                       |   2 +-
 doc/src/sgml/catalogs.sgml                  |  11 ++
 doc/src/sgml/ecpg.sgml                      |   2 +
 doc/src/sgml/event-trigger.sgml             |  70 ++++++++-
 src/backend/commands/dbcommands.c           |   2 +
 src/backend/commands/event_trigger.c        | 162 +++++++++++++++++---
 src/backend/tcop/postgres.c                 |   4 +
 src/backend/utils/cache/evtcache.c          |   2 +
 src/bin/pg_dump/pg_dump.c                   |   5 +
 src/bin/psql/tab-complete.c                 |   3 +-
 src/include/catalog/pg_database.dat         |   2 +-
 src/include/catalog/pg_database.h           |   3 +
 src/include/commands/event_trigger.h        |   1 +
 src/include/tcop/cmdtaglist.h               |   1 +
 src/include/utils/evtcache.h                |   3 +-
 src/test/recovery/t/001_stream_rep.pl       |  23 +++
 src/test/regress/expected/event_trigger.out |  38 +++++
 src/test/regress/sql/event_trigger.sql      |  24 +++
 18 files changed, 335 insertions(+), 23 deletions(-)

diff --git a/doc/src/sgml/bki.sgml b/doc/src/sgml/bki.sgml
index 33955494c6..caef1df200 100644
--- a/doc/src/sgml/bki.sgml
+++ b/doc/src/sgml/bki.sgml
@@ -183,7 +183,7 @@
 { oid => '1', oid_symbol => 'TemplateDbOid',
   descr => 'database\'s default template',
   datname => 'template1', encoding => 'ENCODING', datistemplate => 't',
-  datallowconn => 't', datconnlimit => '-1', datfrozenxid => '0',
+  datallowconn => 't', dathasloginevt => 'f', datconnlimit => '-1', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datcollate => 'LC_COLLATE',
   datctype => 'LC_CTYPE', datacl => '_null_' },
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 7777d60514..3a3c943176 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2973,6 +2973,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>dathasloginevt</structfield> <type>bool</type>
+      </para>
+      <para>
+        Indicates that there are login event triggers defined for this database.
+        This flag is used to avoid extra lookups on the <structname>pg_event_trigger</structname> table during each backend startup.
+        This flag is used internally by <productname>PostgreSQL</productname> and should not be manually changed by DBA or application.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>datconnlimit</structfield> <type>int4</type>
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index cdc4761c60..197b476205 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -4731,6 +4731,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = t (type: 1)
 datallowconn = t (type: 1)
+dathasloginevt = f (type: 1)
 datconnlimit = -1 (type: 5)
 datfrozenxid = 379 (type: 1)
 dattablespace = 1663 (type: 1)
@@ -4755,6 +4756,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = f (type: 1)
 datallowconn = t (type: 1)
+dathasloginevt = f (type: 1)
 datconnlimit = -1 (type: 5)
 datfrozenxid = 379 (type: 1)
 dattablespace = 1663 (type: 1)
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 9c66f97b0f..1d10b98b76 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>login</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,16 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>login</literal> event occurs when a user logs in to the
+     system.
+     Any bugs in a trigger procedure for this event may prevent successful
+     login to the system. Such bugs may be fixed after first restarting the
+     system in single-user mode (as event triggers are disabled in this mode).
+     See the <xref linkend="app-postgres"/> reference page for details about
+     using single-user mode.
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1156,7 +1167,7 @@ typedef struct EventTriggerData
   </sect1>
 
   <sect1 id="event-trigger-example">
-   <title>A Complete Event Trigger Example</title>
+   <title>A C language Event Trigger Example</title>
 
    <para>
     Here is a very simple example of an event trigger function written in C.
@@ -1296,6 +1307,63 @@ $$;
 CREATE EVENT TRIGGER no_rewrite_allowed
                   ON table_rewrite
    EXECUTE FUNCTION no_rewrite();
+</programlisting>
+   </para>
+ </sect1>
+
+  <sect1 id="event-trigger-database-login-example">
+   <title>A Database Login Event Trigger Example</title>
+
+   <para>
+    The event trigger on the <literal>login</literal> event can be
+    useful for logging user logins, for verifying the connection and
+    assigning roles according to current circumstances, or for some session data
+    initialization.
+   </para>
+
+   <para>
+    The following example demonstrates these options.
+<programlisting>
+-- create test tables and roles
+CREATE TABLE user_login_log (
+  "user" text,
+  "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+  RETURNS event_trigger SECURITY DEFINER
+  LANGUAGE plpgsql AS
+$$
+DECLARE
+  hour integer = EXTRACT('hour' FROM current_time);
+BEGIN
+-- 1) Assign some roles
+IF hour BETWEEN 8 AND 20 THEN         -- at daytime grant the day_worker role
+  EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT    day_worker TO '   || quote_ident(session_user);
+ELSIF hour BETWEEN 2 AND 4 THEN
+  RAISE EXCEPTION 'Login forbidden';  -- do not allow to login these hours
+ELSE                                  -- at other time grant the night_worker role
+  EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT  night_worker TO ' || quote_ident(session_user);
+END IF;
+
+-- 2) Initialize some user session data
+CREATE TEMP TABLE session_storage (x float, y integer);
+
+-- 3) Log the connection time
+INSERT INTO user_login_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+  ON login
+  EXECUTE FUNCTION init_session();
 </programlisting>
    </para>
  </sect1>
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index c37e3c9a9a..f26679a08f 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -631,6 +631,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	new_record[Anum_pg_database_encoding - 1] = Int32GetDatum(encoding);
 	new_record[Anum_pg_database_datistemplate - 1] = BoolGetDatum(dbistemplate);
 	new_record[Anum_pg_database_datallowconn - 1] = BoolGetDatum(dballowconnections);
+	new_record[Anum_pg_database_dathasloginevt - 1] = BoolGetDatum(false);
 	new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 	new_record[Anum_pg_database_datfrozenxid - 1] = TransactionIdGetDatum(src_frozenxid);
 	new_record[Anum_pg_database_datminmxid - 1] = TransactionIdGetDatum(src_minmxid);
@@ -1699,6 +1700,7 @@ AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel)
 		new_record_repl[Anum_pg_database_datconnlimit - 1] = true;
 	}
 
+	new_record[Anum_pg_database_dathasloginevt - 1] = BoolGetDatum(datform->dathasloginevt);
 	newtuple = heap_modify_tuple(tuple, RelationGetDescr(rel), new_record,
 								 new_record_nulls, new_record_repl);
 	CatalogTupleUpdate(rel, &tuple->t_self, newtuple);
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 70995cfc49..ef09080a74 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -43,9 +44,11 @@
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 typedef struct EventTriggerQueryState
@@ -134,6 +137,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "login") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -297,6 +301,27 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
 	CatalogTupleInsert(tgrel, tuple);
 	heap_freetuple(tuple);
 
+	if (strcmp(eventname, "login") == 0)
+	{
+		Form_pg_database db;
+		Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+
+		/* Set dathasloginevt flag in pg_database */
+		tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+		db = (Form_pg_database) GETSTRUCT(tuple);
+		if (!db->dathasloginevt)
+		{
+			db->dathasloginevt = true;
+			CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+		}
+		else
+			CacheInvalidateRelcacheByTuple(tuple);
+		table_close(pg_db, RowExclusiveLock);
+		heap_freetuple(tuple);
+	}
+
 	/* Depend on owner. */
 	recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
 
@@ -566,6 +591,9 @@ EventTriggerCommonSetup(Node *parsetree,
 	ListCell   *lc;
 	List	   *runlist = NIL;
 
+	/* Get the command tag. */
+	tag = (event == EVT_Login) ? CMDTAG_LOGIN : CreateCommandTag(parsetree);
+
 	/*
 	 * We want the list of command tags for which this procedure is actually
 	 * invoked to match up exactly with the list that CREATE EVENT TRIGGER
@@ -581,22 +609,18 @@ EventTriggerCommonSetup(Node *parsetree,
 	 * relevant command tag.
 	 */
 #ifdef USE_ASSERT_CHECKING
+	if (event == EVT_DDLCommandStart ||
+		event == EVT_DDLCommandEnd ||
+		event == EVT_SQLDrop ||
+		event == EVT_Login)
 	{
-		CommandTag	dbgtag;
-
-		dbgtag = CreateCommandTag(parsetree);
-		if (event == EVT_DDLCommandStart ||
-			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
-		{
-			if (!command_tag_event_trigger_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
-		else if (event == EVT_TableRewrite)
-		{
-			if (!command_tag_table_rewrite_ok(dbgtag))
-				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
-		}
+		if (!command_tag_event_trigger_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
+	}
+	else if (event == EVT_TableRewrite)
+	{
+		if (!command_tag_table_rewrite_ok(tag))
+			elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(tag));
 	}
 #endif
 
@@ -605,9 +629,6 @@ EventTriggerCommonSetup(Node *parsetree,
 	if (cachelist == NIL)
 		return NIL;
 
-	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
-
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
 	 * memory context.  Once we start running the command triggers, or indeed
@@ -807,6 +828,111 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Return true if this database has login event triggers, false otherwise.
+ */
+static bool
+DatabaseHasLoginEventTriggers(void)
+{
+	bool		has_login_event_triggers;
+	HeapTuple	tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+	has_login_event_triggers = ((Form_pg_database) GETSTRUCT(tuple))->dathasloginevt;
+	ReleaseSysCache(tuple);
+	return has_login_event_triggers;
+}
+
+/*
+ * Fire login event triggers.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId))
+		return;
+
+	StartTransactionCommand();
+
+	if (DatabaseHasLoginEventTriggers())
+	{
+		runlist = EventTriggerCommonSetup(NULL,
+										  EVT_Login, "login",
+										  &trigdata);
+
+		if (runlist != NIL)
+		{
+			/*
+			 * Make sure anything the main command did will be visible to the
+			 * event triggers.
+			 */
+			CommandCounterIncrement();
+
+			/*
+			 * Event trigger execution may require an active snapshot.
+			 */
+			PushActiveSnapshot(GetTransactionSnapshot());
+
+			/* Run the triggers. */
+			EventTriggerInvoke(runlist, &trigdata);
+
+			/* Cleanup. */
+			list_free(runlist);
+
+			PopActiveSnapshot();
+		}
+		else
+		{
+			/*
+			 * Runlist is empty: clear dathasloginevt flag
+			 */
+			Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+			HeapTuple	tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+			Form_pg_database db;
+
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+			db = (Form_pg_database) GETSTRUCT(tuple);
+			if (db->dathasloginevt)
+			{
+				/*
+				 * There can be a race condition: a login event trigger may
+				 * have been added after the pg_event_trigger table was
+				 * scanned, and we don't want to erroneously clear the
+				 * dathasloginevt flag in this case. To be sure that this
+				 * hasn't happened, repeat the scan under the pg_database
+				 * table lock.
+				 */
+				AcceptInvalidationMessages();
+				runlist = EventTriggerCommonSetup(NULL,
+												  EVT_Login, "login",
+												  &trigdata);
+				if (runlist == NULL)	/* list is still empty, so clear the
+										 * flag */
+				{
+					db->dathasloginevt = false;
+					CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				}
+				else
+					CacheInvalidateRelcacheByTuple(tuple);
+			}
+			table_close(pg_db, RowExclusiveLock);
+			heap_freetuple(tuple);
+		}
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index ba2fcfeb4a..5638cd95b2 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -41,6 +41,7 @@
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/prepare.h"
 #include "common/pg_prng.h"
 #include "jit/jit.h"
@@ -4202,6 +4203,9 @@ PostgresMain(const char *dbname, const char *username)
 	initStringInfo(&row_description_buf);
 	MemoryContextSwitchTo(TopMemoryContext);
 
+	/* Fire any defined login event triggers, if appropriate */
+	EventTriggerOnLogin();
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 3a9c9f0c50..43a38ae6c7 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "login") == 0)
+			event = EVT_Login;
 		else
 			continue;
 
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 4dd24b8c89..4572c1d463 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3026,6 +3026,11 @@ dumpDatabase(Archive *fout)
 		appendPQExpBufferStr(delQry, ";\n");
 	}
 
+	/*
+	 * We do not restore pg_database.dathasloginevt because it is set
+	 * automatically on login event trigger creation.
+	 */
+
 	/* Add database-specific SET options */
 	dumpDatabaseConfig(fout, creaQry, datname, dbCatId.oid);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 17172827a9..d016144a45 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3354,7 +3354,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end",
+					  "login", "sql_drop");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat
index e7e42d6023..468d8453a5 100644
--- a/src/include/catalog/pg_database.dat
+++ b/src/include/catalog/pg_database.dat
@@ -15,7 +15,7 @@
 { oid => '1', oid_symbol => 'TemplateDbOid',
   descr => 'default template for new databases',
   datname => 'template1', encoding => 'ENCODING', datistemplate => 't',
-  datallowconn => 't', datconnlimit => '-1', datfrozenxid => '0',
+  datallowconn => 't', dathasloginevt => 'f', datconnlimit => '-1', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datcollate => 'LC_COLLATE',
   datctype => 'LC_CTYPE', datacl => '_null_' },
 
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index 76adbd4aad..4dd40bf693 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -46,6 +46,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
 	/* new connections allowed? */
 	bool		datallowconn;
 
+	/* database has login event triggers? */
+	bool		dathasloginevt;
+
 	/* max connections allowed (-1=no limit) */
 	int32		datconnlimit;
 
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 9e7671e63f..4d9e7dba94 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -62,6 +62,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnLogin(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 4bc7ddf410..d6e408e1ed 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
+PG_CMDTAG(CMDTAG_LOGIN, "LOGIN", true, false, false)
 PG_CMDTAG(CMDTAG_MOVE, "MOVE", false, false, true)
 PG_CMDTAG(CMDTAG_NOTIFY, "NOTIFY", false, false, false)
 PG_CMDTAG(CMDTAG_PREPARE, "PREPARE", false, false, false)
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index ddb67a68fa..a66344aacb 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Login,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 583ee87da8..94d69039c4 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,26 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+
+CREATE TABLE user_logins(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO user_logins (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 my $primary_lsn = $node_primary->lsn('write');
 $node_primary->wait_for_catchup($node_standby_1, 'replay', $primary_lsn);
@@ -387,6 +407,9 @@ sub replay_check
 
 replay_check();
 
+$node_standby_1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$node_standby_2->safe_psql('postgres', "SELECT 2", extra_params => [ '-U', 'regress_user', '-w' ]);
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 44d545de25..fb2c799ebb 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -604,3 +604,41 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     1
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     2
+(1 row)
+
+-- Check dathasloginevt in system catalog
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+ dathasloginevt 
+----------------
+ t
+(1 row)
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 1446cf8cc8..3123cbb23d 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -462,3 +462,27 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+SELECT COUNT(*) FROM user_logins;
+\c
+SELECT COUNT(*) FROM user_logins;
+
+-- Check dathasloginevt in system catalog
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
-- 
2.24.3 (Apple Git-128)

v26-0001-Provide-a-GUC-for-temporarily-ignoring-event-tri.patchapplication/octet-stream; name=v26-0001-Provide-a-GUC-for-temporarily-ignoring-event-tri.patch; x-unix-mode=0644Download
From 4ba70cc033ffe07d5378e666affeb2c2071a3e52 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <daniel@yesql.se>
Date: Mon, 14 Mar 2022 14:24:09 +0100
Subject: [PATCH v26 1/3] Provide a GUC for temporarily ignoring event triggers

In order to allow troubleshooting and repair without the need for
restarting in single-user mode, allow to ignore event triggers per
session.
---
 doc/src/sgml/config.sgml                   | 15 ++++++++
 doc/src/sgml/ref/create_event_trigger.sgml |  4 ++-
 src/backend/commands/event_trigger.c       | 40 ++++++++++++++++++----
 src/backend/utils/misc/guc.c               | 17 +++++++++
 src/include/commands/event_trigger.h       |  8 +++++
 5 files changed, 76 insertions(+), 8 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 5b763bf60f..40604f19a3 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9195,6 +9195,21 @@ SET XML OPTION { DOCUMENT | CONTENT };
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-ignore-event-trigger" xreflabel="ignore_event_trigger">
+      <term><varname>ignore_event_trigger</varname> (<type>enum</type>)
+      <indexterm>
+       <primary><varname>ignore_event_trigger</varname></primary>
+       <secondary>configuration parameter</secondary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Allows to temporarily disable event triggers from executing in order
+        to troubleshoot and repair faulty event triggers.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
     </sect2>
      <sect2 id="runtime-config-client-format">
diff --git a/doc/src/sgml/ref/create_event_trigger.sgml b/doc/src/sgml/ref/create_event_trigger.sgml
index 22c8119198..1a78555c64 100644
--- a/doc/src/sgml/ref/create_event_trigger.sgml
+++ b/doc/src/sgml/ref/create_event_trigger.sgml
@@ -123,7 +123,9 @@ CREATE EVENT TRIGGER <replaceable class="parameter">name</replaceable>
    Event triggers are disabled in single-user mode (see <xref
    linkend="app-postgres"/>).  If an erroneous event trigger disables the
    database so much that you can't even drop the trigger, restart in
-   single-user mode and you'll be able to do that.
+   single-user mode and you'll be able to do that. Even triggers can also be
+   temporarily disabled for such troubleshooting, see
+   <xref linkend="gux-ignore-event-trigger"/>.
   </para>
  </refsect1>
 
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 3c3fc2515b..70995cfc49 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -72,6 +72,9 @@ typedef struct EventTriggerQueryState
 
 static EventTriggerQueryState *currentEventTriggerState = NULL;
 
+/* GUC parameter */
+int		ignore_event_trigger = IGNORE_EVENT_TRIGGER_NONE;
+
 /* Support for dropped objects */
 typedef struct SQLDropObject
 {
@@ -100,6 +103,7 @@ static void validate_table_rewrite_tags(const char *filtervar, List *taglist);
 static void EventTriggerInvoke(List *fn_oid_list, EventTriggerData *trigdata);
 static const char *stringify_grant_objtype(ObjectType objtype);
 static const char *stringify_adefprivs_objtype(ObjectType objtype);
+static bool ignore_event_trigger_check(EventTriggerEvent event);
 
 /*
  * Create an event trigger.
@@ -658,8 +662,11 @@ EventTriggerDDLCommandStart(Node *parsetree)
 	 * wherein event triggers are disabled.  (Or we could implement
 	 * heapscan-and-sort logic for that case, but having disaster recovery
 	 * scenarios depend on code that's otherwise untested isn't appetizing.)
+	 *
+	 * Additionally, event triggers can be disabled with a superuser only GUC
+	 * to make fixing database easier as per 1 above.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_DDLCommandStart))
 		return;
 
 	runlist = EventTriggerCommonSetup(parsetree,
@@ -693,9 +700,9 @@ EventTriggerDDLCommandEnd(Node *parsetree)
 
 	/*
 	 * See EventTriggerDDLCommandStart for a discussion about why event
-	 * triggers are disabled in single user mode.
+	 * triggers are disabled in single user mode or via GUC.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_DDLCommandEnd))
 		return;
 
 	/*
@@ -741,9 +748,9 @@ EventTriggerSQLDrop(Node *parsetree)
 
 	/*
 	 * See EventTriggerDDLCommandStart for a discussion about why event
-	 * triggers are disabled in single user mode.
+	 * triggers are disabled in single user mode or via a GUC.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_SQLDrop))
 		return;
 
 	/*
@@ -812,9 +819,9 @@ EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason)
 
 	/*
 	 * See EventTriggerDDLCommandStart for a discussion about why event
-	 * triggers are disabled in single user mode.
+	 * triggers are disabled in single user mode or via a GUC.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_TableRewrite))
 		return;
 
 	/*
@@ -2175,3 +2182,22 @@ stringify_adefprivs_objtype(ObjectType objtype)
 
 	return "???";				/* keep compiler quiet */
 }
+
+
+/*
+ * Checks whether the specified event is ignored by the ignore_event_trigger
+ * GUC or not. Currently, the GUC only supports ignoreing all or nothing but
+ * that will most likely change so the function takes an event to aid that.
+ */
+static bool
+ignore_event_trigger_check(EventTriggerEvent event)
+{
+	(void) event;			/* unused parameter */
+
+	if (ignore_event_trigger == IGNORE_EVENT_TRIGGER_ALL)
+		return true;
+
+	/* IGNORE_EVENT_TRIGGER_NONE */
+	return false;
+
+}
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index e7f0a380e6..088c4690d9 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -46,6 +46,7 @@
 #include "catalog/pg_authid.h"
 #include "catalog/storage.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/prepare.h"
 #include "commands/tablespace.h"
 #include "commands/trigger.h"
@@ -565,6 +566,12 @@ static const struct config_enum_entry wal_compression_options[] = {
 	{NULL, 0, false}
 };
 
+static const struct config_enum_entry ignore_event_trigger_options[] = {
+	{"none", IGNORE_EVENT_TRIGGER_NONE, false},
+	{"all", IGNORE_EVENT_TRIGGER_ALL, false},
+	{NULL, 0, false}
+};
+
 /*
  * Options for enum values stored in other modules
  */
@@ -4904,6 +4911,16 @@ static struct config_enum ConfigureNamesEnum[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"ignore_event_trigger", PGC_SUSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Disable event triggers during the session."),
+			NULL
+		},
+		&ignore_event_trigger,
+		IGNORE_EVENT_TRIGGER_NONE, ignore_event_trigger_options,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"wal_level", PGC_POSTMASTER, WAL_SETTINGS,
 			gettext_noop("Sets the level of information written to the WAL."),
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 10091c3aaf..9e7671e63f 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -29,6 +29,14 @@ typedef struct EventTriggerData
 	CommandTag	tag;
 } EventTriggerData;
 
+typedef enum ignore_event_trigger_events
+{
+	IGNORE_EVENT_TRIGGER_NONE,
+	IGNORE_EVENT_TRIGGER_ALL
+} IgnoreEventTriggersEvents;
+
+extern int ignore_event_trigger;
+
 #define AT_REWRITE_ALTER_PERSISTENCE	0x01
 #define AT_REWRITE_DEFAULT_VAL			0x02
 #define AT_REWRITE_COLUMN_REWRITE		0x04
-- 
2.24.3 (Apple Git-128)

#100Andres Freund
andres@anarazel.de
In reply to: Daniel Gustafsson (#99)
Re: On login trigger: take three

Hi,

On 2022-03-14 14:47:09 +0100, Daniel Gustafsson wrote:

On 14 Mar 2022, at 01:08, Andres Freund <andres@anarazel.de> wrote:

I was thinking that the way to use it would be to specify it as a client
option. Like PGOPTIONS='-c ignore_event_trigger=login' psql.

Attached is a quick PoC/sketch of such a patch, where 0001 adds a guc, 0002 is
the previously posted v25 login event trigger patch, and 0003 adapts is to
allow ignoring it (0002/0003 split only for illustrative purposes of course).
Is this along the lines of what you were thinking?

Yes.

Of course there's still the bogus cache invalidation stuff etc that I
commented on upthread.

Greetings,

Andres Freund

#101Daniel Gustafsson
daniel@yesql.se
In reply to: Andres Freund (#100)
2 attachment(s)
Re: On login trigger: take three

On 14 Mar 2022, at 17:10, Andres Freund <andres@anarazel.de> wrote:

Hi,

On 2022-03-14 14:47:09 +0100, Daniel Gustafsson wrote:

On 14 Mar 2022, at 01:08, Andres Freund <andres@anarazel.de> wrote:

I was thinking that the way to use it would be to specify it as a client
option. Like PGOPTIONS='-c ignore_event_trigger=login' psql.

Attached is a quick PoC/sketch of such a patch, where 0001 adds a guc, 0002 is
the previously posted v25 login event trigger patch, and 0003 adapts is to
allow ignoring it (0002/0003 split only for illustrative purposes of course).
Is this along the lines of what you were thinking?

Yes.

Of course there's still the bogus cache invalidation stuff etc that I
commented on upthread.

Yeah, that was the previously posted v25 from the author (who adopted it from
the original author). I took the liberty to quickly poke at the review
comments you had left as well as the ones that I had found to try and progress
the patch. 0001 should really go in it's own thread though to not hide it from
anyone interested who isn't looking at this thread.

--
Daniel Gustafsson https://vmware.com/

Attachments:

v27-0002-Add-a-new-login-event-and-login-event-trigger-su.patchapplication/octet-stream; name=v27-0002-Add-a-new-login-event-and-login-event-trigger-su.patch; x-unix-mode=0644Download
From 1380050a6a139e1c21348233c1d96db481fefd9c Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <daniel@yesql.se>
Date: Mon, 14 Mar 2022 14:27:10 +0100
Subject: [PATCH v27 2/2] Add a new "login" event and login event trigger
 support.

The login event occurs when a user logs in to the system.

Author: Konstantin Knizhnik and Greg Nancarrow
Discussion: https://postgr.es/m/0d46d29f-4558-3af9-9c85-7774e14a7709%40postgrespro.ru
---
 doc/src/sgml/bki.sgml                       |   2 +-
 doc/src/sgml/catalogs.sgml                  |  13 ++
 doc/src/sgml/ecpg.sgml                      |   2 +
 doc/src/sgml/event-trigger.sgml             |  81 +++++++++-
 src/backend/commands/dbcommands.c           |   1 +
 src/backend/commands/event_trigger.c        | 156 +++++++++++++++++++-
 src/backend/tcop/postgres.c                 |   4 +
 src/backend/utils/cache/evtcache.c          |   2 +
 src/backend/utils/misc/guc.c                |   1 +
 src/bin/pg_dump/pg_dump.c                   |   5 +
 src/bin/psql/tab-complete.c                 |   3 +-
 src/include/catalog/pg_database.dat         |   2 +-
 src/include/catalog/pg_database.h           |   3 +
 src/include/commands/event_trigger.h        |   4 +-
 src/include/tcop/cmdtaglist.h               |   1 +
 src/include/utils/evtcache.h                |   3 +-
 src/test/recovery/t/001_stream_rep.pl       |  23 +++
 src/test/regress/expected/event_trigger.out |  38 +++++
 src/test/regress/sql/event_trigger.sql      |  24 +++
 19 files changed, 357 insertions(+), 11 deletions(-)

diff --git a/doc/src/sgml/bki.sgml b/doc/src/sgml/bki.sgml
index 33955494c6..caef1df200 100644
--- a/doc/src/sgml/bki.sgml
+++ b/doc/src/sgml/bki.sgml
@@ -183,7 +183,7 @@
 { oid => '1', oid_symbol => 'TemplateDbOid',
   descr => 'database\'s default template',
   datname => 'template1', encoding => 'ENCODING', datistemplate => 't',
-  datallowconn => 't', datconnlimit => '-1', datfrozenxid => '0',
+  datallowconn => 't', dathasloginevt => 'f', datconnlimit => '-1', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datcollate => 'LC_COLLATE',
   datctype => 'LC_CTYPE', datacl => '_null_' },
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 7777d60514..ae35571317 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2973,6 +2973,19 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>dathasloginevt</structfield> <type>bool</type>
+      </para>
+      <para>
+        Indicates that there are login event triggers defined for this database.
+        This flag is used to avoid extra lookups on the
+        <structname>pg_event_trigger</structname> table during each backend
+        startup.  This flag is used internally by <productname>PostgreSQL</productname>
+        and should not be manually altered or read for monitoring purposes.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>datconnlimit</structfield> <type>int4</type>
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index cdc4761c60..197b476205 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -4731,6 +4731,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = t (type: 1)
 datallowconn = t (type: 1)
+dathasloginevt = f (type: 1)
 datconnlimit = -1 (type: 5)
 datfrozenxid = 379 (type: 1)
 dattablespace = 1663 (type: 1)
@@ -4755,6 +4756,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = f (type: 1)
 datallowconn = t (type: 1)
+dathasloginevt = f (type: 1)
 datconnlimit = -1 (type: 5)
 datfrozenxid = 379 (type: 1)
 dattablespace = 1663 (type: 1)
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 9c66f97b0f..e3b03e0518 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>login</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,18 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>login</literal> event occurs when a user logs in to the
+     system.
+     Any bugs in a trigger procedure for this event may prevent successful
+     login to the system. Such bugs may be fixed after first reconnecting to
+     the system with event triggers disabled (see <xref="guc-ignore-event-trigger"/>)
+     or using restarting the system in single-user mode (as event triggers are
+     disabled in this mode).
+     See the <xref linkend="app-postgres"/> reference page for details about
+     using single-user mode.
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1156,7 +1169,7 @@ typedef struct EventTriggerData
   </sect1>
 
   <sect1 id="event-trigger-example">
-   <title>A Complete Event Trigger Example</title>
+   <title>A C language Event Trigger Example</title>
 
    <para>
     Here is a very simple example of an event trigger function written in C.
@@ -1296,6 +1309,72 @@ $$;
 CREATE EVENT TRIGGER no_rewrite_allowed
                   ON table_rewrite
    EXECUTE FUNCTION no_rewrite();
+</programlisting>
+   </para>
+ </sect1>
+
+  <sect1 id="event-trigger-database-login-example">
+   <title>A Database Login Event Trigger Example</title>
+
+   <para>
+    The event trigger on the <literal>login</literal> event can be
+    useful for logging user logins, for verifying the connection and
+    assigning roles according to current circumstances, or for some session
+    data initialization. It is vital that any event trigger using the
+    <literal>login</literal> event checks whether or not the database is in
+    recovery.
+   </para>
+
+   <para>
+    The following example demonstrates these options.
+<programlisting>
+-- create test tables and roles
+CREATE TABLE user_login_log (
+  "user" text,
+  "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+  RETURNS event_trigger SECURITY DEFINER
+  LANGUAGE plpgsql AS
+$$
+DECLARE
+  hour integer = EXTRACT('hour' FROM current_time);
+  rec boolean;
+BEGIN
+-- Ensure the database is not in recovery
+SELECT pg_is_in_recovery() INTO rec;
+IF rec THEN
+  RETURN;
+END IF
+
+-- 1) Assign some roles
+IF hour BETWEEN 8 AND 20 THEN         -- at daytime grant the day_worker role
+  EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT    day_worker TO '   || quote_ident(session_user);
+ELSIF hour BETWEEN 2 AND 4 THEN
+  RAISE EXCEPTION 'Login forbidden';  -- do not allow to login these hours
+ELSE                                  -- at other time grant the night_worker role
+  EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT  night_worker TO ' || quote_ident(session_user);
+END IF;
+
+-- 2) Initialize some user session data
+CREATE TEMP TABLE session_storage (x float, y integer);
+
+-- 3) Log the connection time
+INSERT INTO user_login_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+  ON login
+  EXECUTE FUNCTION init_session();
 </programlisting>
    </para>
  </sect1>
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index c37e3c9a9a..d49925b948 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -631,6 +631,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	new_record[Anum_pg_database_encoding - 1] = Int32GetDatum(encoding);
 	new_record[Anum_pg_database_datistemplate - 1] = BoolGetDatum(dbistemplate);
 	new_record[Anum_pg_database_datallowconn - 1] = BoolGetDatum(dballowconnections);
+	new_record[Anum_pg_database_dathasloginevt - 1] = BoolGetDatum(false);
 	new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 	new_record[Anum_pg_database_datfrozenxid - 1] = TransactionIdGetDatum(src_frozenxid);
 	new_record[Anum_pg_database_datminmxid - 1] = TransactionIdGetDatum(src_minmxid);
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 70995cfc49..4e2e6629bd 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -37,15 +38,18 @@
 #include "miscadmin.h"
 #include "parser/parse_func.h"
 #include "pgstat.h"
+#include "storage/lmgr.h"
 #include "tcop/deparse_utility.h"
 #include "tcop/utility.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 typedef struct EventTriggerQueryState
@@ -134,6 +138,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "login") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -297,6 +302,30 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
 	CatalogTupleInsert(tgrel, tuple);
 	heap_freetuple(tuple);
 
+	/*
+	 * Login event triggers have an additional flag in pg_database to allow
+	 * faster lookups in hot codepaths. Set the flag unless already True.
+	 */
+	if (strcmp(eventname, "login") == 0)
+	{
+		Form_pg_database db;
+		Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+
+		/* Set dathasloginevt flag in pg_database */
+		tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+		db = (Form_pg_database) GETSTRUCT(tuple);
+		if (!db->dathasloginevt)
+		{
+			db->dathasloginevt = true;
+			CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+			CommandCounterIncrement();
+		}
+		table_close(pg_db, RowExclusiveLock);
+		heap_freetuple(tuple);
+	}
+
 	/* Depend on owner. */
 	recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
 
@@ -584,10 +613,15 @@ EventTriggerCommonSetup(Node *parsetree,
 	{
 		CommandTag	dbgtag;
 
-		dbgtag = CreateCommandTag(parsetree);
+		if (event == EVT_Login)
+			dbgtag = CMDTAG_LOGIN;
+		else
+			dbgtag = CreateCommandTag(parsetree);
+
 		if (event == EVT_DDLCommandStart ||
 			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
+			event == EVT_SQLDrop ||
+			event == EVT_Login)
 		{
 			if (!command_tag_event_trigger_ok(dbgtag))
 				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
@@ -606,7 +640,10 @@ EventTriggerCommonSetup(Node *parsetree,
 		return NIL;
 
 	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
+	if (event == EVT_Login)
+		tag = CMDTAG_LOGIN;
+	else
+		tag = CreateCommandTag(parsetree);
 
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
@@ -807,6 +844,112 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Return true if this database has login event triggers, false otherwise.
+ */
+static bool
+DatabaseHasLoginEventTriggers(void)
+{
+	bool		has_login_event_triggers;
+	HeapTuple	tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+	has_login_event_triggers = ((Form_pg_database) GETSTRUCT(tuple))->dathasloginevt;
+	ReleaseSysCache(tuple);
+	return has_login_event_triggers;
+}
+
+/*
+ * Fire login event triggers if any are present.  The dathasloginevt
+ * pg_database flag is left when an event trigger is dropped, to avoid
+ * complicating the codepath in the case of multiple event triggers.  This
+ * function will instead unset the flag if no trigger is defined.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId)
+		|| ignore_event_trigger_check(EVT_Login))
+		return;
+
+	StartTransactionCommand();
+
+	if (DatabaseHasLoginEventTriggers())
+	{
+		runlist = EventTriggerCommonSetup(NULL,
+										  EVT_Login, "login",
+										  &trigdata);
+
+		if (runlist != NIL)
+		{
+			/*
+			 * Event trigger execution may require an active snapshot.
+			 */
+			PushActiveSnapshot(GetTransactionSnapshot());
+
+			/* Run the triggers. */
+			EventTriggerInvoke(runlist, &trigdata);
+
+			/* Cleanup. */
+			list_free(runlist);
+
+			PopActiveSnapshot();
+		}
+		else
+		{
+			/*
+			 * Runlist is empty: clear dathasloginevt flag
+			 */
+			Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+			HeapTuple	tuple;
+			Form_pg_database db;
+
+			LockSharedObject(DatabaseRelationId, MyDatabaseId, 0, AccessExclusiveLock);
+
+			tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+			db = (Form_pg_database) GETSTRUCT(tuple);
+			if (db->dathasloginevt)
+			{
+				/*
+				 * There can be a race condition: a login event trigger may
+				 * have been added after the pg_event_trigger table was
+				 * scanned, and we don't want to erroneously clear the
+				 * dathasloginevt flag in this case. To be sure that this
+				 * hasn't happened, repeat the scan under the pg_database
+				 * table lock.
+				 */
+				runlist = EventTriggerCommonSetup(NULL,
+												  EVT_Login, "login",
+												  &trigdata);
+				if (runlist == NIL)	/* list is still empty, so clear the
+										 * flag */
+				{
+					db->dathasloginevt = false;
+					CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				}
+				else
+					list_free(runlist);
+			}
+			table_close(pg_db, RowExclusiveLock);
+			heap_freetuple(tuple);
+		}
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
@@ -2186,8 +2329,7 @@ stringify_adefprivs_objtype(ObjectType objtype)
 
 /*
  * Checks whether the specified event is ignored by the ignore_event_trigger
- * GUC or not. Currently, the GUC only supports ignoreing all or nothing but
- * that will most likely change so the function takes an event to aid that.
+ * GUC or not.
  */
 static bool
 ignore_event_trigger_check(EventTriggerEvent event)
@@ -2197,6 +2339,10 @@ ignore_event_trigger_check(EventTriggerEvent event)
 	if (ignore_event_trigger == IGNORE_EVENT_TRIGGER_ALL)
 		return true;
 
+	if (ignore_event_trigger == IGNORE_EVENT_TRIGGER_LOGIN
+		&& event == EVT_Login)
+		return true;
+
 	/* IGNORE_EVENT_TRIGGER_NONE */
 	return false;
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index ba2fcfeb4a..5638cd95b2 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -41,6 +41,7 @@
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/prepare.h"
 #include "common/pg_prng.h"
 #include "jit/jit.h"
@@ -4202,6 +4203,9 @@ PostgresMain(const char *dbname, const char *username)
 	initStringInfo(&row_description_buf);
 	MemoryContextSwitchTo(TopMemoryContext);
 
+	/* Fire any defined login event triggers, if appropriate */
+	EventTriggerOnLogin();
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 3a9c9f0c50..43a38ae6c7 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "login") == 0)
+			event = EVT_Login;
 		else
 			continue;
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 088c4690d9..c5d53386e7 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -569,6 +569,7 @@ static const struct config_enum_entry wal_compression_options[] = {
 static const struct config_enum_entry ignore_event_trigger_options[] = {
 	{"none", IGNORE_EVENT_TRIGGER_NONE, false},
 	{"all", IGNORE_EVENT_TRIGGER_ALL, false},
+	{"login", IGNORE_EVENT_TRIGGER_LOGIN, false},
 	{NULL, 0, false}
 };
 
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 4dd24b8c89..4572c1d463 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3026,6 +3026,11 @@ dumpDatabase(Archive *fout)
 		appendPQExpBufferStr(delQry, ";\n");
 	}
 
+	/*
+	 * We do not restore pg_database.dathasloginevt because it is set
+	 * automatically on login event trigger creation.
+	 */
+
 	/* Add database-specific SET options */
 	dumpDatabaseConfig(fout, creaQry, datname, dbCatId.oid);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 17172827a9..d016144a45 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3354,7 +3354,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end",
+					  "login", "sql_drop");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat
index e7e42d6023..468d8453a5 100644
--- a/src/include/catalog/pg_database.dat
+++ b/src/include/catalog/pg_database.dat
@@ -15,7 +15,7 @@
 { oid => '1', oid_symbol => 'TemplateDbOid',
   descr => 'default template for new databases',
   datname => 'template1', encoding => 'ENCODING', datistemplate => 't',
-  datallowconn => 't', datconnlimit => '-1', datfrozenxid => '0',
+  datallowconn => 't', dathasloginevt => 'f', datconnlimit => '-1', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datcollate => 'LC_COLLATE',
   datctype => 'LC_CTYPE', datacl => '_null_' },
 
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index 76adbd4aad..4dd40bf693 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -46,6 +46,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
 	/* new connections allowed? */
 	bool		datallowconn;
 
+	/* database has login event triggers? */
+	bool		dathasloginevt;
+
 	/* max connections allowed (-1=no limit) */
 	int32		datconnlimit;
 
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 9e7671e63f..2400cc7d7a 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -32,7 +32,8 @@ typedef struct EventTriggerData
 typedef enum ignore_event_trigger_events
 {
 	IGNORE_EVENT_TRIGGER_NONE,
-	IGNORE_EVENT_TRIGGER_ALL
+	IGNORE_EVENT_TRIGGER_ALL,
+	IGNORE_EVENT_TRIGGER_LOGIN
 } IgnoreEventTriggersEvents;
 
 extern int ignore_event_trigger;
@@ -62,6 +63,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnLogin(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 4bc7ddf410..d6e408e1ed 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
+PG_CMDTAG(CMDTAG_LOGIN, "LOGIN", true, false, false)
 PG_CMDTAG(CMDTAG_MOVE, "MOVE", false, false, true)
 PG_CMDTAG(CMDTAG_NOTIFY, "NOTIFY", false, false, false)
 PG_CMDTAG(CMDTAG_PREPARE, "PREPARE", false, false, false)
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index ddb67a68fa..a66344aacb 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Login,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 583ee87da8..94d69039c4 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,26 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE ROLE regress_user LOGIN PASSWORD 'pass';
+
+CREATE TABLE user_logins(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO user_logins (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 my $primary_lsn = $node_primary->lsn('write');
 $node_primary->wait_for_catchup($node_standby_1, 'replay', $primary_lsn);
@@ -387,6 +407,9 @@ sub replay_check
 
 replay_check();
 
+$node_standby_1->safe_psql('postgres', "SELECT 1", extra_params => [ '-U', 'regress_user', '-w' ]);
+$node_standby_2->safe_psql('postgres', "SELECT 2", extra_params => [ '-U', 'regress_user', '-w' ]);
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 44d545de25..fb2c799ebb 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -604,3 +604,41 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     1
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     2
+(1 row)
+
+-- Check dathasloginevt in system catalog
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+ dathasloginevt 
+----------------
+ t
+(1 row)
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 1446cf8cc8..3123cbb23d 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -462,3 +462,27 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+SELECT COUNT(*) FROM user_logins;
+\c
+SELECT COUNT(*) FROM user_logins;
+
+-- Check dathasloginevt in system catalog
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
-- 
2.24.3 (Apple Git-128)

v27-0001-Provide-a-GUC-for-temporarily-ignoring-event-tri.patchapplication/octet-stream; name=v27-0001-Provide-a-GUC-for-temporarily-ignoring-event-tri.patch; x-unix-mode=0644Download
From 9668f9e4f0882abe343c8cb4df944ff3da3a1031 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <daniel@yesql.se>
Date: Mon, 14 Mar 2022 14:24:09 +0100
Subject: [PATCH v27 1/2] Provide a GUC for temporarily ignoring event triggers

In order to allow troubleshooting and repair without the need for
restarting in single-user mode, allow to ignore event triggers per
session.
---
 doc/src/sgml/config.sgml                   | 15 ++++++++
 doc/src/sgml/ref/create_event_trigger.sgml |  4 ++-
 src/backend/commands/event_trigger.c       | 40 ++++++++++++++++++----
 src/backend/utils/misc/guc.c               | 17 +++++++++
 src/include/commands/event_trigger.h       |  8 +++++
 5 files changed, 76 insertions(+), 8 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 7a48973b3c..54e86ba3de 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9195,6 +9195,21 @@ SET XML OPTION { DOCUMENT | CONTENT };
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-ignore-event-trigger" xreflabel="ignore_event_trigger">
+      <term><varname>ignore_event_trigger</varname> (<type>enum</type>)
+      <indexterm>
+       <primary><varname>ignore_event_trigger</varname></primary>
+       <secondary>configuration parameter</secondary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Allows to temporarily disable event triggers from executing in order
+        to troubleshoot and repair faulty event triggers.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
     </sect2>
      <sect2 id="runtime-config-client-format">
diff --git a/doc/src/sgml/ref/create_event_trigger.sgml b/doc/src/sgml/ref/create_event_trigger.sgml
index 22c8119198..1a78555c64 100644
--- a/doc/src/sgml/ref/create_event_trigger.sgml
+++ b/doc/src/sgml/ref/create_event_trigger.sgml
@@ -123,7 +123,9 @@ CREATE EVENT TRIGGER <replaceable class="parameter">name</replaceable>
    Event triggers are disabled in single-user mode (see <xref
    linkend="app-postgres"/>).  If an erroneous event trigger disables the
    database so much that you can't even drop the trigger, restart in
-   single-user mode and you'll be able to do that.
+   single-user mode and you'll be able to do that. Even triggers can also be
+   temporarily disabled for such troubleshooting, see
+   <xref linkend="gux-ignore-event-trigger"/>.
   </para>
  </refsect1>
 
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 3c3fc2515b..70995cfc49 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -72,6 +72,9 @@ typedef struct EventTriggerQueryState
 
 static EventTriggerQueryState *currentEventTriggerState = NULL;
 
+/* GUC parameter */
+int		ignore_event_trigger = IGNORE_EVENT_TRIGGER_NONE;
+
 /* Support for dropped objects */
 typedef struct SQLDropObject
 {
@@ -100,6 +103,7 @@ static void validate_table_rewrite_tags(const char *filtervar, List *taglist);
 static void EventTriggerInvoke(List *fn_oid_list, EventTriggerData *trigdata);
 static const char *stringify_grant_objtype(ObjectType objtype);
 static const char *stringify_adefprivs_objtype(ObjectType objtype);
+static bool ignore_event_trigger_check(EventTriggerEvent event);
 
 /*
  * Create an event trigger.
@@ -658,8 +662,11 @@ EventTriggerDDLCommandStart(Node *parsetree)
 	 * wherein event triggers are disabled.  (Or we could implement
 	 * heapscan-and-sort logic for that case, but having disaster recovery
 	 * scenarios depend on code that's otherwise untested isn't appetizing.)
+	 *
+	 * Additionally, event triggers can be disabled with a superuser only GUC
+	 * to make fixing database easier as per 1 above.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_DDLCommandStart))
 		return;
 
 	runlist = EventTriggerCommonSetup(parsetree,
@@ -693,9 +700,9 @@ EventTriggerDDLCommandEnd(Node *parsetree)
 
 	/*
 	 * See EventTriggerDDLCommandStart for a discussion about why event
-	 * triggers are disabled in single user mode.
+	 * triggers are disabled in single user mode or via GUC.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_DDLCommandEnd))
 		return;
 
 	/*
@@ -741,9 +748,9 @@ EventTriggerSQLDrop(Node *parsetree)
 
 	/*
 	 * See EventTriggerDDLCommandStart for a discussion about why event
-	 * triggers are disabled in single user mode.
+	 * triggers are disabled in single user mode or via a GUC.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_SQLDrop))
 		return;
 
 	/*
@@ -812,9 +819,9 @@ EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason)
 
 	/*
 	 * See EventTriggerDDLCommandStart for a discussion about why event
-	 * triggers are disabled in single user mode.
+	 * triggers are disabled in single user mode or via a GUC.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_TableRewrite))
 		return;
 
 	/*
@@ -2175,3 +2182,22 @@ stringify_adefprivs_objtype(ObjectType objtype)
 
 	return "???";				/* keep compiler quiet */
 }
+
+
+/*
+ * Checks whether the specified event is ignored by the ignore_event_trigger
+ * GUC or not. Currently, the GUC only supports ignoreing all or nothing but
+ * that will most likely change so the function takes an event to aid that.
+ */
+static bool
+ignore_event_trigger_check(EventTriggerEvent event)
+{
+	(void) event;			/* unused parameter */
+
+	if (ignore_event_trigger == IGNORE_EVENT_TRIGGER_ALL)
+		return true;
+
+	/* IGNORE_EVENT_TRIGGER_NONE */
+	return false;
+
+}
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index e7f0a380e6..088c4690d9 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -46,6 +46,7 @@
 #include "catalog/pg_authid.h"
 #include "catalog/storage.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/prepare.h"
 #include "commands/tablespace.h"
 #include "commands/trigger.h"
@@ -565,6 +566,12 @@ static const struct config_enum_entry wal_compression_options[] = {
 	{NULL, 0, false}
 };
 
+static const struct config_enum_entry ignore_event_trigger_options[] = {
+	{"none", IGNORE_EVENT_TRIGGER_NONE, false},
+	{"all", IGNORE_EVENT_TRIGGER_ALL, false},
+	{NULL, 0, false}
+};
+
 /*
  * Options for enum values stored in other modules
  */
@@ -4904,6 +4911,16 @@ static struct config_enum ConfigureNamesEnum[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"ignore_event_trigger", PGC_SUSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Disable event triggers during the session."),
+			NULL
+		},
+		&ignore_event_trigger,
+		IGNORE_EVENT_TRIGGER_NONE, ignore_event_trigger_options,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"wal_level", PGC_POSTMASTER, WAL_SETTINGS,
 			gettext_noop("Sets the level of information written to the WAL."),
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 10091c3aaf..9e7671e63f 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -29,6 +29,14 @@ typedef struct EventTriggerData
 	CommandTag	tag;
 } EventTriggerData;
 
+typedef enum ignore_event_trigger_events
+{
+	IGNORE_EVENT_TRIGGER_NONE,
+	IGNORE_EVENT_TRIGGER_ALL
+} IgnoreEventTriggersEvents;
+
+extern int ignore_event_trigger;
+
 #define AT_REWRITE_ALTER_PERSISTENCE	0x01
 #define AT_REWRITE_DEFAULT_VAL			0x02
 #define AT_REWRITE_COLUMN_REWRITE		0x04
-- 
2.24.3 (Apple Git-128)

#102Noname
a.sokolov@postgrespro.ru
In reply to: Daniel Gustafsson (#101)
Re: On login trigger: take three

Daniel Gustafsson писал 2022-03-15 23:52:

Yeah, that was the previously posted v25 from the author (who adopted
it from
the original author). I took the liberty to quickly poke at the review
comments you had left as well as the ones that I had found to try and
progress
the patch. 0001 should really go in it's own thread though to not hide
it from
anyone interested who isn't looking at this thread.

Hi
I got an error while running tests on Windows:
2022-03-17 17:30:02.458 MSK [6920] [unknown] LOG: no match in usermap
"regress" for user "regress_user" authenticated as
"vagrant@WINDOWS-2019"
2022-03-17 17:30:02.458 MSK [6920] [unknown] FATAL: SSPI authentication
failed for user "regress_user"
2022-03-17 17:30:02.458 MSK [6920] [unknown] DETAIL: Connection matched
pg_hba.conf line 2: "host all all 127.0.0.1/32 sspi include_realm=1
map=regress"
2022-03-17 17:30:02.526 MSK [3432] FATAL: could not receive data from
WAL stream: server closed the connection unexpectedly
This probably means the server terminated abnormally
before or while processing the request.

I propose to make such correction:
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -14,7 +14,7 @@ my $node_primary = get_new_node('primary');
  # and it needs proper authentication configuration.
  $node_primary->init(
         allows_streaming => 1,
-       auth_extra       => [ '--create-role', 'repl_role' ]);
+       auth_extra       => [ '--create-role', 'repl_role,regress_user' 
]);
  $node_primary->start;
  my $backup_name = 'my_backup';

--
Andrey Sokolov
Postgres Professional: http://www.postgrespro.com

#103Daniel Gustafsson
daniel@yesql.se
In reply to: Noname (#102)
2 attachment(s)
Re: On login trigger: take three

On 18 Mar 2022, at 08:24, a.sokolov@postgrespro.ru wrote:

-       auth_extra       => [ '--create-role', 'repl_role' ]);
+       auth_extra       => [ '--create-role', 'repl_role,regress_user' ]);

Looking at the test in question it's not entirely clear to me what the original
author really intended here, so I've changed it up a bit to actually test
something and removed the need for the regress_user role. I've also fixed the
silly mistake highlighted in the postgresql.conf.sample test.

--
Daniel Gustafsson https://vmware.com/

Attachments:

v28-0002-Add-a-new-login-event-and-login-event-trigger-su.patchapplication/octet-stream; name=v28-0002-Add-a-new-login-event-and-login-event-trigger-su.patch; x-unix-mode=0644Download
From 71c7b67e7ecf1254370afb11eef200a7e0cee60c Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <daniel@yesql.se>
Date: Mon, 14 Mar 2022 14:27:10 +0100
Subject: [PATCH v28 2/2] Add a new "login" event and login event trigger
 support.

The login event occurs when a user logs in to the system.

Author: Konstantin Knizhnik and Greg Nancarrow
Discussion: https://postgr.es/m/0d46d29f-4558-3af9-9c85-7774e14a7709%40postgrespro.ru
---
 doc/src/sgml/bki.sgml                         |   2 +-
 doc/src/sgml/catalogs.sgml                    |  13 ++
 doc/src/sgml/ecpg.sgml                        |   2 +
 doc/src/sgml/event-trigger.sgml               |  81 ++++++++-
 src/backend/commands/dbcommands.c             |   1 +
 src/backend/commands/event_trigger.c          | 156 +++++++++++++++++-
 src/backend/tcop/postgres.c                   |   4 +
 src/backend/utils/cache/evtcache.c            |   2 +
 src/backend/utils/misc/guc.c                  |   1 +
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/bin/pg_dump/pg_dump.c                     |   5 +
 src/bin/psql/tab-complete.c                   |   3 +-
 src/include/catalog/pg_database.dat           |   2 +-
 src/include/catalog/pg_database.h             |   3 +
 src/include/commands/event_trigger.h          |   4 +-
 src/include/tcop/cmdtaglist.h                 |   1 +
 src/include/utils/evtcache.h                  |   3 +-
 src/test/recovery/t/001_stream_rep.pl         |  23 +++
 src/test/regress/expected/event_trigger.out   |  38 +++++
 src/test/regress/sql/event_trigger.sql        |  24 +++
 20 files changed, 358 insertions(+), 11 deletions(-)

diff --git a/doc/src/sgml/bki.sgml b/doc/src/sgml/bki.sgml
index 33955494c6..caef1df200 100644
--- a/doc/src/sgml/bki.sgml
+++ b/doc/src/sgml/bki.sgml
@@ -183,7 +183,7 @@
 { oid => '1', oid_symbol => 'TemplateDbOid',
   descr => 'database\'s default template',
   datname => 'template1', encoding => 'ENCODING', datistemplate => 't',
-  datallowconn => 't', datconnlimit => '-1', datfrozenxid => '0',
+  datallowconn => 't', dathasloginevt => 'f', datconnlimit => '-1', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datcollate => 'LC_COLLATE',
   datctype => 'LC_CTYPE', datacl => '_null_' },
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 4dc5b34d21..c56f3d0542 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2982,6 +2982,19 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>dathasloginevt</structfield> <type>bool</type>
+      </para>
+      <para>
+        Indicates that there are login event triggers defined for this database.
+        This flag is used to avoid extra lookups on the
+        <structname>pg_event_trigger</structname> table during each backend
+        startup.  This flag is used internally by <productname>PostgreSQL</productname>
+        and should not be manually altered or read for monitoring purposes.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>datconnlimit</structfield> <type>int4</type>
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index cdc4761c60..197b476205 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -4731,6 +4731,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = t (type: 1)
 datallowconn = t (type: 1)
+dathasloginevt = f (type: 1)
 datconnlimit = -1 (type: 5)
 datfrozenxid = 379 (type: 1)
 dattablespace = 1663 (type: 1)
@@ -4755,6 +4756,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = f (type: 1)
 datallowconn = t (type: 1)
+dathasloginevt = f (type: 1)
 datconnlimit = -1 (type: 5)
 datfrozenxid = 379 (type: 1)
 dattablespace = 1663 (type: 1)
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 9c66f97b0f..e3b03e0518 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>login</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,18 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>login</literal> event occurs when a user logs in to the
+     system.
+     Any bugs in a trigger procedure for this event may prevent successful
+     login to the system. Such bugs may be fixed after first reconnecting to
+     the system with event triggers disabled (see <xref="guc-ignore-event-trigger"/>)
+     or using restarting the system in single-user mode (as event triggers are
+     disabled in this mode).
+     See the <xref linkend="app-postgres"/> reference page for details about
+     using single-user mode.
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1156,7 +1169,7 @@ typedef struct EventTriggerData
   </sect1>
 
   <sect1 id="event-trigger-example">
-   <title>A Complete Event Trigger Example</title>
+   <title>A C language Event Trigger Example</title>
 
    <para>
     Here is a very simple example of an event trigger function written in C.
@@ -1296,6 +1309,72 @@ $$;
 CREATE EVENT TRIGGER no_rewrite_allowed
                   ON table_rewrite
    EXECUTE FUNCTION no_rewrite();
+</programlisting>
+   </para>
+ </sect1>
+
+  <sect1 id="event-trigger-database-login-example">
+   <title>A Database Login Event Trigger Example</title>
+
+   <para>
+    The event trigger on the <literal>login</literal> event can be
+    useful for logging user logins, for verifying the connection and
+    assigning roles according to current circumstances, or for some session
+    data initialization. It is vital that any event trigger using the
+    <literal>login</literal> event checks whether or not the database is in
+    recovery.
+   </para>
+
+   <para>
+    The following example demonstrates these options.
+<programlisting>
+-- create test tables and roles
+CREATE TABLE user_login_log (
+  "user" text,
+  "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+  RETURNS event_trigger SECURITY DEFINER
+  LANGUAGE plpgsql AS
+$$
+DECLARE
+  hour integer = EXTRACT('hour' FROM current_time);
+  rec boolean;
+BEGIN
+-- Ensure the database is not in recovery
+SELECT pg_is_in_recovery() INTO rec;
+IF rec THEN
+  RETURN;
+END IF
+
+-- 1) Assign some roles
+IF hour BETWEEN 8 AND 20 THEN         -- at daytime grant the day_worker role
+  EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT    day_worker TO '   || quote_ident(session_user);
+ELSIF hour BETWEEN 2 AND 4 THEN
+  RAISE EXCEPTION 'Login forbidden';  -- do not allow to login these hours
+ELSE                                  -- at other time grant the night_worker role
+  EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT  night_worker TO ' || quote_ident(session_user);
+END IF;
+
+-- 2) Initialize some user session data
+CREATE TEMP TABLE session_storage (x float, y integer);
+
+-- 3) Log the connection time
+INSERT INTO user_login_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+  ON login
+  EXECUTE FUNCTION init_session();
 </programlisting>
    </para>
  </sect1>
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 2e1af642e4..cd8d4585de 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -728,6 +728,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	new_record[Anum_pg_database_datlocprovider - 1] = CharGetDatum(dblocprovider);
 	new_record[Anum_pg_database_datistemplate - 1] = BoolGetDatum(dbistemplate);
 	new_record[Anum_pg_database_datallowconn - 1] = BoolGetDatum(dballowconnections);
+	new_record[Anum_pg_database_dathasloginevt - 1] = BoolGetDatum(false);
 	new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 	new_record[Anum_pg_database_datfrozenxid - 1] = TransactionIdGetDatum(src_frozenxid);
 	new_record[Anum_pg_database_datminmxid - 1] = TransactionIdGetDatum(src_minmxid);
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 70995cfc49..4e2e6629bd 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -37,15 +38,18 @@
 #include "miscadmin.h"
 #include "parser/parse_func.h"
 #include "pgstat.h"
+#include "storage/lmgr.h"
 #include "tcop/deparse_utility.h"
 #include "tcop/utility.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 typedef struct EventTriggerQueryState
@@ -134,6 +138,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "login") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -297,6 +302,30 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
 	CatalogTupleInsert(tgrel, tuple);
 	heap_freetuple(tuple);
 
+	/*
+	 * Login event triggers have an additional flag in pg_database to allow
+	 * faster lookups in hot codepaths. Set the flag unless already True.
+	 */
+	if (strcmp(eventname, "login") == 0)
+	{
+		Form_pg_database db;
+		Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+
+		/* Set dathasloginevt flag in pg_database */
+		tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+		db = (Form_pg_database) GETSTRUCT(tuple);
+		if (!db->dathasloginevt)
+		{
+			db->dathasloginevt = true;
+			CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+			CommandCounterIncrement();
+		}
+		table_close(pg_db, RowExclusiveLock);
+		heap_freetuple(tuple);
+	}
+
 	/* Depend on owner. */
 	recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
 
@@ -584,10 +613,15 @@ EventTriggerCommonSetup(Node *parsetree,
 	{
 		CommandTag	dbgtag;
 
-		dbgtag = CreateCommandTag(parsetree);
+		if (event == EVT_Login)
+			dbgtag = CMDTAG_LOGIN;
+		else
+			dbgtag = CreateCommandTag(parsetree);
+
 		if (event == EVT_DDLCommandStart ||
 			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
+			event == EVT_SQLDrop ||
+			event == EVT_Login)
 		{
 			if (!command_tag_event_trigger_ok(dbgtag))
 				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
@@ -606,7 +640,10 @@ EventTriggerCommonSetup(Node *parsetree,
 		return NIL;
 
 	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
+	if (event == EVT_Login)
+		tag = CMDTAG_LOGIN;
+	else
+		tag = CreateCommandTag(parsetree);
 
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
@@ -807,6 +844,112 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Return true if this database has login event triggers, false otherwise.
+ */
+static bool
+DatabaseHasLoginEventTriggers(void)
+{
+	bool		has_login_event_triggers;
+	HeapTuple	tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+	has_login_event_triggers = ((Form_pg_database) GETSTRUCT(tuple))->dathasloginevt;
+	ReleaseSysCache(tuple);
+	return has_login_event_triggers;
+}
+
+/*
+ * Fire login event triggers if any are present.  The dathasloginevt
+ * pg_database flag is left when an event trigger is dropped, to avoid
+ * complicating the codepath in the case of multiple event triggers.  This
+ * function will instead unset the flag if no trigger is defined.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId)
+		|| ignore_event_trigger_check(EVT_Login))
+		return;
+
+	StartTransactionCommand();
+
+	if (DatabaseHasLoginEventTriggers())
+	{
+		runlist = EventTriggerCommonSetup(NULL,
+										  EVT_Login, "login",
+										  &trigdata);
+
+		if (runlist != NIL)
+		{
+			/*
+			 * Event trigger execution may require an active snapshot.
+			 */
+			PushActiveSnapshot(GetTransactionSnapshot());
+
+			/* Run the triggers. */
+			EventTriggerInvoke(runlist, &trigdata);
+
+			/* Cleanup. */
+			list_free(runlist);
+
+			PopActiveSnapshot();
+		}
+		else
+		{
+			/*
+			 * Runlist is empty: clear dathasloginevt flag
+			 */
+			Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+			HeapTuple	tuple;
+			Form_pg_database db;
+
+			LockSharedObject(DatabaseRelationId, MyDatabaseId, 0, AccessExclusiveLock);
+
+			tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+			db = (Form_pg_database) GETSTRUCT(tuple);
+			if (db->dathasloginevt)
+			{
+				/*
+				 * There can be a race condition: a login event trigger may
+				 * have been added after the pg_event_trigger table was
+				 * scanned, and we don't want to erroneously clear the
+				 * dathasloginevt flag in this case. To be sure that this
+				 * hasn't happened, repeat the scan under the pg_database
+				 * table lock.
+				 */
+				runlist = EventTriggerCommonSetup(NULL,
+												  EVT_Login, "login",
+												  &trigdata);
+				if (runlist == NIL)	/* list is still empty, so clear the
+										 * flag */
+				{
+					db->dathasloginevt = false;
+					CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				}
+				else
+					list_free(runlist);
+			}
+			table_close(pg_db, RowExclusiveLock);
+			heap_freetuple(tuple);
+		}
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
@@ -2186,8 +2329,7 @@ stringify_adefprivs_objtype(ObjectType objtype)
 
 /*
  * Checks whether the specified event is ignored by the ignore_event_trigger
- * GUC or not. Currently, the GUC only supports ignoreing all or nothing but
- * that will most likely change so the function takes an event to aid that.
+ * GUC or not.
  */
 static bool
 ignore_event_trigger_check(EventTriggerEvent event)
@@ -2197,6 +2339,10 @@ ignore_event_trigger_check(EventTriggerEvent event)
 	if (ignore_event_trigger == IGNORE_EVENT_TRIGGER_ALL)
 		return true;
 
+	if (ignore_event_trigger == IGNORE_EVENT_TRIGGER_LOGIN
+		&& event == EVT_Login)
+		return true;
+
 	/* IGNORE_EVENT_TRIGGER_NONE */
 	return false;
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index ba2fcfeb4a..5638cd95b2 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -41,6 +41,7 @@
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/prepare.h"
 #include "common/pg_prng.h"
 #include "jit/jit.h"
@@ -4202,6 +4203,9 @@ PostgresMain(const char *dbname, const char *username)
 	initStringInfo(&row_description_buf);
 	MemoryContextSwitchTo(TopMemoryContext);
 
+	/* Fire any defined login event triggers, if appropriate */
+	EventTriggerOnLogin();
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 3a9c9f0c50..43a38ae6c7 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "login") == 0)
+			event = EVT_Login;
 		else
 			continue;
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 088c4690d9..c5d53386e7 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -569,6 +569,7 @@ static const struct config_enum_entry wal_compression_options[] = {
 static const struct config_enum_entry ignore_event_trigger_options[] = {
 	{"none", IGNORE_EVENT_TRIGGER_NONE, false},
 	{"all", IGNORE_EVENT_TRIGGER_ALL, false},
+	{"login", IGNORE_EVENT_TRIGGER_LOGIN, false},
 	{NULL, 0, false}
 };
 
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 4cf5b26a36..0d7623fc7a 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -674,6 +674,7 @@
 					#   error
 #search_path = '"$user", public'	# schema names
 #row_security = on
+#ignore_event_trigger = 'none'
 #default_table_access_method = 'heap'
 #default_tablespace = ''		# a tablespace name, '' uses the default
 #default_toast_compression = 'pglz'	# 'pglz' or 'lz4'
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 725cd2e4eb..a97974cbf8 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3052,6 +3052,11 @@ dumpDatabase(Archive *fout)
 		appendPQExpBufferStr(delQry, ";\n");
 	}
 
+	/*
+	 * We do not restore pg_database.dathasloginevt because it is set
+	 * automatically on login event trigger creation.
+	 */
+
 	/* Add database-specific SET options */
 	dumpDatabaseConfig(fout, creaQry, datname, dbCatId.oid);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 380cbc0b1f..1a02e29000 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3355,7 +3355,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end",
+					  "login", "sql_drop");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat
index 5feedff7bf..78402d48bc 100644
--- a/src/include/catalog/pg_database.dat
+++ b/src/include/catalog/pg_database.dat
@@ -15,7 +15,7 @@
 { oid => '1', oid_symbol => 'TemplateDbOid',
   descr => 'default template for new databases',
   datname => 'template1', encoding => 'ENCODING', datlocprovider => 'LOCALE_PROVIDER', datistemplate => 't',
-  datallowconn => 't', datconnlimit => '-1', datfrozenxid => '0',
+  datallowconn => 't', dathasloginevt => 'f', datconnlimit => '-1', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datcollate => 'LC_COLLATE',
   datctype => 'LC_CTYPE', daticulocale => 'ICU_LOCALE', datacl => '_null_' },
 
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index a9f4a8071f..55d7ae7988 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -49,6 +49,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
 	/* new connections allowed? */
 	bool		datallowconn;
 
+	/* database has login event triggers? */
+	bool		dathasloginevt;
+
 	/* max connections allowed (-1=no limit) */
 	int32		datconnlimit;
 
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 9e7671e63f..2400cc7d7a 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -32,7 +32,8 @@ typedef struct EventTriggerData
 typedef enum ignore_event_trigger_events
 {
 	IGNORE_EVENT_TRIGGER_NONE,
-	IGNORE_EVENT_TRIGGER_ALL
+	IGNORE_EVENT_TRIGGER_ALL,
+	IGNORE_EVENT_TRIGGER_LOGIN
 } IgnoreEventTriggersEvents;
 
 extern int ignore_event_trigger;
@@ -62,6 +63,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnLogin(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 4bc7ddf410..d6e408e1ed 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
+PG_CMDTAG(CMDTAG_LOGIN, "LOGIN", true, false, false)
 PG_CMDTAG(CMDTAG_MOVE, "MOVE", false, false, true)
 PG_CMDTAG(CMDTAG_NOTIFY, "NOTIFY", false, false, false)
 PG_CMDTAG(CMDTAG_PREPARE, "PREPARE", false, false, false)
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index ddb67a68fa..a66344aacb 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Login,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 583ee87da8..c3dbd75af3 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,24 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE TABLE user_logins(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO user_logins (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 my $primary_lsn = $node_primary->lsn('write');
 $node_primary->wait_for_catchup($node_standby_1, 'replay', $primary_lsn);
@@ -387,6 +405,11 @@ sub replay_check
 
 replay_check();
 
+my $evttrig = $node_standby_1->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+$evttrig = $node_standby_2->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 44d545de25..fb2c799ebb 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -604,3 +604,41 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     1
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     2
+(1 row)
+
+-- Check dathasloginevt in system catalog
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+ dathasloginevt 
+----------------
+ t
+(1 row)
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 1446cf8cc8..3123cbb23d 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -462,3 +462,27 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+SELECT COUNT(*) FROM user_logins;
+\c
+SELECT COUNT(*) FROM user_logins;
+
+-- Check dathasloginevt in system catalog
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
-- 
2.24.3 (Apple Git-128)

v28-0001-Provide-a-GUC-for-temporarily-ignoring-event-tri.patchapplication/octet-stream; name=v28-0001-Provide-a-GUC-for-temporarily-ignoring-event-tri.patch; x-unix-mode=0644Download
From 02b6e3c86b578bddb213955c29757af528213b03 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <daniel@yesql.se>
Date: Mon, 14 Mar 2022 14:24:09 +0100
Subject: [PATCH v28 1/2] Provide a GUC for temporarily ignoring event triggers

In order to allow troubleshooting and repair without the need for
restarting in single-user mode, allow to ignore event triggers per
session.
---
 doc/src/sgml/config.sgml                   | 15 ++++++++
 doc/src/sgml/ref/create_event_trigger.sgml |  4 ++-
 src/backend/commands/event_trigger.c       | 40 ++++++++++++++++++----
 src/backend/utils/misc/guc.c               | 17 +++++++++
 src/include/commands/event_trigger.h       |  8 +++++
 5 files changed, 76 insertions(+), 8 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 7a48973b3c..54e86ba3de 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9195,6 +9195,21 @@ SET XML OPTION { DOCUMENT | CONTENT };
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-ignore-event-trigger" xreflabel="ignore_event_trigger">
+      <term><varname>ignore_event_trigger</varname> (<type>enum</type>)
+      <indexterm>
+       <primary><varname>ignore_event_trigger</varname></primary>
+       <secondary>configuration parameter</secondary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Allows to temporarily disable event triggers from executing in order
+        to troubleshoot and repair faulty event triggers.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
     </sect2>
      <sect2 id="runtime-config-client-format">
diff --git a/doc/src/sgml/ref/create_event_trigger.sgml b/doc/src/sgml/ref/create_event_trigger.sgml
index 22c8119198..1a78555c64 100644
--- a/doc/src/sgml/ref/create_event_trigger.sgml
+++ b/doc/src/sgml/ref/create_event_trigger.sgml
@@ -123,7 +123,9 @@ CREATE EVENT TRIGGER <replaceable class="parameter">name</replaceable>
    Event triggers are disabled in single-user mode (see <xref
    linkend="app-postgres"/>).  If an erroneous event trigger disables the
    database so much that you can't even drop the trigger, restart in
-   single-user mode and you'll be able to do that.
+   single-user mode and you'll be able to do that. Even triggers can also be
+   temporarily disabled for such troubleshooting, see
+   <xref linkend="gux-ignore-event-trigger"/>.
   </para>
  </refsect1>
 
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 3c3fc2515b..70995cfc49 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -72,6 +72,9 @@ typedef struct EventTriggerQueryState
 
 static EventTriggerQueryState *currentEventTriggerState = NULL;
 
+/* GUC parameter */
+int		ignore_event_trigger = IGNORE_EVENT_TRIGGER_NONE;
+
 /* Support for dropped objects */
 typedef struct SQLDropObject
 {
@@ -100,6 +103,7 @@ static void validate_table_rewrite_tags(const char *filtervar, List *taglist);
 static void EventTriggerInvoke(List *fn_oid_list, EventTriggerData *trigdata);
 static const char *stringify_grant_objtype(ObjectType objtype);
 static const char *stringify_adefprivs_objtype(ObjectType objtype);
+static bool ignore_event_trigger_check(EventTriggerEvent event);
 
 /*
  * Create an event trigger.
@@ -658,8 +662,11 @@ EventTriggerDDLCommandStart(Node *parsetree)
 	 * wherein event triggers are disabled.  (Or we could implement
 	 * heapscan-and-sort logic for that case, but having disaster recovery
 	 * scenarios depend on code that's otherwise untested isn't appetizing.)
+	 *
+	 * Additionally, event triggers can be disabled with a superuser only GUC
+	 * to make fixing database easier as per 1 above.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_DDLCommandStart))
 		return;
 
 	runlist = EventTriggerCommonSetup(parsetree,
@@ -693,9 +700,9 @@ EventTriggerDDLCommandEnd(Node *parsetree)
 
 	/*
 	 * See EventTriggerDDLCommandStart for a discussion about why event
-	 * triggers are disabled in single user mode.
+	 * triggers are disabled in single user mode or via GUC.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_DDLCommandEnd))
 		return;
 
 	/*
@@ -741,9 +748,9 @@ EventTriggerSQLDrop(Node *parsetree)
 
 	/*
 	 * See EventTriggerDDLCommandStart for a discussion about why event
-	 * triggers are disabled in single user mode.
+	 * triggers are disabled in single user mode or via a GUC.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_SQLDrop))
 		return;
 
 	/*
@@ -812,9 +819,9 @@ EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason)
 
 	/*
 	 * See EventTriggerDDLCommandStart for a discussion about why event
-	 * triggers are disabled in single user mode.
+	 * triggers are disabled in single user mode or via a GUC.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_TableRewrite))
 		return;
 
 	/*
@@ -2175,3 +2182,22 @@ stringify_adefprivs_objtype(ObjectType objtype)
 
 	return "???";				/* keep compiler quiet */
 }
+
+
+/*
+ * Checks whether the specified event is ignored by the ignore_event_trigger
+ * GUC or not. Currently, the GUC only supports ignoreing all or nothing but
+ * that will most likely change so the function takes an event to aid that.
+ */
+static bool
+ignore_event_trigger_check(EventTriggerEvent event)
+{
+	(void) event;			/* unused parameter */
+
+	if (ignore_event_trigger == IGNORE_EVENT_TRIGGER_ALL)
+		return true;
+
+	/* IGNORE_EVENT_TRIGGER_NONE */
+	return false;
+
+}
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index e7f0a380e6..088c4690d9 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -46,6 +46,7 @@
 #include "catalog/pg_authid.h"
 #include "catalog/storage.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/prepare.h"
 #include "commands/tablespace.h"
 #include "commands/trigger.h"
@@ -565,6 +566,12 @@ static const struct config_enum_entry wal_compression_options[] = {
 	{NULL, 0, false}
 };
 
+static const struct config_enum_entry ignore_event_trigger_options[] = {
+	{"none", IGNORE_EVENT_TRIGGER_NONE, false},
+	{"all", IGNORE_EVENT_TRIGGER_ALL, false},
+	{NULL, 0, false}
+};
+
 /*
  * Options for enum values stored in other modules
  */
@@ -4904,6 +4911,16 @@ static struct config_enum ConfigureNamesEnum[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"ignore_event_trigger", PGC_SUSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Disable event triggers during the session."),
+			NULL
+		},
+		&ignore_event_trigger,
+		IGNORE_EVENT_TRIGGER_NONE, ignore_event_trigger_options,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"wal_level", PGC_POSTMASTER, WAL_SETTINGS,
 			gettext_noop("Sets the level of information written to the WAL."),
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 10091c3aaf..9e7671e63f 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -29,6 +29,14 @@ typedef struct EventTriggerData
 	CommandTag	tag;
 } EventTriggerData;
 
+typedef enum ignore_event_trigger_events
+{
+	IGNORE_EVENT_TRIGGER_NONE,
+	IGNORE_EVENT_TRIGGER_ALL
+} IgnoreEventTriggersEvents;
+
+extern int ignore_event_trigger;
+
 #define AT_REWRITE_ALTER_PERSISTENCE	0x01
 #define AT_REWRITE_DEFAULT_VAL			0x02
 #define AT_REWRITE_COLUMN_REWRITE		0x04
-- 
2.24.3 (Apple Git-128)

#104Andres Freund
andres@anarazel.de
In reply to: Daniel Gustafsson (#103)
Re: On login trigger: take three

Hi,

On 2022-03-18 17:03:39 +0100, Daniel Gustafsson wrote:

On 18 Mar 2022, at 08:24, a.sokolov@postgrespro.ru wrote:

-       auth_extra       => [ '--create-role', 'repl_role' ]);
+       auth_extra       => [ '--create-role', 'repl_role,regress_user' ]);

Looking at the test in question it's not entirely clear to me what the original
author really intended here, so I've changed it up a bit to actually test
something and removed the need for the regress_user role. I've also fixed the
silly mistake highlighted in the postgresql.conf.sample test.

docs fail to build: https://cirrus-ci.com/task/5556234047717376?logs=docs_build#L349

Greetings,

Andres Freund

#105Daniel Gustafsson
daniel@yesql.se
In reply to: Andres Freund (#104)
2 attachment(s)
Re: On login trigger: take three

On 22 Mar 2022, at 04:48, Andres Freund <andres@anarazel.de> wrote:

docs fail to build: https://cirrus-ci.com/task/5556234047717376?logs=docs_build#L349

Ugh, that one was on me and not the original author. Fixed.

--
Daniel Gustafsson https://vmware.com/

Attachments:

v29-0001-Provide-a-GUC-for-temporarily-ignoring-event-tri.patchapplication/octet-stream; name=v29-0001-Provide-a-GUC-for-temporarily-ignoring-event-tri.patch; x-unix-mode=0644Download
From d897cc2a0d242e6aa3c4ca2256e792126b0394cc Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <daniel@yesql.se>
Date: Mon, 14 Mar 2022 14:24:09 +0100
Subject: [PATCH v29 1/2] Provide a GUC for temporarily ignoring event triggers

In order to allow troubleshooting and repair without the need for
restarting in single-user mode, allow to ignore event triggers per
session.
---
 doc/src/sgml/config.sgml                   | 15 ++++++++
 doc/src/sgml/ref/create_event_trigger.sgml |  4 ++-
 src/backend/commands/event_trigger.c       | 40 ++++++++++++++++++----
 src/backend/utils/misc/guc.c               | 17 +++++++++
 src/include/commands/event_trigger.h       |  8 +++++
 5 files changed, 76 insertions(+), 8 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 7a48973b3c..54e86ba3de 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9195,6 +9195,21 @@ SET XML OPTION { DOCUMENT | CONTENT };
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-ignore-event-trigger" xreflabel="ignore_event_trigger">
+      <term><varname>ignore_event_trigger</varname> (<type>enum</type>)
+      <indexterm>
+       <primary><varname>ignore_event_trigger</varname></primary>
+       <secondary>configuration parameter</secondary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Allows to temporarily disable event triggers from executing in order
+        to troubleshoot and repair faulty event triggers.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
     </sect2>
      <sect2 id="runtime-config-client-format">
diff --git a/doc/src/sgml/ref/create_event_trigger.sgml b/doc/src/sgml/ref/create_event_trigger.sgml
index 22c8119198..1a78555c64 100644
--- a/doc/src/sgml/ref/create_event_trigger.sgml
+++ b/doc/src/sgml/ref/create_event_trigger.sgml
@@ -123,7 +123,9 @@ CREATE EVENT TRIGGER <replaceable class="parameter">name</replaceable>
    Event triggers are disabled in single-user mode (see <xref
    linkend="app-postgres"/>).  If an erroneous event trigger disables the
    database so much that you can't even drop the trigger, restart in
-   single-user mode and you'll be able to do that.
+   single-user mode and you'll be able to do that. Even triggers can also be
+   temporarily disabled for such troubleshooting, see
+   <xref linkend="gux-ignore-event-trigger"/>.
   </para>
  </refsect1>
 
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 3c3fc2515b..70995cfc49 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -72,6 +72,9 @@ typedef struct EventTriggerQueryState
 
 static EventTriggerQueryState *currentEventTriggerState = NULL;
 
+/* GUC parameter */
+int		ignore_event_trigger = IGNORE_EVENT_TRIGGER_NONE;
+
 /* Support for dropped objects */
 typedef struct SQLDropObject
 {
@@ -100,6 +103,7 @@ static void validate_table_rewrite_tags(const char *filtervar, List *taglist);
 static void EventTriggerInvoke(List *fn_oid_list, EventTriggerData *trigdata);
 static const char *stringify_grant_objtype(ObjectType objtype);
 static const char *stringify_adefprivs_objtype(ObjectType objtype);
+static bool ignore_event_trigger_check(EventTriggerEvent event);
 
 /*
  * Create an event trigger.
@@ -658,8 +662,11 @@ EventTriggerDDLCommandStart(Node *parsetree)
 	 * wherein event triggers are disabled.  (Or we could implement
 	 * heapscan-and-sort logic for that case, but having disaster recovery
 	 * scenarios depend on code that's otherwise untested isn't appetizing.)
+	 *
+	 * Additionally, event triggers can be disabled with a superuser only GUC
+	 * to make fixing database easier as per 1 above.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_DDLCommandStart))
 		return;
 
 	runlist = EventTriggerCommonSetup(parsetree,
@@ -693,9 +700,9 @@ EventTriggerDDLCommandEnd(Node *parsetree)
 
 	/*
 	 * See EventTriggerDDLCommandStart for a discussion about why event
-	 * triggers are disabled in single user mode.
+	 * triggers are disabled in single user mode or via GUC.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_DDLCommandEnd))
 		return;
 
 	/*
@@ -741,9 +748,9 @@ EventTriggerSQLDrop(Node *parsetree)
 
 	/*
 	 * See EventTriggerDDLCommandStart for a discussion about why event
-	 * triggers are disabled in single user mode.
+	 * triggers are disabled in single user mode or via a GUC.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_SQLDrop))
 		return;
 
 	/*
@@ -812,9 +819,9 @@ EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason)
 
 	/*
 	 * See EventTriggerDDLCommandStart for a discussion about why event
-	 * triggers are disabled in single user mode.
+	 * triggers are disabled in single user mode or via a GUC.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_TableRewrite))
 		return;
 
 	/*
@@ -2175,3 +2182,22 @@ stringify_adefprivs_objtype(ObjectType objtype)
 
 	return "???";				/* keep compiler quiet */
 }
+
+
+/*
+ * Checks whether the specified event is ignored by the ignore_event_trigger
+ * GUC or not. Currently, the GUC only supports ignoreing all or nothing but
+ * that will most likely change so the function takes an event to aid that.
+ */
+static bool
+ignore_event_trigger_check(EventTriggerEvent event)
+{
+	(void) event;			/* unused parameter */
+
+	if (ignore_event_trigger == IGNORE_EVENT_TRIGGER_ALL)
+		return true;
+
+	/* IGNORE_EVENT_TRIGGER_NONE */
+	return false;
+
+}
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index e7f0a380e6..088c4690d9 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -46,6 +46,7 @@
 #include "catalog/pg_authid.h"
 #include "catalog/storage.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/prepare.h"
 #include "commands/tablespace.h"
 #include "commands/trigger.h"
@@ -565,6 +566,12 @@ static const struct config_enum_entry wal_compression_options[] = {
 	{NULL, 0, false}
 };
 
+static const struct config_enum_entry ignore_event_trigger_options[] = {
+	{"none", IGNORE_EVENT_TRIGGER_NONE, false},
+	{"all", IGNORE_EVENT_TRIGGER_ALL, false},
+	{NULL, 0, false}
+};
+
 /*
  * Options for enum values stored in other modules
  */
@@ -4904,6 +4911,16 @@ static struct config_enum ConfigureNamesEnum[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"ignore_event_trigger", PGC_SUSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Disable event triggers during the session."),
+			NULL
+		},
+		&ignore_event_trigger,
+		IGNORE_EVENT_TRIGGER_NONE, ignore_event_trigger_options,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"wal_level", PGC_POSTMASTER, WAL_SETTINGS,
 			gettext_noop("Sets the level of information written to the WAL."),
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 10091c3aaf..9e7671e63f 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -29,6 +29,14 @@ typedef struct EventTriggerData
 	CommandTag	tag;
 } EventTriggerData;
 
+typedef enum ignore_event_trigger_events
+{
+	IGNORE_EVENT_TRIGGER_NONE,
+	IGNORE_EVENT_TRIGGER_ALL
+} IgnoreEventTriggersEvents;
+
+extern int ignore_event_trigger;
+
 #define AT_REWRITE_ALTER_PERSISTENCE	0x01
 #define AT_REWRITE_DEFAULT_VAL			0x02
 #define AT_REWRITE_COLUMN_REWRITE		0x04
-- 
2.24.3 (Apple Git-128)

v29-0002-Add-a-new-login-event-and-login-event-trigger-su.patchapplication/octet-stream; name=v29-0002-Add-a-new-login-event-and-login-event-trigger-su.patch; x-unix-mode=0644Download
From 5e0e9817cae754bb28f786ecf2435f459a5e7e81 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <daniel@yesql.se>
Date: Mon, 14 Mar 2022 14:27:10 +0100
Subject: [PATCH v29 2/2] Add a new "login" event and login event trigger
 support.

The login event occurs when a user logs in to the system.

Author: Konstantin Knizhnik and Greg Nancarrow
Discussion: https://postgr.es/m/0d46d29f-4558-3af9-9c85-7774e14a7709%40postgrespro.ru
---
 doc/src/sgml/bki.sgml                         |   2 +-
 doc/src/sgml/catalogs.sgml                    |  13 ++
 doc/src/sgml/ecpg.sgml                        |   2 +
 doc/src/sgml/event-trigger.sgml               |  81 ++++++++-
 doc/src/sgml/ref/create_event_trigger.sgml    |   2 +-
 src/backend/commands/dbcommands.c             |   1 +
 src/backend/commands/event_trigger.c          | 156 +++++++++++++++++-
 src/backend/tcop/postgres.c                   |   4 +
 src/backend/utils/cache/evtcache.c            |   2 +
 src/backend/utils/misc/guc.c                  |   1 +
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/bin/pg_dump/pg_dump.c                     |   5 +
 src/bin/psql/tab-complete.c                   |   3 +-
 src/include/catalog/pg_database.dat           |   2 +-
 src/include/catalog/pg_database.h             |   3 +
 src/include/commands/event_trigger.h          |   4 +-
 src/include/tcop/cmdtaglist.h                 |   1 +
 src/include/utils/evtcache.h                  |   3 +-
 src/test/recovery/t/001_stream_rep.pl         |  23 +++
 src/test/regress/expected/event_trigger.out   |  38 +++++
 src/test/regress/sql/event_trigger.sql        |  24 +++
 21 files changed, 359 insertions(+), 12 deletions(-)

diff --git a/doc/src/sgml/bki.sgml b/doc/src/sgml/bki.sgml
index 33955494c6..caef1df200 100644
--- a/doc/src/sgml/bki.sgml
+++ b/doc/src/sgml/bki.sgml
@@ -183,7 +183,7 @@
 { oid => '1', oid_symbol => 'TemplateDbOid',
   descr => 'database\'s default template',
   datname => 'template1', encoding => 'ENCODING', datistemplate => 't',
-  datallowconn => 't', datconnlimit => '-1', datfrozenxid => '0',
+  datallowconn => 't', dathasloginevt => 'f', datconnlimit => '-1', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datcollate => 'LC_COLLATE',
   datctype => 'LC_CTYPE', datacl => '_null_' },
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 2a8cd02664..4dd9039b8a 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2982,6 +2982,19 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>dathasloginevt</structfield> <type>bool</type>
+      </para>
+      <para>
+        Indicates that there are login event triggers defined for this database.
+        This flag is used to avoid extra lookups on the
+        <structname>pg_event_trigger</structname> table during each backend
+        startup.  This flag is used internally by <productname>PostgreSQL</productname>
+        and should not be manually altered or read for monitoring purposes.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>datconnlimit</structfield> <type>int4</type>
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index cdc4761c60..197b476205 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -4731,6 +4731,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = t (type: 1)
 datallowconn = t (type: 1)
+dathasloginevt = f (type: 1)
 datconnlimit = -1 (type: 5)
 datfrozenxid = 379 (type: 1)
 dattablespace = 1663 (type: 1)
@@ -4755,6 +4756,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = f (type: 1)
 datallowconn = t (type: 1)
+dathasloginevt = f (type: 1)
 datconnlimit = -1 (type: 5)
 datfrozenxid = 379 (type: 1)
 dattablespace = 1663 (type: 1)
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 9c66f97b0f..b568f3b2aa 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>login</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,18 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>login</literal> event occurs when a user logs in to the
+     system.
+     Any bugs in a trigger procedure for this event may prevent successful
+     login to the system. Such bugs may be fixed after first reconnecting to
+     the system with event triggers disabled (see <xref linkend="guc-ignore-event-trigger"/>)
+     or using restarting the system in single-user mode (as event triggers are
+     disabled in this mode).
+     See the <xref linkend="app-postgres"/> reference page for details about
+     using single-user mode.
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1156,7 +1169,7 @@ typedef struct EventTriggerData
   </sect1>
 
   <sect1 id="event-trigger-example">
-   <title>A Complete Event Trigger Example</title>
+   <title>A C language Event Trigger Example</title>
 
    <para>
     Here is a very simple example of an event trigger function written in C.
@@ -1296,6 +1309,72 @@ $$;
 CREATE EVENT TRIGGER no_rewrite_allowed
                   ON table_rewrite
    EXECUTE FUNCTION no_rewrite();
+</programlisting>
+   </para>
+ </sect1>
+
+  <sect1 id="event-trigger-database-login-example">
+   <title>A Database Login Event Trigger Example</title>
+
+   <para>
+    The event trigger on the <literal>login</literal> event can be
+    useful for logging user logins, for verifying the connection and
+    assigning roles according to current circumstances, or for some session
+    data initialization. It is vital that any event trigger using the
+    <literal>login</literal> event checks whether or not the database is in
+    recovery.
+   </para>
+
+   <para>
+    The following example demonstrates these options.
+<programlisting>
+-- create test tables and roles
+CREATE TABLE user_login_log (
+  "user" text,
+  "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+  RETURNS event_trigger SECURITY DEFINER
+  LANGUAGE plpgsql AS
+$$
+DECLARE
+  hour integer = EXTRACT('hour' FROM current_time);
+  rec boolean;
+BEGIN
+-- Ensure the database is not in recovery
+SELECT pg_is_in_recovery() INTO rec;
+IF rec THEN
+  RETURN;
+END IF
+
+-- 1) Assign some roles
+IF hour BETWEEN 8 AND 20 THEN         -- at daytime grant the day_worker role
+  EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT    day_worker TO '   || quote_ident(session_user);
+ELSIF hour BETWEEN 2 AND 4 THEN
+  RAISE EXCEPTION 'Login forbidden';  -- do not allow to login these hours
+ELSE                                  -- at other time grant the night_worker role
+  EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT  night_worker TO ' || quote_ident(session_user);
+END IF;
+
+-- 2) Initialize some user session data
+CREATE TEMP TABLE session_storage (x float, y integer);
+
+-- 3) Log the connection time
+INSERT INTO user_login_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+  ON login
+  EXECUTE FUNCTION init_session();
 </programlisting>
    </para>
  </sect1>
diff --git a/doc/src/sgml/ref/create_event_trigger.sgml b/doc/src/sgml/ref/create_event_trigger.sgml
index 1a78555c64..92c376b8e7 100644
--- a/doc/src/sgml/ref/create_event_trigger.sgml
+++ b/doc/src/sgml/ref/create_event_trigger.sgml
@@ -125,7 +125,7 @@ CREATE EVENT TRIGGER <replaceable class="parameter">name</replaceable>
    database so much that you can't even drop the trigger, restart in
    single-user mode and you'll be able to do that. Even triggers can also be
    temporarily disabled for such troubleshooting, see
-   <xref linkend="gux-ignore-event-trigger"/>.
+   <xref linkend="guc-ignore-event-trigger"/>.
   </para>
  </refsect1>
 
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 623e5ec778..08871ec5e0 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -712,6 +712,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	new_record[Anum_pg_database_datlocprovider - 1] = CharGetDatum(dblocprovider);
 	new_record[Anum_pg_database_datistemplate - 1] = BoolGetDatum(dbistemplate);
 	new_record[Anum_pg_database_datallowconn - 1] = BoolGetDatum(dballowconnections);
+	new_record[Anum_pg_database_dathasloginevt - 1] = BoolGetDatum(false);
 	new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 	new_record[Anum_pg_database_datfrozenxid - 1] = TransactionIdGetDatum(src_frozenxid);
 	new_record[Anum_pg_database_datminmxid - 1] = TransactionIdGetDatum(src_minmxid);
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 70995cfc49..4e2e6629bd 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -37,15 +38,18 @@
 #include "miscadmin.h"
 #include "parser/parse_func.h"
 #include "pgstat.h"
+#include "storage/lmgr.h"
 #include "tcop/deparse_utility.h"
 #include "tcop/utility.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 typedef struct EventTriggerQueryState
@@ -134,6 +138,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "login") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -297,6 +302,30 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
 	CatalogTupleInsert(tgrel, tuple);
 	heap_freetuple(tuple);
 
+	/*
+	 * Login event triggers have an additional flag in pg_database to allow
+	 * faster lookups in hot codepaths. Set the flag unless already True.
+	 */
+	if (strcmp(eventname, "login") == 0)
+	{
+		Form_pg_database db;
+		Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+
+		/* Set dathasloginevt flag in pg_database */
+		tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+		db = (Form_pg_database) GETSTRUCT(tuple);
+		if (!db->dathasloginevt)
+		{
+			db->dathasloginevt = true;
+			CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+			CommandCounterIncrement();
+		}
+		table_close(pg_db, RowExclusiveLock);
+		heap_freetuple(tuple);
+	}
+
 	/* Depend on owner. */
 	recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
 
@@ -584,10 +613,15 @@ EventTriggerCommonSetup(Node *parsetree,
 	{
 		CommandTag	dbgtag;
 
-		dbgtag = CreateCommandTag(parsetree);
+		if (event == EVT_Login)
+			dbgtag = CMDTAG_LOGIN;
+		else
+			dbgtag = CreateCommandTag(parsetree);
+
 		if (event == EVT_DDLCommandStart ||
 			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
+			event == EVT_SQLDrop ||
+			event == EVT_Login)
 		{
 			if (!command_tag_event_trigger_ok(dbgtag))
 				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
@@ -606,7 +640,10 @@ EventTriggerCommonSetup(Node *parsetree,
 		return NIL;
 
 	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
+	if (event == EVT_Login)
+		tag = CMDTAG_LOGIN;
+	else
+		tag = CreateCommandTag(parsetree);
 
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
@@ -807,6 +844,112 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Return true if this database has login event triggers, false otherwise.
+ */
+static bool
+DatabaseHasLoginEventTriggers(void)
+{
+	bool		has_login_event_triggers;
+	HeapTuple	tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+	has_login_event_triggers = ((Form_pg_database) GETSTRUCT(tuple))->dathasloginevt;
+	ReleaseSysCache(tuple);
+	return has_login_event_triggers;
+}
+
+/*
+ * Fire login event triggers if any are present.  The dathasloginevt
+ * pg_database flag is left when an event trigger is dropped, to avoid
+ * complicating the codepath in the case of multiple event triggers.  This
+ * function will instead unset the flag if no trigger is defined.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId)
+		|| ignore_event_trigger_check(EVT_Login))
+		return;
+
+	StartTransactionCommand();
+
+	if (DatabaseHasLoginEventTriggers())
+	{
+		runlist = EventTriggerCommonSetup(NULL,
+										  EVT_Login, "login",
+										  &trigdata);
+
+		if (runlist != NIL)
+		{
+			/*
+			 * Event trigger execution may require an active snapshot.
+			 */
+			PushActiveSnapshot(GetTransactionSnapshot());
+
+			/* Run the triggers. */
+			EventTriggerInvoke(runlist, &trigdata);
+
+			/* Cleanup. */
+			list_free(runlist);
+
+			PopActiveSnapshot();
+		}
+		else
+		{
+			/*
+			 * Runlist is empty: clear dathasloginevt flag
+			 */
+			Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+			HeapTuple	tuple;
+			Form_pg_database db;
+
+			LockSharedObject(DatabaseRelationId, MyDatabaseId, 0, AccessExclusiveLock);
+
+			tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+			db = (Form_pg_database) GETSTRUCT(tuple);
+			if (db->dathasloginevt)
+			{
+				/*
+				 * There can be a race condition: a login event trigger may
+				 * have been added after the pg_event_trigger table was
+				 * scanned, and we don't want to erroneously clear the
+				 * dathasloginevt flag in this case. To be sure that this
+				 * hasn't happened, repeat the scan under the pg_database
+				 * table lock.
+				 */
+				runlist = EventTriggerCommonSetup(NULL,
+												  EVT_Login, "login",
+												  &trigdata);
+				if (runlist == NIL)	/* list is still empty, so clear the
+										 * flag */
+				{
+					db->dathasloginevt = false;
+					CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				}
+				else
+					list_free(runlist);
+			}
+			table_close(pg_db, RowExclusiveLock);
+			heap_freetuple(tuple);
+		}
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
@@ -2186,8 +2329,7 @@ stringify_adefprivs_objtype(ObjectType objtype)
 
 /*
  * Checks whether the specified event is ignored by the ignore_event_trigger
- * GUC or not. Currently, the GUC only supports ignoreing all or nothing but
- * that will most likely change so the function takes an event to aid that.
+ * GUC or not.
  */
 static bool
 ignore_event_trigger_check(EventTriggerEvent event)
@@ -2197,6 +2339,10 @@ ignore_event_trigger_check(EventTriggerEvent event)
 	if (ignore_event_trigger == IGNORE_EVENT_TRIGGER_ALL)
 		return true;
 
+	if (ignore_event_trigger == IGNORE_EVENT_TRIGGER_LOGIN
+		&& event == EVT_Login)
+		return true;
+
 	/* IGNORE_EVENT_TRIGGER_NONE */
 	return false;
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index ba2fcfeb4a..5638cd95b2 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -41,6 +41,7 @@
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/prepare.h"
 #include "common/pg_prng.h"
 #include "jit/jit.h"
@@ -4202,6 +4203,9 @@ PostgresMain(const char *dbname, const char *username)
 	initStringInfo(&row_description_buf);
 	MemoryContextSwitchTo(TopMemoryContext);
 
+	/* Fire any defined login event triggers, if appropriate */
+	EventTriggerOnLogin();
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 3a9c9f0c50..43a38ae6c7 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "login") == 0)
+			event = EVT_Login;
 		else
 			continue;
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 088c4690d9..c5d53386e7 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -569,6 +569,7 @@ static const struct config_enum_entry wal_compression_options[] = {
 static const struct config_enum_entry ignore_event_trigger_options[] = {
 	{"none", IGNORE_EVENT_TRIGGER_NONE, false},
 	{"all", IGNORE_EVENT_TRIGGER_ALL, false},
+	{"login", IGNORE_EVENT_TRIGGER_LOGIN, false},
 	{NULL, 0, false}
 };
 
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 4cf5b26a36..0d7623fc7a 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -674,6 +674,7 @@
 					#   error
 #search_path = '"$user", public'	# schema names
 #row_security = on
+#ignore_event_trigger = 'none'
 #default_table_access_method = 'heap'
 #default_tablespace = ''		# a tablespace name, '' uses the default
 #default_toast_compression = 'pglz'	# 'pglz' or 'lz4'
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index e5816c4cce..dd756ae4db 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3052,6 +3052,11 @@ dumpDatabase(Archive *fout)
 		appendPQExpBufferStr(delQry, ";\n");
 	}
 
+	/*
+	 * We do not restore pg_database.dathasloginevt because it is set
+	 * automatically on login event trigger creation.
+	 */
+
 	/* Add database-specific SET options */
 	dumpDatabaseConfig(fout, creaQry, datname, dbCatId.oid);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 5c064595a9..8850324a3b 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3395,7 +3395,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end",
+					  "login", "sql_drop");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat
index 5feedff7bf..78402d48bc 100644
--- a/src/include/catalog/pg_database.dat
+++ b/src/include/catalog/pg_database.dat
@@ -15,7 +15,7 @@
 { oid => '1', oid_symbol => 'TemplateDbOid',
   descr => 'default template for new databases',
   datname => 'template1', encoding => 'ENCODING', datlocprovider => 'LOCALE_PROVIDER', datistemplate => 't',
-  datallowconn => 't', datconnlimit => '-1', datfrozenxid => '0',
+  datallowconn => 't', dathasloginevt => 'f', datconnlimit => '-1', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datcollate => 'LC_COLLATE',
   datctype => 'LC_CTYPE', daticulocale => 'ICU_LOCALE', datacl => '_null_' },
 
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index a9f4a8071f..55d7ae7988 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -49,6 +49,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
 	/* new connections allowed? */
 	bool		datallowconn;
 
+	/* database has login event triggers? */
+	bool		dathasloginevt;
+
 	/* max connections allowed (-1=no limit) */
 	int32		datconnlimit;
 
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 9e7671e63f..2400cc7d7a 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -32,7 +32,8 @@ typedef struct EventTriggerData
 typedef enum ignore_event_trigger_events
 {
 	IGNORE_EVENT_TRIGGER_NONE,
-	IGNORE_EVENT_TRIGGER_ALL
+	IGNORE_EVENT_TRIGGER_ALL,
+	IGNORE_EVENT_TRIGGER_LOGIN
 } IgnoreEventTriggersEvents;
 
 extern int ignore_event_trigger;
@@ -62,6 +63,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnLogin(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 4bc7ddf410..d6e408e1ed 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
+PG_CMDTAG(CMDTAG_LOGIN, "LOGIN", true, false, false)
 PG_CMDTAG(CMDTAG_MOVE, "MOVE", false, false, true)
 PG_CMDTAG(CMDTAG_NOTIFY, "NOTIFY", false, false, false)
 PG_CMDTAG(CMDTAG_PREPARE, "PREPARE", false, false, false)
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index ddb67a68fa..a66344aacb 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Login,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 583ee87da8..c3dbd75af3 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,24 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE TABLE user_logins(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO user_logins (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 my $primary_lsn = $node_primary->lsn('write');
 $node_primary->wait_for_catchup($node_standby_1, 'replay', $primary_lsn);
@@ -387,6 +405,11 @@ sub replay_check
 
 replay_check();
 
+my $evttrig = $node_standby_1->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+$evttrig = $node_standby_2->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 44d545de25..fb2c799ebb 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -604,3 +604,41 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     1
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     2
+(1 row)
+
+-- Check dathasloginevt in system catalog
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+ dathasloginevt 
+----------------
+ t
+(1 row)
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 1446cf8cc8..3123cbb23d 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -462,3 +462,27 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+SELECT COUNT(*) FROM user_logins;
+\c
+SELECT COUNT(*) FROM user_logins;
+
+-- Check dathasloginevt in system catalog
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
-- 
2.24.3 (Apple Git-128)

#106Noname
a.sokolov@postgrespro.ru
In reply to: Daniel Gustafsson (#105)
Re: On login trigger: take three

Daniel Gustafsson писал 2022-03-22 11:43:

On 22 Mar 2022, at 04:48, Andres Freund <andres@anarazel.de> wrote:

docs fail to build:
https://cirrus-ci.com/task/5556234047717376?logs=docs_build#L349

Ugh, that one was on me and not the original author. Fixed.

+    data initialization. It is vital that any event trigger using the
+    <literal>login</literal> event checks whether or not the database 
is in
+    recovery.

Does any trigger really have to contain a pg_is_in_recovery() call? In
this message
(/messages/by-id/20220312024652.lvgehszwke4hhove@alap3.anarazel.de)
it was only about triggers on hot standby, which run not read-only
queries

--
Andrey Sokolov
Postgres Professional: http://www.postgrespro.com

#107Andres Freund
andres@anarazel.de
In reply to: Noname (#106)
Re: On login trigger: take three

Hi,

On 2022-03-28 15:57:37 +0300, a.sokolov@postgrespro.ru wrote:

+    data initialization. It is vital that any event trigger using the
+    <literal>login</literal> event checks whether or not the database is in
+    recovery.

Does any trigger really have to contain a pg_is_in_recovery() call?

Not *any* trigger, just any trigger that writes.

In this message
(/messages/by-id/20220312024652.lvgehszwke4hhove@alap3.anarazel.de)
it was only about triggers on hot standby, which run not read-only queries

The problem precisely is that the login triggers run on hot standby nodes, and
that if they do writes, you can't login anymore.

Greetings,

Andres Freund

#108Daniel Gustafsson
daniel@yesql.se
In reply to: Andres Freund (#107)
Re: On login trigger: take three

On 28 Mar 2022, at 19:10, Andres Freund <andres@anarazel.de> wrote:
On 2022-03-28 15:57:37 +0300, a.sokolov@postgrespro.ru wrote:

+    data initialization. It is vital that any event trigger using the
+    <literal>login</literal> event checks whether or not the database is in
+    recovery.

Does any trigger really have to contain a pg_is_in_recovery() call?

Not *any* trigger, just any trigger that writes.

Thats correct, the docs should be updated with something like the below I
reckon.

It is vital that event trigger using the <literal>login</literal> event
which has side-effects checks whether or not the database is in recovery to
ensure they are not performing modifications to hot standby nodes.

In this message
(/messages/by-id/20220312024652.lvgehszwke4hhove@alap3.anarazel.de)
it was only about triggers on hot standby, which run not read-only queries

The problem precisely is that the login triggers run on hot standby nodes, and
that if they do writes, you can't login anymore.

Do you think this potential foot-gun is scary enough to reject this patch?
There are lots of creative ways to cause Nagios alerts from ones database, but
this has the potential to do so with a small bug in userland code. Still, I
kind of like the feature so I'm indecisive.

--
Daniel Gustafsson https://vmware.com/

#109Andres Freund
andres@anarazel.de
In reply to: Daniel Gustafsson (#108)
Re: On login trigger: take three

Hi,

On 2022-03-28 23:27:56 +0200, Daniel Gustafsson wrote:

On 28 Mar 2022, at 19:10, Andres Freund <andres@anarazel.de> wrote:
On 2022-03-28 15:57:37 +0300, a.sokolov@postgrespro.ru wrote:

+    data initialization. It is vital that any event trigger using the
+    <literal>login</literal> event checks whether or not the database is in
+    recovery.

Does any trigger really have to contain a pg_is_in_recovery() call?

Not *any* trigger, just any trigger that writes.

Thats correct, the docs should be updated with something like the below I
reckon.

It is vital that event trigger using the <literal>login</literal> event
which has side-effects checks whether or not the database is in recovery to
ensure they are not performing modifications to hot standby nodes.

Maybe side-effects is a bit too general? Emitting a log message, rejecting a
login, setting some GUCs, etc are all side-effects too.

In this message
(/messages/by-id/20220312024652.lvgehszwke4hhove@alap3.anarazel.de)
it was only about triggers on hot standby, which run not read-only queries

The problem precisely is that the login triggers run on hot standby nodes, and
that if they do writes, you can't login anymore.

Do you think this potential foot-gun is scary enough to reject this patch?
There are lots of creative ways to cause Nagios alerts from ones database, but
this has the potential to do so with a small bug in userland code. Still, I
kind of like the feature so I'm indecisive.

It does seem like a huge footgun. But also kinda useful. So I'm really +-0.

Greetings,

Andres Freund

#110Daniel Gustafsson
daniel@yesql.se
In reply to: Andres Freund (#109)
Re: On login trigger: take three

On 28 Mar 2022, at 23:31, Andres Freund <andres@anarazel.de> wrote:

Hi,

On 2022-03-28 23:27:56 +0200, Daniel Gustafsson wrote:

On 28 Mar 2022, at 19:10, Andres Freund <andres@anarazel.de> wrote:
On 2022-03-28 15:57:37 +0300, a.sokolov@postgrespro.ru wrote:

+    data initialization. It is vital that any event trigger using the
+    <literal>login</literal> event checks whether or not the database is in
+    recovery.

Does any trigger really have to contain a pg_is_in_recovery() call?

Not *any* trigger, just any trigger that writes.

Thats correct, the docs should be updated with something like the below I
reckon.

It is vital that event trigger using the <literal>login</literal> event
which has side-effects checks whether or not the database is in recovery to
ensure they are not performing modifications to hot standby nodes.

Maybe side-effects is a bit too general? Emitting a log message, rejecting a
login, setting some GUCs, etc are all side-effects too.

Good point, it needs to say that modifications that cause WAL to be generated
are prohibited, but in a more user-friendly readable way. Perhaps in a big red
warning box.

In this message
(/messages/by-id/20220312024652.lvgehszwke4hhove@alap3.anarazel.de)
it was only about triggers on hot standby, which run not read-only queries

The problem precisely is that the login triggers run on hot standby nodes, and
that if they do writes, you can't login anymore.

Do you think this potential foot-gun is scary enough to reject this patch?
There are lots of creative ways to cause Nagios alerts from ones database, but
this has the potential to do so with a small bug in userland code. Still, I
kind of like the feature so I'm indecisive.

It does seem like a huge footgun. But also kinda useful. So I'm really +-0.

Looks like we are in agreement here. I'm going to go over it again and sleep
on it some more before the deadline hits.

--
Daniel Gustafsson https://vmware.com/

#111Tom Lane
tgl@sss.pgh.pa.us
In reply to: Andres Freund (#109)
Re: On login trigger: take three

Andres Freund <andres@anarazel.de> writes:

On 2022-03-28 23:27:56 +0200, Daniel Gustafsson wrote:

Do you think this potential foot-gun is scary enough to reject this patch?
There are lots of creative ways to cause Nagios alerts from ones database, but
this has the potential to do so with a small bug in userland code. Still, I
kind of like the feature so I'm indecisive.

It does seem like a huge footgun. But also kinda useful. So I'm really +-0.

An on-login trigger is *necessarily* a foot-gun; I don't see that this
particular failure mode makes it any worse than it would be anyway.
There has to be some not-too-difficult-to-use way to bypass a broken
login trigger. Assuming we are happy with the design for doing that,
might as well accept the hazards.

regards, tom lane

In reply to: Andres Freund (#109)
Re[2]: On login trigger: take three

 
Hi,

Tue, March 29, 2022, 0:31 +03:00 from Andres Freund <andres@anarazel.de>:
 
Hi,

On 2022-03-28 23:27:56 +0200, Daniel Gustafsson wrote:

On 28 Mar 2022, at 19:10, Andres Freund < andres@anarazel.de > wrote:
On 2022-03-28 15:57:37 +0300, a.sokolov@postgrespro.ru wrote:

+ data initialization. It is vital that any event trigger using the
+ <literal>login</literal> event checks whether or not the database is in
+ recovery.

»

 

Does any trigger really have to contain a pg_is_in_recovery() call?

Not *any* trigger, just any trigger that writes.

Thats correct, the docs should be updated with something like the below I
reckon.

It is vital that event trigger using the <literal>login</literal> event
which has side-effects checks whether or not the database is in recovery to
ensure they are not performing modifications to hot standby nodes.

Maybe side-effects is a bit too general? Emitting a log message, rejecting a
login, setting some GUCs, etc are all side-effects too.

Something like this:
 
<important>
    <para>
      The <literal>login</literal> triggers fire also on standby servers.
      To keep them from becoming inaccessible, such triggers should
      avoid writing anything to the database when running on a standby.
      This can be achieved by checking <function>pg_is_in_recovery</function>(), see an example below.
    </para>
</important>

 

Also, please fix a typo in doc/src/sgml/ref/create_event_trigger.sgml :
 
- single-user mode and you'll be able to do that. Even triggers can also be
+ single-user mode and you'll be able to do that. Event triggers can also be
 
Regarding the trigger function example:
It does not do anything if run on a standby. To show that it can do something on a standby to, I propose to move throwing the night exception to the beginning.
So it will be:
 
CREATE OR REPLACE FUNCTION init_session() 
RETURNS event_trigger SECURITY DEFINER LANGUAGE plpgsql AS 
$$ 
DECLARE 
  hour integer = EXTRACT('hour' FROM current_time); 
  rec boolean;
BEGIN

-- 1) Forbid logging in late:
IF hour BETWEEN 2 AND 4 THEN
RAISE EXCEPTION 'Login forbidden'; -- do not allow to login these hours
END IF;

-- The remaining stuff cannot be done on standbys,
-- so ensure the database is not in recovery
SELECT pg_is_in_recovery() INTO rec;
IF rec THEN
RETURN;
END IF

-- 2) Assign some roles

IF hour BETWEEN 8 AND 20 THEN -- at daytime grant the day_worker role
EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
EXECUTE 'GRANT day_worker TO ' || quote_ident(session_user);
ELSE -- at other time grant the night_worker role
EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
EXECUTE 'GRANT night_worker TO ' || quote_ident(session_user);
END IF;

-- 3) Initialize some user session data

CREATE TEMP TABLE session_storage (x float, y integer);

-- 4) Log the connection time

INSERT INTO user_login_log VALUES (session_user, current_timestamp);

END;
$$;
Finally, let me propose to append to the regression test the following:
 
 
\c
SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
 
which should output:
dathasloginevt
----------------
f
(1 row)
 
So we can check that removal of the event trigger resets this flag in pg_database. Note that reconnect (\c) is necessary here.
 
Regards,
Ivan
 

In this message
( /messages/by-id/20220312024652.lvgehszwke4hhove@alap3.anarazel.de )
it was only about triggers on hot standby, which run not read-only queries

The problem precisely is that the login triggers run on hot standby nodes, and
that if they do writes, you can't login anymore.

Do you think this potential foot-gun is scary enough to reject this patch?
There are lots of creative ways to cause Nagios alerts from ones database, but
this has the potential to do so with a small bug in userland code. Still, I
kind of like the feature so I'm indecisive.

It does seem like a huge footgun. But also kinda useful. So I'm really +-0.

Greetings,

Andres Freund

 

#113Daniel Gustafsson
daniel@yesql.se
In reply to: Ivan Panchenko (#112)
2 attachment(s)
Re: On login trigger: take three

On 30 Mar 2022, at 13:21, Ivan Panchenko <wao@mail.ru> wrote:
Maybe side-effects is a bit too general? Emitting a log message, rejecting a
login, setting some GUCs, etc are all side-effects too.
Something like this:

I've reworded the docs close to what you suggested here.

Also, please fix a typo in doc/src/sgml/ref/create_event_trigger.sgml :

Done.

Regarding the trigger function example:
It does not do anything if run on a standby. To show that it can do something on a standby to, I propose to move throwing the night exception to the beginning.

Good idea, done.

Finally, let me propose to append to the regression test the following:

Also a good idea, done.

--
Daniel Gustafsson https://vmware.com/

Attachments:

v30-0002-Add-a-new-login-event-and-login-event-trigger-su.patchapplication/octet-stream; name=v30-0002-Add-a-new-login-event-and-login-event-trigger-su.patch; x-unix-mode=0644Download
From 7d191279d39a6052fb5b7436bb379da934456628 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <daniel@yesql.se>
Date: Wed, 30 Mar 2022 14:34:47 +0200
Subject: [PATCH v30 2/2] Add a new "login" event and login event trigger
 support.

The login event occurs when a user logs in to the system.

Author: Konstantin Knizhnik and Greg Nancarrow
Discussion: https://postgr.es/m/0d46d29f-4558-3af9-9c85-7774e14a7709%40postgrespro.ru
---
 doc/src/sgml/bki.sgml                         |   2 +-
 doc/src/sgml/catalogs.sgml                    |  13 ++
 doc/src/sgml/ecpg.sgml                        |   2 +
 doc/src/sgml/event-trigger.sgml               |  88 +++++++++-
 doc/src/sgml/ref/create_event_trigger.sgml    |   4 +-
 src/backend/commands/dbcommands.c             |   1 +
 src/backend/commands/event_trigger.c          | 156 +++++++++++++++++-
 src/backend/tcop/postgres.c                   |   4 +
 src/backend/utils/cache/evtcache.c            |   2 +
 src/backend/utils/misc/guc.c                  |   1 +
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/bin/pg_dump/pg_dump.c                     |   5 +
 src/bin/psql/tab-complete.c                   |   3 +-
 src/include/catalog/pg_database.dat           |   2 +-
 src/include/catalog/pg_database.h             |   3 +
 src/include/commands/event_trigger.h          |   4 +-
 src/include/tcop/cmdtaglist.h                 |   1 +
 src/include/utils/evtcache.h                  |   3 +-
 src/test/recovery/t/001_stream_rep.pl         |  23 +++
 src/test/regress/expected/event_trigger.out   |  45 +++++
 src/test/regress/sql/event_trigger.sql        |  26 +++
 21 files changed, 376 insertions(+), 13 deletions(-)

diff --git a/doc/src/sgml/bki.sgml b/doc/src/sgml/bki.sgml
index 33955494c6..caef1df200 100644
--- a/doc/src/sgml/bki.sgml
+++ b/doc/src/sgml/bki.sgml
@@ -183,7 +183,7 @@
 { oid => '1', oid_symbol => 'TemplateDbOid',
   descr => 'database\'s default template',
   datname => 'template1', encoding => 'ENCODING', datistemplate => 't',
-  datallowconn => 't', datconnlimit => '-1', datfrozenxid => '0',
+  datallowconn => 't', dathasloginevt => 'f', datconnlimit => '-1', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datcollate => 'LC_COLLATE',
   datctype => 'LC_CTYPE', datacl => '_null_' },
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 7f4f79d1b5..d018f3dbd8 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2982,6 +2982,19 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>dathasloginevt</structfield> <type>bool</type>
+      </para>
+      <para>
+        Indicates that there are login event triggers defined for this database.
+        This flag is used to avoid extra lookups on the
+        <structname>pg_event_trigger</structname> table during each backend
+        startup.  This flag is used internally by <productname>PostgreSQL</productname>
+        and should not be manually altered or read for monitoring purposes.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>datconnlimit</structfield> <type>int4</type>
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index cdc4761c60..197b476205 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -4731,6 +4731,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = t (type: 1)
 datallowconn = t (type: 1)
+dathasloginevt = f (type: 1)
 datconnlimit = -1 (type: 5)
 datfrozenxid = 379 (type: 1)
 dattablespace = 1663 (type: 1)
@@ -4755,6 +4756,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = f (type: 1)
 datallowconn = t (type: 1)
+dathasloginevt = f (type: 1)
 datconnlimit = -1 (type: 5)
 datfrozenxid = 379 (type: 1)
 dattablespace = 1663 (type: 1)
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 9c66f97b0f..34837c5ac7 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>login</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,20 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>login</literal> event occurs when an authenticated user logs
+     in to the system. Any bugs in a trigger procedure for this event may
+     prevent successful login to the system. Such bugs may be fixed after first
+     reconnecting to the system with event triggers disabled (see
+     <xref linkend="guc-ignore-event-trigger"/>) or using restarting the system
+     in single-user mode (as event triggers are disabled in this mode). See the
+     <xref linkend="app-postgres"/> reference page for details about using
+     single-user mode.  The <literal>login</literal> event will also fire on
+     standby servers.  To precvent servers from becoming inaccessible, such
+     triggers must avoid writing anything to the database when running on a
+     standby.
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1156,7 +1171,7 @@ typedef struct EventTriggerData
   </sect1>
 
   <sect1 id="event-trigger-example">
-   <title>A Complete Event Trigger Example</title>
+   <title>A C language Event Trigger Example</title>
 
    <para>
     Here is a very simple example of an event trigger function written in C.
@@ -1296,6 +1311,77 @@ $$;
 CREATE EVENT TRIGGER no_rewrite_allowed
                   ON table_rewrite
    EXECUTE FUNCTION no_rewrite();
+</programlisting>
+   </para>
+ </sect1>
+
+  <sect1 id="event-trigger-database-login-example">
+   <title>A Database Login Event Trigger Example</title>
+
+   <para>
+    The event trigger on the <literal>login</literal> event can be
+    useful for logging user logins, for verifying the connection and
+    assigning roles according to current circumstances, or for some session
+    data initialization. It is very important that any event trigger using the
+    <literal>login</literal> event checks whether or not the database is in
+    recovery before performing any writes. Writing to a standby server will
+    make it inaccessible.
+   </para>
+
+   <para>
+    The following example demonstrates these options.
+<programlisting>
+-- create test tables and roles
+CREATE TABLE user_login_log (
+  "user" text,
+  "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+  RETURNS event_trigger SECURITY DEFINER
+  LANGUAGE plpgsql AS
+$$
+DECLARE
+  hour integer = EXTRACT('hour' FROM current_time);
+  rec boolean;
+BEGIN
+-- 1. Forbid logging in during nighttime
+IF hour BETWEEN 2 AND 4 THEN
+  RAISE EXCEPTION 'Login forbidden';
+END IF;
+
+-- The checks below cannot be performed on standby servers so
+-- ensure the database is not in recovery
+SELECT pg_is_in_recovery() INTO rec;
+IF rec THEN
+  RETURN;
+END IF
+
+-- 2. Assign some roles
+IF hour BETWEEN 8 AND 20 THEN         -- at daytime grant the day_worker role
+  EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT    day_worker TO '   || quote_ident(session_user);
+ELSE                                  -- at other time grant the night_worker role
+  EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT  night_worker TO ' || quote_ident(session_user);
+END IF;
+
+-- 3. Initialize some user session data
+CREATE TEMP TABLE session_storage (x float, y integer);
+
+-- 4. Log the connection time
+INSERT INTO user_login_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+  ON login
+  EXECUTE FUNCTION init_session();
 </programlisting>
    </para>
  </sect1>
diff --git a/doc/src/sgml/ref/create_event_trigger.sgml b/doc/src/sgml/ref/create_event_trigger.sgml
index 1a78555c64..f6922c3de3 100644
--- a/doc/src/sgml/ref/create_event_trigger.sgml
+++ b/doc/src/sgml/ref/create_event_trigger.sgml
@@ -123,9 +123,9 @@ CREATE EVENT TRIGGER <replaceable class="parameter">name</replaceable>
    Event triggers are disabled in single-user mode (see <xref
    linkend="app-postgres"/>).  If an erroneous event trigger disables the
    database so much that you can't even drop the trigger, restart in
-   single-user mode and you'll be able to do that. Even triggers can also be
+   single-user mode and you'll be able to do that. Event triggers can also be
    temporarily disabled for such troubleshooting, see
-   <xref linkend="gux-ignore-event-trigger"/>.
+   <xref linkend="guc-ignore-event-trigger"/>.
   </para>
  </refsect1>
 
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index df16533901..5b95236598 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -1303,6 +1303,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	new_record[Anum_pg_database_datlocprovider - 1] = CharGetDatum(dblocprovider);
 	new_record[Anum_pg_database_datistemplate - 1] = BoolGetDatum(dbistemplate);
 	new_record[Anum_pg_database_datallowconn - 1] = BoolGetDatum(dballowconnections);
+	new_record[Anum_pg_database_dathasloginevt - 1] = BoolGetDatum(false);
 	new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 	new_record[Anum_pg_database_datfrozenxid - 1] = TransactionIdGetDatum(src_frozenxid);
 	new_record[Anum_pg_database_datminmxid - 1] = TransactionIdGetDatum(src_minmxid);
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 70995cfc49..4e2e6629bd 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -37,15 +38,18 @@
 #include "miscadmin.h"
 #include "parser/parse_func.h"
 #include "pgstat.h"
+#include "storage/lmgr.h"
 #include "tcop/deparse_utility.h"
 #include "tcop/utility.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 typedef struct EventTriggerQueryState
@@ -134,6 +138,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "login") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -297,6 +302,30 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
 	CatalogTupleInsert(tgrel, tuple);
 	heap_freetuple(tuple);
 
+	/*
+	 * Login event triggers have an additional flag in pg_database to allow
+	 * faster lookups in hot codepaths. Set the flag unless already True.
+	 */
+	if (strcmp(eventname, "login") == 0)
+	{
+		Form_pg_database db;
+		Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+
+		/* Set dathasloginevt flag in pg_database */
+		tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+		db = (Form_pg_database) GETSTRUCT(tuple);
+		if (!db->dathasloginevt)
+		{
+			db->dathasloginevt = true;
+			CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+			CommandCounterIncrement();
+		}
+		table_close(pg_db, RowExclusiveLock);
+		heap_freetuple(tuple);
+	}
+
 	/* Depend on owner. */
 	recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
 
@@ -584,10 +613,15 @@ EventTriggerCommonSetup(Node *parsetree,
 	{
 		CommandTag	dbgtag;
 
-		dbgtag = CreateCommandTag(parsetree);
+		if (event == EVT_Login)
+			dbgtag = CMDTAG_LOGIN;
+		else
+			dbgtag = CreateCommandTag(parsetree);
+
 		if (event == EVT_DDLCommandStart ||
 			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
+			event == EVT_SQLDrop ||
+			event == EVT_Login)
 		{
 			if (!command_tag_event_trigger_ok(dbgtag))
 				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
@@ -606,7 +640,10 @@ EventTriggerCommonSetup(Node *parsetree,
 		return NIL;
 
 	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
+	if (event == EVT_Login)
+		tag = CMDTAG_LOGIN;
+	else
+		tag = CreateCommandTag(parsetree);
 
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
@@ -807,6 +844,112 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Return true if this database has login event triggers, false otherwise.
+ */
+static bool
+DatabaseHasLoginEventTriggers(void)
+{
+	bool		has_login_event_triggers;
+	HeapTuple	tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+	has_login_event_triggers = ((Form_pg_database) GETSTRUCT(tuple))->dathasloginevt;
+	ReleaseSysCache(tuple);
+	return has_login_event_triggers;
+}
+
+/*
+ * Fire login event triggers if any are present.  The dathasloginevt
+ * pg_database flag is left when an event trigger is dropped, to avoid
+ * complicating the codepath in the case of multiple event triggers.  This
+ * function will instead unset the flag if no trigger is defined.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId)
+		|| ignore_event_trigger_check(EVT_Login))
+		return;
+
+	StartTransactionCommand();
+
+	if (DatabaseHasLoginEventTriggers())
+	{
+		runlist = EventTriggerCommonSetup(NULL,
+										  EVT_Login, "login",
+										  &trigdata);
+
+		if (runlist != NIL)
+		{
+			/*
+			 * Event trigger execution may require an active snapshot.
+			 */
+			PushActiveSnapshot(GetTransactionSnapshot());
+
+			/* Run the triggers. */
+			EventTriggerInvoke(runlist, &trigdata);
+
+			/* Cleanup. */
+			list_free(runlist);
+
+			PopActiveSnapshot();
+		}
+		else
+		{
+			/*
+			 * Runlist is empty: clear dathasloginevt flag
+			 */
+			Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+			HeapTuple	tuple;
+			Form_pg_database db;
+
+			LockSharedObject(DatabaseRelationId, MyDatabaseId, 0, AccessExclusiveLock);
+
+			tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+			db = (Form_pg_database) GETSTRUCT(tuple);
+			if (db->dathasloginevt)
+			{
+				/*
+				 * There can be a race condition: a login event trigger may
+				 * have been added after the pg_event_trigger table was
+				 * scanned, and we don't want to erroneously clear the
+				 * dathasloginevt flag in this case. To be sure that this
+				 * hasn't happened, repeat the scan under the pg_database
+				 * table lock.
+				 */
+				runlist = EventTriggerCommonSetup(NULL,
+												  EVT_Login, "login",
+												  &trigdata);
+				if (runlist == NIL)	/* list is still empty, so clear the
+										 * flag */
+				{
+					db->dathasloginevt = false;
+					CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				}
+				else
+					list_free(runlist);
+			}
+			table_close(pg_db, RowExclusiveLock);
+			heap_freetuple(tuple);
+		}
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
@@ -2186,8 +2329,7 @@ stringify_adefprivs_objtype(ObjectType objtype)
 
 /*
  * Checks whether the specified event is ignored by the ignore_event_trigger
- * GUC or not. Currently, the GUC only supports ignoreing all or nothing but
- * that will most likely change so the function takes an event to aid that.
+ * GUC or not.
  */
 static bool
 ignore_event_trigger_check(EventTriggerEvent event)
@@ -2197,6 +2339,10 @@ ignore_event_trigger_check(EventTriggerEvent event)
 	if (ignore_event_trigger == IGNORE_EVENT_TRIGGER_ALL)
 		return true;
 
+	if (ignore_event_trigger == IGNORE_EVENT_TRIGGER_LOGIN
+		&& event == EVT_Login)
+		return true;
+
 	/* IGNORE_EVENT_TRIGGER_NONE */
 	return false;
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index ba2fcfeb4a..5638cd95b2 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -41,6 +41,7 @@
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/prepare.h"
 #include "common/pg_prng.h"
 #include "jit/jit.h"
@@ -4202,6 +4203,9 @@ PostgresMain(const char *dbname, const char *username)
 	initStringInfo(&row_description_buf);
 	MemoryContextSwitchTo(TopMemoryContext);
 
+	/* Fire any defined login event triggers, if appropriate */
+	EventTriggerOnLogin();
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 3a9c9f0c50..43a38ae6c7 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "login") == 0)
+			event = EVT_Login;
 		else
 			continue;
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 1c5b277fb0..cf557a691e 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -570,6 +570,7 @@ static const struct config_enum_entry wal_compression_options[] = {
 static const struct config_enum_entry ignore_event_trigger_options[] = {
 	{"none", IGNORE_EVENT_TRIGGER_NONE, false},
 	{"all", IGNORE_EVENT_TRIGGER_ALL, false},
+	{"login", IGNORE_EVENT_TRIGGER_LOGIN, false},
 	{NULL, 0, false}
 };
 
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index b933fade8c..749073e467 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -675,6 +675,7 @@
 					#   error
 #search_path = '"$user", public'	# schema names
 #row_security = on
+#ignore_event_trigger = 'none'
 #default_table_access_method = 'heap'
 #default_tablespace = ''		# a tablespace name, '' uses the default
 #default_toast_compression = 'pglz'	# 'pglz' or 'lz4'
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 535b160165..aabdcab1b4 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3052,6 +3052,11 @@ dumpDatabase(Archive *fout)
 		appendPQExpBufferStr(delQry, ";\n");
 	}
 
+	/*
+	 * We do not restore pg_database.dathasloginevt because it is set
+	 * automatically on login event trigger creation.
+	 */
+
 	/* Add database-specific SET options */
 	dumpDatabaseConfig(fout, creaQry, datname, dbCatId.oid);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 3f9dfffd57..e13c0fb9dd 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3418,7 +3418,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end",
+					  "login", "sql_drop");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat
index 5feedff7bf..78402d48bc 100644
--- a/src/include/catalog/pg_database.dat
+++ b/src/include/catalog/pg_database.dat
@@ -15,7 +15,7 @@
 { oid => '1', oid_symbol => 'TemplateDbOid',
   descr => 'default template for new databases',
   datname => 'template1', encoding => 'ENCODING', datlocprovider => 'LOCALE_PROVIDER', datistemplate => 't',
-  datallowconn => 't', datconnlimit => '-1', datfrozenxid => '0',
+  datallowconn => 't', dathasloginevt => 'f', datconnlimit => '-1', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datcollate => 'LC_COLLATE',
   datctype => 'LC_CTYPE', daticulocale => 'ICU_LOCALE', datacl => '_null_' },
 
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index a9f4a8071f..55d7ae7988 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -49,6 +49,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
 	/* new connections allowed? */
 	bool		datallowconn;
 
+	/* database has login event triggers? */
+	bool		dathasloginevt;
+
 	/* max connections allowed (-1=no limit) */
 	int32		datconnlimit;
 
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 9e7671e63f..2400cc7d7a 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -32,7 +32,8 @@ typedef struct EventTriggerData
 typedef enum ignore_event_trigger_events
 {
 	IGNORE_EVENT_TRIGGER_NONE,
-	IGNORE_EVENT_TRIGGER_ALL
+	IGNORE_EVENT_TRIGGER_ALL,
+	IGNORE_EVENT_TRIGGER_LOGIN
 } IgnoreEventTriggersEvents;
 
 extern int ignore_event_trigger;
@@ -62,6 +63,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnLogin(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 2b1163ce33..e7b0f83c19 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
+PG_CMDTAG(CMDTAG_LOGIN, "LOGIN", true, false, false)
 PG_CMDTAG(CMDTAG_MERGE, "MERGE", false, false, true)
 PG_CMDTAG(CMDTAG_MOVE, "MOVE", false, false, true)
 PG_CMDTAG(CMDTAG_NOTIFY, "NOTIFY", false, false, false)
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index ddb67a68fa..a66344aacb 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Login,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 583ee87da8..c3dbd75af3 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,24 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE TABLE user_logins(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO user_logins (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 my $primary_lsn = $node_primary->lsn('write');
 $node_primary->wait_for_catchup($node_standby_1, 'replay', $primary_lsn);
@@ -387,6 +405,11 @@ sub replay_check
 
 replay_check();
 
+my $evttrig = $node_standby_1->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+$evttrig = $node_standby_2->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 44d545de25..b170e392a0 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -604,3 +604,48 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     1
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     2
+(1 row)
+
+-- Check dathasloginevt in system catalog
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+ dathasloginevt 
+----------------
+ t
+(1 row)
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
+\c
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+ dathasloginevt 
+----------------
+ f
+(1 row)
+
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 1446cf8cc8..4e878dd2d6 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -462,3 +462,29 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+SELECT COUNT(*) FROM user_logins;
+\c
+SELECT COUNT(*) FROM user_logins;
+
+-- Check dathasloginevt in system catalog
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
+\c
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
-- 
2.24.3 (Apple Git-128)

v30-0001-Provide-a-GUC-for-temporarily-ignoring-event-tri.patchapplication/octet-stream; name=v30-0001-Provide-a-GUC-for-temporarily-ignoring-event-tri.patch; x-unix-mode=0644Download
From 562c0037d6929c14093254ee859c2630b19aac5d Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <daniel@yesql.se>
Date: Mon, 14 Mar 2022 14:24:09 +0100
Subject: [PATCH v30 1/2] Provide a GUC for temporarily ignoring event triggers

In order to allow troubleshooting and repair without the need for
restarting in single-user mode, allow to ignore event triggers per
session.
---
 doc/src/sgml/config.sgml                   | 15 ++++++++
 doc/src/sgml/ref/create_event_trigger.sgml |  4 ++-
 src/backend/commands/event_trigger.c       | 40 ++++++++++++++++++----
 src/backend/utils/misc/guc.c               | 17 +++++++++
 src/include/commands/event_trigger.h       |  8 +++++
 5 files changed, 76 insertions(+), 8 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 9788e831bc..84c08035fb 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9224,6 +9224,21 @@ SET XML OPTION { DOCUMENT | CONTENT };
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-ignore-event-trigger" xreflabel="ignore_event_trigger">
+      <term><varname>ignore_event_trigger</varname> (<type>enum</type>)
+      <indexterm>
+       <primary><varname>ignore_event_trigger</varname></primary>
+       <secondary>configuration parameter</secondary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Allows to temporarily disable event triggers from executing in order
+        to troubleshoot and repair faulty event triggers.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
     </sect2>
      <sect2 id="runtime-config-client-format">
diff --git a/doc/src/sgml/ref/create_event_trigger.sgml b/doc/src/sgml/ref/create_event_trigger.sgml
index 22c8119198..1a78555c64 100644
--- a/doc/src/sgml/ref/create_event_trigger.sgml
+++ b/doc/src/sgml/ref/create_event_trigger.sgml
@@ -123,7 +123,9 @@ CREATE EVENT TRIGGER <replaceable class="parameter">name</replaceable>
    Event triggers are disabled in single-user mode (see <xref
    linkend="app-postgres"/>).  If an erroneous event trigger disables the
    database so much that you can't even drop the trigger, restart in
-   single-user mode and you'll be able to do that.
+   single-user mode and you'll be able to do that. Even triggers can also be
+   temporarily disabled for such troubleshooting, see
+   <xref linkend="gux-ignore-event-trigger"/>.
   </para>
  </refsect1>
 
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 3c3fc2515b..70995cfc49 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -72,6 +72,9 @@ typedef struct EventTriggerQueryState
 
 static EventTriggerQueryState *currentEventTriggerState = NULL;
 
+/* GUC parameter */
+int		ignore_event_trigger = IGNORE_EVENT_TRIGGER_NONE;
+
 /* Support for dropped objects */
 typedef struct SQLDropObject
 {
@@ -100,6 +103,7 @@ static void validate_table_rewrite_tags(const char *filtervar, List *taglist);
 static void EventTriggerInvoke(List *fn_oid_list, EventTriggerData *trigdata);
 static const char *stringify_grant_objtype(ObjectType objtype);
 static const char *stringify_adefprivs_objtype(ObjectType objtype);
+static bool ignore_event_trigger_check(EventTriggerEvent event);
 
 /*
  * Create an event trigger.
@@ -658,8 +662,11 @@ EventTriggerDDLCommandStart(Node *parsetree)
 	 * wherein event triggers are disabled.  (Or we could implement
 	 * heapscan-and-sort logic for that case, but having disaster recovery
 	 * scenarios depend on code that's otherwise untested isn't appetizing.)
+	 *
+	 * Additionally, event triggers can be disabled with a superuser only GUC
+	 * to make fixing database easier as per 1 above.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_DDLCommandStart))
 		return;
 
 	runlist = EventTriggerCommonSetup(parsetree,
@@ -693,9 +700,9 @@ EventTriggerDDLCommandEnd(Node *parsetree)
 
 	/*
 	 * See EventTriggerDDLCommandStart for a discussion about why event
-	 * triggers are disabled in single user mode.
+	 * triggers are disabled in single user mode or via GUC.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_DDLCommandEnd))
 		return;
 
 	/*
@@ -741,9 +748,9 @@ EventTriggerSQLDrop(Node *parsetree)
 
 	/*
 	 * See EventTriggerDDLCommandStart for a discussion about why event
-	 * triggers are disabled in single user mode.
+	 * triggers are disabled in single user mode or via a GUC.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_SQLDrop))
 		return;
 
 	/*
@@ -812,9 +819,9 @@ EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason)
 
 	/*
 	 * See EventTriggerDDLCommandStart for a discussion about why event
-	 * triggers are disabled in single user mode.
+	 * triggers are disabled in single user mode or via a GUC.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_TableRewrite))
 		return;
 
 	/*
@@ -2175,3 +2182,22 @@ stringify_adefprivs_objtype(ObjectType objtype)
 
 	return "???";				/* keep compiler quiet */
 }
+
+
+/*
+ * Checks whether the specified event is ignored by the ignore_event_trigger
+ * GUC or not. Currently, the GUC only supports ignoreing all or nothing but
+ * that will most likely change so the function takes an event to aid that.
+ */
+static bool
+ignore_event_trigger_check(EventTriggerEvent event)
+{
+	(void) event;			/* unused parameter */
+
+	if (ignore_event_trigger == IGNORE_EVENT_TRIGGER_ALL)
+		return true;
+
+	/* IGNORE_EVENT_TRIGGER_NONE */
+	return false;
+
+}
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index eb3a03b976..1c5b277fb0 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -47,6 +47,7 @@
 #include "catalog/pg_authid.h"
 #include "catalog/storage.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/prepare.h"
 #include "commands/tablespace.h"
 #include "commands/trigger.h"
@@ -566,6 +567,12 @@ static const struct config_enum_entry wal_compression_options[] = {
 	{NULL, 0, false}
 };
 
+static const struct config_enum_entry ignore_event_trigger_options[] = {
+	{"none", IGNORE_EVENT_TRIGGER_NONE, false},
+	{"all", IGNORE_EVENT_TRIGGER_ALL, false},
+	{NULL, 0, false}
+};
+
 /*
  * Options for enum values stored in other modules
  */
@@ -4917,6 +4924,16 @@ static struct config_enum ConfigureNamesEnum[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"ignore_event_trigger", PGC_SUSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Disable event triggers during the session."),
+			NULL
+		},
+		&ignore_event_trigger,
+		IGNORE_EVENT_TRIGGER_NONE, ignore_event_trigger_options,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"wal_level", PGC_POSTMASTER, WAL_SETTINGS,
 			gettext_noop("Sets the level of information written to the WAL."),
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 10091c3aaf..9e7671e63f 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -29,6 +29,14 @@ typedef struct EventTriggerData
 	CommandTag	tag;
 } EventTriggerData;
 
+typedef enum ignore_event_trigger_events
+{
+	IGNORE_EVENT_TRIGGER_NONE,
+	IGNORE_EVENT_TRIGGER_ALL
+} IgnoreEventTriggersEvents;
+
+extern int ignore_event_trigger;
+
 #define AT_REWRITE_ALTER_PERSISTENCE	0x01
 #define AT_REWRITE_DEFAULT_VAL			0x02
 #define AT_REWRITE_COLUMN_REWRITE		0x04
-- 
2.24.3 (Apple Git-128)

#114Daniel Gustafsson
daniel@yesql.se
In reply to: Tom Lane (#111)
Re: On login trigger: take three

On 29 Mar 2022, at 00:40, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Andres Freund <andres@anarazel.de> writes:

On 2022-03-28 23:27:56 +0200, Daniel Gustafsson wrote:

Do you think this potential foot-gun is scary enough to reject this patch?
There are lots of creative ways to cause Nagios alerts from ones database, but
this has the potential to do so with a small bug in userland code. Still, I
kind of like the feature so I'm indecisive.

It does seem like a huge footgun. But also kinda useful. So I'm really +-0.

An on-login trigger is *necessarily* a foot-gun; I don't see that this
particular failure mode makes it any worse than it would be anyway.

Agreed.

There has to be some not-too-difficult-to-use way to bypass a broken
login trigger. Assuming we are happy with the design for doing that,
might as well accept the hazards.

The GUC in this patchset seems to be in line with what most in this thread have
preferred, and with that in place (and single-user mode which still works for
this) I think we have that covered.

--
Daniel Gustafsson https://vmware.com/

#115Noname
a.sokolov@postgrespro.ru
In reply to: Daniel Gustafsson (#113)
Re: On login trigger: take three

Daniel Gustafsson писал 2022-03-30 16:48:

On 30 Mar 2022, at 13:21, Ivan Panchenko <wao@mail.ru> wrote:
Maybe side-effects is a bit too general? Emitting a log message,
rejecting a
login, setting some GUCs, etc are all side-effects too.
Something like this:

I've reworded the docs close to what you suggested here.

Also, please fix a typo in doc/src/sgml/ref/create_event_trigger.sgml
:

Done.

Regarding the trigger function example:
It does not do anything if run on a standby. To show that it can do
something on a standby to, I propose to move throwing the night
exception to the beginning.

Good idea, done.

Finally, let me propose to append to the regression test the
following:

Also a good idea, done.

--
Daniel Gustafsson https://vmware.com/

Please fix a typo in doc/src/sgml/event-trigger.sgml: "precvent"

--
Andrey Sokolov
Postgres Professional: http://www.postgrespro.com

#116Daniel Gustafsson
daniel@yesql.se
In reply to: Noname (#115)
Re: On login trigger: take three

On 1 Apr 2022, at 09:16, a.sokolov@postgrespro.ru wrote:

Please fix a typo in doc/src/sgml/event-trigger.sgml: "precvent"

Will do. With that fixed I think this is ready and unless I find something on
another read through and test pass I hope to be able to push this before the CF
closes today.

--
Daniel Gustafsson https://vmware.com/

#117Daniel Gustafsson
daniel@yesql.se
In reply to: Daniel Gustafsson (#116)
2 attachment(s)
Re: On login trigger: take three

This had bitrotted a fair bit, attached is a rebase along with (mostly)
documentation fixes. 0001 adds a generic GUC for ignoring event triggers and
0002 adds the login event with event trigger support, and hooks it up to the
GUC such that broken triggers wont require single-user mode. Moving the CF
entry back to Needs Review.

--
Daniel Gustafsson https://vmware.com/

Attachments:

v31-0002-Add-support-event-triggers-on-authenticated-logi.patchapplication/octet-stream; name=v31-0002-Add-support-event-triggers-on-authenticated-logi.patch; x-unix-mode=0644Download
From c146e4e0c4a12a595238e64522d3bf8267ba7756 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Fri, 2 Sep 2022 17:29:55 +0200
Subject: [PATCH v31 2/2] Add support event triggers on authenticated login

---
 doc/src/sgml/bki.sgml                         |   2 +-
 doc/src/sgml/catalogs.sgml                    |  13 ++
 doc/src/sgml/ecpg.sgml                        |   2 +
 doc/src/sgml/event-trigger.sgml               |  88 ++++++++++
 src/backend/commands/dbcommands.c             |   1 +
 src/backend/commands/event_trigger.c          | 156 +++++++++++++++++-
 src/backend/tcop/postgres.c                   |   4 +
 src/backend/utils/cache/evtcache.c            |   2 +
 src/backend/utils/misc/guc.c                  |   1 +
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/bin/pg_dump/pg_dump.c                     |   5 +
 src/bin/psql/tab-complete.c                   |   4 +-
 src/include/catalog/pg_database.dat           |   2 +-
 src/include/catalog/pg_database.h             |   3 +
 src/include/commands/event_trigger.h          |   4 +-
 src/include/tcop/cmdtaglist.h                 |   1 +
 src/include/utils/evtcache.h                  |   3 +-
 src/test/recovery/t/001_stream_rep.pl         |  23 +++
 src/test/regress/expected/event_trigger.out   |  45 +++++
 src/test/regress/sql/event_trigger.sql        |  26 +++
 20 files changed, 375 insertions(+), 11 deletions(-)

diff --git a/doc/src/sgml/bki.sgml b/doc/src/sgml/bki.sgml
index f71644e398..315ba81951 100644
--- a/doc/src/sgml/bki.sgml
+++ b/doc/src/sgml/bki.sgml
@@ -184,7 +184,7 @@
   descr => 'database\'s default template',
   datname => 'template1', encoding => 'ENCODING',
   datlocprovider => 'LOCALE_PROVIDER', datistemplate => 't',
-  datallowconn => 't', datconnlimit => '-1', datfrozenxid => '0',
+  datallowconn => 't', dathasloginevt => 'f', datconnlimit => '-1', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datcollate => 'LC_COLLATE',
   datctype => 'LC_CTYPE', daticulocale => 'ICU_LOCALE', datacl => '_null_' },
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 00f833d210..9d0ef1cbb2 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -3016,6 +3016,19 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>dathasloginevt</structfield> <type>bool</type>
+      </para>
+      <para>
+        Indicates that there are login event triggers defined for this database.
+        This flag is used to avoid extra lookups on the
+        <structname>pg_event_trigger</structname> table during each backend
+        startup.  This flag is used internally by <productname>PostgreSQL</productname>
+        and should not be manually altered or read for monitoring purposes.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>datconnlimit</structfield> <type>int4</type>
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index 16853ced6f..8afa2ff05c 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -4769,6 +4769,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = t (type: 1)
 datallowconn = t (type: 1)
+dathasloginevt = f (type: 1)
 datconnlimit = -1 (type: 5)
 datfrozenxid = 379 (type: 1)
 dattablespace = 1663 (type: 1)
@@ -4793,6 +4794,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = f (type: 1)
 datallowconn = t (type: 1)
+dathasloginevt = f (type: 1)
 datconnlimit = -1 (type: 5)
 datfrozenxid = 379 (type: 1)
 dattablespace = 1663 (type: 1)
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index f1235a2c9f..a24f2e7cfa 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>login</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,20 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>login</literal> event occurs when an authenticated user logs
+     in to the system. Any bugs in a trigger procedure for this event may
+     prevent successful login to the system. Such bugs may be fixed after first
+     reconnecting to the system with event triggers disabled (see
+     <xref linkend="guc-ignore-event-trigger"/>) or by restarting the system
+     in single-user mode (as event triggers are disabled in this mode). See the
+     <xref linkend="app-postgres"/> reference page for details about using
+     single-user mode.  The <literal>login</literal> event will also fire on
+     standby servers.  To prevent servers from becoming inaccessible, such
+     triggers must avoid writing anything to the database when running on a
+     standby.
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1296,6 +1311,79 @@ $$;
 CREATE EVENT TRIGGER no_rewrite_allowed
                   ON table_rewrite
    EXECUTE FUNCTION no_rewrite();
+</programlisting>
+   </para>
+ </sect1>
+
+  <sect1 id="event-trigger-database-login-example">
+   <title>A Database Login Event Trigger Example</title>
+
+   <para>
+    The event trigger on the <literal>login</literal> event can be
+    useful for logging user logins, for verifying the connection and
+    assigning roles according to current circumstances, or for session
+    data initialization. It is very important that any event trigger using the
+    <literal>login</literal> event checks whether or not the database is in
+    recovery before performing any writes. Writing to a standby server will
+    make it inaccessible.
+   </para>
+
+   <para>
+    The following example demonstrates these options.
+<programlisting>
+-- create test tables and roles
+CREATE TABLE user_login_log (
+  "user" text,
+  "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+  RETURNS event_trigger SECURITY DEFINER
+  LANGUAGE plpgsql AS
+$$
+DECLARE
+  hour integer = EXTRACT('hour' FROM current_time);
+  rec boolean;
+BEGIN
+-- 1. Forbid logging in between 2AM and 4AM.
+IF hour BETWEEN 2 AND 4 THEN
+  RAISE EXCEPTION 'Login forbidden';
+END IF;
+
+-- The checks below cannot be performed on standby servers so
+-- ensure the database is not in recovery before we perform any
+-- operations.
+SELECT pg_is_in_recovery() INTO rec;
+IF rec THEN
+  RETURN;
+END IF
+
+-- 2. Assign some roles. At daytime, grant the day_worker role, else the
+-- night_worker role.
+IF hour BETWEEN 8 AND 20 THEN
+  EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT day_worker TO ' || quote_ident(session_user);
+ELSE
+  EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT night_worker TO ' || quote_ident(session_user);
+END IF;
+
+-- 3. Initialize user session data
+CREATE TEMP TABLE session_storage (x float, y integer);
+
+-- 4. Log the connection time
+INSERT INTO user_login_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+  ON login
+  EXECUTE FUNCTION init_session();
 </programlisting>
    </para>
  </sect1>
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 6ff48bb18f..4e541d631e 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -1296,6 +1296,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	new_record[Anum_pg_database_datlocprovider - 1] = CharGetDatum(dblocprovider);
 	new_record[Anum_pg_database_datistemplate - 1] = BoolGetDatum(dbistemplate);
 	new_record[Anum_pg_database_datallowconn - 1] = BoolGetDatum(dballowconnections);
+	new_record[Anum_pg_database_dathasloginevt - 1] = BoolGetDatum(false);
 	new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 	new_record[Anum_pg_database_datfrozenxid - 1] = TransactionIdGetDatum(src_frozenxid);
 	new_record[Anum_pg_database_datminmxid - 1] = TransactionIdGetDatum(src_minmxid);
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 85d01c559e..e23773d0e1 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -37,15 +38,18 @@
 #include "miscadmin.h"
 #include "parser/parse_func.h"
 #include "pgstat.h"
+#include "storage/lmgr.h"
 #include "tcop/deparse_utility.h"
 #include "tcop/utility.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 typedef struct EventTriggerQueryState
@@ -134,6 +138,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "login") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -297,6 +302,30 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
 	CatalogTupleInsert(tgrel, tuple);
 	heap_freetuple(tuple);
 
+	/*
+	 * Login event triggers have an additional flag in pg_database to allow
+	 * faster lookups in hot codepaths. Set the flag unless already True.
+	 */
+	if (strcmp(eventname, "login") == 0)
+	{
+		Form_pg_database db;
+		Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+
+		/* Set dathasloginevt flag in pg_database */
+		tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+		db = (Form_pg_database) GETSTRUCT(tuple);
+		if (!db->dathasloginevt)
+		{
+			db->dathasloginevt = true;
+			CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+			CommandCounterIncrement();
+		}
+		table_close(pg_db, RowExclusiveLock);
+		heap_freetuple(tuple);
+	}
+
 	/* Depend on owner. */
 	recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
 
@@ -583,10 +612,15 @@ EventTriggerCommonSetup(Node *parsetree,
 	{
 		CommandTag	dbgtag;
 
-		dbgtag = CreateCommandTag(parsetree);
+		if (event == EVT_Login)
+			dbgtag = CMDTAG_LOGIN;
+		else
+			dbgtag = CreateCommandTag(parsetree);
+
 		if (event == EVT_DDLCommandStart ||
 			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
+			event == EVT_SQLDrop ||
+			event == EVT_Login)
 		{
 			if (!command_tag_event_trigger_ok(dbgtag))
 				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
@@ -605,7 +639,10 @@ EventTriggerCommonSetup(Node *parsetree,
 		return NIL;
 
 	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
+	if (event == EVT_Login)
+		tag = CMDTAG_LOGIN;
+	else
+		tag = CreateCommandTag(parsetree);
 
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
@@ -806,6 +843,112 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Return true if this database has login event triggers, false otherwise.
+ */
+static bool
+DatabaseHasLoginEventTriggers(void)
+{
+	bool		has_login_event_triggers;
+	HeapTuple	tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+	has_login_event_triggers = ((Form_pg_database) GETSTRUCT(tuple))->dathasloginevt;
+	ReleaseSysCache(tuple);
+	return has_login_event_triggers;
+}
+
+/*
+ * Fire login event triggers if any are present.  The dathasloginevt
+ * pg_database flag is left when an event trigger is dropped, to avoid
+ * complicating the codepath in the case of multiple event triggers.  This
+ * function will instead unset the flag if no trigger is defined.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId)
+		|| ignore_event_trigger_check(EVT_Login))
+		return;
+
+	StartTransactionCommand();
+
+	if (DatabaseHasLoginEventTriggers())
+	{
+		runlist = EventTriggerCommonSetup(NULL,
+										  EVT_Login, "login",
+										  &trigdata);
+
+		if (runlist != NIL)
+		{
+			/*
+			 * Event trigger execution may require an active snapshot.
+			 */
+			PushActiveSnapshot(GetTransactionSnapshot());
+
+			/* Run the triggers. */
+			EventTriggerInvoke(runlist, &trigdata);
+
+			/* Cleanup. */
+			list_free(runlist);
+
+			PopActiveSnapshot();
+		}
+		else
+		{
+			/*
+			 * Runlist is empty: clear dathasloginevt flag
+			 */
+			Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+			HeapTuple	tuple;
+			Form_pg_database db;
+
+			LockSharedObject(DatabaseRelationId, MyDatabaseId, 0, AccessExclusiveLock);
+
+			tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+			db = (Form_pg_database) GETSTRUCT(tuple);
+			if (db->dathasloginevt)
+			{
+				/*
+				 * There can be a race condition: a login event trigger may
+				 * have been added after the pg_event_trigger table was
+				 * scanned, and we don't want to erroneously clear the
+				 * dathasloginevt flag in this case. To be sure that this
+				 * hasn't happened, repeat the scan under the pg_database
+				 * table lock.
+				 */
+				runlist = EventTriggerCommonSetup(NULL,
+												  EVT_Login, "login",
+												  &trigdata);
+				if (runlist == NIL)	/* list is still empty, so clear the
+										 * flag */
+				{
+					db->dathasloginevt = false;
+					CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				}
+				else
+					list_free(runlist);
+			}
+			table_close(pg_db, RowExclusiveLock);
+			heap_freetuple(tuple);
+		}
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
@@ -2186,8 +2329,7 @@ stringify_adefprivs_objtype(ObjectType objtype)
 
 /*
  * Checks whether the specified event is ignored by the ignore_event_trigger
- * GUC or not. Currently, the GUC only supports ignoreing all or nothing but
- * that will most likely change so the function takes an event to aid that.
+ * GUC or not.
  */
 static bool
 ignore_event_trigger_check(EventTriggerEvent event)
@@ -2197,6 +2339,10 @@ ignore_event_trigger_check(EventTriggerEvent event)
 	if (ignore_event_trigger == IGNORE_EVENT_TRIGGER_ALL)
 		return true;
 
+	if (ignore_event_trigger == IGNORE_EVENT_TRIGGER_LOGIN
+		&& event == EVT_Login)
+		return true;
+
 	/* IGNORE_EVENT_TRIGGER_NONE */
 	return false;
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 7bec4e4ff5..51c1fe8f87 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -33,6 +33,7 @@
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/prepare.h"
 #include "common/pg_prng.h"
 #include "jit/jit.h"
@@ -4151,6 +4152,9 @@ PostgresMain(const char *dbname, const char *username)
 	initStringInfo(&row_description_buf);
 	MemoryContextSwitchTo(TopMemoryContext);
 
+	/* Fire any defined login event triggers, if appropriate */
+	EventTriggerOnLogin();
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index f7f7165f7f..3e9388e956 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "login") == 0)
+			event = EVT_Login;
 		else
 			continue;
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index cb688f6789..402c43f803 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -600,6 +600,7 @@ static const struct config_enum_entry wal_compression_options[] = {
 static const struct config_enum_entry ignore_event_trigger_options[] = {
 	{"none", IGNORE_EVENT_TRIGGER_NONE, false},
 	{"all", IGNORE_EVENT_TRIGGER_ALL, false},
+	{"login", IGNORE_EVENT_TRIGGER_LOGIN, false},
 	{NULL, 0, false}
 };
 
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 90bec0502c..b8bdf29d7f 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -679,6 +679,7 @@
 					#   error
 #search_path = '"$user", public'	# schema names
 #row_security = on
+#ignore_event_trigger = 'none'
 #default_table_access_method = 'heap'
 #default_tablespace = ''		# a tablespace name, '' uses the default
 #default_toast_compression = 'pglz'	# 'pglz' or 'lz4'
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index d25709ad5f..b1f1ff85f9 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3104,6 +3104,11 @@ dumpDatabase(Archive *fout)
 		appendPQExpBufferStr(delQry, ";\n");
 	}
 
+	/*
+	 * We do not restore pg_database.dathasloginevt because it is set
+	 * automatically on login event trigger creation.
+	 */
+
 	/* Add database-specific SET options */
 	dumpDatabaseConfig(fout, creaQry, datname, dbCatId.oid);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 62a39779b9..e5f1ffd230 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3409,8 +3409,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop",
-					  "table_rewrite");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "login",
+					  "sql_drop", "table_rewrite");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat
index 47dcbfb343..df32761f74 100644
--- a/src/include/catalog/pg_database.dat
+++ b/src/include/catalog/pg_database.dat
@@ -16,7 +16,7 @@
   descr => 'default template for new databases',
   datname => 'template1', encoding => 'ENCODING',
   datlocprovider => 'LOCALE_PROVIDER', datistemplate => 't',
-  datallowconn => 't', datconnlimit => '-1', datfrozenxid => '0',
+  datallowconn => 't', dathasloginevt => 'f', datconnlimit => '-1', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datcollate => 'LC_COLLATE',
   datctype => 'LC_CTYPE', daticulocale => 'ICU_LOCALE', datacl => '_null_' },
 
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index 611c95656a..ebf7151109 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -49,6 +49,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
 	/* new connections allowed? */
 	bool		datallowconn;
 
+	/* database has login event triggers? */
+	bool		dathasloginevt;
+
 	/* max connections allowed (-1=no limit) */
 	int32		datconnlimit;
 
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 9e7671e63f..2400cc7d7a 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -32,7 +32,8 @@ typedef struct EventTriggerData
 typedef enum ignore_event_trigger_events
 {
 	IGNORE_EVENT_TRIGGER_NONE,
-	IGNORE_EVENT_TRIGGER_ALL
+	IGNORE_EVENT_TRIGGER_ALL,
+	IGNORE_EVENT_TRIGGER_LOGIN
 } IgnoreEventTriggersEvents;
 
 extern int ignore_event_trigger;
@@ -62,6 +63,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnLogin(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 9e94f44c5f..b900227e0c 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
+PG_CMDTAG(CMDTAG_LOGIN, "LOGIN", true, false, false)
 PG_CMDTAG(CMDTAG_MERGE, "MERGE", false, false, true)
 PG_CMDTAG(CMDTAG_MOVE, "MOVE", false, false, true)
 PG_CMDTAG(CMDTAG_NOTIFY, "NOTIFY", false, false, false)
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index ddb67a68fa..a66344aacb 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Login,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 986147b730..8ad50762c4 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,24 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE TABLE user_logins(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO user_logins (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 my $primary_lsn = $node_primary->lsn('write');
 $node_primary->wait_for_catchup($node_standby_1, 'replay', $primary_lsn);
@@ -388,6 +406,11 @@ sub replay_check
 
 replay_check();
 
+my $evttrig = $node_standby_1->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+$evttrig = $node_standby_2->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 5a10958df5..cb16f47c35 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -614,3 +614,48 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     1
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     2
+(1 row)
+
+-- Check dathasloginevt in system catalog
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+ dathasloginevt 
+----------------
+ t
+(1 row)
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
+\c
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+ dathasloginevt 
+----------------
+ f
+(1 row)
+
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 1aeaddbe71..9d5bb328b0 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -471,3 +471,29 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+SELECT COUNT(*) FROM user_logins;
+\c
+SELECT COUNT(*) FROM user_logins;
+
+-- Check dathasloginevt in system catalog
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
+\c
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
-- 
2.32.1 (Apple Git-133)

v31-0001-Provide-a-GUC-for-temporarily-ignoring-event-tri.patchapplication/octet-stream; name=v31-0001-Provide-a-GUC-for-temporarily-ignoring-event-tri.patch; x-unix-mode=0644Download
From fdf34f4485f1c1944ce77351f72124cb83cf06d6 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Fri, 2 Sep 2022 14:34:10 +0200
Subject: [PATCH v31 1/2] Provide a GUC for temporarily ignoring event triggers

In order to allow troubleshooting and repair without the need for
restarting in single-user mode, allow to ignore event triggers per
session.
---
 doc/src/sgml/config.sgml                   | 15 ++++++++
 doc/src/sgml/ref/create_event_trigger.sgml |  4 ++-
 src/backend/commands/event_trigger.c       | 40 ++++++++++++++++++----
 src/backend/utils/misc/guc.c               | 17 +++++++++
 src/include/commands/event_trigger.h       |  8 +++++
 5 files changed, 76 insertions(+), 8 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index a5cd4e44c7..58818d8e66 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9383,6 +9383,21 @@ SET XML OPTION { DOCUMENT | CONTENT };
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-ignore-event-trigger" xreflabel="ignore_event_trigger">
+      <term><varname>ignore_event_trigger</varname> (<type>enum</type>)
+      <indexterm>
+       <primary><varname>ignore_event_trigger</varname></primary>
+       <secondary>configuration parameter</secondary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Allows to temporarily disable event triggers from executing in order
+        to troubleshoot and repair faulty event triggers.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
     </sect2>
      <sect2 id="runtime-config-client-format">
diff --git a/doc/src/sgml/ref/create_event_trigger.sgml b/doc/src/sgml/ref/create_event_trigger.sgml
index 22c8119198..f6922c3de3 100644
--- a/doc/src/sgml/ref/create_event_trigger.sgml
+++ b/doc/src/sgml/ref/create_event_trigger.sgml
@@ -123,7 +123,9 @@ CREATE EVENT TRIGGER <replaceable class="parameter">name</replaceable>
    Event triggers are disabled in single-user mode (see <xref
    linkend="app-postgres"/>).  If an erroneous event trigger disables the
    database so much that you can't even drop the trigger, restart in
-   single-user mode and you'll be able to do that.
+   single-user mode and you'll be able to do that. Event triggers can also be
+   temporarily disabled for such troubleshooting, see
+   <xref linkend="guc-ignore-event-trigger"/>.
   </para>
  </refsect1>
 
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 635d05405e..85d01c559e 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -72,6 +72,9 @@ typedef struct EventTriggerQueryState
 
 static EventTriggerQueryState *currentEventTriggerState = NULL;
 
+/* GUC parameter */
+int		ignore_event_trigger = IGNORE_EVENT_TRIGGER_NONE;
+
 /* Support for dropped objects */
 typedef struct SQLDropObject
 {
@@ -100,6 +103,7 @@ static void validate_table_rewrite_tags(const char *filtervar, List *taglist);
 static void EventTriggerInvoke(List *fn_oid_list, EventTriggerData *trigdata);
 static const char *stringify_grant_objtype(ObjectType objtype);
 static const char *stringify_adefprivs_objtype(ObjectType objtype);
+static bool ignore_event_trigger_check(EventTriggerEvent event);
 
 /*
  * Create an event trigger.
@@ -657,8 +661,11 @@ EventTriggerDDLCommandStart(Node *parsetree)
 	 * wherein event triggers are disabled.  (Or we could implement
 	 * heapscan-and-sort logic for that case, but having disaster recovery
 	 * scenarios depend on code that's otherwise untested isn't appetizing.)
+	 *
+	 * Additionally, event triggers can be disabled with a superuser only GUC
+	 * to make fixing database easier as per 1 above.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_DDLCommandStart))
 		return;
 
 	runlist = EventTriggerCommonSetup(parsetree,
@@ -692,9 +699,9 @@ EventTriggerDDLCommandEnd(Node *parsetree)
 
 	/*
 	 * See EventTriggerDDLCommandStart for a discussion about why event
-	 * triggers are disabled in single user mode.
+	 * triggers are disabled in single user mode or via GUC.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_DDLCommandEnd))
 		return;
 
 	/*
@@ -740,9 +747,9 @@ EventTriggerSQLDrop(Node *parsetree)
 
 	/*
 	 * See EventTriggerDDLCommandStart for a discussion about why event
-	 * triggers are disabled in single user mode.
+	 * triggers are disabled in single user mode or via a GUC.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_SQLDrop))
 		return;
 
 	/*
@@ -811,9 +818,9 @@ EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason)
 
 	/*
 	 * See EventTriggerDDLCommandStart for a discussion about why event
-	 * triggers are disabled in single user mode.
+	 * triggers are disabled in single user mode or via a GUC.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_TableRewrite))
 		return;
 
 	/*
@@ -2175,3 +2182,22 @@ stringify_adefprivs_objtype(ObjectType objtype)
 
 	return "???";				/* keep compiler quiet */
 }
+
+
+/*
+ * Checks whether the specified event is ignored by the ignore_event_trigger
+ * GUC or not. Currently, the GUC only supports ignoreing all or nothing but
+ * that will most likely change so the function takes an event to aid that.
+ */
+static bool
+ignore_event_trigger_check(EventTriggerEvent event)
+{
+	(void) event;			/* unused parameter */
+
+	if (ignore_event_trigger == IGNORE_EVENT_TRIGGER_ALL)
+		return true;
+
+	/* IGNORE_EVENT_TRIGGER_NONE */
+	return false;
+
+}
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 9fbbfb1be5..cb688f6789 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -49,6 +49,7 @@
 #include "catalog/pg_parameter_acl.h"
 #include "catalog/storage.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/prepare.h"
 #include "commands/tablespace.h"
 #include "commands/trigger.h"
@@ -596,6 +597,12 @@ static const struct config_enum_entry wal_compression_options[] = {
 	{NULL, 0, false}
 };
 
+static const struct config_enum_entry ignore_event_trigger_options[] = {
+	{"none", IGNORE_EVENT_TRIGGER_NONE, false},
+	{"all", IGNORE_EVENT_TRIGGER_ALL, false},
+	{NULL, 0, false}
+};
+
 /*
  * Options for enum values stored in other modules
  */
@@ -4969,6 +4976,16 @@ static struct config_enum ConfigureNamesEnum[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"ignore_event_trigger", PGC_SUSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Disable event triggers during the session."),
+			NULL
+		},
+		&ignore_event_trigger,
+		IGNORE_EVENT_TRIGGER_NONE, ignore_event_trigger_options,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"wal_level", PGC_POSTMASTER, WAL_SETTINGS,
 			gettext_noop("Sets the level of information written to the WAL."),
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 10091c3aaf..9e7671e63f 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -29,6 +29,14 @@ typedef struct EventTriggerData
 	CommandTag	tag;
 } EventTriggerData;
 
+typedef enum ignore_event_trigger_events
+{
+	IGNORE_EVENT_TRIGGER_NONE,
+	IGNORE_EVENT_TRIGGER_ALL
+} IgnoreEventTriggersEvents;
+
+extern int ignore_event_trigger;
+
 #define AT_REWRITE_ALTER_PERSISTENCE	0x01
 #define AT_REWRITE_DEFAULT_VAL			0x02
 #define AT_REWRITE_COLUMN_REWRITE		0x04
-- 
2.32.1 (Apple Git-133)

#118Zhihong Yu
zyu@yugabyte.com
In reply to: Daniel Gustafsson (#117)
Re: On login trigger: take three

On Fri, Sep 2, 2022 at 8:37 AM Daniel Gustafsson <daniel@yesql.se> wrote:

This had bitrotted a fair bit, attached is a rebase along with (mostly)
documentation fixes. 0001 adds a generic GUC for ignoring event triggers
and
0002 adds the login event with event trigger support, and hooks it up to
the
GUC such that broken triggers wont require single-user mode. Moving the CF
entry back to Needs Review.

--
Daniel Gustafsson https://vmware.com/

Hi,

For EventTriggerOnLogin():

+           LockSharedObject(DatabaseRelationId, MyDatabaseId, 0,
AccessExclusiveLock);
+
+           tuple = SearchSysCacheCopy1(DATABASEOID,
ObjectIdGetDatum(MyDatabaseId));
+
+           if (!HeapTupleIsValid(tuple))
+               elog(ERROR, "cache lookup failed for database %u",
MyDatabaseId);

Should the lock be taken after the check (HeapTupleIsValid) is done ?

Cheers

#119Sergey Shinderuk
s.shinderuk@postgrespro.ru
In reply to: Daniel Gustafsson (#117)
Re: On login trigger: take three

On 02.09.2022 18:36, Daniel Gustafsson wrote:

This had bitrotted a fair bit, attached is a rebase along with (mostly)
documentation fixes. 0001 adds a generic GUC for ignoring event triggers and
0002 adds the login event with event trigger support, and hooks it up to the
GUC such that broken triggers wont require single-user mode. Moving the CF
entry back to Needs Review.

Hello!

There is a race around setting and clearing of dathasloginevt.

Steps to reproduce:

1. Create a trigger:

CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
BEGIN
RAISE NOTICE 'You are welcome!';
END;
$$ LANGUAGE plpgsql;

CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE
on_login_proc();

2. Then drop it, but don't start new sessions:

DROP EVENT TRIGGER on_login_trigger;

3. Create another trigger, but don't commit yet:

BEGIN;
CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE
on_login_proc();

4. Start a new session. This clears dathasloginevt.

5. Commit the transaction:

COMMIT;

Now we have a trigger, but it doesn't fire, because dathasloginevt=false.

If two sessions create triggers concurrently, one of them will fail.

Steps:

1. In the first session, start a transaction and create a trigger:

BEGIN;
CREATE EVENT TRIGGER on_login_trigger1 ON login EXECUTE PROCEDURE
on_login_proc();

2. In the second session, create another trigger (this query blocks):

CREATE EVENT TRIGGER on_login_trigger2 ON login EXECUTE PROCEDURE
on_login_proc();

3. Commit in the first session:

COMMIT;

The second session fails:

postgres=# CREATE EVENT TRIGGER on_login_trigger2 ON login EXECUTE
PROCEDURE on_login_proc();
ERROR: tuple concurrently updated

What else bothers me is that login triggers execute in an environment
under user control and one has to be very careful. The example trigger
from the documentation

+DECLARE

+ hour integer = EXTRACT('hour' FROM current_time);

+ rec boolean;

+BEGIN

+-- 1. Forbid logging in between 2AM and 4AM.

+IF hour BETWEEN 2 AND 4 THEN

+ RAISE EXCEPTION 'Login forbidden';

+END IF;

can be bypassed with PGOPTIONS='-c timezone=...'. Probably this is
nothing new and concerns any SECURITY DEFINER function, but still...

Best regards,

--
Sergey Shinderuk https://postgrespro.com/

#120Daniel Gustafsson
daniel@yesql.se
In reply to: Sergey Shinderuk (#119)
Re: On login trigger: take three

On 20 Sep 2022, at 15:43, Sergey Shinderuk <s.shinderuk@postgrespro.ru> wrote:

On 02.09.2022 18:36, Daniel Gustafsson wrote:

This had bitrotted a fair bit, attached is a rebase along with (mostly)
documentation fixes. 0001 adds a generic GUC for ignoring event triggers and
0002 adds the login event with event trigger support, and hooks it up to the
GUC such that broken triggers wont require single-user mode. Moving the CF
entry back to Needs Review.

There is a race around setting and clearing of dathasloginevt.

Thanks for the report! The whole dathasloginevt mechanism is IMO too clunky
and unelegant to go ahead with, I wouldn't be surprised if there are other bugs
lurking there. Since the original authors seems to have retired from the patch
(I've only rebased it to try and help) I am inclined to mark it as returned
with feedback.

--
Daniel Gustafsson https://vmware.com/

#121Sergey Shinderuk
s.shinderuk@postgrespro.ru
In reply to: Daniel Gustafsson (#120)
Re: On login trigger: take three

On 20.09.2022 17:10, Daniel Gustafsson wrote:

On 20 Sep 2022, at 15:43, Sergey Shinderuk <s.shinderuk@postgrespro.ru> wrote:
There is a race around setting and clearing of dathasloginevt.

Thanks for the report! The whole dathasloginevt mechanism is IMO too clunky
and unelegant to go ahead with, I wouldn't be surprised if there are other bugs
lurking there.

Indeed.

CREATE DATABASE doesn't copy dathasloginevt from the template database.
ALTER EVENT TRIGGER ... ENABLE doesn't set dathasloginevt.

In both cases triggers are enabled according to \dy output, but don't
fire. The admin may not notice it immediately.

--
Sergey Shinderuk https://postgrespro.com/

#122Daniel Gustafsson
daniel@yesql.se
In reply to: Daniel Gustafsson (#120)
Re: On login trigger: take three

On 20 Sep 2022, at 16:10, Daniel Gustafsson <daniel@yesql.se> wrote:

Since the original authors seems to have retired from the patch
(I've only rebased it to try and help) I am inclined to mark it as returned
with feedback.

Doing so now since no updates have been posted for quite some time and holes
have been poked in the patch.

The GUC for temporarily disabling event triggers, to avoid the need for single-
user mode during troubleshooting, might however be of interest so I will break
that out and post to a new thread.

--
Daniel Gustafsson https://vmware.com/

#123Ian Lawrence Barwick
barwick@gmail.com
In reply to: Daniel Gustafsson (#122)
Re: On login trigger: take three

2022年11月4日(金) 5:23 Daniel Gustafsson <daniel@yesql.se>:

On 20 Sep 2022, at 16:10, Daniel Gustafsson <daniel@yesql.se> wrote:

Since the original authors seems to have retired from the patch
(I've only rebased it to try and help) I am inclined to mark it as returned
with feedback.

Doing so now since no updates have been posted for quite some time and holes
have been poked in the patch.

I see it was still "Waiting on Author" so I went ahead and changed the status.

The GUC for temporarily disabling event triggers, to avoid the need for single-
user mode during troubleshooting, might however be of interest so I will break
that out and post to a new thread.

For reference this is the new thread:

/messages/by-id/9140106E-F9BF-4D85-8FC8-F2D3C094A6D9@yesql.se

Regards

Ian Barwick

#124Mikhail Gribkov
youzhick@gmail.com
In reply to: Ian Lawrence Barwick (#123)
4 attachment(s)
Re: On login trigger: take three

Hi hackers,

Since the original authors, as Daniel said, seems to have retired from the
patch, I have allowed myself to continue the patch polishing.

Attached v32 includes fresh rebase and the following fixes:

- Copying dathasloginevt flag during DB creation from template;

- Restoring dathasloginevt flag after re-enabling a disabled trigger;

- Preserving dathasloginevt flag during not-running a trigger because of
some filters (running with 'session_replication_role=replica' for example);

- Checking tags during the trigger creation;

The (en/dis)abling GUC was removed from the patch by default because it is
now discussed and supported in a separate thread by Daniel Gustaffson:

*/messages/by-id/9140106E-F9BF-4D85-8FC8-F2D3C094A6D9@yesql.se*
</messages/by-id/9140106E-F9BF-4D85-8FC8-F2D3C094A6D9@yesql.se&gt;

Still the back-ported version of the Daniel’s patch is attached here too –
just for convenience.

While the above changes represent the straight work continue, there is
another version to consider. Most of the patch problems seem to originate
from the dathasloginevt flag support. There are lots of known problems
(partly fixed) and probably a lot more unfound. At the same time the flag
itself is not a critical element, but just a performance optimizer for
logging-in of users who are NOT using the on-login triggers.

I have made a simpler version without the flag and tried to compare the
performance. The results show that life without dathasloginevt is still
possible. When there is a small number of non-login event triggers, the
difference is barely noticeable indeed. To show the difference, I have
added 1000 sql_drop event triggers and used pgbench to continuously query
the database for “select 1” with reconnects for 3 seconds. Here are the
results. For each version I recorded average connection time with 0 and
with 1000 event triggers:

With dathasloginevt flag: 0.609 ms VS 0.611 ms

No-flag version: 0.617 ms VS 0.727 ms

You can see that the difference is noticeable, but not that critical as we
can expect for 1000 triggers (while in a vast majority of real cases there
would be a much lesser number of triggers). I.e. the flagless version of
the patch introduces a huge reliability raise at the cost of a small
performance drop.

What do you think? Maybe it’s better to use the flagless version? Or even a
small performance drop is considered as unacceptable?

The attached files include the two versions of the patch: “v32” for the
updated version with flag and “v31_nf” – for the flagless version. Both
versions contain “0001“ file for the patch itself and “0002” for
back-ported controlling GUC.
--
best regards,
Mikhail A. Gribkov

On Fri, Nov 4, 2022 at 3:58 AM Ian Lawrence Barwick <barwick@gmail.com>
wrote:

Show quoted text

2022年11月4日(金) 5:23 Daniel Gustafsson <daniel@yesql.se>:

On 20 Sep 2022, at 16:10, Daniel Gustafsson <daniel@yesql.se> wrote:

Since the original authors seems to have retired from the patch
(I've only rebased it to try and help) I am inclined to mark it as

returned

with feedback.

Doing so now since no updates have been posted for quite some time and

holes

have been poked in the patch.

I see it was still "Waiting on Author" so I went ahead and changed the
status.

The GUC for temporarily disabling event triggers, to avoid the need for

single-

user mode during troubleshooting, might however be of interest so I will

break

that out and post to a new thread.

For reference this is the new thread:

/messages/by-id/9140106E-F9BF-4D85-8FC8-F2D3C094A6D9@yesql.se

Regards

Ian Barwick

Attachments:

v32-0002-Add-GUC-for-temporarily-disabling-event-triggers.patchapplication/octet-stream; name=v32-0002-Add-GUC-for-temporarily-disabling-event-triggers.patchDownload
commit a6abe3dc29341bfdbd1b1a9ff5e277f1235861b8
Author: Mikhail Gribkov <m.gribkov@postgrespro.ru>
Commit: Mikhail Gribkov <m.gribkov@postgrespro.ru>

    [PATCH v1_а] Add GUC for temporarily disabling event triggers
    
    In order to troubleshoot misbehaving or buggy event triggers, the
    documented advice is to enter single-user mode.  In an attempt to
    reduce the number of situations where single-user mode is required
    for non-extraordinary maintenance, this GUC allows to temporarily
    suspending event triggers.
    
    This was extracted from a larger patchset which aimed at supporting
    event triggers on login events.
    
    The very this version (_a) is a back-porting of an extracted patch to
    the login event fork. The GUC patch is curerntly supported by
    Daniel Gustafsson in this thread:
    https://www.postgresql.org/message-id/9140106E-F9BF-4D85-8FC8-F2D3C094A6D9%40yesql.se

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 24b1624bad..ddb70a4adc 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9381,6 +9381,25 @@ SET XML OPTION { DOCUMENT | CONTENT };
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-ignore-event-trigger" xreflabel="ignore_event_trigger">
+      <term><varname>ignore_event_trigger</varname> (<type>enum</type>)
+      <indexterm>
+       <primary><varname>ignore_event_trigger</varname></primary>
+       <secondary>configuration parameter</secondary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Allows to temporarily disable event triggers from executing in order
+        to troubleshoot and repair faulty event triggers.  The default value
+        is <literal>none</literal>, with which all event triggers are enabled.
+        When set to <literal>all</literal> then all event triggers will be
+        disabled.  Only superusers and users with the appropriate
+        <literal>SET</literal> privilege can change this setting.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
     </sect2>
      <sect2 id="runtime-config-client-format">
diff --git a/doc/src/sgml/ref/create_event_trigger.sgml b/doc/src/sgml/ref/create_event_trigger.sgml
index 22c8119198..f6922c3de3 100644
--- a/doc/src/sgml/ref/create_event_trigger.sgml
+++ b/doc/src/sgml/ref/create_event_trigger.sgml
@@ -123,7 +123,9 @@ CREATE EVENT TRIGGER <replaceable class="parameter">name</replaceable>
    Event triggers are disabled in single-user mode (see <xref
    linkend="app-postgres"/>).  If an erroneous event trigger disables the
    database so much that you can't even drop the trigger, restart in
-   single-user mode and you'll be able to do that.
+   single-user mode and you'll be able to do that. Event triggers can also be
+   temporarily disabled for such troubleshooting, see
+   <xref linkend="guc-ignore-event-trigger"/>.
   </para>
  </refsect1>
 
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 66d8ebe0cd..85621dafd7 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -76,6 +76,9 @@ typedef struct EventTriggerQueryState
 
 static EventTriggerQueryState *currentEventTriggerState = NULL;
 
+/* GUC parameter */
+int		ignore_event_trigger = IGNORE_EVENT_TRIGGER_NONE;
+
 /* Support for dropped objects */
 typedef struct SQLDropObject
 {
@@ -104,6 +107,7 @@ static void validate_table_rewrite_tags(const char *filtervar, List *taglist);
 static void EventTriggerInvoke(List *fn_oid_list, EventTriggerData *trigdata);
 static const char *stringify_grant_objtype(ObjectType objtype);
 static const char *stringify_adefprivs_objtype(ObjectType objtype);
+static bool ignore_event_trigger_check(EventTriggerEvent event);
 static void set_dathasloginevt(bool isActive);
 
 /*
@@ -707,8 +711,11 @@ EventTriggerDDLCommandStart(Node *parsetree)
 	 * wherein event triggers are disabled.  (Or we could implement
 	 * heapscan-and-sort logic for that case, but having disaster recovery
 	 * scenarios depend on code that's otherwise untested isn't appetizing.)
+	 *
+	 * Additionally, event triggers can be disabled with a superuser-only GUC
+	 * to make fixing database easier as per 1 above.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_DDLCommandStart))
 		return;
 
 	runlist = EventTriggerCommonSetup(parsetree,
@@ -742,9 +749,9 @@ EventTriggerDDLCommandEnd(Node *parsetree)
 
 	/*
 	 * See EventTriggerDDLCommandStart for a discussion about why event
-	 * triggers are disabled in single user mode.
+	 * triggers are disabled in single user mode or via GUC.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_DDLCommandEnd))
 		return;
 
 	/*
@@ -790,9 +797,9 @@ EventTriggerSQLDrop(Node *parsetree)
 
 	/*
 	 * See EventTriggerDDLCommandStart for a discussion about why event
-	 * triggers are disabled in single user mode.
+	 * triggers are disabled in single user mode or via a GUC.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_SQLDrop))
 		return;
 
 	/*
@@ -882,7 +889,7 @@ EventTriggerOnLogin(void)
 	 * See EventTriggerDDLCommandStart for a discussion about why event
 	 * triggers are disabled in single user mode.
 	 */
-	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId))
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId) || ignore_event_trigger_check(EVT_Login))
 		return;
 
 	StartTransactionCommand();
@@ -966,9 +973,9 @@ EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason)
 
 	/*
 	 * See EventTriggerDDLCommandStart for a discussion about why event
-	 * triggers are disabled in single user mode.
+	 * triggers are disabled in single user mode or via a GUC.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_TableRewrite))
 		return;
 
 	/*
@@ -2330,3 +2337,22 @@ stringify_adefprivs_objtype(ObjectType objtype)
 
 	return "???";				/* keep compiler quiet */
 }
+
+
+/*
+ * Checks whether the specified event is ignored by the ignore_event_trigger
+ * GUC or not. Currently, the GUC only supports ignoring all or nothing but
+ * that might change so the function takes an event to aid that.
+ */
+static bool
+ignore_event_trigger_check(EventTriggerEvent event)
+{
+	(void) event;			/* unused parameter */
+
+	if (ignore_event_trigger == IGNORE_EVENT_TRIGGER_ALL)
+		return true;
+
+	/* IGNORE_EVENT_TRIGGER_NONE */
+	return false;
+
+}
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 349dd6a537..12f65c2f5b 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -36,6 +36,7 @@
 #include "catalog/namespace.h"
 #include "catalog/storage.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/tablespace.h"
 #include "commands/trigger.h"
 #include "commands/user.h"
@@ -446,6 +447,12 @@ static const struct config_enum_entry wal_compression_options[] = {
 	{NULL, 0, false}
 };
 
+static const struct config_enum_entry ignore_event_trigger_options[] = {
+	{"none", IGNORE_EVENT_TRIGGER_NONE, false},
+	{"all", IGNORE_EVENT_TRIGGER_ALL, false},
+	{NULL, 0, false}
+};
+
 /*
  * Options for enum values stored in other modules
  */
@@ -4724,6 +4731,16 @@ struct config_enum ConfigureNamesEnum[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"ignore_event_trigger", PGC_SUSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Disable event triggers for the duration of the session."),
+			NULL
+		},
+		&ignore_event_trigger,
+		IGNORE_EVENT_TRIGGER_NONE, ignore_event_trigger_options,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"wal_level", PGC_POSTMASTER, WAL_SETTINGS,
 			gettext_noop("Sets the level of information written to the WAL."),
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index c9efee59bf..7144b67761 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -29,6 +29,14 @@ typedef struct EventTriggerData
 	CommandTag	tag;
 } EventTriggerData;
 
+typedef enum ignore_event_trigger_events
+{
+	IGNORE_EVENT_TRIGGER_NONE,
+	IGNORE_EVENT_TRIGGER_ALL
+} IgnoreEventTriggersEvents;
+
+extern PGDLLIMPORT int ignore_event_trigger;
+
 #define AT_REWRITE_ALTER_PERSISTENCE	0x01
 #define AT_REWRITE_DEFAULT_VAL			0x02
 #define AT_REWRITE_COLUMN_REWRITE		0x04
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index cb16f47c35..66127a592d 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -614,6 +614,27 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- Check the GUC for disabling event triggers
+CREATE FUNCTION test_event_trigger_guc() RETURNS event_trigger
+LANGUAGE plpgsql AS $$
+DECLARE
+       obj record;
+BEGIN
+       FOR obj IN SELECT * FROM pg_event_trigger_dropped_objects()
+       LOOP
+               RAISE NOTICE '% dropped %', tg_tag, obj.object_type;
+       END LOOP;
+END;
+$$;
+CREATE EVENT TRIGGER test_event_trigger_guc
+       ON sql_drop
+       WHEN TAG IN ('DROP POLICY') EXECUTE FUNCTION test_event_trigger_guc();
+CREATE POLICY pguc ON event_trigger_test USING (FALSE);
+DROP POLICY pguc ON event_trigger_test;
+NOTICE:  DROP POLICY dropped policy
+SET ignore_event_trigger = 'all';
+CREATE POLICY pguc ON event_trigger_test USING (FALSE);
+DROP POLICY pguc ON event_trigger_test;
 -- Login event triggers
 CREATE TABLE user_logins(id serial, who text);
 GRANT SELECT ON user_logins TO public;
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 9d5bb328b0..538d92efce 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -472,6 +472,28 @@ DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
 
+-- Check the GUC for disabling event triggers
+CREATE FUNCTION test_event_trigger_guc() RETURNS event_trigger
+LANGUAGE plpgsql AS $$
+DECLARE
+       obj record;
+BEGIN
+       FOR obj IN SELECT * FROM pg_event_trigger_dropped_objects()
+       LOOP
+               RAISE NOTICE '% dropped %', tg_tag, obj.object_type;
+       END LOOP;
+END;
+$$;
+CREATE EVENT TRIGGER test_event_trigger_guc
+       ON sql_drop
+       WHEN TAG IN ('DROP POLICY') EXECUTE FUNCTION test_event_trigger_guc();
+
+CREATE POLICY pguc ON event_trigger_test USING (FALSE);
+DROP POLICY pguc ON event_trigger_test;
+SET ignore_event_trigger = 'all';
+CREATE POLICY pguc ON event_trigger_test USING (FALSE);
+DROP POLICY pguc ON event_trigger_test;
+
 -- Login event triggers
 CREATE TABLE user_logins(id serial, who text);
 GRANT SELECT ON user_logins TO public;
v31_nf-0001-Add-support-event-triggers-on-authenticated-login-noflag.patchapplication/octet-stream; name=v31_nf-0001-Add-support-event-triggers-on-authenticated-login-noflag.patchDownload
commit 881c0e97770e1229f21e565c8656fa0e52e6912b
Author: Mikhail Gribkov <m.gribkov@postgrespro.ru>
Commit: Mikhail Gribkov <m.gribkov@postgrespro.ru>

    [PATCH v31_NF] Add support event triggers on authenticated login
    
    A no-dathasloginevt-flag version of the 31 patch.
    Most of the patch problems seem to originate from the dathasloginevent
    flag support. There are lots of known ptoblems and probably a lot
    more unfound. At the same time performance tests show that this flag,
    originally added for performance reasons, is not that critical.
    Thus proposing a flagless version of the patch. Such version introduces
    a huge reliability raise at a cost of tiny performance drop.

diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index f1235a2c9f..1d1981fa9f 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>login</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,18 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>login</literal> event occurs when an authenticated user logs
+     in to the system. Any bugs in a trigger procedure for this event may
+     prevent successful login to the system. Such bugs may be fixed by
+     restarting the system in single-user mode (as event triggers are
+     disabled in this mode). See the <xref linkend="app-postgres"/> reference
+     page for details about using single-user mode.
+     The <literal>login</literal> event will also fire on standby servers.
+     To prevent servers from becoming inaccessible, such triggers must avoid
+     writing anything to the database when running on a standby.
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1296,6 +1309,79 @@ $$;
 CREATE EVENT TRIGGER no_rewrite_allowed
                   ON table_rewrite
    EXECUTE FUNCTION no_rewrite();
+</programlisting>
+   </para>
+ </sect1>
+
+  <sect1 id="event-trigger-database-login-example">
+   <title>A Database Login Event Trigger Example</title>
+
+   <para>
+    The event trigger on the <literal>login</literal> event can be
+    useful for logging user logins, for verifying the connection and
+    assigning roles according to current circumstances, or for session
+    data initialization. It is very important that any event trigger using the
+    <literal>login</literal> event checks whether or not the database is in
+    recovery before performing any writes. Writing to a standby server will
+    make it inaccessible.
+   </para>
+
+   <para>
+    The following example demonstrates these options.
+<programlisting>
+-- create test tables and roles
+CREATE TABLE user_login_log (
+  "user" text,
+  "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+  RETURNS event_trigger SECURITY DEFINER
+  LANGUAGE plpgsql AS
+$$
+DECLARE
+  hour integer = EXTRACT('hour' FROM current_time);
+  rec boolean;
+BEGIN
+-- 1. Forbid logging in between 2AM and 4AM.
+IF hour BETWEEN 2 AND 4 THEN
+  RAISE EXCEPTION 'Login forbidden';
+END IF;
+
+-- The checks below cannot be performed on standby servers so
+-- ensure the database is not in recovery before we perform any
+-- operations.
+SELECT pg_is_in_recovery() INTO rec;
+IF rec THEN
+  RETURN;
+END IF
+
+-- 2. Assign some roles. At daytime, grant the day_worker role, else the
+-- night_worker role.
+IF hour BETWEEN 8 AND 20 THEN
+  EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT day_worker TO ' || quote_ident(session_user);
+ELSE
+  EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT night_worker TO ' || quote_ident(session_user);
+END IF;
+
+-- 3. Initialize user session data
+CREATE TEMP TABLE session_storage (x float, y integer);
+
+-- 4. Log the connection time
+INSERT INTO user_login_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+  ON login
+  EXECUTE FUNCTION init_session();
 </programlisting>
    </para>
  </sect1>
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index a3bdc5db07..6971a5474a 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -37,15 +38,18 @@
 #include "miscadmin.h"
 #include "parser/parse_func.h"
 #include "pgstat.h"
+#include "storage/lmgr.h"
 #include "tcop/deparse_utility.h"
 #include "tcop/utility.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 typedef struct EventTriggerQueryState
@@ -130,6 +134,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "login") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -579,10 +584,15 @@ EventTriggerCommonSetup(Node *parsetree,
 	{
 		CommandTag	dbgtag;
 
-		dbgtag = CreateCommandTag(parsetree);
+		if (event == EVT_Login)
+			dbgtag = CMDTAG_LOGIN;
+		else
+			dbgtag = CreateCommandTag(parsetree);
+
 		if (event == EVT_DDLCommandStart ||
 			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
+			event == EVT_SQLDrop ||
+			event == EVT_Login)
 		{
 			if (!command_tag_event_trigger_ok(dbgtag))
 				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
@@ -601,7 +611,10 @@ EventTriggerCommonSetup(Node *parsetree,
 		return NIL;
 
 	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
+	if (event == EVT_Login)
+		tag = CMDTAG_LOGIN;
+	else
+		tag = CreateCommandTag(parsetree);
 
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
@@ -799,6 +812,46 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Fire login event triggers if any are present.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId))
+		return;
+
+	StartTransactionCommand();
+
+	/* Find login triggers and run what was found */
+	runlist = EventTriggerCommonSetup(NULL,
+										EVT_Login, "login",
+										&trigdata);
+
+	if (runlist != NIL)
+	{
+		/* Event trigger execution may require an active snapshot. */
+		PushActiveSnapshot(GetTransactionSnapshot());
+
+		/* Run the triggers. */
+		EventTriggerInvoke(runlist, &trigdata);
+
+		/* Cleanup. */
+		list_free(runlist);
+
+		PopActiveSnapshot();
+	}
+
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 3082093d1e..14164ad898 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -33,6 +33,7 @@
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/prepare.h"
 #include "common/pg_prng.h"
 #include "jit/jit.h"
@@ -4205,6 +4206,9 @@ PostgresMain(const char *dbname, const char *username)
 	initStringInfo(&row_description_buf);
 	MemoryContextSwitchTo(TopMemoryContext);
 
+	/* Fire any defined login event triggers, if appropriate */
+	EventTriggerOnLogin();
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index f7f7165f7f..3e9388e956 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "login") == 0)
+			event = EVT_Login;
 		else
 			continue;
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 13014f074f..45f737e7c6 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3453,8 +3453,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop",
-					  "table_rewrite");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "login",
+					  "sql_drop", "table_rewrite");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 10091c3aaf..c9efee59bf 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -54,6 +54,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnLogin(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 9e94f44c5f..b900227e0c 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
+PG_CMDTAG(CMDTAG_LOGIN, "LOGIN", true, false, false)
 PG_CMDTAG(CMDTAG_MERGE, "MERGE", false, false, true)
 PG_CMDTAG(CMDTAG_MOVE, "MOVE", false, false, true)
 PG_CMDTAG(CMDTAG_NOTIFY, "NOTIFY", false, false, false)
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index ddb67a68fa..a66344aacb 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Login,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 986147b730..8ad50762c4 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,24 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE TABLE user_logins(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO user_logins (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 my $primary_lsn = $node_primary->lsn('write');
 $node_primary->wait_for_catchup($node_standby_1, 'replay', $primary_lsn);
@@ -388,6 +406,11 @@ sub replay_check
 
 replay_check();
 
+my $evttrig = $node_standby_1->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+$evttrig = $node_standby_2->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 5a10958df5..433f5f6e95 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -614,3 +614,34 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     1
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     2
+(1 row)
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 1aeaddbe71..3371ee07c4 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -471,3 +471,24 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+SELECT COUNT(*) FROM user_logins;
+\c
+SELECT COUNT(*) FROM user_logins;
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
\ No newline at end of file
v31_nf-0002-Add-GUC-for-temporarily-disabling-event-triggers.patchapplication/octet-stream; name=v31_nf-0002-Add-GUC-for-temporarily-disabling-event-triggers.patchDownload
commit 0fe8e91f90f80c2a4c19ee371079410bd1b3a414
Author: Mikhail Gribkov <m.gribkov@postgrespro.ru>
Commit: Mikhail Gribkov <m.gribkov@postgrespro.ru>

    [PATCH v1_а_NF] Add GUC for temporarily disabling event triggers
    
    In order to troubleshoot misbehaving or buggy event triggers, the
    documented advice is to enter single-user mode.  In an attempt to
    reduce the number of situations where single-user mode is required
    for non-extraordinary maintenance, this GUC allows to temporarily
    suspending event triggers.
    
    This was extracted from a larger patchset which aimed at supporting
    event triggers on login events.
    
    The very this version (_a) is a back-porting of an extracted patch to
    the login event fork. The GUC patch is curerntly supported by
    Daniel Gustafsson in this thread:
    https://www.postgresql.org/message-id/9140106E-F9BF-4D85-8FC8-F2D3C094A6D9%40yesql.se

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 24b1624bad..ddb70a4adc 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9381,6 +9381,25 @@ SET XML OPTION { DOCUMENT | CONTENT };
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-ignore-event-trigger" xreflabel="ignore_event_trigger">
+      <term><varname>ignore_event_trigger</varname> (<type>enum</type>)
+      <indexterm>
+       <primary><varname>ignore_event_trigger</varname></primary>
+       <secondary>configuration parameter</secondary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Allows to temporarily disable event triggers from executing in order
+        to troubleshoot and repair faulty event triggers.  The default value
+        is <literal>none</literal>, with which all event triggers are enabled.
+        When set to <literal>all</literal> then all event triggers will be
+        disabled.  Only superusers and users with the appropriate
+        <literal>SET</literal> privilege can change this setting.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
     </sect2>
      <sect2 id="runtime-config-client-format">
diff --git a/doc/src/sgml/ref/create_event_trigger.sgml b/doc/src/sgml/ref/create_event_trigger.sgml
index 22c8119198..f6922c3de3 100644
--- a/doc/src/sgml/ref/create_event_trigger.sgml
+++ b/doc/src/sgml/ref/create_event_trigger.sgml
@@ -123,7 +123,9 @@ CREATE EVENT TRIGGER <replaceable class="parameter">name</replaceable>
    Event triggers are disabled in single-user mode (see <xref
    linkend="app-postgres"/>).  If an erroneous event trigger disables the
    database so much that you can't even drop the trigger, restart in
-   single-user mode and you'll be able to do that.
+   single-user mode and you'll be able to do that. Event triggers can also be
+   temporarily disabled for such troubleshooting, see
+   <xref linkend="guc-ignore-event-trigger"/>.
   </para>
  </refsect1>
 
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 6971a5474a..ce6b089ee1 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -76,6 +76,9 @@ typedef struct EventTriggerQueryState
 
 static EventTriggerQueryState *currentEventTriggerState = NULL;
 
+/* GUC parameter */
+int		ignore_event_trigger = IGNORE_EVENT_TRIGGER_NONE;
+
 /* Support for dropped objects */
 typedef struct SQLDropObject
 {
@@ -104,6 +107,7 @@ static void validate_table_rewrite_tags(const char *filtervar, List *taglist);
 static void EventTriggerInvoke(List *fn_oid_list, EventTriggerData *trigdata);
 static const char *stringify_grant_objtype(ObjectType objtype);
 static const char *stringify_adefprivs_objtype(ObjectType objtype);
+static bool ignore_event_trigger_check(EventTriggerEvent event);
 
 /*
  * Create an event trigger.
@@ -670,8 +674,11 @@ EventTriggerDDLCommandStart(Node *parsetree)
 	 * wherein event triggers are disabled.  (Or we could implement
 	 * heapscan-and-sort logic for that case, but having disaster recovery
 	 * scenarios depend on code that's otherwise untested isn't appetizing.)
+	 *
+	 * Additionally, event triggers can be disabled with a superuser-only GUC
+	 * to make fixing database easier as per 1 above.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_DDLCommandStart))
 		return;
 
 	runlist = EventTriggerCommonSetup(parsetree,
@@ -705,9 +712,9 @@ EventTriggerDDLCommandEnd(Node *parsetree)
 
 	/*
 	 * See EventTriggerDDLCommandStart for a discussion about why event
-	 * triggers are disabled in single user mode.
+	 * triggers are disabled in single user mode or via GUC.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_DDLCommandEnd))
 		return;
 
 	/*
@@ -753,9 +760,9 @@ EventTriggerSQLDrop(Node *parsetree)
 
 	/*
 	 * See EventTriggerDDLCommandStart for a discussion about why event
-	 * triggers are disabled in single user mode.
+	 * triggers are disabled in single user mode or via a GUC.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_SQLDrop))
 		return;
 
 	/*
@@ -825,7 +832,7 @@ EventTriggerOnLogin(void)
 	 * See EventTriggerDDLCommandStart for a discussion about why event
 	 * triggers are disabled in single user mode.
 	 */
-	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId))
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId) || ignore_event_trigger_check(EVT_Login))
 		return;
 
 	StartTransactionCommand();
@@ -864,9 +871,9 @@ EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason)
 
 	/*
 	 * See EventTriggerDDLCommandStart for a discussion about why event
-	 * triggers are disabled in single user mode.
+	 * triggers are disabled in single user mode or via a GUC.
 	 */
-	if (!IsUnderPostmaster)
+	if (!IsUnderPostmaster || ignore_event_trigger_check(EVT_TableRewrite))
 		return;
 
 	/*
@@ -2228,3 +2235,22 @@ stringify_adefprivs_objtype(ObjectType objtype)
 
 	return "???";				/* keep compiler quiet */
 }
+
+
+/*
+ * Checks whether the specified event is ignored by the ignore_event_trigger
+ * GUC or not. Currently, the GUC only supports ignoring all or nothing but
+ * that might change so the function takes an event to aid that.
+ */
+static bool
+ignore_event_trigger_check(EventTriggerEvent event)
+{
+	(void) event;			/* unused parameter */
+
+	if (ignore_event_trigger == IGNORE_EVENT_TRIGGER_ALL)
+		return true;
+
+	/* IGNORE_EVENT_TRIGGER_NONE */
+	return false;
+
+}
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 349dd6a537..12f65c2f5b 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -36,6 +36,7 @@
 #include "catalog/namespace.h"
 #include "catalog/storage.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/tablespace.h"
 #include "commands/trigger.h"
 #include "commands/user.h"
@@ -446,6 +447,12 @@ static const struct config_enum_entry wal_compression_options[] = {
 	{NULL, 0, false}
 };
 
+static const struct config_enum_entry ignore_event_trigger_options[] = {
+	{"none", IGNORE_EVENT_TRIGGER_NONE, false},
+	{"all", IGNORE_EVENT_TRIGGER_ALL, false},
+	{NULL, 0, false}
+};
+
 /*
  * Options for enum values stored in other modules
  */
@@ -4724,6 +4731,16 @@ struct config_enum ConfigureNamesEnum[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"ignore_event_trigger", PGC_SUSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Disable event triggers for the duration of the session."),
+			NULL
+		},
+		&ignore_event_trigger,
+		IGNORE_EVENT_TRIGGER_NONE, ignore_event_trigger_options,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"wal_level", PGC_POSTMASTER, WAL_SETTINGS,
 			gettext_noop("Sets the level of information written to the WAL."),
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index c9efee59bf..7144b67761 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -29,6 +29,14 @@ typedef struct EventTriggerData
 	CommandTag	tag;
 } EventTriggerData;
 
+typedef enum ignore_event_trigger_events
+{
+	IGNORE_EVENT_TRIGGER_NONE,
+	IGNORE_EVENT_TRIGGER_ALL
+} IgnoreEventTriggersEvents;
+
+extern PGDLLIMPORT int ignore_event_trigger;
+
 #define AT_REWRITE_ALTER_PERSISTENCE	0x01
 #define AT_REWRITE_DEFAULT_VAL			0x02
 #define AT_REWRITE_COLUMN_REWRITE		0x04
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 433f5f6e95..2fc75e44ea 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -614,6 +614,27 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- Check the GUC for disabling event triggers
+CREATE FUNCTION test_event_trigger_guc() RETURNS event_trigger
+LANGUAGE plpgsql AS $$
+DECLARE
+       obj record;
+BEGIN
+       FOR obj IN SELECT * FROM pg_event_trigger_dropped_objects()
+       LOOP
+               RAISE NOTICE '% dropped %', tg_tag, obj.object_type;
+       END LOOP;
+END;
+$$;
+CREATE EVENT TRIGGER test_event_trigger_guc
+       ON sql_drop
+       WHEN TAG IN ('DROP POLICY') EXECUTE FUNCTION test_event_trigger_guc();
+CREATE POLICY pguc ON event_trigger_test USING (FALSE);
+DROP POLICY pguc ON event_trigger_test;
+NOTICE:  DROP POLICY dropped policy
+SET ignore_event_trigger = 'all';
+CREATE POLICY pguc ON event_trigger_test USING (FALSE);
+DROP POLICY pguc ON event_trigger_test;
 -- Login event triggers
 CREATE TABLE user_logins(id serial, who text);
 GRANT SELECT ON user_logins TO public;
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 3371ee07c4..dfc835c913 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -472,6 +472,28 @@ DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
 
+-- Check the GUC for disabling event triggers
+CREATE FUNCTION test_event_trigger_guc() RETURNS event_trigger
+LANGUAGE plpgsql AS $$
+DECLARE
+       obj record;
+BEGIN
+       FOR obj IN SELECT * FROM pg_event_trigger_dropped_objects()
+       LOOP
+               RAISE NOTICE '% dropped %', tg_tag, obj.object_type;
+       END LOOP;
+END;
+$$;
+CREATE EVENT TRIGGER test_event_trigger_guc
+       ON sql_drop
+       WHEN TAG IN ('DROP POLICY') EXECUTE FUNCTION test_event_trigger_guc();
+
+CREATE POLICY pguc ON event_trigger_test USING (FALSE);
+DROP POLICY pguc ON event_trigger_test;
+SET ignore_event_trigger = 'all';
+CREATE POLICY pguc ON event_trigger_test USING (FALSE);
+DROP POLICY pguc ON event_trigger_test;
+
 -- Login event triggers
 CREATE TABLE user_logins(id serial, who text);
 GRANT SELECT ON user_logins TO public;
v32-0001-Add-support-event-triggers-on-authenticated-login.patchapplication/octet-stream; name=v32-0001-Add-support-event-triggers-on-authenticated-login.patchDownload
commit 0abbbe4cd4c0a6464fe99088285a86956486ecd0
Author: Mikhail Gribkov <m.gribkov@postgrespro.ru>
Commit: Mikhail Gribkov <m.gribkov@postgrespro.ru>

    [PATCH v32] Add support event triggers on authenticated login
    
    The version includes the v31 patch fresh rebase and the following fixes:
    - Copying dathasloginevent falg during DB creation from template;
    - Restoring dathasloginevent flag after re-enabling a disabled trigger;
    - Preserving dathasloginevent flag during not-running a trigger because
      of some filters (running with 'session_replication_role=replica'
      for example);
    - Checking tags during the trigger creation;
    
    The (en/dis)abling GUC was removed from the patch because it is now
    discussed and supported in a separate thread:
    https://www.postgresql.org/message-id/9140106E-F9BF-4D85-8FC8-F2D3C094A6D9@yesql.se

diff --git a/doc/src/sgml/bki.sgml b/doc/src/sgml/bki.sgml
index f71644e398..315ba81951 100644
--- a/doc/src/sgml/bki.sgml
+++ b/doc/src/sgml/bki.sgml
@@ -184,7 +184,7 @@
   descr => 'database\'s default template',
   datname => 'template1', encoding => 'ENCODING',
   datlocprovider => 'LOCALE_PROVIDER', datistemplate => 't',
-  datallowconn => 't', datconnlimit => '-1', datfrozenxid => '0',
+  datallowconn => 't', dathasloginevt => 'f', datconnlimit => '-1', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datcollate => 'LC_COLLATE',
   datctype => 'LC_CTYPE', daticulocale => 'ICU_LOCALE', datacl => '_null_' },
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 9ed2b020b7..09925263e2 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -3027,6 +3027,19 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>dathasloginevt</structfield> <type>bool</type>
+      </para>
+      <para>
+        Indicates that there are login event triggers defined for this database.
+        This flag is used to avoid extra lookups on the
+        <structname>pg_event_trigger</structname> table during each backend
+        startup.  This flag is used internally by <productname>PostgreSQL</productname>
+        and should not be manually altered or read for monitoring purposes.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>datconnlimit</structfield> <type>int4</type>
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index 16853ced6f..8afa2ff05c 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -4769,6 +4769,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = t (type: 1)
 datallowconn = t (type: 1)
+dathasloginevt = f (type: 1)
 datconnlimit = -1 (type: 5)
 datfrozenxid = 379 (type: 1)
 dattablespace = 1663 (type: 1)
@@ -4793,6 +4794,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = f (type: 1)
 datallowconn = t (type: 1)
+dathasloginevt = f (type: 1)
 datconnlimit = -1 (type: 5)
 datfrozenxid = 379 (type: 1)
 dattablespace = 1663 (type: 1)
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index f1235a2c9f..c7ba506cc0 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>login</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,18 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>login</literal> event occurs when an authenticated user logs
+     in to the system. Any bugs in a trigger procedure for this event may
+     prevent successful login to the system. Such bugs may be fixed by
+     restarting the system in single-user mode (as event triggers are
+     disabled in this mode). See the <xref linkend="app-postgres"/> reference
+     page for details about using single-user mode.
+     The <literal>login</literal> event will also fire on standby servers. 
+     To prevent servers from becoming inaccessible, such triggers must avoid
+     writing anything to the database when running on a standby.
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1296,6 +1309,79 @@ $$;
 CREATE EVENT TRIGGER no_rewrite_allowed
                   ON table_rewrite
    EXECUTE FUNCTION no_rewrite();
+</programlisting>
+   </para>
+ </sect1>
+
+  <sect1 id="event-trigger-database-login-example">
+   <title>A Database Login Event Trigger Example</title>
+
+   <para>
+    The event trigger on the <literal>login</literal> event can be
+    useful for logging user logins, for verifying the connection and
+    assigning roles according to current circumstances, or for session
+    data initialization. It is very important that any event trigger using the
+    <literal>login</literal> event checks whether or not the database is in
+    recovery before performing any writes. Writing to a standby server will
+    make it inaccessible.
+   </para>
+
+   <para>
+    The following example demonstrates these options.
+<programlisting>
+-- create test tables and roles
+CREATE TABLE user_login_log (
+  "user" text,
+  "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+  RETURNS event_trigger SECURITY DEFINER
+  LANGUAGE plpgsql AS
+$$
+DECLARE
+  hour integer = EXTRACT('hour' FROM current_time);
+  rec boolean;
+BEGIN
+-- 1. Forbid logging in between 2AM and 4AM.
+IF hour BETWEEN 2 AND 4 THEN
+  RAISE EXCEPTION 'Login forbidden';
+END IF;
+
+-- The checks below cannot be performed on standby servers so
+-- ensure the database is not in recovery before we perform any
+-- operations.
+SELECT pg_is_in_recovery() INTO rec;
+IF rec THEN
+  RETURN;
+END IF
+
+-- 2. Assign some roles. At daytime, grant the day_worker role, else the
+-- night_worker role.
+IF hour BETWEEN 8 AND 20 THEN
+  EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT day_worker TO ' || quote_ident(session_user);
+ELSE
+  EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT night_worker TO ' || quote_ident(session_user);
+END IF;
+
+-- 3. Initialize user session data
+CREATE TEMP TABLE session_storage (x float, y integer);
+
+-- 4. Log the connection time
+INSERT INTO user_login_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+  ON login
+  EXECUTE FUNCTION init_session();
 </programlisting>
    </para>
  </sect1>
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 6eb8742718..bef255e70a 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -116,7 +116,7 @@ static void movedb(const char *dbname, const char *tblspcname);
 static void movedb_failure_callback(int code, Datum arg);
 static bool get_db_info(const char *name, LOCKMODE lockmode,
 						Oid *dbIdP, Oid *ownerIdP,
-						int *encodingP, bool *dbIsTemplateP, bool *dbAllowConnP,
+						int *encodingP, bool *dbIsTemplateP, bool *dbAllowConnP, bool *dbHasLoginEvtP,
 						TransactionId *dbFrozenXidP, MultiXactId *dbMinMultiP,
 						Oid *dbTablespace, char **dbCollate, char **dbCtype, char **dbIculocale,
 						char *dbLocProvider,
@@ -679,6 +679,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	char		src_locprovider = '\0';
 	char	   *src_collversion = NULL;
 	bool		src_istemplate;
+	bool		src_hasloginevt;
 	bool		src_allowconn;
 	TransactionId src_frozenxid = InvalidTransactionId;
 	MultiXactId src_minmxid = InvalidMultiXactId;
@@ -957,7 +958,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 
 	if (!get_db_info(dbtemplate, ShareLock,
 					 &src_dboid, &src_owner, &src_encoding,
-					 &src_istemplate, &src_allowconn,
+					 &src_istemplate, &src_allowconn, &src_hasloginevt,
 					 &src_frozenxid, &src_minmxid, &src_deftablespace,
 					 &src_collate, &src_ctype, &src_iculocale, &src_locprovider,
 					 &src_collversion))
@@ -1304,6 +1305,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	new_record[Anum_pg_database_datlocprovider - 1] = CharGetDatum(dblocprovider);
 	new_record[Anum_pg_database_datistemplate - 1] = BoolGetDatum(dbistemplate);
 	new_record[Anum_pg_database_datallowconn - 1] = BoolGetDatum(dballowconnections);
+	new_record[Anum_pg_database_dathasloginevt - 1] = BoolGetDatum(src_hasloginevt);
 	new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 	new_record[Anum_pg_database_datfrozenxid - 1] = TransactionIdGetDatum(src_frozenxid);
 	new_record[Anum_pg_database_datminmxid - 1] = TransactionIdGetDatum(src_minmxid);
@@ -1527,7 +1529,7 @@ dropdb(const char *dbname, bool missing_ok, bool force)
 	pgdbrel = table_open(DatabaseRelationId, RowExclusiveLock);
 
 	if (!get_db_info(dbname, AccessExclusiveLock, &db_id, NULL, NULL,
-					 &db_istemplate, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL))
+					 &db_istemplate, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL))
 	{
 		if (!missing_ok)
 		{
@@ -1726,7 +1728,7 @@ RenameDatabase(const char *oldname, const char *newname)
 	 */
 	rel = table_open(DatabaseRelationId, RowExclusiveLock);
 
-	if (!get_db_info(oldname, AccessExclusiveLock, &db_id, NULL, NULL,
+	if (!get_db_info(oldname, AccessExclusiveLock, &db_id, NULL, NULL, NULL,
 					 NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL))
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_DATABASE),
@@ -1836,7 +1838,7 @@ movedb(const char *dbname, const char *tblspcname)
 	 */
 	pgdbrel = table_open(DatabaseRelationId, RowExclusiveLock);
 
-	if (!get_db_info(dbname, AccessExclusiveLock, &db_id, NULL, NULL,
+	if (!get_db_info(dbname, AccessExclusiveLock, &db_id, NULL, NULL, NULL,
 					 NULL, NULL, NULL, NULL, &src_tblspcoid, NULL, NULL, NULL, NULL, NULL))
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_DATABASE),
@@ -2597,7 +2599,7 @@ pg_database_collation_actual_version(PG_FUNCTION_ARGS)
 static bool
 get_db_info(const char *name, LOCKMODE lockmode,
 			Oid *dbIdP, Oid *ownerIdP,
-			int *encodingP, bool *dbIsTemplateP, bool *dbAllowConnP,
+			int *encodingP, bool *dbIsTemplateP, bool *dbAllowConnP, bool *dbHasLoginEvtP,
 			TransactionId *dbFrozenXidP, MultiXactId *dbMinMultiP,
 			Oid *dbTablespace, char **dbCollate, char **dbCtype, char **dbIculocale,
 			char *dbLocProvider,
@@ -2681,6 +2683,9 @@ get_db_info(const char *name, LOCKMODE lockmode,
 				/* allowed as template? */
 				if (dbIsTemplateP)
 					*dbIsTemplateP = dbform->datistemplate;
+				/* Has on login event trigger? */
+				if (dbHasLoginEvtP)
+					*dbHasLoginEvtP = dbform->dathasloginevt;
 				/* allowing connections? */
 				if (dbAllowConnP)
 					*dbAllowConnP = dbform->datallowconn;
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index a3bdc5db07..66d8ebe0cd 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -37,15 +38,18 @@
 #include "miscadmin.h"
 #include "parser/parse_func.h"
 #include "pgstat.h"
+#include "storage/lmgr.h"
 #include "tcop/deparse_utility.h"
 #include "tcop/utility.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 typedef struct EventTriggerQueryState
@@ -100,6 +104,7 @@ static void validate_table_rewrite_tags(const char *filtervar, List *taglist);
 static void EventTriggerInvoke(List *fn_oid_list, EventTriggerData *trigdata);
 static const char *stringify_grant_objtype(ObjectType objtype);
 static const char *stringify_adefprivs_objtype(ObjectType objtype);
+static void set_dathasloginevt(bool isActive);
 
 /*
  * Create an event trigger.
@@ -130,6 +135,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "login") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -162,6 +168,10 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	else if (strcmp(stmt->eventname, "table_rewrite") == 0
 			 && tags != NULL)
 		validate_table_rewrite_tags("tag", tags);
+	else if (strcmp(stmt->eventname, "login") == 0 && tags != NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("Tag filtering is not supported for login event trigger")));
 
 	/*
 	 * Give user a nice error message if an event trigger of the same name
@@ -293,6 +303,13 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
 	CatalogTupleInsert(tgrel, tuple);
 	heap_freetuple(tuple);
 
+	/*
+	 * Login event triggers have an additional flag in pg_database to allow
+	 * faster lookups in hot codepaths. Set the flag unless already True.
+	 */
+	if (strcmp(eventname, "login") == 0)
+		set_dathasloginevt(true);
+
 	/* Depend on owner. */
 	recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
 
@@ -354,6 +371,28 @@ filter_list_to_array(List *filterlist)
 	return PointerGetDatum(construct_array_builtin(data, l, TEXTOID));
 }
 
+void
+set_dathasloginevt(bool isActive)
+{
+	Form_pg_database db;
+	Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+
+	/* Set dathasloginevt flag in pg_database */
+	HeapTuple	tuple;
+	tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+	db = (Form_pg_database) GETSTRUCT(tuple);
+	if (!db->dathasloginevt)
+	{
+		db->dathasloginevt = isActive;
+		CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+		CommandCounterIncrement();
+	}
+	table_close(pg_db, RowExclusiveLock);
+	heap_freetuple(tuple);
+}
+
 /*
  * ALTER EVENT TRIGGER foo ENABLE|DISABLE|ENABLE ALWAYS|REPLICA
  */
@@ -388,6 +427,9 @@ AlterEventTrigger(AlterEventTrigStmt *stmt)
 
 	CatalogTupleUpdate(tgrel, &tup->t_self, tup);
 
+	if (strcmp(stmt->trigname, "on_login_trigger") == 0 && tgenabled != TRIGGER_DISABLED)
+		set_dathasloginevt(true);
+
 	InvokeObjectPostAlterHook(EventTriggerRelationId,
 							  trigoid, 0);
 
@@ -554,7 +596,7 @@ filter_event_trigger(CommandTag tag, EventTriggerCacheItem *item)
 static List *
 EventTriggerCommonSetup(Node *parsetree,
 						EventTriggerEvent event, const char *eventstr,
-						EventTriggerData *trigdata)
+						EventTriggerData *trigdata, bool unfiltered)
 {
 	CommandTag	tag;
 	List	   *cachelist;
@@ -579,10 +621,15 @@ EventTriggerCommonSetup(Node *parsetree,
 	{
 		CommandTag	dbgtag;
 
-		dbgtag = CreateCommandTag(parsetree);
+		if (event == EVT_Login)
+			dbgtag = CMDTAG_LOGIN;
+		else
+			dbgtag = CreateCommandTag(parsetree);
+
 		if (event == EVT_DDLCommandStart ||
 			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
+			event == EVT_SQLDrop ||
+			event == EVT_Login)
 		{
 			if (!command_tag_event_trigger_ok(dbgtag))
 				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
@@ -601,7 +648,10 @@ EventTriggerCommonSetup(Node *parsetree,
 		return NIL;
 
 	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
+	if (event == EVT_Login)
+		tag = CMDTAG_LOGIN;
+	else
+		tag = CreateCommandTag(parsetree);
 
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
@@ -614,7 +664,7 @@ EventTriggerCommonSetup(Node *parsetree,
 	{
 		EventTriggerCacheItem *item = lfirst(lc);
 
-		if (filter_event_trigger(tag, item))
+		if (unfiltered || filter_event_trigger(tag, item))
 		{
 			/* We must plan to fire this trigger. */
 			runlist = lappend_oid(runlist, item->fnoid);
@@ -664,7 +714,7 @@ EventTriggerDDLCommandStart(Node *parsetree)
 	runlist = EventTriggerCommonSetup(parsetree,
 									  EVT_DDLCommandStart,
 									  "ddl_command_start",
-									  &trigdata);
+									  &trigdata, false);
 	if (runlist == NIL)
 		return;
 
@@ -712,7 +762,7 @@ EventTriggerDDLCommandEnd(Node *parsetree)
 
 	runlist = EventTriggerCommonSetup(parsetree,
 									  EVT_DDLCommandEnd, "ddl_command_end",
-									  &trigdata);
+									  &trigdata, false);
 	if (runlist == NIL)
 		return;
 
@@ -758,7 +808,7 @@ EventTriggerSQLDrop(Node *parsetree)
 
 	runlist = EventTriggerCommonSetup(parsetree,
 									  EVT_SQLDrop, "sql_drop",
-									  &trigdata);
+									  &trigdata, false);
 
 	/*
 	 * Nothing to do if run list is empty.  Note this typically can't happen,
@@ -799,6 +849,111 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Return true if this database has login event triggers, false otherwise.
+ */
+static bool
+DatabaseHasLoginEventTriggers(void)
+{
+	bool		has_login_event_triggers;
+	HeapTuple	tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+	has_login_event_triggers = ((Form_pg_database) GETSTRUCT(tuple))->dathasloginevt;
+	ReleaseSysCache(tuple);
+	return has_login_event_triggers;
+}
+
+/*
+ * Fire login event triggers if any are present.  The dathasloginevt
+ * pg_database flag is left when an event trigger is dropped, to avoid
+ * complicating the codepath in the case of multiple event triggers.  This
+ * function will instead unset the flag if no trigger is defined.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId))
+		return;
+
+	StartTransactionCommand();
+
+	if (DatabaseHasLoginEventTriggers())
+	{
+		runlist = EventTriggerCommonSetup(NULL,
+										  EVT_Login, "login",
+										  &trigdata, false);
+
+		if (runlist != NIL)
+		{
+			/*
+			 * Event trigger execution may require an active snapshot.
+			 */
+			PushActiveSnapshot(GetTransactionSnapshot());
+
+			/* Run the triggers. */
+			EventTriggerInvoke(runlist, &trigdata);
+
+			/* Cleanup. */
+			list_free(runlist);
+
+			PopActiveSnapshot();
+		}
+		else
+		{
+			/*
+			 * Runlist is empty: clear dathasloginevt flag
+			 */
+			Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+			HeapTuple	tuple;
+			Form_pg_database db;
+
+			LockSharedObject(DatabaseRelationId, MyDatabaseId, 0, AccessExclusiveLock);
+
+			tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+			db = (Form_pg_database) GETSTRUCT(tuple);
+			if (db->dathasloginevt)
+			{
+				/*
+				 * There can be a race condition: a login event trigger may
+				 * have been added after the pg_event_trigger table was
+				 * scanned, and we don't want to erroneously clear the
+				 * dathasloginevt flag in this case. To be sure that this
+				 * hasn't happened, repeat the scan under the pg_database
+				 * table lock.
+				 */
+				runlist = EventTriggerCommonSetup(NULL,
+												  EVT_Login, "login",
+												  &trigdata, true);
+				if (runlist == NIL)	/* list is still empty, so clear the
+										 * flag */
+				{
+					db->dathasloginevt = false;
+					CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+				}
+				else
+					list_free(runlist);
+			}
+			table_close(pg_db, RowExclusiveLock);
+			heap_freetuple(tuple);
+		}
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
@@ -829,7 +984,7 @@ EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason)
 	runlist = EventTriggerCommonSetup(parsetree,
 									  EVT_TableRewrite,
 									  "table_rewrite",
-									  &trigdata);
+									  &trigdata, false);
 	if (runlist == NIL)
 		return;
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 3082093d1e..14164ad898 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -33,6 +33,7 @@
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/prepare.h"
 #include "common/pg_prng.h"
 #include "jit/jit.h"
@@ -4205,6 +4206,9 @@ PostgresMain(const char *dbname, const char *username)
 	initStringInfo(&row_description_buf);
 	MemoryContextSwitchTo(TopMemoryContext);
 
+	/* Fire any defined login event triggers, if appropriate */
+	EventTriggerOnLogin();
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index f7f7165f7f..3e9388e956 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "login") == 0)
+			event = EVT_Login;
 		else
 			continue;
 
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index da427f4d4a..50addbf608 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3105,6 +3105,11 @@ dumpDatabase(Archive *fout)
 		appendPQExpBufferStr(delQry, ";\n");
 	}
 
+	/*
+	 * We do not restore pg_database.dathasloginevt because it is set
+	 * automatically on login event trigger creation.
+	 */
+
 	/* Add database-specific SET options */
 	dumpDatabaseConfig(fout, creaQry, datname, dbCatId.oid);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 13014f074f..45f737e7c6 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3453,8 +3453,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop",
-					  "table_rewrite");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "login",
+					  "sql_drop", "table_rewrite");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat
index 47dcbfb343..df32761f74 100644
--- a/src/include/catalog/pg_database.dat
+++ b/src/include/catalog/pg_database.dat
@@ -16,7 +16,7 @@
   descr => 'default template for new databases',
   datname => 'template1', encoding => 'ENCODING',
   datlocprovider => 'LOCALE_PROVIDER', datistemplate => 't',
-  datallowconn => 't', datconnlimit => '-1', datfrozenxid => '0',
+  datallowconn => 't', dathasloginevt => 'f', datconnlimit => '-1', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datcollate => 'LC_COLLATE',
   datctype => 'LC_CTYPE', daticulocale => 'ICU_LOCALE', datacl => '_null_' },
 
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index 611c95656a..ebf7151109 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -49,6 +49,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
 	/* new connections allowed? */
 	bool		datallowconn;
 
+	/* database has login event triggers? */
+	bool		dathasloginevt;
+
 	/* max connections allowed (-1=no limit) */
 	int32		datconnlimit;
 
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 10091c3aaf..c9efee59bf 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -54,6 +54,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnLogin(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 9e94f44c5f..b900227e0c 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
+PG_CMDTAG(CMDTAG_LOGIN, "LOGIN", true, false, false)
 PG_CMDTAG(CMDTAG_MERGE, "MERGE", false, false, true)
 PG_CMDTAG(CMDTAG_MOVE, "MOVE", false, false, true)
 PG_CMDTAG(CMDTAG_NOTIFY, "NOTIFY", false, false, false)
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index ddb67a68fa..a66344aacb 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Login,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 986147b730..8ad50762c4 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,24 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE TABLE user_logins(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO user_logins (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 my $primary_lsn = $node_primary->lsn('write');
 $node_primary->wait_for_catchup($node_standby_1, 'replay', $primary_lsn);
@@ -388,6 +406,11 @@ sub replay_check
 
 replay_check();
 
+my $evttrig = $node_standby_1->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+$evttrig = $node_standby_2->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 5a10958df5..cb16f47c35 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -614,3 +614,48 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     1
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     2
+(1 row)
+
+-- Check dathasloginevt in system catalog
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+ dathasloginevt 
+----------------
+ t
+(1 row)
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
+\c
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+ dathasloginevt 
+----------------
+ f
+(1 row)
+
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 1aeaddbe71..9d5bb328b0 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -471,3 +471,29 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+SELECT COUNT(*) FROM user_logins;
+\c
+SELECT COUNT(*) FROM user_logins;
+
+-- Check dathasloginevt in system catalog
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
+\c
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
#125Mikhail Gribkov
youzhick@gmail.com
In reply to: Mikhail Gribkov (#124)
2 attachment(s)
Re: On login trigger: take three

Hi hackers,
Attached v33 is a new rebase of the flagless version of the patch. As
there were no objections at first glance, I’ll try to post it to the
upcoming commitfest, thus the brief recap of all the patch details is below.

v33-On_client_login_event_trigger
The patch introduces a trigger on login event, allowing to fire some
actions right on the user connection. This can be useful for logging or
connection check purposes as well as for some personalization of the
environment. Usage details are described in the documentation included, but
shortly usage is the same as for other triggers: create function returning
event_trigger and then create event trigger on login event.

The patch is prepared to be applied to the master branch and tested when
applied after e52f8b301ed54aac5162b185b43f5f1e44b6b17e commit by Thomas
Munro (Date: Fri Dec 16 17:36:22 2022 +1300).
Regression tests and documentation included.

A couple of words about what and why I changed compared to the previous
author's version.
First, the (en/dis)abling GUC was removed from the patch because
ideologically it is a separate feature and nowadays it’s discussed and
supported in a separate thread by Daniel Gustaffson:
/messages/by-id/9140106E-F9BF-4D85-8FC8-F2D3C094A6D9@yesql.se
Second, I have removed the dathasloginevt flag. The flag was initially
added to the patch for performance reasons and it did the job, although it
was quite a clumsy construct causing tons of bugs and could potentially
lead to tons more during further PostgreSQL evolution. I have removed the
flag and found that the performance drop is not that significant.

And this is an aspect I should describe in more detail.
The possible performance drop is expected as an increased time used to
login a user NOT using a login trigger.
First of all, the method of performance check:
echo 'select 1;' > ./tst.sql
pgbench -n -C -T3 -f tst.sql -U postgres postgres
The output value "average connection time" is the one I use to compare
performance.
Now, what are the results.
* master branch: 0.641 ms
* patched version: 0.644 ms
No significant difference here and it is just what was expected. Based on
the patch design the performance drop can be expected when there are lots
of event triggers created, but not the login one. Thus I have created 1000
drop triggers (the script for creating triggers is attached too) in the
database and repeated the test:
* master branch: 0.646 ms
* patched version: 0.754 ms
For 2000 triggers the patched version connection time is further increased
to 0.862. Thus we have a login time rise in about 16.5% per 1000 event
triggers in the database. It is a statistically noticeable value, still I
don’t think it’s a critical one we should be afraid of.
N.B. The exact values of the login times slightly differ from the ones I
posted in the previous email. Well, that’s the repeatability level we have.
This convinces me even more that the observed level of performance drop is
acceptable.
--
best regards,
Mikhail A. Gribkov

e-mail: youzhick@gmail.com
*http://www.flickr.com/photos/youzhick/albums
<http://www.flickr.com/photos/youzhick/albums&gt;*
http://www.strava.com/athletes/5085772
phone: +7(916)604-71-12
Telegram: @youzhick

Attachments:

v33-On_client_login_event_trigger.patchapplication/octet-stream; name=v33-On_client_login_event_trigger.patchDownload
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index f1235a2c9f..1d1981fa9f 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>login</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,18 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>login</literal> event occurs when an authenticated user logs
+     in to the system. Any bugs in a trigger procedure for this event may
+     prevent successful login to the system. Such bugs may be fixed by
+     restarting the system in single-user mode (as event triggers are
+     disabled in this mode). See the <xref linkend="app-postgres"/> reference
+     page for details about using single-user mode.
+     The <literal>login</literal> event will also fire on standby servers.
+     To prevent servers from becoming inaccessible, such triggers must avoid
+     writing anything to the database when running on a standby.
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1296,6 +1309,79 @@ $$;
 CREATE EVENT TRIGGER no_rewrite_allowed
                   ON table_rewrite
    EXECUTE FUNCTION no_rewrite();
+</programlisting>
+   </para>
+ </sect1>
+
+  <sect1 id="event-trigger-database-login-example">
+   <title>A Database Login Event Trigger Example</title>
+
+   <para>
+    The event trigger on the <literal>login</literal> event can be
+    useful for logging user logins, for verifying the connection and
+    assigning roles according to current circumstances, or for session
+    data initialization. It is very important that any event trigger using the
+    <literal>login</literal> event checks whether or not the database is in
+    recovery before performing any writes. Writing to a standby server will
+    make it inaccessible.
+   </para>
+
+   <para>
+    The following example demonstrates these options.
+<programlisting>
+-- create test tables and roles
+CREATE TABLE user_login_log (
+  "user" text,
+  "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+  RETURNS event_trigger SECURITY DEFINER
+  LANGUAGE plpgsql AS
+$$
+DECLARE
+  hour integer = EXTRACT('hour' FROM current_time);
+  rec boolean;
+BEGIN
+-- 1. Forbid logging in between 2AM and 4AM.
+IF hour BETWEEN 2 AND 4 THEN
+  RAISE EXCEPTION 'Login forbidden';
+END IF;
+
+-- The checks below cannot be performed on standby servers so
+-- ensure the database is not in recovery before we perform any
+-- operations.
+SELECT pg_is_in_recovery() INTO rec;
+IF rec THEN
+  RETURN;
+END IF
+
+-- 2. Assign some roles. At daytime, grant the day_worker role, else the
+-- night_worker role.
+IF hour BETWEEN 8 AND 20 THEN
+  EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT day_worker TO ' || quote_ident(session_user);
+ELSE
+  EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT night_worker TO ' || quote_ident(session_user);
+END IF;
+
+-- 3. Initialize user session data
+CREATE TEMP TABLE session_storage (x float, y integer);
+
+-- 4. Log the connection time
+INSERT INTO user_login_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+  ON login
+  EXECUTE FUNCTION init_session();
 </programlisting>
    </para>
  </sect1>
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index a3bdc5db07..6971a5474a 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -37,15 +38,18 @@
 #include "miscadmin.h"
 #include "parser/parse_func.h"
 #include "pgstat.h"
+#include "storage/lmgr.h"
 #include "tcop/deparse_utility.h"
 #include "tcop/utility.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 typedef struct EventTriggerQueryState
@@ -130,6 +134,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "login") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -579,10 +584,15 @@ EventTriggerCommonSetup(Node *parsetree,
 	{
 		CommandTag	dbgtag;
 
-		dbgtag = CreateCommandTag(parsetree);
+		if (event == EVT_Login)
+			dbgtag = CMDTAG_LOGIN;
+		else
+			dbgtag = CreateCommandTag(parsetree);
+
 		if (event == EVT_DDLCommandStart ||
 			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
+			event == EVT_SQLDrop ||
+			event == EVT_Login)
 		{
 			if (!command_tag_event_trigger_ok(dbgtag))
 				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
@@ -601,7 +611,10 @@ EventTriggerCommonSetup(Node *parsetree,
 		return NIL;
 
 	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
+	if (event == EVT_Login)
+		tag = CMDTAG_LOGIN;
+	else
+		tag = CreateCommandTag(parsetree);
 
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
@@ -799,6 +812,46 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Fire login event triggers if any are present.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId))
+		return;
+
+	StartTransactionCommand();
+
+	/* Find login triggers and run what was found */
+	runlist = EventTriggerCommonSetup(NULL,
+										EVT_Login, "login",
+										&trigdata);
+
+	if (runlist != NIL)
+	{
+		/* Event trigger execution may require an active snapshot. */
+		PushActiveSnapshot(GetTransactionSnapshot());
+
+		/* Run the triggers. */
+		EventTriggerInvoke(runlist, &trigdata);
+
+		/* Cleanup. */
+		list_free(runlist);
+
+		PopActiveSnapshot();
+	}
+
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 01d264b5ab..6235269b4b 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -33,6 +33,7 @@
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/prepare.h"
 #include "common/pg_prng.h"
 #include "jit/jit.h"
@@ -4217,6 +4218,9 @@ PostgresMain(const char *dbname, const char *username)
 	initStringInfo(&row_description_buf);
 	MemoryContextSwitchTo(TopMemoryContext);
 
+	/* Fire any defined login event triggers, if appropriate */
+	EventTriggerOnLogin();
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index f7f7165f7f..3e9388e956 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "login") == 0)
+			event = EVT_Login;
 		else
 			continue;
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 2a3921937c..a9f16b5e3a 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3453,8 +3453,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop",
-					  "table_rewrite");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "login",
+					  "sql_drop", "table_rewrite");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 10091c3aaf..c9efee59bf 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -54,6 +54,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnLogin(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 9e94f44c5f..b900227e0c 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
+PG_CMDTAG(CMDTAG_LOGIN, "LOGIN", true, false, false)
 PG_CMDTAG(CMDTAG_MERGE, "MERGE", false, false, true)
 PG_CMDTAG(CMDTAG_MOVE, "MOVE", false, false, true)
 PG_CMDTAG(CMDTAG_NOTIFY, "NOTIFY", false, false, false)
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index ddb67a68fa..a66344aacb 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Login,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 986147b730..8ad50762c4 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,24 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE TABLE user_logins(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO user_logins (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 my $primary_lsn = $node_primary->lsn('write');
 $node_primary->wait_for_catchup($node_standby_1, 'replay', $primary_lsn);
@@ -388,6 +406,11 @@ sub replay_check
 
 replay_check();
 
+my $evttrig = $node_standby_1->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+$evttrig = $node_standby_2->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 5a10958df5..433f5f6e95 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -614,3 +614,34 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     1
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     2
+(1 row)
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 1aeaddbe71..420224f3ba 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -471,3 +471,24 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+SELECT COUNT(*) FROM user_logins;
+\c
+SELECT COUNT(*) FROM user_logins;
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
create_1000_triggers.sqlapplication/octet-stream; name=create_1000_triggers.sqlDownload
#126Nikita Malakhov
hukutoc@gmail.com
In reply to: Mikhail Gribkov (#125)
Re: On login trigger: take three

Hi,

Mikhail, I've checked the patch and previous discussion,
the condition mentioned earlier is still actual:

The example trigger from the documentation

+DECLARE

+ hour integer = EXTRACT('hour' FROM current_time);

+ rec boolean;

+BEGIN

+-- 1. Forbid logging in between 2AM and 4AM.

+IF hour BETWEEN 2 AND 4 THEN

+ RAISE EXCEPTION 'Login forbidden';

+END IF;

can be bypassed with PGOPTIONS='-c timezone=...'. Probably this is
nothing new and concerns any SECURITY DEFINER function, but still...

along with
+IF hour BETWEEN 8 AND 20 THEN
It seems to be a minor security issue, so just in case you haven't noticed
it.

On Fri, Dec 16, 2022 at 9:14 PM Mikhail Gribkov <youzhick@gmail.com> wrote:

Hi hackers,
Attached v33 is a new rebase of the flagless version of the patch. As
there were no objections at first glance, I’ll try to post it to the
upcoming commitfest, thus the brief recap of all the patch details is below.

v33-On_client_login_event_trigger
The patch introduces a trigger on login event, allowing to fire some
actions right on the user connection. This can be useful for logging or
connection check purposes as well as for some personalization of the
environment. Usage details are described in the documentation included, but
shortly usage is the same as for other triggers: create function returning
event_trigger and then create event trigger on login event.

The patch is prepared to be applied to the master branch and tested when
applied after e52f8b301ed54aac5162b185b43f5f1e44b6b17e commit by Thomas
Munro (Date: Fri Dec 16 17:36:22 2022 +1300).
Regression tests and documentation included.

A couple of words about what and why I changed compared to the previous
author's version.
First, the (en/dis)abling GUC was removed from the patch because
ideologically it is a separate feature and nowadays it’s discussed and
supported in a separate thread by Daniel Gustaffson:

/messages/by-id/9140106E-F9BF-4D85-8FC8-F2D3C094A6D9@yesql.se
Second, I have removed the dathasloginevt flag. The flag was initially
added to the patch for performance reasons and it did the job, although it
was quite a clumsy construct causing tons of bugs and could potentially
lead to tons more during further PostgreSQL evolution. I have removed the
flag and found that the performance drop is not that significant.

And this is an aspect I should describe in more detail.
The possible performance drop is expected as an increased time used to
login a user NOT using a login trigger.
First of all, the method of performance check:
echo 'select 1;' > ./tst.sql
pgbench -n -C -T3 -f tst.sql -U postgres postgres
The output value "average connection time" is the one I use to compare
performance.
Now, what are the results.
* master branch: 0.641 ms
* patched version: 0.644 ms
No significant difference here and it is just what was expected. Based on
the patch design the performance drop can be expected when there are lots
of event triggers created, but not the login one. Thus I have created 1000
drop triggers (the script for creating triggers is attached too) in the
database and repeated the test:
* master branch: 0.646 ms
* patched version: 0.754 ms
For 2000 triggers the patched version connection time is further increased
to 0.862. Thus we have a login time rise in about 16.5% per 1000 event
triggers in the database. It is a statistically noticeable value, still I
don’t think it’s a critical one we should be afraid of.
N.B. The exact values of the login times slightly differ from the ones I
posted in the previous email. Well, that’s the repeatability level we have.
This convinces me even more that the observed level of performance drop is
acceptable.
--
best regards,
Mikhail A. Gribkov

e-mail: youzhick@gmail.com
*http://www.flickr.com/photos/youzhick/albums
<http://www.flickr.com/photos/youzhick/albums&gt;*
http://www.strava.com/athletes/5085772
phone: +7(916)604-71-12
Telegram: @youzhick

--
Regards,
Nikita Malakhov
Postgres Professional
https://postgrespro.ru/

#127Mikhail Gribkov
youzhick@gmail.com
In reply to: Nikita Malakhov (#126)
1 attachment(s)
Re: On login trigger: take three

Hi Nikita,

Mikhail, I've checked the patch and previous discussion,
the condition mentioned earlier is still actual:

Thanks for pointing this out! My bad, I forgot to fix the documentation
example.
Attached v34 has this issue fixed, as well as a couple other problems with
the example code.

--
best regards,
Mikhail A. Gribkov

Attachments:

v34-On_client_login_event_trigger.patchapplication/octet-stream; name=v34-On_client_login_event_trigger.patchDownload
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index f1235a2c9f..3f9110f4e9 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>login</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,18 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>login</literal> event occurs when an authenticated user logs
+     in to the system. Any bugs in a trigger procedure for this event may
+     prevent successful login to the system. Such bugs may be fixed by
+     restarting the system in single-user mode (as event triggers are
+     disabled in this mode). See the <xref linkend="app-postgres"/> reference
+     page for details about using single-user mode.
+     The <literal>login</literal> event will also fire on standby servers.
+     To prevent servers from becoming inaccessible, such triggers must avoid
+     writing anything to the database when running on a standby.
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1299,4 +1312,79 @@ CREATE EVENT TRIGGER no_rewrite_allowed
 </programlisting>
    </para>
  </sect1>
+
+  <sect1 id="event-trigger-database-login-example">
+    <title>A Database Login Event Trigger Example</title>
+
+    <para>
+      The event trigger on the <literal>login</literal> event can be
+      useful for logging user logins, for verifying the connection and
+      assigning roles according to current circumstances, or for session
+      data initialization. It is very important that any event trigger using
+      the <literal>login</literal> event checks whether or not the database is
+      in recovery before performing any writes. Writing to a standby server
+      will make it inaccessible.
+    </para>
+
+    <para>
+      The following example demonstrates these options.
+<programlisting>
+-- create test tables and roles
+CREATE TABLE user_login_log (
+  "user" text,
+  "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+  RETURNS event_trigger SECURITY DEFINER
+  LANGUAGE plpgsql AS
+$$
+DECLARE
+  hour integer = EXTRACT('hour' FROM current_time at time zone 'utc');
+  rec boolean;
+BEGIN
+-- 1. Forbid logging in between 2AM and 4AM.
+IF hour BETWEEN 2 AND 4 THEN
+  RAISE EXCEPTION 'Login forbidden';
+END IF;
+
+-- The checks below cannot be performed on standby servers so
+-- ensure the database is not in recovery before we perform any
+-- operations.
+SELECT pg_is_in_recovery() INTO rec;
+IF rec THEN
+  RETURN;
+END IF;
+
+-- 2. Assign some roles. At daytime, grant the day_worker role, else the
+-- night_worker role.
+IF hour BETWEEN 8 AND 20 THEN
+  EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT day_worker TO ' || quote_ident(session_user);
+ELSE
+  EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT night_worker TO ' || quote_ident(session_user);
+END IF;
+
+-- 3. Initialize user session data
+CREATE TEMP TABLE session_storage (x float, y integer);
+ALTER TABLE session_storage OWNER TO session_user;
+
+-- 4. Log the connection time
+INSERT INTO public.user_login_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+  ON login
+  EXECUTE FUNCTION init_session();
+ALTER EVENT TRIGGER init_session ENABLE ALWAYS;
+</programlisting>
+    </para>
+  </sect1>
 </chapter>
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index a3bdc5db07..6971a5474a 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -37,15 +38,18 @@
 #include "miscadmin.h"
 #include "parser/parse_func.h"
 #include "pgstat.h"
+#include "storage/lmgr.h"
 #include "tcop/deparse_utility.h"
 #include "tcop/utility.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 typedef struct EventTriggerQueryState
@@ -130,6 +134,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "login") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -579,10 +584,15 @@ EventTriggerCommonSetup(Node *parsetree,
 	{
 		CommandTag	dbgtag;
 
-		dbgtag = CreateCommandTag(parsetree);
+		if (event == EVT_Login)
+			dbgtag = CMDTAG_LOGIN;
+		else
+			dbgtag = CreateCommandTag(parsetree);
+
 		if (event == EVT_DDLCommandStart ||
 			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
+			event == EVT_SQLDrop ||
+			event == EVT_Login)
 		{
 			if (!command_tag_event_trigger_ok(dbgtag))
 				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
@@ -601,7 +611,10 @@ EventTriggerCommonSetup(Node *parsetree,
 		return NIL;
 
 	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
+	if (event == EVT_Login)
+		tag = CMDTAG_LOGIN;
+	else
+		tag = CreateCommandTag(parsetree);
 
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
@@ -799,6 +812,46 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Fire login event triggers if any are present.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId))
+		return;
+
+	StartTransactionCommand();
+
+	/* Find login triggers and run what was found */
+	runlist = EventTriggerCommonSetup(NULL,
+										EVT_Login, "login",
+										&trigdata);
+
+	if (runlist != NIL)
+	{
+		/* Event trigger execution may require an active snapshot. */
+		PushActiveSnapshot(GetTransactionSnapshot());
+
+		/* Run the triggers. */
+		EventTriggerInvoke(runlist, &trigdata);
+
+		/* Cleanup. */
+		list_free(runlist);
+
+		PopActiveSnapshot();
+	}
+
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 01d264b5ab..6235269b4b 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -33,6 +33,7 @@
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/prepare.h"
 #include "common/pg_prng.h"
 #include "jit/jit.h"
@@ -4217,6 +4218,9 @@ PostgresMain(const char *dbname, const char *username)
 	initStringInfo(&row_description_buf);
 	MemoryContextSwitchTo(TopMemoryContext);
 
+	/* Fire any defined login event triggers, if appropriate */
+	EventTriggerOnLogin();
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index f7f7165f7f..3e9388e956 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "login") == 0)
+			event = EVT_Login;
 		else
 			continue;
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 2a3921937c..a9f16b5e3a 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3453,8 +3453,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop",
-					  "table_rewrite");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "login",
+					  "sql_drop", "table_rewrite");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 10091c3aaf..c9efee59bf 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -54,6 +54,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnLogin(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 9e94f44c5f..b900227e0c 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
+PG_CMDTAG(CMDTAG_LOGIN, "LOGIN", true, false, false)
 PG_CMDTAG(CMDTAG_MERGE, "MERGE", false, false, true)
 PG_CMDTAG(CMDTAG_MOVE, "MOVE", false, false, true)
 PG_CMDTAG(CMDTAG_NOTIFY, "NOTIFY", false, false, false)
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index ddb67a68fa..a66344aacb 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Login,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 986147b730..8ad50762c4 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,24 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE TABLE user_logins(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO user_logins (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 my $primary_lsn = $node_primary->lsn('write');
 $node_primary->wait_for_catchup($node_standby_1, 'replay', $primary_lsn);
@@ -388,6 +406,11 @@ sub replay_check
 
 replay_check();
 
+my $evttrig = $node_standby_1->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+$evttrig = $node_standby_2->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 5a10958df5..433f5f6e95 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -614,3 +614,34 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     1
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     2
+(1 row)
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 1aeaddbe71..420224f3ba 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -471,3 +471,24 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+SELECT COUNT(*) FROM user_logins;
+\c
+SELECT COUNT(*) FROM user_logins;
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
#128Ted Yu
yuzhihong@gmail.com
In reply to: Mikhail Gribkov (#127)
Re: On login trigger: take three

On Sat, Dec 17, 2022 at 3:46 AM Mikhail Gribkov <youzhick@gmail.com> wrote:

Hi Nikita,

Mikhail, I've checked the patch and previous discussion,
the condition mentioned earlier is still actual:

Thanks for pointing this out! My bad, I forgot to fix the documentation
example.
Attached v34 has this issue fixed, as well as a couple other problems with
the example code.

--
best regards,
Mikhail A. Gribkov

Hi,

bq. in to the system

'in to' -> into

bq. Any bugs in a trigger procedure

Any bugs -> Any bug

+               if (event == EVT_Login)
+                       dbgtag = CMDTAG_LOGIN;
+               else
+                       dbgtag = CreateCommandTag(parsetree);

The same snippet appears more than once. It seems CMDTAG_LOGIN handling can
be moved into `CreateCommandTag`.

Cheers

#129Mikhail Gribkov
youzhick@gmail.com
In reply to: Ted Yu (#128)
1 attachment(s)
Re: On login trigger: take three

Hi Ted,

bq. in to the system
'in to' -> into
bq. Any bugs in a trigger procedure
Any bugs -> Any bug

Thanks! Fixed typos in the attached v35.

+               if (event == EVT_Login)
+                       dbgtag = CMDTAG_LOGIN;
+               else
+                       dbgtag = CreateCommandTag(parsetree);
The same snippet appears more than once. It seems CMDTAG_LOGIN handling

can be moved into `CreateCommandTag`.

It appears twice in fact, both cases are nearby: in the main code and under
the assert-checking ifdef.
Moving CMDTAG_LOGIN to CreateCommandTag would change the CreateCommandTag
function signature and ideology. CreateCommandTag finds a tag based on the
command parse tree, while login event is a specific case when we do not
have any command and the parse tree is NULL. Changing CreateCommandTag
signature for these two calls looks a little bit overkill as it would lead
to changing lots of other places the function is called from.
We could move this snippet to a separate function, but here are the similar
concerns I think: it would make sense for a more common or a more complex
snippet, but not for two cases of if-else one-liners.

--
best regards,
Mikhail A. Gribkov

e-mail: youzhick@gmail.com
*http://www.flickr.com/photos/youzhick/albums
<http://www.flickr.com/photos/youzhick/albums&gt;*
http://www.strava.com/athletes/5085772
phone: +7(916)604-71-12
Telegram: @youzhick

On Sat, Dec 17, 2022 at 3:29 PM Ted Yu <yuzhihong@gmail.com> wrote:

Show quoted text

On Sat, Dec 17, 2022 at 3:46 AM Mikhail Gribkov <youzhick@gmail.com>
wrote:

Hi Nikita,

Mikhail, I've checked the patch and previous discussion,
the condition mentioned earlier is still actual:

Thanks for pointing this out! My bad, I forgot to fix the documentation
example.
Attached v34 has this issue fixed, as well as a couple other problems
with the example code.

--
best regards,
Mikhail A. Gribkov

Hi,

bq. in to the system

'in to' -> into

bq. Any bugs in a trigger procedure

Any bugs -> Any bug

+               if (event == EVT_Login)
+                       dbgtag = CMDTAG_LOGIN;
+               else
+                       dbgtag = CreateCommandTag(parsetree);

The same snippet appears more than once. It seems CMDTAG_LOGIN handling
can be moved into `CreateCommandTag`.

Cheers

Attachments:

v35-On_client_login_event_trigger.patchapplication/octet-stream; name=v35-On_client_login_event_trigger.patchDownload
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index f1235a2c9f..04c77ae0cf 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>login</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,18 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>login</literal> event occurs when an authenticated user logs
+     into the system. Any bug in a trigger procedure for this event may
+     prevent successful login to the system. Such bugs may be fixed by
+     restarting the system in single-user mode (as event triggers are
+     disabled in this mode). See the <xref linkend="app-postgres"/> reference
+     page for details about using single-user mode.
+     The <literal>login</literal> event will also fire on standby servers.
+     To prevent servers from becoming inaccessible, such triggers must avoid
+     writing anything to the database when running on a standby.
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1299,4 +1312,79 @@ CREATE EVENT TRIGGER no_rewrite_allowed
 </programlisting>
    </para>
  </sect1>
+
+  <sect1 id="event-trigger-database-login-example">
+    <title>A Database Login Event Trigger Example</title>
+
+    <para>
+      The event trigger on the <literal>login</literal> event can be
+      useful for logging user logins, for verifying the connection and
+      assigning roles according to current circumstances, or for session
+      data initialization. It is very important that any event trigger using
+      the <literal>login</literal> event checks whether or not the database is
+      in recovery before performing any writes. Writing to a standby server
+      will make it inaccessible.
+    </para>
+
+    <para>
+      The following example demonstrates these options.
+<programlisting>
+-- create test tables and roles
+CREATE TABLE user_login_log (
+  "user" text,
+  "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+  RETURNS event_trigger SECURITY DEFINER
+  LANGUAGE plpgsql AS
+$$
+DECLARE
+  hour integer = EXTRACT('hour' FROM current_time at time zone 'utc');
+  rec boolean;
+BEGIN
+-- 1. Forbid logging in between 2AM and 4AM.
+IF hour BETWEEN 2 AND 4 THEN
+  RAISE EXCEPTION 'Login forbidden';
+END IF;
+
+-- The checks below cannot be performed on standby servers so
+-- ensure the database is not in recovery before we perform any
+-- operations.
+SELECT pg_is_in_recovery() INTO rec;
+IF rec THEN
+  RETURN;
+END IF;
+
+-- 2. Assign some roles. At daytime, grant the day_worker role, else the
+-- night_worker role.
+IF hour BETWEEN 8 AND 20 THEN
+  EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT day_worker TO ' || quote_ident(session_user);
+ELSE
+  EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT night_worker TO ' || quote_ident(session_user);
+END IF;
+
+-- 3. Initialize user session data
+CREATE TEMP TABLE session_storage (x float, y integer);
+ALTER TABLE session_storage OWNER TO session_user;
+
+-- 4. Log the connection time
+INSERT INTO public.user_login_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+  ON login
+  EXECUTE FUNCTION init_session();
+ALTER EVENT TRIGGER init_session ENABLE ALWAYS;
+</programlisting>
+    </para>
+  </sect1>
 </chapter>
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index a3bdc5db07..6971a5474a 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -37,15 +38,18 @@
 #include "miscadmin.h"
 #include "parser/parse_func.h"
 #include "pgstat.h"
+#include "storage/lmgr.h"
 #include "tcop/deparse_utility.h"
 #include "tcop/utility.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 typedef struct EventTriggerQueryState
@@ -130,6 +134,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "login") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -579,10 +584,15 @@ EventTriggerCommonSetup(Node *parsetree,
 	{
 		CommandTag	dbgtag;
 
-		dbgtag = CreateCommandTag(parsetree);
+		if (event == EVT_Login)
+			dbgtag = CMDTAG_LOGIN;
+		else
+			dbgtag = CreateCommandTag(parsetree);
+
 		if (event == EVT_DDLCommandStart ||
 			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
+			event == EVT_SQLDrop ||
+			event == EVT_Login)
 		{
 			if (!command_tag_event_trigger_ok(dbgtag))
 				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
@@ -601,7 +611,10 @@ EventTriggerCommonSetup(Node *parsetree,
 		return NIL;
 
 	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
+	if (event == EVT_Login)
+		tag = CMDTAG_LOGIN;
+	else
+		tag = CreateCommandTag(parsetree);
 
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
@@ -799,6 +812,46 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Fire login event triggers if any are present.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId))
+		return;
+
+	StartTransactionCommand();
+
+	/* Find login triggers and run what was found */
+	runlist = EventTriggerCommonSetup(NULL,
+										EVT_Login, "login",
+										&trigdata);
+
+	if (runlist != NIL)
+	{
+		/* Event trigger execution may require an active snapshot. */
+		PushActiveSnapshot(GetTransactionSnapshot());
+
+		/* Run the triggers. */
+		EventTriggerInvoke(runlist, &trigdata);
+
+		/* Cleanup. */
+		list_free(runlist);
+
+		PopActiveSnapshot();
+	}
+
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 01d264b5ab..6235269b4b 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -33,6 +33,7 @@
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/prepare.h"
 #include "common/pg_prng.h"
 #include "jit/jit.h"
@@ -4217,6 +4218,9 @@ PostgresMain(const char *dbname, const char *username)
 	initStringInfo(&row_description_buf);
 	MemoryContextSwitchTo(TopMemoryContext);
 
+	/* Fire any defined login event triggers, if appropriate */
+	EventTriggerOnLogin();
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index f7f7165f7f..3e9388e956 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "login") == 0)
+			event = EVT_Login;
 		else
 			continue;
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 2a3921937c..a9f16b5e3a 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3453,8 +3453,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop",
-					  "table_rewrite");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "login",
+					  "sql_drop", "table_rewrite");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 10091c3aaf..c9efee59bf 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -54,6 +54,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnLogin(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 9e94f44c5f..b900227e0c 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
+PG_CMDTAG(CMDTAG_LOGIN, "LOGIN", true, false, false)
 PG_CMDTAG(CMDTAG_MERGE, "MERGE", false, false, true)
 PG_CMDTAG(CMDTAG_MOVE, "MOVE", false, false, true)
 PG_CMDTAG(CMDTAG_NOTIFY, "NOTIFY", false, false, false)
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index ddb67a68fa..a66344aacb 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Login,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 986147b730..8ad50762c4 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,24 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE TABLE user_logins(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO user_logins (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 my $primary_lsn = $node_primary->lsn('write');
 $node_primary->wait_for_catchup($node_standby_1, 'replay', $primary_lsn);
@@ -388,6 +406,11 @@ sub replay_check
 
 replay_check();
 
+my $evttrig = $node_standby_1->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+$evttrig = $node_standby_2->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 5a10958df5..433f5f6e95 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -614,3 +614,34 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     1
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     2
+(1 row)
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 1aeaddbe71..420224f3ba 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -471,3 +471,24 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+SELECT COUNT(*) FROM user_logins;
+\c
+SELECT COUNT(*) FROM user_logins;
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
#130Ted Yu
yuzhihong@gmail.com
In reply to: Mikhail Gribkov (#129)
Re: On login trigger: take three

On Mon, Dec 19, 2022 at 1:40 AM Mikhail Gribkov <youzhick@gmail.com> wrote:

Hi Ted,

bq. in to the system
'in to' -> into
bq. Any bugs in a trigger procedure
Any bugs -> Any bug

Thanks! Fixed typos in the attached v35.

+               if (event == EVT_Login)
+                       dbgtag = CMDTAG_LOGIN;
+               else
+                       dbgtag = CreateCommandTag(parsetree);
The same snippet appears more than once. It seems CMDTAG_LOGIN handling

can be moved into `CreateCommandTag`.

It appears twice in fact, both cases are nearby: in the main code and
under the assert-checking ifdef.
Moving CMDTAG_LOGIN to CreateCommandTag would change the CreateCommandTag
function signature and ideology. CreateCommandTag finds a tag based on the
command parse tree, while login event is a specific case when we do not
have any command and the parse tree is NULL. Changing CreateCommandTag
signature for these two calls looks a little bit overkill as it would lead
to changing lots of other places the function is called from.
We could move this snippet to a separate function, but here are the
similar concerns I think: it would make sense for a more common or a more
complex snippet, but not for two cases of if-else one-liners.

--
best regards,
Mikhail A. Gribkov

e-mail: youzhick@gmail.com
*http://www.flickr.com/photos/youzhick/albums
<http://www.flickr.com/photos/youzhick/albums&gt;*
http://www.strava.com/athletes/5085772
phone: +7(916)604-71-12
Telegram: @youzhick

Hi, Mikhail:

Thanks for the explanation.
It is Okay to keep the current formation of CMDTAG_LOGIN handling.

Cheers

#131Pavel Stehule
pavel.stehule@gmail.com
In reply to: Mikhail Gribkov (#129)
Re: On login trigger: take three

Hi

po 19. 12. 2022 v 10:40 odesílatel Mikhail Gribkov <youzhick@gmail.com>
napsal:

Hi Ted,

bq. in to the system
'in to' -> into
bq. Any bugs in a trigger procedure
Any bugs -> Any bug

Thanks! Fixed typos in the attached v35.

+               if (event == EVT_Login)
+                       dbgtag = CMDTAG_LOGIN;
+               else
+                       dbgtag = CreateCommandTag(parsetree);
The same snippet appears more than once. It seems CMDTAG_LOGIN handling

can be moved into `CreateCommandTag`.

It appears twice in fact, both cases are nearby: in the main code and
under the assert-checking ifdef.
Moving CMDTAG_LOGIN to CreateCommandTag would change the CreateCommandTag
function signature and ideology. CreateCommandTag finds a tag based on the
command parse tree, while login event is a specific case when we do not
have any command and the parse tree is NULL. Changing CreateCommandTag
signature for these two calls looks a little bit overkill as it would lead
to changing lots of other places the function is called from.
We could move this snippet to a separate function, but here are the
similar concerns I think: it would make sense for a more common or a more
complex snippet, but not for two cases of if-else one-liners.

I checked this patch and it looks well. All tests passed. Together with
https://commitfest.postgresql.org/41/4013/ it can be a good feature.

I re-tested impact on performance and for the worst case looks like less
than 1% (0.8%). I think it is acceptable. Tested pgbench scenario "SELECT
1"

pgbench -f ~/test.sql -C -c 3 -j 5 -T 100 -P10 postgres

733 tps (master), 727 tps (patched).

I think raising an exception inside should be better tested - not it is
only in 001_stream_rep.pl - generally more tests are welcome - there are no
tested handling exceptions.

Regards

Pavel

Show quoted text

--
best regards,
Mikhail A. Gribkov

e-mail: youzhick@gmail.com
*http://www.flickr.com/photos/youzhick/albums
<http://www.flickr.com/photos/youzhick/albums&gt;*
http://www.strava.com/athletes/5085772
phone: +7(916)604-71-12
Telegram: @youzhick

On Sat, Dec 17, 2022 at 3:29 PM Ted Yu <yuzhihong@gmail.com> wrote:

On Sat, Dec 17, 2022 at 3:46 AM Mikhail Gribkov <youzhick@gmail.com>
wrote:

Hi Nikita,

Mikhail, I've checked the patch and previous discussion,
the condition mentioned earlier is still actual:

Thanks for pointing this out! My bad, I forgot to fix the documentation
example.
Attached v34 has this issue fixed, as well as a couple other problems
with the example code.

--
best regards,
Mikhail A. Gribkov

Hi,

bq. in to the system

'in to' -> into

bq. Any bugs in a trigger procedure

Any bugs -> Any bug

+               if (event == EVT_Login)
+                       dbgtag = CMDTAG_LOGIN;
+               else
+                       dbgtag = CreateCommandTag(parsetree);

The same snippet appears more than once. It seems CMDTAG_LOGIN handling
can be moved into `CreateCommandTag`.

Cheers

#132Mikhail Gribkov
youzhick@gmail.com
In reply to: Pavel Stehule (#131)
1 attachment(s)
Re: On login trigger: take three

Hi Pavel,

Thanks for pointing out the tests. I completely agree that using an
exception inside on-login trigger should be tested. It cannot be done via
regular *.sql/*.out regress tests, thus I have added another perl test to
authentication group doing this.
Attached v36 patch contains this test along with the fresh rebase on master.

--
best regards,
Mikhail A. Gribkov

e-mail: youzhick@gmail.com
*http://www.flickr.com/photos/youzhick/albums
<http://www.flickr.com/photos/youzhick/albums&gt;*
http://www.strava.com/athletes/5085772
phone: +7(916)604-71-12
Telegram: @youzhick

On Thu, Jan 12, 2023 at 9:51 AM Pavel Stehule <pavel.stehule@gmail.com>
wrote:

Show quoted text

Hi

I checked this patch and it looks well. All tests passed. Together with
https://commitfest.postgresql.org/41/4013/ it can be a good feature.

I re-tested impact on performance and for the worst case looks like less
than 1% (0.8%). I think it is acceptable. Tested pgbench scenario "SELECT
1"

pgbench -f ~/test.sql -C -c 3 -j 5 -T 100 -P10 postgres

733 tps (master), 727 tps (patched).

I think raising an exception inside should be better tested - not it is
only in 001_stream_rep.pl - generally more tests are welcome - there are
no tested handling exceptions.

Regards

Pavel

Attachments:

v36-On_client_login_event_trigger.patchapplication/octet-stream; name=v36-On_client_login_event_trigger.patchDownload
commit 8cfb9995cab2946d66369c7dc8d8a779a609bb57
Author: Mikhail Gribkov <m.gribkov@postgrespro.ru>
Commit: Mikhail Gribkov <m.gribkov@postgrespro.ru>

    [PATCH v36] Add support of event triggers on authenticated login
    
    The patch introduces trigger on login event, allowing to fire some actions
    right on the user connection. This can be useful for  logging or connectio
    check purposes as well as for some personalization of environment. Usage
    details are described in the documentation included, but shortly usage is
    the same as for other triggers: create function returning event_trigger and
    then create event trigger on login event.

diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 3b6a5361b3..94d28835d1 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>login</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,18 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>login</literal> event occurs when an authenticated user logs
+     into the system. Any bug in a trigger procedure for this event may
+     prevent successful login to the system. Such bugs may be fixed by
+     restarting the system in single-user mode (as event triggers are
+     disabled in this mode). See the <xref linkend="app-postgres"/> reference
+     page for details about using single-user mode.
+     The <literal>login</literal> event will also fire on standby servers.
+     To prevent servers from becoming inaccessible, such triggers must avoid
+     writing anything to the database when running on a standby.
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1300,4 +1313,79 @@ CREATE EVENT TRIGGER no_rewrite_allowed
 </programlisting>
    </para>
  </sect1>
+
+  <sect1 id="event-trigger-database-login-example">
+    <title>A Database Login Event Trigger Example</title>
+
+    <para>
+      The event trigger on the <literal>login</literal> event can be
+      useful for logging user logins, for verifying the connection and
+      assigning roles according to current circumstances, or for session
+      data initialization. It is very important that any event trigger using
+      the <literal>login</literal> event checks whether or not the database is
+      in recovery before performing any writes. Writing to a standby server
+      will make it inaccessible.
+    </para>
+
+    <para>
+      The following example demonstrates these options.
+<programlisting>
+-- create test tables and roles
+CREATE TABLE user_login_log (
+  "user" text,
+  "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+  RETURNS event_trigger SECURITY DEFINER
+  LANGUAGE plpgsql AS
+$$
+DECLARE
+  hour integer = EXTRACT('hour' FROM current_time at time zone 'utc');
+  rec boolean;
+BEGIN
+-- 1. Forbid logging in between 2AM and 4AM.
+IF hour BETWEEN 2 AND 4 THEN
+  RAISE EXCEPTION 'Login forbidden';
+END IF;
+
+-- The checks below cannot be performed on standby servers so
+-- ensure the database is not in recovery before we perform any
+-- operations.
+SELECT pg_is_in_recovery() INTO rec;
+IF rec THEN
+  RETURN;
+END IF;
+
+-- 2. Assign some roles. At daytime, grant the day_worker role, else the
+-- night_worker role.
+IF hour BETWEEN 8 AND 20 THEN
+  EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT day_worker TO ' || quote_ident(session_user);
+ELSE
+  EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT night_worker TO ' || quote_ident(session_user);
+END IF;
+
+-- 3. Initialize user session data
+CREATE TEMP TABLE session_storage (x float, y integer);
+ALTER TABLE session_storage OWNER TO session_user;
+
+-- 4. Log the connection time
+INSERT INTO public.user_login_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+  ON login
+  EXECUTE FUNCTION init_session();
+ALTER EVENT TRIGGER init_session ENABLE ALWAYS;
+</programlisting>
+    </para>
+  </sect1>
 </chapter>
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index d4b00d1a82..c83c5d78dd 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -37,15 +38,18 @@
 #include "miscadmin.h"
 #include "parser/parse_func.h"
 #include "pgstat.h"
+#include "storage/lmgr.h"
 #include "tcop/deparse_utility.h"
 #include "tcop/utility.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 typedef struct EventTriggerQueryState
@@ -130,6 +134,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "login") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -579,10 +584,15 @@ EventTriggerCommonSetup(Node *parsetree,
 	{
 		CommandTag	dbgtag;
 
-		dbgtag = CreateCommandTag(parsetree);
+		if (event == EVT_Login)
+			dbgtag = CMDTAG_LOGIN;
+		else
+			dbgtag = CreateCommandTag(parsetree);
+
 		if (event == EVT_DDLCommandStart ||
 			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
+			event == EVT_SQLDrop ||
+			event == EVT_Login)
 		{
 			if (!command_tag_event_trigger_ok(dbgtag))
 				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
@@ -601,7 +611,10 @@ EventTriggerCommonSetup(Node *parsetree,
 		return NIL;
 
 	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
+	if (event == EVT_Login)
+		tag = CMDTAG_LOGIN;
+	else
+		tag = CreateCommandTag(parsetree);
 
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
@@ -799,6 +812,46 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Fire login event triggers if any are present.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId))
+		return;
+
+	StartTransactionCommand();
+
+	/* Find login triggers and run what was found */
+	runlist = EventTriggerCommonSetup(NULL,
+										EVT_Login, "login",
+										&trigdata);
+
+	if (runlist != NIL)
+	{
+		/* Event trigger execution may require an active snapshot. */
+		PushActiveSnapshot(GetTransactionSnapshot());
+
+		/* Run the triggers. */
+		EventTriggerInvoke(runlist, &trigdata);
+
+		/* Cleanup. */
+		list_free(runlist);
+
+		PopActiveSnapshot();
+	}
+
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 470b734e9e..d0d234da80 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -32,6 +32,7 @@
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/prepare.h"
 #include "common/pg_prng.h"
 #include "jit/jit.h"
@@ -4219,6 +4220,9 @@ PostgresMain(const char *dbname, const char *username)
 	initStringInfo(&row_description_buf);
 	MemoryContextSwitchTo(TopMemoryContext);
 
+	/* Fire any defined login event triggers, if appropriate */
+	EventTriggerOnLogin();
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 1f5e7eb4c6..9b598276f4 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "login") == 0)
+			event = EVT_Login;
 		else
 			continue;
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 5e1882eaea..baa406cbe7 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3519,8 +3519,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop",
-					  "table_rewrite");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "login",
+					  "sql_drop", "table_rewrite");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 5ed6ece555..bf756d471d 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -54,6 +54,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnLogin(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index e738ac1c09..553a31874f 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
+PG_CMDTAG(CMDTAG_LOGIN, "LOGIN", true, false, false)
 PG_CMDTAG(CMDTAG_MERGE, "MERGE", false, false, true)
 PG_CMDTAG(CMDTAG_MOVE, "MOVE", false, false, true)
 PG_CMDTAG(CMDTAG_NOTIFY, "NOTIFY", false, false, false)
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index d340026518..52052e6252 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Login,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/authentication/t/005_login_trigger.pl b/src/test/authentication/t/005_login_trigger.pl
new file mode 100644
index 0000000000..fec664fb86
--- /dev/null
+++ b/src/test/authentication/t/005_login_trigger.pl
@@ -0,0 +1,157 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+# Tests of authentication via login trigger. Mostly for rejection via
+# exception, because this scenario cannot be covered with *.sql/*.out regress
+# tests.
+
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Execute a psql command and compare its output towards given regexps
+sub psql_command
+{
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+	my ($node, $sql, $expected_ret, $test_name, %params) = @_;
+
+	my $connstr;
+	if (defined($params{connstr}))
+	{
+		$connstr = $params{connstr};
+	}
+	else
+	{
+		$connstr = '';
+	}
+
+	# Execute command
+	my ($ret, $stdout, $stderr) = $node->psql('postgres', $sql,
+											  connstr => "$connstr");
+
+	# Check return code
+	is($ret, $expected_ret,  "$test_name: exit code $expected_ret");
+
+	# Check stdout
+	if (defined($params{log_like}))
+	{
+		my @log_like = @{ $params{log_like} };
+		while (my $regex = shift @log_like)
+		{
+			like($stdout, $regex, "$test_name: log matches");
+		}
+	}
+	if (defined($params{log_unlike}))
+	{
+		my @log_unlike = @{ $params{log_unlike} };
+		while (my $regex = shift @log_unlike)
+		{
+			unlike($stdout, $regex, "$test_name: log unmatches");
+		}
+	}
+	if (defined($params{log_exact}))
+	{
+		is($stdout, $params{log_exact}, "$test_name: log equals");
+	}
+
+	# Check stderr
+	if (defined($params{err_like}))
+	{
+		my @err_like = @{ $params{err_like} };
+		while (my $regex = shift @err_like)
+		{
+			like($stderr, $regex, "$test_name: err matches");
+		}
+	}
+	if (defined($params{err_unlike}))
+	{
+		my @err_unlike = @{ $params{err_unlike} };
+		while (my $regex = shift @err_unlike)
+		{
+			unlike($stderr, $regex, "$test_name: err unmatches");
+		}
+	}
+	if (defined($params{err_exact}))
+	{
+		is($stderr, $params{err_exact}, "$test_name: err equals");
+	}
+
+	return;
+}
+
+# New node
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init(extra => [ '--locale=C', '--encoding=UTF8' ]);
+$node->append_conf(
+	'postgresql.conf', q{
+wal_level = 'logical'
+max_replication_slots = 4
+max_wal_senders = 4
+});
+$node->start;
+
+# Create temporary roles and log table
+psql_command($node, 'CREATE ROLE alice WITH LOGIN;
+CREATE ROLE mallory WITH LOGIN;
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+', 0, 'create tmp objects', log_exact => '', err_exact => ''), ;
+
+# Create login event function and trigger
+psql_command($node,
+			 'CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  IF SESSION_USER = \'mallory\' THEN
+    RAISE EXCEPTION \'Hello %! You are NOT welcome here!\', SESSION_USER;
+  END IF;
+  RAISE NOTICE \'Hello %! You are welcome!\', SESSION_USER;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+', 0, 'create trigger function', log_exact => '', err_exact => '');
+
+psql_command($node,
+			 'CREATE EVENT TRIGGER on_login_trigger '
+			 .'ON login EXECUTE PROCEDURE on_login_proc();', 0,
+			 'create event trigger', log_exact => '', err_exact => '');
+psql_command($node, 'ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;', 0,
+			 'alter event trigger', log_exact => '',
+			 err_like => [qr/You are welcome/]);
+
+# Check the two requests were logged via login trigger
+psql_command($node, 'SELECT COUNT(*) FROM user_logins;', 0, 'select count',
+			 log_exact => '2', err_like => [qr/You are welcome/]);
+
+# Try to log as allowed Alice and disallowed Mallory (two times)
+psql_command($node, 'SELECT 1;', 0, 'try alice', connstr => 'user=alice',
+			 log_exact => '1', err_like => [qr/You are welcome/],
+			 err_unlike => [qr/You are NOT welcome/]);
+psql_command($node, 'SELECT 1;', 2, 'try mallory', connstr => 'user=mallory',
+			 log_exact => '', err_like => [qr/You are NOT welcome/],
+			 err_unlike => [qr/You are welcome/]);
+psql_command($node, 'SELECT 1;', 2, 'try mallory', connstr => 'user=mallory',
+			 log_exact => '', err_like => [qr/You are NOT welcome/],
+			 err_unlike => [qr/You are welcome/]);
+
+# Check that Alice's login record is here, while the Mallory's one is not
+psql_command($node, 'SELECT * FROM user_logins;', 0, 'select *',
+			 log_like => [qr/3\|alice/], log_unlike => [qr/mallory/],
+			 err_like => [qr/You are welcome/]);
+
+# Check total number of successful logins so far
+psql_command($node, 'SELECT COUNT(*) FROM user_logins;', 0, 'select count',
+			 log_exact => '5', err_like => [qr/You are welcome/]);
+
+# Cleanup the temporary stuff
+psql_command($node, 'DROP EVENT TRIGGER on_login_trigger;', 0,
+			 'drop event trigger', log_exact => '',
+			 err_like => [qr/You are welcome/]);
+psql_command($node, 'DROP TABLE user_logins;
+DROP FUNCTION on_login_proc;
+DROP ROLE mallory;
+DROP ROLE alice;
+', 0, 'cleanup', log_exact => '', err_exact => '');
+
+done_testing();
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 23a90dd85b..5a9b667dcd 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,24 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE TABLE user_logins(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO user_logins (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 my $primary_lsn = $node_primary->lsn('write');
 $node_primary->wait_for_catchup($node_standby_1, 'replay', $primary_lsn);
@@ -388,6 +406,11 @@ sub replay_check
 
 replay_check();
 
+my $evttrig = $node_standby_1->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+$evttrig = $node_standby_2->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 5a10958df5..433f5f6e95 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -614,3 +614,34 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     1
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     2
+(1 row)
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 1aeaddbe71..420224f3ba 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -471,3 +471,24 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+SELECT COUNT(*) FROM user_logins;
+\c
+SELECT COUNT(*) FROM user_logins;
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
#133Pavel Stehule
pavel.stehule@gmail.com
In reply to: Mikhail Gribkov (#132)
Re: On login trigger: take three

Hi

so 14. 1. 2023 v 22:56 odesílatel Mikhail Gribkov <youzhick@gmail.com>
napsal:

Hi Pavel,

Thanks for pointing out the tests. I completely agree that using an
exception inside on-login trigger should be tested. It cannot be done via
regular *.sql/*.out regress tests, thus I have added another perl test to
authentication group doing this.
Attached v36 patch contains this test along with the fresh rebase on
master.

--
best regards,
Mikhail A. Gribkov

e-mail: youzhick@gmail.com
*http://www.flickr.com/photos/youzhick/albums
<http://www.flickr.com/photos/youzhick/albums&gt;*
http://www.strava.com/athletes/5085772
phone: +7(916)604-71-12
Telegram: @youzhick

On Thu, Jan 12, 2023 at 9:51 AM Pavel Stehule <pavel.stehule@gmail.com>
wrote:

Hi

I checked this patch and it looks well. All tests passed. Together with
https://commitfest.postgresql.org/41/4013/ it can be a good feature.

I re-tested impact on performance and for the worst case looks like less
than 1% (0.8%). I think it is acceptable. Tested pgbench scenario "SELECT
1"

pgbench -f ~/test.sql -C -c 3 -j 5 -T 100 -P10 postgres

733 tps (master), 727 tps (patched).

I think raising an exception inside should be better tested - not it is
only in 001_stream_rep.pl - generally more tests are welcome - there are
no tested handling exceptions.

Thank you

check-world passed without problems
build doc passed without problems
I think so tests are now enough

I'll mark this patch as ready for committer

Regards

Pavel

Show quoted text

Regards

Pavel

#134Pavel Stehule
pavel.stehule@gmail.com
In reply to: Pavel Stehule (#133)
Re: On login trigger: take three

ne 15. 1. 2023 v 7:32 odesílatel Pavel Stehule <pavel.stehule@gmail.com>
napsal:

Hi

On Thu, Jan 12, 2023 at 9:51 AM Pavel Stehule <pavel.stehule@gmail.com>
wrote:

Hi

I checked this patch and it looks well. All tests passed. Together with
https://commitfest.postgresql.org/41/4013/ it can be a good feature.

I re-tested impact on performance and for the worst case looks like less
than 1% (0.8%). I think it is acceptable. Tested pgbench scenario "SELECT
1"

pgbench -f ~/test.sql -C -c 3 -j 5 -T 100 -P10 postgres

733 tps (master), 727 tps (patched).

I think raising an exception inside should be better tested - not it is
only in 001_stream_rep.pl - generally more tests are welcome - there
are no tested handling exceptions.

Thank you

check-world passed without problems
build doc passed without problems
I think so tests are now enough

I'll mark this patch as ready for committer

Unfortunately, I forgot one important point. There are not any tests
related to backup.

I miss pg_dump related tests.

I mark this patch as waiting on the author.

Regards

Pavel

Show quoted text

Regards

Pavel

Regards

Pavel

#135Mikhail Gribkov
youzhick@gmail.com
In reply to: Pavel Stehule (#134)
1 attachment(s)
Re: On login trigger: take three

Hi Pavel,

On Mon, Jan 16, 2023 at 9:10 AM Pavel Stehule <pavel.stehule@gmail.com>
wrote:

ne 15. 1. 2023 v 7:32 odesílatel Pavel Stehule <pavel.stehule@gmail.com>
napsal:

Hi

On Thu, Jan 12, 2023 at 9:51 AM Pavel Stehule <pavel.stehule@gmail.com>
wrote:

Hi

I checked this patch and it looks well. All tests passed. Together with
https://commitfest.postgresql.org/41/4013/ it can be a good feature.

I re-tested impact on performance and for the worst case looks like
less than 1% (0.8%). I think it is acceptable. Tested pgbench scenario
"SELECT 1"

pgbench -f ~/test.sql -C -c 3 -j 5 -T 100 -P10 postgres

733 tps (master), 727 tps (patched).

I think raising an exception inside should be better tested - not it is
only in 001_stream_rep.pl - generally more tests are welcome - there
are no tested handling exceptions.

Thank you

check-world passed without problems
build doc passed without problems
I think so tests are now enough

I'll mark this patch as ready for committer

Unfortunately, I forgot one important point. There are not any tests
related to backup.

I miss pg_dump related tests.

I mark this patch as waiting on the author.

Thanks for noticing this.
I have added sections to pg_dump tests. Attached v37 patch contains these
additions along with the fresh rebase on master.

--
best regards,
Mikhail A. Gribkov

e-mail: youzhick@gmail.com
*http://www.flickr.com/photos/youzhick/albums
<http://www.flickr.com/photos/youzhick/albums&gt;*
http://www.strava.com/athletes/5085772
phone: +7(916)604-71-12
Telegram: @youzhick

Show quoted text

Attachments:

v37-On_client_login_event_trigger.patchapplication/octet-stream; name=v37-On_client_login_event_trigger.patchDownload
commit 47c3aea46963dcad3d3b1b22c696ff3805b8e302
Author: Mikhail Gribkov <m.gribkov@postgrespro.ru>
Commit: Mikhail Gribkov <m.gribkov@postgrespro.ru>

    [PATCH v37] Add support of event triggers on authenticated login
    
    The patch introduces trigger on login event, allowing to fire some actions
    right on the user connection. This can be useful for  logging or connection
    check purposes as well as for some personalization of environment. Usage
    details are described in the documentation included, but shortly usage is
    the same as for other triggers: create function returning event_trigger and
    then create event trigger on login event.

diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 3b6a5361b3..94d28835d1 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>login</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,18 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>login</literal> event occurs when an authenticated user logs
+     into the system. Any bug in a trigger procedure for this event may
+     prevent successful login to the system. Such bugs may be fixed by
+     restarting the system in single-user mode (as event triggers are
+     disabled in this mode). See the <xref linkend="app-postgres"/> reference
+     page for details about using single-user mode.
+     The <literal>login</literal> event will also fire on standby servers.
+     To prevent servers from becoming inaccessible, such triggers must avoid
+     writing anything to the database when running on a standby.
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1300,4 +1313,79 @@ CREATE EVENT TRIGGER no_rewrite_allowed
 </programlisting>
    </para>
  </sect1>
+
+  <sect1 id="event-trigger-database-login-example">
+    <title>A Database Login Event Trigger Example</title>
+
+    <para>
+      The event trigger on the <literal>login</literal> event can be
+      useful for logging user logins, for verifying the connection and
+      assigning roles according to current circumstances, or for session
+      data initialization. It is very important that any event trigger using
+      the <literal>login</literal> event checks whether or not the database is
+      in recovery before performing any writes. Writing to a standby server
+      will make it inaccessible.
+    </para>
+
+    <para>
+      The following example demonstrates these options.
+<programlisting>
+-- create test tables and roles
+CREATE TABLE user_login_log (
+  "user" text,
+  "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+  RETURNS event_trigger SECURITY DEFINER
+  LANGUAGE plpgsql AS
+$$
+DECLARE
+  hour integer = EXTRACT('hour' FROM current_time at time zone 'utc');
+  rec boolean;
+BEGIN
+-- 1. Forbid logging in between 2AM and 4AM.
+IF hour BETWEEN 2 AND 4 THEN
+  RAISE EXCEPTION 'Login forbidden';
+END IF;
+
+-- The checks below cannot be performed on standby servers so
+-- ensure the database is not in recovery before we perform any
+-- operations.
+SELECT pg_is_in_recovery() INTO rec;
+IF rec THEN
+  RETURN;
+END IF;
+
+-- 2. Assign some roles. At daytime, grant the day_worker role, else the
+-- night_worker role.
+IF hour BETWEEN 8 AND 20 THEN
+  EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT day_worker TO ' || quote_ident(session_user);
+ELSE
+  EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT night_worker TO ' || quote_ident(session_user);
+END IF;
+
+-- 3. Initialize user session data
+CREATE TEMP TABLE session_storage (x float, y integer);
+ALTER TABLE session_storage OWNER TO session_user;
+
+-- 4. Log the connection time
+INSERT INTO public.user_login_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+  ON login
+  EXECUTE FUNCTION init_session();
+ALTER EVENT TRIGGER init_session ENABLE ALWAYS;
+</programlisting>
+    </para>
+  </sect1>
 </chapter>
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index d4b00d1a82..c83c5d78dd 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -37,15 +38,18 @@
 #include "miscadmin.h"
 #include "parser/parse_func.h"
 #include "pgstat.h"
+#include "storage/lmgr.h"
 #include "tcop/deparse_utility.h"
 #include "tcop/utility.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 typedef struct EventTriggerQueryState
@@ -130,6 +134,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "login") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -579,10 +584,15 @@ EventTriggerCommonSetup(Node *parsetree,
 	{
 		CommandTag	dbgtag;
 
-		dbgtag = CreateCommandTag(parsetree);
+		if (event == EVT_Login)
+			dbgtag = CMDTAG_LOGIN;
+		else
+			dbgtag = CreateCommandTag(parsetree);
+
 		if (event == EVT_DDLCommandStart ||
 			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
+			event == EVT_SQLDrop ||
+			event == EVT_Login)
 		{
 			if (!command_tag_event_trigger_ok(dbgtag))
 				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
@@ -601,7 +611,10 @@ EventTriggerCommonSetup(Node *parsetree,
 		return NIL;
 
 	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
+	if (event == EVT_Login)
+		tag = CMDTAG_LOGIN;
+	else
+		tag = CreateCommandTag(parsetree);
 
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
@@ -799,6 +812,46 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Fire login event triggers if any are present.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId))
+		return;
+
+	StartTransactionCommand();
+
+	/* Find login triggers and run what was found */
+	runlist = EventTriggerCommonSetup(NULL,
+										EVT_Login, "login",
+										&trigdata);
+
+	if (runlist != NIL)
+	{
+		/* Event trigger execution may require an active snapshot. */
+		PushActiveSnapshot(GetTransactionSnapshot());
+
+		/* Run the triggers. */
+		EventTriggerInvoke(runlist, &trigdata);
+
+		/* Cleanup. */
+		list_free(runlist);
+
+		PopActiveSnapshot();
+	}
+
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 470b734e9e..d0d234da80 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -32,6 +32,7 @@
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/prepare.h"
 #include "common/pg_prng.h"
 #include "jit/jit.h"
@@ -4219,6 +4220,9 @@ PostgresMain(const char *dbname, const char *username)
 	initStringInfo(&row_description_buf);
 	MemoryContextSwitchTo(TopMemoryContext);
 
+	/* Fire any defined login event triggers, if appropriate */
+	EventTriggerOnLogin();
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 1f5e7eb4c6..9b598276f4 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "login") == 0)
+			event = EVT_Login;
 		else
 			continue;
 
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index d92247c915..2d58638b04 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -1807,6 +1807,22 @@ my %tests = (
 		unlike => { exclude_dump_test_schema => 1, },
 	},
 
+	'CREATE FUNCTION dump_test.login_event_trigger_func' => {
+		create_order => 32,
+		create_sql   => 'CREATE FUNCTION dump_test.login_event_trigger_func()
+					   RETURNS event_trigger LANGUAGE plpgsql
+					   AS $$ BEGIN RETURN; END;$$;',
+		regexp => qr/^
+			\QCREATE FUNCTION dump_test.login_event_trigger_func() RETURNS event_trigger\E
+			\n\s+\QLANGUAGE plpgsql\E
+			\n\s+AS\ \$\$
+			\Q BEGIN RETURN; END;\E
+			\$\$;/xm,
+		like =>
+		  { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
 	'CREATE OPERATOR FAMILY dump_test.op_family' => {
 		create_order => 73,
 		create_sql =>
@@ -1907,6 +1923,19 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE EVENT TRIGGER test_login_event_trigger' => {
+		create_order => 33,
+		create_sql   => 'CREATE EVENT TRIGGER test_login_event_trigger
+					   ON login
+					   EXECUTE FUNCTION dump_test.login_event_trigger_func();',
+		regexp => qr/^
+			\QCREATE EVENT TRIGGER test_login_event_trigger \E
+			\QON login\E
+			\n\s+\QEXECUTE FUNCTION dump_test.login_event_trigger_func();\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE TRIGGER test_trigger' => {
 		create_order => 31,
 		create_sql   => 'CREATE TRIGGER test_trigger
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 5e1882eaea..baa406cbe7 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3519,8 +3519,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop",
-					  "table_rewrite");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "login",
+					  "sql_drop", "table_rewrite");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 5ed6ece555..bf756d471d 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -54,6 +54,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnLogin(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index e738ac1c09..553a31874f 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
+PG_CMDTAG(CMDTAG_LOGIN, "LOGIN", true, false, false)
 PG_CMDTAG(CMDTAG_MERGE, "MERGE", false, false, true)
 PG_CMDTAG(CMDTAG_MOVE, "MOVE", false, false, true)
 PG_CMDTAG(CMDTAG_NOTIFY, "NOTIFY", false, false, false)
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index d340026518..52052e6252 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Login,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/authentication/t/005_login_trigger.pl b/src/test/authentication/t/005_login_trigger.pl
new file mode 100644
index 0000000000..fec664fb86
--- /dev/null
+++ b/src/test/authentication/t/005_login_trigger.pl
@@ -0,0 +1,157 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+# Tests of authentication via login trigger. Mostly for rejection via
+# exception, because this scenario cannot be covered with *.sql/*.out regress
+# tests.
+
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Execute a psql command and compare its output towards given regexps
+sub psql_command
+{
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+	my ($node, $sql, $expected_ret, $test_name, %params) = @_;
+
+	my $connstr;
+	if (defined($params{connstr}))
+	{
+		$connstr = $params{connstr};
+	}
+	else
+	{
+		$connstr = '';
+	}
+
+	# Execute command
+	my ($ret, $stdout, $stderr) = $node->psql('postgres', $sql,
+											  connstr => "$connstr");
+
+	# Check return code
+	is($ret, $expected_ret,  "$test_name: exit code $expected_ret");
+
+	# Check stdout
+	if (defined($params{log_like}))
+	{
+		my @log_like = @{ $params{log_like} };
+		while (my $regex = shift @log_like)
+		{
+			like($stdout, $regex, "$test_name: log matches");
+		}
+	}
+	if (defined($params{log_unlike}))
+	{
+		my @log_unlike = @{ $params{log_unlike} };
+		while (my $regex = shift @log_unlike)
+		{
+			unlike($stdout, $regex, "$test_name: log unmatches");
+		}
+	}
+	if (defined($params{log_exact}))
+	{
+		is($stdout, $params{log_exact}, "$test_name: log equals");
+	}
+
+	# Check stderr
+	if (defined($params{err_like}))
+	{
+		my @err_like = @{ $params{err_like} };
+		while (my $regex = shift @err_like)
+		{
+			like($stderr, $regex, "$test_name: err matches");
+		}
+	}
+	if (defined($params{err_unlike}))
+	{
+		my @err_unlike = @{ $params{err_unlike} };
+		while (my $regex = shift @err_unlike)
+		{
+			unlike($stderr, $regex, "$test_name: err unmatches");
+		}
+	}
+	if (defined($params{err_exact}))
+	{
+		is($stderr, $params{err_exact}, "$test_name: err equals");
+	}
+
+	return;
+}
+
+# New node
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init(extra => [ '--locale=C', '--encoding=UTF8' ]);
+$node->append_conf(
+	'postgresql.conf', q{
+wal_level = 'logical'
+max_replication_slots = 4
+max_wal_senders = 4
+});
+$node->start;
+
+# Create temporary roles and log table
+psql_command($node, 'CREATE ROLE alice WITH LOGIN;
+CREATE ROLE mallory WITH LOGIN;
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+', 0, 'create tmp objects', log_exact => '', err_exact => ''), ;
+
+# Create login event function and trigger
+psql_command($node,
+			 'CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  IF SESSION_USER = \'mallory\' THEN
+    RAISE EXCEPTION \'Hello %! You are NOT welcome here!\', SESSION_USER;
+  END IF;
+  RAISE NOTICE \'Hello %! You are welcome!\', SESSION_USER;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+', 0, 'create trigger function', log_exact => '', err_exact => '');
+
+psql_command($node,
+			 'CREATE EVENT TRIGGER on_login_trigger '
+			 .'ON login EXECUTE PROCEDURE on_login_proc();', 0,
+			 'create event trigger', log_exact => '', err_exact => '');
+psql_command($node, 'ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;', 0,
+			 'alter event trigger', log_exact => '',
+			 err_like => [qr/You are welcome/]);
+
+# Check the two requests were logged via login trigger
+psql_command($node, 'SELECT COUNT(*) FROM user_logins;', 0, 'select count',
+			 log_exact => '2', err_like => [qr/You are welcome/]);
+
+# Try to log as allowed Alice and disallowed Mallory (two times)
+psql_command($node, 'SELECT 1;', 0, 'try alice', connstr => 'user=alice',
+			 log_exact => '1', err_like => [qr/You are welcome/],
+			 err_unlike => [qr/You are NOT welcome/]);
+psql_command($node, 'SELECT 1;', 2, 'try mallory', connstr => 'user=mallory',
+			 log_exact => '', err_like => [qr/You are NOT welcome/],
+			 err_unlike => [qr/You are welcome/]);
+psql_command($node, 'SELECT 1;', 2, 'try mallory', connstr => 'user=mallory',
+			 log_exact => '', err_like => [qr/You are NOT welcome/],
+			 err_unlike => [qr/You are welcome/]);
+
+# Check that Alice's login record is here, while the Mallory's one is not
+psql_command($node, 'SELECT * FROM user_logins;', 0, 'select *',
+			 log_like => [qr/3\|alice/], log_unlike => [qr/mallory/],
+			 err_like => [qr/You are welcome/]);
+
+# Check total number of successful logins so far
+psql_command($node, 'SELECT COUNT(*) FROM user_logins;', 0, 'select count',
+			 log_exact => '5', err_like => [qr/You are welcome/]);
+
+# Cleanup the temporary stuff
+psql_command($node, 'DROP EVENT TRIGGER on_login_trigger;', 0,
+			 'drop event trigger', log_exact => '',
+			 err_like => [qr/You are welcome/]);
+psql_command($node, 'DROP TABLE user_logins;
+DROP FUNCTION on_login_proc;
+DROP ROLE mallory;
+DROP ROLE alice;
+', 0, 'cleanup', log_exact => '', err_exact => '');
+
+done_testing();
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 23a90dd85b..5a9b667dcd 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,24 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE TABLE user_logins(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO user_logins (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 my $primary_lsn = $node_primary->lsn('write');
 $node_primary->wait_for_catchup($node_standby_1, 'replay', $primary_lsn);
@@ -388,6 +406,11 @@ sub replay_check
 
 replay_check();
 
+my $evttrig = $node_standby_1->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+$evttrig = $node_standby_2->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 5a10958df5..433f5f6e95 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -614,3 +614,34 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     1
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     2
+(1 row)
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 1aeaddbe71..420224f3ba 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -471,3 +471,24 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+SELECT COUNT(*) FROM user_logins;
+\c
+SELECT COUNT(*) FROM user_logins;
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
#136Pavel Stehule
pavel.stehule@gmail.com
In reply to: Mikhail Gribkov (#135)
Re: On login trigger: take three

pá 20. 1. 2023 v 19:46 odesílatel Mikhail Gribkov <youzhick@gmail.com>
napsal:

Hi Pavel,

On Mon, Jan 16, 2023 at 9:10 AM Pavel Stehule <pavel.stehule@gmail.com>
wrote:

ne 15. 1. 2023 v 7:32 odesílatel Pavel Stehule <pavel.stehule@gmail.com>
napsal:

Hi

On Thu, Jan 12, 2023 at 9:51 AM Pavel Stehule <pavel.stehule@gmail.com>
wrote:

Hi

I checked this patch and it looks well. All tests passed. Together
with https://commitfest.postgresql.org/41/4013/ it can be a good
feature.

I re-tested impact on performance and for the worst case looks like
less than 1% (0.8%). I think it is acceptable. Tested pgbench scenario
"SELECT 1"

pgbench -f ~/test.sql -C -c 3 -j 5 -T 100 -P10 postgres

733 tps (master), 727 tps (patched).

I think raising an exception inside should be better tested - not it
is only in 001_stream_rep.pl - generally more tests are welcome -
there are no tested handling exceptions.

Thank you

check-world passed without problems
build doc passed without problems
I think so tests are now enough

I'll mark this patch as ready for committer

Unfortunately, I forgot one important point. There are not any tests
related to backup.

I miss pg_dump related tests.

I mark this patch as waiting on the author.

Thanks for noticing this.
I have added sections to pg_dump tests. Attached v37 patch contains these
additions along with the fresh rebase on master.

Thank you

marked as ready for committer

Regards

Pavel

Show quoted text

--
best regards,
Mikhail A. Gribkov

e-mail: youzhick@gmail.com
*http://www.flickr.com/photos/youzhick/albums
<http://www.flickr.com/photos/youzhick/albums&gt;*
http://www.strava.com/athletes/5085772
phone: +7(916)604-71-12
Telegram: @youzhick

#137Mikhail Gribkov
youzhick@gmail.com
In reply to: Pavel Stehule (#136)
1 attachment(s)
Re: On login trigger: take three

Hi hackers,

The attached v38 patch is a fresh rebase on master branch.
Nothing has changed beyond rebasing.

And just for convenience, here is a link to the exact message of the thread
describing the current approach:
/messages/by-id/CAMEv5_vg4aJOoUC74XJm+5B7+TF1nT-Yhtg+awtBOESXG5Grfg@mail.gmail.com

--
best regards,
Mikhail A. Gribkov

e-mail: youzhick@gmail.com
*http://www.flickr.com/photos/youzhick/albums
<http://www.flickr.com/photos/youzhick/albums&gt;*
http://www.strava.com/athletes/5085772
phone: +7(916)604-71-12
Telegram: @youzhick

Show quoted text

Thank you

marked as ready for committer

Regards

Pavel

Attachments:

v38-On_client_login_event_trigger.patchapplication/octet-stream; name=v38-On_client_login_event_trigger.patchDownload
commit 4572dbe250f3afd2a797c4d96518193503c1023a
Author: Mikhail Gribkov <m.gribkov@postgrespro.ru>
Commit: Mikhail Gribkov <m.gribkov@postgrespro.ru>

    [PATCH v38] Add support of event triggers on authenticated login
    
    The patch introduces trigger on login event, allowing to fire some actions
    right on the user connection. This can be useful for  logging or connection
    check purposes as well as for some personalization of environment. Usage
    details are described in the documentation included, but shortly usage is
    the same as for other triggers: create function returning event_trigger and
    then create event trigger on login event.

diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 3b6a5361b3..94d28835d1 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>login</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,18 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>login</literal> event occurs when an authenticated user logs
+     into the system. Any bug in a trigger procedure for this event may
+     prevent successful login to the system. Such bugs may be fixed by
+     restarting the system in single-user mode (as event triggers are
+     disabled in this mode). See the <xref linkend="app-postgres"/> reference
+     page for details about using single-user mode.
+     The <literal>login</literal> event will also fire on standby servers.
+     To prevent servers from becoming inaccessible, such triggers must avoid
+     writing anything to the database when running on a standby.
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1300,4 +1313,79 @@ CREATE EVENT TRIGGER no_rewrite_allowed
 </programlisting>
    </para>
  </sect1>
+
+  <sect1 id="event-trigger-database-login-example">
+    <title>A Database Login Event Trigger Example</title>
+
+    <para>
+      The event trigger on the <literal>login</literal> event can be
+      useful for logging user logins, for verifying the connection and
+      assigning roles according to current circumstances, or for session
+      data initialization. It is very important that any event trigger using
+      the <literal>login</literal> event checks whether or not the database is
+      in recovery before performing any writes. Writing to a standby server
+      will make it inaccessible.
+    </para>
+
+    <para>
+      The following example demonstrates these options.
+<programlisting>
+-- create test tables and roles
+CREATE TABLE user_login_log (
+  "user" text,
+  "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+  RETURNS event_trigger SECURITY DEFINER
+  LANGUAGE plpgsql AS
+$$
+DECLARE
+  hour integer = EXTRACT('hour' FROM current_time at time zone 'utc');
+  rec boolean;
+BEGIN
+-- 1. Forbid logging in between 2AM and 4AM.
+IF hour BETWEEN 2 AND 4 THEN
+  RAISE EXCEPTION 'Login forbidden';
+END IF;
+
+-- The checks below cannot be performed on standby servers so
+-- ensure the database is not in recovery before we perform any
+-- operations.
+SELECT pg_is_in_recovery() INTO rec;
+IF rec THEN
+  RETURN;
+END IF;
+
+-- 2. Assign some roles. At daytime, grant the day_worker role, else the
+-- night_worker role.
+IF hour BETWEEN 8 AND 20 THEN
+  EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT day_worker TO ' || quote_ident(session_user);
+ELSE
+  EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT night_worker TO ' || quote_ident(session_user);
+END IF;
+
+-- 3. Initialize user session data
+CREATE TEMP TABLE session_storage (x float, y integer);
+ALTER TABLE session_storage OWNER TO session_user;
+
+-- 4. Log the connection time
+INSERT INTO public.user_login_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+  ON login
+  EXECUTE FUNCTION init_session();
+ALTER EVENT TRIGGER init_session ENABLE ALWAYS;
+</programlisting>
+    </para>
+  </sect1>
 </chapter>
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index d4b00d1a82..c83c5d78dd 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -37,15 +38,18 @@
 #include "miscadmin.h"
 #include "parser/parse_func.h"
 #include "pgstat.h"
+#include "storage/lmgr.h"
 #include "tcop/deparse_utility.h"
 #include "tcop/utility.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 typedef struct EventTriggerQueryState
@@ -130,6 +134,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "login") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -579,10 +584,15 @@ EventTriggerCommonSetup(Node *parsetree,
 	{
 		CommandTag	dbgtag;
 
-		dbgtag = CreateCommandTag(parsetree);
+		if (event == EVT_Login)
+			dbgtag = CMDTAG_LOGIN;
+		else
+			dbgtag = CreateCommandTag(parsetree);
+
 		if (event == EVT_DDLCommandStart ||
 			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
+			event == EVT_SQLDrop ||
+			event == EVT_Login)
 		{
 			if (!command_tag_event_trigger_ok(dbgtag))
 				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
@@ -601,7 +611,10 @@ EventTriggerCommonSetup(Node *parsetree,
 		return NIL;
 
 	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
+	if (event == EVT_Login)
+		tag = CMDTAG_LOGIN;
+	else
+		tag = CreateCommandTag(parsetree);
 
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
@@ -799,6 +812,46 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Fire login event triggers if any are present.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId))
+		return;
+
+	StartTransactionCommand();
+
+	/* Find login triggers and run what was found */
+	runlist = EventTriggerCommonSetup(NULL,
+										EVT_Login, "login",
+										&trigdata);
+
+	if (runlist != NIL)
+	{
+		/* Event trigger execution may require an active snapshot. */
+		PushActiveSnapshot(GetTransactionSnapshot());
+
+		/* Run the triggers. */
+		EventTriggerInvoke(runlist, &trigdata);
+
+		/* Cleanup. */
+		list_free(runlist);
+
+		PopActiveSnapshot();
+	}
+
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index cab709b07b..33a4dfeaa2 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -32,6 +32,7 @@
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/prepare.h"
 #include "common/pg_prng.h"
 #include "jit/jit.h"
@@ -4226,6 +4227,9 @@ PostgresMain(const char *dbname, const char *username)
 	initStringInfo(&row_description_buf);
 	MemoryContextSwitchTo(TopMemoryContext);
 
+	/* Fire any defined login event triggers, if appropriate */
+	EventTriggerOnLogin();
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 1f5e7eb4c6..9b598276f4 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "login") == 0)
+			event = EVT_Login;
 		else
 			continue;
 
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 72b19ee6cd..fc9d269224 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -1881,6 +1881,22 @@ my %tests = (
 		unlike => { exclude_dump_test_schema => 1, },
 	},
 
+	'CREATE FUNCTION dump_test.login_event_trigger_func' => {
+		create_order => 32,
+		create_sql   => 'CREATE FUNCTION dump_test.login_event_trigger_func()
+					   RETURNS event_trigger LANGUAGE plpgsql
+					   AS $$ BEGIN RETURN; END;$$;',
+		regexp => qr/^
+			\QCREATE FUNCTION dump_test.login_event_trigger_func() RETURNS event_trigger\E
+			\n\s+\QLANGUAGE plpgsql\E
+			\n\s+AS\ \$\$
+			\Q BEGIN RETURN; END;\E
+			\$\$;/xm,
+		like =>
+		  { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
 	'CREATE OPERATOR FAMILY dump_test.op_family' => {
 		create_order => 73,
 		create_sql =>
@@ -1981,6 +1997,19 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE EVENT TRIGGER test_login_event_trigger' => {
+		create_order => 33,
+		create_sql   => 'CREATE EVENT TRIGGER test_login_event_trigger
+					   ON login
+					   EXECUTE FUNCTION dump_test.login_event_trigger_func();',
+		regexp => qr/^
+			\QCREATE EVENT TRIGGER test_login_event_trigger \E
+			\QON login\E
+			\n\s+\QEXECUTE FUNCTION dump_test.login_event_trigger_func();\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE TRIGGER test_trigger' => {
 		create_order => 31,
 		create_sql   => 'CREATE TRIGGER test_trigger
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 5e1882eaea..baa406cbe7 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3519,8 +3519,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop",
-					  "table_rewrite");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "login",
+					  "sql_drop", "table_rewrite");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 5ed6ece555..bf756d471d 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -54,6 +54,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnLogin(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index e738ac1c09..553a31874f 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
+PG_CMDTAG(CMDTAG_LOGIN, "LOGIN", true, false, false)
 PG_CMDTAG(CMDTAG_MERGE, "MERGE", false, false, true)
 PG_CMDTAG(CMDTAG_MOVE, "MOVE", false, false, true)
 PG_CMDTAG(CMDTAG_NOTIFY, "NOTIFY", false, false, false)
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index d340026518..52052e6252 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Login,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/authentication/t/005_login_trigger.pl b/src/test/authentication/t/005_login_trigger.pl
new file mode 100644
index 0000000000..fec664fb86
--- /dev/null
+++ b/src/test/authentication/t/005_login_trigger.pl
@@ -0,0 +1,157 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+# Tests of authentication via login trigger. Mostly for rejection via
+# exception, because this scenario cannot be covered with *.sql/*.out regress
+# tests.
+
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Execute a psql command and compare its output towards given regexps
+sub psql_command
+{
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+	my ($node, $sql, $expected_ret, $test_name, %params) = @_;
+
+	my $connstr;
+	if (defined($params{connstr}))
+	{
+		$connstr = $params{connstr};
+	}
+	else
+	{
+		$connstr = '';
+	}
+
+	# Execute command
+	my ($ret, $stdout, $stderr) = $node->psql('postgres', $sql,
+											  connstr => "$connstr");
+
+	# Check return code
+	is($ret, $expected_ret,  "$test_name: exit code $expected_ret");
+
+	# Check stdout
+	if (defined($params{log_like}))
+	{
+		my @log_like = @{ $params{log_like} };
+		while (my $regex = shift @log_like)
+		{
+			like($stdout, $regex, "$test_name: log matches");
+		}
+	}
+	if (defined($params{log_unlike}))
+	{
+		my @log_unlike = @{ $params{log_unlike} };
+		while (my $regex = shift @log_unlike)
+		{
+			unlike($stdout, $regex, "$test_name: log unmatches");
+		}
+	}
+	if (defined($params{log_exact}))
+	{
+		is($stdout, $params{log_exact}, "$test_name: log equals");
+	}
+
+	# Check stderr
+	if (defined($params{err_like}))
+	{
+		my @err_like = @{ $params{err_like} };
+		while (my $regex = shift @err_like)
+		{
+			like($stderr, $regex, "$test_name: err matches");
+		}
+	}
+	if (defined($params{err_unlike}))
+	{
+		my @err_unlike = @{ $params{err_unlike} };
+		while (my $regex = shift @err_unlike)
+		{
+			unlike($stderr, $regex, "$test_name: err unmatches");
+		}
+	}
+	if (defined($params{err_exact}))
+	{
+		is($stderr, $params{err_exact}, "$test_name: err equals");
+	}
+
+	return;
+}
+
+# New node
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init(extra => [ '--locale=C', '--encoding=UTF8' ]);
+$node->append_conf(
+	'postgresql.conf', q{
+wal_level = 'logical'
+max_replication_slots = 4
+max_wal_senders = 4
+});
+$node->start;
+
+# Create temporary roles and log table
+psql_command($node, 'CREATE ROLE alice WITH LOGIN;
+CREATE ROLE mallory WITH LOGIN;
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+', 0, 'create tmp objects', log_exact => '', err_exact => ''), ;
+
+# Create login event function and trigger
+psql_command($node,
+			 'CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  IF SESSION_USER = \'mallory\' THEN
+    RAISE EXCEPTION \'Hello %! You are NOT welcome here!\', SESSION_USER;
+  END IF;
+  RAISE NOTICE \'Hello %! You are welcome!\', SESSION_USER;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+', 0, 'create trigger function', log_exact => '', err_exact => '');
+
+psql_command($node,
+			 'CREATE EVENT TRIGGER on_login_trigger '
+			 .'ON login EXECUTE PROCEDURE on_login_proc();', 0,
+			 'create event trigger', log_exact => '', err_exact => '');
+psql_command($node, 'ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;', 0,
+			 'alter event trigger', log_exact => '',
+			 err_like => [qr/You are welcome/]);
+
+# Check the two requests were logged via login trigger
+psql_command($node, 'SELECT COUNT(*) FROM user_logins;', 0, 'select count',
+			 log_exact => '2', err_like => [qr/You are welcome/]);
+
+# Try to log as allowed Alice and disallowed Mallory (two times)
+psql_command($node, 'SELECT 1;', 0, 'try alice', connstr => 'user=alice',
+			 log_exact => '1', err_like => [qr/You are welcome/],
+			 err_unlike => [qr/You are NOT welcome/]);
+psql_command($node, 'SELECT 1;', 2, 'try mallory', connstr => 'user=mallory',
+			 log_exact => '', err_like => [qr/You are NOT welcome/],
+			 err_unlike => [qr/You are welcome/]);
+psql_command($node, 'SELECT 1;', 2, 'try mallory', connstr => 'user=mallory',
+			 log_exact => '', err_like => [qr/You are NOT welcome/],
+			 err_unlike => [qr/You are welcome/]);
+
+# Check that Alice's login record is here, while the Mallory's one is not
+psql_command($node, 'SELECT * FROM user_logins;', 0, 'select *',
+			 log_like => [qr/3\|alice/], log_unlike => [qr/mallory/],
+			 err_like => [qr/You are welcome/]);
+
+# Check total number of successful logins so far
+psql_command($node, 'SELECT COUNT(*) FROM user_logins;', 0, 'select count',
+			 log_exact => '5', err_like => [qr/You are welcome/]);
+
+# Cleanup the temporary stuff
+psql_command($node, 'DROP EVENT TRIGGER on_login_trigger;', 0,
+			 'drop event trigger', log_exact => '',
+			 err_like => [qr/You are welcome/]);
+psql_command($node, 'DROP TABLE user_logins;
+DROP FUNCTION on_login_proc;
+DROP ROLE mallory;
+DROP ROLE alice;
+', 0, 'cleanup', log_exact => '', err_exact => '');
+
+done_testing();
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 76846905a7..12e365aac8 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,24 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE TABLE user_logins(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO user_logins (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 $node_primary->wait_for_replay_catchup($node_standby_1);
 $node_standby_1->wait_for_replay_catchup($node_standby_2, $node_primary);
@@ -384,6 +402,11 @@ sub replay_check
 
 replay_check();
 
+my $evttrig = $node_standby_1->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+$evttrig = $node_standby_2->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 5a10958df5..433f5f6e95 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -614,3 +614,34 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     1
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     2
+(1 row)
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 1aeaddbe71..420224f3ba 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -471,3 +471,24 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+SELECT COUNT(*) FROM user_logins;
+\c
+SELECT COUNT(*) FROM user_logins;
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
#138Gregory Stark (as CFM)
stark.cfm@gmail.com
In reply to: Mikhail Gribkov (#137)
Re: On login trigger: take three

It looks like Daniel Gustafsson, Andres, and Tom have all weighed in
on this patch with at least a neutral comment (+-0 from Andres :)

It looks like the main concern was breaking physical replicas and that
there was consensus that as long as single-user mode worked that it
was ok?

So maybe it's time after 2 1/2 years to get this one committed?

--
Gregory Stark
As Commitfest Manager

#139Daniel Gustafsson
daniel@yesql.se
In reply to: Gregory Stark (as CFM) (#138)
Re: On login trigger: take three

On 6 Mar 2023, at 21:55, Gregory Stark (as CFM) <stark.cfm@gmail.com> wrote:

It looks like Daniel Gustafsson, Andres, and Tom have all weighed in
on this patch with at least a neutral comment (+-0 from Andres :)

I think the concept of a login event trigger has merits, even though it's kind
of a niche use case.

It looks like the main concern was breaking physical replicas and that
there was consensus that as long as single-user mode worked that it
was ok?

Having a way to not rely on single-user mode and not causing an unacceptable
performance hit (which judging by recent benchmarks might not be an issue?). I
still intend to revisit this and I hope to get to it during this CF.

--
Daniel Gustafsson

#140Andres Freund
andres@anarazel.de
In reply to: Gregory Stark (as CFM) (#138)
Re: On login trigger: take three

Hi,

On 2023-03-06 15:55:01 -0500, Gregory Stark (as CFM) wrote:

It looks like Daniel Gustafsson, Andres, and Tom have all weighed in
on this patch with at least a neutral comment (+-0 from Andres :)

It looks like the main concern was breaking physical replicas and that
there was consensus that as long as single-user mode worked that it
was ok?

I don't think it's OK with just single user mode as an scape hatch. We need
the GUC that was discussed as part of the thread (and I think there's a patch
for that too).

Greetings,

Andres Freund

#141Daniel Gustafsson
daniel@yesql.se
In reply to: Andres Freund (#140)
Re: On login trigger: take three

On 6 Mar 2023, at 23:10, Andres Freund <andres@anarazel.de> wrote:

Hi,

On 2023-03-06 15:55:01 -0500, Gregory Stark (as CFM) wrote:

It looks like Daniel Gustafsson, Andres, and Tom have all weighed in
on this patch with at least a neutral comment (+-0 from Andres :)

It looks like the main concern was breaking physical replicas and that
there was consensus that as long as single-user mode worked that it
was ok?

I don't think it's OK with just single user mode as an scape hatch. We need
the GUC that was discussed as part of the thread (and I think there's a patch
for that too).

This is the patch which originated from this thread:

https://commitfest.postgresql.org/42/4013/

--
Daniel Gustafsson

#142Gregory Stark (as CFM)
stark.cfm@gmail.com
In reply to: Mikhail Gribkov (#137)
Re: On login trigger: take three

It looks like the patch is failing a test by not dumping
login_event_trigger_func? I'm guessing there's a race condition in the
test but I don't know. I also don't see the tmp_test_jI6t output file
being preserved in the artifacts anywhere :(

https://cirrus-ci.com/task/6391161871400960?logs=test_world#L2671

[16:16:48.594] # Looks like you failed 1 test of 10350.

# Running: pg_dump --no-sync
--file=/tmp/cirrus-ci-build/src/bin/pg_dump/tmp_check/tmp_test_jI6t/only_dump_measurement.sql
--table-and-children=dump_test.measurement --lock-wait-timeout=180000
postgres
[16:16:27.027](0.154s) ok 6765 - only_dump_measurement: pg_dump runs
.....
[16:16:27.035](0.000s) not ok 6870 - only_dump_measurement: should
dump CREATE FUNCTION dump_test.login_event_trigger_func
[16:16:27.035](0.000s)
[16:16:27.035](0.000s) # Failed test 'only_dump_measurement: should
dump CREATE FUNCTION dump_test.login_event_trigger_func'
# at t/002_pg_dump.pl line 4761.
.....
[16:16:48.594] +++ tap check in src/bin/pg_dump +++
[16:16:48.594] [16:16:05] t/001_basic.pl ................ ok 612 ms (
0.01 usr 0.00 sys + 0.24 cusr 0.26 csys = 0.51 CPU)
[16:16:48.594]
[16:16:48.594] # Failed test 'only_dump_measurement: should dump
CREATE FUNCTION dump_test.login_event_trigger_func'
[16:16:48.594] # at t/002_pg_dump.pl line 4761.
[16:16:48.594] # Review only_dump_measurement results in
/tmp/cirrus-ci-build/src/bin/pg_dump/tmp_check/tmp_test_jI6t

#143Mikhail Gribkov
youzhick@gmail.com
In reply to: Gregory Stark (as CFM) (#142)
1 attachment(s)
Re: On login trigger: take three

Hi Gregory,

Thanks for the note. The problem was that the patch was not aware of
yesterday Tom Lane's changes in the test.
It's fixed now: the attached v39 patch contains the updated version along
with the freshest rebase on master branch.

--
best regards,
Mikhail A. Gribkov

e-mail: youzhick@gmail.com
*http://www.flickr.com/photos/youzhick/albums
<http://www.flickr.com/photos/youzhick/albums&gt;*
http://www.strava.com/athletes/5085772
phone: +7(916)604-71-12
Telegram: @youzhick

On Wed, Mar 15, 2023 at 8:45 PM Gregory Stark (as CFM) <stark.cfm@gmail.com>
wrote:

Show quoted text

It looks like the patch is failing a test by not dumping
login_event_trigger_func? I'm guessing there's a race condition in the
test but I don't know. I also don't see the tmp_test_jI6t output file
being preserved in the artifacts anywhere :(

https://cirrus-ci.com/task/6391161871400960?logs=test_world#L2671

[16:16:48.594] # Looks like you failed 1 test of 10350.

# Running: pg_dump --no-sync

--file=/tmp/cirrus-ci-build/src/bin/pg_dump/tmp_check/tmp_test_jI6t/only_dump_measurement.sql
--table-and-children=dump_test.measurement --lock-wait-timeout=180000
postgres
[16:16:27.027](0.154s) ok 6765 - only_dump_measurement: pg_dump runs
.....
[16:16:27.035](0.000s) not ok 6870 - only_dump_measurement: should
dump CREATE FUNCTION dump_test.login_event_trigger_func
[16:16:27.035](0.000s)
[16:16:27.035](0.000s) # Failed test 'only_dump_measurement: should
dump CREATE FUNCTION dump_test.login_event_trigger_func'
# at t/002_pg_dump.pl line 4761.
.....
[16:16:48.594] +++ tap check in src/bin/pg_dump +++
[16:16:48.594] [16:16:05] t/001_basic.pl ................ ok 612 ms (
0.01 usr 0.00 sys + 0.24 cusr 0.26 csys = 0.51 CPU)
[16:16:48.594]
[16:16:48.594] # Failed test 'only_dump_measurement: should dump
CREATE FUNCTION dump_test.login_event_trigger_func'
[16:16:48.594] # at t/002_pg_dump.pl line 4761.
[16:16:48.594] # Review only_dump_measurement results in
/tmp/cirrus-ci-build/src/bin/pg_dump/tmp_check/tmp_test_jI6t

Attachments:

v39-On_client_login_event_trigger.patchapplication/octet-stream; name=v39-On_client_login_event_trigger.patchDownload
commit 305c79e03f659b96d9b8fb1f4316218d1271be28
Author: Mikhail Gribkov <m.gribkov@postgrespro.ru>
Commit: Mikhail Gribkov <m.gribkov@postgrespro.ru>

    [PATCH v39] Add support of event triggers on authenticated login
    
    The patch introduces trigger on login event, allowing to fire some actions
    right on the user connection. This can be useful for  logging or connection
    check purposes as well as for some personalization of environment. Usage
    details are described in the documentation included, but shortly usage is
    the same as for other triggers: create function returning event_trigger and
    then create event trigger on login event.

diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 3b6a5361b3..94d28835d1 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>login</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,18 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>login</literal> event occurs when an authenticated user logs
+     into the system. Any bug in a trigger procedure for this event may
+     prevent successful login to the system. Such bugs may be fixed by
+     restarting the system in single-user mode (as event triggers are
+     disabled in this mode). See the <xref linkend="app-postgres"/> reference
+     page for details about using single-user mode.
+     The <literal>login</literal> event will also fire on standby servers.
+     To prevent servers from becoming inaccessible, such triggers must avoid
+     writing anything to the database when running on a standby.
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1300,4 +1313,79 @@ CREATE EVENT TRIGGER no_rewrite_allowed
 </programlisting>
    </para>
  </sect1>
+
+  <sect1 id="event-trigger-database-login-example">
+    <title>A Database Login Event Trigger Example</title>
+
+    <para>
+      The event trigger on the <literal>login</literal> event can be
+      useful for logging user logins, for verifying the connection and
+      assigning roles according to current circumstances, or for session
+      data initialization. It is very important that any event trigger using
+      the <literal>login</literal> event checks whether or not the database is
+      in recovery before performing any writes. Writing to a standby server
+      will make it inaccessible.
+    </para>
+
+    <para>
+      The following example demonstrates these options.
+<programlisting>
+-- create test tables and roles
+CREATE TABLE user_login_log (
+  "user" text,
+  "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+  RETURNS event_trigger SECURITY DEFINER
+  LANGUAGE plpgsql AS
+$$
+DECLARE
+  hour integer = EXTRACT('hour' FROM current_time at time zone 'utc');
+  rec boolean;
+BEGIN
+-- 1. Forbid logging in between 2AM and 4AM.
+IF hour BETWEEN 2 AND 4 THEN
+  RAISE EXCEPTION 'Login forbidden';
+END IF;
+
+-- The checks below cannot be performed on standby servers so
+-- ensure the database is not in recovery before we perform any
+-- operations.
+SELECT pg_is_in_recovery() INTO rec;
+IF rec THEN
+  RETURN;
+END IF;
+
+-- 2. Assign some roles. At daytime, grant the day_worker role, else the
+-- night_worker role.
+IF hour BETWEEN 8 AND 20 THEN
+  EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT day_worker TO ' || quote_ident(session_user);
+ELSE
+  EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT night_worker TO ' || quote_ident(session_user);
+END IF;
+
+-- 3. Initialize user session data
+CREATE TEMP TABLE session_storage (x float, y integer);
+ALTER TABLE session_storage OWNER TO session_user;
+
+-- 4. Log the connection time
+INSERT INTO public.user_login_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+  ON login
+  EXECUTE FUNCTION init_session();
+ALTER EVENT TRIGGER init_session ENABLE ALWAYS;
+</programlisting>
+    </para>
+  </sect1>
 </chapter>
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index d4b00d1a82..c83c5d78dd 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -37,15 +38,18 @@
 #include "miscadmin.h"
 #include "parser/parse_func.h"
 #include "pgstat.h"
+#include "storage/lmgr.h"
 #include "tcop/deparse_utility.h"
 #include "tcop/utility.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 typedef struct EventTriggerQueryState
@@ -130,6 +134,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "login") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -579,10 +584,15 @@ EventTriggerCommonSetup(Node *parsetree,
 	{
 		CommandTag	dbgtag;
 
-		dbgtag = CreateCommandTag(parsetree);
+		if (event == EVT_Login)
+			dbgtag = CMDTAG_LOGIN;
+		else
+			dbgtag = CreateCommandTag(parsetree);
+
 		if (event == EVT_DDLCommandStart ||
 			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
+			event == EVT_SQLDrop ||
+			event == EVT_Login)
 		{
 			if (!command_tag_event_trigger_ok(dbgtag))
 				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
@@ -601,7 +611,10 @@ EventTriggerCommonSetup(Node *parsetree,
 		return NIL;
 
 	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
+	if (event == EVT_Login)
+		tag = CMDTAG_LOGIN;
+	else
+		tag = CreateCommandTag(parsetree);
 
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
@@ -799,6 +812,46 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Fire login event triggers if any are present.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId))
+		return;
+
+	StartTransactionCommand();
+
+	/* Find login triggers and run what was found */
+	runlist = EventTriggerCommonSetup(NULL,
+										EVT_Login, "login",
+										&trigdata);
+
+	if (runlist != NIL)
+	{
+		/* Event trigger execution may require an active snapshot. */
+		PushActiveSnapshot(GetTransactionSnapshot());
+
+		/* Run the triggers. */
+		EventTriggerInvoke(runlist, &trigdata);
+
+		/* Cleanup. */
+		list_free(runlist);
+
+		PopActiveSnapshot();
+	}
+
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index cab709b07b..33a4dfeaa2 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -32,6 +32,7 @@
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/prepare.h"
 #include "common/pg_prng.h"
 #include "jit/jit.h"
@@ -4226,6 +4227,9 @@ PostgresMain(const char *dbname, const char *username)
 	initStringInfo(&row_description_buf);
 	MemoryContextSwitchTo(TopMemoryContext);
 
+	/* Fire any defined login event triggers, if appropriate */
+	EventTriggerOnLogin();
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 1f5e7eb4c6..9b598276f4 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "login") == 0)
+			event = EVT_Login;
 		else
 			continue;
 
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index a22f27f300..a69ad73bae 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2015,6 +2015,25 @@ my %tests = (
 		},
 	},
 
+	'CREATE FUNCTION dump_test.login_event_trigger_func' => {
+		create_order => 32,
+		create_sql   => 'CREATE FUNCTION dump_test.login_event_trigger_func()
+					   RETURNS event_trigger LANGUAGE plpgsql
+					   AS $$ BEGIN RETURN; END;$$;',
+		regexp => qr/^
+			\QCREATE FUNCTION dump_test.login_event_trigger_func() RETURNS event_trigger\E
+			\n\s+\QLANGUAGE plpgsql\E
+			\n\s+AS\ \$\$
+			\Q BEGIN RETURN; END;\E
+			\$\$;/xm,
+		like =>
+		  { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			only_dump_measurement    => 1,
+		},
+	},
+
 	'CREATE OPERATOR FAMILY dump_test.op_family' => {
 		create_order => 73,
 		create_sql =>
@@ -2127,6 +2146,19 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE EVENT TRIGGER test_login_event_trigger' => {
+		create_order => 33,
+		create_sql   => 'CREATE EVENT TRIGGER test_login_event_trigger
+					   ON login
+					   EXECUTE FUNCTION dump_test.login_event_trigger_func();',
+		regexp => qr/^
+			\QCREATE EVENT TRIGGER test_login_event_trigger \E
+			\QON login\E
+			\n\s+\QEXECUTE FUNCTION dump_test.login_event_trigger_func();\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE TRIGGER test_trigger' => {
 		create_order => 31,
 		create_sql   => 'CREATE TRIGGER test_trigger
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 42e87b9e49..fb0d98c1de 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3519,8 +3519,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop",
-					  "table_rewrite");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "login",
+					  "sql_drop", "table_rewrite");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 5ed6ece555..bf756d471d 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -54,6 +54,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnLogin(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index e738ac1c09..553a31874f 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
+PG_CMDTAG(CMDTAG_LOGIN, "LOGIN", true, false, false)
 PG_CMDTAG(CMDTAG_MERGE, "MERGE", false, false, true)
 PG_CMDTAG(CMDTAG_MOVE, "MOVE", false, false, true)
 PG_CMDTAG(CMDTAG_NOTIFY, "NOTIFY", false, false, false)
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index d340026518..52052e6252 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Login,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/authentication/t/005_login_trigger.pl b/src/test/authentication/t/005_login_trigger.pl
new file mode 100644
index 0000000000..fec664fb86
--- /dev/null
+++ b/src/test/authentication/t/005_login_trigger.pl
@@ -0,0 +1,157 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+# Tests of authentication via login trigger. Mostly for rejection via
+# exception, because this scenario cannot be covered with *.sql/*.out regress
+# tests.
+
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Execute a psql command and compare its output towards given regexps
+sub psql_command
+{
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+	my ($node, $sql, $expected_ret, $test_name, %params) = @_;
+
+	my $connstr;
+	if (defined($params{connstr}))
+	{
+		$connstr = $params{connstr};
+	}
+	else
+	{
+		$connstr = '';
+	}
+
+	# Execute command
+	my ($ret, $stdout, $stderr) = $node->psql('postgres', $sql,
+											  connstr => "$connstr");
+
+	# Check return code
+	is($ret, $expected_ret,  "$test_name: exit code $expected_ret");
+
+	# Check stdout
+	if (defined($params{log_like}))
+	{
+		my @log_like = @{ $params{log_like} };
+		while (my $regex = shift @log_like)
+		{
+			like($stdout, $regex, "$test_name: log matches");
+		}
+	}
+	if (defined($params{log_unlike}))
+	{
+		my @log_unlike = @{ $params{log_unlike} };
+		while (my $regex = shift @log_unlike)
+		{
+			unlike($stdout, $regex, "$test_name: log unmatches");
+		}
+	}
+	if (defined($params{log_exact}))
+	{
+		is($stdout, $params{log_exact}, "$test_name: log equals");
+	}
+
+	# Check stderr
+	if (defined($params{err_like}))
+	{
+		my @err_like = @{ $params{err_like} };
+		while (my $regex = shift @err_like)
+		{
+			like($stderr, $regex, "$test_name: err matches");
+		}
+	}
+	if (defined($params{err_unlike}))
+	{
+		my @err_unlike = @{ $params{err_unlike} };
+		while (my $regex = shift @err_unlike)
+		{
+			unlike($stderr, $regex, "$test_name: err unmatches");
+		}
+	}
+	if (defined($params{err_exact}))
+	{
+		is($stderr, $params{err_exact}, "$test_name: err equals");
+	}
+
+	return;
+}
+
+# New node
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init(extra => [ '--locale=C', '--encoding=UTF8' ]);
+$node->append_conf(
+	'postgresql.conf', q{
+wal_level = 'logical'
+max_replication_slots = 4
+max_wal_senders = 4
+});
+$node->start;
+
+# Create temporary roles and log table
+psql_command($node, 'CREATE ROLE alice WITH LOGIN;
+CREATE ROLE mallory WITH LOGIN;
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+', 0, 'create tmp objects', log_exact => '', err_exact => ''), ;
+
+# Create login event function and trigger
+psql_command($node,
+			 'CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  IF SESSION_USER = \'mallory\' THEN
+    RAISE EXCEPTION \'Hello %! You are NOT welcome here!\', SESSION_USER;
+  END IF;
+  RAISE NOTICE \'Hello %! You are welcome!\', SESSION_USER;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+', 0, 'create trigger function', log_exact => '', err_exact => '');
+
+psql_command($node,
+			 'CREATE EVENT TRIGGER on_login_trigger '
+			 .'ON login EXECUTE PROCEDURE on_login_proc();', 0,
+			 'create event trigger', log_exact => '', err_exact => '');
+psql_command($node, 'ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;', 0,
+			 'alter event trigger', log_exact => '',
+			 err_like => [qr/You are welcome/]);
+
+# Check the two requests were logged via login trigger
+psql_command($node, 'SELECT COUNT(*) FROM user_logins;', 0, 'select count',
+			 log_exact => '2', err_like => [qr/You are welcome/]);
+
+# Try to log as allowed Alice and disallowed Mallory (two times)
+psql_command($node, 'SELECT 1;', 0, 'try alice', connstr => 'user=alice',
+			 log_exact => '1', err_like => [qr/You are welcome/],
+			 err_unlike => [qr/You are NOT welcome/]);
+psql_command($node, 'SELECT 1;', 2, 'try mallory', connstr => 'user=mallory',
+			 log_exact => '', err_like => [qr/You are NOT welcome/],
+			 err_unlike => [qr/You are welcome/]);
+psql_command($node, 'SELECT 1;', 2, 'try mallory', connstr => 'user=mallory',
+			 log_exact => '', err_like => [qr/You are NOT welcome/],
+			 err_unlike => [qr/You are welcome/]);
+
+# Check that Alice's login record is here, while the Mallory's one is not
+psql_command($node, 'SELECT * FROM user_logins;', 0, 'select *',
+			 log_like => [qr/3\|alice/], log_unlike => [qr/mallory/],
+			 err_like => [qr/You are welcome/]);
+
+# Check total number of successful logins so far
+psql_command($node, 'SELECT COUNT(*) FROM user_logins;', 0, 'select count',
+			 log_exact => '5', err_like => [qr/You are welcome/]);
+
+# Cleanup the temporary stuff
+psql_command($node, 'DROP EVENT TRIGGER on_login_trigger;', 0,
+			 'drop event trigger', log_exact => '',
+			 err_like => [qr/You are welcome/]);
+psql_command($node, 'DROP TABLE user_logins;
+DROP FUNCTION on_login_proc;
+DROP ROLE mallory;
+DROP ROLE alice;
+', 0, 'cleanup', log_exact => '', err_exact => '');
+
+done_testing();
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 76846905a7..12e365aac8 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,24 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE TABLE user_logins(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO user_logins (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 $node_primary->wait_for_replay_catchup($node_standby_1);
 $node_standby_1->wait_for_replay_catchup($node_standby_2, $node_primary);
@@ -384,6 +402,11 @@ sub replay_check
 
 replay_check();
 
+my $evttrig = $node_standby_1->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+$evttrig = $node_standby_2->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 5a10958df5..433f5f6e95 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -614,3 +614,34 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     1
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     2
+(1 row)
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 1aeaddbe71..420224f3ba 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -471,3 +471,24 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+SELECT COUNT(*) FROM user_logins;
+\c
+SELECT COUNT(*) FROM user_logins;
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
#144Robert Haas
robertmhaas@gmail.com
In reply to: Daniel Gustafsson (#101)
Re: On login trigger: take three

On Tue, Mar 15, 2022 at 4:52 PM Daniel Gustafsson <daniel@yesql.se> wrote:

Yeah, that was the previously posted v25 from the author (who adopted it from
the original author). I took the liberty to quickly poke at the review
comments you had left as well as the ones that I had found to try and progress
the patch. 0001 should really go in it's own thread though to not hide it from
anyone interested who isn't looking at this thread.

Some comments on 0001:

- In general, I think we should prefer to phrase options in terms of
what is done, rather than what is not done. For instance, the
corresponding GUC for row-level security is row_security={on|off}, not
ignore_row_security.

- I think it's odd that the GUC in question doesn't accept true and
false and our usual synonyms for those values. I suggest that it
should, even if we want to add more possible values later.

- "ignoreing" is mispleled. So is gux-ignore-event-trigger. "Even
triggers" -> "Event triggers".

- Perhaps the documentation for the GUC should mention that the GUC is
not relevant in single-user mode because event triggers don't fire
then anyway.

- "Disable event triggers during the session." isn't a very good
description because there is in theory nothing to prevent this from
being set in postgresql.conf.

Basically, I think 0001 is a good idea -- I'm much more nervous about
0002. I think we should get 0001 polished up and committed.

--
Robert Haas
EDB: http://www.enterprisedb.com

#145Daniel Gustafsson
daniel@yesql.se
In reply to: Robert Haas (#144)
Re: On login trigger: take three

On 22 Mar 2023, at 18:54, Robert Haas <robertmhaas@gmail.com> wrote:

Basically, I think 0001 is a good idea -- I'm much more nervous about
0002. I think we should get 0001 polished up and committed.

Correct me if I'm wrong, but I believe you commented on v27-0001 of the login
event trigger patch series? Sorry about the confusion if so, this is a very
old and lengthy thread with many twists and turns. This series was closed
downthread when the original authors of login EVT left, and the 0001 GUC patch
extracted into its own thread. That patch now lives at:

https://commitfest.postgresql.org/42/4013/

This thread was then later revived by Mikhail Gribkov but without 0001 instead
referring to the above patch for that part.

The new patch for 0001 is quite different, and I welcome your eyes on that
since I agree with you that it would be a good idea.

--
Daniel Gustafsson

#146Mikhail Gribkov
youzhick@gmail.com
In reply to: Daniel Gustafsson (#145)
1 attachment(s)
Re: On login trigger: take three

Hi hackers,

The attached v40 patch is a fresh rebase on master branch to actualize the
state before the upcoming commitfest.
Nothing has changed beyond rebasing.

And just for convenience, here is a link to the exact message of the thread
describing the current approach:
/messages/by-id/CAMEv5_vg4aJOoUC74XJm+5B7+TF1nT-Yhtg+awtBOESXG5Grfg@mail.gmail.com

--
best regards,
Mikhail A. Gribkov

e-mail: youzhick@gmail.com
*http://www.flickr.com/photos/youzhick/albums
<http://www.flickr.com/photos/youzhick/albums&gt;*
http://www.strava.com/athletes/5085772
phone: +7(916)604-71-12
Telegram: @youzhick

On Wed, Mar 22, 2023 at 10:38 PM Daniel Gustafsson <daniel@yesql.se> wrote:

Show quoted text

On 22 Mar 2023, at 18:54, Robert Haas <robertmhaas@gmail.com> wrote:

Basically, I think 0001 is a good idea -- I'm much more nervous about
0002. I think we should get 0001 polished up and committed.

Correct me if I'm wrong, but I believe you commented on v27-0001 of the
login
event trigger patch series? Sorry about the confusion if so, this is a
very
old and lengthy thread with many twists and turns. This series was closed
downthread when the original authors of login EVT left, and the 0001 GUC
patch
extracted into its own thread. That patch now lives at:

https://commitfest.postgresql.org/42/4013/

This thread was then later revived by Mikhail Gribkov but without 0001
instead
referring to the above patch for that part.

The new patch for 0001 is quite different, and I welcome your eyes on that
since I agree with you that it would be a good idea.

--
Daniel Gustafsson

Attachments:

v40-On_client_login_event_trigger.patchapplication/octet-stream; name=v40-On_client_login_event_trigger.patchDownload
commit ba5592fd3a76639e49388fc24d7d59f7b1598e74
Author: Mikhail Gribkov <m.gribkov@postgrespro.ru>
Commit: Mikhail Gribkov <m.gribkov@postgrespro.ru>

    [PATCH v40] Add support of event triggers on authenticated login
    
    The patch introduces trigger on login event, allowing to fire some actions
    right on the user connection. This can be useful for  logging or connection
    check purposes as well as for some personalization of environment. Usage
    details are described in the documentation included, but shortly usage is
    the same as for other triggers: create function returning event_trigger and
    then create event trigger on login event.

diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 3b6a5361b3..94d28835d1 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>login</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,18 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>login</literal> event occurs when an authenticated user logs
+     into the system. Any bug in a trigger procedure for this event may
+     prevent successful login to the system. Such bugs may be fixed by
+     restarting the system in single-user mode (as event triggers are
+     disabled in this mode). See the <xref linkend="app-postgres"/> reference
+     page for details about using single-user mode.
+     The <literal>login</literal> event will also fire on standby servers.
+     To prevent servers from becoming inaccessible, such triggers must avoid
+     writing anything to the database when running on a standby.
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1300,4 +1313,79 @@ CREATE EVENT TRIGGER no_rewrite_allowed
 </programlisting>
    </para>
  </sect1>
+
+  <sect1 id="event-trigger-database-login-example">
+    <title>A Database Login Event Trigger Example</title>
+
+    <para>
+      The event trigger on the <literal>login</literal> event can be
+      useful for logging user logins, for verifying the connection and
+      assigning roles according to current circumstances, or for session
+      data initialization. It is very important that any event trigger using
+      the <literal>login</literal> event checks whether or not the database is
+      in recovery before performing any writes. Writing to a standby server
+      will make it inaccessible.
+    </para>
+
+    <para>
+      The following example demonstrates these options.
+<programlisting>
+-- create test tables and roles
+CREATE TABLE user_login_log (
+  "user" text,
+  "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+  RETURNS event_trigger SECURITY DEFINER
+  LANGUAGE plpgsql AS
+$$
+DECLARE
+  hour integer = EXTRACT('hour' FROM current_time at time zone 'utc');
+  rec boolean;
+BEGIN
+-- 1. Forbid logging in between 2AM and 4AM.
+IF hour BETWEEN 2 AND 4 THEN
+  RAISE EXCEPTION 'Login forbidden';
+END IF;
+
+-- The checks below cannot be performed on standby servers so
+-- ensure the database is not in recovery before we perform any
+-- operations.
+SELECT pg_is_in_recovery() INTO rec;
+IF rec THEN
+  RETURN;
+END IF;
+
+-- 2. Assign some roles. At daytime, grant the day_worker role, else the
+-- night_worker role.
+IF hour BETWEEN 8 AND 20 THEN
+  EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT day_worker TO ' || quote_ident(session_user);
+ELSE
+  EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT night_worker TO ' || quote_ident(session_user);
+END IF;
+
+-- 3. Initialize user session data
+CREATE TEMP TABLE session_storage (x float, y integer);
+ALTER TABLE session_storage OWNER TO session_user;
+
+-- 4. Log the connection time
+INSERT INTO public.user_login_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+  ON login
+  EXECUTE FUNCTION init_session();
+ALTER EVENT TRIGGER init_session ENABLE ALWAYS;
+</programlisting>
+    </para>
+  </sect1>
 </chapter>
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index d4b00d1a82..c83c5d78dd 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -37,15 +38,18 @@
 #include "miscadmin.h"
 #include "parser/parse_func.h"
 #include "pgstat.h"
+#include "storage/lmgr.h"
 #include "tcop/deparse_utility.h"
 #include "tcop/utility.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 typedef struct EventTriggerQueryState
@@ -130,6 +134,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "login") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -579,10 +584,15 @@ EventTriggerCommonSetup(Node *parsetree,
 	{
 		CommandTag	dbgtag;
 
-		dbgtag = CreateCommandTag(parsetree);
+		if (event == EVT_Login)
+			dbgtag = CMDTAG_LOGIN;
+		else
+			dbgtag = CreateCommandTag(parsetree);
+
 		if (event == EVT_DDLCommandStart ||
 			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
+			event == EVT_SQLDrop ||
+			event == EVT_Login)
 		{
 			if (!command_tag_event_trigger_ok(dbgtag))
 				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
@@ -601,7 +611,10 @@ EventTriggerCommonSetup(Node *parsetree,
 		return NIL;
 
 	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
+	if (event == EVT_Login)
+		tag = CMDTAG_LOGIN;
+	else
+		tag = CreateCommandTag(parsetree);
 
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
@@ -799,6 +812,46 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Fire login event triggers if any are present.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode.
+	 */
+	if (!IsUnderPostmaster || !OidIsValid(MyDatabaseId))
+		return;
+
+	StartTransactionCommand();
+
+	/* Find login triggers and run what was found */
+	runlist = EventTriggerCommonSetup(NULL,
+										EVT_Login, "login",
+										&trigdata);
+
+	if (runlist != NIL)
+	{
+		/* Event trigger execution may require an active snapshot. */
+		PushActiveSnapshot(GetTransactionSnapshot());
+
+		/* Run the triggers. */
+		EventTriggerInvoke(runlist, &trigdata);
+
+		/* Cleanup. */
+		list_free(runlist);
+
+		PopActiveSnapshot();
+	}
+
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 01b6cc1f7d..7417f391f3 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -36,6 +36,7 @@
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/prepare.h"
 #include "common/pg_prng.h"
 #include "jit/jit.h"
@@ -4273,6 +4274,9 @@ PostgresMain(const char *dbname, const char *username)
 	initStringInfo(&row_description_buf);
 	MemoryContextSwitchTo(TopMemoryContext);
 
+	/* Fire any defined login event triggers, if appropriate */
+	EventTriggerOnLogin();
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 1f5e7eb4c6..9b598276f4 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "login") == 0)
+			event = EVT_Login;
 		else
 			continue;
 
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 15852188c4..5ba665cca8 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2106,6 +2106,25 @@ my %tests = (
 		},
 	},
 
+	'CREATE FUNCTION dump_test.login_event_trigger_func' => {
+		create_order => 32,
+		create_sql   => 'CREATE FUNCTION dump_test.login_event_trigger_func()
+					   RETURNS event_trigger LANGUAGE plpgsql
+					   AS $$ BEGIN RETURN; END;$$;',
+		regexp => qr/^
+			\QCREATE FUNCTION dump_test.login_event_trigger_func() RETURNS event_trigger\E
+			\n\s+\QLANGUAGE plpgsql\E
+			\n\s+AS\ \$\$
+			\Q BEGIN RETURN; END;\E
+			\$\$;/xm,
+		like =>
+		  { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			only_dump_measurement    => 1,
+		},
+	},
+
 	'CREATE OPERATOR FAMILY dump_test.op_family' => {
 		create_order => 73,
 		create_sql =>
@@ -2218,6 +2237,19 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE EVENT TRIGGER test_login_event_trigger' => {
+		create_order => 33,
+		create_sql   => 'CREATE EVENT TRIGGER test_login_event_trigger
+					   ON login
+					   EXECUTE FUNCTION dump_test.login_event_trigger_func();',
+		regexp => qr/^
+			\QCREATE EVENT TRIGGER test_login_event_trigger \E
+			\QON login\E
+			\n\s+\QEXECUTE FUNCTION dump_test.login_event_trigger_func();\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE TRIGGER test_trigger' => {
 		create_order => 31,
 		create_sql => 'CREATE TRIGGER test_trigger
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 677847e434..d73dc95a31 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3521,8 +3521,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop",
-					  "table_rewrite");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "login",
+					  "sql_drop", "table_rewrite");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 5ed6ece555..bf756d471d 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -54,6 +54,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnLogin(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index e738ac1c09..553a31874f 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
+PG_CMDTAG(CMDTAG_LOGIN, "LOGIN", true, false, false)
 PG_CMDTAG(CMDTAG_MERGE, "MERGE", false, false, true)
 PG_CMDTAG(CMDTAG_MOVE, "MOVE", false, false, true)
 PG_CMDTAG(CMDTAG_NOTIFY, "NOTIFY", false, false, false)
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index d340026518..52052e6252 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Login,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/authentication/t/005_login_trigger.pl b/src/test/authentication/t/005_login_trigger.pl
new file mode 100644
index 0000000000..fec664fb86
--- /dev/null
+++ b/src/test/authentication/t/005_login_trigger.pl
@@ -0,0 +1,157 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+# Tests of authentication via login trigger. Mostly for rejection via
+# exception, because this scenario cannot be covered with *.sql/*.out regress
+# tests.
+
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Execute a psql command and compare its output towards given regexps
+sub psql_command
+{
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+	my ($node, $sql, $expected_ret, $test_name, %params) = @_;
+
+	my $connstr;
+	if (defined($params{connstr}))
+	{
+		$connstr = $params{connstr};
+	}
+	else
+	{
+		$connstr = '';
+	}
+
+	# Execute command
+	my ($ret, $stdout, $stderr) = $node->psql('postgres', $sql,
+											  connstr => "$connstr");
+
+	# Check return code
+	is($ret, $expected_ret,  "$test_name: exit code $expected_ret");
+
+	# Check stdout
+	if (defined($params{log_like}))
+	{
+		my @log_like = @{ $params{log_like} };
+		while (my $regex = shift @log_like)
+		{
+			like($stdout, $regex, "$test_name: log matches");
+		}
+	}
+	if (defined($params{log_unlike}))
+	{
+		my @log_unlike = @{ $params{log_unlike} };
+		while (my $regex = shift @log_unlike)
+		{
+			unlike($stdout, $regex, "$test_name: log unmatches");
+		}
+	}
+	if (defined($params{log_exact}))
+	{
+		is($stdout, $params{log_exact}, "$test_name: log equals");
+	}
+
+	# Check stderr
+	if (defined($params{err_like}))
+	{
+		my @err_like = @{ $params{err_like} };
+		while (my $regex = shift @err_like)
+		{
+			like($stderr, $regex, "$test_name: err matches");
+		}
+	}
+	if (defined($params{err_unlike}))
+	{
+		my @err_unlike = @{ $params{err_unlike} };
+		while (my $regex = shift @err_unlike)
+		{
+			unlike($stderr, $regex, "$test_name: err unmatches");
+		}
+	}
+	if (defined($params{err_exact}))
+	{
+		is($stderr, $params{err_exact}, "$test_name: err equals");
+	}
+
+	return;
+}
+
+# New node
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init(extra => [ '--locale=C', '--encoding=UTF8' ]);
+$node->append_conf(
+	'postgresql.conf', q{
+wal_level = 'logical'
+max_replication_slots = 4
+max_wal_senders = 4
+});
+$node->start;
+
+# Create temporary roles and log table
+psql_command($node, 'CREATE ROLE alice WITH LOGIN;
+CREATE ROLE mallory WITH LOGIN;
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+', 0, 'create tmp objects', log_exact => '', err_exact => ''), ;
+
+# Create login event function and trigger
+psql_command($node,
+			 'CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  IF SESSION_USER = \'mallory\' THEN
+    RAISE EXCEPTION \'Hello %! You are NOT welcome here!\', SESSION_USER;
+  END IF;
+  RAISE NOTICE \'Hello %! You are welcome!\', SESSION_USER;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+', 0, 'create trigger function', log_exact => '', err_exact => '');
+
+psql_command($node,
+			 'CREATE EVENT TRIGGER on_login_trigger '
+			 .'ON login EXECUTE PROCEDURE on_login_proc();', 0,
+			 'create event trigger', log_exact => '', err_exact => '');
+psql_command($node, 'ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;', 0,
+			 'alter event trigger', log_exact => '',
+			 err_like => [qr/You are welcome/]);
+
+# Check the two requests were logged via login trigger
+psql_command($node, 'SELECT COUNT(*) FROM user_logins;', 0, 'select count',
+			 log_exact => '2', err_like => [qr/You are welcome/]);
+
+# Try to log as allowed Alice and disallowed Mallory (two times)
+psql_command($node, 'SELECT 1;', 0, 'try alice', connstr => 'user=alice',
+			 log_exact => '1', err_like => [qr/You are welcome/],
+			 err_unlike => [qr/You are NOT welcome/]);
+psql_command($node, 'SELECT 1;', 2, 'try mallory', connstr => 'user=mallory',
+			 log_exact => '', err_like => [qr/You are NOT welcome/],
+			 err_unlike => [qr/You are welcome/]);
+psql_command($node, 'SELECT 1;', 2, 'try mallory', connstr => 'user=mallory',
+			 log_exact => '', err_like => [qr/You are NOT welcome/],
+			 err_unlike => [qr/You are welcome/]);
+
+# Check that Alice's login record is here, while the Mallory's one is not
+psql_command($node, 'SELECT * FROM user_logins;', 0, 'select *',
+			 log_like => [qr/3\|alice/], log_unlike => [qr/mallory/],
+			 err_like => [qr/You are welcome/]);
+
+# Check total number of successful logins so far
+psql_command($node, 'SELECT COUNT(*) FROM user_logins;', 0, 'select count',
+			 log_exact => '5', err_like => [qr/You are welcome/]);
+
+# Cleanup the temporary stuff
+psql_command($node, 'DROP EVENT TRIGGER on_login_trigger;', 0,
+			 'drop event trigger', log_exact => '',
+			 err_like => [qr/You are welcome/]);
+psql_command($node, 'DROP TABLE user_logins;
+DROP FUNCTION on_login_proc;
+DROP ROLE mallory;
+DROP ROLE alice;
+', 0, 'cleanup', log_exact => '', err_exact => '');
+
+done_testing();
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 0c72ba0944..8644854eb3 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,24 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE TABLE user_logins(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO user_logins (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 $node_primary->wait_for_replay_catchup($node_standby_1);
 $node_standby_1->wait_for_replay_catchup($node_standby_2, $node_primary);
@@ -384,6 +402,11 @@ sub replay_check
 
 replay_check();
 
+my $evttrig = $node_standby_1->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+$evttrig = $node_standby_2->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 5a10958df5..433f5f6e95 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -614,3 +614,34 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     1
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     2
+(1 row)
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 1aeaddbe71..420224f3ba 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -471,3 +471,24 @@ SELECT
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
 DROP EVENT TRIGGER sql_drop_command;
+
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+SELECT COUNT(*) FROM user_logins;
+\c
+SELECT COUNT(*) FROM user_logins;
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
#147Alexander Korotkov
aekorotkov@gmail.com
In reply to: Mikhail Gribkov (#146)
Re: On login trigger: take three

Hi!

On Wed, Jun 14, 2023 at 10:49 PM Mikhail Gribkov <youzhick@gmail.com> wrote:

The attached v40 patch is a fresh rebase on master branch to actualize the state before the upcoming commitfest.
Nothing has changed beyond rebasing.

And just for convenience, here is a link to the exact message of the thread
describing the current approach:
/messages/by-id/CAMEv5_vg4aJOoUC74XJm+5B7+TF1nT-Yhtg+awtBOESXG5Grfg@mail.gmail.com

Thank you for the update. I think the patch is interesting and
demanding. The code, docs and tests seem to be quite polished
already. Simultaneously, the thread is long and spread over a long
time. So, it's easy to lose track. I'd like to do a short summary of
design issues on this thread.

1. Initially the patch introduced "on login" triggers. It was unclear
if they need to rerun on RESET ALL or DISCARD ALL [1]. The new name is
"client connection" trigger, which seems fine [2].

2. Another question is how to deal with triggers, which hangs or fails
with error [1]. One possible way to workaround that is single-user
mode, which is already advised to workaround the errors in other event
triggers. However, in perspective single-user mode might be deleted.
Also, single-user mode is considered as a worst-case scenario recovery
tool, while it's very easy to block the database connections with
client connection triggers. As addition/alternative to single-user
mode, GUC options to disable all event triggers and/or client
connection triggers. Finally, the patch for the GUC option to disable
all event triggers resides in a separate thread [4]. Apparently that
patch should be committed first [5].

3. Yet another question is connection-time overhead introduced by this
patch. The overhead estimate varies from no measurable overhead [6] to
5% overhead [7]. In order to overcome that, [8] has introduced a
database-level flag indicating whether there are connection triggers.
Later this flag was removed [9] in a hope that the possible overhead
is acceptable.

4. [10] points that there is no clean way to store information about
unsuccessful connections (declined by either authentication or
trigger). However, this is considered out-of-scope for the current
patch, and could be implemented later if needed.

5. It was also pointed out [11] that ^C in psql doesn't cancel
long-running client connection triggers. That might be considered a
psql problem though.

6. It has been also pointed out that [12] all triggers, which write
data to the database, must check pg_is_in_recovery() to work correctly
on standby. That seems to be currently reflected in the documentation.

So, for me the open issues seem to be 2, 3 and 5. My plan to revive
this patch is to commit the GUC patch [4], recheck the overhead and
probably leave "^C in psql" problem as a separate standalone issue.
Any thoughts?

Links.

1. /messages/by-id/CAFj8pRBdqdqvkU3mVKzoOnO+jPz-6manRV47CDEa+1jD6x6LFg@mail.gmail.com
2. /messages/by-id/CAFj8pRCxdQgHy8Mynk3hz6pFsqQ9BN6Vfgy0MJLtQBAUhWDf3w@mail.gmail.com
3. /messages/by-id/E0D5DC61-C490-45BD-A984-E8D56493EC4F@yesql.se
4. /messages/by-id/9140106E-F9BF-4D85-8FC8-F2D3C094A6D9@yesql.se
5. /messages/by-id/20230306221010.gszjoakt5jp7oqpd@awork3.anarazel.de
6. /messages/by-id/90760f2d-2f9c-12ab-d2c5-e8e6fb7d08de@postgrespro.ru
7. /messages/by-id/CAFj8pRChwu01VLx76nKBVyScHCsd1YnBGiKfDJ6h17g4CSnUBg@mail.gmail.com
8. /messages/by-id/4471d472-5dfc-f2b0-ad05-0ff8d0a3bb0c@postgrespro.ru
9. /messages/by-id/CAMEv5_vg4aJOoUC74XJm+5B7+TF1nT-Yhtg+awtBOESXG5Grfg@mail.gmail.com
10. /messages/by-id/9c897136-4755-dcfc-2d24-b12bcfe4467f@sigaev.ru
11. /messages/by-id/CA+TgmoZv9f1s797tihx-zXQN4AE4ZFBV5C0K=zngbgNu3xNNkg@mail.gmail.com
12. /messages/by-id/20220312024652.lvgehszwke4hhove@alap3.anarazel.de

------
Regards,
Alexander Korotkov

#148Daniel Gustafsson
daniel@yesql.se
In reply to: Alexander Korotkov (#147)
Re: On login trigger: take three

On 25 Sep 2023, at 11:13, Alexander Korotkov <aekorotkov@gmail.com> wrote:

I'd like to do a short summary of
design issues on this thread.

Thanks for summarizing this long thread!

the patch for the GUC option to disable
all event triggers resides in a separate thread [4]. Apparently that
patch should be committed first [5].

I have committed the prerequisite patch for temporarily disabling EVTs via a
GUC in 7750fefdb2. We should absolutely avoid creating any more dependencies
on single-user mode (yes, I have changed my mind since the beginning of the
thread).

3. Yet another question is connection-time overhead introduced by this
patch. The overhead estimate varies from no measurable overhead [6] to
5% overhead [7]. In order to overcome that, [8] has introduced a
database-level flag indicating whether there are connection triggers.
Later this flag was removed [9] in a hope that the possible overhead
is acceptable.

While I disliked the flag, I do think the overhead is problematic. Last time I
profiled it I found it noticeable, and it seems expensive for such a niche
feature to impact such a hot path. Maybe you can think of other ways to reduce
the cost here (if it indeed is an issue in the latest version of the patch,
which is not one I've benchmarked)?

5. It was also pointed out [11] that ^C in psql doesn't cancel
long-running client connection triggers. That might be considered a
psql problem though.

While it is a psql problem, it's exacerbated by a slow login EVT and it breaks
what I would guess is the mental model of many who press ^C in a stalled login.
At the very least we should probably document the risk?

--
Daniel Gustafsson

#149Alexander Korotkov
aekorotkov@gmail.com
In reply to: Daniel Gustafsson (#148)
1 attachment(s)
Re: On login trigger: take three

Hi, Daniel!

On Mon, Sep 25, 2023 at 3:42 PM Daniel Gustafsson <daniel@yesql.se> wrote:

On 25 Sep 2023, at 11:13, Alexander Korotkov <aekorotkov@gmail.com> wrote:

I'd like to do a short summary of
design issues on this thread.

Thanks for summarizing this long thread!

the patch for the GUC option to disable
all event triggers resides in a separate thread [4]. Apparently that
patch should be committed first [5].

I have committed the prerequisite patch for temporarily disabling EVTs via a
GUC in 7750fefdb2. We should absolutely avoid creating any more dependencies
on single-user mode (yes, I have changed my mind since the beginning of the
thread).

Thank you for committing 7750fefdb2. I've revised this patch
according to it. I've resolved the conflicts, make use of
event_triggers GUC and adjusted some comments.

3. Yet another question is connection-time overhead introduced by this
patch. The overhead estimate varies from no measurable overhead [6] to
5% overhead [7]. In order to overcome that, [8] has introduced a
database-level flag indicating whether there are connection triggers.
Later this flag was removed [9] in a hope that the possible overhead
is acceptable.

While I disliked the flag, I do think the overhead is problematic. Last time I
profiled it I found it noticeable, and it seems expensive for such a niche
feature to impact such a hot path. Maybe you can think of other ways to reduce
the cost here (if it indeed is an issue in the latest version of the patch,
which is not one I've benchmarked)?

I don't think I can reproduce the performance regression pointed out
by Pavel Stehule [1].

I run a simple ";" sql script (this script doesn't even get the
snapshot) on my laptop and run it multiple times with event_triggers =
on and event_triggers = off;

pgbench -c 10 -j 10 -M prepared -f 1.sql -P 1 -T 60 -C postgres
event_triggers = on
run1: 2261
run2: 2301
run3: 2281
event_triggers = off
run1: 2321
run2: 2277
run3: 2267

pgbench -c 10 -j 10 -M prepared -f 1.sql -P 1 -T 60 -C postgres
event_triggers = on
run1: 731
run2: 740
run3: 733
event_triggers = off
run1: 739
run2: 734
run3: 731

I can't confirm the measurable overhead.

5. It was also pointed out [11] that ^C in psql doesn't cancel
long-running client connection triggers. That might be considered a
psql problem though.

While it is a psql problem, it's exacerbated by a slow login EVT and it breaks
what I would guess is the mental model of many who press ^C in a stalled login.
At the very least we should probably document the risk?

Done in the attached patch.

Links
1. /messages/by-id/CAFj8pRChwu01VLx76nKBVyScHCsd1YnBGiKfDJ6h17g4CSnUBg@mail.gmail.com

------
Regards,
Alexander Korotkov

Attachments:

0001-Add-support-of-event-triggers-on-authenticated-l-v41.patchapplication/octet-stream; name=0001-Add-support-of-event-triggers-on-authenticated-l-v41.patchDownload
From 4f4267e712f3b3aa9ffa15b94d670c0b1897154d Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <akorotkov@postgresql.org>
Date: Thu, 28 Sep 2023 15:07:10 +0300
Subject: [PATCH] Add support of event triggers on authenticated login

The patch introduces trigger on login event, allowing to fire some actions
right on the user connection. This can be useful for  logging or connection
check purposes as well as for some personalization of environment. Usage
details are described in the documentation included, but shortly usage is
the same as for other triggers: create function returning event_trigger and
then create event trigger on login event.
---
 doc/src/sgml/event-trigger.sgml               |  92 ++++++++++
 src/backend/commands/event_trigger.c          |  60 ++++++-
 src/backend/tcop/postgres.c                   |   4 +
 src/backend/utils/cache/evtcache.c            |   2 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  32 ++++
 src/bin/psql/tab-complete.c                   |   4 +-
 src/include/commands/event_trigger.h          |   1 +
 src/include/tcop/cmdtaglist.h                 |   1 +
 src/include/utils/evtcache.h                  |   3 +-
 .../authentication/t/005_login_trigger.pl     | 157 ++++++++++++++++++
 src/test/recovery/t/001_stream_rep.pl         |  23 +++
 src/test/regress/expected/event_trigger.out   |  31 ++++
 src/test/regress/sql/event_trigger.sql        |  21 +++
 13 files changed, 425 insertions(+), 6 deletions(-)
 create mode 100644 src/test/authentication/t/005_login_trigger.pl

diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 3b6a5361b34..0ccc295f131 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>login</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,22 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>login</literal> event occurs when an authenticated user logs
+     into the system. Any bug in a trigger procedure for this event may
+     prevent successful login to the system. Such bugs may be fixed by
+     restarting the system in single-user mode (as event triggers are
+     disabled in this mode). See the <xref linkend="app-postgres"/> reference
+     page for details about using single-user mode.
+     The <literal>login</literal> event will also fire on standby servers.
+     To prevent servers from becoming inaccessible, such triggers must avoid
+     writing anything to the database when running on a standby.
+     Also, it's recommended to evade long-running queries in
+     <literal>login</literal> event triggers.  Notes that, for instance,
+     cancelling connection in <application>psql</application> wouldn't cancel
+     the in-progress <literal>login</literal> trigger.
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1300,4 +1317,79 @@ CREATE EVENT TRIGGER no_rewrite_allowed
 </programlisting>
    </para>
  </sect1>
+
+  <sect1 id="event-trigger-database-login-example">
+    <title>A Database Login Event Trigger Example</title>
+
+    <para>
+      The event trigger on the <literal>login</literal> event can be
+      useful for logging user logins, for verifying the connection and
+      assigning roles according to current circumstances, or for session
+      data initialization. It is very important that any event trigger using
+      the <literal>login</literal> event checks whether or not the database is
+      in recovery before performing any writes. Writing to a standby server
+      will make it inaccessible.
+    </para>
+
+    <para>
+      The following example demonstrates these options.
+<programlisting>
+-- create test tables and roles
+CREATE TABLE user_login_log (
+  "user" text,
+  "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+  RETURNS event_trigger SECURITY DEFINER
+  LANGUAGE plpgsql AS
+$$
+DECLARE
+  hour integer = EXTRACT('hour' FROM current_time at time zone 'utc');
+  rec boolean;
+BEGIN
+-- 1. Forbid logging in between 2AM and 4AM.
+IF hour BETWEEN 2 AND 4 THEN
+  RAISE EXCEPTION 'Login forbidden';
+END IF;
+
+-- The checks below cannot be performed on standby servers so
+-- ensure the database is not in recovery before we perform any
+-- operations.
+SELECT pg_is_in_recovery() INTO rec;
+IF rec THEN
+  RETURN;
+END IF;
+
+-- 2. Assign some roles. At daytime, grant the day_worker role, else the
+-- night_worker role.
+IF hour BETWEEN 8 AND 20 THEN
+  EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT day_worker TO ' || quote_ident(session_user);
+ELSE
+  EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT night_worker TO ' || quote_ident(session_user);
+END IF;
+
+-- 3. Initialize user session data
+CREATE TEMP TABLE session_storage (x float, y integer);
+ALTER TABLE session_storage OWNER TO session_user;
+
+-- 4. Log the connection time
+INSERT INTO public.user_login_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+  ON login
+  EXECUTE FUNCTION init_session();
+ALTER EVENT TRIGGER init_session ENABLE ALWAYS;
+</programlisting>
+    </para>
+  </sect1>
 </chapter>
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index bd812e42d94..0a5649f84dd 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -37,15 +38,18 @@
 #include "miscadmin.h"
 #include "parser/parse_func.h"
 #include "pgstat.h"
+#include "storage/lmgr.h"
 #include "tcop/deparse_utility.h"
 #include "tcop/utility.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 typedef struct EventTriggerQueryState
@@ -133,6 +137,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "login") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -582,10 +587,15 @@ EventTriggerCommonSetup(Node *parsetree,
 	{
 		CommandTag	dbgtag;
 
-		dbgtag = CreateCommandTag(parsetree);
+		if (event == EVT_Login)
+			dbgtag = CMDTAG_LOGIN;
+		else
+			dbgtag = CreateCommandTag(parsetree);
+
 		if (event == EVT_DDLCommandStart ||
 			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
+			event == EVT_SQLDrop ||
+			event == EVT_Login)
 		{
 			if (!command_tag_event_trigger_ok(dbgtag))
 				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
@@ -604,7 +614,10 @@ EventTriggerCommonSetup(Node *parsetree,
 		return NIL;
 
 	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
+	if (event == EVT_Login)
+		tag = CMDTAG_LOGIN;
+	else
+		tag = CreateCommandTag(parsetree);
 
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
@@ -805,6 +818,47 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Fire login event triggers if any are present.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode or via a GUC.  We also need a
+	 * database connection (some background workers doesn't have it).
+	 */
+	if (!IsUnderPostmaster || !event_triggers || !OidIsValid(MyDatabaseId))
+		return;
+
+	StartTransactionCommand();
+
+	/* Find login triggers and run what was found */
+	runlist = EventTriggerCommonSetup(NULL,
+									  EVT_Login, "login",
+									  &trigdata);
+
+	if (runlist != NIL)
+	{
+		/* Event trigger execution may require an active snapshot. */
+		PushActiveSnapshot(GetTransactionSnapshot());
+
+		/* Run the triggers. */
+		EventTriggerInvoke(runlist, &trigdata);
+
+		/* Cleanup. */
+		list_free(runlist);
+
+		PopActiveSnapshot();
+	}
+
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 21b9763183e..20d8202cb7d 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -36,6 +36,7 @@
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/prepare.h"
 #include "common/pg_prng.h"
 #include "jit/jit.h"
@@ -4288,6 +4289,9 @@ PostgresMain(const char *dbname, const char *username)
 	initStringInfo(&row_description_buf);
 	MemoryContextSwitchTo(TopMemoryContext);
 
+	/* Fire any defined login event triggers, if appropriate */
+	EventTriggerOnLogin();
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index b080f7a35f3..ab5111c90fd 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "login") == 0)
+			event = EVT_Login;
 		else
 			continue;
 
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 55e98ec8e39..04813d0e511 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2133,6 +2133,25 @@ my %tests = (
 		},
 	},
 
+	'CREATE FUNCTION dump_test.login_event_trigger_func' => {
+		create_order => 32,
+		create_sql   => 'CREATE FUNCTION dump_test.login_event_trigger_func()
+					   RETURNS event_trigger LANGUAGE plpgsql
+					   AS $$ BEGIN RETURN; END;$$;',
+		regexp => qr/^
+			\QCREATE FUNCTION dump_test.login_event_trigger_func() RETURNS event_trigger\E
+			\n\s+\QLANGUAGE plpgsql\E
+			\n\s+AS\ \$\$
+			\Q BEGIN RETURN; END;\E
+			\$\$;/xm,
+		like =>
+		  { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			only_dump_measurement    => 1,
+		},
+	},
+
 	'CREATE OPERATOR FAMILY dump_test.op_family' => {
 		create_order => 73,
 		create_sql =>
@@ -2245,6 +2264,19 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE EVENT TRIGGER test_login_event_trigger' => {
+		create_order => 33,
+		create_sql   => 'CREATE EVENT TRIGGER test_login_event_trigger
+					   ON login
+					   EXECUTE FUNCTION dump_test.login_event_trigger_func();',
+		regexp => qr/^
+			\QCREATE EVENT TRIGGER test_login_event_trigger \E
+			\QON login\E
+			\n\s+\QEXECUTE FUNCTION dump_test.login_event_trigger_func();\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE TRIGGER test_trigger' => {
 		create_order => 31,
 		create_sql => 'CREATE TRIGGER test_trigger
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index d30d719a1f8..2eb8ff1afca 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3552,8 +3552,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop",
-					  "table_rewrite");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "login",
+					  "sql_drop", "table_rewrite");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 1c925dbf257..9e3ece50d5f 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -56,6 +56,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnLogin(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index e738ac1c097..553a31874f1 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
+PG_CMDTAG(CMDTAG_LOGIN, "LOGIN", true, false, false)
 PG_CMDTAG(CMDTAG_MERGE, "MERGE", false, false, true)
 PG_CMDTAG(CMDTAG_MOVE, "MOVE", false, false, true)
 PG_CMDTAG(CMDTAG_NOTIFY, "NOTIFY", false, false, false)
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index d340026518a..52052e6252a 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Login,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/authentication/t/005_login_trigger.pl b/src/test/authentication/t/005_login_trigger.pl
new file mode 100644
index 00000000000..fec664fb865
--- /dev/null
+++ b/src/test/authentication/t/005_login_trigger.pl
@@ -0,0 +1,157 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+# Tests of authentication via login trigger. Mostly for rejection via
+# exception, because this scenario cannot be covered with *.sql/*.out regress
+# tests.
+
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Execute a psql command and compare its output towards given regexps
+sub psql_command
+{
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+	my ($node, $sql, $expected_ret, $test_name, %params) = @_;
+
+	my $connstr;
+	if (defined($params{connstr}))
+	{
+		$connstr = $params{connstr};
+	}
+	else
+	{
+		$connstr = '';
+	}
+
+	# Execute command
+	my ($ret, $stdout, $stderr) = $node->psql('postgres', $sql,
+											  connstr => "$connstr");
+
+	# Check return code
+	is($ret, $expected_ret,  "$test_name: exit code $expected_ret");
+
+	# Check stdout
+	if (defined($params{log_like}))
+	{
+		my @log_like = @{ $params{log_like} };
+		while (my $regex = shift @log_like)
+		{
+			like($stdout, $regex, "$test_name: log matches");
+		}
+	}
+	if (defined($params{log_unlike}))
+	{
+		my @log_unlike = @{ $params{log_unlike} };
+		while (my $regex = shift @log_unlike)
+		{
+			unlike($stdout, $regex, "$test_name: log unmatches");
+		}
+	}
+	if (defined($params{log_exact}))
+	{
+		is($stdout, $params{log_exact}, "$test_name: log equals");
+	}
+
+	# Check stderr
+	if (defined($params{err_like}))
+	{
+		my @err_like = @{ $params{err_like} };
+		while (my $regex = shift @err_like)
+		{
+			like($stderr, $regex, "$test_name: err matches");
+		}
+	}
+	if (defined($params{err_unlike}))
+	{
+		my @err_unlike = @{ $params{err_unlike} };
+		while (my $regex = shift @err_unlike)
+		{
+			unlike($stderr, $regex, "$test_name: err unmatches");
+		}
+	}
+	if (defined($params{err_exact}))
+	{
+		is($stderr, $params{err_exact}, "$test_name: err equals");
+	}
+
+	return;
+}
+
+# New node
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init(extra => [ '--locale=C', '--encoding=UTF8' ]);
+$node->append_conf(
+	'postgresql.conf', q{
+wal_level = 'logical'
+max_replication_slots = 4
+max_wal_senders = 4
+});
+$node->start;
+
+# Create temporary roles and log table
+psql_command($node, 'CREATE ROLE alice WITH LOGIN;
+CREATE ROLE mallory WITH LOGIN;
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+', 0, 'create tmp objects', log_exact => '', err_exact => ''), ;
+
+# Create login event function and trigger
+psql_command($node,
+			 'CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  IF SESSION_USER = \'mallory\' THEN
+    RAISE EXCEPTION \'Hello %! You are NOT welcome here!\', SESSION_USER;
+  END IF;
+  RAISE NOTICE \'Hello %! You are welcome!\', SESSION_USER;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+', 0, 'create trigger function', log_exact => '', err_exact => '');
+
+psql_command($node,
+			 'CREATE EVENT TRIGGER on_login_trigger '
+			 .'ON login EXECUTE PROCEDURE on_login_proc();', 0,
+			 'create event trigger', log_exact => '', err_exact => '');
+psql_command($node, 'ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;', 0,
+			 'alter event trigger', log_exact => '',
+			 err_like => [qr/You are welcome/]);
+
+# Check the two requests were logged via login trigger
+psql_command($node, 'SELECT COUNT(*) FROM user_logins;', 0, 'select count',
+			 log_exact => '2', err_like => [qr/You are welcome/]);
+
+# Try to log as allowed Alice and disallowed Mallory (two times)
+psql_command($node, 'SELECT 1;', 0, 'try alice', connstr => 'user=alice',
+			 log_exact => '1', err_like => [qr/You are welcome/],
+			 err_unlike => [qr/You are NOT welcome/]);
+psql_command($node, 'SELECT 1;', 2, 'try mallory', connstr => 'user=mallory',
+			 log_exact => '', err_like => [qr/You are NOT welcome/],
+			 err_unlike => [qr/You are welcome/]);
+psql_command($node, 'SELECT 1;', 2, 'try mallory', connstr => 'user=mallory',
+			 log_exact => '', err_like => [qr/You are NOT welcome/],
+			 err_unlike => [qr/You are welcome/]);
+
+# Check that Alice's login record is here, while the Mallory's one is not
+psql_command($node, 'SELECT * FROM user_logins;', 0, 'select *',
+			 log_like => [qr/3\|alice/], log_unlike => [qr/mallory/],
+			 err_like => [qr/You are welcome/]);
+
+# Check total number of successful logins so far
+psql_command($node, 'SELECT COUNT(*) FROM user_logins;', 0, 'select count',
+			 log_exact => '5', err_like => [qr/You are welcome/]);
+
+# Cleanup the temporary stuff
+psql_command($node, 'DROP EVENT TRIGGER on_login_trigger;', 0,
+			 'drop event trigger', log_exact => '',
+			 err_like => [qr/You are welcome/]);
+psql_command($node, 'DROP TABLE user_logins;
+DROP FUNCTION on_login_proc;
+DROP ROLE mallory;
+DROP ROLE alice;
+', 0, 'cleanup', log_exact => '', err_exact => '');
+
+done_testing();
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 0c72ba09441..8644854eb39 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,24 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE TABLE user_logins(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO user_logins (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 $node_primary->wait_for_replay_catchup($node_standby_1);
 $node_standby_1->wait_for_replay_catchup($node_standby_2, $node_primary);
@@ -384,6 +402,11 @@ sub replay_check
 
 replay_check();
 
+my $evttrig = $node_standby_1->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+$evttrig = $node_standby_2->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 0b87a42d0a9..a5f6ca76e49 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -638,3 +638,34 @@ NOTICE:  DROP POLICY dropped policy
 CREATE POLICY pguc ON event_trigger_test USING (FALSE);
 SET event_triggers = 'off';
 DROP POLICY pguc ON event_trigger_test;
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     1
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     2
+(1 row)
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 6f0933b9e88..8340683fe10 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -495,3 +495,24 @@ DROP POLICY pguc ON event_trigger_test;
 CREATE POLICY pguc ON event_trigger_test USING (FALSE);
 SET event_triggers = 'off';
 DROP POLICY pguc ON event_trigger_test;
+
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+SELECT COUNT(*) FROM user_logins;
+\c
+SELECT COUNT(*) FROM user_logins;
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
-- 
2.37.1 (Apple Git-137.1)

#150Daniel Gustafsson
daniel@yesql.se
In reply to: Alexander Korotkov (#149)
Re: On login trigger: take three

On 28 Sep 2023, at 23:50, Alexander Korotkov <aekorotkov@gmail.com> wrote:

I don't think I can reproduce the performance regression pointed out
by Pavel Stehule [1].

I can't confirm the measurable overhead.

Running the same pgbench command on my laptop looking at the average connection
times, and the averaging that over five runs (low/avg/high) I see ~5% increase
over master with the patched version (compiled without assertions and debug):

Patched event_triggers on: 6.858 ms/7.038 ms/7.434 ms
Patched event_triggers off: 6.601 ms/6.958 ms/7.539 ms
Master: 6.676 ms/6.697 ms/6.760 ms

This is all quite unscientific with a lot of jitter so grains of salt are to be
applied, but I find it odd that you don't see any measurable effect. Are you
seeing the same/similar connection times between master and with this patch
applied?

A few small comments on the patch:

+     prevent successful login to the system. Such bugs may be fixed by
+     restarting the system in single-user mode (as event triggers are
This paragraph should be reworded to recommend the GUC instead of single-user
mode (while retaining mention of single-user mode, just not as the primary
option).

+ Also, it's recommended to evade long-running queries in
s/evade/avoid/ perhaps?

Thanks for working on this!

--
Daniel Gustafsson

#151Alexander Korotkov
aekorotkov@gmail.com
In reply to: Daniel Gustafsson (#150)
1 attachment(s)
Re: On login trigger: take three

On Fri, Sep 29, 2023 at 1:15 PM Daniel Gustafsson <daniel@yesql.se> wrote:

On 28 Sep 2023, at 23:50, Alexander Korotkov <aekorotkov@gmail.com> wrote:

I don't think I can reproduce the performance regression pointed out
by Pavel Stehule [1].

I can't confirm the measurable overhead.

Running the same pgbench command on my laptop looking at the average connection
times, and the averaging that over five runs (low/avg/high) I see ~5% increase
over master with the patched version (compiled without assertions and debug):

Patched event_triggers on: 6.858 ms/7.038 ms/7.434 ms
Patched event_triggers off: 6.601 ms/6.958 ms/7.539 ms
Master: 6.676 ms/6.697 ms/6.760 ms

This is all quite unscientific with a lot of jitter so grains of salt are to be
applied, but I find it odd that you don't see any measurable effect. Are you
seeing the same/similar connection times between master and with this patch
applied?

Thank you for doing experiments on your side. I've rechecked. It
appears that I didn't do enough runs, thus I didn't see the overhead
as more than an error. Now, I also can confirm ~5% overhead.

I spent some time thinking about how to overcome this overhead, but I
didn't find a brilliant option. Previously pg_database flag was
proposed but then criticized as complex and error-prone. I can also
imagine shmem caching mechanism. But it would require overcoming
possible race conditions between shared cache invalidation and
transaction commit etc. So, that would be also complex and
error-prone. Any better ideas?

A few small comments on the patch:

+     prevent successful login to the system. Such bugs may be fixed by
+     restarting the system in single-user mode (as event triggers are
This paragraph should be reworded to recommend the GUC instead of single-user
mode (while retaining mention of single-user mode, just not as the primary
option).

+ Also, it's recommended to evade long-running queries in
s/evade/avoid/ perhaps?

Fixed.

Thanks for working on this!

Thank you as well!

------
Regards,
Alexander Korotkov

Attachments:

0001-Add-support-of-event-triggers-on-authenticated-l-v42.patchapplication/octet-stream; name=0001-Add-support-of-event-triggers-on-authenticated-l-v42.patchDownload
From 37e7779c5c44224d987a987538fb550fc7e067a3 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <akorotkov@postgresql.org>
Date: Thu, 28 Sep 2023 15:07:10 +0300
Subject: [PATCH] Add support of event triggers on authenticated login

The patch introduces trigger on login event, allowing to fire some actions
right on the user connection. This can be useful for  logging or connection
check purposes as well as for some personalization of environment. Usage
details are described in the documentation included, but shortly usage is
the same as for other triggers: create function returning event_trigger and
then create event trigger on login event.
---
 doc/src/sgml/event-trigger.sgml               |  94 +++++++++++
 src/backend/commands/event_trigger.c          |  60 ++++++-
 src/backend/tcop/postgres.c                   |   4 +
 src/backend/utils/cache/evtcache.c            |   2 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  32 ++++
 src/bin/psql/tab-complete.c                   |   4 +-
 src/include/commands/event_trigger.h          |   1 +
 src/include/tcop/cmdtaglist.h                 |   1 +
 src/include/utils/evtcache.h                  |   3 +-
 .../authentication/t/005_login_trigger.pl     | 157 ++++++++++++++++++
 src/test/recovery/t/001_stream_rep.pl         |  23 +++
 src/test/regress/expected/event_trigger.out   |  31 ++++
 src/test/regress/sql/event_trigger.sql        |  21 +++
 13 files changed, 427 insertions(+), 6 deletions(-)
 create mode 100644 src/test/authentication/t/005_login_trigger.pl

diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 3b6a5361b34..10b20f0339a 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>login</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,24 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>login</literal> event occurs when an authenticated user logs
+     into the system. Any bug in a trigger procedure for this event may
+     prevent successful login to the system. Such bugs may be fixed by
+     setting <xref linkend="guc-event-triggers"/> is set to <literal>false</literal>
+     either in a connection string or configuration file. Alternative is
+     restarting the system in single-user mode (as event triggers are
+     disabled in this mode). See the <xref linkend="app-postgres"/> reference
+     page for details about using single-user mode.
+     The <literal>login</literal> event will also fire on standby servers.
+     To prevent servers from becoming inaccessible, such triggers must avoid
+     writing anything to the database when running on a standby.
+     Also, it's recommended to avoid long-running queries in
+     <literal>login</literal> event triggers.  Notes that, for instance,
+     cancelling connection in <application>psql</application> wouldn't cancel
+     the in-progress <literal>login</literal> trigger.
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1300,4 +1319,79 @@ CREATE EVENT TRIGGER no_rewrite_allowed
 </programlisting>
    </para>
  </sect1>
+
+  <sect1 id="event-trigger-database-login-example">
+    <title>A Database Login Event Trigger Example</title>
+
+    <para>
+      The event trigger on the <literal>login</literal> event can be
+      useful for logging user logins, for verifying the connection and
+      assigning roles according to current circumstances, or for session
+      data initialization. It is very important that any event trigger using
+      the <literal>login</literal> event checks whether or not the database is
+      in recovery before performing any writes. Writing to a standby server
+      will make it inaccessible.
+    </para>
+
+    <para>
+      The following example demonstrates these options.
+<programlisting>
+-- create test tables and roles
+CREATE TABLE user_login_log (
+  "user" text,
+  "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+  RETURNS event_trigger SECURITY DEFINER
+  LANGUAGE plpgsql AS
+$$
+DECLARE
+  hour integer = EXTRACT('hour' FROM current_time at time zone 'utc');
+  rec boolean;
+BEGIN
+-- 1. Forbid logging in between 2AM and 4AM.
+IF hour BETWEEN 2 AND 4 THEN
+  RAISE EXCEPTION 'Login forbidden';
+END IF;
+
+-- The checks below cannot be performed on standby servers so
+-- ensure the database is not in recovery before we perform any
+-- operations.
+SELECT pg_is_in_recovery() INTO rec;
+IF rec THEN
+  RETURN;
+END IF;
+
+-- 2. Assign some roles. At daytime, grant the day_worker role, else the
+-- night_worker role.
+IF hour BETWEEN 8 AND 20 THEN
+  EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT day_worker TO ' || quote_ident(session_user);
+ELSE
+  EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT night_worker TO ' || quote_ident(session_user);
+END IF;
+
+-- 3. Initialize user session data
+CREATE TEMP TABLE session_storage (x float, y integer);
+ALTER TABLE session_storage OWNER TO session_user;
+
+-- 4. Log the connection time
+INSERT INTO public.user_login_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+  ON login
+  EXECUTE FUNCTION init_session();
+ALTER EVENT TRIGGER init_session ENABLE ALWAYS;
+</programlisting>
+    </para>
+  </sect1>
 </chapter>
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index bd812e42d94..0a5649f84dd 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -37,15 +38,18 @@
 #include "miscadmin.h"
 #include "parser/parse_func.h"
 #include "pgstat.h"
+#include "storage/lmgr.h"
 #include "tcop/deparse_utility.h"
 #include "tcop/utility.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 typedef struct EventTriggerQueryState
@@ -133,6 +137,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "login") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -582,10 +587,15 @@ EventTriggerCommonSetup(Node *parsetree,
 	{
 		CommandTag	dbgtag;
 
-		dbgtag = CreateCommandTag(parsetree);
+		if (event == EVT_Login)
+			dbgtag = CMDTAG_LOGIN;
+		else
+			dbgtag = CreateCommandTag(parsetree);
+
 		if (event == EVT_DDLCommandStart ||
 			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
+			event == EVT_SQLDrop ||
+			event == EVT_Login)
 		{
 			if (!command_tag_event_trigger_ok(dbgtag))
 				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
@@ -604,7 +614,10 @@ EventTriggerCommonSetup(Node *parsetree,
 		return NIL;
 
 	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
+	if (event == EVT_Login)
+		tag = CMDTAG_LOGIN;
+	else
+		tag = CreateCommandTag(parsetree);
 
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
@@ -805,6 +818,47 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Fire login event triggers if any are present.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode or via a GUC.  We also need a
+	 * database connection (some background workers doesn't have it).
+	 */
+	if (!IsUnderPostmaster || !event_triggers || !OidIsValid(MyDatabaseId))
+		return;
+
+	StartTransactionCommand();
+
+	/* Find login triggers and run what was found */
+	runlist = EventTriggerCommonSetup(NULL,
+									  EVT_Login, "login",
+									  &trigdata);
+
+	if (runlist != NIL)
+	{
+		/* Event trigger execution may require an active snapshot. */
+		PushActiveSnapshot(GetTransactionSnapshot());
+
+		/* Run the triggers. */
+		EventTriggerInvoke(runlist, &trigdata);
+
+		/* Cleanup. */
+		list_free(runlist);
+
+		PopActiveSnapshot();
+	}
+
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 21b9763183e..20d8202cb7d 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -36,6 +36,7 @@
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/prepare.h"
 #include "common/pg_prng.h"
 #include "jit/jit.h"
@@ -4288,6 +4289,9 @@ PostgresMain(const char *dbname, const char *username)
 	initStringInfo(&row_description_buf);
 	MemoryContextSwitchTo(TopMemoryContext);
 
+	/* Fire any defined login event triggers, if appropriate */
+	EventTriggerOnLogin();
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index b080f7a35f3..ab5111c90fd 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "login") == 0)
+			event = EVT_Login;
 		else
 			continue;
 
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 55e98ec8e39..04813d0e511 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2133,6 +2133,25 @@ my %tests = (
 		},
 	},
 
+	'CREATE FUNCTION dump_test.login_event_trigger_func' => {
+		create_order => 32,
+		create_sql   => 'CREATE FUNCTION dump_test.login_event_trigger_func()
+					   RETURNS event_trigger LANGUAGE plpgsql
+					   AS $$ BEGIN RETURN; END;$$;',
+		regexp => qr/^
+			\QCREATE FUNCTION dump_test.login_event_trigger_func() RETURNS event_trigger\E
+			\n\s+\QLANGUAGE plpgsql\E
+			\n\s+AS\ \$\$
+			\Q BEGIN RETURN; END;\E
+			\$\$;/xm,
+		like =>
+		  { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			only_dump_measurement    => 1,
+		},
+	},
+
 	'CREATE OPERATOR FAMILY dump_test.op_family' => {
 		create_order => 73,
 		create_sql =>
@@ -2245,6 +2264,19 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE EVENT TRIGGER test_login_event_trigger' => {
+		create_order => 33,
+		create_sql   => 'CREATE EVENT TRIGGER test_login_event_trigger
+					   ON login
+					   EXECUTE FUNCTION dump_test.login_event_trigger_func();',
+		regexp => qr/^
+			\QCREATE EVENT TRIGGER test_login_event_trigger \E
+			\QON login\E
+			\n\s+\QEXECUTE FUNCTION dump_test.login_event_trigger_func();\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE TRIGGER test_trigger' => {
 		create_order => 31,
 		create_sql => 'CREATE TRIGGER test_trigger
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index d30d719a1f8..2eb8ff1afca 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3552,8 +3552,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop",
-					  "table_rewrite");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "login",
+					  "sql_drop", "table_rewrite");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 1c925dbf257..9e3ece50d5f 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -56,6 +56,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnLogin(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index e738ac1c097..553a31874f1 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
+PG_CMDTAG(CMDTAG_LOGIN, "LOGIN", true, false, false)
 PG_CMDTAG(CMDTAG_MERGE, "MERGE", false, false, true)
 PG_CMDTAG(CMDTAG_MOVE, "MOVE", false, false, true)
 PG_CMDTAG(CMDTAG_NOTIFY, "NOTIFY", false, false, false)
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index d340026518a..52052e6252a 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Login,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/authentication/t/005_login_trigger.pl b/src/test/authentication/t/005_login_trigger.pl
new file mode 100644
index 00000000000..fec664fb865
--- /dev/null
+++ b/src/test/authentication/t/005_login_trigger.pl
@@ -0,0 +1,157 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+# Tests of authentication via login trigger. Mostly for rejection via
+# exception, because this scenario cannot be covered with *.sql/*.out regress
+# tests.
+
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Execute a psql command and compare its output towards given regexps
+sub psql_command
+{
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+	my ($node, $sql, $expected_ret, $test_name, %params) = @_;
+
+	my $connstr;
+	if (defined($params{connstr}))
+	{
+		$connstr = $params{connstr};
+	}
+	else
+	{
+		$connstr = '';
+	}
+
+	# Execute command
+	my ($ret, $stdout, $stderr) = $node->psql('postgres', $sql,
+											  connstr => "$connstr");
+
+	# Check return code
+	is($ret, $expected_ret,  "$test_name: exit code $expected_ret");
+
+	# Check stdout
+	if (defined($params{log_like}))
+	{
+		my @log_like = @{ $params{log_like} };
+		while (my $regex = shift @log_like)
+		{
+			like($stdout, $regex, "$test_name: log matches");
+		}
+	}
+	if (defined($params{log_unlike}))
+	{
+		my @log_unlike = @{ $params{log_unlike} };
+		while (my $regex = shift @log_unlike)
+		{
+			unlike($stdout, $regex, "$test_name: log unmatches");
+		}
+	}
+	if (defined($params{log_exact}))
+	{
+		is($stdout, $params{log_exact}, "$test_name: log equals");
+	}
+
+	# Check stderr
+	if (defined($params{err_like}))
+	{
+		my @err_like = @{ $params{err_like} };
+		while (my $regex = shift @err_like)
+		{
+			like($stderr, $regex, "$test_name: err matches");
+		}
+	}
+	if (defined($params{err_unlike}))
+	{
+		my @err_unlike = @{ $params{err_unlike} };
+		while (my $regex = shift @err_unlike)
+		{
+			unlike($stderr, $regex, "$test_name: err unmatches");
+		}
+	}
+	if (defined($params{err_exact}))
+	{
+		is($stderr, $params{err_exact}, "$test_name: err equals");
+	}
+
+	return;
+}
+
+# New node
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init(extra => [ '--locale=C', '--encoding=UTF8' ]);
+$node->append_conf(
+	'postgresql.conf', q{
+wal_level = 'logical'
+max_replication_slots = 4
+max_wal_senders = 4
+});
+$node->start;
+
+# Create temporary roles and log table
+psql_command($node, 'CREATE ROLE alice WITH LOGIN;
+CREATE ROLE mallory WITH LOGIN;
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+', 0, 'create tmp objects', log_exact => '', err_exact => ''), ;
+
+# Create login event function and trigger
+psql_command($node,
+			 'CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  IF SESSION_USER = \'mallory\' THEN
+    RAISE EXCEPTION \'Hello %! You are NOT welcome here!\', SESSION_USER;
+  END IF;
+  RAISE NOTICE \'Hello %! You are welcome!\', SESSION_USER;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+', 0, 'create trigger function', log_exact => '', err_exact => '');
+
+psql_command($node,
+			 'CREATE EVENT TRIGGER on_login_trigger '
+			 .'ON login EXECUTE PROCEDURE on_login_proc();', 0,
+			 'create event trigger', log_exact => '', err_exact => '');
+psql_command($node, 'ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;', 0,
+			 'alter event trigger', log_exact => '',
+			 err_like => [qr/You are welcome/]);
+
+# Check the two requests were logged via login trigger
+psql_command($node, 'SELECT COUNT(*) FROM user_logins;', 0, 'select count',
+			 log_exact => '2', err_like => [qr/You are welcome/]);
+
+# Try to log as allowed Alice and disallowed Mallory (two times)
+psql_command($node, 'SELECT 1;', 0, 'try alice', connstr => 'user=alice',
+			 log_exact => '1', err_like => [qr/You are welcome/],
+			 err_unlike => [qr/You are NOT welcome/]);
+psql_command($node, 'SELECT 1;', 2, 'try mallory', connstr => 'user=mallory',
+			 log_exact => '', err_like => [qr/You are NOT welcome/],
+			 err_unlike => [qr/You are welcome/]);
+psql_command($node, 'SELECT 1;', 2, 'try mallory', connstr => 'user=mallory',
+			 log_exact => '', err_like => [qr/You are NOT welcome/],
+			 err_unlike => [qr/You are welcome/]);
+
+# Check that Alice's login record is here, while the Mallory's one is not
+psql_command($node, 'SELECT * FROM user_logins;', 0, 'select *',
+			 log_like => [qr/3\|alice/], log_unlike => [qr/mallory/],
+			 err_like => [qr/You are welcome/]);
+
+# Check total number of successful logins so far
+psql_command($node, 'SELECT COUNT(*) FROM user_logins;', 0, 'select count',
+			 log_exact => '5', err_like => [qr/You are welcome/]);
+
+# Cleanup the temporary stuff
+psql_command($node, 'DROP EVENT TRIGGER on_login_trigger;', 0,
+			 'drop event trigger', log_exact => '',
+			 err_like => [qr/You are welcome/]);
+psql_command($node, 'DROP TABLE user_logins;
+DROP FUNCTION on_login_proc;
+DROP ROLE mallory;
+DROP ROLE alice;
+', 0, 'cleanup', log_exact => '', err_exact => '');
+
+done_testing();
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 0c72ba09441..8644854eb39 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,24 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE TABLE user_logins(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO user_logins (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 $node_primary->wait_for_replay_catchup($node_standby_1);
 $node_standby_1->wait_for_replay_catchup($node_standby_2, $node_primary);
@@ -384,6 +402,11 @@ sub replay_check
 
 replay_check();
 
+my $evttrig = $node_standby_1->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+$evttrig = $node_standby_2->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 0b87a42d0a9..a5f6ca76e49 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -638,3 +638,34 @@ NOTICE:  DROP POLICY dropped policy
 CREATE POLICY pguc ON event_trigger_test USING (FALSE);
 SET event_triggers = 'off';
 DROP POLICY pguc ON event_trigger_test;
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     1
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     2
+(1 row)
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 6f0933b9e88..8340683fe10 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -495,3 +495,24 @@ DROP POLICY pguc ON event_trigger_test;
 CREATE POLICY pguc ON event_trigger_test USING (FALSE);
 SET event_triggers = 'off';
 DROP POLICY pguc ON event_trigger_test;
+
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+SELECT COUNT(*) FROM user_logins;
+\c
+SELECT COUNT(*) FROM user_logins;
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
-- 
2.37.1 (Apple Git-137.1)

#152Robert Haas
robertmhaas@gmail.com
In reply to: Daniel Gustafsson (#150)
Re: On login trigger: take three

Sorry to have gone dark on this for a long time after having been
asked for my input back in March. I'm not having a great time trying
to keep up with email, and the threads getting split up makes it a lot
worse for me.

On Fri, Sep 29, 2023 at 6:15 AM Daniel Gustafsson <daniel@yesql.se> wrote:

Running the same pgbench command on my laptop looking at the average connection
times, and the averaging that over five runs (low/avg/high) I see ~5% increase
over master with the patched version (compiled without assertions and debug):

Patched event_triggers on: 6.858 ms/7.038 ms/7.434 ms
Patched event_triggers off: 6.601 ms/6.958 ms/7.539 ms
Master: 6.676 ms/6.697 ms/6.760 ms

This seems kind of crazy to me. Why does it happen? It sounds to me
like we must be doing a lot of extra catalog access to find out
whether there are any on-login event triggers. Like maybe a sequential
scan of pg_event_trigger. Maybe we need to engineer a way to avoid
that. I don't have a brilliant idea off-hand, but I feel like there
should be something we can do. I think a lot of users would say that
logins on PostgreSQL are too slow already.

--
Robert Haas
EDB: http://www.enterprisedb.com

#153Daniel Gustafsson
daniel@yesql.se
In reply to: Robert Haas (#152)
Re: On login trigger: take three

On 2 Oct 2023, at 20:10, Robert Haas <robertmhaas@gmail.com> wrote:

Sorry to have gone dark on this for a long time after having been
asked for my input back in March. I'm not having a great time trying
to keep up with email, and the threads getting split up makes it a lot
worse for me.

Not a problem, thanks for chiming in.

On Fri, Sep 29, 2023 at 6:15 AM Daniel Gustafsson <daniel@yesql.se> wrote:

Running the same pgbench command on my laptop looking at the average connection
times, and the averaging that over five runs (low/avg/high) I see ~5% increase
over master with the patched version (compiled without assertions and debug):

Patched event_triggers on: 6.858 ms/7.038 ms/7.434 ms
Patched event_triggers off: 6.601 ms/6.958 ms/7.539 ms
Master: 6.676 ms/6.697 ms/6.760 ms

This seems kind of crazy to me. Why does it happen? It sounds to me
like we must be doing a lot of extra catalog access to find out
whether there are any on-login event triggers. Like maybe a sequential
scan of pg_event_trigger.

That's exactly what happens, the patch is using BuildEventTriggerCache() to
build the hash for EVT which is then checked for login triggers. This is
clearly the bottleneck and there needs to be a fast-path. There used to be a
cache flag in an earlier version of the patch but it was a but klugy, a version
of that needs to be reimplemented for this patch to fly.

I think a lot of users would say that logins on PostgreSQL are too slow already.

Agreed.

--
Daniel Gustafsson

#154Robert Haas
robertmhaas@gmail.com
In reply to: Daniel Gustafsson (#153)
Re: On login trigger: take three

On Tue, Oct 3, 2023 at 9:43 AM Daniel Gustafsson <daniel@yesql.se> wrote:

That's exactly what happens, the patch is using BuildEventTriggerCache() to
build the hash for EVT which is then checked for login triggers. This is
clearly the bottleneck and there needs to be a fast-path. There used to be a
cache flag in an earlier version of the patch but it was a but klugy, a version
of that needs to be reimplemented for this patch to fly.

So I haven't looked at this patch, but we basically saying that only
the superuser can create login triggers, and if they do, those
triggers apply to every single user on the system? That would seem to
be the logical extension of the existing event trigger mechanism, but
it isn't obviously as good of a fit for this case as it is for other
cases where event triggers are a thing.

Changing the catalog representation could be a way around this. What
if you only allowed one login trigger per database, and instead of
being stored in pg_event_trigger, the OID of the function gets
recorded in the pg_database row? Then this would be a lot cheaper
since we have to fetch the pg_database row anyway. Or change the SQL
syntax to something entirely new so you can have different login
triggers for different users -- and maybe users are allowed to create
their own -- but the relevant ones can be found by an index scan
instead of a sequential scan.

I'm just spitballing here. If you think the present design is good and
just want to try to speed it up, I'm not deeply opposed to that. But
it's also not obvious to me how to stick a cache in front of something
that's basically a full-table scan.

--
Robert Haas
EDB: http://www.enterprisedb.com

#155Alexander Korotkov
aekorotkov@gmail.com
In reply to: Robert Haas (#154)
Re: On login trigger: take three

Hi, Robert!

On Tue, Oct 3, 2023 at 5:21 PM Robert Haas <robertmhaas@gmail.com> wrote:

On Tue, Oct 3, 2023 at 9:43 AM Daniel Gustafsson <daniel@yesql.se> wrote:

That's exactly what happens, the patch is using BuildEventTriggerCache() to
build the hash for EVT which is then checked for login triggers. This is
clearly the bottleneck and there needs to be a fast-path. There used to be a
cache flag in an earlier version of the patch but it was a but klugy, a version
of that needs to be reimplemented for this patch to fly.

So I haven't looked at this patch, but we basically saying that only
the superuser can create login triggers, and if they do, those
triggers apply to every single user on the system? That would seem to
be the logical extension of the existing event trigger mechanism, but
it isn't obviously as good of a fit for this case as it is for other
cases where event triggers are a thing.

Changing the catalog representation could be a way around this. What
if you only allowed one login trigger per database, and instead of
being stored in pg_event_trigger, the OID of the function gets
recorded in the pg_database row? Then this would be a lot cheaper
since we have to fetch the pg_database row anyway. Or change the SQL
syntax to something entirely new so you can have different login
triggers for different users -- and maybe users are allowed to create
their own -- but the relevant ones can be found by an index scan
instead of a sequential scan.

I'm just spitballing here. If you think the present design is good and
just want to try to speed it up, I'm not deeply opposed to that. But
it's also not obvious to me how to stick a cache in front of something
that's basically a full-table scan.

Thank you for the interesting ideas. I'd like to try to revive the
version with the flag in pg_database. Will use other ideas as backup
if no success.

------
Regards,
Alexander Korotkov

#156Alexander Korotkov
aekorotkov@gmail.com
In reply to: Alexander Korotkov (#155)
Re: On login trigger: take three

Hi!

On Tue, Oct 3, 2023 at 8:35 PM Alexander Korotkov <aekorotkov@gmail.com> wrote:

Thank you for the interesting ideas. I'd like to try to revive the
version with the flag in pg_database. Will use other ideas as backup
if no success.

I've revived the patch version with pg_database.dathasloginevt flag.
I took v32 version [1] and made the following changes.

* Incorporate enchantments made on flagless version of patch.
* Read dathasloginevt during InitPostgres() to prevent extra catalog
access and even more notable StartTransactionCommand() when there are
no login triggers.
* Hold lock during setting of pg_database.dathasloginevt flag (v32
version actually didn't prevent race condition).
* Fix AlterEventTrigger() to check event name not trigger name
* Acquire conditional lock while resetting pg_database.dathasloginevt
flag to prevent new database connection to hang waiting another
transaction to finish.

This version should be good and has no overhead. Any thoughts?
Daniel, could you please re-run the performance tests?

Links
1. /messages/by-id/CAMEv5_vDjceLr54WUCNPPVsJs8WBWWsRW826VppNEFoLC1LAEw@mail.gmail.com

------
Regards,
Alexander Korotkov

#157Andres Freund
andres@anarazel.de
In reply to: Alexander Korotkov (#156)
Re: On login trigger: take three

On 2023-10-09 17:11:25 +0300, Alexander Korotkov wrote:

This version should be good and has no overhead. Any thoughts?

I think you forgot to attach the patch?

#158Alexander Korotkov
aekorotkov@gmail.com
In reply to: Andres Freund (#157)
1 attachment(s)
Re: On login trigger: take three

On Mon, Oct 9, 2023 at 11:58 PM Andres Freund <andres@anarazel.de> wrote:

On 2023-10-09 17:11:25 +0300, Alexander Korotkov wrote:

This version should be good and has no overhead. Any thoughts?

I think you forgot to attach the patch?

That's it!

------
Regards,
Alexander Korotkov

Attachments:

0001-Add-support-event-triggers-on-authenticated-logi-v43.patchapplication/octet-stream; name=0001-Add-support-event-triggers-on-authenticated-logi-v43.patchDownload
From 8438b5f2e8d4c9503814a42ec3b4aa9f2f7d0199 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <akorotkov@postgresql.org>
Date: Mon, 9 Oct 2023 15:00:26 +0300
Subject: [PATCH] Add support event triggers on authenticated login

This commit introduces trigger on login event, allowing to fire some actions
right on the user connection.  This can be useful for logging or connection
check purposes as well as for some personalization of environment.  Usage
details are described in the documentation included, but shortly usage is
the same as for other triggers: create function returning event_trigger and
then create event trigger on login event.

In order to prevent the connection time overhead when there are no triggers
the commit introduces pg_database.dathasloginevt flag, which indicates database
has active login triggers.  This flag is set by CREATE/ALTER EVENT TRIGGER
command, and unset at connection time when no active triggers found.

Author: Konstantin Knizhnik, Mikhail Gribkov
Discussion: https://postgr.es/m/0d46d29f-4558-3af9-9c85-7774e14a7709%40postgrespro.ru
Reviewed-by: Pavel Stehule, Takayuki Tsunakawa, Greg Nancarrow, Ivan Panchenko
Reviewed-by: Daniel Gustafsson, Teodor Sigaev, Robert Haas, Andres Freund
Reviewed-by: Tom Lane, Andrey Sokolov, Zhihong Yu, Sergey Shinderuk
Reviewed-by: Gregory Stark, Nikita Malakhov, Ted Yu
---
 doc/src/sgml/bki.sgml                         |   2 +-
 doc/src/sgml/catalogs.sgml                    |  13 ++
 doc/src/sgml/ecpg.sgml                        |   2 +
 doc/src/sgml/event-trigger.sgml               |  94 ++++++++++
 src/backend/commands/dbcommands.c             |  17 +-
 src/backend/commands/event_trigger.c          | 170 +++++++++++++++++-
 src/backend/storage/lmgr/lmgr.c               |  38 ++++
 src/backend/tcop/postgres.c                   |   4 +
 src/backend/utils/cache/evtcache.c            |   2 +
 src/backend/utils/init/globals.c              |   2 +
 src/backend/utils/init/postinit.c             |   1 +
 src/bin/pg_dump/pg_dump.c                     |   5 +
 src/bin/psql/tab-complete.c                   |   4 +-
 src/include/catalog/pg_database.dat           |   2 +-
 src/include/catalog/pg_database.h             |   3 +
 src/include/commands/event_trigger.h          |   1 +
 src/include/miscadmin.h                       |   2 +
 src/include/storage/lmgr.h                    |   2 +
 src/include/tcop/cmdtaglist.h                 |   1 +
 src/include/utils/evtcache.h                  |   3 +-
 .../authentication/t/005_login_trigger.pl     | 157 ++++++++++++++++
 src/test/recovery/t/001_stream_rep.pl         |  23 +++
 src/test/regress/expected/event_trigger.out   |  45 +++++
 src/test/regress/sql/event_trigger.sql        |  26 +++
 24 files changed, 599 insertions(+), 20 deletions(-)
 create mode 100644 src/test/authentication/t/005_login_trigger.pl

diff --git a/doc/src/sgml/bki.sgml b/doc/src/sgml/bki.sgml
index f71644e3989..315ba819514 100644
--- a/doc/src/sgml/bki.sgml
+++ b/doc/src/sgml/bki.sgml
@@ -184,7 +184,7 @@
   descr => 'database\'s default template',
   datname => 'template1', encoding => 'ENCODING',
   datlocprovider => 'LOCALE_PROVIDER', datistemplate => 't',
-  datallowconn => 't', datconnlimit => '-1', datfrozenxid => '0',
+  datallowconn => 't', dathasloginevt => 'f', datconnlimit => '-1', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datcollate => 'LC_COLLATE',
   datctype => 'LC_CTYPE', daticulocale => 'ICU_LOCALE', datacl => '_null_' },
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index e09adb45e41..d3458840fbe 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -3035,6 +3035,19 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>dathasloginevt</structfield> <type>bool</type>
+      </para>
+      <para>
+        Indicates that there are login event triggers defined for this database.
+        This flag is used to avoid extra lookups on the
+        <structname>pg_event_trigger</structname> table during each backend
+        startup.  This flag is used internally by <productname>PostgreSQL</productname>
+        and should not be manually altered or read for monitoring purposes.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>datconnlimit</structfield> <type>int4</type>
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index f52165165dc..54de81158b5 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -4769,6 +4769,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = t (type: 1)
 datallowconn = t (type: 1)
+dathasloginevt = f (type: 1)
 datconnlimit = -1 (type: 5)
 datfrozenxid = 379 (type: 1)
 dattablespace = 1663 (type: 1)
@@ -4793,6 +4794,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = f (type: 1)
 datallowconn = t (type: 1)
+dathasloginevt = f (type: 1)
 datconnlimit = -1 (type: 5)
 datfrozenxid = 379 (type: 1)
 dattablespace = 1663 (type: 1)
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 3b6a5361b34..10b20f0339a 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>login</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,24 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>login</literal> event occurs when an authenticated user logs
+     into the system. Any bug in a trigger procedure for this event may
+     prevent successful login to the system. Such bugs may be fixed by
+     setting <xref linkend="guc-event-triggers"/> is set to <literal>false</literal>
+     either in a connection string or configuration file. Alternative is
+     restarting the system in single-user mode (as event triggers are
+     disabled in this mode). See the <xref linkend="app-postgres"/> reference
+     page for details about using single-user mode.
+     The <literal>login</literal> event will also fire on standby servers.
+     To prevent servers from becoming inaccessible, such triggers must avoid
+     writing anything to the database when running on a standby.
+     Also, it's recommended to avoid long-running queries in
+     <literal>login</literal> event triggers.  Notes that, for instance,
+     cancelling connection in <application>psql</application> wouldn't cancel
+     the in-progress <literal>login</literal> trigger.
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1300,4 +1319,79 @@ CREATE EVENT TRIGGER no_rewrite_allowed
 </programlisting>
    </para>
  </sect1>
+
+  <sect1 id="event-trigger-database-login-example">
+    <title>A Database Login Event Trigger Example</title>
+
+    <para>
+      The event trigger on the <literal>login</literal> event can be
+      useful for logging user logins, for verifying the connection and
+      assigning roles according to current circumstances, or for session
+      data initialization. It is very important that any event trigger using
+      the <literal>login</literal> event checks whether or not the database is
+      in recovery before performing any writes. Writing to a standby server
+      will make it inaccessible.
+    </para>
+
+    <para>
+      The following example demonstrates these options.
+<programlisting>
+-- create test tables and roles
+CREATE TABLE user_login_log (
+  "user" text,
+  "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+  RETURNS event_trigger SECURITY DEFINER
+  LANGUAGE plpgsql AS
+$$
+DECLARE
+  hour integer = EXTRACT('hour' FROM current_time at time zone 'utc');
+  rec boolean;
+BEGIN
+-- 1. Forbid logging in between 2AM and 4AM.
+IF hour BETWEEN 2 AND 4 THEN
+  RAISE EXCEPTION 'Login forbidden';
+END IF;
+
+-- The checks below cannot be performed on standby servers so
+-- ensure the database is not in recovery before we perform any
+-- operations.
+SELECT pg_is_in_recovery() INTO rec;
+IF rec THEN
+  RETURN;
+END IF;
+
+-- 2. Assign some roles. At daytime, grant the day_worker role, else the
+-- night_worker role.
+IF hour BETWEEN 8 AND 20 THEN
+  EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT day_worker TO ' || quote_ident(session_user);
+ELSE
+  EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT night_worker TO ' || quote_ident(session_user);
+END IF;
+
+-- 3. Initialize user session data
+CREATE TEMP TABLE session_storage (x float, y integer);
+ALTER TABLE session_storage OWNER TO session_user;
+
+-- 4. Log the connection time
+INSERT INTO public.user_login_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+  ON login
+  EXECUTE FUNCTION init_session();
+ALTER EVENT TRIGGER init_session ENABLE ALWAYS;
+</programlisting>
+    </para>
+  </sect1>
 </chapter>
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 307729ab7ef..b0c562aa1ea 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -116,7 +116,7 @@ static void movedb(const char *dbname, const char *tblspcname);
 static void movedb_failure_callback(int code, Datum arg);
 static bool get_db_info(const char *name, LOCKMODE lockmode,
 						Oid *dbIdP, Oid *ownerIdP,
-						int *encodingP, bool *dbIsTemplateP, bool *dbAllowConnP,
+						int *encodingP, bool *dbIsTemplateP, bool *dbAllowConnP, bool *dbHasLoginEvtP,
 						TransactionId *dbFrozenXidP, MultiXactId *dbMinMultiP,
 						Oid *dbTablespace, char **dbCollate, char **dbCtype, char **dbIculocale,
 						char **dbIcurules,
@@ -680,6 +680,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	char		src_locprovider = '\0';
 	char	   *src_collversion = NULL;
 	bool		src_istemplate;
+	bool		src_hasloginevt;
 	bool		src_allowconn;
 	TransactionId src_frozenxid = InvalidTransactionId;
 	MultiXactId src_minmxid = InvalidMultiXactId;
@@ -968,7 +969,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 
 	if (!get_db_info(dbtemplate, ShareLock,
 					 &src_dboid, &src_owner, &src_encoding,
-					 &src_istemplate, &src_allowconn,
+					 &src_istemplate, &src_allowconn, &src_hasloginevt,
 					 &src_frozenxid, &src_minmxid, &src_deftablespace,
 					 &src_collate, &src_ctype, &src_iculocale, &src_icurules, &src_locprovider,
 					 &src_collversion))
@@ -1375,6 +1376,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	new_record[Anum_pg_database_datlocprovider - 1] = CharGetDatum(dblocprovider);
 	new_record[Anum_pg_database_datistemplate - 1] = BoolGetDatum(dbistemplate);
 	new_record[Anum_pg_database_datallowconn - 1] = BoolGetDatum(dballowconnections);
+	new_record[Anum_pg_database_dathasloginevt - 1] = BoolGetDatum(src_hasloginevt);
 	new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 	new_record[Anum_pg_database_datfrozenxid - 1] = TransactionIdGetDatum(src_frozenxid);
 	new_record[Anum_pg_database_datminmxid - 1] = TransactionIdGetDatum(src_minmxid);
@@ -1602,7 +1604,7 @@ dropdb(const char *dbname, bool missing_ok, bool force)
 	 */
 	pgdbrel = table_open(DatabaseRelationId, RowExclusiveLock);
 
-	if (!get_db_info(dbname, AccessExclusiveLock, &db_id, NULL, NULL,
+	if (!get_db_info(dbname, AccessExclusiveLock, &db_id, NULL, NULL, NULL,
 					 &db_istemplate, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL))
 	{
 		if (!missing_ok)
@@ -1817,7 +1819,7 @@ RenameDatabase(const char *oldname, const char *newname)
 	 */
 	rel = table_open(DatabaseRelationId, RowExclusiveLock);
 
-	if (!get_db_info(oldname, AccessExclusiveLock, &db_id, NULL, NULL,
+	if (!get_db_info(oldname, AccessExclusiveLock, &db_id, NULL, NULL, NULL,
 					 NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL))
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_DATABASE),
@@ -1927,7 +1929,7 @@ movedb(const char *dbname, const char *tblspcname)
 	 */
 	pgdbrel = table_open(DatabaseRelationId, RowExclusiveLock);
 
-	if (!get_db_info(dbname, AccessExclusiveLock, &db_id, NULL, NULL,
+	if (!get_db_info(dbname, AccessExclusiveLock, &db_id, NULL, NULL, NULL,
 					 NULL, NULL, NULL, NULL, &src_tblspcoid, NULL, NULL, NULL, NULL, NULL, NULL))
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_DATABASE),
@@ -2693,7 +2695,7 @@ pg_database_collation_actual_version(PG_FUNCTION_ARGS)
 static bool
 get_db_info(const char *name, LOCKMODE lockmode,
 			Oid *dbIdP, Oid *ownerIdP,
-			int *encodingP, bool *dbIsTemplateP, bool *dbAllowConnP,
+			int *encodingP, bool *dbIsTemplateP, bool *dbAllowConnP, bool *dbHasLoginEvtP,
 			TransactionId *dbFrozenXidP, MultiXactId *dbMinMultiP,
 			Oid *dbTablespace, char **dbCollate, char **dbCtype, char **dbIculocale,
 			char **dbIcurules,
@@ -2778,6 +2780,9 @@ get_db_info(const char *name, LOCKMODE lockmode,
 				/* allowed as template? */
 				if (dbIsTemplateP)
 					*dbIsTemplateP = dbform->datistemplate;
+				/* Has on login event trigger? */
+				if (dbHasLoginEvtP)
+					*dbHasLoginEvtP = dbform->dathasloginevt;
 				/* allowing connections? */
 				if (dbAllowConnP)
 					*dbAllowConnP = dbform->datallowconn;
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index bd812e42d94..b09005792fb 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -37,15 +38,18 @@
 #include "miscadmin.h"
 #include "parser/parse_func.h"
 #include "pgstat.h"
+#include "storage/lmgr.h"
 #include "tcop/deparse_utility.h"
 #include "tcop/utility.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 typedef struct EventTriggerQueryState
@@ -103,6 +107,7 @@ static void validate_table_rewrite_tags(const char *filtervar, List *taglist);
 static void EventTriggerInvoke(List *fn_oid_list, EventTriggerData *trigdata);
 static const char *stringify_grant_objtype(ObjectType objtype);
 static const char *stringify_adefprivs_objtype(ObjectType objtype);
+static void SetDatatabaseHasLoginEventTriggers(void);
 
 /*
  * Create an event trigger.
@@ -133,6 +138,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "login") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -165,6 +171,10 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	else if (strcmp(stmt->eventname, "table_rewrite") == 0
 			 && tags != NULL)
 		validate_table_rewrite_tags("tag", tags);
+	else if (strcmp(stmt->eventname, "login") == 0 && tags != NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("Tag filtering is not supported for login event trigger")));
 
 	/*
 	 * Give user a nice error message if an event trigger of the same name
@@ -296,6 +306,13 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
 	CatalogTupleInsert(tgrel, tuple);
 	heap_freetuple(tuple);
 
+	/*
+	 * Login event triggers have an additional flag in pg_database to allow
+	 * faster lookups in hot codepaths. Set the flag unless already True.
+	 */
+	if (strcmp(eventname, "login") == 0)
+		SetDatatabaseHasLoginEventTriggers();
+
 	/* Depend on owner. */
 	recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
 
@@ -357,6 +374,39 @@ filter_list_to_array(List *filterlist)
 	return PointerGetDatum(construct_array_builtin(data, l, TEXTOID));
 }
 
+/*
+ * Set pg_database.dathasloginevt flag for current database indicating that
+ * current database has on login triggers.
+ */
+void
+SetDatatabaseHasLoginEventTriggers(void)
+{
+	/* Set dathasloginevt flag in pg_database */
+	Form_pg_database db;
+	Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+	HeapTuple	tuple;
+
+	/*
+	 * Use shared lock to prevent a conflit with EventTriggerOnLogin() trying
+	 * to reset pg_database.dathasloginevt flag.  Note that we use
+	 * AccessShareLock allowing setters concurently.
+	 */
+	LockSharedObject(DatabaseRelationId, MyDatabaseId, 0, AccessShareLock);
+
+	tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+	db = (Form_pg_database) GETSTRUCT(tuple);
+	if (!db->dathasloginevt)
+	{
+		db->dathasloginevt = true;
+		CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+		CommandCounterIncrement();
+	}
+	table_close(pg_db, RowExclusiveLock);
+	heap_freetuple(tuple);
+}
+
 /*
  * ALTER EVENT TRIGGER foo ENABLE|DISABLE|ENABLE ALWAYS|REPLICA
  */
@@ -391,6 +441,10 @@ AlterEventTrigger(AlterEventTrigStmt *stmt)
 
 	CatalogTupleUpdate(tgrel, &tup->t_self, tup);
 
+	if (namestrcmp(&evtForm->evtevent, "login") == 0 &&
+		tgenabled != TRIGGER_DISABLED)
+		SetDatatabaseHasLoginEventTriggers();
+
 	InvokeObjectPostAlterHook(EventTriggerRelationId,
 							  trigoid, 0);
 
@@ -557,7 +611,7 @@ filter_event_trigger(CommandTag tag, EventTriggerCacheItem *item)
 static List *
 EventTriggerCommonSetup(Node *parsetree,
 						EventTriggerEvent event, const char *eventstr,
-						EventTriggerData *trigdata)
+						EventTriggerData *trigdata, bool unfiltered)
 {
 	CommandTag	tag;
 	List	   *cachelist;
@@ -582,10 +636,15 @@ EventTriggerCommonSetup(Node *parsetree,
 	{
 		CommandTag	dbgtag;
 
-		dbgtag = CreateCommandTag(parsetree);
+		if (event == EVT_Login)
+			dbgtag = CMDTAG_LOGIN;
+		else
+			dbgtag = CreateCommandTag(parsetree);
+
 		if (event == EVT_DDLCommandStart ||
 			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
+			event == EVT_SQLDrop ||
+			event == EVT_Login)
 		{
 			if (!command_tag_event_trigger_ok(dbgtag))
 				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
@@ -604,7 +663,10 @@ EventTriggerCommonSetup(Node *parsetree,
 		return NIL;
 
 	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
+	if (event == EVT_Login)
+		tag = CMDTAG_LOGIN;
+	else
+		tag = CreateCommandTag(parsetree);
 
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
@@ -617,7 +679,7 @@ EventTriggerCommonSetup(Node *parsetree,
 	{
 		EventTriggerCacheItem *item = lfirst(lc);
 
-		if (filter_event_trigger(tag, item))
+		if (unfiltered || filter_event_trigger(tag, item))
 		{
 			/* We must plan to fire this trigger. */
 			runlist = lappend_oid(runlist, item->fnoid);
@@ -670,7 +732,7 @@ EventTriggerDDLCommandStart(Node *parsetree)
 	runlist = EventTriggerCommonSetup(parsetree,
 									  EVT_DDLCommandStart,
 									  "ddl_command_start",
-									  &trigdata);
+									  &trigdata, false);
 	if (runlist == NIL)
 		return;
 
@@ -718,7 +780,7 @@ EventTriggerDDLCommandEnd(Node *parsetree)
 
 	runlist = EventTriggerCommonSetup(parsetree,
 									  EVT_DDLCommandEnd, "ddl_command_end",
-									  &trigdata);
+									  &trigdata, false);
 	if (runlist == NIL)
 		return;
 
@@ -764,7 +826,7 @@ EventTriggerSQLDrop(Node *parsetree)
 
 	runlist = EventTriggerCommonSetup(parsetree,
 									  EVT_SQLDrop, "sql_drop",
-									  &trigdata);
+									  &trigdata, false);
 
 	/*
 	 * Nothing to do if run list is empty.  Note this typically can't happen,
@@ -805,6 +867,96 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Fire login event triggers if any are present.  The dathasloginevt
+ * pg_database flag is left when an event trigger is dropped, to avoid
+ * complicating the codepath in the case of multiple event triggers.  This
+ * function will instead unset the flag if no trigger is defined.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode or via a GUC.  We also need a
+	 * database connection (some background workers doesn't have it).
+	 */
+	if (!IsUnderPostmaster || !event_triggers ||
+		!OidIsValid(MyDatabaseId) || !MyDatabaseHasLoginEventTriggers)
+		return;
+
+	StartTransactionCommand();
+	runlist = EventTriggerCommonSetup(NULL,
+										EVT_Login, "login",
+										&trigdata, false);
+
+	if (runlist != NIL)
+	{
+		/*
+		 * Event trigger execution may require an active snapshot.
+		 */
+		PushActiveSnapshot(GetTransactionSnapshot());
+
+		/* Run the triggers. */
+		EventTriggerInvoke(runlist, &trigdata);
+
+		/* Cleanup. */
+		list_free(runlist);
+
+		PopActiveSnapshot();
+	}
+	/*
+	 * There is no active login event trigger, but our pg_database.dathasloginevt was set.
+	 * Try to unset this flag.  We use the lock to prevent concurrent
+	 * SetDatatabaseHasLoginEventTriggers(), but we don't want to hang the
+	 * connection waiting on the lock.  Thus, we are just trying to acquire
+	 * the lock conditionally.
+	 */
+	else if (ConditionalLockSharedObject(DatabaseRelationId, MyDatabaseId,
+										 0, AccessExclusiveLock))
+	{
+		/*
+		 * The lock is held.  Now we need to recheck that login event triggers
+		 * list is still empty.  Once the list is empty, we know that even if
+		 * there is a backend, which concurrently inserts/enables login trigger,
+		 * it will update pg_database.dathasloginevt *afterwards*.
+		 */
+		runlist = EventTriggerCommonSetup(NULL,
+										  EVT_Login, "login",
+										  &trigdata, true);
+
+		if (runlist == NIL)
+		{
+			Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+			HeapTuple	tuple;
+			Form_pg_database db;
+
+			tuple = SearchSysCacheCopy1(DATABASEOID,
+										ObjectIdGetDatum(MyDatabaseId));
+
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+			db = (Form_pg_database) GETSTRUCT(tuple);
+			if (db->dathasloginevt)
+			{
+				db->dathasloginevt = false;
+				CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+			}
+			table_close(pg_db, RowExclusiveLock);
+			heap_freetuple(tuple);
+		}
+		else
+		{
+			list_free(runlist);
+		}
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
@@ -835,7 +987,7 @@ EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason)
 	runlist = EventTriggerCommonSetup(parsetree,
 									  EVT_TableRewrite,
 									  "table_rewrite",
-									  &trigdata);
+									  &trigdata, false);
 	if (runlist == NIL)
 		return;
 
diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c
index ee9b89a6726..b447ddf11ba 100644
--- a/src/backend/storage/lmgr/lmgr.c
+++ b/src/backend/storage/lmgr/lmgr.c
@@ -1060,6 +1060,44 @@ LockSharedObject(Oid classid, Oid objid, uint16 objsubid,
 	AcceptInvalidationMessages();
 }
 
+/*
+ *		ConditionalLockSharedObject
+ *
+ * As above, but only lock if we can get the lock without blocking.
+ * Returns true iff the lock was acquired.
+ */
+bool
+ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid,
+							LOCKMODE lockmode)
+{
+	LOCKTAG		tag;
+	LOCALLOCK  *locallock;
+	LockAcquireResult res;
+
+	SET_LOCKTAG_OBJECT(tag,
+					   InvalidOid,
+					   classid,
+					   objid,
+					   objsubid);
+
+	res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock);
+
+	if (res == LOCKACQUIRE_NOT_AVAIL)
+		return false;
+
+	/*
+	 * Now that we have the lock, check for invalidation messages; see notes
+	 * in LockRelationOid.
+	 */
+	if (res != LOCKACQUIRE_ALREADY_CLEAR)
+	{
+		AcceptInvalidationMessages();
+		MarkLockClear(locallock);
+	}
+
+	return true;
+}
+
 /*
  *		UnlockSharedObject
  */
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 21b9763183e..20d8202cb7d 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -36,6 +36,7 @@
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/prepare.h"
 #include "common/pg_prng.h"
 #include "jit/jit.h"
@@ -4288,6 +4289,9 @@ PostgresMain(const char *dbname, const char *username)
 	initStringInfo(&row_description_buf);
 	MemoryContextSwitchTo(TopMemoryContext);
 
+	/* Fire any defined login event triggers, if appropriate */
+	EventTriggerOnLogin();
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index b080f7a35f3..ab5111c90fd 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "login") == 0)
+			event = EVT_Login;
 		else
 			continue;
 
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 011ec18015a..60bc1217fb4 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -90,6 +90,8 @@ Oid			MyDatabaseId = InvalidOid;
 
 Oid			MyDatabaseTableSpace = InvalidOid;
 
+bool		MyDatabaseHasLoginEventTriggers = false;
+
 /*
  * DatabasePath is the path (relative to DataDir) of my database's
  * primary directory, ie, its directory in the default tablespace.
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index df4d15a50fb..e23fba41157 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -1101,6 +1101,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 		}
 
 		MyDatabaseTableSpace = datform->dattablespace;
+		MyDatabaseHasLoginEventTriggers = datform->dathasloginevt;
 		/* pass the database name back to the caller */
 		if (out_dbname)
 			strcpy(out_dbname, dbname);
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index f7b61766921..83aeef2751b 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3263,6 +3263,11 @@ dumpDatabase(Archive *fout)
 		appendPQExpBufferStr(delQry, ";\n");
 	}
 
+	/*
+	 * We do not restore pg_database.dathasloginevt because it is set
+	 * automatically on login event trigger creation.
+	 */
+
 	/* Add database-specific SET options */
 	dumpDatabaseConfig(fout, creaQry, datname, dbCatId.oid);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index d30d719a1f8..2eb8ff1afca 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3552,8 +3552,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop",
-					  "table_rewrite");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "login",
+					  "sql_drop", "table_rewrite");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat
index 0754ef1bce4..8d91e3bf8da 100644
--- a/src/include/catalog/pg_database.dat
+++ b/src/include/catalog/pg_database.dat
@@ -16,7 +16,7 @@
   descr => 'default template for new databases',
   datname => 'template1', encoding => 'ENCODING',
   datlocprovider => 'LOCALE_PROVIDER', datistemplate => 't',
-  datallowconn => 't', datconnlimit => '-1', datfrozenxid => '0',
+  datallowconn => 't', dathasloginevt => 'f', datconnlimit => '-1', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datcollate => 'LC_COLLATE',
   datctype => 'LC_CTYPE', daticulocale => 'ICU_LOCALE',
   daticurules => 'ICU_RULES', datacl => '_null_' },
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index e9eb06b2e53..3e50a570046 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -49,6 +49,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
 	/* new connections allowed? */
 	bool		datallowconn;
 
+	/* database has login event triggers? */
+	bool		dathasloginevt;
+
 	/*
 	 * Max connections allowed. Negative values have special meaning, see
 	 * DATCONNLIMIT_* defines below.
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 1c925dbf257..9e3ece50d5f 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -56,6 +56,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnLogin(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 14bd574fc24..c9cad452d96 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -203,6 +203,8 @@ extern PGDLLIMPORT Oid MyDatabaseId;
 
 extern PGDLLIMPORT Oid MyDatabaseTableSpace;
 
+extern PGDLLIMPORT bool MyDatabaseHasLoginEventTriggers;
+
 /*
  * Date/Time Configuration
  *
diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h
index 4ee91e3cf93..952ebe75cb4 100644
--- a/src/include/storage/lmgr.h
+++ b/src/include/storage/lmgr.h
@@ -99,6 +99,8 @@ extern void UnlockDatabaseObject(Oid classid, Oid objid, uint16 objsubid,
 /* Lock a shared-across-databases object (other than a relation) */
 extern void LockSharedObject(Oid classid, Oid objid, uint16 objsubid,
 							 LOCKMODE lockmode);
+extern bool ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid,
+										LOCKMODE lockmode);
 extern void UnlockSharedObject(Oid classid, Oid objid, uint16 objsubid,
 							   LOCKMODE lockmode);
 
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index e738ac1c097..553a31874f1 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
+PG_CMDTAG(CMDTAG_LOGIN, "LOGIN", true, false, false)
 PG_CMDTAG(CMDTAG_MERGE, "MERGE", false, false, true)
 PG_CMDTAG(CMDTAG_MOVE, "MOVE", false, false, true)
 PG_CMDTAG(CMDTAG_NOTIFY, "NOTIFY", false, false, false)
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index d340026518a..52052e6252a 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Login,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/authentication/t/005_login_trigger.pl b/src/test/authentication/t/005_login_trigger.pl
new file mode 100644
index 00000000000..fec664fb865
--- /dev/null
+++ b/src/test/authentication/t/005_login_trigger.pl
@@ -0,0 +1,157 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+# Tests of authentication via login trigger. Mostly for rejection via
+# exception, because this scenario cannot be covered with *.sql/*.out regress
+# tests.
+
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Execute a psql command and compare its output towards given regexps
+sub psql_command
+{
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+	my ($node, $sql, $expected_ret, $test_name, %params) = @_;
+
+	my $connstr;
+	if (defined($params{connstr}))
+	{
+		$connstr = $params{connstr};
+	}
+	else
+	{
+		$connstr = '';
+	}
+
+	# Execute command
+	my ($ret, $stdout, $stderr) = $node->psql('postgres', $sql,
+											  connstr => "$connstr");
+
+	# Check return code
+	is($ret, $expected_ret,  "$test_name: exit code $expected_ret");
+
+	# Check stdout
+	if (defined($params{log_like}))
+	{
+		my @log_like = @{ $params{log_like} };
+		while (my $regex = shift @log_like)
+		{
+			like($stdout, $regex, "$test_name: log matches");
+		}
+	}
+	if (defined($params{log_unlike}))
+	{
+		my @log_unlike = @{ $params{log_unlike} };
+		while (my $regex = shift @log_unlike)
+		{
+			unlike($stdout, $regex, "$test_name: log unmatches");
+		}
+	}
+	if (defined($params{log_exact}))
+	{
+		is($stdout, $params{log_exact}, "$test_name: log equals");
+	}
+
+	# Check stderr
+	if (defined($params{err_like}))
+	{
+		my @err_like = @{ $params{err_like} };
+		while (my $regex = shift @err_like)
+		{
+			like($stderr, $regex, "$test_name: err matches");
+		}
+	}
+	if (defined($params{err_unlike}))
+	{
+		my @err_unlike = @{ $params{err_unlike} };
+		while (my $regex = shift @err_unlike)
+		{
+			unlike($stderr, $regex, "$test_name: err unmatches");
+		}
+	}
+	if (defined($params{err_exact}))
+	{
+		is($stderr, $params{err_exact}, "$test_name: err equals");
+	}
+
+	return;
+}
+
+# New node
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init(extra => [ '--locale=C', '--encoding=UTF8' ]);
+$node->append_conf(
+	'postgresql.conf', q{
+wal_level = 'logical'
+max_replication_slots = 4
+max_wal_senders = 4
+});
+$node->start;
+
+# Create temporary roles and log table
+psql_command($node, 'CREATE ROLE alice WITH LOGIN;
+CREATE ROLE mallory WITH LOGIN;
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+', 0, 'create tmp objects', log_exact => '', err_exact => ''), ;
+
+# Create login event function and trigger
+psql_command($node,
+			 'CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  IF SESSION_USER = \'mallory\' THEN
+    RAISE EXCEPTION \'Hello %! You are NOT welcome here!\', SESSION_USER;
+  END IF;
+  RAISE NOTICE \'Hello %! You are welcome!\', SESSION_USER;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+', 0, 'create trigger function', log_exact => '', err_exact => '');
+
+psql_command($node,
+			 'CREATE EVENT TRIGGER on_login_trigger '
+			 .'ON login EXECUTE PROCEDURE on_login_proc();', 0,
+			 'create event trigger', log_exact => '', err_exact => '');
+psql_command($node, 'ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;', 0,
+			 'alter event trigger', log_exact => '',
+			 err_like => [qr/You are welcome/]);
+
+# Check the two requests were logged via login trigger
+psql_command($node, 'SELECT COUNT(*) FROM user_logins;', 0, 'select count',
+			 log_exact => '2', err_like => [qr/You are welcome/]);
+
+# Try to log as allowed Alice and disallowed Mallory (two times)
+psql_command($node, 'SELECT 1;', 0, 'try alice', connstr => 'user=alice',
+			 log_exact => '1', err_like => [qr/You are welcome/],
+			 err_unlike => [qr/You are NOT welcome/]);
+psql_command($node, 'SELECT 1;', 2, 'try mallory', connstr => 'user=mallory',
+			 log_exact => '', err_like => [qr/You are NOT welcome/],
+			 err_unlike => [qr/You are welcome/]);
+psql_command($node, 'SELECT 1;', 2, 'try mallory', connstr => 'user=mallory',
+			 log_exact => '', err_like => [qr/You are NOT welcome/],
+			 err_unlike => [qr/You are welcome/]);
+
+# Check that Alice's login record is here, while the Mallory's one is not
+psql_command($node, 'SELECT * FROM user_logins;', 0, 'select *',
+			 log_like => [qr/3\|alice/], log_unlike => [qr/mallory/],
+			 err_like => [qr/You are welcome/]);
+
+# Check total number of successful logins so far
+psql_command($node, 'SELECT COUNT(*) FROM user_logins;', 0, 'select count',
+			 log_exact => '5', err_like => [qr/You are welcome/]);
+
+# Cleanup the temporary stuff
+psql_command($node, 'DROP EVENT TRIGGER on_login_trigger;', 0,
+			 'drop event trigger', log_exact => '',
+			 err_like => [qr/You are welcome/]);
+psql_command($node, 'DROP TABLE user_logins;
+DROP FUNCTION on_login_proc;
+DROP ROLE mallory;
+DROP ROLE alice;
+', 0, 'cleanup', log_exact => '', err_exact => '');
+
+done_testing();
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 0c72ba09441..8644854eb39 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,24 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE TABLE user_logins(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO user_logins (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 $node_primary->wait_for_replay_catchup($node_standby_1);
 $node_standby_1->wait_for_replay_catchup($node_standby_2, $node_primary);
@@ -384,6 +402,11 @@ sub replay_check
 
 replay_check();
 
+my $evttrig = $node_standby_1->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+$evttrig = $node_standby_2->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 0b87a42d0a9..eaaff6ba6f1 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -638,3 +638,48 @@ NOTICE:  DROP POLICY dropped policy
 CREATE POLICY pguc ON event_trigger_test USING (FALSE);
 SET event_triggers = 'off';
 DROP POLICY pguc ON event_trigger_test;
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     1
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     2
+(1 row)
+
+-- Check dathasloginevt in system catalog
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+ dathasloginevt 
+----------------
+ t
+(1 row)
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
+\c
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+ dathasloginevt 
+----------------
+ f
+(1 row)
+
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 6f0933b9e88..9c2b7903fba 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -495,3 +495,29 @@ DROP POLICY pguc ON event_trigger_test;
 CREATE POLICY pguc ON event_trigger_test USING (FALSE);
 SET event_triggers = 'off';
 DROP POLICY pguc ON event_trigger_test;
+
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+SELECT COUNT(*) FROM user_logins;
+\c
+SELECT COUNT(*) FROM user_logins;
+
+-- Check dathasloginevt in system catalog
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
+\c
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
-- 
2.39.3 (Apple Git-145)

#159Robert Haas
robertmhaas@gmail.com
In reply to: Alexander Korotkov (#156)
Re: On login trigger: take three

On Mon, Oct 9, 2023 at 10:11 AM Alexander Korotkov <aekorotkov@gmail.com> wrote:

* Hold lock during setting of pg_database.dathasloginevt flag (v32
version actually didn't prevent race condition).

So ... how does getting this flag set actually work? And how does
clearing it work?

In the case of row-level security, you have to explicitly enable the
flag on the table level using DDL provided for that purpose. In the
case of relhas{rules,triggers,subclass} the flag is set automatically
as a side-effect of some other operation. I tend to consider that the
latter design is somewhat messy. It's potentially even messier here,
because at least when you add a rule or a trigger to a table you're
expecting to take a lock on the table anyway. I don't think you
necessarily expect creating a login trigger to take a lock on the
database. That's a bit odd and could have visible side effects. And if
you don't, then what happens is that if you create two login triggers
in overlapping transactions, then (1) if there were no login triggers
previously, one of the transactions fails with an internal-looking
error message about a concurrent tuple update and (2) if there were
login triggers previously, then it works fine. That's also a bit weird
and surprising. Now the counter-argument could be that adding new DDL
to enable login triggers for a database is too much cognitive burden
and it's better to have the kind of weird and surprising behavior that
I just discussed. I don't know that I would buy that argument, but it
could be made ... and my real point here is that I don't even see
these trade-offs being discussed. Apologies if they were discussed
earlier and I just missed that; I confess to not having read every
email message on this topic, and some of the ones I did read I read a
long time ago.

This version should be good and has no overhead. Any thoughts?
Daniel, could you please re-run the performance tests?

Is "no overhead" an overly bold claim here?

--
Robert Haas
EDB: http://www.enterprisedb.com

#160Andres Freund
andres@anarazel.de
In reply to: Alexander Korotkov (#158)
Re: On login trigger: take three

Hi,

On 2023-10-10 08:18:46 +0300, Alexander Korotkov wrote:

@@ -968,7 +969,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)

if (!get_db_info(dbtemplate, ShareLock,
&src_dboid, &src_owner, &src_encoding,
-					 &src_istemplate, &src_allowconn,
+					 &src_istemplate, &src_allowconn, &src_hasloginevt,
&src_frozenxid, &src_minmxid, &src_deftablespace,
&src_collate, &src_ctype, &src_iculocale, &src_icurules, &src_locprovider,
&src_collversion))

This isn't your fault, but this imo has become unreadable. Think we ought to
move the information about a database to a struct.

@@ -296,6 +306,13 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
CatalogTupleInsert(tgrel, tuple);
heap_freetuple(tuple);

+	/*
+	 * Login event triggers have an additional flag in pg_database to allow
+	 * faster lookups in hot codepaths. Set the flag unless already True.
+	 */
+	if (strcmp(eventname, "login") == 0)
+		SetDatatabaseHasLoginEventTriggers();

It's not really faster lookups, it's no lookups, right?

/* Depend on owner. */
recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);

@@ -357,6 +374,39 @@ filter_list_to_array(List *filterlist)
return PointerGetDatum(construct_array_builtin(data, l, TEXTOID));
}

+/*
+ * Set pg_database.dathasloginevt flag for current database indicating that
+ * current database has on login triggers.
+ */
+void
+SetDatatabaseHasLoginEventTriggers(void)
+{
+	/* Set dathasloginevt flag in pg_database */
+	Form_pg_database db;
+	Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+	HeapTuple	tuple;
+
+	/*
+	 * Use shared lock to prevent a conflit with EventTriggerOnLogin() trying
+	 * to reset pg_database.dathasloginevt flag.  Note that we use
+	 * AccessShareLock allowing setters concurently.
+	 */
+	LockSharedObject(DatabaseRelationId, MyDatabaseId, 0, AccessShareLock);

That seems like a very odd approach - how does this avoid concurrency issues
with one backend setting and another unsetting the flag? And outside of that,
won't this just lead to concurrently updated tuples?

+	tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+	db = (Form_pg_database) GETSTRUCT(tuple);
+	if (!db->dathasloginevt)
+	{
+		db->dathasloginevt = true;
+		CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+		CommandCounterIncrement();
+	}
+	table_close(pg_db, RowExclusiveLock);
+	heap_freetuple(tuple);
+}
+
/*
* ALTER EVENT TRIGGER foo ENABLE|DISABLE|ENABLE ALWAYS|REPLICA
*/
@@ -391,6 +441,10 @@ AlterEventTrigger(AlterEventTrigStmt *stmt)

CatalogTupleUpdate(tgrel, &tup->t_self, tup);

+	if (namestrcmp(&evtForm->evtevent, "login") == 0 &&
+		tgenabled != TRIGGER_DISABLED)
+		SetDatatabaseHasLoginEventTriggers();
+
InvokeObjectPostAlterHook(EventTriggerRelationId,
trigoid, 0);
@@ -557,7 +611,7 @@ filter_event_trigger(CommandTag tag, EventTriggerCacheItem *item)
static List *
EventTriggerCommonSetup(Node *parsetree,
EventTriggerEvent event, const char *eventstr,
-						EventTriggerData *trigdata)
+						EventTriggerData *trigdata, bool unfiltered)
{
CommandTag	tag;
List	   *cachelist;
@@ -582,10 +636,15 @@ EventTriggerCommonSetup(Node *parsetree,
{
CommandTag	dbgtag;
-		dbgtag = CreateCommandTag(parsetree);
+		if (event == EVT_Login)
+			dbgtag = CMDTAG_LOGIN;
+		else
+			dbgtag = CreateCommandTag(parsetree);
+
if (event == EVT_DDLCommandStart ||
event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
+			event == EVT_SQLDrop ||
+			event == EVT_Login)
{
if (!command_tag_event_trigger_ok(dbgtag))
elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
@@ -604,7 +663,10 @@ EventTriggerCommonSetup(Node *parsetree,
return NIL;
/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
+	if (event == EVT_Login)
+		tag = CMDTAG_LOGIN;
+	else
+		tag = CreateCommandTag(parsetree);

Seems this bit should instead be in a function, given that you have it in
multiple places.

+/*
+ * Fire login event triggers if any are present.  The dathasloginevt
+ * pg_database flag is left when an event trigger is dropped, to avoid
+ * complicating the codepath in the case of multiple event triggers.  This
+ * function will instead unset the flag if no trigger is defined.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode or via a GUC.  We also need a
+	 * database connection (some background workers doesn't have it).
+	 */
+	if (!IsUnderPostmaster || !event_triggers ||
+		!OidIsValid(MyDatabaseId) || !MyDatabaseHasLoginEventTriggers)
+		return;
+
+	StartTransactionCommand();
+	runlist = EventTriggerCommonSetup(NULL,
+										EVT_Login, "login",
+										&trigdata, false);
+
+	if (runlist != NIL)
+	{
+		/*
+		 * Event trigger execution may require an active snapshot.
+		 */
+		PushActiveSnapshot(GetTransactionSnapshot());
+
+		/* Run the triggers. */
+		EventTriggerInvoke(runlist, &trigdata);
+
+		/* Cleanup. */
+		list_free(runlist);
+
+		PopActiveSnapshot();
+	}
+	/*
+	 * There is no active login event trigger, but our pg_database.dathasloginevt was set.
+	 * Try to unset this flag.  We use the lock to prevent concurrent
+	 * SetDatatabaseHasLoginEventTriggers(), but we don't want to hang the
+	 * connection waiting on the lock.  Thus, we are just trying to acquire
+	 * the lock conditionally.
+	 */
+	else if (ConditionalLockSharedObject(DatabaseRelationId, MyDatabaseId,
+										 0, AccessExclusiveLock))

Eek. Why are we doing it this way? I think this is a seriously bad
idea. Maybe it's obvious to you, but it seems much more reasonable to make the
pg_database column an integer and count the number of login event
triggers. When 0, then we don't need to look for login event triggers.

Greetings,

Andres Freund

#161Alexander Korotkov
aekorotkov@gmail.com
In reply to: Andres Freund (#160)
1 attachment(s)
Re: On login trigger: take three

Hi!

Thank you for the review.

On Tue, Oct 10, 2023 at 7:37 PM Andres Freund <andres@anarazel.de> wrote:

On 2023-10-10 08:18:46 +0300, Alexander Korotkov wrote:

@@ -968,7 +969,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)

if (!get_db_info(dbtemplate, ShareLock,
&src_dboid, &src_owner, &src_encoding,
-                                      &src_istemplate, &src_allowconn,
+                                      &src_istemplate, &src_allowconn, &src_hasloginevt,
&src_frozenxid, &src_minmxid, &src_deftablespace,
&src_collate, &src_ctype, &src_iculocale, &src_icurules, &src_locprovider,
&src_collversion))

This isn't your fault, but this imo has become unreadable. Think we ought to
move the information about a database to a struct.

Should I do this in a separate patch?

@@ -296,6 +306,13 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
CatalogTupleInsert(tgrel, tuple);
heap_freetuple(tuple);

+     /*
+      * Login event triggers have an additional flag in pg_database to allow
+      * faster lookups in hot codepaths. Set the flag unless already True.
+      */
+     if (strcmp(eventname, "login") == 0)
+             SetDatatabaseHasLoginEventTriggers();

It's not really faster lookups, it's no lookups, right?

Yes, no extra lookups. Fixed.

/* Depend on owner. */
recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);

@@ -357,6 +374,39 @@ filter_list_to_array(List *filterlist)
return PointerGetDatum(construct_array_builtin(data, l, TEXTOID));
}

+/*
+ * Set pg_database.dathasloginevt flag for current database indicating that
+ * current database has on login triggers.
+ */
+void
+SetDatatabaseHasLoginEventTriggers(void)
+{
+     /* Set dathasloginevt flag in pg_database */
+     Form_pg_database db;
+     Relation        pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+     HeapTuple       tuple;
+
+     /*
+      * Use shared lock to prevent a conflit with EventTriggerOnLogin() trying
+      * to reset pg_database.dathasloginevt flag.  Note that we use
+      * AccessShareLock allowing setters concurently.
+      */
+     LockSharedObject(DatabaseRelationId, MyDatabaseId, 0, AccessShareLock);

That seems like a very odd approach - how does this avoid concurrency issues
with one backend setting and another unsetting the flag? And outside of that,
won't this just lead to concurrently updated tuples?

When backend unsets the flag, it acquires the lock first. If lock is
acquired successfully then no other backends hold it. If the
concurrent backend have already inserted an event trigger then we will
detect it by rechecking event list under lock. If the concurrent
backend inserted/enabled event trigger and waiting for the lock, then
it will set the flag once we release the lock.

+     tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+     if (!HeapTupleIsValid(tuple))
+             elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+     db = (Form_pg_database) GETSTRUCT(tuple);
+     if (!db->dathasloginevt)
+     {
+             db->dathasloginevt = true;
+             CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+             CommandCounterIncrement();
+     }
+     table_close(pg_db, RowExclusiveLock);
+     heap_freetuple(tuple);
+}
+
/*
* ALTER EVENT TRIGGER foo ENABLE|DISABLE|ENABLE ALWAYS|REPLICA
*/
@@ -391,6 +441,10 @@ AlterEventTrigger(AlterEventTrigStmt *stmt)

CatalogTupleUpdate(tgrel, &tup->t_self, tup);

+     if (namestrcmp(&evtForm->evtevent, "login") == 0 &&
+             tgenabled != TRIGGER_DISABLED)
+             SetDatatabaseHasLoginEventTriggers();
+
InvokeObjectPostAlterHook(EventTriggerRelationId,
trigoid, 0);
@@ -557,7 +611,7 @@ filter_event_trigger(CommandTag tag, EventTriggerCacheItem *item)
static List *
EventTriggerCommonSetup(Node *parsetree,
EventTriggerEvent event, const char *eventstr,
-                                             EventTriggerData *trigdata)
+                                             EventTriggerData *trigdata, bool unfiltered)
{
CommandTag      tag;
List       *cachelist;
@@ -582,10 +636,15 @@ EventTriggerCommonSetup(Node *parsetree,
{
CommandTag      dbgtag;
-             dbgtag = CreateCommandTag(parsetree);
+             if (event == EVT_Login)
+                     dbgtag = CMDTAG_LOGIN;
+             else
+                     dbgtag = CreateCommandTag(parsetree);
+
if (event == EVT_DDLCommandStart ||
event == EVT_DDLCommandEnd ||
-                     event == EVT_SQLDrop)
+                     event == EVT_SQLDrop ||
+                     event == EVT_Login)
{
if (!command_tag_event_trigger_ok(dbgtag))
elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
@@ -604,7 +663,10 @@ EventTriggerCommonSetup(Node *parsetree,
return NIL;
/* Get the command tag. */
-     tag = CreateCommandTag(parsetree);
+     if (event == EVT_Login)
+             tag = CMDTAG_LOGIN;
+     else
+             tag = CreateCommandTag(parsetree);

Seems this bit should instead be in a function, given that you have it in
multiple places.

Done.

+/*
+ * Fire login event triggers if any are present.  The dathasloginevt
+ * pg_database flag is left when an event trigger is dropped, to avoid
+ * complicating the codepath in the case of multiple event triggers.  This
+ * function will instead unset the flag if no trigger is defined.
+ */
+void
+EventTriggerOnLogin(void)
+{
+     List       *runlist;
+     EventTriggerData trigdata;
+
+     /*
+      * See EventTriggerDDLCommandStart for a discussion about why event
+      * triggers are disabled in single user mode or via a GUC.  We also need a
+      * database connection (some background workers doesn't have it).
+      */
+     if (!IsUnderPostmaster || !event_triggers ||
+             !OidIsValid(MyDatabaseId) || !MyDatabaseHasLoginEventTriggers)
+             return;
+
+     StartTransactionCommand();
+     runlist = EventTriggerCommonSetup(NULL,
+                                                                             EVT_Login, "login",
+                                                                             &trigdata, false);
+
+     if (runlist != NIL)
+     {
+             /*
+              * Event trigger execution may require an active snapshot.
+              */
+             PushActiveSnapshot(GetTransactionSnapshot());
+
+             /* Run the triggers. */
+             EventTriggerInvoke(runlist, &trigdata);
+
+             /* Cleanup. */
+             list_free(runlist);
+
+             PopActiveSnapshot();
+     }
+     /*
+      * There is no active login event trigger, but our pg_database.dathasloginevt was set.
+      * Try to unset this flag.  We use the lock to prevent concurrent
+      * SetDatatabaseHasLoginEventTriggers(), but we don't want to hang the
+      * connection waiting on the lock.  Thus, we are just trying to acquire
+      * the lock conditionally.
+      */
+     else if (ConditionalLockSharedObject(DatabaseRelationId, MyDatabaseId,
+                                                                              0, AccessExclusiveLock))

Eek. Why are we doing it this way? I think this is a seriously bad
idea. Maybe it's obvious to you, but it seems much more reasonable to make the
pg_database column an integer and count the number of login event
triggers. When 0, then we don't need to look for login event triggers.

The approach with number of login event trigger obviously will also
work. But this version with lazy flag cleaning avoids need to handle
every unobvious case we delete the event triggers (cascading deletion
etc).

------
Regards,
Alexander Korotkov

Attachments:

0001-Add-support-event-triggers-on-authenticated-logi-v44.patchapplication/x-patch; name=0001-Add-support-event-triggers-on-authenticated-logi-v44.patchDownload
From ee3d74a916c389f041c24bf6cd5c50230cf795fc Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <akorotkov@postgresql.org>
Date: Mon, 9 Oct 2023 15:00:26 +0300
Subject: [PATCH] Add support event triggers on authenticated login

This commit introduces trigger on login event, allowing to fire some actions
right on the user connection.  This can be useful for logging or connection
check purposes as well as for some personalization of environment.  Usage
details are described in the documentation included, but shortly usage is
the same as for other triggers: create function returning event_trigger and
then create event trigger on login event.

In order to prevent the connection time overhead when there are no triggers
the commit introduces pg_database.dathasloginevt flag, which indicates database
has active login triggers.  This flag is set by CREATE/ALTER EVENT TRIGGER
command, and unset at connection time when no active triggers found.

Author: Konstantin Knizhnik, Mikhail Gribkov
Discussion: https://postgr.es/m/0d46d29f-4558-3af9-9c85-7774e14a7709%40postgrespro.ru
Reviewed-by: Pavel Stehule, Takayuki Tsunakawa, Greg Nancarrow, Ivan Panchenko
Reviewed-by: Daniel Gustafsson, Teodor Sigaev, Robert Haas, Andres Freund
Reviewed-by: Tom Lane, Andrey Sokolov, Zhihong Yu, Sergey Shinderuk
Reviewed-by: Gregory Stark, Nikita Malakhov, Ted Yu
---
 doc/src/sgml/bki.sgml                         |   2 +-
 doc/src/sgml/catalogs.sgml                    |  13 ++
 doc/src/sgml/ecpg.sgml                        |   2 +
 doc/src/sgml/event-trigger.sgml               |  94 ++++++++++
 src/backend/commands/dbcommands.c             |  17 +-
 src/backend/commands/event_trigger.c          | 177 +++++++++++++++++-
 src/backend/storage/lmgr/lmgr.c               |  38 ++++
 src/backend/tcop/postgres.c                   |   4 +
 src/backend/utils/cache/evtcache.c            |   2 +
 src/backend/utils/init/globals.c              |   2 +
 src/backend/utils/init/postinit.c             |   1 +
 src/bin/pg_dump/pg_dump.c                     |   5 +
 src/bin/psql/tab-complete.c                   |   4 +-
 src/include/catalog/pg_database.dat           |   2 +-
 src/include/catalog/pg_database.h             |   3 +
 src/include/commands/event_trigger.h          |   1 +
 src/include/miscadmin.h                       |   2 +
 src/include/storage/lmgr.h                    |   2 +
 src/include/tcop/cmdtaglist.h                 |   1 +
 src/include/utils/evtcache.h                  |   3 +-
 .../authentication/t/005_login_trigger.pl     | 157 ++++++++++++++++
 src/test/recovery/t/001_stream_rep.pl         |  23 +++
 src/test/regress/expected/event_trigger.out   |  45 +++++
 src/test/regress/sql/event_trigger.sql        |  26 +++
 24 files changed, 606 insertions(+), 20 deletions(-)
 create mode 100644 src/test/authentication/t/005_login_trigger.pl

diff --git a/doc/src/sgml/bki.sgml b/doc/src/sgml/bki.sgml
index f71644e3989..315ba819514 100644
--- a/doc/src/sgml/bki.sgml
+++ b/doc/src/sgml/bki.sgml
@@ -184,7 +184,7 @@
   descr => 'database\'s default template',
   datname => 'template1', encoding => 'ENCODING',
   datlocprovider => 'LOCALE_PROVIDER', datistemplate => 't',
-  datallowconn => 't', datconnlimit => '-1', datfrozenxid => '0',
+  datallowconn => 't', dathasloginevt => 'f', datconnlimit => '-1', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datcollate => 'LC_COLLATE',
   datctype => 'LC_CTYPE', daticulocale => 'ICU_LOCALE', datacl => '_null_' },
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index e09adb45e41..d3458840fbe 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -3035,6 +3035,19 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>dathasloginevt</structfield> <type>bool</type>
+      </para>
+      <para>
+        Indicates that there are login event triggers defined for this database.
+        This flag is used to avoid extra lookups on the
+        <structname>pg_event_trigger</structname> table during each backend
+        startup.  This flag is used internally by <productname>PostgreSQL</productname>
+        and should not be manually altered or read for monitoring purposes.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>datconnlimit</structfield> <type>int4</type>
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index f52165165dc..54de81158b5 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -4769,6 +4769,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = t (type: 1)
 datallowconn = t (type: 1)
+dathasloginevt = f (type: 1)
 datconnlimit = -1 (type: 5)
 datfrozenxid = 379 (type: 1)
 dattablespace = 1663 (type: 1)
@@ -4793,6 +4794,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = f (type: 1)
 datallowconn = t (type: 1)
+dathasloginevt = f (type: 1)
 datconnlimit = -1 (type: 5)
 datfrozenxid = 379 (type: 1)
 dattablespace = 1663 (type: 1)
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 3b6a5361b34..10b20f0339a 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>login</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,24 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>login</literal> event occurs when an authenticated user logs
+     into the system. Any bug in a trigger procedure for this event may
+     prevent successful login to the system. Such bugs may be fixed by
+     setting <xref linkend="guc-event-triggers"/> is set to <literal>false</literal>
+     either in a connection string or configuration file. Alternative is
+     restarting the system in single-user mode (as event triggers are
+     disabled in this mode). See the <xref linkend="app-postgres"/> reference
+     page for details about using single-user mode.
+     The <literal>login</literal> event will also fire on standby servers.
+     To prevent servers from becoming inaccessible, such triggers must avoid
+     writing anything to the database when running on a standby.
+     Also, it's recommended to avoid long-running queries in
+     <literal>login</literal> event triggers.  Notes that, for instance,
+     cancelling connection in <application>psql</application> wouldn't cancel
+     the in-progress <literal>login</literal> trigger.
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1300,4 +1319,79 @@ CREATE EVENT TRIGGER no_rewrite_allowed
 </programlisting>
    </para>
  </sect1>
+
+  <sect1 id="event-trigger-database-login-example">
+    <title>A Database Login Event Trigger Example</title>
+
+    <para>
+      The event trigger on the <literal>login</literal> event can be
+      useful for logging user logins, for verifying the connection and
+      assigning roles according to current circumstances, or for session
+      data initialization. It is very important that any event trigger using
+      the <literal>login</literal> event checks whether or not the database is
+      in recovery before performing any writes. Writing to a standby server
+      will make it inaccessible.
+    </para>
+
+    <para>
+      The following example demonstrates these options.
+<programlisting>
+-- create test tables and roles
+CREATE TABLE user_login_log (
+  "user" text,
+  "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+  RETURNS event_trigger SECURITY DEFINER
+  LANGUAGE plpgsql AS
+$$
+DECLARE
+  hour integer = EXTRACT('hour' FROM current_time at time zone 'utc');
+  rec boolean;
+BEGIN
+-- 1. Forbid logging in between 2AM and 4AM.
+IF hour BETWEEN 2 AND 4 THEN
+  RAISE EXCEPTION 'Login forbidden';
+END IF;
+
+-- The checks below cannot be performed on standby servers so
+-- ensure the database is not in recovery before we perform any
+-- operations.
+SELECT pg_is_in_recovery() INTO rec;
+IF rec THEN
+  RETURN;
+END IF;
+
+-- 2. Assign some roles. At daytime, grant the day_worker role, else the
+-- night_worker role.
+IF hour BETWEEN 8 AND 20 THEN
+  EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT day_worker TO ' || quote_ident(session_user);
+ELSE
+  EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT night_worker TO ' || quote_ident(session_user);
+END IF;
+
+-- 3. Initialize user session data
+CREATE TEMP TABLE session_storage (x float, y integer);
+ALTER TABLE session_storage OWNER TO session_user;
+
+-- 4. Log the connection time
+INSERT INTO public.user_login_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+  ON login
+  EXECUTE FUNCTION init_session();
+ALTER EVENT TRIGGER init_session ENABLE ALWAYS;
+</programlisting>
+    </para>
+  </sect1>
 </chapter>
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 307729ab7ef..b0c562aa1ea 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -116,7 +116,7 @@ static void movedb(const char *dbname, const char *tblspcname);
 static void movedb_failure_callback(int code, Datum arg);
 static bool get_db_info(const char *name, LOCKMODE lockmode,
 						Oid *dbIdP, Oid *ownerIdP,
-						int *encodingP, bool *dbIsTemplateP, bool *dbAllowConnP,
+						int *encodingP, bool *dbIsTemplateP, bool *dbAllowConnP, bool *dbHasLoginEvtP,
 						TransactionId *dbFrozenXidP, MultiXactId *dbMinMultiP,
 						Oid *dbTablespace, char **dbCollate, char **dbCtype, char **dbIculocale,
 						char **dbIcurules,
@@ -680,6 +680,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	char		src_locprovider = '\0';
 	char	   *src_collversion = NULL;
 	bool		src_istemplate;
+	bool		src_hasloginevt;
 	bool		src_allowconn;
 	TransactionId src_frozenxid = InvalidTransactionId;
 	MultiXactId src_minmxid = InvalidMultiXactId;
@@ -968,7 +969,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 
 	if (!get_db_info(dbtemplate, ShareLock,
 					 &src_dboid, &src_owner, &src_encoding,
-					 &src_istemplate, &src_allowconn,
+					 &src_istemplate, &src_allowconn, &src_hasloginevt,
 					 &src_frozenxid, &src_minmxid, &src_deftablespace,
 					 &src_collate, &src_ctype, &src_iculocale, &src_icurules, &src_locprovider,
 					 &src_collversion))
@@ -1375,6 +1376,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	new_record[Anum_pg_database_datlocprovider - 1] = CharGetDatum(dblocprovider);
 	new_record[Anum_pg_database_datistemplate - 1] = BoolGetDatum(dbistemplate);
 	new_record[Anum_pg_database_datallowconn - 1] = BoolGetDatum(dballowconnections);
+	new_record[Anum_pg_database_dathasloginevt - 1] = BoolGetDatum(src_hasloginevt);
 	new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 	new_record[Anum_pg_database_datfrozenxid - 1] = TransactionIdGetDatum(src_frozenxid);
 	new_record[Anum_pg_database_datminmxid - 1] = TransactionIdGetDatum(src_minmxid);
@@ -1602,7 +1604,7 @@ dropdb(const char *dbname, bool missing_ok, bool force)
 	 */
 	pgdbrel = table_open(DatabaseRelationId, RowExclusiveLock);
 
-	if (!get_db_info(dbname, AccessExclusiveLock, &db_id, NULL, NULL,
+	if (!get_db_info(dbname, AccessExclusiveLock, &db_id, NULL, NULL, NULL,
 					 &db_istemplate, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL))
 	{
 		if (!missing_ok)
@@ -1817,7 +1819,7 @@ RenameDatabase(const char *oldname, const char *newname)
 	 */
 	rel = table_open(DatabaseRelationId, RowExclusiveLock);
 
-	if (!get_db_info(oldname, AccessExclusiveLock, &db_id, NULL, NULL,
+	if (!get_db_info(oldname, AccessExclusiveLock, &db_id, NULL, NULL, NULL,
 					 NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL))
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_DATABASE),
@@ -1927,7 +1929,7 @@ movedb(const char *dbname, const char *tblspcname)
 	 */
 	pgdbrel = table_open(DatabaseRelationId, RowExclusiveLock);
 
-	if (!get_db_info(dbname, AccessExclusiveLock, &db_id, NULL, NULL,
+	if (!get_db_info(dbname, AccessExclusiveLock, &db_id, NULL, NULL, NULL,
 					 NULL, NULL, NULL, NULL, &src_tblspcoid, NULL, NULL, NULL, NULL, NULL, NULL))
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_DATABASE),
@@ -2693,7 +2695,7 @@ pg_database_collation_actual_version(PG_FUNCTION_ARGS)
 static bool
 get_db_info(const char *name, LOCKMODE lockmode,
 			Oid *dbIdP, Oid *ownerIdP,
-			int *encodingP, bool *dbIsTemplateP, bool *dbAllowConnP,
+			int *encodingP, bool *dbIsTemplateP, bool *dbAllowConnP, bool *dbHasLoginEvtP,
 			TransactionId *dbFrozenXidP, MultiXactId *dbMinMultiP,
 			Oid *dbTablespace, char **dbCollate, char **dbCtype, char **dbIculocale,
 			char **dbIcurules,
@@ -2778,6 +2780,9 @@ get_db_info(const char *name, LOCKMODE lockmode,
 				/* allowed as template? */
 				if (dbIsTemplateP)
 					*dbIsTemplateP = dbform->datistemplate;
+				/* Has on login event trigger? */
+				if (dbHasLoginEvtP)
+					*dbHasLoginEvtP = dbform->dathasloginevt;
 				/* allowing connections? */
 				if (dbAllowConnP)
 					*dbAllowConnP = dbform->datallowconn;
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index bd812e42d94..fda1d472fdd 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -37,15 +38,18 @@
 #include "miscadmin.h"
 #include "parser/parse_func.h"
 #include "pgstat.h"
+#include "storage/lmgr.h"
 #include "tcop/deparse_utility.h"
 #include "tcop/utility.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 typedef struct EventTriggerQueryState
@@ -103,6 +107,7 @@ static void validate_table_rewrite_tags(const char *filtervar, List *taglist);
 static void EventTriggerInvoke(List *fn_oid_list, EventTriggerData *trigdata);
 static const char *stringify_grant_objtype(ObjectType objtype);
 static const char *stringify_adefprivs_objtype(ObjectType objtype);
+static void SetDatatabaseHasLoginEventTriggers(void);
 
 /*
  * Create an event trigger.
@@ -133,6 +138,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "login") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -165,6 +171,10 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	else if (strcmp(stmt->eventname, "table_rewrite") == 0
 			 && tags != NULL)
 		validate_table_rewrite_tags("tag", tags);
+	else if (strcmp(stmt->eventname, "login") == 0 && tags != NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("Tag filtering is not supported for login event trigger")));
 
 	/*
 	 * Give user a nice error message if an event trigger of the same name
@@ -296,6 +306,13 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
 	CatalogTupleInsert(tgrel, tuple);
 	heap_freetuple(tuple);
 
+	/*
+	 * Login event triggers have an additional flag in pg_database to avoid
+	 * faster lookups in hot codepaths. Set the flag unless already True.
+	 */
+	if (strcmp(eventname, "login") == 0)
+		SetDatatabaseHasLoginEventTriggers();
+
 	/* Depend on owner. */
 	recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
 
@@ -357,6 +374,39 @@ filter_list_to_array(List *filterlist)
 	return PointerGetDatum(construct_array_builtin(data, l, TEXTOID));
 }
 
+/*
+ * Set pg_database.dathasloginevt flag for current database indicating that
+ * current database has on login triggers.
+ */
+void
+SetDatatabaseHasLoginEventTriggers(void)
+{
+	/* Set dathasloginevt flag in pg_database */
+	Form_pg_database db;
+	Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+	HeapTuple	tuple;
+
+	/*
+	 * Use shared lock to prevent a conflit with EventTriggerOnLogin() trying
+	 * to reset pg_database.dathasloginevt flag.  Note that we use
+	 * AccessShareLock allowing setters concurently.
+	 */
+	LockSharedObject(DatabaseRelationId, MyDatabaseId, 0, AccessExclusiveLock);
+
+	tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+	db = (Form_pg_database) GETSTRUCT(tuple);
+	if (!db->dathasloginevt)
+	{
+		db->dathasloginevt = true;
+		CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+		CommandCounterIncrement();
+	}
+	table_close(pg_db, RowExclusiveLock);
+	heap_freetuple(tuple);
+}
+
 /*
  * ALTER EVENT TRIGGER foo ENABLE|DISABLE|ENABLE ALWAYS|REPLICA
  */
@@ -391,6 +441,14 @@ AlterEventTrigger(AlterEventTrigStmt *stmt)
 
 	CatalogTupleUpdate(tgrel, &tup->t_self, tup);
 
+	/*
+	 * Login event triggers have an additional flag in pg_database to avoid
+	 * faster lookups in hot codepaths. Set the flag unless already True.
+	 */
+	if (namestrcmp(&evtForm->evtevent, "login") == 0 &&
+		tgenabled != TRIGGER_DISABLED)
+		SetDatatabaseHasLoginEventTriggers();
+
 	InvokeObjectPostAlterHook(EventTriggerRelationId,
 							  trigoid, 0);
 
@@ -549,6 +607,15 @@ filter_event_trigger(CommandTag tag, EventTriggerCacheItem *item)
 	return true;
 }
 
+static CommandTag
+EventTriggerGetTag(Node *parsetree, EventTriggerEvent event)
+{
+	if (event == EVT_Login)
+		return CMDTAG_LOGIN;
+	else
+		return CreateCommandTag(parsetree);
+}
+
 /*
  * Setup for running triggers for the given event.  Return value is an OID list
  * of functions to run; if there are any, trigdata is filled with an
@@ -557,7 +624,7 @@ filter_event_trigger(CommandTag tag, EventTriggerCacheItem *item)
 static List *
 EventTriggerCommonSetup(Node *parsetree,
 						EventTriggerEvent event, const char *eventstr,
-						EventTriggerData *trigdata)
+						EventTriggerData *trigdata, bool unfiltered)
 {
 	CommandTag	tag;
 	List	   *cachelist;
@@ -582,10 +649,12 @@ EventTriggerCommonSetup(Node *parsetree,
 	{
 		CommandTag	dbgtag;
 
-		dbgtag = CreateCommandTag(parsetree);
+		dbgtag = EventTriggerGetTag(parsetree, event);
+
 		if (event == EVT_DDLCommandStart ||
 			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
+			event == EVT_SQLDrop ||
+			event == EVT_Login)
 		{
 			if (!command_tag_event_trigger_ok(dbgtag))
 				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
@@ -604,7 +673,7 @@ EventTriggerCommonSetup(Node *parsetree,
 		return NIL;
 
 	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
+	tag = EventTriggerGetTag(parsetree, event);
 
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
@@ -617,7 +686,7 @@ EventTriggerCommonSetup(Node *parsetree,
 	{
 		EventTriggerCacheItem *item = lfirst(lc);
 
-		if (filter_event_trigger(tag, item))
+		if (unfiltered || filter_event_trigger(tag, item))
 		{
 			/* We must plan to fire this trigger. */
 			runlist = lappend_oid(runlist, item->fnoid);
@@ -670,7 +739,7 @@ EventTriggerDDLCommandStart(Node *parsetree)
 	runlist = EventTriggerCommonSetup(parsetree,
 									  EVT_DDLCommandStart,
 									  "ddl_command_start",
-									  &trigdata);
+									  &trigdata, false);
 	if (runlist == NIL)
 		return;
 
@@ -718,7 +787,7 @@ EventTriggerDDLCommandEnd(Node *parsetree)
 
 	runlist = EventTriggerCommonSetup(parsetree,
 									  EVT_DDLCommandEnd, "ddl_command_end",
-									  &trigdata);
+									  &trigdata, false);
 	if (runlist == NIL)
 		return;
 
@@ -764,7 +833,7 @@ EventTriggerSQLDrop(Node *parsetree)
 
 	runlist = EventTriggerCommonSetup(parsetree,
 									  EVT_SQLDrop, "sql_drop",
-									  &trigdata);
+									  &trigdata, false);
 
 	/*
 	 * Nothing to do if run list is empty.  Note this typically can't happen,
@@ -805,6 +874,96 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Fire login event triggers if any are present.  The dathasloginevt
+ * pg_database flag is left when an event trigger is dropped, to avoid
+ * complicating the codepath in the case of multiple event triggers.  This
+ * function will instead unset the flag if no trigger is defined.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode or via a GUC.  We also need a
+	 * database connection (some background workers doesn't have it).
+	 */
+	if (!IsUnderPostmaster || !event_triggers ||
+		!OidIsValid(MyDatabaseId) || !MyDatabaseHasLoginEventTriggers)
+		return;
+
+	StartTransactionCommand();
+	runlist = EventTriggerCommonSetup(NULL,
+										EVT_Login, "login",
+										&trigdata, false);
+
+	if (runlist != NIL)
+	{
+		/*
+		 * Event trigger execution may require an active snapshot.
+		 */
+		PushActiveSnapshot(GetTransactionSnapshot());
+
+		/* Run the triggers. */
+		EventTriggerInvoke(runlist, &trigdata);
+
+		/* Cleanup. */
+		list_free(runlist);
+
+		PopActiveSnapshot();
+	}
+	/*
+	 * There is no active login event trigger, but our pg_database.dathasloginevt was set.
+	 * Try to unset this flag.  We use the lock to prevent concurrent
+	 * SetDatatabaseHasLoginEventTriggers(), but we don't want to hang the
+	 * connection waiting on the lock.  Thus, we are just trying to acquire
+	 * the lock conditionally.
+	 */
+	else if (ConditionalLockSharedObject(DatabaseRelationId, MyDatabaseId,
+										 0, AccessExclusiveLock))
+	{
+		/*
+		 * The lock is held.  Now we need to recheck that login event triggers
+		 * list is still empty.  Once the list is empty, we know that even if
+		 * there is a backend, which concurrently inserts/enables login trigger,
+		 * it will update pg_database.dathasloginevt *afterwards*.
+		 */
+		runlist = EventTriggerCommonSetup(NULL,
+										  EVT_Login, "login",
+										  &trigdata, true);
+
+		if (runlist == NIL)
+		{
+			Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+			HeapTuple	tuple;
+			Form_pg_database db;
+
+			tuple = SearchSysCacheCopy1(DATABASEOID,
+										ObjectIdGetDatum(MyDatabaseId));
+
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+			db = (Form_pg_database) GETSTRUCT(tuple);
+			if (db->dathasloginevt)
+			{
+				db->dathasloginevt = false;
+				CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+			}
+			table_close(pg_db, RowExclusiveLock);
+			heap_freetuple(tuple);
+		}
+		else
+		{
+			list_free(runlist);
+		}
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
@@ -835,7 +994,7 @@ EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason)
 	runlist = EventTriggerCommonSetup(parsetree,
 									  EVT_TableRewrite,
 									  "table_rewrite",
-									  &trigdata);
+									  &trigdata, false);
 	if (runlist == NIL)
 		return;
 
diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c
index ee9b89a6726..b447ddf11ba 100644
--- a/src/backend/storage/lmgr/lmgr.c
+++ b/src/backend/storage/lmgr/lmgr.c
@@ -1060,6 +1060,44 @@ LockSharedObject(Oid classid, Oid objid, uint16 objsubid,
 	AcceptInvalidationMessages();
 }
 
+/*
+ *		ConditionalLockSharedObject
+ *
+ * As above, but only lock if we can get the lock without blocking.
+ * Returns true iff the lock was acquired.
+ */
+bool
+ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid,
+							LOCKMODE lockmode)
+{
+	LOCKTAG		tag;
+	LOCALLOCK  *locallock;
+	LockAcquireResult res;
+
+	SET_LOCKTAG_OBJECT(tag,
+					   InvalidOid,
+					   classid,
+					   objid,
+					   objsubid);
+
+	res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock);
+
+	if (res == LOCKACQUIRE_NOT_AVAIL)
+		return false;
+
+	/*
+	 * Now that we have the lock, check for invalidation messages; see notes
+	 * in LockRelationOid.
+	 */
+	if (res != LOCKACQUIRE_ALREADY_CLEAR)
+	{
+		AcceptInvalidationMessages();
+		MarkLockClear(locallock);
+	}
+
+	return true;
+}
+
 /*
  *		UnlockSharedObject
  */
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 21b9763183e..20d8202cb7d 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -36,6 +36,7 @@
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/prepare.h"
 #include "common/pg_prng.h"
 #include "jit/jit.h"
@@ -4288,6 +4289,9 @@ PostgresMain(const char *dbname, const char *username)
 	initStringInfo(&row_description_buf);
 	MemoryContextSwitchTo(TopMemoryContext);
 
+	/* Fire any defined login event triggers, if appropriate */
+	EventTriggerOnLogin();
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index b080f7a35f3..ab5111c90fd 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "login") == 0)
+			event = EVT_Login;
 		else
 			continue;
 
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 011ec18015a..60bc1217fb4 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -90,6 +90,8 @@ Oid			MyDatabaseId = InvalidOid;
 
 Oid			MyDatabaseTableSpace = InvalidOid;
 
+bool		MyDatabaseHasLoginEventTriggers = false;
+
 /*
  * DatabasePath is the path (relative to DataDir) of my database's
  * primary directory, ie, its directory in the default tablespace.
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index df4d15a50fb..e23fba41157 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -1101,6 +1101,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 		}
 
 		MyDatabaseTableSpace = datform->dattablespace;
+		MyDatabaseHasLoginEventTriggers = datform->dathasloginevt;
 		/* pass the database name back to the caller */
 		if (out_dbname)
 			strcpy(out_dbname, dbname);
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index f7b61766921..83aeef2751b 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3263,6 +3263,11 @@ dumpDatabase(Archive *fout)
 		appendPQExpBufferStr(delQry, ";\n");
 	}
 
+	/*
+	 * We do not restore pg_database.dathasloginevt because it is set
+	 * automatically on login event trigger creation.
+	 */
+
 	/* Add database-specific SET options */
 	dumpDatabaseConfig(fout, creaQry, datname, dbCatId.oid);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index d30d719a1f8..2eb8ff1afca 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3552,8 +3552,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop",
-					  "table_rewrite");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "login",
+					  "sql_drop", "table_rewrite");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat
index 0754ef1bce4..8d91e3bf8da 100644
--- a/src/include/catalog/pg_database.dat
+++ b/src/include/catalog/pg_database.dat
@@ -16,7 +16,7 @@
   descr => 'default template for new databases',
   datname => 'template1', encoding => 'ENCODING',
   datlocprovider => 'LOCALE_PROVIDER', datistemplate => 't',
-  datallowconn => 't', datconnlimit => '-1', datfrozenxid => '0',
+  datallowconn => 't', dathasloginevt => 'f', datconnlimit => '-1', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datcollate => 'LC_COLLATE',
   datctype => 'LC_CTYPE', daticulocale => 'ICU_LOCALE',
   daticurules => 'ICU_RULES', datacl => '_null_' },
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index e9eb06b2e53..3e50a570046 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -49,6 +49,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
 	/* new connections allowed? */
 	bool		datallowconn;
 
+	/* database has login event triggers? */
+	bool		dathasloginevt;
+
 	/*
 	 * Max connections allowed. Negative values have special meaning, see
 	 * DATCONNLIMIT_* defines below.
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 1c925dbf257..9e3ece50d5f 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -56,6 +56,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnLogin(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 14bd574fc24..c9cad452d96 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -203,6 +203,8 @@ extern PGDLLIMPORT Oid MyDatabaseId;
 
 extern PGDLLIMPORT Oid MyDatabaseTableSpace;
 
+extern PGDLLIMPORT bool MyDatabaseHasLoginEventTriggers;
+
 /*
  * Date/Time Configuration
  *
diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h
index 4ee91e3cf93..952ebe75cb4 100644
--- a/src/include/storage/lmgr.h
+++ b/src/include/storage/lmgr.h
@@ -99,6 +99,8 @@ extern void UnlockDatabaseObject(Oid classid, Oid objid, uint16 objsubid,
 /* Lock a shared-across-databases object (other than a relation) */
 extern void LockSharedObject(Oid classid, Oid objid, uint16 objsubid,
 							 LOCKMODE lockmode);
+extern bool ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid,
+										LOCKMODE lockmode);
 extern void UnlockSharedObject(Oid classid, Oid objid, uint16 objsubid,
 							   LOCKMODE lockmode);
 
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index e738ac1c097..553a31874f1 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
+PG_CMDTAG(CMDTAG_LOGIN, "LOGIN", true, false, false)
 PG_CMDTAG(CMDTAG_MERGE, "MERGE", false, false, true)
 PG_CMDTAG(CMDTAG_MOVE, "MOVE", false, false, true)
 PG_CMDTAG(CMDTAG_NOTIFY, "NOTIFY", false, false, false)
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index d340026518a..52052e6252a 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Login,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/authentication/t/005_login_trigger.pl b/src/test/authentication/t/005_login_trigger.pl
new file mode 100644
index 00000000000..fec664fb865
--- /dev/null
+++ b/src/test/authentication/t/005_login_trigger.pl
@@ -0,0 +1,157 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+# Tests of authentication via login trigger. Mostly for rejection via
+# exception, because this scenario cannot be covered with *.sql/*.out regress
+# tests.
+
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Execute a psql command and compare its output towards given regexps
+sub psql_command
+{
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+	my ($node, $sql, $expected_ret, $test_name, %params) = @_;
+
+	my $connstr;
+	if (defined($params{connstr}))
+	{
+		$connstr = $params{connstr};
+	}
+	else
+	{
+		$connstr = '';
+	}
+
+	# Execute command
+	my ($ret, $stdout, $stderr) = $node->psql('postgres', $sql,
+											  connstr => "$connstr");
+
+	# Check return code
+	is($ret, $expected_ret,  "$test_name: exit code $expected_ret");
+
+	# Check stdout
+	if (defined($params{log_like}))
+	{
+		my @log_like = @{ $params{log_like} };
+		while (my $regex = shift @log_like)
+		{
+			like($stdout, $regex, "$test_name: log matches");
+		}
+	}
+	if (defined($params{log_unlike}))
+	{
+		my @log_unlike = @{ $params{log_unlike} };
+		while (my $regex = shift @log_unlike)
+		{
+			unlike($stdout, $regex, "$test_name: log unmatches");
+		}
+	}
+	if (defined($params{log_exact}))
+	{
+		is($stdout, $params{log_exact}, "$test_name: log equals");
+	}
+
+	# Check stderr
+	if (defined($params{err_like}))
+	{
+		my @err_like = @{ $params{err_like} };
+		while (my $regex = shift @err_like)
+		{
+			like($stderr, $regex, "$test_name: err matches");
+		}
+	}
+	if (defined($params{err_unlike}))
+	{
+		my @err_unlike = @{ $params{err_unlike} };
+		while (my $regex = shift @err_unlike)
+		{
+			unlike($stderr, $regex, "$test_name: err unmatches");
+		}
+	}
+	if (defined($params{err_exact}))
+	{
+		is($stderr, $params{err_exact}, "$test_name: err equals");
+	}
+
+	return;
+}
+
+# New node
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init(extra => [ '--locale=C', '--encoding=UTF8' ]);
+$node->append_conf(
+	'postgresql.conf', q{
+wal_level = 'logical'
+max_replication_slots = 4
+max_wal_senders = 4
+});
+$node->start;
+
+# Create temporary roles and log table
+psql_command($node, 'CREATE ROLE alice WITH LOGIN;
+CREATE ROLE mallory WITH LOGIN;
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+', 0, 'create tmp objects', log_exact => '', err_exact => ''), ;
+
+# Create login event function and trigger
+psql_command($node,
+			 'CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  IF SESSION_USER = \'mallory\' THEN
+    RAISE EXCEPTION \'Hello %! You are NOT welcome here!\', SESSION_USER;
+  END IF;
+  RAISE NOTICE \'Hello %! You are welcome!\', SESSION_USER;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+', 0, 'create trigger function', log_exact => '', err_exact => '');
+
+psql_command($node,
+			 'CREATE EVENT TRIGGER on_login_trigger '
+			 .'ON login EXECUTE PROCEDURE on_login_proc();', 0,
+			 'create event trigger', log_exact => '', err_exact => '');
+psql_command($node, 'ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;', 0,
+			 'alter event trigger', log_exact => '',
+			 err_like => [qr/You are welcome/]);
+
+# Check the two requests were logged via login trigger
+psql_command($node, 'SELECT COUNT(*) FROM user_logins;', 0, 'select count',
+			 log_exact => '2', err_like => [qr/You are welcome/]);
+
+# Try to log as allowed Alice and disallowed Mallory (two times)
+psql_command($node, 'SELECT 1;', 0, 'try alice', connstr => 'user=alice',
+			 log_exact => '1', err_like => [qr/You are welcome/],
+			 err_unlike => [qr/You are NOT welcome/]);
+psql_command($node, 'SELECT 1;', 2, 'try mallory', connstr => 'user=mallory',
+			 log_exact => '', err_like => [qr/You are NOT welcome/],
+			 err_unlike => [qr/You are welcome/]);
+psql_command($node, 'SELECT 1;', 2, 'try mallory', connstr => 'user=mallory',
+			 log_exact => '', err_like => [qr/You are NOT welcome/],
+			 err_unlike => [qr/You are welcome/]);
+
+# Check that Alice's login record is here, while the Mallory's one is not
+psql_command($node, 'SELECT * FROM user_logins;', 0, 'select *',
+			 log_like => [qr/3\|alice/], log_unlike => [qr/mallory/],
+			 err_like => [qr/You are welcome/]);
+
+# Check total number of successful logins so far
+psql_command($node, 'SELECT COUNT(*) FROM user_logins;', 0, 'select count',
+			 log_exact => '5', err_like => [qr/You are welcome/]);
+
+# Cleanup the temporary stuff
+psql_command($node, 'DROP EVENT TRIGGER on_login_trigger;', 0,
+			 'drop event trigger', log_exact => '',
+			 err_like => [qr/You are welcome/]);
+psql_command($node, 'DROP TABLE user_logins;
+DROP FUNCTION on_login_proc;
+DROP ROLE mallory;
+DROP ROLE alice;
+', 0, 'cleanup', log_exact => '', err_exact => '');
+
+done_testing();
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 0c72ba09441..8644854eb39 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,24 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE TABLE user_logins(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO user_logins (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 $node_primary->wait_for_replay_catchup($node_standby_1);
 $node_standby_1->wait_for_replay_catchup($node_standby_2, $node_primary);
@@ -384,6 +402,11 @@ sub replay_check
 
 replay_check();
 
+my $evttrig = $node_standby_1->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+$evttrig = $node_standby_2->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 0b87a42d0a9..eaaff6ba6f1 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -638,3 +638,48 @@ NOTICE:  DROP POLICY dropped policy
 CREATE POLICY pguc ON event_trigger_test USING (FALSE);
 SET event_triggers = 'off';
 DROP POLICY pguc ON event_trigger_test;
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     1
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     2
+(1 row)
+
+-- Check dathasloginevt in system catalog
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+ dathasloginevt 
+----------------
+ t
+(1 row)
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
+\c
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+ dathasloginevt 
+----------------
+ f
+(1 row)
+
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 6f0933b9e88..9c2b7903fba 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -495,3 +495,29 @@ DROP POLICY pguc ON event_trigger_test;
 CREATE POLICY pguc ON event_trigger_test USING (FALSE);
 SET event_triggers = 'off';
 DROP POLICY pguc ON event_trigger_test;
+
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+SELECT COUNT(*) FROM user_logins;
+\c
+SELECT COUNT(*) FROM user_logins;
+
+-- Check dathasloginevt in system catalog
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
+\c
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
-- 
2.39.3 (Apple Git-145)

#162Alexander Korotkov
aekorotkov@gmail.com
In reply to: Robert Haas (#159)
Re: On login trigger: take three

Hi, Robert!

Thank you for your feedback.

On Tue, Oct 10, 2023 at 5:51 PM Robert Haas <robertmhaas@gmail.com> wrote:

On Mon, Oct 9, 2023 at 10:11 AM Alexander Korotkov <aekorotkov@gmail.com> wrote:

* Hold lock during setting of pg_database.dathasloginevt flag (v32
version actually didn't prevent race condition).

So ... how does getting this flag set actually work? And how does
clearing it work?

I hope I explained that in [1].

In the case of row-level security, you have to explicitly enable the
flag on the table level using DDL provided for that purpose. In the
case of relhas{rules,triggers,subclass} the flag is set automatically
as a side-effect of some other operation. I tend to consider that the
latter design is somewhat messy. It's potentially even messier here,
because at least when you add a rule or a trigger to a table you're
expecting to take a lock on the table anyway. I don't think you
necessarily expect creating a login trigger to take a lock on the
database. That's a bit odd and could have visible side effects. And if
you don't, then what happens is that if you create two login triggers
in overlapping transactions, then (1) if there were no login triggers
previously, one of the transactions fails with an internal-looking
error message about a concurrent tuple update and (2) if there were
login triggers previously, then it works fine.

Yep, in v43 it worked that way. One transaction has to wait for
another finishing update of pg_database tuple, then fails. This is
obviously ridiculous. Since overlapping setters of flag will have to
wait anyway, I changed lock mode in v44 for them to
AccessExclusiveLock. Now, waiting transaction then sees the updated
tuple and doesn't fail.

That's also a bit weird
and surprising. Now the counter-argument could be that adding new DDL
to enable login triggers for a database is too much cognitive burden
and it's better to have the kind of weird and surprising behavior that
I just discussed. I don't know that I would buy that argument, but it
could be made ... and my real point here is that I don't even see
these trade-offs being discussed. Apologies if they were discussed
earlier and I just missed that; I confess to not having read every
email message on this topic, and some of the ones I did read I read a
long time ago.

I have read the thread quite carefully. I don't think manual setting
of the flag was discussed. I do think it would be extra burden for
users, and I would prefer automatic flag as long as it works
transparently and reliably.

This version should be good and has no overhead. Any thoughts?
Daniel, could you please re-run the performance tests?

Is "no overhead" an overly bold claim here?

Yes, sure. I meant no extra lookup. Hopefully that would mean no
measurable overhead when is no enabled login triggers.

Links
1. /messages/by-id/CAPpHfdtvvozi=ttp8kvJQwuLrP5Q0D_5c4Pw1U67MRXcROB9yA@mail.gmail.com

------
Regards,
Alexander Korotkov

#163Robert Haas
robertmhaas@gmail.com
In reply to: Alexander Korotkov (#162)
Re: On login trigger: take three

On Tue, Oct 10, 2023 at 3:43 PM Alexander Korotkov <aekorotkov@gmail.com> wrote:

Yep, in v43 it worked that way. One transaction has to wait for
another finishing update of pg_database tuple, then fails. This is
obviously ridiculous. Since overlapping setters of flag will have to
wait anyway, I changed lock mode in v44 for them to
AccessExclusiveLock. Now, waiting transaction then sees the updated
tuple and doesn't fail.

Doesn't that mean that if you create the first login trigger in a
database and leave the transaction open, nobody can connect to that
database until the transaction ends?

--
Robert Haas
EDB: http://www.enterprisedb.com

#164Alexander Korotkov
aekorotkov@gmail.com
In reply to: Robert Haas (#163)
Re: On login trigger: take three

On Thu, Oct 12, 2023 at 8:35 PM Robert Haas <robertmhaas@gmail.com> wrote:

On Tue, Oct 10, 2023 at 3:43 PM Alexander Korotkov <aekorotkov@gmail.com> wrote:

Yep, in v43 it worked that way. One transaction has to wait for
another finishing update of pg_database tuple, then fails. This is
obviously ridiculous. Since overlapping setters of flag will have to
wait anyway, I changed lock mode in v44 for them to
AccessExclusiveLock. Now, waiting transaction then sees the updated
tuple and doesn't fail.

Doesn't that mean that if you create the first login trigger in a
database and leave the transaction open, nobody can connect to that
database until the transaction ends?

It doesn't mean that, because when trying to reset the flag v44 does
conditional lock. So, if another transaction is holding the log we
will just skip resetting the flag. So, the flag will be cleared on
the first connection after that transaction ends.

------
Regards,
Alexander Korotkov

#165Robert Haas
robertmhaas@gmail.com
In reply to: Alexander Korotkov (#164)
Re: On login trigger: take three

On Thu, Oct 12, 2023 at 6:54 PM Alexander Korotkov <aekorotkov@gmail.com> wrote:

On Thu, Oct 12, 2023 at 8:35 PM Robert Haas <robertmhaas@gmail.com> wrote:

Doesn't that mean that if you create the first login trigger in a
database and leave the transaction open, nobody can connect to that
database until the transaction ends?

It doesn't mean that, because when trying to reset the flag v44 does
conditional lock. So, if another transaction is holding the log we
will just skip resetting the flag. So, the flag will be cleared on
the first connection after that transaction ends.

But in the scenario I am describing the flag is being set, not reset.

--
Robert Haas
EDB: http://www.enterprisedb.com

#166Alexander Korotkov
aekorotkov@gmail.com
In reply to: Robert Haas (#165)
Re: On login trigger: take three

On Fri, Oct 13, 2023 at 4:18 AM Robert Haas <robertmhaas@gmail.com> wrote:

On Thu, Oct 12, 2023 at 6:54 PM Alexander Korotkov <aekorotkov@gmail.com> wrote:

On Thu, Oct 12, 2023 at 8:35 PM Robert Haas <robertmhaas@gmail.com> wrote:

Doesn't that mean that if you create the first login trigger in a
database and leave the transaction open, nobody can connect to that
database until the transaction ends?

It doesn't mean that, because when trying to reset the flag v44 does
conditional lock. So, if another transaction is holding the log we
will just skip resetting the flag. So, the flag will be cleared on
the first connection after that transaction ends.

But in the scenario I am describing the flag is being set, not reset.

Sorry, it seems I just missed some logical steps. Imagine, that
transaction A created the first login trigger and hangs open. Then
the new connection B sees no visible triggers yet, but dathasloginevt
flag is set. Therefore, connection B tries conditional lock but just
gives up because the lock is held by transaction A.

Also, note that the lock has been just some lock with a custom tag.
It doesn't effectively block the database. You may think about it as
of custom advisory lock.

------
Regards,
Alexander Korotkov

#167Alexander Korotkov
aekorotkov@gmail.com
In reply to: Alexander Korotkov (#166)
1 attachment(s)
Re: On login trigger: take three

On Fri, Oct 13, 2023 at 11:26 AM Alexander Korotkov
<aekorotkov@gmail.com> wrote:

On Fri, Oct 13, 2023 at 4:18 AM Robert Haas <robertmhaas@gmail.com> wrote:

On Thu, Oct 12, 2023 at 6:54 PM Alexander Korotkov <aekorotkov@gmail.com> wrote:

On Thu, Oct 12, 2023 at 8:35 PM Robert Haas <robertmhaas@gmail.com> wrote:

Doesn't that mean that if you create the first login trigger in a
database and leave the transaction open, nobody can connect to that
database until the transaction ends?

It doesn't mean that, because when trying to reset the flag v44 does
conditional lock. So, if another transaction is holding the log we
will just skip resetting the flag. So, the flag will be cleared on
the first connection after that transaction ends.

But in the scenario I am describing the flag is being set, not reset.

Sorry, it seems I just missed some logical steps. Imagine, that
transaction A created the first login trigger and hangs open. Then
the new connection B sees no visible triggers yet, but dathasloginevt
flag is set. Therefore, connection B tries conditional lock but just
gives up because the lock is held by transaction A.

Also, note that the lock has been just some lock with a custom tag.
It doesn't effectively block the database. You may think about it as
of custom advisory lock.

I've revised the comments about the lock a bit. I've also run some
tests regarding the connection time (5 runs).

v45, event_triggers=on: avg=3.081ms, min=2.992ms, max=3.146ms
v45, event_triggers=off: avg=3.132ms, min=3.048ms, max=3.186ms
master: 3.080ms, min=3.023ms, max=3.167ms

So, no measurable overhead (not surprising since no extra catalog lookup).
I think this patch is in the commitable shape. Any objections?

------
Regards,
Alexander Korotkov

Attachments:

0001-Add-support-event-triggers-on-authenticated-logi-v45.patchapplication/x-patch; name=0001-Add-support-event-triggers-on-authenticated-logi-v45.patchDownload
From 1d59d7fe6f510a4606600b9bf41b5dbaa6d2b283 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <akorotkov@postgresql.org>
Date: Mon, 9 Oct 2023 15:00:26 +0300
Subject: [PATCH] Add support event triggers on authenticated login

This commit introduces trigger on login event, allowing to fire some actions
right on the user connection.  This can be useful for logging or connection
check purposes as well as for some personalization of environment.  Usage
details are described in the documentation included, but shortly usage is
the same as for other triggers: create function returning event_trigger and
then create event trigger on login event.

In order to prevent the connection time overhead when there are no triggers
the commit introduces pg_database.dathasloginevt flag, which indicates database
has active login triggers.  This flag is set by CREATE/ALTER EVENT TRIGGER
command, and unset at connection time when no active triggers found.

Author: Konstantin Knizhnik, Mikhail Gribkov
Discussion: https://postgr.es/m/0d46d29f-4558-3af9-9c85-7774e14a7709%40postgrespro.ru
Reviewed-by: Pavel Stehule, Takayuki Tsunakawa, Greg Nancarrow, Ivan Panchenko
Reviewed-by: Daniel Gustafsson, Teodor Sigaev, Robert Haas, Andres Freund
Reviewed-by: Tom Lane, Andrey Sokolov, Zhihong Yu, Sergey Shinderuk
Reviewed-by: Gregory Stark, Nikita Malakhov, Ted Yu
---
 doc/src/sgml/bki.sgml                         |   2 +-
 doc/src/sgml/catalogs.sgml                    |  13 ++
 doc/src/sgml/ecpg.sgml                        |   2 +
 doc/src/sgml/event-trigger.sgml               |  94 +++++++++
 src/backend/commands/dbcommands.c             |  17 +-
 src/backend/commands/event_trigger.c          | 179 +++++++++++++++++-
 src/backend/storage/lmgr/lmgr.c               |  38 ++++
 src/backend/tcop/postgres.c                   |   4 +
 src/backend/utils/cache/evtcache.c            |   2 +
 src/backend/utils/init/globals.c              |   2 +
 src/backend/utils/init/postinit.c             |   1 +
 src/bin/pg_dump/pg_dump.c                     |   5 +
 src/bin/psql/tab-complete.c                   |   4 +-
 src/include/catalog/pg_database.dat           |   2 +-
 src/include/catalog/pg_database.h             |   3 +
 src/include/commands/event_trigger.h          |   1 +
 src/include/miscadmin.h                       |   2 +
 src/include/storage/lmgr.h                    |   2 +
 src/include/tcop/cmdtaglist.h                 |   1 +
 src/include/utils/evtcache.h                  |   3 +-
 .../authentication/t/005_login_trigger.pl     | 157 +++++++++++++++
 src/test/recovery/t/001_stream_rep.pl         |  23 +++
 src/test/regress/expected/event_trigger.out   |  45 +++++
 src/test/regress/sql/event_trigger.sql        |  26 +++
 24 files changed, 608 insertions(+), 20 deletions(-)
 create mode 100644 src/test/authentication/t/005_login_trigger.pl

diff --git a/doc/src/sgml/bki.sgml b/doc/src/sgml/bki.sgml
index f71644e3989..315ba819514 100644
--- a/doc/src/sgml/bki.sgml
+++ b/doc/src/sgml/bki.sgml
@@ -184,7 +184,7 @@
   descr => 'database\'s default template',
   datname => 'template1', encoding => 'ENCODING',
   datlocprovider => 'LOCALE_PROVIDER', datistemplate => 't',
-  datallowconn => 't', datconnlimit => '-1', datfrozenxid => '0',
+  datallowconn => 't', dathasloginevt => 'f', datconnlimit => '-1', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datcollate => 'LC_COLLATE',
   datctype => 'LC_CTYPE', daticulocale => 'ICU_LOCALE', datacl => '_null_' },
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index e09adb45e41..d3458840fbe 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -3035,6 +3035,19 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>dathasloginevt</structfield> <type>bool</type>
+      </para>
+      <para>
+        Indicates that there are login event triggers defined for this database.
+        This flag is used to avoid extra lookups on the
+        <structname>pg_event_trigger</structname> table during each backend
+        startup.  This flag is used internally by <productname>PostgreSQL</productname>
+        and should not be manually altered or read for monitoring purposes.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>datconnlimit</structfield> <type>int4</type>
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index f52165165dc..54de81158b5 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -4769,6 +4769,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = t (type: 1)
 datallowconn = t (type: 1)
+dathasloginevt = f (type: 1)
 datconnlimit = -1 (type: 5)
 datfrozenxid = 379 (type: 1)
 dattablespace = 1663 (type: 1)
@@ -4793,6 +4794,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = f (type: 1)
 datallowconn = t (type: 1)
+dathasloginevt = f (type: 1)
 datconnlimit = -1 (type: 5)
 datfrozenxid = 379 (type: 1)
 dattablespace = 1663 (type: 1)
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 3b6a5361b34..10b20f0339a 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>login</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,24 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>login</literal> event occurs when an authenticated user logs
+     into the system. Any bug in a trigger procedure for this event may
+     prevent successful login to the system. Such bugs may be fixed by
+     setting <xref linkend="guc-event-triggers"/> is set to <literal>false</literal>
+     either in a connection string or configuration file. Alternative is
+     restarting the system in single-user mode (as event triggers are
+     disabled in this mode). See the <xref linkend="app-postgres"/> reference
+     page for details about using single-user mode.
+     The <literal>login</literal> event will also fire on standby servers.
+     To prevent servers from becoming inaccessible, such triggers must avoid
+     writing anything to the database when running on a standby.
+     Also, it's recommended to avoid long-running queries in
+     <literal>login</literal> event triggers.  Notes that, for instance,
+     cancelling connection in <application>psql</application> wouldn't cancel
+     the in-progress <literal>login</literal> trigger.
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1300,4 +1319,79 @@ CREATE EVENT TRIGGER no_rewrite_allowed
 </programlisting>
    </para>
  </sect1>
+
+  <sect1 id="event-trigger-database-login-example">
+    <title>A Database Login Event Trigger Example</title>
+
+    <para>
+      The event trigger on the <literal>login</literal> event can be
+      useful for logging user logins, for verifying the connection and
+      assigning roles according to current circumstances, or for session
+      data initialization. It is very important that any event trigger using
+      the <literal>login</literal> event checks whether or not the database is
+      in recovery before performing any writes. Writing to a standby server
+      will make it inaccessible.
+    </para>
+
+    <para>
+      The following example demonstrates these options.
+<programlisting>
+-- create test tables and roles
+CREATE TABLE user_login_log (
+  "user" text,
+  "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+  RETURNS event_trigger SECURITY DEFINER
+  LANGUAGE plpgsql AS
+$$
+DECLARE
+  hour integer = EXTRACT('hour' FROM current_time at time zone 'utc');
+  rec boolean;
+BEGIN
+-- 1. Forbid logging in between 2AM and 4AM.
+IF hour BETWEEN 2 AND 4 THEN
+  RAISE EXCEPTION 'Login forbidden';
+END IF;
+
+-- The checks below cannot be performed on standby servers so
+-- ensure the database is not in recovery before we perform any
+-- operations.
+SELECT pg_is_in_recovery() INTO rec;
+IF rec THEN
+  RETURN;
+END IF;
+
+-- 2. Assign some roles. At daytime, grant the day_worker role, else the
+-- night_worker role.
+IF hour BETWEEN 8 AND 20 THEN
+  EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT day_worker TO ' || quote_ident(session_user);
+ELSE
+  EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT night_worker TO ' || quote_ident(session_user);
+END IF;
+
+-- 3. Initialize user session data
+CREATE TEMP TABLE session_storage (x float, y integer);
+ALTER TABLE session_storage OWNER TO session_user;
+
+-- 4. Log the connection time
+INSERT INTO public.user_login_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+  ON login
+  EXECUTE FUNCTION init_session();
+ALTER EVENT TRIGGER init_session ENABLE ALWAYS;
+</programlisting>
+    </para>
+  </sect1>
 </chapter>
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 307729ab7ef..b0c562aa1ea 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -116,7 +116,7 @@ static void movedb(const char *dbname, const char *tblspcname);
 static void movedb_failure_callback(int code, Datum arg);
 static bool get_db_info(const char *name, LOCKMODE lockmode,
 						Oid *dbIdP, Oid *ownerIdP,
-						int *encodingP, bool *dbIsTemplateP, bool *dbAllowConnP,
+						int *encodingP, bool *dbIsTemplateP, bool *dbAllowConnP, bool *dbHasLoginEvtP,
 						TransactionId *dbFrozenXidP, MultiXactId *dbMinMultiP,
 						Oid *dbTablespace, char **dbCollate, char **dbCtype, char **dbIculocale,
 						char **dbIcurules,
@@ -680,6 +680,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	char		src_locprovider = '\0';
 	char	   *src_collversion = NULL;
 	bool		src_istemplate;
+	bool		src_hasloginevt;
 	bool		src_allowconn;
 	TransactionId src_frozenxid = InvalidTransactionId;
 	MultiXactId src_minmxid = InvalidMultiXactId;
@@ -968,7 +969,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 
 	if (!get_db_info(dbtemplate, ShareLock,
 					 &src_dboid, &src_owner, &src_encoding,
-					 &src_istemplate, &src_allowconn,
+					 &src_istemplate, &src_allowconn, &src_hasloginevt,
 					 &src_frozenxid, &src_minmxid, &src_deftablespace,
 					 &src_collate, &src_ctype, &src_iculocale, &src_icurules, &src_locprovider,
 					 &src_collversion))
@@ -1375,6 +1376,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	new_record[Anum_pg_database_datlocprovider - 1] = CharGetDatum(dblocprovider);
 	new_record[Anum_pg_database_datistemplate - 1] = BoolGetDatum(dbistemplate);
 	new_record[Anum_pg_database_datallowconn - 1] = BoolGetDatum(dballowconnections);
+	new_record[Anum_pg_database_dathasloginevt - 1] = BoolGetDatum(src_hasloginevt);
 	new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 	new_record[Anum_pg_database_datfrozenxid - 1] = TransactionIdGetDatum(src_frozenxid);
 	new_record[Anum_pg_database_datminmxid - 1] = TransactionIdGetDatum(src_minmxid);
@@ -1602,7 +1604,7 @@ dropdb(const char *dbname, bool missing_ok, bool force)
 	 */
 	pgdbrel = table_open(DatabaseRelationId, RowExclusiveLock);
 
-	if (!get_db_info(dbname, AccessExclusiveLock, &db_id, NULL, NULL,
+	if (!get_db_info(dbname, AccessExclusiveLock, &db_id, NULL, NULL, NULL,
 					 &db_istemplate, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL))
 	{
 		if (!missing_ok)
@@ -1817,7 +1819,7 @@ RenameDatabase(const char *oldname, const char *newname)
 	 */
 	rel = table_open(DatabaseRelationId, RowExclusiveLock);
 
-	if (!get_db_info(oldname, AccessExclusiveLock, &db_id, NULL, NULL,
+	if (!get_db_info(oldname, AccessExclusiveLock, &db_id, NULL, NULL, NULL,
 					 NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL))
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_DATABASE),
@@ -1927,7 +1929,7 @@ movedb(const char *dbname, const char *tblspcname)
 	 */
 	pgdbrel = table_open(DatabaseRelationId, RowExclusiveLock);
 
-	if (!get_db_info(dbname, AccessExclusiveLock, &db_id, NULL, NULL,
+	if (!get_db_info(dbname, AccessExclusiveLock, &db_id, NULL, NULL, NULL,
 					 NULL, NULL, NULL, NULL, &src_tblspcoid, NULL, NULL, NULL, NULL, NULL, NULL))
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_DATABASE),
@@ -2693,7 +2695,7 @@ pg_database_collation_actual_version(PG_FUNCTION_ARGS)
 static bool
 get_db_info(const char *name, LOCKMODE lockmode,
 			Oid *dbIdP, Oid *ownerIdP,
-			int *encodingP, bool *dbIsTemplateP, bool *dbAllowConnP,
+			int *encodingP, bool *dbIsTemplateP, bool *dbAllowConnP, bool *dbHasLoginEvtP,
 			TransactionId *dbFrozenXidP, MultiXactId *dbMinMultiP,
 			Oid *dbTablespace, char **dbCollate, char **dbCtype, char **dbIculocale,
 			char **dbIcurules,
@@ -2778,6 +2780,9 @@ get_db_info(const char *name, LOCKMODE lockmode,
 				/* allowed as template? */
 				if (dbIsTemplateP)
 					*dbIsTemplateP = dbform->datistemplate;
+				/* Has on login event trigger? */
+				if (dbHasLoginEvtP)
+					*dbHasLoginEvtP = dbform->dathasloginevt;
 				/* allowing connections? */
 				if (dbAllowConnP)
 					*dbAllowConnP = dbform->datallowconn;
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index bd812e42d94..0b08552fd7a 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -37,15 +38,18 @@
 #include "miscadmin.h"
 #include "parser/parse_func.h"
 #include "pgstat.h"
+#include "storage/lmgr.h"
 #include "tcop/deparse_utility.h"
 #include "tcop/utility.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 typedef struct EventTriggerQueryState
@@ -103,6 +107,7 @@ static void validate_table_rewrite_tags(const char *filtervar, List *taglist);
 static void EventTriggerInvoke(List *fn_oid_list, EventTriggerData *trigdata);
 static const char *stringify_grant_objtype(ObjectType objtype);
 static const char *stringify_adefprivs_objtype(ObjectType objtype);
+static void SetDatatabaseHasLoginEventTriggers(void);
 
 /*
  * Create an event trigger.
@@ -133,6 +138,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "login") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -165,6 +171,10 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	else if (strcmp(stmt->eventname, "table_rewrite") == 0
 			 && tags != NULL)
 		validate_table_rewrite_tags("tag", tags);
+	else if (strcmp(stmt->eventname, "login") == 0 && tags != NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("Tag filtering is not supported for login event trigger")));
 
 	/*
 	 * Give user a nice error message if an event trigger of the same name
@@ -296,6 +306,13 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
 	CatalogTupleInsert(tgrel, tuple);
 	heap_freetuple(tuple);
 
+	/*
+	 * Login event triggers have an additional flag in pg_database to avoid
+	 * faster lookups in hot codepaths. Set the flag unless already True.
+	 */
+	if (strcmp(eventname, "login") == 0)
+		SetDatatabaseHasLoginEventTriggers();
+
 	/* Depend on owner. */
 	recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
 
@@ -357,6 +374,41 @@ filter_list_to_array(List *filterlist)
 	return PointerGetDatum(construct_array_builtin(data, l, TEXTOID));
 }
 
+/*
+ * Set pg_database.dathasloginevt flag for current database indicating that
+ * current database has on login triggers.
+ */
+void
+SetDatatabaseHasLoginEventTriggers(void)
+{
+	/* Set dathasloginevt flag in pg_database */
+	Form_pg_database db;
+	Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+	HeapTuple	tuple;
+
+	/*
+	 * Use shared lock to prevent a conflit with EventTriggerOnLogin() trying
+	 * to reset pg_database.dathasloginevt flag.  Note, this lock doesn't
+	 * effectively blocks database or other objection.  It's just custom lock
+	 * tag used to prevent multiple backends changing pg_database.dathasloginevt
+	 * flag.
+	 */
+	LockSharedObject(DatabaseRelationId, MyDatabaseId, 0, AccessExclusiveLock);
+
+	tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+	db = (Form_pg_database) GETSTRUCT(tuple);
+	if (!db->dathasloginevt)
+	{
+		db->dathasloginevt = true;
+		CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+		CommandCounterIncrement();
+	}
+	table_close(pg_db, RowExclusiveLock);
+	heap_freetuple(tuple);
+}
+
 /*
  * ALTER EVENT TRIGGER foo ENABLE|DISABLE|ENABLE ALWAYS|REPLICA
  */
@@ -391,6 +443,14 @@ AlterEventTrigger(AlterEventTrigStmt *stmt)
 
 	CatalogTupleUpdate(tgrel, &tup->t_self, tup);
 
+	/*
+	 * Login event triggers have an additional flag in pg_database to avoid
+	 * faster lookups in hot codepaths. Set the flag unless already True.
+	 */
+	if (namestrcmp(&evtForm->evtevent, "login") == 0 &&
+		tgenabled != TRIGGER_DISABLED)
+		SetDatatabaseHasLoginEventTriggers();
+
 	InvokeObjectPostAlterHook(EventTriggerRelationId,
 							  trigoid, 0);
 
@@ -549,6 +609,15 @@ filter_event_trigger(CommandTag tag, EventTriggerCacheItem *item)
 	return true;
 }
 
+static CommandTag
+EventTriggerGetTag(Node *parsetree, EventTriggerEvent event)
+{
+	if (event == EVT_Login)
+		return CMDTAG_LOGIN;
+	else
+		return CreateCommandTag(parsetree);
+}
+
 /*
  * Setup for running triggers for the given event.  Return value is an OID list
  * of functions to run; if there are any, trigdata is filled with an
@@ -557,7 +626,7 @@ filter_event_trigger(CommandTag tag, EventTriggerCacheItem *item)
 static List *
 EventTriggerCommonSetup(Node *parsetree,
 						EventTriggerEvent event, const char *eventstr,
-						EventTriggerData *trigdata)
+						EventTriggerData *trigdata, bool unfiltered)
 {
 	CommandTag	tag;
 	List	   *cachelist;
@@ -582,10 +651,12 @@ EventTriggerCommonSetup(Node *parsetree,
 	{
 		CommandTag	dbgtag;
 
-		dbgtag = CreateCommandTag(parsetree);
+		dbgtag = EventTriggerGetTag(parsetree, event);
+
 		if (event == EVT_DDLCommandStart ||
 			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
+			event == EVT_SQLDrop ||
+			event == EVT_Login)
 		{
 			if (!command_tag_event_trigger_ok(dbgtag))
 				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
@@ -604,7 +675,7 @@ EventTriggerCommonSetup(Node *parsetree,
 		return NIL;
 
 	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
+	tag = EventTriggerGetTag(parsetree, event);
 
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
@@ -617,7 +688,7 @@ EventTriggerCommonSetup(Node *parsetree,
 	{
 		EventTriggerCacheItem *item = lfirst(lc);
 
-		if (filter_event_trigger(tag, item))
+		if (unfiltered || filter_event_trigger(tag, item))
 		{
 			/* We must plan to fire this trigger. */
 			runlist = lappend_oid(runlist, item->fnoid);
@@ -670,7 +741,7 @@ EventTriggerDDLCommandStart(Node *parsetree)
 	runlist = EventTriggerCommonSetup(parsetree,
 									  EVT_DDLCommandStart,
 									  "ddl_command_start",
-									  &trigdata);
+									  &trigdata, false);
 	if (runlist == NIL)
 		return;
 
@@ -718,7 +789,7 @@ EventTriggerDDLCommandEnd(Node *parsetree)
 
 	runlist = EventTriggerCommonSetup(parsetree,
 									  EVT_DDLCommandEnd, "ddl_command_end",
-									  &trigdata);
+									  &trigdata, false);
 	if (runlist == NIL)
 		return;
 
@@ -764,7 +835,7 @@ EventTriggerSQLDrop(Node *parsetree)
 
 	runlist = EventTriggerCommonSetup(parsetree,
 									  EVT_SQLDrop, "sql_drop",
-									  &trigdata);
+									  &trigdata, false);
 
 	/*
 	 * Nothing to do if run list is empty.  Note this typically can't happen,
@@ -805,6 +876,96 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Fire login event triggers if any are present.  The dathasloginevt
+ * pg_database flag is left when an event trigger is dropped, to avoid
+ * complicating the codepath in the case of multiple event triggers.  This
+ * function will instead unset the flag if no trigger is defined.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode or via a GUC.  We also need a
+	 * database connection (some background workers doesn't have it).
+	 */
+	if (!IsUnderPostmaster || !event_triggers ||
+		!OidIsValid(MyDatabaseId) || !MyDatabaseHasLoginEventTriggers)
+		return;
+
+	StartTransactionCommand();
+	runlist = EventTriggerCommonSetup(NULL,
+										EVT_Login, "login",
+										&trigdata, false);
+
+	if (runlist != NIL)
+	{
+		/*
+		 * Event trigger execution may require an active snapshot.
+		 */
+		PushActiveSnapshot(GetTransactionSnapshot());
+
+		/* Run the triggers. */
+		EventTriggerInvoke(runlist, &trigdata);
+
+		/* Cleanup. */
+		list_free(runlist);
+
+		PopActiveSnapshot();
+	}
+	/*
+	 * There is no active login event trigger, but our pg_database.dathasloginevt was set.
+	 * Try to unset this flag.  We use the lock to prevent concurrent
+	 * SetDatatabaseHasLoginEventTriggers(), but we don't want to hang the
+	 * connection waiting on the lock.  Thus, we are just trying to acquire
+	 * the lock conditionally.
+	 */
+	else if (ConditionalLockSharedObject(DatabaseRelationId, MyDatabaseId,
+										 0, AccessExclusiveLock))
+	{
+		/*
+		 * The lock is held.  Now we need to recheck that login event triggers
+		 * list is still empty.  Once the list is empty, we know that even if
+		 * there is a backend, which concurrently inserts/enables login trigger,
+		 * it will update pg_database.dathasloginevt *afterwards*.
+		 */
+		runlist = EventTriggerCommonSetup(NULL,
+										  EVT_Login, "login",
+										  &trigdata, true);
+
+		if (runlist == NIL)
+		{
+			Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+			HeapTuple	tuple;
+			Form_pg_database db;
+
+			tuple = SearchSysCacheCopy1(DATABASEOID,
+										ObjectIdGetDatum(MyDatabaseId));
+
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+			db = (Form_pg_database) GETSTRUCT(tuple);
+			if (db->dathasloginevt)
+			{
+				db->dathasloginevt = false;
+				CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+			}
+			table_close(pg_db, RowExclusiveLock);
+			heap_freetuple(tuple);
+		}
+		else
+		{
+			list_free(runlist);
+		}
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
@@ -835,7 +996,7 @@ EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason)
 	runlist = EventTriggerCommonSetup(parsetree,
 									  EVT_TableRewrite,
 									  "table_rewrite",
-									  &trigdata);
+									  &trigdata, false);
 	if (runlist == NIL)
 		return;
 
diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c
index ee9b89a6726..b447ddf11ba 100644
--- a/src/backend/storage/lmgr/lmgr.c
+++ b/src/backend/storage/lmgr/lmgr.c
@@ -1060,6 +1060,44 @@ LockSharedObject(Oid classid, Oid objid, uint16 objsubid,
 	AcceptInvalidationMessages();
 }
 
+/*
+ *		ConditionalLockSharedObject
+ *
+ * As above, but only lock if we can get the lock without blocking.
+ * Returns true iff the lock was acquired.
+ */
+bool
+ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid,
+							LOCKMODE lockmode)
+{
+	LOCKTAG		tag;
+	LOCALLOCK  *locallock;
+	LockAcquireResult res;
+
+	SET_LOCKTAG_OBJECT(tag,
+					   InvalidOid,
+					   classid,
+					   objid,
+					   objsubid);
+
+	res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock);
+
+	if (res == LOCKACQUIRE_NOT_AVAIL)
+		return false;
+
+	/*
+	 * Now that we have the lock, check for invalidation messages; see notes
+	 * in LockRelationOid.
+	 */
+	if (res != LOCKACQUIRE_ALREADY_CLEAR)
+	{
+		AcceptInvalidationMessages();
+		MarkLockClear(locallock);
+	}
+
+	return true;
+}
+
 /*
  *		UnlockSharedObject
  */
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index f3c9f1f9bab..c900427ecf9 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -36,6 +36,7 @@
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/prepare.h"
 #include "common/pg_prng.h"
 #include "jit/jit.h"
@@ -4289,6 +4290,9 @@ PostgresMain(const char *dbname, const char *username)
 	initStringInfo(&row_description_buf);
 	MemoryContextSwitchTo(TopMemoryContext);
 
+	/* Fire any defined login event triggers, if appropriate */
+	EventTriggerOnLogin();
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index b080f7a35f3..ab5111c90fd 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "login") == 0)
+			event = EVT_Login;
 		else
 			continue;
 
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 011ec18015a..60bc1217fb4 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -90,6 +90,8 @@ Oid			MyDatabaseId = InvalidOid;
 
 Oid			MyDatabaseTableSpace = InvalidOid;
 
+bool		MyDatabaseHasLoginEventTriggers = false;
+
 /*
  * DatabasePath is the path (relative to DataDir) of my database's
  * primary directory, ie, its directory in the default tablespace.
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index e60ecd1e366..552cf9d950a 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -1103,6 +1103,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 		}
 
 		MyDatabaseTableSpace = datform->dattablespace;
+		MyDatabaseHasLoginEventTriggers = datform->dathasloginevt;
 		/* pass the database name back to the caller */
 		if (out_dbname)
 			strcpy(out_dbname, dbname);
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index f7b61766921..83aeef2751b 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3263,6 +3263,11 @@ dumpDatabase(Archive *fout)
 		appendPQExpBufferStr(delQry, ";\n");
 	}
 
+	/*
+	 * We do not restore pg_database.dathasloginevt because it is set
+	 * automatically on login event trigger creation.
+	 */
+
 	/* Add database-specific SET options */
 	dumpDatabaseConfig(fout, creaQry, datname, dbCatId.oid);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index eb4dfe80b50..93742fc6ac9 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3552,8 +3552,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop",
-					  "table_rewrite");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "login",
+					  "sql_drop", "table_rewrite");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat
index 0754ef1bce4..8d91e3bf8da 100644
--- a/src/include/catalog/pg_database.dat
+++ b/src/include/catalog/pg_database.dat
@@ -16,7 +16,7 @@
   descr => 'default template for new databases',
   datname => 'template1', encoding => 'ENCODING',
   datlocprovider => 'LOCALE_PROVIDER', datistemplate => 't',
-  datallowconn => 't', datconnlimit => '-1', datfrozenxid => '0',
+  datallowconn => 't', dathasloginevt => 'f', datconnlimit => '-1', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datcollate => 'LC_COLLATE',
   datctype => 'LC_CTYPE', daticulocale => 'ICU_LOCALE',
   daticurules => 'ICU_RULES', datacl => '_null_' },
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index e9eb06b2e53..3e50a570046 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -49,6 +49,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
 	/* new connections allowed? */
 	bool		datallowconn;
 
+	/* database has login event triggers? */
+	bool		dathasloginevt;
+
 	/*
 	 * Max connections allowed. Negative values have special meaning, see
 	 * DATCONNLIMIT_* defines below.
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 1c925dbf257..9e3ece50d5f 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -56,6 +56,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnLogin(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index c2f9de63a13..7232b03e379 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -203,6 +203,8 @@ extern PGDLLIMPORT Oid MyDatabaseId;
 
 extern PGDLLIMPORT Oid MyDatabaseTableSpace;
 
+extern PGDLLIMPORT bool MyDatabaseHasLoginEventTriggers;
+
 /*
  * Date/Time Configuration
  *
diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h
index 4ee91e3cf93..952ebe75cb4 100644
--- a/src/include/storage/lmgr.h
+++ b/src/include/storage/lmgr.h
@@ -99,6 +99,8 @@ extern void UnlockDatabaseObject(Oid classid, Oid objid, uint16 objsubid,
 /* Lock a shared-across-databases object (other than a relation) */
 extern void LockSharedObject(Oid classid, Oid objid, uint16 objsubid,
 							 LOCKMODE lockmode);
+extern bool ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid,
+										LOCKMODE lockmode);
 extern void UnlockSharedObject(Oid classid, Oid objid, uint16 objsubid,
 							   LOCKMODE lockmode);
 
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index e738ac1c097..553a31874f1 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
+PG_CMDTAG(CMDTAG_LOGIN, "LOGIN", true, false, false)
 PG_CMDTAG(CMDTAG_MERGE, "MERGE", false, false, true)
 PG_CMDTAG(CMDTAG_MOVE, "MOVE", false, false, true)
 PG_CMDTAG(CMDTAG_NOTIFY, "NOTIFY", false, false, false)
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index d340026518a..52052e6252a 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Login,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/authentication/t/005_login_trigger.pl b/src/test/authentication/t/005_login_trigger.pl
new file mode 100644
index 00000000000..fec664fb865
--- /dev/null
+++ b/src/test/authentication/t/005_login_trigger.pl
@@ -0,0 +1,157 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+# Tests of authentication via login trigger. Mostly for rejection via
+# exception, because this scenario cannot be covered with *.sql/*.out regress
+# tests.
+
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Execute a psql command and compare its output towards given regexps
+sub psql_command
+{
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+	my ($node, $sql, $expected_ret, $test_name, %params) = @_;
+
+	my $connstr;
+	if (defined($params{connstr}))
+	{
+		$connstr = $params{connstr};
+	}
+	else
+	{
+		$connstr = '';
+	}
+
+	# Execute command
+	my ($ret, $stdout, $stderr) = $node->psql('postgres', $sql,
+											  connstr => "$connstr");
+
+	# Check return code
+	is($ret, $expected_ret,  "$test_name: exit code $expected_ret");
+
+	# Check stdout
+	if (defined($params{log_like}))
+	{
+		my @log_like = @{ $params{log_like} };
+		while (my $regex = shift @log_like)
+		{
+			like($stdout, $regex, "$test_name: log matches");
+		}
+	}
+	if (defined($params{log_unlike}))
+	{
+		my @log_unlike = @{ $params{log_unlike} };
+		while (my $regex = shift @log_unlike)
+		{
+			unlike($stdout, $regex, "$test_name: log unmatches");
+		}
+	}
+	if (defined($params{log_exact}))
+	{
+		is($stdout, $params{log_exact}, "$test_name: log equals");
+	}
+
+	# Check stderr
+	if (defined($params{err_like}))
+	{
+		my @err_like = @{ $params{err_like} };
+		while (my $regex = shift @err_like)
+		{
+			like($stderr, $regex, "$test_name: err matches");
+		}
+	}
+	if (defined($params{err_unlike}))
+	{
+		my @err_unlike = @{ $params{err_unlike} };
+		while (my $regex = shift @err_unlike)
+		{
+			unlike($stderr, $regex, "$test_name: err unmatches");
+		}
+	}
+	if (defined($params{err_exact}))
+	{
+		is($stderr, $params{err_exact}, "$test_name: err equals");
+	}
+
+	return;
+}
+
+# New node
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init(extra => [ '--locale=C', '--encoding=UTF8' ]);
+$node->append_conf(
+	'postgresql.conf', q{
+wal_level = 'logical'
+max_replication_slots = 4
+max_wal_senders = 4
+});
+$node->start;
+
+# Create temporary roles and log table
+psql_command($node, 'CREATE ROLE alice WITH LOGIN;
+CREATE ROLE mallory WITH LOGIN;
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+', 0, 'create tmp objects', log_exact => '', err_exact => ''), ;
+
+# Create login event function and trigger
+psql_command($node,
+			 'CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  IF SESSION_USER = \'mallory\' THEN
+    RAISE EXCEPTION \'Hello %! You are NOT welcome here!\', SESSION_USER;
+  END IF;
+  RAISE NOTICE \'Hello %! You are welcome!\', SESSION_USER;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+', 0, 'create trigger function', log_exact => '', err_exact => '');
+
+psql_command($node,
+			 'CREATE EVENT TRIGGER on_login_trigger '
+			 .'ON login EXECUTE PROCEDURE on_login_proc();', 0,
+			 'create event trigger', log_exact => '', err_exact => '');
+psql_command($node, 'ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;', 0,
+			 'alter event trigger', log_exact => '',
+			 err_like => [qr/You are welcome/]);
+
+# Check the two requests were logged via login trigger
+psql_command($node, 'SELECT COUNT(*) FROM user_logins;', 0, 'select count',
+			 log_exact => '2', err_like => [qr/You are welcome/]);
+
+# Try to log as allowed Alice and disallowed Mallory (two times)
+psql_command($node, 'SELECT 1;', 0, 'try alice', connstr => 'user=alice',
+			 log_exact => '1', err_like => [qr/You are welcome/],
+			 err_unlike => [qr/You are NOT welcome/]);
+psql_command($node, 'SELECT 1;', 2, 'try mallory', connstr => 'user=mallory',
+			 log_exact => '', err_like => [qr/You are NOT welcome/],
+			 err_unlike => [qr/You are welcome/]);
+psql_command($node, 'SELECT 1;', 2, 'try mallory', connstr => 'user=mallory',
+			 log_exact => '', err_like => [qr/You are NOT welcome/],
+			 err_unlike => [qr/You are welcome/]);
+
+# Check that Alice's login record is here, while the Mallory's one is not
+psql_command($node, 'SELECT * FROM user_logins;', 0, 'select *',
+			 log_like => [qr/3\|alice/], log_unlike => [qr/mallory/],
+			 err_like => [qr/You are welcome/]);
+
+# Check total number of successful logins so far
+psql_command($node, 'SELECT COUNT(*) FROM user_logins;', 0, 'select count',
+			 log_exact => '5', err_like => [qr/You are welcome/]);
+
+# Cleanup the temporary stuff
+psql_command($node, 'DROP EVENT TRIGGER on_login_trigger;', 0,
+			 'drop event trigger', log_exact => '',
+			 err_like => [qr/You are welcome/]);
+psql_command($node, 'DROP TABLE user_logins;
+DROP FUNCTION on_login_proc;
+DROP ROLE mallory;
+DROP ROLE alice;
+', 0, 'cleanup', log_exact => '', err_exact => '');
+
+done_testing();
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 0c72ba09441..8644854eb39 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,24 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql('postgres', q{
+CREATE TABLE user_logins(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO user_logins (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 $node_primary->wait_for_replay_catchup($node_standby_1);
 $node_standby_1->wait_for_replay_catchup($node_standby_2, $node_primary);
@@ -384,6 +402,11 @@ sub replay_check
 
 replay_check();
 
+my $evttrig = $node_standby_1->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+$evttrig = $node_standby_2->safe_psql('postgres', "SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 0b87a42d0a9..eaaff6ba6f1 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -638,3 +638,48 @@ NOTICE:  DROP POLICY dropped policy
 CREATE POLICY pguc ON event_trigger_test USING (FALSE);
 SET event_triggers = 'off';
 DROP POLICY pguc ON event_trigger_test;
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     1
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     2
+(1 row)
+
+-- Check dathasloginevt in system catalog
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+ dathasloginevt 
+----------------
+ t
+(1 row)
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
+\c
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+ dathasloginevt 
+----------------
+ f
+(1 row)
+
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 6f0933b9e88..9c2b7903fba 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -495,3 +495,29 @@ DROP POLICY pguc ON event_trigger_test;
 CREATE POLICY pguc ON event_trigger_test USING (FALSE);
 SET event_triggers = 'off';
 DROP POLICY pguc ON event_trigger_test;
+
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+SELECT COUNT(*) FROM user_logins;
+\c
+SELECT COUNT(*) FROM user_logins;
+
+-- Check dathasloginevt in system catalog
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
+\c
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
-- 
2.39.3 (Apple Git-145)

#168Alexander Korotkov
aekorotkov@gmail.com
In reply to: Alexander Korotkov (#167)
1 attachment(s)
Re: On login trigger: take three

On Sat, Oct 14, 2023 at 2:10 AM Alexander Korotkov <aekorotkov@gmail.com> wrote:

On Fri, Oct 13, 2023 at 11:26 AM Alexander Korotkov
<aekorotkov@gmail.com> wrote:

On Fri, Oct 13, 2023 at 4:18 AM Robert Haas <robertmhaas@gmail.com> wrote:

On Thu, Oct 12, 2023 at 6:54 PM Alexander Korotkov <aekorotkov@gmail.com> wrote:

On Thu, Oct 12, 2023 at 8:35 PM Robert Haas <robertmhaas@gmail.com> wrote:

Doesn't that mean that if you create the first login trigger in a
database and leave the transaction open, nobody can connect to that
database until the transaction ends?

It doesn't mean that, because when trying to reset the flag v44 does
conditional lock. So, if another transaction is holding the log we
will just skip resetting the flag. So, the flag will be cleared on
the first connection after that transaction ends.

But in the scenario I am describing the flag is being set, not reset.

Sorry, it seems I just missed some logical steps. Imagine, that
transaction A created the first login trigger and hangs open. Then
the new connection B sees no visible triggers yet, but dathasloginevt
flag is set. Therefore, connection B tries conditional lock but just
gives up because the lock is held by transaction A.

Also, note that the lock has been just some lock with a custom tag.
It doesn't effectively block the database. You may think about it as
of custom advisory lock.

I've revised the comments about the lock a bit. I've also run some
tests regarding the connection time (5 runs).

v45, event_triggers=on: avg=3.081ms, min=2.992ms, max=3.146ms
v45, event_triggers=off: avg=3.132ms, min=3.048ms, max=3.186ms
master: 3.080ms, min=3.023ms, max=3.167ms

So, no measurable overhead (not surprising since no extra catalog lookup).
I think this patch is in the commitable shape. Any objections?

The attached revision fixes test failures spotted by
commitfest.cputube.org. Also, perl scripts passed perltidy.

------
Regards,
Alexander Korotkov

Attachments:

0001-Add-support-event-triggers-on-authenticated-logi-v46.patchapplication/octet-stream; name=0001-Add-support-event-triggers-on-authenticated-logi-v46.patchDownload
From 3950478facd1b42129a5ebe352aebe3ce090f860 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <akorotkov@postgresql.org>
Date: Mon, 9 Oct 2023 15:00:26 +0300
Subject: [PATCH] Add support event triggers on authenticated login

This commit introduces trigger on login event, allowing to fire some actions
right on the user connection.  This can be useful for logging or connection
check purposes as well as for some personalization of environment.  Usage
details are described in the documentation included, but shortly usage is
the same as for other triggers: create function returning event_trigger and
then create event trigger on login event.

In order to prevent the connection time overhead when there are no triggers
the commit introduces pg_database.dathasloginevt flag, which indicates database
has active login triggers.  This flag is set by CREATE/ALTER EVENT TRIGGER
command, and unset at connection time when no active triggers found.

Author: Konstantin Knizhnik, Mikhail Gribkov
Discussion: https://postgr.es/m/0d46d29f-4558-3af9-9c85-7774e14a7709%40postgrespro.ru
Reviewed-by: Pavel Stehule, Takayuki Tsunakawa, Greg Nancarrow, Ivan Panchenko
Reviewed-by: Daniel Gustafsson, Teodor Sigaev, Robert Haas, Andres Freund
Reviewed-by: Tom Lane, Andrey Sokolov, Zhihong Yu, Sergey Shinderuk
Reviewed-by: Gregory Stark, Nikita Malakhov, Ted Yu
---
 doc/src/sgml/bki.sgml                         |   2 +-
 doc/src/sgml/catalogs.sgml                    |  13 ++
 doc/src/sgml/ecpg.sgml                        |   2 +
 doc/src/sgml/event-trigger.sgml               |  94 +++++++++
 src/backend/commands/dbcommands.c             |  17 +-
 src/backend/commands/event_trigger.c          | 179 ++++++++++++++++-
 src/backend/storage/lmgr/lmgr.c               |  38 ++++
 src/backend/tcop/postgres.c                   |   4 +
 src/backend/utils/cache/evtcache.c            |   2 +
 src/backend/utils/init/globals.c              |   2 +
 src/backend/utils/init/postinit.c             |   1 +
 src/bin/pg_dump/pg_dump.c                     |   5 +
 src/bin/psql/tab-complete.c                   |   4 +-
 src/include/catalog/pg_database.dat           |   2 +-
 src/include/catalog/pg_database.h             |   3 +
 src/include/commands/event_trigger.h          |   1 +
 src/include/miscadmin.h                       |   2 +
 src/include/storage/lmgr.h                    |   2 +
 src/include/tcop/cmdtaglist.h                 |   1 +
 src/include/utils/evtcache.h                  |   3 +-
 .../authentication/t/005_login_trigger.pl     | 189 ++++++++++++++++++
 src/test/recovery/t/001_stream_rep.pl         |  26 +++
 src/test/regress/expected/event_trigger.out   |  45 +++++
 src/test/regress/sql/event_trigger.sql        |  26 +++
 24 files changed, 643 insertions(+), 20 deletions(-)
 create mode 100644 src/test/authentication/t/005_login_trigger.pl

diff --git a/doc/src/sgml/bki.sgml b/doc/src/sgml/bki.sgml
index f71644e3989..315ba819514 100644
--- a/doc/src/sgml/bki.sgml
+++ b/doc/src/sgml/bki.sgml
@@ -184,7 +184,7 @@
   descr => 'database\'s default template',
   datname => 'template1', encoding => 'ENCODING',
   datlocprovider => 'LOCALE_PROVIDER', datistemplate => 't',
-  datallowconn => 't', datconnlimit => '-1', datfrozenxid => '0',
+  datallowconn => 't', dathasloginevt => 'f', datconnlimit => '-1', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datcollate => 'LC_COLLATE',
   datctype => 'LC_CTYPE', daticulocale => 'ICU_LOCALE', datacl => '_null_' },
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index e09adb45e41..d3458840fbe 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -3035,6 +3035,19 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>dathasloginevt</structfield> <type>bool</type>
+      </para>
+      <para>
+        Indicates that there are login event triggers defined for this database.
+        This flag is used to avoid extra lookups on the
+        <structname>pg_event_trigger</structname> table during each backend
+        startup.  This flag is used internally by <productname>PostgreSQL</productname>
+        and should not be manually altered or read for monitoring purposes.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>datconnlimit</structfield> <type>int4</type>
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index f52165165dc..54de81158b5 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -4769,6 +4769,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = t (type: 1)
 datallowconn = t (type: 1)
+dathasloginevt = f (type: 1)
 datconnlimit = -1 (type: 5)
 datfrozenxid = 379 (type: 1)
 dattablespace = 1663 (type: 1)
@@ -4793,6 +4794,7 @@ datdba = 10 (type: 1)
 encoding = 0 (type: 5)
 datistemplate = f (type: 1)
 datallowconn = t (type: 1)
+dathasloginevt = f (type: 1)
 datconnlimit = -1 (type: 5)
 datfrozenxid = 379 (type: 1)
 dattablespace = 1663 (type: 1)
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index 3b6a5361b34..10b20f0339a 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -28,6 +28,7 @@
      An event trigger fires whenever the event with which it is associated
      occurs in the database in which it is defined. Currently, the only
      supported events are
+     <literal>login</literal>,
      <literal>ddl_command_start</literal>,
      <literal>ddl_command_end</literal>,
      <literal>table_rewrite</literal>
@@ -35,6 +36,24 @@
      Support for additional events may be added in future releases.
    </para>
 
+   <para>
+     The <literal>login</literal> event occurs when an authenticated user logs
+     into the system. Any bug in a trigger procedure for this event may
+     prevent successful login to the system. Such bugs may be fixed by
+     setting <xref linkend="guc-event-triggers"/> is set to <literal>false</literal>
+     either in a connection string or configuration file. Alternative is
+     restarting the system in single-user mode (as event triggers are
+     disabled in this mode). See the <xref linkend="app-postgres"/> reference
+     page for details about using single-user mode.
+     The <literal>login</literal> event will also fire on standby servers.
+     To prevent servers from becoming inaccessible, such triggers must avoid
+     writing anything to the database when running on a standby.
+     Also, it's recommended to avoid long-running queries in
+     <literal>login</literal> event triggers.  Notes that, for instance,
+     cancelling connection in <application>psql</application> wouldn't cancel
+     the in-progress <literal>login</literal> trigger.
+   </para>
+
    <para>
      The <literal>ddl_command_start</literal> event occurs just before the
      execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
@@ -1300,4 +1319,79 @@ CREATE EVENT TRIGGER no_rewrite_allowed
 </programlisting>
    </para>
  </sect1>
+
+  <sect1 id="event-trigger-database-login-example">
+    <title>A Database Login Event Trigger Example</title>
+
+    <para>
+      The event trigger on the <literal>login</literal> event can be
+      useful for logging user logins, for verifying the connection and
+      assigning roles according to current circumstances, or for session
+      data initialization. It is very important that any event trigger using
+      the <literal>login</literal> event checks whether or not the database is
+      in recovery before performing any writes. Writing to a standby server
+      will make it inaccessible.
+    </para>
+
+    <para>
+      The following example demonstrates these options.
+<programlisting>
+-- create test tables and roles
+CREATE TABLE user_login_log (
+  "user" text,
+  "session_start" timestamp with time zone
+);
+CREATE ROLE day_worker;
+CREATE ROLE night_worker;
+
+-- the example trigger function
+CREATE OR REPLACE FUNCTION init_session()
+  RETURNS event_trigger SECURITY DEFINER
+  LANGUAGE plpgsql AS
+$$
+DECLARE
+  hour integer = EXTRACT('hour' FROM current_time at time zone 'utc');
+  rec boolean;
+BEGIN
+-- 1. Forbid logging in between 2AM and 4AM.
+IF hour BETWEEN 2 AND 4 THEN
+  RAISE EXCEPTION 'Login forbidden';
+END IF;
+
+-- The checks below cannot be performed on standby servers so
+-- ensure the database is not in recovery before we perform any
+-- operations.
+SELECT pg_is_in_recovery() INTO rec;
+IF rec THEN
+  RETURN;
+END IF;
+
+-- 2. Assign some roles. At daytime, grant the day_worker role, else the
+-- night_worker role.
+IF hour BETWEEN 8 AND 20 THEN
+  EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT day_worker TO ' || quote_ident(session_user);
+ELSE
+  EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
+  EXECUTE 'GRANT night_worker TO ' || quote_ident(session_user);
+END IF;
+
+-- 3. Initialize user session data
+CREATE TEMP TABLE session_storage (x float, y integer);
+ALTER TABLE session_storage OWNER TO session_user;
+
+-- 4. Log the connection time
+INSERT INTO public.user_login_log VALUES (session_user, current_timestamp);
+
+END;
+$$;
+
+-- trigger definition
+CREATE EVENT TRIGGER init_session
+  ON login
+  EXECUTE FUNCTION init_session();
+ALTER EVENT TRIGGER init_session ENABLE ALWAYS;
+</programlisting>
+    </para>
+  </sect1>
 </chapter>
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 307729ab7ef..c52ecc61a6b 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -116,7 +116,7 @@ static void movedb(const char *dbname, const char *tblspcname);
 static void movedb_failure_callback(int code, Datum arg);
 static bool get_db_info(const char *name, LOCKMODE lockmode,
 						Oid *dbIdP, Oid *ownerIdP,
-						int *encodingP, bool *dbIsTemplateP, bool *dbAllowConnP,
+						int *encodingP, bool *dbIsTemplateP, bool *dbAllowConnP, bool *dbHasLoginEvtP,
 						TransactionId *dbFrozenXidP, MultiXactId *dbMinMultiP,
 						Oid *dbTablespace, char **dbCollate, char **dbCtype, char **dbIculocale,
 						char **dbIcurules,
@@ -680,6 +680,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	char		src_locprovider = '\0';
 	char	   *src_collversion = NULL;
 	bool		src_istemplate;
+	bool		src_hasloginevt;
 	bool		src_allowconn;
 	TransactionId src_frozenxid = InvalidTransactionId;
 	MultiXactId src_minmxid = InvalidMultiXactId;
@@ -968,7 +969,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 
 	if (!get_db_info(dbtemplate, ShareLock,
 					 &src_dboid, &src_owner, &src_encoding,
-					 &src_istemplate, &src_allowconn,
+					 &src_istemplate, &src_allowconn, &src_hasloginevt,
 					 &src_frozenxid, &src_minmxid, &src_deftablespace,
 					 &src_collate, &src_ctype, &src_iculocale, &src_icurules, &src_locprovider,
 					 &src_collversion))
@@ -1375,6 +1376,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
 	new_record[Anum_pg_database_datlocprovider - 1] = CharGetDatum(dblocprovider);
 	new_record[Anum_pg_database_datistemplate - 1] = BoolGetDatum(dbistemplate);
 	new_record[Anum_pg_database_datallowconn - 1] = BoolGetDatum(dballowconnections);
+	new_record[Anum_pg_database_dathasloginevt - 1] = BoolGetDatum(src_hasloginevt);
 	new_record[Anum_pg_database_datconnlimit - 1] = Int32GetDatum(dbconnlimit);
 	new_record[Anum_pg_database_datfrozenxid - 1] = TransactionIdGetDatum(src_frozenxid);
 	new_record[Anum_pg_database_datminmxid - 1] = TransactionIdGetDatum(src_minmxid);
@@ -1603,7 +1605,7 @@ dropdb(const char *dbname, bool missing_ok, bool force)
 	pgdbrel = table_open(DatabaseRelationId, RowExclusiveLock);
 
 	if (!get_db_info(dbname, AccessExclusiveLock, &db_id, NULL, NULL,
-					 &db_istemplate, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL))
+					 &db_istemplate, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL))
 	{
 		if (!missing_ok)
 		{
@@ -1817,7 +1819,7 @@ RenameDatabase(const char *oldname, const char *newname)
 	 */
 	rel = table_open(DatabaseRelationId, RowExclusiveLock);
 
-	if (!get_db_info(oldname, AccessExclusiveLock, &db_id, NULL, NULL,
+	if (!get_db_info(oldname, AccessExclusiveLock, &db_id, NULL, NULL, NULL,
 					 NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL))
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_DATABASE),
@@ -1927,7 +1929,7 @@ movedb(const char *dbname, const char *tblspcname)
 	 */
 	pgdbrel = table_open(DatabaseRelationId, RowExclusiveLock);
 
-	if (!get_db_info(dbname, AccessExclusiveLock, &db_id, NULL, NULL,
+	if (!get_db_info(dbname, AccessExclusiveLock, &db_id, NULL, NULL, NULL,
 					 NULL, NULL, NULL, NULL, &src_tblspcoid, NULL, NULL, NULL, NULL, NULL, NULL))
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_DATABASE),
@@ -2693,7 +2695,7 @@ pg_database_collation_actual_version(PG_FUNCTION_ARGS)
 static bool
 get_db_info(const char *name, LOCKMODE lockmode,
 			Oid *dbIdP, Oid *ownerIdP,
-			int *encodingP, bool *dbIsTemplateP, bool *dbAllowConnP,
+			int *encodingP, bool *dbIsTemplateP, bool *dbAllowConnP, bool *dbHasLoginEvtP,
 			TransactionId *dbFrozenXidP, MultiXactId *dbMinMultiP,
 			Oid *dbTablespace, char **dbCollate, char **dbCtype, char **dbIculocale,
 			char **dbIcurules,
@@ -2778,6 +2780,9 @@ get_db_info(const char *name, LOCKMODE lockmode,
 				/* allowed as template? */
 				if (dbIsTemplateP)
 					*dbIsTemplateP = dbform->datistemplate;
+				/* Has on login event trigger? */
+				if (dbHasLoginEvtP)
+					*dbHasLoginEvtP = dbform->dathasloginevt;
 				/* allowing connections? */
 				if (dbAllowConnP)
 					*dbAllowConnP = dbform->datallowconn;
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index bd812e42d94..0b08552fd7a 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -20,6 +20,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -37,15 +38,18 @@
 #include "miscadmin.h"
 #include "parser/parse_func.h"
 #include "pgstat.h"
+#include "storage/lmgr.h"
 #include "tcop/deparse_utility.h"
 #include "tcop/utility.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/evtcache.h"
 #include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 typedef struct EventTriggerQueryState
@@ -103,6 +107,7 @@ static void validate_table_rewrite_tags(const char *filtervar, List *taglist);
 static void EventTriggerInvoke(List *fn_oid_list, EventTriggerData *trigdata);
 static const char *stringify_grant_objtype(ObjectType objtype);
 static const char *stringify_adefprivs_objtype(ObjectType objtype);
+static void SetDatatabaseHasLoginEventTriggers(void);
 
 /*
  * Create an event trigger.
@@ -133,6 +138,7 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
 		strcmp(stmt->eventname, "ddl_command_end") != 0 &&
 		strcmp(stmt->eventname, "sql_drop") != 0 &&
+		strcmp(stmt->eventname, "login") != 0 &&
 		strcmp(stmt->eventname, "table_rewrite") != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
@@ -165,6 +171,10 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
 	else if (strcmp(stmt->eventname, "table_rewrite") == 0
 			 && tags != NULL)
 		validate_table_rewrite_tags("tag", tags);
+	else if (strcmp(stmt->eventname, "login") == 0 && tags != NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("Tag filtering is not supported for login event trigger")));
 
 	/*
 	 * Give user a nice error message if an event trigger of the same name
@@ -296,6 +306,13 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
 	CatalogTupleInsert(tgrel, tuple);
 	heap_freetuple(tuple);
 
+	/*
+	 * Login event triggers have an additional flag in pg_database to avoid
+	 * faster lookups in hot codepaths. Set the flag unless already True.
+	 */
+	if (strcmp(eventname, "login") == 0)
+		SetDatatabaseHasLoginEventTriggers();
+
 	/* Depend on owner. */
 	recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
 
@@ -357,6 +374,41 @@ filter_list_to_array(List *filterlist)
 	return PointerGetDatum(construct_array_builtin(data, l, TEXTOID));
 }
 
+/*
+ * Set pg_database.dathasloginevt flag for current database indicating that
+ * current database has on login triggers.
+ */
+void
+SetDatatabaseHasLoginEventTriggers(void)
+{
+	/* Set dathasloginevt flag in pg_database */
+	Form_pg_database db;
+	Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+	HeapTuple	tuple;
+
+	/*
+	 * Use shared lock to prevent a conflit with EventTriggerOnLogin() trying
+	 * to reset pg_database.dathasloginevt flag.  Note, this lock doesn't
+	 * effectively blocks database or other objection.  It's just custom lock
+	 * tag used to prevent multiple backends changing pg_database.dathasloginevt
+	 * flag.
+	 */
+	LockSharedObject(DatabaseRelationId, MyDatabaseId, 0, AccessExclusiveLock);
+
+	tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+	db = (Form_pg_database) GETSTRUCT(tuple);
+	if (!db->dathasloginevt)
+	{
+		db->dathasloginevt = true;
+		CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+		CommandCounterIncrement();
+	}
+	table_close(pg_db, RowExclusiveLock);
+	heap_freetuple(tuple);
+}
+
 /*
  * ALTER EVENT TRIGGER foo ENABLE|DISABLE|ENABLE ALWAYS|REPLICA
  */
@@ -391,6 +443,14 @@ AlterEventTrigger(AlterEventTrigStmt *stmt)
 
 	CatalogTupleUpdate(tgrel, &tup->t_self, tup);
 
+	/*
+	 * Login event triggers have an additional flag in pg_database to avoid
+	 * faster lookups in hot codepaths. Set the flag unless already True.
+	 */
+	if (namestrcmp(&evtForm->evtevent, "login") == 0 &&
+		tgenabled != TRIGGER_DISABLED)
+		SetDatatabaseHasLoginEventTriggers();
+
 	InvokeObjectPostAlterHook(EventTriggerRelationId,
 							  trigoid, 0);
 
@@ -549,6 +609,15 @@ filter_event_trigger(CommandTag tag, EventTriggerCacheItem *item)
 	return true;
 }
 
+static CommandTag
+EventTriggerGetTag(Node *parsetree, EventTriggerEvent event)
+{
+	if (event == EVT_Login)
+		return CMDTAG_LOGIN;
+	else
+		return CreateCommandTag(parsetree);
+}
+
 /*
  * Setup for running triggers for the given event.  Return value is an OID list
  * of functions to run; if there are any, trigdata is filled with an
@@ -557,7 +626,7 @@ filter_event_trigger(CommandTag tag, EventTriggerCacheItem *item)
 static List *
 EventTriggerCommonSetup(Node *parsetree,
 						EventTriggerEvent event, const char *eventstr,
-						EventTriggerData *trigdata)
+						EventTriggerData *trigdata, bool unfiltered)
 {
 	CommandTag	tag;
 	List	   *cachelist;
@@ -582,10 +651,12 @@ EventTriggerCommonSetup(Node *parsetree,
 	{
 		CommandTag	dbgtag;
 
-		dbgtag = CreateCommandTag(parsetree);
+		dbgtag = EventTriggerGetTag(parsetree, event);
+
 		if (event == EVT_DDLCommandStart ||
 			event == EVT_DDLCommandEnd ||
-			event == EVT_SQLDrop)
+			event == EVT_SQLDrop ||
+			event == EVT_Login)
 		{
 			if (!command_tag_event_trigger_ok(dbgtag))
 				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
@@ -604,7 +675,7 @@ EventTriggerCommonSetup(Node *parsetree,
 		return NIL;
 
 	/* Get the command tag. */
-	tag = CreateCommandTag(parsetree);
+	tag = EventTriggerGetTag(parsetree, event);
 
 	/*
 	 * Filter list of event triggers by command tag, and copy them into our
@@ -617,7 +688,7 @@ EventTriggerCommonSetup(Node *parsetree,
 	{
 		EventTriggerCacheItem *item = lfirst(lc);
 
-		if (filter_event_trigger(tag, item))
+		if (unfiltered || filter_event_trigger(tag, item))
 		{
 			/* We must plan to fire this trigger. */
 			runlist = lappend_oid(runlist, item->fnoid);
@@ -670,7 +741,7 @@ EventTriggerDDLCommandStart(Node *parsetree)
 	runlist = EventTriggerCommonSetup(parsetree,
 									  EVT_DDLCommandStart,
 									  "ddl_command_start",
-									  &trigdata);
+									  &trigdata, false);
 	if (runlist == NIL)
 		return;
 
@@ -718,7 +789,7 @@ EventTriggerDDLCommandEnd(Node *parsetree)
 
 	runlist = EventTriggerCommonSetup(parsetree,
 									  EVT_DDLCommandEnd, "ddl_command_end",
-									  &trigdata);
+									  &trigdata, false);
 	if (runlist == NIL)
 		return;
 
@@ -764,7 +835,7 @@ EventTriggerSQLDrop(Node *parsetree)
 
 	runlist = EventTriggerCommonSetup(parsetree,
 									  EVT_SQLDrop, "sql_drop",
-									  &trigdata);
+									  &trigdata, false);
 
 	/*
 	 * Nothing to do if run list is empty.  Note this typically can't happen,
@@ -805,6 +876,96 @@ EventTriggerSQLDrop(Node *parsetree)
 	list_free(runlist);
 }
 
+/*
+ * Fire login event triggers if any are present.  The dathasloginevt
+ * pg_database flag is left when an event trigger is dropped, to avoid
+ * complicating the codepath in the case of multiple event triggers.  This
+ * function will instead unset the flag if no trigger is defined.
+ */
+void
+EventTriggerOnLogin(void)
+{
+	List	   *runlist;
+	EventTriggerData trigdata;
+
+	/*
+	 * See EventTriggerDDLCommandStart for a discussion about why event
+	 * triggers are disabled in single user mode or via a GUC.  We also need a
+	 * database connection (some background workers doesn't have it).
+	 */
+	if (!IsUnderPostmaster || !event_triggers ||
+		!OidIsValid(MyDatabaseId) || !MyDatabaseHasLoginEventTriggers)
+		return;
+
+	StartTransactionCommand();
+	runlist = EventTriggerCommonSetup(NULL,
+										EVT_Login, "login",
+										&trigdata, false);
+
+	if (runlist != NIL)
+	{
+		/*
+		 * Event trigger execution may require an active snapshot.
+		 */
+		PushActiveSnapshot(GetTransactionSnapshot());
+
+		/* Run the triggers. */
+		EventTriggerInvoke(runlist, &trigdata);
+
+		/* Cleanup. */
+		list_free(runlist);
+
+		PopActiveSnapshot();
+	}
+	/*
+	 * There is no active login event trigger, but our pg_database.dathasloginevt was set.
+	 * Try to unset this flag.  We use the lock to prevent concurrent
+	 * SetDatatabaseHasLoginEventTriggers(), but we don't want to hang the
+	 * connection waiting on the lock.  Thus, we are just trying to acquire
+	 * the lock conditionally.
+	 */
+	else if (ConditionalLockSharedObject(DatabaseRelationId, MyDatabaseId,
+										 0, AccessExclusiveLock))
+	{
+		/*
+		 * The lock is held.  Now we need to recheck that login event triggers
+		 * list is still empty.  Once the list is empty, we know that even if
+		 * there is a backend, which concurrently inserts/enables login trigger,
+		 * it will update pg_database.dathasloginevt *afterwards*.
+		 */
+		runlist = EventTriggerCommonSetup(NULL,
+										  EVT_Login, "login",
+										  &trigdata, true);
+
+		if (runlist == NIL)
+		{
+			Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
+			HeapTuple	tuple;
+			Form_pg_database db;
+
+			tuple = SearchSysCacheCopy1(DATABASEOID,
+										ObjectIdGetDatum(MyDatabaseId));
+
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+
+			db = (Form_pg_database) GETSTRUCT(tuple);
+			if (db->dathasloginevt)
+			{
+				db->dathasloginevt = false;
+				CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+			}
+			table_close(pg_db, RowExclusiveLock);
+			heap_freetuple(tuple);
+		}
+		else
+		{
+			list_free(runlist);
+		}
+	}
+	CommitTransactionCommand();
+}
+
 
 /*
  * Fire table_rewrite triggers.
@@ -835,7 +996,7 @@ EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason)
 	runlist = EventTriggerCommonSetup(parsetree,
 									  EVT_TableRewrite,
 									  "table_rewrite",
-									  &trigdata);
+									  &trigdata, false);
 	if (runlist == NIL)
 		return;
 
diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c
index ee9b89a6726..b447ddf11ba 100644
--- a/src/backend/storage/lmgr/lmgr.c
+++ b/src/backend/storage/lmgr/lmgr.c
@@ -1060,6 +1060,44 @@ LockSharedObject(Oid classid, Oid objid, uint16 objsubid,
 	AcceptInvalidationMessages();
 }
 
+/*
+ *		ConditionalLockSharedObject
+ *
+ * As above, but only lock if we can get the lock without blocking.
+ * Returns true iff the lock was acquired.
+ */
+bool
+ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid,
+							LOCKMODE lockmode)
+{
+	LOCKTAG		tag;
+	LOCALLOCK  *locallock;
+	LockAcquireResult res;
+
+	SET_LOCKTAG_OBJECT(tag,
+					   InvalidOid,
+					   classid,
+					   objid,
+					   objsubid);
+
+	res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock);
+
+	if (res == LOCKACQUIRE_NOT_AVAIL)
+		return false;
+
+	/*
+	 * Now that we have the lock, check for invalidation messages; see notes
+	 * in LockRelationOid.
+	 */
+	if (res != LOCKACQUIRE_ALREADY_CLEAR)
+	{
+		AcceptInvalidationMessages();
+		MarkLockClear(locallock);
+	}
+
+	return true;
+}
+
 /*
  *		UnlockSharedObject
  */
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index f3c9f1f9bab..c900427ecf9 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -36,6 +36,7 @@
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/async.h"
+#include "commands/event_trigger.h"
 #include "commands/prepare.h"
 #include "common/pg_prng.h"
 #include "jit/jit.h"
@@ -4289,6 +4290,9 @@ PostgresMain(const char *dbname, const char *username)
 	initStringInfo(&row_description_buf);
 	MemoryContextSwitchTo(TopMemoryContext);
 
+	/* Fire any defined login event triggers, if appropriate */
+	EventTriggerOnLogin();
+
 	/*
 	 * POSTGRES main processing loop begins here
 	 *
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index b080f7a35f3..ab5111c90fd 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -167,6 +167,8 @@ BuildEventTriggerCache(void)
 			event = EVT_SQLDrop;
 		else if (strcmp(evtevent, "table_rewrite") == 0)
 			event = EVT_TableRewrite;
+		else if (strcmp(evtevent, "login") == 0)
+			event = EVT_Login;
 		else
 			continue;
 
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 011ec18015a..60bc1217fb4 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -90,6 +90,8 @@ Oid			MyDatabaseId = InvalidOid;
 
 Oid			MyDatabaseTableSpace = InvalidOid;
 
+bool		MyDatabaseHasLoginEventTriggers = false;
+
 /*
  * DatabasePath is the path (relative to DataDir) of my database's
  * primary directory, ie, its directory in the default tablespace.
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index e60ecd1e366..552cf9d950a 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -1103,6 +1103,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 		}
 
 		MyDatabaseTableSpace = datform->dattablespace;
+		MyDatabaseHasLoginEventTriggers = datform->dathasloginevt;
 		/* pass the database name back to the caller */
 		if (out_dbname)
 			strcpy(out_dbname, dbname);
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index f7b61766921..83aeef2751b 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3263,6 +3263,11 @@ dumpDatabase(Archive *fout)
 		appendPQExpBufferStr(delQry, ";\n");
 	}
 
+	/*
+	 * We do not restore pg_database.dathasloginevt because it is set
+	 * automatically on login event trigger creation.
+	 */
+
 	/* Add database-specific SET options */
 	dumpDatabaseConfig(fout, creaQry, datname, dbCatId.oid);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index eb4dfe80b50..93742fc6ac9 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3552,8 +3552,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ON");
 	/* Complete CREATE EVENT TRIGGER <name> ON with event_type */
 	else if (Matches("CREATE", "EVENT", "TRIGGER", MatchAny, "ON"))
-		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "sql_drop",
-					  "table_rewrite");
+		COMPLETE_WITH("ddl_command_start", "ddl_command_end", "login",
+					  "sql_drop", "table_rewrite");
 
 	/*
 	 * Complete CREATE EVENT TRIGGER <name> ON <event_type>.  EXECUTE FUNCTION
diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat
index 0754ef1bce4..8d91e3bf8da 100644
--- a/src/include/catalog/pg_database.dat
+++ b/src/include/catalog/pg_database.dat
@@ -16,7 +16,7 @@
   descr => 'default template for new databases',
   datname => 'template1', encoding => 'ENCODING',
   datlocprovider => 'LOCALE_PROVIDER', datistemplate => 't',
-  datallowconn => 't', datconnlimit => '-1', datfrozenxid => '0',
+  datallowconn => 't', dathasloginevt => 'f', datconnlimit => '-1', datfrozenxid => '0',
   datminmxid => '1', dattablespace => 'pg_default', datcollate => 'LC_COLLATE',
   datctype => 'LC_CTYPE', daticulocale => 'ICU_LOCALE',
   daticurules => 'ICU_RULES', datacl => '_null_' },
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index e9eb06b2e53..3e50a570046 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -49,6 +49,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
 	/* new connections allowed? */
 	bool		datallowconn;
 
+	/* database has login event triggers? */
+	bool		dathasloginevt;
+
 	/*
 	 * Max connections allowed. Negative values have special meaning, see
 	 * DATCONNLIMIT_* defines below.
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 1c925dbf257..9e3ece50d5f 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -56,6 +56,7 @@ extern void EventTriggerDDLCommandStart(Node *parsetree);
 extern void EventTriggerDDLCommandEnd(Node *parsetree);
 extern void EventTriggerSQLDrop(Node *parsetree);
 extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason);
+extern void EventTriggerOnLogin(void);
 
 extern bool EventTriggerBeginCompleteQuery(void);
 extern void EventTriggerEndCompleteQuery(void);
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index c2f9de63a13..7232b03e379 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -203,6 +203,8 @@ extern PGDLLIMPORT Oid MyDatabaseId;
 
 extern PGDLLIMPORT Oid MyDatabaseTableSpace;
 
+extern PGDLLIMPORT bool MyDatabaseHasLoginEventTriggers;
+
 /*
  * Date/Time Configuration
  *
diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h
index 4ee91e3cf93..952ebe75cb4 100644
--- a/src/include/storage/lmgr.h
+++ b/src/include/storage/lmgr.h
@@ -99,6 +99,8 @@ extern void UnlockDatabaseObject(Oid classid, Oid objid, uint16 objsubid,
 /* Lock a shared-across-databases object (other than a relation) */
 extern void LockSharedObject(Oid classid, Oid objid, uint16 objsubid,
 							 LOCKMODE lockmode);
+extern bool ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid,
+										LOCKMODE lockmode);
 extern void UnlockSharedObject(Oid classid, Oid objid, uint16 objsubid,
 							   LOCKMODE lockmode);
 
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index e738ac1c097..553a31874f1 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
+PG_CMDTAG(CMDTAG_LOGIN, "LOGIN", true, false, false)
 PG_CMDTAG(CMDTAG_MERGE, "MERGE", false, false, true)
 PG_CMDTAG(CMDTAG_MOVE, "MOVE", false, false, true)
 PG_CMDTAG(CMDTAG_NOTIFY, "NOTIFY", false, false, false)
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index d340026518a..52052e6252a 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -22,7 +22,8 @@ typedef enum
 	EVT_DDLCommandStart,
 	EVT_DDLCommandEnd,
 	EVT_SQLDrop,
-	EVT_TableRewrite
+	EVT_TableRewrite,
+	EVT_Login,
 } EventTriggerEvent;
 
 typedef struct
diff --git a/src/test/authentication/t/005_login_trigger.pl b/src/test/authentication/t/005_login_trigger.pl
new file mode 100644
index 00000000000..f317012a19d
--- /dev/null
+++ b/src/test/authentication/t/005_login_trigger.pl
@@ -0,0 +1,189 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+# Tests of authentication via login trigger. Mostly for rejection via
+# exception, because this scenario cannot be covered with *.sql/*.out regress
+# tests.
+
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Execute a psql command and compare its output towards given regexps
+sub psql_command
+{
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+	my ($node, $sql, $expected_ret, $test_name, %params) = @_;
+
+	my $connstr;
+	if (defined($params{connstr}))
+	{
+		$connstr = $params{connstr};
+	}
+	else
+	{
+		$connstr = '';
+	}
+
+	# Execute command
+	my ($ret, $stdout, $stderr) =
+	  $node->psql('postgres', $sql, connstr => "$connstr");
+
+	# Check return code
+	is($ret, $expected_ret, "$test_name: exit code $expected_ret");
+
+	# Check stdout
+	if (defined($params{log_like}))
+	{
+		my @log_like = @{ $params{log_like} };
+		while (my $regex = shift @log_like)
+		{
+			like($stdout, $regex, "$test_name: log matches");
+		}
+	}
+	if (defined($params{log_unlike}))
+	{
+		my @log_unlike = @{ $params{log_unlike} };
+		while (my $regex = shift @log_unlike)
+		{
+			unlike($stdout, $regex, "$test_name: log unmatches");
+		}
+	}
+	if (defined($params{log_exact}))
+	{
+		is($stdout, $params{log_exact}, "$test_name: log equals");
+	}
+
+	# Check stderr
+	if (defined($params{err_like}))
+	{
+		my @err_like = @{ $params{err_like} };
+		while (my $regex = shift @err_like)
+		{
+			like($stderr, $regex, "$test_name: err matches");
+		}
+	}
+	if (defined($params{err_unlike}))
+	{
+		my @err_unlike = @{ $params{err_unlike} };
+		while (my $regex = shift @err_unlike)
+		{
+			unlike($stderr, $regex, "$test_name: err unmatches");
+		}
+	}
+	if (defined($params{err_exact}))
+	{
+		is($stderr, $params{err_exact}, "$test_name: err equals");
+	}
+
+	return;
+}
+
+# New node
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init(extra => [ '--locale=C', '--encoding=UTF8' ]);
+$node->append_conf(
+	'postgresql.conf', q{
+wal_level = 'logical'
+max_replication_slots = 4
+max_wal_senders = 4
+});
+$node->start;
+
+# Create temporary roles and log table
+psql_command(
+	$node, 'CREATE ROLE alice WITH LOGIN;
+CREATE ROLE mallory WITH LOGIN;
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+', 0, 'create tmp objects',
+	log_exact => '',
+	err_exact => ''),
+  ;
+
+# Create login event function and trigger
+psql_command(
+	$node,
+	'CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  IF SESSION_USER = \'mallory\' THEN
+    RAISE EXCEPTION \'Hello %! You are NOT welcome here!\', SESSION_USER;
+  END IF;
+  RAISE NOTICE \'Hello %! You are welcome!\', SESSION_USER;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+', 0, 'create trigger function',
+	log_exact => '',
+	err_exact => '');
+
+psql_command(
+	$node,
+	'CREATE EVENT TRIGGER on_login_trigger '
+	  . 'ON login EXECUTE PROCEDURE on_login_proc();', 0,
+	'create event trigger',
+	log_exact => '',
+	err_exact => '');
+psql_command(
+	$node, 'ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;', 0,
+	'alter event trigger',
+	log_exact => '',
+	err_like => [qr/You are welcome/]);
+
+# Check the two requests were logged via login trigger
+psql_command(
+	$node, 'SELECT COUNT(*) FROM user_logins;', 0, 'select count',
+	log_exact => '2',
+	err_like => [qr/You are welcome/]);
+
+# Try to log as allowed Alice and disallowed Mallory (two times)
+psql_command(
+	$node, 'SELECT 1;', 0, 'try alice',
+	connstr => 'user=alice',
+	log_exact => '1',
+	err_like => [qr/You are welcome/],
+	err_unlike => [qr/You are NOT welcome/]);
+psql_command(
+	$node, 'SELECT 1;', 2, 'try mallory',
+	connstr => 'user=mallory',
+	log_exact => '',
+	err_like => [qr/You are NOT welcome/],
+	err_unlike => [qr/You are welcome/]);
+psql_command(
+	$node, 'SELECT 1;', 2, 'try mallory',
+	connstr => 'user=mallory',
+	log_exact => '',
+	err_like => [qr/You are NOT welcome/],
+	err_unlike => [qr/You are welcome/]);
+
+# Check that Alice's login record is here, while the Mallory's one is not
+psql_command(
+	$node, 'SELECT * FROM user_logins;', 0, 'select *',
+	log_like => [qr/3\|alice/],
+	log_unlike => [qr/mallory/],
+	err_like => [qr/You are welcome/]);
+
+# Check total number of successful logins so far
+psql_command(
+	$node, 'SELECT COUNT(*) FROM user_logins;', 0, 'select count',
+	log_exact => '5',
+	err_like => [qr/You are welcome/]);
+
+# Cleanup the temporary stuff
+psql_command(
+	$node, 'DROP EVENT TRIGGER on_login_trigger;', 0,
+	'drop event trigger',
+	log_exact => '',
+	err_like => [qr/You are welcome/]);
+psql_command(
+	$node, 'DROP TABLE user_logins;
+DROP FUNCTION on_login_proc;
+DROP ROLE mallory;
+DROP ROLE alice;
+', 0, 'cleanup',
+	log_exact => '',
+	err_exact => '');
+
+done_testing();
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index 0c72ba09441..95f9b0d7726 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -46,6 +46,25 @@ $node_standby_2->start;
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
 
+$node_primary->safe_psql(
+	'postgres', q{
+CREATE TABLE user_logins(id serial, who text);
+
+CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
+BEGIN
+  IF NOT pg_is_in_recovery() THEN
+    INSERT INTO user_logins (who) VALUES (session_user);
+  END IF;
+  IF session_user = 'regress_hacker' THEN
+    RAISE EXCEPTION 'You are not welcome!';
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE FUNCTION on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+});
+
 # Wait for standbys to catch up
 $node_primary->wait_for_replay_catchup($node_standby_1);
 $node_standby_1->wait_for_replay_catchup($node_standby_2, $node_primary);
@@ -384,6 +403,13 @@ sub replay_check
 
 replay_check();
 
+my $evttrig = $node_standby_1->safe_psql('postgres',
+	"SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+$evttrig = $node_standby_2->safe_psql('postgres',
+	"SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'");
+is($evttrig, 'on_login_trigger', 'Name of login trigger');
+
 note "enabling hot_standby_feedback";
 
 # Enable hs_feedback. The slot should gain an xmin. We set the status interval
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 0b87a42d0a9..eaaff6ba6f1 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -638,3 +638,48 @@ NOTICE:  DROP POLICY dropped policy
 CREATE POLICY pguc ON event_trigger_test USING (FALSE);
 SET event_triggers = 'off';
 DROP POLICY pguc ON event_trigger_test;
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     1
+(1 row)
+
+\c
+NOTICE:  You are welcome!
+SELECT COUNT(*) FROM user_logins;
+ count 
+-------
+     2
+(1 row)
+
+-- Check dathasloginevt in system catalog
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+ dathasloginevt 
+----------------
+ t
+(1 row)
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
+\c
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+ dathasloginevt 
+----------------
+ f
+(1 row)
+
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 6f0933b9e88..9c2b7903fba 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -495,3 +495,29 @@ DROP POLICY pguc ON event_trigger_test;
 CREATE POLICY pguc ON event_trigger_test USING (FALSE);
 SET event_triggers = 'off';
 DROP POLICY pguc ON event_trigger_test;
+
+-- Login event triggers
+CREATE TABLE user_logins(id serial, who text);
+GRANT SELECT ON user_logins TO public;
+CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
+BEGIN
+  INSERT INTO user_logins (who) VALUES (SESSION_USER);
+  RAISE NOTICE 'You are welcome!';
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
+ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS;
+\c
+SELECT COUNT(*) FROM user_logins;
+\c
+SELECT COUNT(*) FROM user_logins;
+
+-- Check dathasloginevt in system catalog
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
+
+-- Cleanup
+DROP TABLE user_logins;
+DROP EVENT TRIGGER on_login_trigger;
+DROP FUNCTION on_login_proc();
+\c
+SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
-- 
2.39.3 (Apple Git-145)

#169Michael Paquier
michael@paquier.xyz
In reply to: Alexander Korotkov (#168)
Re: On login trigger: take three

On Mon, Oct 16, 2023 at 02:47:03AM +0300, Alexander Korotkov wrote:

The attached revision fixes test failures spotted by
commitfest.cputube.org. Also, perl scripts passed perltidy.

Still you've missed a few things. At quick glance:
- The code indentation was off a bit in event_trigger.c.
- 005_login_trigger.pl fails if the code is compiled with
ENFORCE_REGRESSION_TEST_NAME_RESTRICTIONS because a WARNING is
reported in test "create tmp objects: err equals".
- 005_sspi.pl is older than the new test 005_login_trigger.pl, could
you rename it with a different number?
--
Michael

#170Alexander Korotkov
aekorotkov@gmail.com
In reply to: Michael Paquier (#169)
Re: On login trigger: take three

On Mon, Oct 16, 2023 at 4:00 AM Michael Paquier <michael@paquier.xyz> wrote:

On Mon, Oct 16, 2023 at 02:47:03AM +0300, Alexander Korotkov wrote:

The attached revision fixes test failures spotted by
commitfest.cputube.org. Also, perl scripts passed perltidy.

Still you've missed a few things. At quick glance:
- The code indentation was off a bit in event_trigger.c.
- 005_login_trigger.pl fails if the code is compiled with
ENFORCE_REGRESSION_TEST_NAME_RESTRICTIONS because a WARNING is
reported in test "create tmp objects: err equals".
- 005_sspi.pl is older than the new test 005_login_trigger.pl, could
you rename it with a different number?

You are very fast and sharp eye!
Thank you for fixing the indentation. I just pushed fixes for the rest.

------
Regards,
Alexander Korotkov

#171Mikhail Gribkov
youzhick@gmail.com
In reply to: Alexander Korotkov (#170)
Re: On login trigger: take three

Hi Alexander,

Sorry for my long offline and thanks for the activity. So should we close
the patch on the commitfest page now?

By the way I had one more issue with the login trigger tests (quite a rare
one though). A race condition may occur on some systems, when oidjoins test
starts a moment later than normally and affects logins count for on-login
trigger test. Thus I had to split event_trigger and oidjoins tests into
separate parallel groups. I'll post this as an independent patch then.

--
best regards,
Mikhail A. Gribkov

e-mail: youzhick@gmail.com
*http://www.flickr.com/photos/youzhick/albums
<http://www.flickr.com/photos/youzhick/albums&gt;*
http://www.strava.com/athletes/5085772
phone: +7(916)604-71-12
Telegram: @youzhick

On Mon, Oct 16, 2023 at 4:05 AM Alexander Korotkov <aekorotkov@gmail.com>
wrote:

Show quoted text

On Mon, Oct 16, 2023 at 4:00 AM Michael Paquier <michael@paquier.xyz>
wrote:

On Mon, Oct 16, 2023 at 02:47:03AM +0300, Alexander Korotkov wrote:

The attached revision fixes test failures spotted by
commitfest.cputube.org. Also, perl scripts passed perltidy.

Still you've missed a few things. At quick glance:
- The code indentation was off a bit in event_trigger.c.
- 005_login_trigger.pl fails if the code is compiled with
ENFORCE_REGRESSION_TEST_NAME_RESTRICTIONS because a WARNING is
reported in test "create tmp objects: err equals".
- 005_sspi.pl is older than the new test 005_login_trigger.pl, could
you rename it with a different number?

You are very fast and sharp eye!
Thank you for fixing the indentation. I just pushed fixes for the rest.

------
Regards,
Alexander Korotkov

#172Alexander Korotkov
aekorotkov@gmail.com
In reply to: Mikhail Gribkov (#171)
Re: On login trigger: take three

Hi, Mikhail.

On Wed, Oct 18, 2023 at 1:30 PM Mikhail Gribkov <youzhick@gmail.com> wrote:

Sorry for my long offline and thanks for the activity. So should we close the patch on the commitfest page now?

I have just done this.

By the way I had one more issue with the login trigger tests (quite a rare one though). A race condition may occur on some systems, when oidjoins test starts a moment later than normally and affects logins count for on-login trigger test. Thus I had to split event_trigger and oidjoins tests into separate parallel groups. I'll post this as an independent patch then.

Thank you for catching it. Please, post this.

------
Regards,
Alexander Korotkov

#173Mikhail Gribkov
youzhick@gmail.com
In reply to: Alexander Korotkov (#172)
Re: On login trigger: take three

Hi Alexander,

Thank you for catching it. Please, post this.

Just for a more complete picture of the final state here.
I have posted the described fix (for avoiding race condition in the tests)
separately:
https://commitfest.postgresql.org/45/4616/

--
best regards,
Mikhail A. Gribkov

e-mail: youzhick@gmail.com

Show quoted text
#174Tom Lane
tgl@sss.pgh.pa.us
In reply to: Mikhail Gribkov (#173)
Re: On login trigger: take three

Mikhail Gribkov <youzhick@gmail.com> writes:

Just for a more complete picture of the final state here.
I have posted the described fix (for avoiding race condition in the tests)
separately:
https://commitfest.postgresql.org/45/4616/

It turns out that the TAP test for this feature (006_login_trigger.pl)
also has a race condition:

https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=mamba&amp;dt=2023-10-28%2003%3A33%3A28

The critical bit of the log is

ack Broken pipe: write( 14, 'SELECT 1;' ) at /usr/pkg/lib/perl5/vendor_perl/5.36.0/IPC/Run/IO.pm line 550.

It looks to me that what happened here is that the backend completed the
authentication handshake, and then the login trigger caused a FATAL exit,
and after than the connected psql session tried to send "SELECT 1" on
an already-closed pipe. That failed, causing IPC::Run to panic.

mamba is a fairly slow machine and doubtless has timing a bit different
from what this test was created on. But I doubt there is any way to make
this behavior perfectly stable across a range of machines, so I recommend
just removing the test case involving a fatal exit.

regards, tom lane

#175Alexander Korotkov
aekorotkov@gmail.com
In reply to: Tom Lane (#174)
1 attachment(s)
Re: On login trigger: take three

On Sun, Oct 29, 2023 at 6:16 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:

Mikhail Gribkov <youzhick@gmail.com> writes:

Just for a more complete picture of the final state here.
I have posted the described fix (for avoiding race condition in the tests)
separately:
https://commitfest.postgresql.org/45/4616/

It turns out that the TAP test for this feature (006_login_trigger.pl)
also has a race condition:

https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=mamba&amp;dt=2023-10-28%2003%3A33%3A28

The critical bit of the log is

ack Broken pipe: write( 14, 'SELECT 1;' ) at /usr/pkg/lib/perl5/vendor_perl/5.36.0/IPC/Run/IO.pm line 550.

It looks to me that what happened here is that the backend completed the
authentication handshake, and then the login trigger caused a FATAL exit,
and after than the connected psql session tried to send "SELECT 1" on
an already-closed pipe. That failed, causing IPC::Run to panic.

mamba is a fairly slow machine and doubtless has timing a bit different
from what this test was created on. But I doubt there is any way to make
this behavior perfectly stable across a range of machines, so I recommend
just removing the test case involving a fatal exit.

Makes sense. Are you good with the attached patch?

------
Regards,
Alexander Korotkov

Attachments:

fix_login_trigger_test_instability.patchapplication/octet-stream; name=fix_login_trigger_test_instability.patchDownload
diff --git a/src/test/authentication/t/006_login_trigger.pl b/src/test/authentication/t/006_login_trigger.pl
index 24beb0a0b2d..68435ffc6ac 100644
--- a/src/test/authentication/t/006_login_trigger.pl
+++ b/src/test/authentication/t/006_login_trigger.pl
@@ -143,27 +143,16 @@ psql_command(
 	log_exact => '2',
 	err_like => [qr/You are welcome/]);
 
-# Try to log as allowed Alice and disallowed Mallory (two times)
+# Try to login as allowed Alice.  We don't check the Malroy login, because
+# FATAL error could cause a timing-dependant panic of IPC::Run.
 psql_command(
 	$node, 'SELECT 1;', 0, 'try regress_alice',
 	connstr => 'user=regress_alice',
 	log_exact => '1',
 	err_like => [qr/You are welcome/],
 	err_unlike => [qr/You are NOT welcome/]);
-psql_command(
-	$node, 'SELECT 1;', 2, 'try regress_mallory',
-	connstr => 'user=regress_mallory',
-	log_exact => '',
-	err_like => [qr/You are NOT welcome/],
-	err_unlike => [qr/You are welcome/]);
-psql_command(
-	$node, 'SELECT 1;', 2, 'try regress_mallory',
-	connstr => 'user=regress_mallory',
-	log_exact => '',
-	err_like => [qr/You are NOT welcome/],
-	err_unlike => [qr/You are welcome/]);
 
-# Check that Alice's login record is here, while the Mallory's one is not
+# Check that Alice's login record is here
 psql_command(
 	$node, 'SELECT * FROM user_logins;', 0, 'select *',
 	log_like => [qr/3\|regress_alice/],
#176Tom Lane
tgl@sss.pgh.pa.us
In reply to: Alexander Korotkov (#175)
Re: On login trigger: take three

Alexander Korotkov <aekorotkov@gmail.com> writes:

On Sun, Oct 29, 2023 at 6:16 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:

It looks to me that what happened here is that the backend completed the
authentication handshake, and then the login trigger caused a FATAL exit,
and after than the connected psql session tried to send "SELECT 1" on
an already-closed pipe. That failed, causing IPC::Run to panic.

Looking closer, what must have happened is that the psql session ended
before IPC::Run could jam 'SELECT 1' into its stdin. I wonder if this
could be stabilized by doing 'psql -c "SELECT 1"' and not having to
write anything to the child process stdin? But I'm not sure this test
case is valuable enough to put a great deal of work into.

mamba is a fairly slow machine and doubtless has timing a bit different
from what this test was created on. But I doubt there is any way to make
this behavior perfectly stable across a range of machines, so I recommend
just removing the test case involving a fatal exit.

Makes sense. Are you good with the attached patch?

OK by me, though I note you misspelled "mallory" in the comment.

regards, tom lane

#177Alexander Lakhin
exclusion@gmail.com
In reply to: Alexander Korotkov (#175)
Re: On login trigger: take three

Hello Alexander,

I've discovered one more instability in the event_trigger_login test.
Please look for example at case [1]https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=skink&amp;dt=2024-01-03%2023%3A04%3A20:
ok 213       + event_trigger                           28946 ms
not ok 214   - event_trigger_login                      6430 ms
ok 215       - fast_default                            19349 ms
ok 216       - tablespace                              44789 ms
1..216
# 1 of 216 tests failed.

--- /home/bf/bf-build/skink-master/HEAD/pgsql/src/test/regress/expected/event_trigger_login.out 2023-10-27 
22:55:12.574139524 +0000
+++ /home/bf/bf-build/skink-master/HEAD/pgsql.build/src/test/regress/results/event_trigger_login.out 2024-01-03 
23:49:50.177461816 +0000
@@ -40,6 +40,6 @@
  SELECT dathasloginevt FROM pg_database WHERE datname= :'DBNAME';
   dathasloginevt
  ----------------
- f
+ t
  (1 row)

2024-01-03 23:49:40.378 UTC [2235175][client backend][3/5949:0] STATEMENT:  REINDEX INDEX CONCURRENTLY concur_reindex_ind;
...
2024-01-03 23:49:50.340 UTC [2260974][autovacuum worker][5/5439:18812] LOG:  automatic vacuum of table
"regression.pg_catalog.pg_statistic": index scans: 1
(operations logged around 23:49:50.177461816)

I suspected that this failure was caused by autovacuum, and I've managed to
reproduce it without Valgrind or slowing down a machine.
With /tmp/extra.config:
log_autovacuum_min_duration = 0
autovacuum_naptime = 1
autovacuum = on

I run:
for ((i=1;i<10;i++)); do echo "ITERATION $i"; \
TEMP_CONFIG=/tmp/extra.config \
TESTS=$(printf 'event_trigger_login %.0s' `seq 1000`) \
make -s check-tests || break; done
and get failures on iterations 1, 2, 1...
ok 693       - event_trigger_login                        15 ms
not ok 694   - event_trigger_login                        15 ms
ok 695       - event_trigger_login                        21 ms

Also, I've observed an anomaly after dropping a login event trigger:
CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
BEGIN RAISE NOTICE 'You are welcome!'; END; $$ LANGUAGE plpgsql;
CREATE EVENT TRIGGER olt ON login EXECUTE PROCEDURE on_login_proc();
SELECT dathasloginevt FROM pg_database WHERE datname= current_database();
 dathasloginevt
----------------
 t
(1 row)

DROP EVENT TRIGGER olt;
SELECT dathasloginevt FROM pg_database WHERE datname= current_database();
 dathasloginevt
----------------
 t
(1 row)

Although after reconnection (\c, as done in the event_trigger_login test),
this query returns 'f'.

[1]: https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=skink&amp;dt=2024-01-03%2023%3A04%3A20
[2]: https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=skink&amp;dt=2023-12-26%2019%3A33%3A02

Best regards,
Alexander

#178Alexander Korotkov
aekorotkov@gmail.com
In reply to: Alexander Lakhin (#177)
Re: On login trigger: take three

Hi, Alexander!

On Sat, Jan 13, 2024 at 6:00 PM Alexander Lakhin <exclusion@gmail.com> wrote:

I've discovered one more instability in the event_trigger_login test.
Please look for example at case [1]:
ok 213 + event_trigger 28946 ms
not ok 214 - event_trigger_login 6430 ms
ok 215 - fast_default 19349 ms
ok 216 - tablespace 44789 ms
1..216
# 1 of 216 tests failed.

Thank you for reporting this.

I'm going to take a closer look at this tomorrow. But I doubt I would
find a solution other than removing the flaky parts of the test.

------
Regards,
Alexander Korotkov

#179Daniel Gustafsson
daniel@yesql.se
In reply to: Alexander Lakhin (#177)
Re: On login trigger: take three

On 13 Jan 2024, at 17:00, Alexander Lakhin <exclusion@gmail.com> wrote:

DROP EVENT TRIGGER olt;
SELECT dathasloginevt FROM pg_database WHERE datname= current_database();
dathasloginevt
----------------
t
(1 row)

Although after reconnection (\c, as done in the event_trigger_login test),
this query returns 'f'.

This is by design in the patch. The flag isn't changed on DROP, it is only
cleared on logins iff there is no login event trigger. The description in the
docs is worded to indicate that it shouldn't be taken as truth for monitoring
purposes:

"This flag is used internally by PostgreSQL and should not be manually
altered or read for monitoring purposes."

--
Daniel Gustafsson

#180Daniel Gustafsson
daniel@yesql.se
In reply to: Alexander Lakhin (#177)
Re: On login trigger: take three

On 13 Jan 2024, at 17:00, Alexander Lakhin <exclusion@gmail.com> wrote:

I suspected that this failure was caused by autovacuum, and I've managed to
reproduce it without Valgrind or slowing down a machine.

This might be due to the fact that the cleanup codepath to remove the flag when
there is no login event trigger doesn't block on locking pg_database to avoid
stalling connections. There are no guarantees when the flag is cleared, so a
test like this will always have potential to be flaky it seems.

--
Daniel Gustafsson

#181Alexander Korotkov
aekorotkov@gmail.com
In reply to: Daniel Gustafsson (#180)
Re: On login trigger: take three

On Mon, Jan 15, 2024 at 11:29 AM Daniel Gustafsson <daniel@yesql.se> wrote:

On 13 Jan 2024, at 17:00, Alexander Lakhin <exclusion@gmail.com> wrote:

I suspected that this failure was caused by autovacuum, and I've managed to
reproduce it without Valgrind or slowing down a machine.

This might be due to the fact that the cleanup codepath to remove the flag when
there is no login event trigger doesn't block on locking pg_database to avoid
stalling connections. There are no guarantees when the flag is cleared, so a
test like this will always have potential to be flaky it seems.

+1
Thank you, Daniel. I think you described everything absolutely
correctly. As I wrote upthread, it doesn't seem much could be done
with this, at least within a regression test. So, I just removed this
query from the test.

------
Regards,
Alexander Korotkov

#182Alexander Lakhin
exclusion@gmail.com
In reply to: Alexander Korotkov (#181)
Re: On login trigger: take three

Hello Alexander and Daniel,

Please look at the following query, which triggers an assertion failure on
updating the field dathasloginevt for an entry in pg_database:
SELECT format('CREATE DATABASE db1 LOCALE_PROVIDER icu ICU_LOCALE en ENCODING utf8
ICU_RULES ''' || repeat(' ', 200000) || ''' TEMPLATE template0;')
\gexec
\c db1 -

CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
BEGIN
  RAISE NOTICE 'You are welcome!';
END;
$$ LANGUAGE plpgsql;

CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
DROP EVENT TRIGGER on_login_trigger;

\c

\connect: connection to server on socket "/tmp/.s.PGSQL.5432" failed: server closed the connection unexpectedly

The stack trace of the assertion failure is:
...
#5  0x000055c8699b9b8d in ExceptionalCondition (
    conditionName=conditionName@entry=0x55c869a1f7c0 "HaveRegisteredOrActiveSnapshot()",
    fileName=fileName@entry=0x55c869a1f4c6 "toast_internals.c", lineNumber=lineNumber@entry=669) at assert.c:66
#6  0x000055c86945df0a in init_toast_snapshot (...) at toast_internals.c:669
#7  0x000055c86945dfbe in toast_delete_datum (...) at toast_internals.c:429
#8  0x000055c8694fd1da in toast_tuple_cleanup (...) at toast_helper.c:309
#9  0x000055c8694b55a1 in heap_toast_insert_or_update (...) at heaptoast.c:333
#10 0x000055c8694a8c6c in heap_update (... at heapam.c:3604
#11 0x000055c8694a96cb in simple_heap_update (...) at heapam.c:4062
#12 0x000055c869555b7b in CatalogTupleUpdate (...) at indexing.c:322
#13 0x000055c8695f0725 in EventTriggerOnLogin () at event_trigger.c:957
...

Funnily enough, when Tom Lane was wondering, whether pg_database's toast
table could pose a risk [1]/messages/by-id/1284094.1695479962@sss.pgh.pa.us, I came to the conclusion that it's impossible,
but that was before the login triggers introduction...

[1]: /messages/by-id/1284094.1695479962@sss.pgh.pa.us

Best regards,
Alexander

#183Alexander Korotkov
aekorotkov@gmail.com
In reply to: Alexander Lakhin (#182)
Re: On login trigger: take three

On Tue, Jan 23, 2024 at 8:00 PM Alexander Lakhin <exclusion@gmail.com> wrote:

Please look at the following query, which triggers an assertion failure on
updating the field dathasloginevt for an entry in pg_database:
SELECT format('CREATE DATABASE db1 LOCALE_PROVIDER icu ICU_LOCALE en ENCODING utf8
ICU_RULES ''' || repeat(' ', 200000) || ''' TEMPLATE template0;')
\gexec
\c db1 -

CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
BEGIN
RAISE NOTICE 'You are welcome!';
END;
$$ LANGUAGE plpgsql;

CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
DROP EVENT TRIGGER on_login_trigger;

\c

\connect: connection to server on socket "/tmp/.s.PGSQL.5432" failed: server closed the connection unexpectedly

The stack trace of the assertion failure is:
...
#5 0x000055c8699b9b8d in ExceptionalCondition (
conditionName=conditionName@entry=0x55c869a1f7c0 "HaveRegisteredOrActiveSnapshot()",
fileName=fileName@entry=0x55c869a1f4c6 "toast_internals.c", lineNumber=lineNumber@entry=669) at assert.c:66
#6 0x000055c86945df0a in init_toast_snapshot (...) at toast_internals.c:669
#7 0x000055c86945dfbe in toast_delete_datum (...) at toast_internals.c:429
#8 0x000055c8694fd1da in toast_tuple_cleanup (...) at toast_helper.c:309
#9 0x000055c8694b55a1 in heap_toast_insert_or_update (...) at heaptoast.c:333
#10 0x000055c8694a8c6c in heap_update (... at heapam.c:3604
#11 0x000055c8694a96cb in simple_heap_update (...) at heapam.c:4062
#12 0x000055c869555b7b in CatalogTupleUpdate (...) at indexing.c:322
#13 0x000055c8695f0725 in EventTriggerOnLogin () at event_trigger.c:957
...

Funnily enough, when Tom Lane was wondering, whether pg_database's toast
table could pose a risk [1], I came to the conclusion that it's impossible,
but that was before the login triggers introduction...

[1] /messages/by-id/1284094.1695479962@sss.pgh.pa.us

Thank you for reporting. I'm looking into this...
I wonder if there is a way to avoid toast update given that we don't
touch the toasted field here.

------
Regards,
Alexander Korotkov

#184Alexander Korotkov
aekorotkov@gmail.com
In reply to: Alexander Korotkov (#183)
1 attachment(s)
Re: On login trigger: take three

On Tue, Jan 23, 2024 at 11:52 PM Alexander Korotkov
<aekorotkov@gmail.com> wrote:

On Tue, Jan 23, 2024 at 8:00 PM Alexander Lakhin <exclusion@gmail.com> wrote:

Please look at the following query, which triggers an assertion failure on
updating the field dathasloginevt for an entry in pg_database:
SELECT format('CREATE DATABASE db1 LOCALE_PROVIDER icu ICU_LOCALE en ENCODING utf8
ICU_RULES ''' || repeat(' ', 200000) || ''' TEMPLATE template0;')
\gexec
\c db1 -

CREATE FUNCTION on_login_proc() RETURNS event_trigger AS $$
BEGIN
RAISE NOTICE 'You are welcome!';
END;
$$ LANGUAGE plpgsql;

CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE PROCEDURE on_login_proc();
DROP EVENT TRIGGER on_login_trigger;

\c

\connect: connection to server on socket "/tmp/.s.PGSQL.5432" failed: server closed the connection unexpectedly

The stack trace of the assertion failure is:
...
#5 0x000055c8699b9b8d in ExceptionalCondition (
conditionName=conditionName@entry=0x55c869a1f7c0 "HaveRegisteredOrActiveSnapshot()",
fileName=fileName@entry=0x55c869a1f4c6 "toast_internals.c", lineNumber=lineNumber@entry=669) at assert.c:66
#6 0x000055c86945df0a in init_toast_snapshot (...) at toast_internals.c:669
#7 0x000055c86945dfbe in toast_delete_datum (...) at toast_internals.c:429
#8 0x000055c8694fd1da in toast_tuple_cleanup (...) at toast_helper.c:309
#9 0x000055c8694b55a1 in heap_toast_insert_or_update (...) at heaptoast.c:333
#10 0x000055c8694a8c6c in heap_update (... at heapam.c:3604
#11 0x000055c8694a96cb in simple_heap_update (...) at heapam.c:4062
#12 0x000055c869555b7b in CatalogTupleUpdate (...) at indexing.c:322
#13 0x000055c8695f0725 in EventTriggerOnLogin () at event_trigger.c:957
...

Funnily enough, when Tom Lane was wondering, whether pg_database's toast
table could pose a risk [1], I came to the conclusion that it's impossible,
but that was before the login triggers introduction...

[1] /messages/by-id/1284094.1695479962@sss.pgh.pa.us

Thank you for reporting. I'm looking into this...
I wonder if there is a way to avoid toast update given that we don't
touch the toasted field here.

Usage of heap_inplace_update() seems appropriate for me here. It
avoids trouble with both TOAST and row-level locks. Alexander, could
you please recheck this fixes the problem.

------
Regards,
Alexander Korotkov

Attachments:

0001-Use-heap_inplace_update-to-unset-pg_database.dath-v1.patchapplication/octet-stream; name=0001-Use-heap_inplace_update-to-unset-pg_database.dath-v1.patchDownload
From 0da5aaaedf20085cb078ac2ec5ede0f0cabcb86a Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <akorotkov@postgresql.org>
Date: Mon, 5 Feb 2024 01:46:41 +0200
Subject: [PATCH] Use heap_inplace_update() to unset pg_database.dathasloginevt

Reported-by: Alexander Lakhin
Discussion: https://postgr.es/m/e2a0248e-5f32-af0c-9832-a90d303c2c61%40gmail.com
---
 src/backend/commands/event_trigger.c | 31 ++++++++++++++++++++++++----
 1 file changed, 27 insertions(+), 4 deletions(-)

diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index f193c7ddf60..da01b458dc3 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -13,6 +13,7 @@
  */
 #include "postgres.h"
 
+#include "access/heapam.h"
 #include "access/htup_details.h"
 #include "access/table.h"
 #include "access/xact.h"
@@ -943,18 +944,40 @@ EventTriggerOnLogin(void)
 			Relation	pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
 			HeapTuple	tuple;
 			Form_pg_database db;
+			ScanKeyData key[1];
+			SysScanDesc scan;
 
-			tuple = SearchSysCacheCopy1(DATABASEOID,
-										ObjectIdGetDatum(MyDatabaseId));
+			/*
+			 * Get the pg_database tuple to scribble on.  Note that this does
+			 * not directly rely on the syscache to avoid issues with
+			 * flattened toast values for the in-place update.
+			 */
+			ScanKeyInit(&key[0],
+						Anum_pg_database_oid,
+						BTEqualStrategyNumber, F_OIDEQ,
+						ObjectIdGetDatum(MyDatabaseId));
+
+			scan = systable_beginscan(pg_db, DatabaseOidIndexId, true,
+									  NULL, 1, key);
+			tuple = systable_getnext(scan);
+			tuple = heap_copytuple(tuple);
+			systable_endscan(scan);
 
 			if (!HeapTupleIsValid(tuple))
-				elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+				elog(ERROR, "could not find tuple for database %u", MyDatabaseId);
 
 			db = (Form_pg_database) GETSTRUCT(tuple);
 			if (db->dathasloginevt)
 			{
 				db->dathasloginevt = false;
-				CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
+
+				/*
+				 * Do an "in place" update of the pg_database tuple.  Doing
+				 * this instead of regular updates serves two purposes.
+				 * First, that avoids possible waiting on the row-level lock.
+				 * Second, that avoids dealing with TOAST.
+				 */
+				heap_inplace_update(pg_db, tuple);
 			}
 			table_close(pg_db, RowExclusiveLock);
 			heap_freetuple(tuple);
-- 
2.39.3 (Apple Git-145)

#185Alexander Lakhin
exclusion@gmail.com
In reply to: Alexander Korotkov (#184)
Re: On login trigger: take three

Hello Alexander,

05.02.2024 02:51, Alexander Korotkov wrote:

Usage of heap_inplace_update() seems appropriate for me here. It
avoids trouble with both TOAST and row-level locks. Alexander, could
you please recheck this fixes the problem.

I've re-tested the last problematic scenario and can confirm that the fix
works for it (though it still doesn't prevent the autovacuum issue (with
4b885d01 reverted)), but using heap_inplace_update() was considered risky
in a recent discussion:
/messages/by-id/1596629.1698435146@sss.pgh.pa.us
So maybe it's worth to postpone such a fix till that discussion is
finished or to look for another approach...

Best regards,
Alexander

#186Alexander Korotkov
aekorotkov@gmail.com
In reply to: Alexander Lakhin (#185)
Re: On login trigger: take three

Hi, Alexander!

On Mon, Feb 5, 2024 at 7:00 PM Alexander Lakhin <exclusion@gmail.com> wrote:

05.02.2024 02:51, Alexander Korotkov wrote:

Usage of heap_inplace_update() seems appropriate for me here. It
avoids trouble with both TOAST and row-level locks. Alexander, could
you please recheck this fixes the problem.

I've re-tested the last problematic scenario and can confirm that the fix
works for it (though it still doesn't prevent the autovacuum issue (with
4b885d01 reverted)), but using heap_inplace_update() was considered risky
in a recent discussion:
/messages/by-id/1596629.1698435146@sss.pgh.pa.us
So maybe it's worth to postpone such a fix till that discussion is
finished or to look for another approach...

Thank you for pointing this out. I don't think there is a particular
problem with this use case for the following reasons.
1) Race conditions over pg_database.dathasloginevt are protected with lock tag.
2) Unsetting pg_database.dathasloginevt of old tuple version shouldn't
cause a problem. The next tuple version will be updated by further
connections.
However, I agree that it's better to wait for the discussion you've
pointed out before introducing another use case of
heap_inplace_update().

------
Regards,
Alexander Korotkov

#187Bharath Rupireddy
bharath.rupireddyforpostgres@gmail.com
In reply to: Alexander Korotkov (#170)
1 attachment(s)
Re: On login trigger: take three

On Mon, Oct 16, 2023 at 6:36 AM Alexander Korotkov <aekorotkov@gmail.com> wrote:

On Mon, Oct 16, 2023 at 02:47:03AM +0300, Alexander Korotkov wrote:

The attached revision fixes test failures spotted by
commitfest.cputube.org. Also, perl scripts passed perltidy.

You are very fast and sharp eye!
Thank you for fixing the indentation. I just pushed fixes for the rest.

I noticed some typos related to this feature.

While on this, the event trigger example for C programming language
shown in the docs doesn't compile as-is. fmgr.h is needed.

Please see the attached patch.

[1]: 2024-02-06 05:31:24.453 UTC [1401414] LOG: tag LOGIN, event login
2024-02-06 05:31:24.453 UTC [1401414] LOG: tag LOGIN, event login

#include "postgres.h"

#include "commands/event_trigger.h"
#include "fmgr.h"

PG_MODULE_MAGIC;

PG_FUNCTION_INFO_V1(onlogin);

Datum
onlogin(PG_FUNCTION_ARGS)
{
EventTriggerData *trigdata;

if (!CALLED_AS_EVENT_TRIGGER(fcinfo)) /* internal error */
elog(ERROR, "not fired by event trigger manager");

trigdata = (EventTriggerData *) fcinfo->context;

elog(LOG, "tag %s, event %s", GetCommandTagName(trigdata->tag),
trigdata->event);

PG_RETURN_NULL();
}

gcc -shared -fPIC -I /home/ubuntu/postgres/pg17/include/server -o
login_trigger.so login_trigger.c

LOAD '/home/ubuntu/postgres/login_trigger.so';

CREATE FUNCTION onlogin() RETURNS event_trigger
AS '/home/ubuntu/postgres/login_trigger' LANGUAGE C;

CREATE EVENT TRIGGER onlogin ON login
EXECUTE FUNCTION onlogin();

--
Bharath Rupireddy
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com

Attachments:

v1-0001-Fix-some-typos-in-event-trigger-docs.patchapplication/octet-stream; name=v1-0001-Fix-some-typos-in-event-trigger-docs.patchDownload
From ea847c6a455d97edb5ab934c22531ccab1ae4b0e Mon Sep 17 00:00:00 2001
From: Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com>
Date: Tue, 6 Feb 2024 05:56:50 +0000
Subject: [PATCH v1] Fix some typos in event trigger docs

---
 doc/src/sgml/event-trigger.sgml | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index a76bd84425..23dbb80481 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -49,7 +49,7 @@
      To prevent servers from becoming inaccessible, such triggers must avoid
      writing anything to the database when running on a standby.
      Also, it's recommended to avoid long-running queries in
-     <literal>login</literal> event triggers.  Notes that, for instance,
+     <literal>login</literal> event triggers.  Note that, for instance,
      canceling connection in <application>psql</application> wouldn't cancel
      the in-progress <literal>login</literal> trigger.
    </para>
@@ -1144,8 +1144,9 @@ typedef struct EventTriggerData
       <listitem>
        <para>
         Describes the event for which the function is called, one of
-        <literal>"ddl_command_start"</literal>, <literal>"ddl_command_end"</literal>,
-        <literal>"sql_drop"</literal>, <literal>"table_rewrite"</literal>.
+        <literal>"login"</literal>, <literal>"ddl_command_start"</literal>,
+        <literal>"ddl_command_end"</literal>, <literal>"sql_drop"</literal>,
+        <literal>"table_rewrite"</literal>.
         See <xref linkend="event-trigger-definition"/> for the meaning of these
         events.
        </para>
@@ -1203,8 +1204,9 @@ typedef struct EventTriggerData
     This is the source code of the trigger function:
 <programlisting><![CDATA[
 #include "postgres.h"
-#include "commands/event_trigger.h"
 
+#include "commands/event_trigger.h"
+#include "fmgr.h"
 
 PG_MODULE_MAGIC;
 
-- 
2.34.1