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 Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "block_copy_command"
version = "0.1.4"
version = "0.1.5"
edition = "2021"
description = "PostgreSQL extension that blocks COPY commands via a configurable ProcessUtility hook"
authors = ["RustWizard"]
Expand Down
4 changes: 2 additions & 2 deletions META.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
"name": "block_copy_command",
"abstract": "PostgreSQL extension that blocks COPY commands via a configurable ProcessUtility hook",
"description": "A PostgreSQL security extension that intercepts and blocks COPY commands using a ProcessUtility hook. Supports direction-specific blocking (COPY TO / COPY FROM), COPY PROGRAM blocking, per-role blocklists, superuser bypass, audit logging, and custom error hints.",
"version": "0.1.4",
"version": "0.1.5",
"maintainer": "RustWizard <rustwizard.0@gmail.com>",
"license": "bsd",
"provides": {
"block_copy_command": {
"abstract": "Block COPY commands via a configurable ProcessUtility hook",
"file": "block_copy_command.control",
"version": "0.1.4"
"version": "0.1.5"
}
},
"resources": {
Expand Down
73 changes: 69 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,14 +215,75 @@ ALTER ROLE alice SET block_copy_command.blocked_roles = 'alice';
ALTER ROLE alice RESET block_copy_command.blocked_roles;
```

### Audit logging
### GUC: `block_copy_command.audit_log_enabled`

When a COPY command is blocked, the current username is written to the PostgreSQL server log at `LOG` level:
Controls whether intercepted `COPY` events are written to `block_copy_command.audit_log`. Only superusers can change this setting.

| Value | Effect |
|-------|--------|
| `on` (default) | Every intercepted COPY is recorded in the audit table |
| `off` | No rows are written (server log is unaffected) |

```sql
-- Disable audit writes for a specific role:
ALTER ROLE etl_user SET block_copy_command.audit_log_enabled = off;
```

### Audit log table

Every intercepted `COPY` command (allowed and blocked) is recorded in `block_copy_command.audit_log`:

| Column | Type | Description |
|--------|------|-------------|
| `id` | `bigserial` | Auto-incrementing primary key |
| `ts` | `timestamptz` | Wall-clock time of the event (`clock_timestamp()`) |
| `session_user_name` | `text` | Role that authenticated (stable across `SET ROLE`) |
| `current_user_name` | `text` | Effective role at the time of COPY |
| `query_text` | `text` | Full query string |
| `copy_direction` | `text` | `'TO'` or `'FROM'` |
| `copy_is_program` | `bool` | `true` for `COPY … PROGRAM` statements |
| `client_addr` | `inet` | Client IP address (`NULL` for Unix-socket connections) |
| `application_name` | `text` | `application_name` GUC of the client session |
| `blocked` | `bool` | `true` if the command was blocked |
| `block_reason` | `text` | `NULL` when allowed; one of `role_listed`, `program_blocked`, `direction_blocked` when blocked |

> **Note on blocked events:** the audit row is inserted before the `ERROR` is raised, so it is rolled back when the transaction aborts. For blocked commands the server `LOG` line is the authoritative record; audit rows are reliable only for allowed commands.

**Example queries:**

```sql
-- All COPY events in the last hour
SELECT ts, current_user_name, copy_direction, blocked, block_reason
FROM block_copy_command.audit_log
WHERE ts > now() - interval '1 hour'
ORDER BY ts DESC;

-- Blocked events only
SELECT *
FROM block_copy_command.audit_log
WHERE blocked
ORDER BY ts DESC;

-- Activity per user
SELECT current_user_name, count(*) AS total,
count(*) FILTER (WHERE blocked) AS blocked_count
FROM block_copy_command.audit_log
GROUP BY current_user_name
ORDER BY total DESC;
```

The schema and table are locked down by default; grant `SELECT` to monitoring roles as needed:

```sql
GRANT USAGE ON SCHEMA block_copy_command TO monitoring_role;
GRANT SELECT ON block_copy_command.audit_log TO monitoring_role;
```
LOG: current_user = "someuser"

When a COPY command is blocked, the event is also written to the PostgreSQL server log at `LOG` level:

```
LOG: blocked COPY TO program=false user="someuser" reason="direction_blocked"
ERROR: COPY TO command is not allowed
HINT: Contact DBA to request access
```

## Testing
Expand All @@ -244,6 +305,10 @@ This builds the extension inside Docker, starts a PostgreSQL 17 instance with th
- GUC `block_copy_command.enabled` toggle
- GUC `block_copy_command.blocked_roles` blocks specific roles including superusers
- DDL, DML, and regular queries unaffected
- Audit log: allowed COPY creates a row; correct `copy_direction`, `copy_is_program`, `blocked`, `block_reason` values
- Audit log: `session_user_name` and `current_user_name` both recorded
- `audit_log_enabled=off` suppresses writes
- Blocked COPY does not persist in audit log (transaction rollback)

### With pgrx test runner

Expand Down
26 changes: 26 additions & 0 deletions sql/block_copy_command--0.1.4--0.1.5.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
-- Upgrade block_copy_command from 0.1.4 to 0.1.5
-- Adds the audit_log table and block_copy_command.audit_log_enabled GUC.

CREATE SCHEMA IF NOT EXISTS block_copy_command;

CREATE TABLE block_copy_command.audit_log (
id bigserial NOT NULL,
ts timestamptz NOT NULL DEFAULT clock_timestamp(),
session_user_name text NOT NULL,
current_user_name text NOT NULL,
query_text text NOT NULL,
copy_direction text NOT NULL,
copy_is_program bool NOT NULL DEFAULT false,
client_addr inet,
application_name text,
blocked bool NOT NULL,
block_reason text,
PRIMARY KEY (id)
);

CREATE INDEX ON block_copy_command.audit_log (ts);
CREATE INDEX ON block_copy_command.audit_log (current_user_name);
CREATE INDEX ON block_copy_command.audit_log (ts) WHERE blocked;

REVOKE ALL ON SCHEMA block_copy_command FROM PUBLIC;
REVOKE ALL ON block_copy_command.audit_log FROM PUBLIC;
45 changes: 44 additions & 1 deletion sql/hooks.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,47 @@
-- This extension registers a ProcessUtility hook in _PG_init to block COPY commands.
-- No SQL-level objects are created.
-- To activate for all connections, add to postgresql.conf:
-- shared_preload_libraries = 'block_copy_command'

CREATE SCHEMA IF NOT EXISTS block_copy_command;

-- Audit log for all intercepted COPY commands.
--
-- NOTE: for *blocked* commands the INSERT is performed before ERROR is raised,
-- so it will be rolled back when the current transaction aborts. The server log
-- (LOG: blocked COPY ...) is the authoritative record for blocked events.
-- Allowed COPY commands commit normally and their rows persist here.
CREATE TABLE block_copy_command.audit_log (
id bigserial NOT NULL,
-- clock_timestamp() captures the actual wall-clock time; now() would give
-- the transaction start time, which is the same row for every statement in
-- a multi-statement transaction.
ts timestamptz NOT NULL DEFAULT clock_timestamp(),
-- Both user fields are recorded because SET ROLE makes them diverge:
-- session_user_name is who actually authenticated, current_user_name is the
-- effective role at the time of the COPY.
session_user_name text NOT NULL,
current_user_name text NOT NULL,
query_text text NOT NULL,
copy_direction text NOT NULL, -- 'TO' | 'FROM'
copy_is_program bool NOT NULL DEFAULT false,
-- inet_client_addr() returns NULL for local (Unix-socket) connections.
client_addr inet,
application_name text,
blocked bool NOT NULL,
-- NULL when not blocked; one of: 'role_listed', 'program_blocked',
-- 'direction_blocked' when blocked.
block_reason text,
PRIMARY KEY (id)
);

-- Index for time-range queries and dashboards.
CREATE INDEX ON block_copy_command.audit_log (ts);
-- Index for per-user audits.
CREATE INDEX ON block_copy_command.audit_log (current_user_name);
-- Partial index: fast scan of blocked-only events (typically a small fraction).
CREATE INDEX ON block_copy_command.audit_log (ts) WHERE blocked;

-- Lock down the schema and table; superusers can explicitly grant SELECT to
-- monitoring roles as needed.
REVOKE ALL ON SCHEMA block_copy_command FROM PUBLIC;
REVOKE ALL ON block_copy_command.audit_log FROM PUBLIC;
Loading
Loading