Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Makefile
MODULES = ddl_guard
EXTENSION = ddl_guard
DATA = ddl_guard--1.0.1.sql
DATA = ddl_guard--1.0.1.sql ddl_guard--1.0.2.sql ddl_guard--1.0.1--1.0.2.sql

INPUTDIR ?= test/regular
TESTS = $(wildcard $(INPUTDIR)/sql/*.sql)
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ CREATE EXTENSION ddl_guard;
ALTER SYSTEM SET ddl_guard.enabled = on;
```

As of 1.0.2, DDL enforcement runs in the ProcessUtility hook and no event trigger is created. If you're upgrading from earlier versions, run:

```sql
ALTER EXTENSION ddl_guard UPDATE TO '1.0.2';
```

Note: `ddl_guard` creates the `ddl_guard` schema; ensure it does not already exist before running `CREATE EXTENSION`.

Now, only superusers can run DDL commands.
Expand Down
2 changes: 2 additions & 0 deletions ddl_guard--1.0.1--1.0.2.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- ddl_guard--1.0.1--1.0.2.sql
DROP EVENT TRIGGER IF EXISTS ddl_guard_trigger;
15 changes: 15 additions & 0 deletions ddl_guard--1.0.2.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-- ddl_guard--1.0.2.sql
CREATE SCHEMA ddl_guard;
CREATE TABLE ddl_guard.sentinel_log (
logged_at timestamptz NOT NULL DEFAULT now(),
operation text NOT NULL
);
GRANT USAGE ON SCHEMA ddl_guard TO PUBLIC;
GRANT SELECT ON ddl_guard.sentinel_log TO PUBLIC;

CREATE OR REPLACE FUNCTION ddl_guard_check()
RETURNS event_trigger
SECURITY INVOKER
SET search_path = 'pg_catalog, pg_temp'
LANGUAGE C
AS 'MODULE_PATHNAME', 'ddl_guard_check';
78 changes: 50 additions & 28 deletions ddl_guard.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ PG_MODULE_MAGIC;

void _PG_init(void);
void _PG_fini(void);
void log_sentinel_event(const char *volatile operation);
bool log_sentinel_event(const char *volatile operation);
Datum ddl_guard_check(PG_FUNCTION_ARGS);
static void lob_object_access_hook(ObjectAccessType access, Oid classId, Oid objectId, int subId, void *arg);
static bool set_lobject_funcs(void);
Expand All @@ -43,6 +43,7 @@ static void ddl_guard_process_utility(PlannedStmt *pstmt, const char *queryStrin
QueryEnvironment *queryEnv, DestReceiver *dest, QueryCompletion *qc);
#endif
static bool is_guarded_dcl_statement(Node *parsetree, CommandTag *commandTag);
static bool is_guarded_ddl_statement(Node *parsetree, CommandTag *commandTag);

static bool ddl_guard_enabled = false;
static bool ddl_guard_ddl_sentinel = false;
Expand Down Expand Up @@ -177,6 +178,22 @@ lookup_lobject_log_name(Oid funcid)
return NULL;
}

static bool
is_guarded_ddl_statement(Node *parsetree, CommandTag *commandTag)
Comment on lines +181 to +182

Copilot AI Jan 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function is_guarded_ddl_statement is missing a forward declaration. For consistency with other static functions like is_guarded_dcl_statement (declared at line 45), this function should be declared in the forward declarations section near the top of the file.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot re-review

{
CommandTag tag;

if (parsetree == NULL)
return false;

tag = CreateCommandTag(parsetree);
if (!command_tag_event_trigger_ok(tag))
return false;
if (commandTag != NULL)
*commandTag = tag;
return true;
}

static bool
is_guarded_dcl_statement(Node *parsetree, CommandTag *commandTag)
{
Expand All @@ -197,7 +214,7 @@ is_guarded_dcl_statement(Node *parsetree, CommandTag *commandTag)
case T_GrantStmt:
case T_AlterDefaultPrivilegesStmt:
tag = CreateCommandTag(parsetree);
/* Event-triggered commands are handled by ddl_guard_check. */
/* Event-triggered commands are handled by the ProcessUtility hook. */
if (command_tag_event_trigger_ok(tag))
return false;
if (commandTag != NULL)
Expand Down Expand Up @@ -243,7 +260,7 @@ get_sentinel_log_owner(void)
return relowner;
}

void
bool
log_sentinel_event(const char *volatile operation)
{
int ret;
Expand All @@ -258,9 +275,7 @@ log_sentinel_event(const char *volatile operation)

log_owner = get_sentinel_log_owner();
if (!OidIsValid(log_owner))
ereport(ERROR,
(errcode(ERRCODE_INTERNAL_ERROR),
errmsg("ddl_guard.sentinel_log not found")));
return false;

GetUserIdAndSecContext(&saved_userid, &saved_sec_context);
new_sec_context = saved_sec_context | SECURITY_LOCAL_USERID_CHANGE;
Expand Down Expand Up @@ -308,6 +323,7 @@ log_sentinel_event(const char *volatile operation)
PG_END_TRY();

SetUserIdAndSecContext(saved_userid, saved_sec_context);
return true;
}

static void
Expand Down Expand Up @@ -336,8 +352,10 @@ lob_object_access_hook(ObjectAccessType access, Oid classId, Oid objectId, int s
log_name = lookup_lobject_log_name(objectId);
if (log_name != NULL)
{
log_sentinel_event(log_name);
ereport(WARNING, errmsg("lo_guard: lobject \"%s\" function call, sentinel entry written", log_name));
if (log_sentinel_event(log_name))
ereport(WARNING, errmsg("lo_guard: lobject \"%s\" function call, sentinel entry written", log_name));
else
ereport(WARNING, errmsg("lo_guard: lobject \"%s\" function call, sentinel log unavailable", log_name));
}
break;
default:
Expand All @@ -361,13 +379,34 @@ ddl_guard_process_utility(PlannedStmt *pstmt, const char *queryString,
#endif
{
CommandTag commandTag;
bool guard_context;

if (ddl_guard_enabled && is_guarded_dcl_statement(pstmt->utilityStmt, &commandTag))
/* Skip internal subcommands to avoid duplicate sentinel entries. */
guard_context = (context != PROCESS_UTILITY_SUBCOMMAND);

if (guard_context && ddl_guard_enabled && is_guarded_ddl_statement(pstmt->utilityStmt, &commandTag))
{
if (ddl_guard_ddl_sentinel)
{
if (log_sentinel_event(GetCommandTagName(commandTag)))
ereport(WARNING, (errmsg("ddl_guard: ddl detected, sentinel entry written")));
else
ereport(WARNING, (errmsg("ddl_guard: ddl detected, sentinel log unavailable")));
}
else if (!superuser())
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("Non-superusers are not allowed to execute DDL statements"),
errhint("ddl_guard.enabled is set.")));
}
else if (guard_context && ddl_guard_enabled && is_guarded_dcl_statement(pstmt->utilityStmt, &commandTag))
{
if (ddl_guard_dcl_sentinel)
{
log_sentinel_event(GetCommandTagName(commandTag));
ereport(WARNING, (errmsg("ddl_guard: dcl detected, sentinel entry written")));
if (log_sentinel_event(GetCommandTagName(commandTag)))
ereport(WARNING, (errmsg("ddl_guard: dcl detected, sentinel entry written")));
else
ereport(WARNING, (errmsg("ddl_guard: dcl detected, sentinel log unavailable")));
}
else if (!superuser())
ereport(ERROR,
Expand Down Expand Up @@ -406,23 +445,6 @@ ddl_guard_check(PG_FUNCTION_ARGS)
(errcode(ERRCODE_INTERNAL_ERROR),
errmsg("ddl_guard_check: not fired by event trigger manager")));

if (ddl_guard_enabled)
{
if (ddl_guard_ddl_sentinel)
{
EventTriggerData *trigdata = (EventTriggerData *) fcinfo->context;

log_sentinel_event(GetCommandTagName(trigdata->tag));
ereport(WARNING, (errmsg("ddl_guard: ddl detected, sentinel entry written")));
PG_RETURN_VOID();
}
if (!superuser())
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("Non-superusers are not allowed to execute DDL statements"),
errhint("ddl_guard.enabled is set.")));
}

PG_RETURN_VOID();
}

Expand Down
2 changes: 1 addition & 1 deletion ddl_guard.control
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# ddl_guard.control
default_version = '1.0.1'
default_version = '1.0.2'
comment = 'Prevents DDL execution by non-superusers when a specific GUC is set'
relocatable = false
module_pathname = '$libdir/ddl_guard'
Expand Down
8 changes: 1 addition & 7 deletions test/regular/expected/sentinel.out
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
SELECT count(*) AS before_count FROM ddl_guard.sentinel_log;
before_count
--------------
149
(1 row)

\gset
SELECT count(*) AS before_count FROM ddl_guard.sentinel_log \gset
CREATE TABLE ddl_guard_sentinel_test (id int);
WARNING: ddl_guard: ddl detected, sentinel entry written
INSERT INTO ddl_guard.sentinel_log (operation) VALUES ('spoof');
Expand Down
3 changes: 1 addition & 2 deletions test/regular/sql/sentinel.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
SELECT count(*) AS before_count FROM ddl_guard.sentinel_log;
\gset
SELECT count(*) AS before_count FROM ddl_guard.sentinel_log \gset
CREATE TABLE ddl_guard_sentinel_test (id int);
INSERT INTO ddl_guard.sentinel_log (operation) VALUES ('spoof');
SELECT count(*) >= :before_count::int + 1 AS logged_after_create FROM ddl_guard.sentinel_log;
Expand Down