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
+ login,
ddl_command_start,
ddl_command_end,
table_rewrite
@@ -35,6 +36,18 @@
Support for additional events may be added in future releases.
+
+ The login 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 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.
+
+
The ddl_command_start event occurs just before the
execution of a CREATE, ALTER, DROP,
@@ -1296,6 +1309,79 @@ $$;
CREATE EVENT TRIGGER no_rewrite_allowed
ON table_rewrite
EXECUTE FUNCTION no_rewrite();
+
+
+
+
+
+ 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);
+ 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();
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 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/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();