Add support event triggers on authenticated login
authorAlexander Korotkov
Mon, 16 Oct 2023 00:16:55 +0000 (03:16 +0300)
committerAlexander Korotkov
Mon, 16 Oct 2023 00:18:22 +0000 (03:18 +0300)
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
25 files changed:
doc/src/sgml/bki.sgml
doc/src/sgml/catalogs.sgml
doc/src/sgml/ecpg.sgml
doc/src/sgml/event-trigger.sgml
src/backend/commands/dbcommands.c
src/backend/commands/event_trigger.c
src/backend/storage/lmgr/lmgr.c
src/backend/tcop/postgres.c
src/backend/utils/cache/evtcache.c
src/backend/utils/init/globals.c
src/backend/utils/init/postinit.c
src/bin/pg_dump/pg_dump.c
src/bin/psql/tab-complete.c
src/include/catalog/catversion.h
src/include/catalog/pg_database.dat
src/include/catalog/pg_database.h
src/include/commands/event_trigger.h
src/include/miscadmin.h
src/include/storage/lmgr.h
src/include/tcop/cmdtaglist.h
src/include/utils/evtcache.h
src/test/authentication/t/005_login_trigger.pl [new file with mode: 0644]
src/test/recovery/t/001_stream_rep.pl
src/test/regress/expected/event_trigger.out
src/test/regress/sql/event_trigger.sql

index f71644e39898286216fec9125313e6b32a7eb5e2..315ba81951484a550972914dfbdbd589f299ab32 100644 (file)
   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_' },
 
index e09adb45e410a556e50c1e5c219dbc894e382b5f..d3458840fbecd87ebfbd3d338aa7c9b9ef0042ca 100644 (file)
@@ -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
index f52165165dcdfa6f91ac558d3c2ddfb566609ed3..54de81158b5417ba4a47e85337548c8ae02f6bfd 100644 (file)
@@ -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)
index 3b6a5361b3459228432326ce517ba99cd0197fad..10b20f0339a6263ae881d23b923c5e59512f1de9 100644 (file)
@@ -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
      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 CREATEALTERDROP,
@@ -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;
+
+    
+  
 
index 307729ab7ef7401255d074c60ee3a81ebc2d0f01..c52ecc61a6ba35fe775ca76148ff2e72977f89fb 100644 (file)
@@ -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;
index bd812e42d943bb51dec92136732434adad48d32b..0b08552fd7a0a6a88cab569e6f0d422e2d9de970 100644 (file)
@@ -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"
 #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;
 
index ee9b89a67262195fbac38a6c38427e5046fa9f4d..b447ddf11ba2a6dabc851dfe2ef82f06b45fe12e 100644 (file)
@@ -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
  */
index f3c9f1f9bab1cdd50616ed5829417f0be416719e..c900427ecf957db1612028deff49887762a10b9b 100644 (file)
@@ -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
     *
index b080f7a35f3745562fa34cefb357667ed57ab549..ab5111c90fdd666fbf2fdada4340e8488e727ffa 100644 (file)
@@ -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;
 
index 011ec18015a2c1c148229483e5b0b2c1c7c89c77..60bc1217fb4ac66a24c18512a453fcd7527e3b78 100644 (file)
@@ -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.
index e60ecd1e3668ee9305bc1fb65ab589be903a6bcd..552cf9d950a97d704443b60d4bede1e8ee227f6c 100644 (file)
@@ -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);
index f7b61766921370cc212f776ce1de5036da4bbad7..83aeef2751b6d0a2fcdd9fcc3e426cbcffd5f683 100644 (file)
@@ -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);
 
index eb4dfe80b501f561c7c1f87c0f1d17598d409531..93742fc6ac9fc627d6174fbffcb841c6589bd3ec 100644 (file)
@@ -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
index 5efc1445207017c2e4ec57414fadcb853087f13b..c5f4af24dc1434a53582e45aa2d06028bbc50a73 100644 (file)
@@ -57,6 +57,6 @@
  */
 
 /*                         yyyymmddN */
-#define CATALOG_VERSION_NO 202310141
+#define CATALOG_VERSION_NO 202310161
 
 #endif
index 0754ef1bce409aa060fee6b3090ffbeadd92e280..8d91e3bf8dae03fa17d82d5be68481865f5a5fea 100644 (file)
@@ -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_' },
index e9eb06b2e53f588f87c9f33f5a42bb6379e9789f..3e50a570046e3f6a4d00e864ceccb1e6dc563326 100644 (file)
@@ -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.
index 1c925dbf257007af75283b7585b278a0fba50449..9e3ece50d5f336a8743b21faa2f9f7187a245650 100644 (file)
@@ -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);
index c2f9de63a13707963842e2c339456604d446dece..7232b03e379e460b625d0277bc623451719507e5 100644 (file)
@@ -203,6 +203,8 @@ extern PGDLLIMPORT Oid MyDatabaseId;
 
 extern PGDLLIMPORT Oid MyDatabaseTableSpace;
 
+extern PGDLLIMPORT bool MyDatabaseHasLoginEventTriggers;
+
 /*
  * Date/Time Configuration
  *
index 4ee91e3cf93cd564566b1e267fdf5e0216409822..952ebe75cb4ae260f7cdc79226630c073a9c7382 100644 (file)
@@ -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);
 
index e738ac1c097327fb54a774e35e466edc1dec1df4..553a31874f134d6baf3b70550041eef0977af904 100644 (file)
@@ -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)
index d340026518ae9a3d7ac5953f57958927f7e5fd0a..52052e6252ab1b7ce6bf6a756ddf7addb52dbe83 100644 (file)
@@ -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 (file)
index 0000000..f317012
--- /dev/null
@@ -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();
index 0c72ba094415c5e2d8e3e1287eb814a2f5b4d634..95f9b0d7726fb5f0278d1ad0d3e7c5ac5d431e76 100644 (file)
@@ -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
index 0b87a42d0a9ac6ba17fd86ad926fc33001a22cc7..eaaff6ba6f1050dfae642fc178802e0bba6be997 100644 (file)
@@ -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)
+
index 6f0933b9e88aa53ef0d6eb10a1b0a8a4c5b03ba6..9c2b7903fbaecf650aa1ecdb64f7d0136d7946fe 100644 (file)
@@ -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';