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();