From dfa055f4be7042fa8e49a86c31bffef29dad6c48 Mon Sep 17 00:00:00 2001 From: Konstantin Knizhnik Date: Tue, 17 Jun 2025 18:44:50 +0300 Subject: [PATCH] Support event trigger for Neon users (#10624) ## Problem https://github.com/neondatabase/neon/issues/7570 Even triggers are supported only for superusers. ## Summary of changes Temporary switch to superuser when even trigger is created and disable execution of user's even triggers under superuser. --------- Co-authored-by: Dimitri Fontaine Co-authored-by: Konstantin Knizhnik --- pgxn/neon/Makefile | 2 +- pgxn/neon/control_plane_connector.h | 6 - pgxn/neon/neon.c | 4 +- ...l_plane_connector.c => neon_ddl_handler.c} | 576 +++++++++++++++++- pgxn/neon/neon_ddl_handler.h | 6 + test_runner/regress/test_pg_regress.py | 8 +- .../expected/neon-event-triggers.out | 90 +++ test_runner/sql_regress/parallel_schedule | 1 + .../sql_regress/sql/neon-event-triggers.sql | 96 +++ 9 files changed, 754 insertions(+), 35 deletions(-) delete mode 100644 pgxn/neon/control_plane_connector.h rename pgxn/neon/{control_plane_connector.c => neon_ddl_handler.c} (57%) create mode 100644 pgxn/neon/neon_ddl_handler.h create mode 100644 test_runner/sql_regress/expected/neon-event-triggers.out create mode 100644 test_runner/sql_regress/sql/neon-event-triggers.sql diff --git a/pgxn/neon/Makefile b/pgxn/neon/Makefile index 8bcc6bf924..9bce0e798a 100644 --- a/pgxn/neon/Makefile +++ b/pgxn/neon/Makefile @@ -21,7 +21,7 @@ OBJS = \ unstable_extensions.o \ walproposer.o \ walproposer_pg.o \ - control_plane_connector.o \ + neon_ddl_handler.o \ walsender_hooks.o PG_CPPFLAGS = -I$(libpq_srcdir) diff --git a/pgxn/neon/control_plane_connector.h b/pgxn/neon/control_plane_connector.h deleted file mode 100644 index 7eed449200..0000000000 --- a/pgxn/neon/control_plane_connector.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef CONTROL_PLANE_CONNECTOR_H -#define CONTROL_PLANE_CONNECTOR_H - -void InitControlPlaneConnector(void); - -#endif diff --git a/pgxn/neon/neon.c b/pgxn/neon/neon.c index 5b4ced7cf0..8a405f4129 100644 --- a/pgxn/neon/neon.c +++ b/pgxn/neon/neon.c @@ -33,9 +33,9 @@ #include "extension_server.h" #include "file_cache.h" #include "neon.h" +#include "neon_ddl_handler.h" #include "neon_lwlsncache.h" #include "neon_perf_counters.h" -#include "control_plane_connector.h" #include "logical_replication_monitor.h" #include "unstable_extensions.h" #include "walsender_hooks.h" @@ -454,7 +454,7 @@ _PG_init(void) InitUnstableExtensionsSupport(); InitLogicalReplicationMonitor(); - InitControlPlaneConnector(); + InitDDLHandler(); pg_init_extension_server(); diff --git a/pgxn/neon/control_plane_connector.c b/pgxn/neon/neon_ddl_handler.c similarity index 57% rename from pgxn/neon/control_plane_connector.c rename to pgxn/neon/neon_ddl_handler.c index 47ed37da06..dba28c0ed6 100644 --- a/pgxn/neon/control_plane_connector.c +++ b/pgxn/neon/neon_ddl_handler.c @@ -1,6 +1,6 @@ /*------------------------------------------------------------------------- * - * control_plane_connector.c + * neon_ddl_handler.c * Captures updates to roles/databases using ProcessUtility_hook and * sends them to the control ProcessUtility_hook. The changes are sent * via HTTP to the URL specified by the GUC neon.console_url when the @@ -13,18 +13,30 @@ * accumulate changes. On subtransaction commit, the top of the stack * is merged with the table below it. * + * Support event triggers for neon_superuser + * + * IDENTIFICATION + * contrib/neon/neon_dll_handler.c + * *------------------------------------------------------------------------- */ #include "postgres.h" #include +#include #include "access/xact.h" +#include "catalog/pg_authid.h" +#include "catalog/pg_proc.h" #include "commands/defrem.h" +#include "commands/event_trigger.h" +#include "commands/user.h" #include "fmgr.h" #include "libpq/crypt.h" #include "miscadmin.h" +#include "nodes/makefuncs.h" +#include "parser/parse_func.h" #include "tcop/pquery.h" #include "tcop/utility.h" #include "utils/acl.h" @@ -32,11 +44,16 @@ #include "utils/hsearch.h" #include "utils/memutils.h" #include "utils/jsonb.h" +#include +#include -#include "control_plane_connector.h" +#include "neon_ddl_handler.h" #include "neon_utils.h" static ProcessUtility_hook_type PreviousProcessUtilityHook = NULL; +static fmgr_hook_type next_fmgr_hook = NULL; +static needs_fmgr_hook_type next_needs_fmgr_hook = NULL; +static bool neon_event_triggers = true; static const char *jwt_token = NULL; @@ -773,6 +790,7 @@ HandleDropRole(DropRoleStmt *stmt) } } + static void HandleRename(RenameStmt *stmt) { @@ -782,6 +800,460 @@ HandleRename(RenameStmt *stmt) return HandleRoleRename(stmt); } + +/* + * Support for Event Triggers. + * + * In vanilla only superuser can create Event Triggers. + * + * We allow it for neon_superuser by temporary switching to superuser. But as + * far as event trigger can fire in superuser context we should protect + * superuser from execution of arbitrary user's code. + * + * The idea was taken from Supabase PR series starting at + * https://github.com/supabase/supautils/pull/98 + */ + +static bool +neon_needs_fmgr_hook(Oid functionId) { + + return (next_needs_fmgr_hook && (*next_needs_fmgr_hook) (functionId)) + || get_func_rettype(functionId) == EVENT_TRIGGEROID; +} + +static void +LookupFuncOwnerSecDef(Oid functionId, Oid *funcOwner, bool *is_secdef) +{ + Form_pg_proc procForm; + HeapTuple proc_tup = SearchSysCache1(PROCOID, ObjectIdGetDatum(functionId)); + + if (!HeapTupleIsValid(proc_tup)) + ereport(ERROR, + (errmsg("cache lookup failed for function %u", functionId))); + + procForm = (Form_pg_proc) GETSTRUCT(proc_tup); + + *funcOwner = procForm->proowner; + *is_secdef = procForm->prosecdef; + + ReleaseSysCache(proc_tup); +} + + +PG_FUNCTION_INFO_V1(noop); +Datum noop(__attribute__ ((unused)) PG_FUNCTION_ARGS) { PG_RETURN_VOID();} + +static void +force_noop(FmgrInfo *finfo) +{ + finfo->fn_addr = (PGFunction) noop; + finfo->fn_oid = InvalidOid; /* not a known function OID anymore */ + finfo->fn_nargs = 0; /* no arguments for noop */ + finfo->fn_strict = false; + finfo->fn_retset = false; + finfo->fn_stats = 0; /* no stats collection */ + finfo->fn_extra = NULL; /* clear out old context data */ + finfo->fn_mcxt = CurrentMemoryContext; + finfo->fn_expr = NULL; /* no parse tree */ +} + + +/* + * Skip executing Event Triggers execution for superusers, because Event + * Triggers are SECURITY DEFINER and user provided code could then attempt + * privilege escalation. + * + * Also skip executing Event Triggers when GUC neon.event_triggers has been + * set to false. This might be necessary to be able to connect again after a + * LOGIN Event Trigger has been installed that would prevent connections as + * neon_superuser. + */ +static void +neon_fmgr_hook(FmgrHookEventType event, FmgrInfo *flinfo, Datum *private) +{ + /* + * It can be other needs_fmgr_hook which cause our hook to be invoked for + * non-trigger function, so recheck that is is trigger function. + */ + if (flinfo->fn_oid != InvalidOid && + get_func_rettype(flinfo->fn_oid) != EVENT_TRIGGEROID) + { + if (next_fmgr_hook) + (*next_fmgr_hook) (event, flinfo, private); + + return; + } + + /* + * The neon_superuser role can use the GUC neon.event_triggers to disable + * firing Event Trigger. + * + * SET neon.event_triggers TO false; + * + * This only applies to the neon_superuser role though, and only allows + * skipping Event Triggers owned by neon_superuser, which we check by + * proxy of the Event Trigger function being owned by neon_superuser. + * + * A role that is created in role neon_superuser should be allowed to also + * benefit from the neon_event_triggers GUC, and will be considered the + * same as the neon_superuser role. + */ + if (event == FHET_START + && !neon_event_triggers + && is_neon_superuser()) + { + Oid neon_superuser_oid = get_role_oid("neon_superuser", false); + + /* Find the Function Attributes (owner Oid, security definer) */ + const char *fun_owner_name = NULL; + Oid fun_owner = InvalidOid; + bool fun_is_secdef = false; + + LookupFuncOwnerSecDef(flinfo->fn_oid, &fun_owner, &fun_is_secdef); + fun_owner_name = GetUserNameFromId(fun_owner, false); + + if (RoleIsNeonSuperuser(fun_owner_name) + || has_privs_of_role(fun_owner, neon_superuser_oid)) + { + elog(WARNING, + "Skipping Event Trigger: neon.event_triggers is false"); + + /* + * we can't skip execution directly inside the fmgr_hook so instead we + * change the event trigger function to a noop function. + */ + force_noop(flinfo); + } + } + + /* + * Fire Event Trigger if both function owner and current user are + * superuser, or none of them are. + */ + else if (event == FHET_START + /* still enable it to pass pg_regress tests */ + && !RegressTestMode) + { + /* + * Get the current user oid as of before SECURITY DEFINER change of + * CurrentUserId, and that would be SessionUserId. + */ + Oid current_role_oid = GetSessionUserId(); + bool role_is_super = superuser_arg(current_role_oid); + + /* Find the Function Attributes (owner Oid, security definer) */ + Oid function_owner = InvalidOid; + bool function_is_secdef = false; + bool function_is_owned_by_super = false; + + LookupFuncOwnerSecDef(flinfo->fn_oid, &function_owner, &function_is_secdef); + + function_is_owned_by_super = superuser_arg(function_owner); + + /* + * 1. Refuse to run SECURITY DEFINER function that belongs to a + * superuser when the current user is not a superuser itself. + */ + if (!role_is_super + && function_is_owned_by_super + && function_is_secdef) + { + char *func_name = get_func_name(flinfo->fn_oid); + + ereport(WARNING, + (errmsg("Skipping Event Trigger"), + errdetail("Event Trigger function \"%s\" is owned by \"%s\" " + "and is SECURITY DEFINER", + func_name, + GetUserNameFromId(function_owner, false)))); + + /* + * we can't skip execution directly inside the fmgr_hook so + * instead we change the event trigger function to a noop + * function. + */ + force_noop(flinfo); + } + + /* + * 2. Refuse to run functions that belongs to a non-superuser when the + * current user is a superuser. + * + * We could run a SECURITY DEFINER user-function here and be safe with + * privilege escalation risks, but superuser roles are only used for + * infrastructure maintenance operations, where we prefer to skip + * running user-defined code. + */ + else if (role_is_super && !function_is_owned_by_super) + { + char *func_name = get_func_name(flinfo->fn_oid); + + ereport(WARNING, + (errmsg("Skipping Event Trigger"), + errdetail("Event Trigger function \"%s\" " + "is owned by non-superuser role \"%s\", " + "and current_user \"%s\" is superuser", + func_name, + GetUserNameFromId(function_owner, false), + GetUserNameFromId(current_role_oid, false)))); + + /* + * we can't skip execution directly inside the fmgr_hook so + * instead we change the event trigger function to a noop + * function. + */ + force_noop(flinfo); + } + + } + + if (next_fmgr_hook) + (*next_fmgr_hook) (event, flinfo, private); +} + +static Oid prev_role_oid = 0; +static int prev_role_sec_context = 0; +static bool switched_to_superuser = false; + +/* + * Switch tp superuser if not yet superuser. + * Returns false if already switched to superuser. + */ +static bool +switch_to_superuser(void) +{ + Oid superuser_oid; + + if (switched_to_superuser) + return false; + switched_to_superuser = true; + + superuser_oid = get_role_oid("cloud_admin", true /*missing_ok*/); + if (superuser_oid == InvalidOid) + superuser_oid = BOOTSTRAP_SUPERUSERID; + + GetUserIdAndSecContext(&prev_role_oid, &prev_role_sec_context); + SetUserIdAndSecContext(superuser_oid, prev_role_sec_context | + SECURITY_LOCAL_USERID_CHANGE | + SECURITY_RESTRICTED_OPERATION); + return true; +} + +static void +switch_to_original_role(void) +{ + SetUserIdAndSecContext(prev_role_oid, prev_role_sec_context); + switched_to_superuser = false; +} + +/* + * ALTER ROLE ... SUPERUSER; + * + * Used internally to give superuser to a non-privileged role to allow + * ownership of superuser-only objects such as Event Trigger. + * + * ALTER ROLE foo SUPERUSER; + * ALTER EVENT TRIGGER ... OWNED BY foo; + * ALTER ROLE foo NOSUPERUSER; + * + * Now the EVENT TRIGGER is owned by foo, who can DROP it without having to be + * superuser again. + */ +static void +alter_role_super(const char* rolename, bool make_super) +{ + AlterRoleStmt *alter_stmt = makeNode(AlterRoleStmt); + + DefElem *defel_superuser = +#if PG_MAJORVERSION_NUM <= 14 + makeDefElem("superuser", (Node *) makeInteger(make_super), -1); +#else + makeDefElem("superuser", (Node *) makeBoolean(make_super), -1); +#endif + + RoleSpec *rolespec = makeNode(RoleSpec); + rolespec->roletype = ROLESPEC_CSTRING; + rolespec->rolename = pstrdup(rolename); + rolespec->location = -1; + + alter_stmt->role = rolespec; + alter_stmt->options = list_make1(defel_superuser); + +#if PG_MAJORVERSION_NUM < 15 + AlterRole(alter_stmt); +#else + /* ParseState *pstate, AlterRoleStmt *stmt */ + AlterRole(NULL, alter_stmt); +#endif + + CommandCounterIncrement(); +} + + +/* + * Changes the OWNER of an Event Trigger. + * + * Event Triggers can only be owned by superusers, so this ALTER ROLE with + * SUPERUSER and then removes the property. + */ +static void +alter_event_trigger_owner(const char *obj_name, Oid role_oid) +{ + char* role_name = GetUserNameFromId(role_oid, false); + + alter_role_super(role_name, true); + + AlterEventTriggerOwner(obj_name, role_oid); + CommandCounterIncrement(); + + alter_role_super(role_name, false); +} + + +/* + * Neon processing of the CREATE EVENT TRIGGER requires special attention and + * is worth having its own ProcessUtility_hook for that. + */ +static void +ProcessCreateEventTrigger( + PlannedStmt *pstmt, + const char *queryString, + bool readOnlyTree, + ProcessUtilityContext context, + ParamListInfo params, + QueryEnvironment *queryEnv, + DestReceiver *dest, + QueryCompletion *qc) +{ + Node *parseTree = pstmt->utilityStmt; + bool sudo = false; + + /* We double-check that after local variable declaration block */ + CreateEventTrigStmt *stmt = (CreateEventTrigStmt *) parseTree; + + /* + * We are going to change the current user privileges (sudo) and might + * need after execution cleanup. For that we want to capture the UserId + * before changing it for our sudo implementation. + */ + const Oid current_user_id = GetUserId(); + bool current_user_is_super = superuser_arg(current_user_id); + + if (nodeTag(parseTree) != T_CreateEventTrigStmt) + { + ereport(ERROR, + errcode(ERRCODE_INTERNAL_ERROR), + errmsg("ProcessCreateEventTrigger called for the wrong command")); + } + + /* + * Allow neon_superuser to create Event Trigger, while keeping the + * ownership of the object. + * + * For that we give superuser membership to the role for the execution of + * the command. + */ + if (IsTransactionState() && is_neon_superuser()) + { + /* Find the Event Trigger function Oid */ + Oid func_oid = LookupFuncName(stmt->funcname, 0, NULL, false); + + /* Find the Function Owner Oid */ + Oid func_owner = InvalidOid; + bool is_secdef = false; + bool function_is_owned_by_super = false; + + LookupFuncOwnerSecDef(func_oid, &func_owner, &is_secdef); + + function_is_owned_by_super = superuser_arg(func_owner); + + if(!current_user_is_super && function_is_owned_by_super) + { + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("Permission denied to execute " + "a function owned by a superuser role"), + errdetail("current user \"%s\" is not a superuser " + "and Event Trigger function \"%s\" " + "is owned by a superuser", + GetUserNameFromId(current_user_id, false), + NameListToString(stmt->funcname)))); + } + + if(current_user_is_super && !function_is_owned_by_super) + { + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("Permission denied to execute " + "a function owned by a non-superuser role"), + errdetail("current user \"%s\" is a superuser " + "and function \"%s\" is " + "owned by a non-superuser", + GetUserNameFromId(current_user_id, false), + NameListToString(stmt->funcname)))); + } + + sudo = switch_to_superuser(); + } + + PG_TRY(); + { + if (PreviousProcessUtilityHook) + { + PreviousProcessUtilityHook( + pstmt, + queryString, + readOnlyTree, + context, + params, + queryEnv, + dest, + qc); + } + else + { + standard_ProcessUtility( + pstmt, + queryString, + readOnlyTree, + context, + params, + queryEnv, + dest, + qc); + } + + /* + * Now that the Event Trigger has been installed via our sudo + * mechanism, if the original role was not a superuser then change + * the event trigger ownership back to the original role. + * + * That way [ ALTER | DROP ] EVENT TRIGGER commands just work. + */ + if (IsTransactionState() && is_neon_superuser()) + { + if (!current_user_is_super) + { + /* + * Change event trigger owner to the current role (making + * it a privileged role during the ALTER OWNER command). + */ + alter_event_trigger_owner(stmt->trigname, current_user_id); + } + } + } + PG_FINALLY(); + { + if (sudo) + switch_to_original_role(); + } + PG_END_TRY(); +} + + +/* + * Neon hooks for DDLs (handling privileges, limiting features, etc). + */ static void NeonProcessUtility( PlannedStmt *pstmt, @@ -795,6 +1267,27 @@ NeonProcessUtility( { Node *parseTree = pstmt->utilityStmt; + /* + * The process utility hook for CREATE EVENT TRIGGER is its own + * implementation and warrant being addressed separately from here. + */ + if (nodeTag(parseTree) == T_CreateEventTrigStmt) + { + ProcessCreateEventTrigger( + pstmt, + queryString, + readOnlyTree, + context, + params, + queryEnv, + dest, + qc); + return; + } + + /* + * Other commands that need Neon specific implementations are handled here: + */ switch (nodeTag(parseTree)) { case T_CreatedbStmt: @@ -833,37 +1326,82 @@ NeonProcessUtility( if (PreviousProcessUtilityHook) { PreviousProcessUtilityHook( - pstmt, - queryString, - readOnlyTree, - context, - params, - queryEnv, - dest, - qc); + pstmt, + queryString, + readOnlyTree, + context, + params, + queryEnv, + dest, + qc); } else { standard_ProcessUtility( - pstmt, - queryString, - readOnlyTree, - context, - params, - queryEnv, - dest, - qc); + pstmt, + queryString, + readOnlyTree, + context, + params, + queryEnv, + dest, + qc); } } +/* + * Only neon_superuser is granted privilege to edit neon.event_triggers GUC. + */ +static void +neon_event_triggers_assign_hook(bool newval, void *extra) +{ + /* MyDatabaseId == InvalidOid || !OidIsValid(GetUserId()) */ + + if (IsTransactionState() && !is_neon_superuser()) + { + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied to set neon.event_triggers"), + errdetail("Only \"neon_superuser\" is allowed to set the GUC"))); + } +} + + void -InitControlPlaneConnector() +InitDDLHandler() { PreviousProcessUtilityHook = ProcessUtility_hook; ProcessUtility_hook = NeonProcessUtility; + + next_needs_fmgr_hook = needs_fmgr_hook; + needs_fmgr_hook = neon_needs_fmgr_hook; + + next_fmgr_hook = fmgr_hook; + fmgr_hook = neon_fmgr_hook; + RegisterXactCallback(NeonXactCallback, NULL); RegisterSubXactCallback(NeonSubXactCallback, NULL); + /* + * The GUC neon.event_triggers should provide the same effect as the + * Postgres GUC event_triggers, but the neon one is PGC_USERSET. + * + * This allows using the GUC in the connection string and work out of a + * LOGIN Event Trigger that would break database access, all without + * having to edit and reload the Postgres configuration file. + */ + DefineCustomBoolVariable( + "neon.event_triggers", + "Enable firing of event triggers", + NULL, + &neon_event_triggers, + true, + PGC_USERSET, + 0, + NULL, + neon_event_triggers_assign_hook, + NULL); + DefineCustomStringVariable( "neon.console_url", "URL of the Neon Console, which will be forwarded changes to dbs and roles", diff --git a/pgxn/neon/neon_ddl_handler.h b/pgxn/neon/neon_ddl_handler.h new file mode 100644 index 0000000000..de18ed3d82 --- /dev/null +++ b/pgxn/neon/neon_ddl_handler.h @@ -0,0 +1,6 @@ +#ifndef CONTROL_DDL_HANDLER_H +#define CONTROL_DDL_HANDLER_H + +void InitDDLHandler(void); + +#endif diff --git a/test_runner/regress/test_pg_regress.py b/test_runner/regress/test_pg_regress.py index 474002353b..3695ece66b 100644 --- a/test_runner/regress/test_pg_regress.py +++ b/test_runner/regress/test_pg_regress.py @@ -306,13 +306,7 @@ def test_sql_regress( ) # Connect to postgres and create a database called "regression". - endpoint = env.endpoints.create_start( - "main", - config_lines=[ - # Enable the test mode, so that we don't need to patch the test cases. - "neon.regress_test_mode = true", - ], - ) + endpoint = env.endpoints.create_start("main") endpoint.safe_psql(f"CREATE DATABASE {DBNAME}") # Create some local directories for pg_regress to run in. diff --git a/test_runner/sql_regress/expected/neon-event-triggers.out b/test_runner/sql_regress/expected/neon-event-triggers.out new file mode 100644 index 0000000000..3a62e67316 --- /dev/null +++ b/test_runner/sql_regress/expected/neon-event-triggers.out @@ -0,0 +1,90 @@ +create or replace function admin_proc() + returns event_trigger + language plpgsql as +$$ +begin + raise notice 'admin event trigger is executed for %', current_user; +end; +$$; +create role neon_superuser; +create role neon_admin login inherit createrole createdb in role neon_superuser; +grant create on schema public to neon_admin; +create database neondb with owner neon_admin; +grant all privileges on database neondb to neon_superuser; +create role neon_user; +grant create on schema public to neon_user; +create event trigger on_ddl1 on ddl_command_end +execute procedure admin_proc(); +set role neon_user; +-- check that non-privileged user can not change neon.event_triggers +set neon.event_triggers to false; +ERROR: permission denied to set neon.event_triggers +DETAIL: Only "neon_superuser" is allowed to set the GUC +-- Non-privileged neon user should not be able to create event trigers +create event trigger on_ddl2 on ddl_command_end +execute procedure admin_proc(); +ERROR: permission denied to create event trigger "on_ddl2" +HINT: Must be superuser to create an event trigger. +set role neon_admin; +-- neon_superuser should be able to create event trigers +create or replace function neon_proc() + returns event_trigger + language plpgsql as +$$ +begin + raise notice 'neon event trigger is executed for %', current_user; +end; +$$; +NOTICE: admin event trigger is executed for neon_admin +create event trigger on_ddl2 on ddl_command_end +execute procedure neon_proc(); +\c neondb neon_admin +create or replace function neondb_proc() + returns event_trigger + language plpgsql as +$$ +begin + raise notice 'neondb event trigger is executed for %', current_user; +end; +$$; +create or replace function neondb_secdef_proc() + returns event_trigger + language plpgsql + SECURITY DEFINER +as +$$ +begin + raise notice 'neondb secdef event trigger is executed for %', current_user; +end; +$$; +-- neon_admin (neon_superuser member) should be able to create event triggers +create event trigger on_ddl3 on ddl_command_end +execute procedure neondb_proc(); +create event trigger on_ddl4 on ddl_command_end +execute procedure neondb_secdef_proc(); +-- Check that event trigger is fired for neon_admin +create table t1(x integer); +NOTICE: neondb event trigger is executed for neon_admin +NOTICE: neondb secdef event trigger is executed for neon_admin +-- Check that event trigger can be skipped +set neon.event_triggers to false; +create table t2(x integer); +WARNING: Skipping Event Trigger: neon.event_triggers is false +WARNING: Skipping Event Trigger: neon.event_triggers is false +\c regression cloud_admin +-- Check that event triggers are not fired for superuser +create table t3(x integer); +NOTICE: admin event trigger is executed for cloud_admin +WARNING: Skipping Event Trigger +DETAIL: Event Trigger function "neon_proc" is owned by non-superuser role "neon_admin", and current_user "cloud_admin" is superuser +\c neondb cloud_admin +-- Check that user-defined event triggers are not fired for superuser +create table t4(x integer); +WARNING: Skipping Event Trigger +DETAIL: Event Trigger function "neondb_proc" is owned by non-superuser role "neon_admin", and current_user "cloud_admin" is superuser +WARNING: Skipping Event Trigger +DETAIL: Event Trigger function "neondb_secdef_proc" is owned by non-superuser role "neon_admin", and current_user "cloud_admin" is superuser +\c neondb neon_admin +-- Check that neon_admin can drop event triggers +drop event trigger on_ddl3; +drop event trigger on_ddl4; diff --git a/test_runner/sql_regress/parallel_schedule b/test_runner/sql_regress/parallel_schedule index d9508d1c90..d1bd7226ed 100644 --- a/test_runner/sql_regress/parallel_schedule +++ b/test_runner/sql_regress/parallel_schedule @@ -9,3 +9,4 @@ test: neon-rel-truncate test: neon-clog test: neon-test-utils test: neon-vacuum-full +test: neon-event-triggers diff --git a/test_runner/sql_regress/sql/neon-event-triggers.sql b/test_runner/sql_regress/sql/neon-event-triggers.sql new file mode 100644 index 0000000000..75365455dc --- /dev/null +++ b/test_runner/sql_regress/sql/neon-event-triggers.sql @@ -0,0 +1,96 @@ +create or replace function admin_proc() + returns event_trigger + language plpgsql as +$$ +begin + raise notice 'admin event trigger is executed for %', current_user; +end; +$$; + +create role neon_superuser; +create role neon_admin login inherit createrole createdb in role neon_superuser; +grant create on schema public to neon_admin; +create database neondb with owner neon_admin; +grant all privileges on database neondb to neon_superuser; + +create role neon_user; +grant create on schema public to neon_user; + +create event trigger on_ddl1 on ddl_command_end +execute procedure admin_proc(); + +set role neon_user; + +-- check that non-privileged user can not change neon.event_triggers +set neon.event_triggers to false; + +-- Non-privileged neon user should not be able to create event trigers +create event trigger on_ddl2 on ddl_command_end +execute procedure admin_proc(); + +set role neon_admin; + +-- neon_superuser should be able to create event trigers +create or replace function neon_proc() + returns event_trigger + language plpgsql as +$$ +begin + raise notice 'neon event trigger is executed for %', current_user; +end; +$$; + +create event trigger on_ddl2 on ddl_command_end +execute procedure neon_proc(); + +\c neondb neon_admin + +create or replace function neondb_proc() + returns event_trigger + language plpgsql as +$$ +begin + raise notice 'neondb event trigger is executed for %', current_user; +end; +$$; + +create or replace function neondb_secdef_proc() + returns event_trigger + language plpgsql + SECURITY DEFINER +as +$$ +begin + raise notice 'neondb secdef event trigger is executed for %', current_user; +end; +$$; + +-- neon_admin (neon_superuser member) should be able to create event triggers +create event trigger on_ddl3 on ddl_command_end +execute procedure neondb_proc(); + +create event trigger on_ddl4 on ddl_command_end +execute procedure neondb_secdef_proc(); + +-- Check that event trigger is fired for neon_admin +create table t1(x integer); + +-- Check that event trigger can be skipped +set neon.event_triggers to false; +create table t2(x integer); + +\c regression cloud_admin + +-- Check that event triggers are not fired for superuser +create table t3(x integer); + +\c neondb cloud_admin + +-- Check that user-defined event triggers are not fired for superuser +create table t4(x integer); + +\c neondb neon_admin + +-- Check that neon_admin can drop event triggers +drop event trigger on_ddl3; +drop event trigger on_ddl4;