Skip to content

Commit

Permalink
Add support event triggers on authenticated login
Browse files Browse the repository at this point in the history
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
  • Loading branch information
akorotkov committed Oct 16, 2023
1 parent c558e6f commit e83d1b0
Show file tree
Hide file tree
Showing 25 changed files with 644 additions and 21 deletions.
2 changes: 1 addition & 1 deletion doc/src/sgml/bki.sgml
Expand Up @@ -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_' },

Expand Down
13 changes: 13 additions & 0 deletions doc/src/sgml/catalogs.sgml
Expand Up @@ -3035,6 +3035,19 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
</para></entry>
</row>

<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>dathasloginevt</structfield> <type>bool</type>
</para>
<para>
Indicates that there are login event triggers defined for this database.
This flag is used to avoid extra lookups on the
<structname>pg_event_trigger</structname> table during each backend
startup. This flag is used internally by <productname>PostgreSQL</productname>
and should not be manually altered or read for monitoring purposes.
</para></entry>
</row>

<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>datconnlimit</structfield> <type>int4</type>
Expand Down
2 changes: 2 additions & 0 deletions doc/src/sgml/ecpg.sgml
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
94 changes: 94 additions & 0 deletions doc/src/sgml/event-trigger.sgml
Expand Up @@ -28,13 +28,32 @@
An event trigger fires whenever the event with which it is associated
occurs in the database in which it is defined. Currently, the only
supported events are
<literal>login</literal>,
<literal>ddl_command_start</literal>,
<literal>ddl_command_end</literal>,
<literal>table_rewrite</literal>
and <literal>sql_drop</literal>.
Support for additional events may be added in future releases.
</para>

<para>
The <literal>login</literal> event occurs when an authenticated user logs
into the system. Any bug in a trigger procedure for this event may
prevent successful login to the system. Such bugs may be fixed by
setting <xref linkend="guc-event-triggers"/> is set to <literal>false</literal>
either in a connection string or configuration file. Alternative is
restarting the system in single-user mode (as event triggers are
disabled in this mode). See the <xref linkend="app-postgres"/> reference
page for details about using single-user mode.
The <literal>login</literal> event will also fire on standby servers.
To prevent servers from becoming inaccessible, such triggers must avoid
writing anything to the database when running on a standby.
Also, it's recommended to avoid long-running queries in
<literal>login</literal> event triggers. Notes that, for instance,
cancelling connection in <application>psql</application> wouldn't cancel
the in-progress <literal>login</literal> trigger.
</para>

<para>
The <literal>ddl_command_start</literal> event occurs just before the
execution of a <literal>CREATE</literal>, <literal>ALTER</literal>, <literal>DROP</literal>,
Expand Down Expand Up @@ -1300,4 +1319,79 @@ CREATE EVENT TRIGGER no_rewrite_allowed
</programlisting>
</para>
</sect1>

<sect1 id="event-trigger-database-login-example">
<title>A Database Login Event Trigger Example</title>

<para>
The event trigger on the <literal>login</literal> event can be
useful for logging user logins, for verifying the connection and
assigning roles according to current circumstances, or for session
data initialization. It is very important that any event trigger using
the <literal>login</literal> event checks whether or not the database is
in recovery before performing any writes. Writing to a standby server
will make it inaccessible.
</para>

<para>
The following example demonstrates these options.
<programlisting>
-- create test tables and roles
CREATE TABLE user_login_log (
"user" text,
"session_start" timestamp with time zone
);
CREATE ROLE day_worker;
CREATE ROLE night_worker;

-- the example trigger function
CREATE OR REPLACE FUNCTION init_session()
RETURNS event_trigger SECURITY DEFINER
LANGUAGE plpgsql AS
$$
DECLARE
hour integer = EXTRACT('hour' FROM current_time at time zone 'utc');
rec boolean;
BEGIN
-- 1. Forbid logging in between 2AM and 4AM.
IF hour BETWEEN 2 AND 4 THEN
RAISE EXCEPTION 'Login forbidden';
END IF;

-- The checks below cannot be performed on standby servers so
-- ensure the database is not in recovery before we perform any
-- operations.
SELECT pg_is_in_recovery() INTO rec;
IF rec THEN
RETURN;
END IF;

-- 2. Assign some roles. At daytime, grant the day_worker role, else the
-- night_worker role.
IF hour BETWEEN 8 AND 20 THEN
EXECUTE 'REVOKE night_worker FROM ' || quote_ident(session_user);
EXECUTE 'GRANT day_worker TO ' || quote_ident(session_user);
ELSE
EXECUTE 'REVOKE day_worker FROM ' || quote_ident(session_user);
EXECUTE 'GRANT night_worker TO ' || quote_ident(session_user);
END IF;

-- 3. Initialize user session data
CREATE TEMP TABLE session_storage (x float, y integer);
ALTER TABLE session_storage OWNER TO session_user;

-- 4. Log the connection time
INSERT INTO public.user_login_log VALUES (session_user, current_timestamp);

END;
$$;

-- trigger definition
CREATE EVENT TRIGGER init_session
ON login
EXECUTE FUNCTION init_session();
ALTER EVENT TRIGGER init_session ENABLE ALWAYS;
</programlisting>
</para>
</sect1>
</chapter>
17 changes: 11 additions & 6 deletions src/backend/commands/dbcommands.c
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit e83d1b0

Please sign in to comment.