mirror of
https://github.com/neondatabase/neon.git
synced 2025-12-22 21:59:59 +00:00
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 <dim@tapoueh.org> Co-authored-by: Konstantin Knizhnik <knizhnik@neon.tech>
This commit is contained in:
committed by
GitHub
parent
a4c76740c0
commit
dfa055f4be
@@ -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)
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
#ifndef CONTROL_PLANE_CONNECTOR_H
|
||||
#define CONTROL_PLANE_CONNECTOR_H
|
||||
|
||||
void InitControlPlaneConnector(void);
|
||||
|
||||
#endif
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 <curl/curl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#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 <utils/lsyscache.h>
|
||||
#include <utils/syscache.h>
|
||||
|
||||
#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",
|
||||
6
pgxn/neon/neon_ddl_handler.h
Normal file
6
pgxn/neon/neon_ddl_handler.h
Normal file
@@ -0,0 +1,6 @@
|
||||
#ifndef CONTROL_DDL_HANDLER_H
|
||||
#define CONTROL_DDL_HANDLER_H
|
||||
|
||||
void InitDDLHandler(void);
|
||||
|
||||
#endif
|
||||
@@ -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.
|
||||
|
||||
90
test_runner/sql_regress/expected/neon-event-triggers.out
Normal file
90
test_runner/sql_regress/expected/neon-event-triggers.out
Normal file
@@ -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;
|
||||
@@ -9,3 +9,4 @@ test: neon-rel-truncate
|
||||
test: neon-clog
|
||||
test: neon-test-utils
|
||||
test: neon-vacuum-full
|
||||
test: neon-event-triggers
|
||||
|
||||
96
test_runner/sql_regress/sql/neon-event-triggers.sql
Normal file
96
test_runner/sql_regress/sql/neon-event-triggers.sql
Normal file
@@ -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;
|
||||
Reference in New Issue
Block a user