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 e09adb45e4..d3458840fb 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -3035,6 +3035,19 @@ SCRAM-SHA-256$<iteration count>:&l
+
+
+ dathasloginevt bool
+
+
+ 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 PostgreSQL
+ and should not be manually altered or read for monitoring purposes.
+
+
+
datconnlimit int4
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index f52165165d..54de81158b 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 3b6a5361b3..10b20f0339 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
+ login,
ddl_command_start,
ddl_command_end,
table_rewrite
@@ -35,6 +36,24 @@
Support for additional events may be added in future releases.
+
+ The login 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 is set to false
+ 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 reference
+ page for details about using single-user mode.
+ The login 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
+ login event triggers. Notes that, for instance,
+ cancelling connection in psql wouldn't cancel
+ the in-progress login trigger.
+
+
The ddl_command_start event occurs just before the
execution of a CREATE, ALTER, DROP,
@@ -1300,4 +1319,79 @@ CREATE EVENT TRIGGER no_rewrite_allowed
+
+
+ A Database Login Event Trigger Example
+
+
+ The event trigger on the login 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 login event checks whether or not the database is
+ in recovery before performing any writes. Writing to a standby server
+ will make it inaccessible.
+
+
+
+ The following example demonstrates these options.
+
+-- 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;
+
+
+
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 307729ab7e..c52ecc61a6 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 bd812e42d9..0b08552fd7 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 ee9b89a672..b447ddf11b 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 f3c9f1f9ba..c900427ecf 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 b080f7a35f..ab5111c90f 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 011ec18015..60bc1217fb 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 e60ecd1e36..552cf9d950 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 f7b6176692..83aeef2751 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 eb4dfe80b5..93742fc6ac 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 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 ON . EXECUTE FUNCTION
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index 5efc144520..c5f4af24dc 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -57,6 +57,6 @@
*/
/* yyyymmddN */
-#define CATALOG_VERSION_NO 202310141
+#define CATALOG_VERSION_NO 202310161
#endif
diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat
index 0754ef1bce..8d91e3bf8d 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 e9eb06b2e5..3e50a57004 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 1c925dbf25..9e3ece50d5 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 c2f9de63a1..7232b03e37 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 4ee91e3cf9..952ebe75cb 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 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..f317012a19
--- /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 0c72ba0944..95f9b0d772 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 0b87a42d0a..eaaff6ba6f 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 6f0933b9e8..9c2b7903fb 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';