From ac52bb0442f79076b14acd8ad5b696946c1053b8 Mon Sep 17 00:00:00 2001 From: Stephen Frost Date: Thu, 14 May 2015 10:36:16 -0400 Subject: [PATCH] Add pg_audit, an auditing extension MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This extension provides detailed logging classes, ability to control logging at a per-object level, and includes fully-qualified object names for logged statements (DML and DDL) in independent fields of the log output. Authors: Ian Barwick, Abhijit Menon-Sen, David Steele Reviews by: Robert Haas, Tatsuo Ishii, Sawada Masahiko, Fujii Masao, Simon Riggs Discussion with: Josh Berkus, Jaime Casanova, Peter Eisentraut, David Fetter, Yeb Havinga, Alvaro Herrera, Petr Jelinek, Tom Lane, MauMau, Bruce Momjian, Jim Nasby, Michael Paquier, Fabrízio de Royes Mello, Neil Tiffin --- contrib/Makefile | 1 + contrib/pg_audit/.gitignore | 5 + contrib/pg_audit/Makefile | 21 + contrib/pg_audit/expected/pg_audit.out | 964 ++++++++++++ contrib/pg_audit/pg_audit--1.0.0.sql | 22 + contrib/pg_audit/pg_audit.c | 1870 ++++++++++++++++++++++++ contrib/pg_audit/pg_audit.conf | 1 + contrib/pg_audit/pg_audit.control | 5 + contrib/pg_audit/sql/pg_audit.sql | 617 ++++++++ doc/src/sgml/contrib.sgml | 1 + doc/src/sgml/filelist.sgml | 1 + doc/src/sgml/pgaudit.sgml | 678 +++++++++ 12 files changed, 4186 insertions(+) create mode 100644 contrib/pg_audit/.gitignore create mode 100644 contrib/pg_audit/Makefile create mode 100644 contrib/pg_audit/expected/pg_audit.out create mode 100644 contrib/pg_audit/pg_audit--1.0.0.sql create mode 100644 contrib/pg_audit/pg_audit.c create mode 100644 contrib/pg_audit/pg_audit.conf create mode 100644 contrib/pg_audit/pg_audit.control create mode 100644 contrib/pg_audit/sql/pg_audit.sql create mode 100644 doc/src/sgml/pgaudit.sgml diff --git a/contrib/Makefile b/contrib/Makefile index f84e684972..1f3d3f1185 100644 --- a/contrib/Makefile +++ b/contrib/Makefile @@ -28,6 +28,7 @@ SUBDIRS = \ oid2name \ pageinspect \ passwordcheck \ + pg_audit \ pg_buffercache \ pg_freespacemap \ pg_prewarm \ diff --git a/contrib/pg_audit/.gitignore b/contrib/pg_audit/.gitignore new file mode 100644 index 0000000000..a5267cf5b2 --- /dev/null +++ b/contrib/pg_audit/.gitignore @@ -0,0 +1,5 @@ +log/ +results/ +tmp_check/ +regression.diffs +regression.out diff --git a/contrib/pg_audit/Makefile b/contrib/pg_audit/Makefile new file mode 100644 index 0000000000..7b360110a8 --- /dev/null +++ b/contrib/pg_audit/Makefile @@ -0,0 +1,21 @@ +# pg_audit/Makefile + +MODULE = pg_audit +MODULE_big = pg_audit +OBJS = pg_audit.o + +EXTENSION = pg_audit +REGRESS = pg_audit +REGRESS_OPTS = --temp-config=$(top_srcdir)/contrib/pg_audit/pg_audit.conf +DATA = pg_audit--1.0.0.sql + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = contrib/pg_audit +top_builddir = ../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/contrib/pg_audit/expected/pg_audit.out b/contrib/pg_audit/expected/pg_audit.out new file mode 100644 index 0000000000..af4609b9ba --- /dev/null +++ b/contrib/pg_audit/expected/pg_audit.out @@ -0,0 +1,964 @@ +-- Load pg_audit module +create extension pg_audit; +-- +-- Audit log fields are: +-- AUDIT_TYPE - SESSION or OBJECT +-- STATEMENT_ID - ID of the statement in the current backend +-- SUBSTATEMENT_ID - ID of the substatement in the current backend +-- CLASS - Class of statement being logged (e.g. ROLE, READ, WRITE) +-- COMMAND - e.g. SELECT, CREATE ROLE, UPDATE +-- OBJECT_TYPE - When available, type of object acted on (e.g. TABLE, VIEW) +-- OBJECT_NAME - When available, fully-qualified table of object +-- STATEMENT - The statement being logged +-- PARAMETER - If parameter logging is requested, they will follow the +-- statement +-- +-- Create a superuser role that we know the name of for testing +CREATE USER super SUPERUSER; +ALTER ROLE super SET pg_audit.log = 'Role'; +ALTER ROLE super SET pg_audit.log_level = 'notice'; +\connect contrib_regression super; +-- +-- Create auditor role +CREATE ROLE auditor; +NOTICE: AUDIT: SESSION,1,1,ROLE,CREATE ROLE,,,CREATE ROLE auditor;, +-- +-- Create first test user +CREATE USER user1; +NOTICE: AUDIT: SESSION,2,1,ROLE,CREATE ROLE,,,CREATE USER user1;, +ALTER ROLE user1 SET pg_audit.log = 'ddl, ROLE'; +NOTICE: AUDIT: SESSION,3,1,ROLE,ALTER ROLE,,,"ALTER ROLE user1 SET pg_audit.log = 'ddl, ROLE';", +ALTER ROLE user1 SET pg_audit.log_level = 'notice'; +NOTICE: AUDIT: SESSION,4,1,ROLE,ALTER ROLE,,,ALTER ROLE user1 SET pg_audit.log_level = 'notice';, +-- +-- Create, select, drop (select will not be audited) +\connect contrib_regression user1 +CREATE TABLE public.test (id INT); +NOTICE: AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.test,CREATE TABLE public.test (id INT);, +SELECT * FROM test; + id +---- +(0 rows) + +DROP TABLE test; +NOTICE: AUDIT: SESSION,2,1,DDL,DROP TABLE,TABLE,public.test,DROP TABLE test;, +-- +-- Create second test user +\connect contrib_regression super +CREATE USER user2; +NOTICE: AUDIT: SESSION,1,1,ROLE,CREATE ROLE,,,CREATE USER user2;, +ALTER ROLE user2 SET pg_audit.log = 'Read, writE'; +NOTICE: AUDIT: SESSION,2,1,ROLE,ALTER ROLE,,,"ALTER ROLE user2 SET pg_audit.log = 'Read, writE';", +ALTER ROLE user2 SET pg_audit.log_catalog = OFF; +NOTICE: AUDIT: SESSION,3,1,ROLE,ALTER ROLE,,,ALTER ROLE user2 SET pg_audit.log_catalog = OFF;, +ALTER ROLE user2 SET pg_audit.log_level = 'warning'; +NOTICE: AUDIT: SESSION,4,1,ROLE,ALTER ROLE,,,ALTER ROLE user2 SET pg_audit.log_level = 'warning';, +ALTER ROLE user2 SET pg_audit.role = auditor; +NOTICE: AUDIT: SESSION,5,1,ROLE,ALTER ROLE,,,ALTER ROLE user2 SET pg_audit.role = auditor;, +ALTER ROLE user2 SET pg_audit.log_statement_once = ON; +NOTICE: AUDIT: SESSION,6,1,ROLE,ALTER ROLE,,,ALTER ROLE user2 SET pg_audit.log_statement_once = ON;, +\connect contrib_regression user2 +CREATE TABLE test2 (id INT); +GRANT SELECT ON TABLE public.test2 TO auditor; +-- +-- Role-based tests +CREATE TABLE test3 +( + id INT +); +SELECT count(*) + FROM +( + SELECT relname + FROM pg_class + LIMIT 1 +) SUBQUERY; + count +------- + 1 +(1 row) + +SELECT * + FROM test3, test2; +WARNING: AUDIT: SESSION,1,1,READ,SELECT,,,"SELECT * + FROM test3, test2;", +WARNING: AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.test2,, + id | id +----+---- +(0 rows) + +GRANT INSERT + ON TABLE public.test3 + TO auditor; +-- +-- Create a view to test logging +CREATE VIEW vw_test3 AS +SELECT * + FROM test3; +GRANT SELECT + ON vw_test3 + TO auditor; +-- +-- Object logged because of: +-- select on vw_test3 +-- select on test2 +SELECT * + FROM vw_test3, test2; +WARNING: AUDIT: SESSION,2,1,READ,SELECT,,,"SELECT * + FROM vw_test3, test2;", +WARNING: AUDIT: OBJECT,2,1,READ,SELECT,TABLE,public.test2,, +WARNING: AUDIT: OBJECT,2,1,READ,SELECT,VIEW,public.vw_test3,, + id | id +----+---- +(0 rows) + +-- +-- Object logged because of: +-- insert on test3 +-- select on test2 +WITH CTE AS +( + SELECT id + FROM test2 +) +INSERT INTO test3 +SELECT id + FROM cte; +WARNING: AUDIT: SESSION,3,1,WRITE,INSERT,,,"WITH CTE AS +( + SELECT id + FROM test2 +) +INSERT INTO test3 +SELECT id + FROM cte;", +WARNING: AUDIT: OBJECT,3,1,WRITE,INSERT,TABLE,public.test3,, +WARNING: AUDIT: OBJECT,3,1,READ,SELECT,TABLE,public.test2,, +-- +-- Object logged because of: +-- insert on test3 +WITH CTE AS +( + INSERT INTO test3 VALUES (1) + RETURNING id +) +INSERT INTO test2 +SELECT id + FROM cte; +WARNING: AUDIT: SESSION,4,1,WRITE,INSERT,,,"WITH CTE AS +( + INSERT INTO test3 VALUES (1) + RETURNING id +) +INSERT INTO test2 +SELECT id + FROM cte;", +WARNING: AUDIT: OBJECT,4,1,WRITE,INSERT,TABLE,public.test3,, +GRANT UPDATE ON TABLE public.test2 TO auditor; +-- +-- Object logged because of: +-- insert on test3 +-- update on test2 +WITH CTE AS +( + UPDATE test2 + SET id = 1 + RETURNING id +) +INSERT INTO test3 +SELECT id + FROM cte; +WARNING: AUDIT: SESSION,5,1,WRITE,INSERT,,,"WITH CTE AS +( + UPDATE test2 + SET id = 1 + RETURNING id +) +INSERT INTO test3 +SELECT id + FROM cte;", +WARNING: AUDIT: OBJECT,5,1,WRITE,INSERT,TABLE,public.test3,, +WARNING: AUDIT: OBJECT,5,1,WRITE,UPDATE,TABLE,public.test2,, +-- +-- Object logged because of: +-- insert on test2 +WITH CTE AS +( + INSERT INTO test2 VALUES (1) + RETURNING id +) +UPDATE test3 + SET id = cte.id + FROM cte + WHERE test3.id <> cte.id; +WARNING: AUDIT: SESSION,6,1,WRITE,UPDATE,,,"WITH CTE AS +( + INSERT INTO test2 VALUES (1) + RETURNING id +) +UPDATE test3 + SET id = cte.id + FROM cte + WHERE test3.id <> cte.id;", +WARNING: AUDIT: OBJECT,6,1,WRITE,INSERT,TABLE,public.test2,, +-- +-- Change permissions of user 2 so that only object logging will be done +\connect contrib_regression super +alter role user2 set pg_audit.log = 'NONE'; +NOTICE: AUDIT: SESSION,1,1,ROLE,ALTER ROLE,,,alter role user2 set pg_audit.log = 'NONE';, +\connect contrib_regression user2 +-- +-- Create test4 and add permissions +CREATE TABLE test4 +( + id int, + name text +); +GRANT SELECT (name) + ON TABLE public.test4 + TO auditor; +GRANT UPDATE (id) + ON TABLE public.test4 + TO auditor; +GRANT insert (name) + ON TABLE public.test4 + TO auditor; +-- +-- Not object logged +SELECT id + FROM public.test4; + id +---- +(0 rows) + +-- +-- Object logged because of: +-- select (name) on test4 +SELECT name + FROM public.test4; +WARNING: AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.test4,"SELECT name + FROM public.test4;", + name +------ +(0 rows) + +-- +-- Not object logged +INSERT INTO public.test4 (id) + VALUES (1); +-- +-- Object logged because of: +-- insert (name) on test4 +INSERT INTO public.test4 (name) + VALUES ('test'); +WARNING: AUDIT: OBJECT,2,1,WRITE,INSERT,TABLE,public.test4,"INSERT INTO public.test4 (name) + VALUES ('test');", +-- +-- Not object logged +UPDATE public.test4 + SET name = 'foo'; +-- +-- Object logged because of: +-- update (id) on test4 +UPDATE public.test4 + SET id = 1; +WARNING: AUDIT: OBJECT,3,1,WRITE,UPDATE,TABLE,public.test4,"UPDATE public.test4 + SET id = 1;", +-- +-- Object logged because of: +-- update (name) on test4 +-- update (name) takes precedence over select (name) due to ordering +update public.test4 set name = 'foo' where name = 'bar'; +WARNING: AUDIT: OBJECT,4,1,WRITE,UPDATE,TABLE,public.test4,update public.test4 set name = 'foo' where name = 'bar';, +-- +-- Drop test tables +DROP TABLE test2; +DROP VIEW vw_test3; +DROP TABLE test3; +DROP TABLE test4; +-- +-- Change permissions of user 1 so that session logging will be done +\connect contrib_regression super +alter role user1 set pg_audit.log = 'DDL, READ'; +NOTICE: AUDIT: SESSION,1,1,ROLE,ALTER ROLE,,,"alter role user1 set pg_audit.log = 'DDL, READ';", +\connect contrib_regression user1 +-- +-- Create table is session logged +CREATE TABLE public.account +( + id INT, + name TEXT, + password TEXT, + description TEXT +); +NOTICE: AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.account,"CREATE TABLE public.account +( + id INT, + name TEXT, + password TEXT, + description TEXT +);", +-- +-- Select is session logged +SELECT * + FROM account; +NOTICE: AUDIT: SESSION,2,1,READ,SELECT,,,"SELECT * + FROM account;", + id | name | password | description +----+------+----------+------------- +(0 rows) + +-- +-- Insert is not logged +INSERT INTO account (id, name, password, description) + VALUES (1, 'user1', 'HASH1', 'blah, blah'); +-- +-- Change permissions of user 1 so that only object logging will be done +\connect contrib_regression super +alter role user1 set pg_audit.log = 'none'; +NOTICE: AUDIT: SESSION,1,1,ROLE,ALTER ROLE,,,alter role user1 set pg_audit.log = 'none';, +alter role user1 set pg_audit.role = 'auditor'; +NOTICE: AUDIT: SESSION,2,1,ROLE,ALTER ROLE,,,alter role user1 set pg_audit.role = 'auditor';, +\connect contrib_regression user1 +-- +-- ROLE class not set, so auditor grants not logged +GRANT SELECT (password), + UPDATE (name, password) + ON TABLE public.account + TO auditor; +-- +-- Not object logged +SELECT id, + name + FROM account; + id | name +----+------- + 1 | user1 +(1 row) + +-- +-- Object logged because of: +-- select (password) on account +SELECT password + FROM account; +NOTICE: AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account,"SELECT password + FROM account;", + password +---------- + HASH1 +(1 row) + +-- +-- Not object logged +UPDATE account + SET description = 'yada, yada'; +-- +-- Object logged because of: +-- update (password) on account +UPDATE account + SET password = 'HASH2'; +NOTICE: AUDIT: OBJECT,2,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account + SET password = 'HASH2';", +-- +-- Change permissions of user 1 so that session relation logging will be done +\connect contrib_regression super +alter role user1 set pg_audit.log_relation = on; +NOTICE: AUDIT: SESSION,1,1,ROLE,ALTER ROLE,,,alter role user1 set pg_audit.log_relation = on;, +alter role user1 set pg_audit.log = 'read, WRITE'; +NOTICE: AUDIT: SESSION,2,1,ROLE,ALTER ROLE,,,"alter role user1 set pg_audit.log = 'read, WRITE';", +\connect contrib_regression user1 +-- +-- Not logged +create table ACCOUNT_ROLE_MAP +( + account_id INT, + role_id INT +); +-- +-- ROLE class not set, so auditor grants not logged +GRANT SELECT + ON TABLE public.account_role_map + TO auditor; +-- +-- Object logged because of: +-- select (password) on account +-- select on account_role_map +-- Session logged on all tables because log = read and log_relation = on +SELECT account.password, + account_role_map.role_id + FROM account + INNER JOIN account_role_map + on account.id = account_role_map.account_id; +NOTICE: AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account,"SELECT account.password, + account_role_map.role_id + FROM account + INNER JOIN account_role_map + on account.id = account_role_map.account_id;", +NOTICE: AUDIT: SESSION,1,1,READ,SELECT,TABLE,public.account,"SELECT account.password, + account_role_map.role_id + FROM account + INNER JOIN account_role_map + on account.id = account_role_map.account_id;", +NOTICE: AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account_role_map,"SELECT account.password, + account_role_map.role_id + FROM account + INNER JOIN account_role_map + on account.id = account_role_map.account_id;", +NOTICE: AUDIT: SESSION,1,1,READ,SELECT,TABLE,public.account_role_map,"SELECT account.password, + account_role_map.role_id + FROM account + INNER JOIN account_role_map + on account.id = account_role_map.account_id;", + password | role_id +----------+--------- +(0 rows) + +-- +-- Object logged because of: +-- select (password) on account +-- Session logged on all tables because log = read and log_relation = on +SELECT password + FROM account; +NOTICE: AUDIT: OBJECT,2,1,READ,SELECT,TABLE,public.account,"SELECT password + FROM account;", +NOTICE: AUDIT: SESSION,2,1,READ,SELECT,TABLE,public.account,"SELECT password + FROM account;", + password +---------- + HASH2 +(1 row) + +-- +-- Not object logged +-- Session logged on all tables because log = read and log_relation = on +UPDATE account + SET description = 'yada, yada'; +NOTICE: AUDIT: SESSION,3,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account + SET description = 'yada, yada';", +-- +-- Object logged because of: +-- select (password) on account (in the where clause) +-- Session logged on all tables because log = read and log_relation = on +UPDATE account + SET description = 'yada, yada' + where password = 'HASH2'; +NOTICE: AUDIT: OBJECT,4,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account + SET description = 'yada, yada' + where password = 'HASH2';", +NOTICE: AUDIT: SESSION,4,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account + SET description = 'yada, yada' + where password = 'HASH2';", +-- +-- Object logged because of: +-- update (password) on account +-- Session logged on all tables because log = read and log_relation = on +UPDATE account + SET password = 'HASH2'; +NOTICE: AUDIT: OBJECT,5,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account + SET password = 'HASH2';", +NOTICE: AUDIT: SESSION,5,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account + SET password = 'HASH2';", +-- +-- Change back to superuser to do exhaustive tests +\connect contrib_regression super +SET pg_audit.log = 'ALL'; +NOTICE: AUDIT: SESSION,1,1,MISC,SET,,,SET pg_audit.log = 'ALL';, +SET pg_audit.log_level = 'notice'; +NOTICE: AUDIT: SESSION,2,1,MISC,SET,,,SET pg_audit.log_level = 'notice';, +SET pg_audit.log_relation = ON; +NOTICE: AUDIT: SESSION,3,1,MISC,SET,,,SET pg_audit.log_relation = ON;, +SET pg_audit.log_parameter = ON; +NOTICE: AUDIT: SESSION,4,1,MISC,SET,,,SET pg_audit.log_parameter = ON;, +-- +-- Simple DO block +DO $$ +BEGIN + raise notice 'test'; +END $$; +NOTICE: AUDIT: SESSION,5,1,FUNCTION,DO,,,"DO $$ +BEGIN + raise notice 'test'; +END $$;", +NOTICE: test +-- +-- Create test schema +CREATE SCHEMA test; +NOTICE: AUDIT: SESSION,6,1,DDL,CREATE SCHEMA,SCHEMA,test,CREATE SCHEMA test;, +-- +-- Copy account to stdout +COPY account TO stdout; +NOTICE: AUDIT: SESSION,7,1,READ,SELECT,TABLE,public.account,COPY account TO stdout;, +1 user1 HASH2 yada, yada +-- +-- Create a table from a query +CREATE TABLE test.account_copy AS +SELECT * + FROM account; +NOTICE: AUDIT: SESSION,8,1,READ,SELECT,TABLE,public.account,"CREATE TABLE test.account_copy AS +SELECT * + FROM account;", +NOTICE: AUDIT: SESSION,8,1,WRITE,INSERT,TABLE,test.account_copy,"CREATE TABLE test.account_copy AS +SELECT * + FROM account;", +NOTICE: AUDIT: SESSION,8,2,DDL,CREATE TABLE AS,TABLE,test.account_copy,"CREATE TABLE test.account_copy AS +SELECT * + FROM account;", +-- +-- Copy from stdin to account copy +COPY test.account_copy from stdin; +NOTICE: AUDIT: SESSION,9,1,WRITE,INSERT,TABLE,test.account_copy,COPY test.account_copy from stdin;, +-- +-- Test prepared statement +PREPARE pgclassstmt (oid) AS +SELECT * + FROM account + WHERE id = $1; +NOTICE: AUDIT: SESSION,10,1,READ,PREPARE,,,"PREPARE pgclassstmt (oid) AS +SELECT * + FROM account + WHERE id = $1;", +EXECUTE pgclassstmt (1); +NOTICE: AUDIT: SESSION,11,1,READ,SELECT,TABLE,public.account,"PREPARE pgclassstmt (oid) AS +SELECT * + FROM account + WHERE id = $1;",1 +NOTICE: AUDIT: SESSION,11,2,MISC,EXECUTE,,,EXECUTE pgclassstmt (1);, + id | name | password | description +----+-------+----------+------------- + 1 | user1 | HASH2 | yada, yada +(1 row) + +DEALLOCATE pgclassstmt; +NOTICE: AUDIT: SESSION,12,1,MISC,DEALLOCATE,,,DEALLOCATE pgclassstmt;, +-- +-- Test cursor +BEGIN; +NOTICE: AUDIT: SESSION,13,1,MISC,BEGIN,,,BEGIN;, +DECLARE ctest SCROLL CURSOR FOR +SELECT count(*) + FROM +( + SELECT relname + FROM pg_class + LIMIT 1 + ) subquery; +NOTICE: AUDIT: SESSION,14,1,READ,SELECT,TABLE,pg_catalog.pg_class,"DECLARE ctest SCROLL CURSOR FOR +SELECT count(*) + FROM +( + SELECT relname + FROM pg_class + LIMIT 1 + ) subquery;", +NOTICE: AUDIT: SESSION,14,2,READ,DECLARE CURSOR,,,"DECLARE ctest SCROLL CURSOR FOR +SELECT count(*) + FROM +( + SELECT relname + FROM pg_class + LIMIT 1 + ) subquery;", +FETCH NEXT FROM ctest; +NOTICE: AUDIT: SESSION,15,1,MISC,FETCH,,,FETCH NEXT FROM ctest;, + count +------- + 1 +(1 row) + +CLOSE ctest; +NOTICE: AUDIT: SESSION,16,1,MISC,CLOSE CURSOR,,,CLOSE ctest;, +COMMIT; +NOTICE: AUDIT: SESSION,17,1,MISC,COMMIT,,,COMMIT;, +-- +-- Turn off log_catalog and pg_class will not be logged +SET pg_audit.log_catalog = OFF; +NOTICE: AUDIT: SESSION,18,1,MISC,SET,,,SET pg_audit.log_catalog = OFF;, +SELECT count(*) + FROM +( + SELECT relname + FROM pg_class + LIMIT 1 + ) subquery; + count +------- + 1 +(1 row) + +-- +-- Test prepared insert +CREATE TABLE test.test_insert +( + id INT +); +NOTICE: AUDIT: SESSION,19,1,DDL,CREATE TABLE,TABLE,test.test_insert,"CREATE TABLE test.test_insert +( + id INT +);", +PREPARE pgclassstmt (oid) AS +INSERT INTO test.test_insert (id) + VALUES ($1); +NOTICE: AUDIT: SESSION,20,1,WRITE,PREPARE,,,"PREPARE pgclassstmt (oid) AS +INSERT INTO test.test_insert (id) + VALUES ($1);", +EXECUTE pgclassstmt (1); +NOTICE: AUDIT: SESSION,21,1,WRITE,INSERT,TABLE,test.test_insert,"PREPARE pgclassstmt (oid) AS +INSERT INTO test.test_insert (id) + VALUES ($1);",1 +NOTICE: AUDIT: SESSION,21,2,MISC,EXECUTE,,,EXECUTE pgclassstmt (1);, +-- +-- Check that primary key creation is logged +CREATE TABLE public.test +( + id INT, + name TEXT, + description TEXT, + CONSTRAINT test_pkey PRIMARY KEY (id) +); +NOTICE: AUDIT: SESSION,22,1,DDL,CREATE TABLE,TABLE,public.test,"CREATE TABLE public.test +( + id INT, + name TEXT, + description TEXT, + CONSTRAINT test_pkey PRIMARY KEY (id) +);", +NOTICE: AUDIT: SESSION,22,1,DDL,CREATE TABLE,INDEX,public.test_pkey,"CREATE TABLE public.test +( + id INT, + name TEXT, + description TEXT, + CONSTRAINT test_pkey PRIMARY KEY (id) +);", +-- +-- Check that analyze is logged +ANALYZE test; +NOTICE: AUDIT: SESSION,23,1,MISC,ANALYZE,,,ANALYZE test;, +-- +-- Grants to public should not cause object logging (session logging will +-- still happen) +GRANT SELECT + ON TABLE public.test + TO PUBLIC; +NOTICE: AUDIT: SESSION,24,1,ROLE,GRANT,TABLE,,"GRANT SELECT + ON TABLE public.test + TO PUBLIC;", +SELECT * + FROM test; +NOTICE: AUDIT: SESSION,25,1,READ,SELECT,TABLE,public.test,"SELECT * + FROM test;", + id | name | description +----+------+------------- +(0 rows) + +-- Check that statements without columns log +SELECT + FROM test; +NOTICE: AUDIT: SESSION,26,1,READ,SELECT,TABLE,public.test,"SELECT + FROM test;", +-- +(0 rows) + +SELECT 1, + current_user; +NOTICE: AUDIT: SESSION,27,1,READ,SELECT,,,"SELECT 1, + current_user;", + ?column? | current_user +----------+-------------- + 1 | super +(1 row) + +DO $$ +DECLARE + test INT; +BEGIN + SELECT 1 + INTO test; +END $$; +NOTICE: AUDIT: SESSION,28,1,FUNCTION,DO,,,"DO $$ +DECLARE + test INT; +BEGIN + SELECT 1 + INTO test; +END $$;", +NOTICE: AUDIT: SESSION,28,2,READ,SELECT,,,SELECT 1, +CONTEXT: SQL statement "SELECT 1" +PL/pgSQL function inline_code_block line 5 at SQL statement +explain select 1; +NOTICE: AUDIT: SESSION,29,1,READ,SELECT,,,explain select 1;, +NOTICE: AUDIT: SESSION,29,2,MISC,EXPLAIN,,,explain select 1;, + QUERY PLAN +------------------------------------------ + Result (cost=0.00..0.01 rows=1 width=0) +(1 row) + +-- +-- Test that looks inside of do blocks log +INSERT INTO TEST (id) + VALUES (1); +NOTICE: AUDIT: SESSION,30,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id) + VALUES (1);", +INSERT INTO TEST (id) + VALUES (2); +NOTICE: AUDIT: SESSION,31,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id) + VALUES (2);", +INSERT INTO TEST (id) + VALUES (3); +NOTICE: AUDIT: SESSION,32,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id) + VALUES (3);", +DO $$ +DECLARE + result RECORD; +BEGIN + FOR result IN + SELECT id + FROM test + LOOP + INSERT INTO test (id) + VALUES (result.id + 100); + END LOOP; +END $$; +NOTICE: AUDIT: SESSION,33,1,FUNCTION,DO,,,"DO $$ +DECLARE + result RECORD; +BEGIN + FOR result IN + SELECT id + FROM test + LOOP + INSERT INTO test (id) + VALUES (result.id + 100); + END LOOP; +END $$;", +NOTICE: AUDIT: SESSION,33,2,READ,SELECT,TABLE,public.test,"SELECT id + FROM test", +CONTEXT: PL/pgSQL function inline_code_block line 5 at FOR over SELECT rows +NOTICE: AUDIT: SESSION,33,3,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id) + VALUES (result.id + 100)",",," +CONTEXT: SQL statement "INSERT INTO test (id) + VALUES (result.id + 100)" +PL/pgSQL function inline_code_block line 9 at SQL statement +NOTICE: AUDIT: SESSION,33,4,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id) + VALUES (result.id + 100)",",," +CONTEXT: SQL statement "INSERT INTO test (id) + VALUES (result.id + 100)" +PL/pgSQL function inline_code_block line 9 at SQL statement +NOTICE: AUDIT: SESSION,33,5,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id) + VALUES (result.id + 100)",",," +CONTEXT: SQL statement "INSERT INTO test (id) + VALUES (result.id + 100)" +PL/pgSQL function inline_code_block line 9 at SQL statement +-- +-- Test obfuscated dynamic sql for clean logging +DO $$ +DECLARE + table_name TEXT = 'do_table'; +BEGIN + EXECUTE 'CREATE TABLE ' || table_name || ' ("weird name" INT)'; + EXECUTE 'DROP table ' || table_name; +END $$; +NOTICE: AUDIT: SESSION,34,1,FUNCTION,DO,,,"DO $$ +DECLARE + table_name TEXT = 'do_table'; +BEGIN + EXECUTE 'CREATE TABLE ' || table_name || ' (""weird name"" INT)'; + EXECUTE 'DROP table ' || table_name; +END $$;", +NOTICE: AUDIT: SESSION,34,2,DDL,CREATE TABLE,TABLE,public.do_table,"CREATE TABLE do_table (""weird name"" INT)", +CONTEXT: SQL statement "CREATE TABLE do_table ("weird name" INT)" +PL/pgSQL function inline_code_block line 5 at EXECUTE statement +NOTICE: AUDIT: SESSION,34,3,DDL,DROP TABLE,TABLE,public.do_table,DROP table do_table, +CONTEXT: SQL statement "DROP table do_table" +PL/pgSQL function inline_code_block line 6 at EXECUTE statement +-- +-- Generate an error and make sure the stack gets cleared +DO $$ +BEGIN + CREATE TABLE bogus.test_block + ( + id INT + ); +END $$; +NOTICE: AUDIT: SESSION,35,1,FUNCTION,DO,,,"DO $$ +BEGIN + CREATE TABLE bogus.test_block + ( + id INT + ); +END $$;", +ERROR: schema "bogus" does not exist +LINE 1: CREATE TABLE bogus.test_block + ^ +QUERY: CREATE TABLE bogus.test_block + ( + id INT + ) +CONTEXT: PL/pgSQL function inline_code_block line 3 at SQL statement +-- +-- Test alter table statements +ALTER TABLE public.test + DROP COLUMN description ; +NOTICE: AUDIT: SESSION,36,1,DDL,ALTER TABLE,TABLE COLUMN,public.test.description,"ALTER TABLE public.test + DROP COLUMN description ;", +NOTICE: AUDIT: SESSION,36,1,DDL,ALTER TABLE,TABLE,public.test,"ALTER TABLE public.test + DROP COLUMN description ;", +ALTER TABLE public.test + RENAME TO test2; +NOTICE: AUDIT: SESSION,37,1,DDL,ALTER TABLE,TABLE,public.test2,"ALTER TABLE public.test + RENAME TO test2;", +ALTER TABLE public.test2 + SET SCHEMA test; +NOTICE: AUDIT: SESSION,38,1,DDL,ALTER TABLE,TABLE,test.test2,"ALTER TABLE public.test2 + SET SCHEMA test;", +ALTER TABLE test.test2 + ADD COLUMN description TEXT; +NOTICE: AUDIT: SESSION,39,1,DDL,ALTER TABLE,TABLE,test.test2,"ALTER TABLE test.test2 + ADD COLUMN description TEXT;", +ALTER TABLE test.test2 + DROP COLUMN description; +NOTICE: AUDIT: SESSION,40,1,DDL,ALTER TABLE,TABLE COLUMN,test.test2.description,"ALTER TABLE test.test2 + DROP COLUMN description;", +NOTICE: AUDIT: SESSION,40,1,DDL,ALTER TABLE,TABLE,test.test2,"ALTER TABLE test.test2 + DROP COLUMN description;", +DROP TABLE test.test2; +NOTICE: AUDIT: SESSION,41,1,DDL,DROP TABLE,TABLE,test.test2,DROP TABLE test.test2;, +NOTICE: AUDIT: SESSION,41,1,DDL,DROP TABLE,TABLE CONSTRAINT,test_pkey on test.test2,DROP TABLE test.test2;, +NOTICE: AUDIT: SESSION,41,1,DDL,DROP TABLE,INDEX,test.test_pkey,DROP TABLE test.test2;, +-- +-- Test multiple statements with one semi-colon +CREATE SCHEMA foo + CREATE TABLE foo.bar (id int) + CREATE TABLE foo.baz (id int); +NOTICE: AUDIT: SESSION,42,1,DDL,CREATE SCHEMA,SCHEMA,foo,"CREATE SCHEMA foo + CREATE TABLE foo.bar (id int) + CREATE TABLE foo.baz (id int);", +NOTICE: AUDIT: SESSION,42,1,DDL,CREATE SCHEMA,TABLE,foo.bar,"CREATE SCHEMA foo + CREATE TABLE foo.bar (id int) + CREATE TABLE foo.baz (id int);", +NOTICE: AUDIT: SESSION,42,1,DDL,CREATE SCHEMA,TABLE,foo.baz,"CREATE SCHEMA foo + CREATE TABLE foo.bar (id int) + CREATE TABLE foo.baz (id int);", +-- +-- Test aggregate +CREATE FUNCTION public.int_add +( + a INT, + b INT +) + RETURNS INT LANGUAGE plpgsql AS $$ +BEGIN + return a + b; +END $$; +NOTICE: AUDIT: SESSION,43,1,DDL,CREATE FUNCTION,FUNCTION,"public.int_add(integer,integer)","CREATE FUNCTION public.int_add +( + a INT, + b INT +) + RETURNS INT LANGUAGE plpgsql AS $$ +BEGIN + return a + b; +END $$;", +SELECT int_add(1, 1); +NOTICE: AUDIT: SESSION,44,1,READ,SELECT,,,"SELECT int_add(1, 1);", +NOTICE: AUDIT: SESSION,44,2,FUNCTION,EXECUTE,FUNCTION,public.int_add,"SELECT int_add(1, 1);", + int_add +--------- + 2 +(1 row) + +CREATE AGGREGATE public.sum_test(INT) (SFUNC=public.int_add, STYPE=INT, INITCOND='0'); +NOTICE: AUDIT: SESSION,45,1,DDL,CREATE AGGREGATE,AGGREGATE,public.sum_test(integer),"CREATE AGGREGATE public.sum_test(INT) (SFUNC=public.int_add, STYPE=INT, INITCOND='0');", +ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2; +NOTICE: AUDIT: SESSION,46,1,DDL,ALTER AGGREGATE,AGGREGATE,public.sum_test2(integer),ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2;, +-- +-- Test conversion +CREATE CONVERSION public.conversion_test FOR 'SQL_ASCII' TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic; +NOTICE: AUDIT: SESSION,47,1,DDL,CREATE CONVERSION,CONVERSION,public.conversion_test,CREATE CONVERSION public.conversion_test FOR 'SQL_ASCII' TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic;, +ALTER CONVERSION public.conversion_test RENAME TO conversion_test2; +NOTICE: AUDIT: SESSION,48,1,DDL,ALTER CONVERSION,CONVERSION,public.conversion_test2,ALTER CONVERSION public.conversion_test RENAME TO conversion_test2;, +-- +-- Test create/alter/drop database +CREATE DATABASE contrib_regression_pgaudit; +NOTICE: AUDIT: SESSION,49,1,DDL,CREATE DATABASE,,,CREATE DATABASE contrib_regression_pgaudit;, +ALTER DATABASE contrib_regression_pgaudit RENAME TO contrib_regression_pgaudit2; +NOTICE: AUDIT: SESSION,50,1,DDL,ALTER DATABASE,,,ALTER DATABASE contrib_regression_pgaudit RENAME TO contrib_regression_pgaudit2;, +DROP DATABASE contrib_regression_pgaudit2; +NOTICE: AUDIT: SESSION,51,1,DDL,DROP DATABASE,,,DROP DATABASE contrib_regression_pgaudit2;, +-- +-- Test that frees a memory context earlier than expected +CREATE TABLE hoge +( + id int +); +NOTICE: AUDIT: SESSION,52,1,DDL,CREATE TABLE,TABLE,public.hoge,"CREATE TABLE hoge +( + id int +);", +CREATE FUNCTION test() + RETURNS INT AS $$ +DECLARE + cur1 cursor for select * from hoge; + tmp int; +BEGIN + OPEN cur1; + FETCH cur1 into tmp; + RETURN tmp; +END $$ +LANGUAGE plpgsql ; +NOTICE: AUDIT: SESSION,53,1,DDL,CREATE FUNCTION,FUNCTION,public.test(),"CREATE FUNCTION test() + RETURNS INT AS $$ +DECLARE + cur1 cursor for select * from hoge; + tmp int; +BEGIN + OPEN cur1; + FETCH cur1 into tmp; + RETURN tmp; +END $$ +LANGUAGE plpgsql ;", +SELECT test(); +NOTICE: AUDIT: SESSION,54,1,READ,SELECT,,,SELECT test();, +NOTICE: AUDIT: SESSION,54,2,FUNCTION,EXECUTE,FUNCTION,public.test,SELECT test();, +NOTICE: AUDIT: SESSION,54,3,READ,SELECT,TABLE,public.hoge,select * from hoge, +CONTEXT: PL/pgSQL function test() line 6 at OPEN + test +------ + +(1 row) + +-- +-- Delete all rows then delete 1 row +SET pg_audit.log = 'write'; +SET pg_audit.role = 'auditor'; +create table bar +( + col int +); +grant delete + on bar + to auditor; +insert into bar (col) + values (1); +NOTICE: AUDIT: SESSION,55,1,WRITE,INSERT,TABLE,public.bar,"insert into bar (col) + values (1);", +delete from bar; +NOTICE: AUDIT: OBJECT,56,1,WRITE,DELETE,TABLE,public.bar,delete from bar;, +NOTICE: AUDIT: SESSION,56,1,WRITE,DELETE,TABLE,public.bar,delete from bar;, +insert into bar (col) + values (1); +NOTICE: AUDIT: SESSION,57,1,WRITE,INSERT,TABLE,public.bar,"insert into bar (col) + values (1);", +delete from bar + where col = 1; +NOTICE: AUDIT: OBJECT,58,1,WRITE,DELETE,TABLE,public.bar,"delete from bar + where col = 1;", +NOTICE: AUDIT: SESSION,58,1,WRITE,DELETE,TABLE,public.bar,"delete from bar + where col = 1;", +drop table bar; +-- +-- Grant roles to each other +SET pg_audit.log = 'role'; +GRANT user1 TO user2; +NOTICE: AUDIT: SESSION,59,1,ROLE,GRANT ROLE,,,GRANT user1 TO user2;, +REVOKE user1 FROM user2; +NOTICE: AUDIT: SESSION,60,1,ROLE,REVOKE ROLE,,,REVOKE user1 FROM user2;, diff --git a/contrib/pg_audit/pg_audit--1.0.0.sql b/contrib/pg_audit/pg_audit--1.0.0.sql new file mode 100644 index 0000000000..9d9ee83f73 --- /dev/null +++ b/contrib/pg_audit/pg_audit--1.0.0.sql @@ -0,0 +1,22 @@ +/* pg_audit/pg_audit--1.0.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION pg_audit" to load this file.\quit + +CREATE FUNCTION pg_audit_ddl_command_end() + RETURNS event_trigger + LANGUAGE C + AS 'MODULE_PATHNAME', 'pg_audit_ddl_command_end'; + +CREATE EVENT TRIGGER pg_audit_ddl_command_end + ON ddl_command_end + EXECUTE PROCEDURE pg_audit_ddl_command_end(); + +CREATE FUNCTION pg_audit_sql_drop() + RETURNS event_trigger + LANGUAGE C + AS 'MODULE_PATHNAME', 'pg_audit_sql_drop'; + +CREATE EVENT TRIGGER pg_audit_sql_drop + ON sql_drop + EXECUTE PROCEDURE pg_audit_sql_drop(); diff --git a/contrib/pg_audit/pg_audit.c b/contrib/pg_audit/pg_audit.c new file mode 100644 index 0000000000..384fc07481 --- /dev/null +++ b/contrib/pg_audit/pg_audit.c @@ -0,0 +1,1870 @@ +/*------------------------------------------------------------------------------ + * pg_audit.c + * + * An audit logging extension for PostgreSQL. Provides detailed logging classes, + * object level logging, and fully-qualified object names for all DML and DDL + * statements where possible (See pgaudit.sgml for details). + * + * Copyright (c) 2014-2015, PostgreSQL Global Development Group + * + * IDENTIFICATION + * contrib/pg_audit/pg_audit.c + *------------------------------------------------------------------------------ + */ +#include "postgres.h" + +#include "access/htup_details.h" +#include "access/sysattr.h" +#include "access/xact.h" +#include "catalog/catalog.h" +#include "catalog/objectaccess.h" +#include "catalog/pg_class.h" +#include "catalog/namespace.h" +#include "commands/dbcommands.h" +#include "catalog/pg_proc.h" +#include "commands/event_trigger.h" +#include "executor/executor.h" +#include "executor/spi.h" +#include "miscadmin.h" +#include "libpq/auth.h" +#include "nodes/nodes.h" +#include "tcop/utility.h" +#include "utils/acl.h" +#include "utils/builtins.h" +#include "utils/guc.h" +#include "utils/lsyscache.h" +#include "utils/memutils.h" +#include "utils/rel.h" +#include "utils/syscache.h" +#include "utils/timestamp.h" + +PG_MODULE_MAGIC; + +void _PG_init(void); + +/* Prototypes for functions used with event triggers */ +Datum pg_audit_ddl_command_end(PG_FUNCTION_ARGS); +Datum pg_audit_sql_drop(PG_FUNCTION_ARGS); + +PG_FUNCTION_INFO_V1(pg_audit_ddl_command_end); +PG_FUNCTION_INFO_V1(pg_audit_sql_drop); + +/* + * Log Classes + * + * pgAudit categorizes actions into classes (eg: DDL, FUNCTION calls, READ + * queries, WRITE queries). A GUC is provided for the administrator to + * configure which class (or classes) of actions to include in the + * audit log. We track the currently active set of classes using + * auditLogBitmap. + */ + +/* Bits within auditLogBitmap, defines the classes we understand */ +#define LOG_DDL (1 << 0) /* CREATE/DROP/ALTER objects */ +#define LOG_FUNCTION (1 << 1) /* Functions and DO blocks */ +#define LOG_MISC (1 << 2) /* Statements not covered */ +#define LOG_READ (1 << 3) /* SELECTs */ +#define LOG_ROLE (1 << 4) /* GRANT/REVOKE, CREATE/ALTER/DROP ROLE */ +#define LOG_WRITE (1 << 5) /* INSERT, UPDATE, DELETE, TRUNCATE */ + +#define LOG_NONE 0 /* nothing */ +#define LOG_ALL (0xFFFFFFFF) /* All */ + +/* GUC variable for pg_audit.log, which defines the classes to log. */ +char *auditLog = NULL; + +/* Bitmap of classes selected */ +static int auditLogBitmap = LOG_NONE; + +/* + * String constants for log classes - used when processing tokens in the + * pg_audit.log GUC. + */ +#define CLASS_DDL "DDL" +#define CLASS_FUNCTION "FUNCTION" +#define CLASS_MISC "MISC" +#define CLASS_READ "READ" +#define CLASS_ROLE "ROLE" +#define CLASS_WRITE "WRITE" + +#define CLASS_NONE "NONE" +#define CLASS_ALL "ALL" + +/* + * GUC variable for pg_audit.log_catalog + * + * Administrators can choose to NOT log queries when all relations used in + * the query are in pg_catalog. Interactive sessions (eg: psql) can cause + * a lot of noise in the logs which might be uninteresting. + */ +bool auditLogCatalog = true; + +/* + * GUC variable for pg_audit.log_level + * + * Administrators can choose which log level the audit log is to be logged + * at. The default level is LOG, which goes into the server log but does + * not go to the client. Set to NOTICE in the regression tests. + */ +char *auditLogLevelString = NULL; +int auditLogLevel = LOG; + +/* + * GUC variable for pg_audit.log_parameter + * + * Administrators can choose if parameters passed into a statement are + * included in the audit log. + */ +bool auditLogParameter = false; + +/* + * GUC variable for pg_audit.log_relation + * + * Administrators can choose, in SESSION logging, to log each relation involved + * in READ/WRITE class queries. By default, SESSION logs include the query but + * do not have a log entry for each relation. + */ +bool auditLogRelation = false; + +/* + * GUC variable for pg_audit.log_statement_once + * + * Administrators can choose to have the statement run logged only once instead + * of on every line. By default, the statement is repeated on every line of + * the audit log to facilitate searching, but this can cause the log to be + * unnecessairly bloated in some environments. + */ +bool auditLogStatementOnce = false; + +/* + * GUC variable for pg_audit.role + * + * Administrators can choose which role to base OBJECT auditing off of. + * Object-level auditing uses the privileges which are granted to this role to + * determine if a statement should be logged. + */ +char *auditRole = NULL; + +/* + * String constants for the audit log fields. + */ + +/* + * Audit type, which is responsbile for the log message + */ +#define AUDIT_TYPE_OBJECT "OBJECT" +#define AUDIT_TYPE_SESSION "SESSION" + +/* + * Command, used for SELECT/DML and function calls. + * + * We hook into the executor, but we do not have access to the parsetree there. + * Therefore we can't simply call CreateCommandTag() to get the command and have + * to build it ourselves based on what information we do have. + * + * These should be updated if new commands are added to what the exectuor + * currently handles. Note that most of the interesting commands do not go + * through the executor but rather ProcessUtility, where we have the parsetree. + */ +#define COMMAND_SELECT "SELECT" +#define COMMAND_INSERT "INSERT" +#define COMMAND_UPDATE "UPDATE" +#define COMMAND_DELETE "DELETE" +#define COMMAND_EXECUTE "EXECUTE" +#define COMMAND_UNKNOWN "UNKNOWN" + +/* + * Object type, used for SELECT/DML statements and function calls. + * + * For relation objects, this is essentially relkind (though we do not have + * access to a function which will just return a string given a relkind; + * getRelationTypeDescription() comes close but is not public currently). + * + * We also handle functions, so it isn't quite as simple as just relkind. + * + * This should be kept consistent with what is returned from + * pg_event_trigger_ddl_commands(), as that's what we use for DDL. + */ +#define OBJECT_TYPE_TABLE "TABLE" +#define OBJECT_TYPE_INDEX "INDEX" +#define OBJECT_TYPE_SEQUENCE "SEQUENCE" +#define OBJECT_TYPE_TOASTVALUE "TOAST TABLE" +#define OBJECT_TYPE_VIEW "VIEW" +#define OBJECT_TYPE_MATVIEW "MATERIALIZED VIEW" +#define OBJECT_TYPE_COMPOSITE_TYPE "COMPOSITE TYPE" +#define OBJECT_TYPE_FOREIGN_TABLE "FOREIGN TABLE" +#define OBJECT_TYPE_FUNCTION "FUNCTION" + +#define OBJECT_TYPE_UNKNOWN "UNKNOWN" + +/* + * String constants for testing role commands. Rename and drop role statements + * are assigned the nodeTag T_RenameStmt and T_DropStmt respectively. This is + * not very useful for classification, so we resort to comparing strings + * against the result of CreateCommandTag(parsetree). + */ +#define COMMAND_ALTER_ROLE "ALTER ROLE" +#define COMMAND_DROP_ROLE "DROP ROLE" + +/* + * An AuditEvent represents an operation that potentially affects a single + * object. If a statement affects multiple objects then multiple AuditEvents + * are created to represent them. + */ +typedef struct +{ + int64 statementId; /* Simple counter */ + int64 substatementId; /* Simple counter */ + + LogStmtLevel logStmtLevel; /* From GetCommandLogLevel when possible, */ + /* generated when not. */ + NodeTag commandTag; /* same here */ + const char *command; /* same here */ + const char *objectType; /* From event trigger when possible */ + /* generated when not. */ + char *objectName; /* Fully qualified object identification */ + const char *commandText; /* sourceText / queryString */ + ParamListInfo paramList; /* QueryDesc/ProcessUtility parameters */ + + bool granted; /* Audit role has object permissions? */ + bool logged; /* Track if we have logged this event, used */ + /* post-ProcessUtility to make sure we log */ + bool statementLogged; /* Track if we have logged the statement */ +} AuditEvent; + +/* + * A simple FIFO queue to keep track of the current stack of audit events. + */ +typedef struct AuditEventStackItem +{ + struct AuditEventStackItem *next; + + AuditEvent auditEvent; + + int64 stackId; + + MemoryContext contextAudit; + MemoryContextCallback contextCallback; +} AuditEventStackItem; + +AuditEventStackItem *auditEventStack = NULL; + +/* + * pgAudit runs queries of its own when using the event trigger system. + * + * Track when we are running a query and don't log it. + */ +static bool internalStatement = false; + +/* + * Track running total for statements and substatements and whether or not + * anything has been logged since the current statement began. + */ +static int64 statementTotal = 0; +static int64 substatementTotal = 0; +static int64 stackTotal = 0; + +static bool statementLogged = false; + +/* + * Stack functions + * + * Audit events can go down to multiple levels so a stack is maintained to keep + * track of them. + */ + +/* + * Respond to callbacks registered with MemoryContextRegisterResetCallback(). + * Removes the event(s) off the stack that have become obsolete once the + * MemoryContext has been freed. The callback should always be freeing the top + * of the stack, but the code is tolerant of out-of-order callbacks. + */ +static void +stack_free(void *stackFree) +{ + AuditEventStackItem *nextItem = auditEventStack; + + /* Only process if the stack contains items */ + while (nextItem != NULL) + { + /* Check if this item matches the item to be freed */ + if (nextItem == (AuditEventStackItem *)stackFree) + { + /* Move top of stack to the item after the freed item */ + auditEventStack = nextItem->next; + + /* If the stack is not empty */ + if (auditEventStack == NULL) + { + /* + * Reset internal statement to false. Normally this will be + * reset but in case of an error it might be left set. + */ + internalStatement = false; + + /* + * Reset sub statement total so the next statement will start + * from 1. + */ + substatementTotal = 0; + + /* + * Reset statement logged so that next statement will be logged. + */ + statementLogged = false; + } + + return; + } + + nextItem = nextItem->next; + } +} + +/* + * Push a new audit event onto the stack and create a new memory context to + * store it. + */ +static AuditEventStackItem * +stack_push() +{ + MemoryContext contextAudit; + MemoryContext contextOld; + AuditEventStackItem *stackItem; + + /* + * Create a new memory context to contain the stack item. This will be + * free'd on stack_pop, or by our callback when the parent context is + * destroyed. + */ + contextAudit = AllocSetContextCreate(CurrentMemoryContext, + "pg_audit stack context", + ALLOCSET_DEFAULT_MINSIZE, + ALLOCSET_DEFAULT_INITSIZE, + ALLOCSET_DEFAULT_MAXSIZE); + + /* Save the old context to switch back to at the end */ + contextOld = MemoryContextSwitchTo(contextAudit); + + /* Create our new stack item in our context */ + stackItem = palloc0(sizeof(AuditEventStackItem)); + stackItem->contextAudit = contextAudit; + stackItem->stackId = ++stackTotal; + + /* + * Setup a callback in case an error happens. stack_free() will truncate + * the stack at this item. + */ + stackItem->contextCallback.func = stack_free; + stackItem->contextCallback.arg = (void *)stackItem; + MemoryContextRegisterResetCallback(contextAudit, + &stackItem->contextCallback); + + /* Push new item onto the stack */ + if (auditEventStack != NULL) + stackItem->next = auditEventStack; + else + stackItem->next = NULL; + + auditEventStack = stackItem; + + MemoryContextSwitchTo(contextOld); + + return stackItem; +} + +/* + * Pop an audit event from the stack by deleting the memory context that + * contains it. The callback to stack_free() does the actual pop. + */ +static void +stack_pop(int64 stackId) +{ + /* Make sure what we want to delete is at the top of the stack */ + if (auditEventStack != NULL && auditEventStack->stackId == stackId) + MemoryContextDelete(auditEventStack->contextAudit); + else + elog(ERROR, "pg_audit stack item %ld not found on top - cannot pop", + stackId); +} + +/* + * Check that an item is on the stack. If not, an error will be raised since + * this is a bad state to be in and it might mean audit records are being lost. + */ +static void +stack_valid(int64 stackId) +{ + AuditEventStackItem *nextItem = auditEventStack; + + /* Look through the stack for the stack entry */ + while (nextItem != NULL && nextItem->stackId != stackId) + nextItem = nextItem->next; + + /* If we didn't find it, something went wrong. */ + if (nextItem == NULL) + elog(ERROR, "pg_audit stack item %ld not found - top of stack is %ld", + stackId, auditEventStack == NULL ? -1 : auditEventStack->stackId); + + return; +} + +/* + * Appends a properly quoted CSV field to StringInfo. + */ +static void +append_valid_csv(StringInfoData *buffer, const char *appendStr) +{ + const char *pChar; + + /* + * If the append string is null then do nothing. NULL fields are not + * quoted in CSV. + */ + if (appendStr == NULL) + return; + + /* Only format for CSV if appendStr contains: ", comma, \n, \r */ + if (strstr(appendStr, ",") || strstr(appendStr, "\"") || + strstr(appendStr, "\n") || strstr(appendStr, "\r")) + { + appendStringInfoCharMacro(buffer, '"'); + + for (pChar = appendStr; *pChar; pChar++) + { + if (*pChar == '"') /* double single quotes */ + appendStringInfoCharMacro(buffer, *pChar); + + appendStringInfoCharMacro(buffer, *pChar); + } + + appendStringInfoCharMacro(buffer, '"'); + } + /* Else just append */ + else + appendStringInfoString(buffer, appendStr); +} + +/* + * Takes an AuditEvent, classifies it, then logs it if appropriate. + * + * Logging is decided based on if the statement is in one of the classes being + * logged or if an object used has been marked for auditing. + * + * Objects are marked for auditing by the auditor role being granted access + * to the object. The kind of access (INSERT, UPDATE, etc) is also considered + * and logging is only performed when the kind of access matches the granted + * right on the object. + * + * This will need to be updated if new kinds of GRANTs are added. + */ +static void +log_audit_event(AuditEventStackItem *stackItem) +{ + /* By default, put everything in the MISC class. */ + int class = LOG_MISC; + const char *className = CLASS_MISC; + MemoryContext contextOld; + StringInfoData auditStr; + + + /* Classify the statement using log stmt level and the command tag */ + switch (stackItem->auditEvent.logStmtLevel) + { + /* All mods go in WRITE class, execpt EXECUTE */ + case LOGSTMT_MOD: + className = CLASS_WRITE; + class = LOG_WRITE; + + switch (stackItem->auditEvent.commandTag) + { + /* Currently, only EXECUTE is different */ + case T_ExecuteStmt: + className = CLASS_MISC; + class = LOG_MISC; + break; + default: + break; + } + break; + + /* These are DDL, unless they are ROLE */ + case LOGSTMT_DDL: + className = CLASS_DDL; + class = LOG_DDL; + + /* Identify role statements */ + switch (stackItem->auditEvent.commandTag) + { + /* We know these are all role statements */ + case T_GrantStmt: + case T_GrantRoleStmt: + case T_CreateRoleStmt: + case T_DropRoleStmt: + case T_AlterRoleStmt: + case T_AlterRoleSetStmt: + className = CLASS_ROLE; + class = LOG_ROLE; + break; + /* + * Rename and Drop are general and therefore we have to do an + * additional check against the command string to see if they + * are role or regular DDL. + */ + case T_RenameStmt: + case T_DropStmt: + if (pg_strcasecmp(stackItem->auditEvent.command, + COMMAND_ALTER_ROLE) == 0 || + pg_strcasecmp(stackItem->auditEvent.command, + COMMAND_DROP_ROLE) == 0) + { + className = CLASS_ROLE; + class = LOG_ROLE; + } + break; + + default: + break; + } + break; + + /* Classify the rest */ + case LOGSTMT_ALL: + switch (stackItem->auditEvent.commandTag) + { + /* READ statements */ + case T_CopyStmt: + case T_SelectStmt: + case T_PrepareStmt: + case T_PlannedStmt: + className = CLASS_READ; + class = LOG_READ; + break; + + /* Reindex is DDL (because cluster is DDL) */ + case T_ReindexStmt: + className = CLASS_DDL; + class = LOG_DDL; + break; + + /* FUNCTION statements */ + case T_DoStmt: + className = CLASS_FUNCTION; + class = LOG_FUNCTION; + break; + + default: + break; + } + break; + + case LOGSTMT_NONE: + break; + } + + /* + * Only log the statement if: + * + * 1. If object was selected for audit logging (granted) + * 2. The statement belongs to a class that is being logged + * + * If neither of these is true, return. + */ + if (!stackItem->auditEvent.granted && !(auditLogBitmap & class)) + return; + + /* + * Use audit memory context in case something is not free'd while + * appending strings and parameters. + */ + contextOld = MemoryContextSwitchTo(stackItem->contextAudit); + + /* Set statement and substatement IDs */ + if (stackItem->auditEvent.statementId == 0) + { + /* If nothing has been logged yet then create a new statement Id */ + if (!statementLogged) + { + statementTotal++; + statementLogged = true; + } + + stackItem->auditEvent.statementId = statementTotal; + stackItem->auditEvent.substatementId = ++substatementTotal; + } + + /* + * Create the audit substring + * + * The type-of-audit-log and statement/substatement ID are handled below, + * this string is everything else. + */ + initStringInfo(&auditStr); + append_valid_csv(&auditStr, stackItem->auditEvent.command); + + appendStringInfoCharMacro(&auditStr, ','); + append_valid_csv(&auditStr, stackItem->auditEvent.objectType); + + appendStringInfoCharMacro(&auditStr, ','); + append_valid_csv(&auditStr, stackItem->auditEvent.objectName); + + /* + * If auditLogStatmentOnce is true, then only log the statement and + * parameters if they have not already been logged for this substatement. + */ + appendStringInfoCharMacro(&auditStr, ','); + if (!stackItem->auditEvent.statementLogged || !auditLogStatementOnce) + { + append_valid_csv(&auditStr, stackItem->auditEvent.commandText); + + appendStringInfoCharMacro(&auditStr, ','); + + /* Handle parameter logging, if enabled. */ + if (auditLogParameter) + { + int paramIdx; + int numParams; + StringInfoData paramStrResult; + ParamListInfo paramList = stackItem->auditEvent.paramList; + + numParams = paramList == NULL ? 0 : paramList->numParams; + + /* Create the param substring */ + initStringInfo(¶mStrResult); + + /* Iterate through all params */ + for (paramIdx = 0; paramList != NULL && paramIdx < numParams; + paramIdx++) + { + ParamExternData *prm = ¶mList->params[paramIdx]; + Oid typeOutput; + bool typeIsVarLena; + char *paramStr; + + /* Add a comma for each param */ + if (paramIdx != 0) + appendStringInfoCharMacro(¶mStrResult, ','); + + /* Skip if null or if oid is invalid */ + if (prm->isnull || !OidIsValid(prm->ptype)) + continue; + + /* Output the string */ + getTypeOutputInfo(prm->ptype, &typeOutput, &typeIsVarLena); + paramStr = OidOutputFunctionCall(typeOutput, prm->value); + + append_valid_csv(¶mStrResult, paramStr); + pfree(paramStr); + } + + if (numParams == 0) + appendStringInfoString(&auditStr, ""); + else + append_valid_csv(&auditStr, paramStrResult.data); + } + else + appendStringInfoString(&auditStr, ""); + + stackItem->auditEvent.statementLogged = true; + } + else + /* we were asked to not log it */ + appendStringInfoString(&auditStr, + ","); + + /* Log the audit entry */ + elog(auditLogLevel, "AUDIT: %s,%ld,%ld,%s,%s", + stackItem->auditEvent.granted ? + AUDIT_TYPE_OBJECT : AUDIT_TYPE_SESSION, + stackItem->auditEvent.statementId, + stackItem->auditEvent.substatementId, + className, auditStr.data); + + stackItem->auditEvent.logged = true; + + MemoryContextSwitchTo(contextOld); +} + +/* + * Check if the role or any inherited role has any permission in the mask. The + * public role is excluded from this check and superuser permissions are not + * considered. + */ +static bool +audit_on_acl(Datum aclDatum, + Oid auditOid, + AclMode mask) +{ + bool result = false; + Acl *acl; + AclItem *aclItemData; + int aclIndex; + int aclTotal; + + /* Detoast column's ACL if necessary */ + acl = DatumGetAclP(aclDatum); + + /* Get the acl list and total number of items */ + aclTotal = ACL_NUM(acl); + aclItemData = ACL_DAT(acl); + + /* Check privileges granted directly to auditOid */ + for (aclIndex = 0; aclIndex < aclTotal; aclIndex++) + { + AclItem *aclItem = &aclItemData[aclIndex]; + + if (aclItem->ai_grantee == auditOid && + aclItem->ai_privs & mask) + { + result = true; + break; + } + } + + /* + * Check privileges granted indirectly via role memberships. We do this in + * a separate pass to minimize expensive indirect membership tests. In + * particular, it's worth testing whether a given ACL entry grants any + * privileges still of interest before we perform the has_privs_of_role + * test. + */ + if (!result) + { + for (aclIndex = 0; aclIndex < aclTotal; aclIndex++) + { + AclItem *aclItem = &aclItemData[aclIndex]; + + /* Don't test public or auditOid (it has been tested already) */ + if (aclItem->ai_grantee == ACL_ID_PUBLIC || + aclItem->ai_grantee == auditOid) + continue; + + /* + * Check that the role has the required privileges and that it is + * inherited by auditOid. + */ + if (aclItem->ai_privs & mask && + has_privs_of_role(auditOid, aclItem->ai_grantee)) + { + result = true; + break; + } + } + } + + /* if we have a detoasted copy, free it */ + if (acl && (Pointer) acl != DatumGetPointer(aclDatum)) + pfree(acl); + + return result; +} + +/* + * Check if a role has any of the permissions in the mask on a relation. + */ +static bool +audit_on_relation(Oid relOid, + Oid auditOid, + AclMode mask) +{ + bool result = false; + HeapTuple tuple; + Datum aclDatum; + bool isNull; + + /* Get relation tuple from pg_class */ + tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid)); + if (!HeapTupleIsValid(tuple)) + return false; + + /* Get the relation's ACL */ + aclDatum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_relacl, + &isNull); + + /* Only check if non-NULL, since NULL means no permissions */ + if (!isNull) + result = audit_on_acl(aclDatum, auditOid, mask); + + /* Free the relation tuple */ + ReleaseSysCache(tuple); + + return result; +} + +/* + * Check if a role has any of the permissions in the mask on a column. + */ +static bool +audit_on_attribute(Oid relOid, + AttrNumber attNum, + Oid auditOid, + AclMode mask) +{ + bool result = false; + HeapTuple attTuple; + Datum aclDatum; + bool isNull; + + /* Get the attribute's ACL */ + attTuple = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(relOid), + Int16GetDatum(attNum)); + if (!HeapTupleIsValid(attTuple)) + return false; + + /* Only consider attributes that have not been dropped */ + if (!((Form_pg_attribute) GETSTRUCT(attTuple))->attisdropped) + { + aclDatum = SysCacheGetAttr(ATTNUM, attTuple, Anum_pg_attribute_attacl, + &isNull); + + if (!isNull) + result = audit_on_acl(aclDatum, auditOid, mask); + } + + /* Free attribute */ + ReleaseSysCache(attTuple); + + return result; +} + +/* + * Check if a role has any of the permissions in the mask on a column in + * the provided set. If the set is empty, then all valid columns in the + * relation will be tested. + */ +static bool +audit_on_any_attribute(Oid relOid, + Oid auditOid, + Bitmapset *attributeSet, + AclMode mode) +{ + bool result = false; + AttrNumber col; + Bitmapset *tmpSet; + + /* If bms is empty then check for any column match */ + if (bms_is_empty(attributeSet)) + { + HeapTuple classTuple; + AttrNumber nattrs; + AttrNumber curr_att; + + /* Get relation to determine total columns */ + classTuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid)); + + if (!HeapTupleIsValid(classTuple)) + return false; + + nattrs = ((Form_pg_class) GETSTRUCT(classTuple))->relnatts; + ReleaseSysCache(classTuple); + + /* Check each column */ + for (curr_att = 1; curr_att <= nattrs; curr_att++) + if (audit_on_attribute(relOid, curr_att, auditOid, mode)) + return true; + } + + /* bms_first_member is destructive, so make a copy before using it. */ + tmpSet = bms_copy(attributeSet); + + /* Check each column */ + while ((col = bms_first_member(tmpSet)) >= 0) + { + col += FirstLowInvalidHeapAttributeNumber; + + if (col != InvalidAttrNumber && + audit_on_attribute(relOid, col, auditOid, mode)) + { + result = true; + break; + } + } + + bms_free(tmpSet); + + return result; +} + +/* + * Create AuditEvents for SELECT/DML operations via executor permissions checks. + */ +static void +log_select_dml(Oid auditOid, List *rangeTabls) +{ + ListCell *lr; + bool first = true; + bool found = false; + + /* Do not log if this is an internal statement */ + if (internalStatement) + return; + + foreach(lr, rangeTabls) + { + Oid relOid; + Relation rel; + RangeTblEntry *rte = lfirst(lr); + + /* We only care about tables, and can ignore subqueries etc. */ + if (rte->rtekind != RTE_RELATION) + continue; + + found = true; + + /* + * If we are not logging all-catalog queries (auditLogCatalog is false) + * then filter out any system relations here. + */ + relOid = rte->relid; + rel = relation_open(relOid, NoLock); + + if (!auditLogCatalog && IsSystemNamespace(RelationGetNamespace(rel))) + { + relation_close(rel, NoLock); + continue; + } + + /* + * Default is that this was not through a grant, to support session + * logging. Will be updated below if a grant is found. + */ + auditEventStack->auditEvent.granted = false; + + /* + * If this is the first RTE then session log unless auditLogRelation + * is set. + */ + if (first && !auditLogRelation) + { + log_audit_event(auditEventStack); + + first = false; + } + + /* + * We don't have access to the parsetree here, so we have to generate + * the node type, object type, and command tag by decoding + * rte->requiredPerms and rte->relkind. + */ + if (rte->requiredPerms & ACL_INSERT) + { + auditEventStack->auditEvent.logStmtLevel = LOGSTMT_MOD; + auditEventStack->auditEvent.commandTag = T_InsertStmt; + auditEventStack->auditEvent.command = COMMAND_INSERT; + } + else if (rte->requiredPerms & ACL_UPDATE) + { + auditEventStack->auditEvent.logStmtLevel = LOGSTMT_MOD; + auditEventStack->auditEvent.commandTag = T_UpdateStmt; + auditEventStack->auditEvent.command = COMMAND_UPDATE; + } + else if (rte->requiredPerms & ACL_DELETE) + { + auditEventStack->auditEvent.logStmtLevel = LOGSTMT_MOD; + auditEventStack->auditEvent.commandTag = T_DeleteStmt; + auditEventStack->auditEvent.command = COMMAND_DELETE; + } + else if (rte->requiredPerms & ACL_SELECT) + { + auditEventStack->auditEvent.logStmtLevel = LOGSTMT_ALL; + auditEventStack->auditEvent.commandTag = T_SelectStmt; + auditEventStack->auditEvent.command = COMMAND_SELECT; + } + else + { + auditEventStack->auditEvent.logStmtLevel = LOGSTMT_ALL; + auditEventStack->auditEvent.commandTag = T_Invalid; + auditEventStack->auditEvent.command = COMMAND_UNKNOWN; + } + + /* Use the relation type to assign object type */ + switch (rte->relkind) + { + case RELKIND_RELATION: + auditEventStack->auditEvent.objectType = + OBJECT_TYPE_TABLE; + break; + + case RELKIND_INDEX: + auditEventStack->auditEvent.objectType = + OBJECT_TYPE_INDEX; + break; + + case RELKIND_SEQUENCE: + auditEventStack->auditEvent.objectType = + OBJECT_TYPE_SEQUENCE; + break; + + case RELKIND_TOASTVALUE: + auditEventStack->auditEvent.objectType = + OBJECT_TYPE_TOASTVALUE; + break; + + case RELKIND_VIEW: + auditEventStack->auditEvent.objectType = + OBJECT_TYPE_VIEW; + break; + + case RELKIND_COMPOSITE_TYPE: + auditEventStack->auditEvent.objectType = + OBJECT_TYPE_COMPOSITE_TYPE; + break; + + case RELKIND_FOREIGN_TABLE: + auditEventStack->auditEvent.objectType = + OBJECT_TYPE_FOREIGN_TABLE; + break; + + case RELKIND_MATVIEW: + auditEventStack->auditEvent.objectType = + OBJECT_TYPE_MATVIEW; + break; + + default: + auditEventStack->auditEvent.objectType = + OBJECT_TYPE_UNKNOWN; + break; + } + + /* Get a copy of the relation name and assign it to object name */ + auditEventStack->auditEvent.objectName = + quote_qualified_identifier(get_namespace_name( + RelationGetNamespace(rel)), + RelationGetRelationName(rel)); + relation_close(rel, NoLock); + + /* Perform object auditing only if the audit role is valid */ + if (auditOid != InvalidOid) + { + AclMode auditPerms = + (ACL_SELECT | ACL_UPDATE | ACL_INSERT | ACL_DELETE) & + rte->requiredPerms; + + /* + * If any of the required permissions for the relation are granted + * to the audit role then audit the relation + */ + if (audit_on_relation(relOid, auditOid, auditPerms)) + auditEventStack->auditEvent.granted = true; + + /* + * Else check if the audit role has column-level permissions for + * select, insert, or update. + */ + else if (auditPerms != 0) + { + /* + * Check the select columns + */ + if (auditPerms & ACL_SELECT) + auditEventStack->auditEvent.granted = + audit_on_any_attribute(relOid, auditOid, + rte->selectedCols, + ACL_SELECT); + + /* + * Check the insert columns + */ + if (!auditEventStack->auditEvent.granted && + auditPerms & ACL_INSERT) + auditEventStack->auditEvent.granted = + audit_on_any_attribute(relOid, auditOid, + rte->insertedCols, + auditPerms); + + /* + * Check the update columns + */ + if (!auditEventStack->auditEvent.granted && + auditPerms & ACL_UPDATE) + auditEventStack->auditEvent.granted = + audit_on_any_attribute(relOid, auditOid, + rte->updatedCols, + auditPerms); + } + } + + /* Do relation level logging if a grant was found */ + if (auditEventStack->auditEvent.granted) + { + auditEventStack->auditEvent.logged = false; + log_audit_event(auditEventStack); + } + + /* Do relation level logging if auditLogRelation is set */ + if (auditLogRelation) + { + auditEventStack->auditEvent.logged = false; + auditEventStack->auditEvent.granted = false; + log_audit_event(auditEventStack); + } + + pfree(auditEventStack->auditEvent.objectName); + } + + /* + * If no tables were found that means that RangeTbls was empty or all + * relations were in the system schema. In that case still log a + * session record. + */ + if (!found) + { + auditEventStack->auditEvent.granted = false; + auditEventStack->auditEvent.logged = false; + + log_audit_event(auditEventStack); + } +} + +/* + * Create AuditEvents for non-catalog function execution, as detected by + * log_object_access() below. + */ +static void +log_function_execute(Oid objectId) +{ + HeapTuple proctup; + Form_pg_proc proc; + AuditEventStackItem *stackItem; + + /* Get info about the function. */ + proctup = SearchSysCache1(PROCOID, ObjectIdGetDatum(objectId)); + + if (!proctup) + elog(ERROR, "cache lookup failed for function %u", objectId); + + proc = (Form_pg_proc) GETSTRUCT(proctup); + + /* + * Logging execution of all pg_catalog functions would make the log + * unusably noisy. + */ + if (IsSystemNamespace(proc->pronamespace)) + { + ReleaseSysCache(proctup); + return; + } + + /* Push audit event onto the stack */ + stackItem = stack_push(); + + /* Generate the fully-qualified function name. */ + stackItem->auditEvent.objectName = + quote_qualified_identifier(get_namespace_name(proc->pronamespace), + NameStr(proc->proname)); + ReleaseSysCache(proctup); + + /* Log the function call */ + stackItem->auditEvent.logStmtLevel = LOGSTMT_ALL; + stackItem->auditEvent.commandTag = T_DoStmt; + stackItem->auditEvent.command = COMMAND_EXECUTE; + stackItem->auditEvent.objectType = OBJECT_TYPE_FUNCTION; + stackItem->auditEvent.commandText = stackItem->next->auditEvent.commandText; + + log_audit_event(stackItem); + + /* Pop audit event from the stack */ + stack_pop(stackItem->stackId); +} + +/* + * Hook functions + */ +static ExecutorCheckPerms_hook_type next_ExecutorCheckPerms_hook = NULL; +static ProcessUtility_hook_type next_ProcessUtility_hook = NULL; +static object_access_hook_type next_object_access_hook = NULL; +static ExecutorStart_hook_type next_ExecutorStart_hook = NULL; + +/* + * Hook ExecutorStart to get the query text and basic command type for queries + * that do not contain a table and so can't be idenitified accurately in + * ExecutorCheckPerms. + */ +static void +pg_audit_ExecutorStart_hook(QueryDesc *queryDesc, int eflags) +{ + AuditEventStackItem *stackItem = NULL; + + if (!internalStatement) + { + /* Push the audit even onto the stack */ + stackItem = stack_push(); + + /* Initialize command using queryDesc->operation */ + switch (queryDesc->operation) + { + case CMD_SELECT: + stackItem->auditEvent.logStmtLevel = LOGSTMT_ALL; + stackItem->auditEvent.commandTag = T_SelectStmt; + stackItem->auditEvent.command = COMMAND_SELECT; + break; + + case CMD_INSERT: + stackItem->auditEvent.logStmtLevel = LOGSTMT_MOD; + stackItem->auditEvent.commandTag = T_InsertStmt; + stackItem->auditEvent.command = COMMAND_INSERT; + break; + + case CMD_UPDATE: + stackItem->auditEvent.logStmtLevel = LOGSTMT_MOD; + stackItem->auditEvent.commandTag = T_UpdateStmt; + stackItem->auditEvent.command = COMMAND_UPDATE; + break; + + case CMD_DELETE: + stackItem->auditEvent.logStmtLevel = LOGSTMT_MOD; + stackItem->auditEvent.commandTag = T_DeleteStmt; + stackItem->auditEvent.command = COMMAND_DELETE; + break; + + default: + stackItem->auditEvent.logStmtLevel = LOGSTMT_ALL; + stackItem->auditEvent.commandTag = T_Invalid; + stackItem->auditEvent.command = COMMAND_UNKNOWN; + break; + } + + /* Initialize the audit event */ + stackItem->auditEvent.commandText = queryDesc->sourceText; + stackItem->auditEvent.paramList = queryDesc->params; + } + + /* Call the previous hook or standard function */ + if (next_ExecutorStart_hook) + next_ExecutorStart_hook(queryDesc, eflags); + else + standard_ExecutorStart(queryDesc, eflags); + + /* + * Move the stack memory context to the query memory context. This needs to + * be done here because the query context does not exist before the call + * to standard_ExecutorStart() but the stack item is required by + * pg_audit_ExecutorCheckPerms_hook() which is called during + * standard_ExecutorStart(). + */ + if (stackItem) + MemoryContextSetParent(stackItem->contextAudit, + queryDesc->estate->es_query_cxt); +} + +/* + * Hook ExecutorCheckPerms to do session and object auditing for DML. + */ +static bool +pg_audit_ExecutorCheckPerms_hook(List *rangeTabls, bool abort) +{ + Oid auditOid; + + /* Get the audit oid if the role exists */ + auditOid = get_role_oid(auditRole, true); + + /* Log DML if the audit role is valid or session logging is enabled */ + if ((auditOid != InvalidOid || auditLogBitmap != 0) && + !IsAbortedTransactionBlockState()) + log_select_dml(auditOid, rangeTabls); + + /* Call the next hook function */ + if (next_ExecutorCheckPerms_hook && + !(*next_ExecutorCheckPerms_hook) (rangeTabls, abort)) + return false; + + return true; +} + +/* + * Hook ProcessUtility to do session auditing for DDL and utility commands. + */ +static void +pg_audit_ProcessUtility_hook(Node *parsetree, + const char *queryString, + ProcessUtilityContext context, + ParamListInfo params, + DestReceiver *dest, + char *completionTag) +{ + AuditEventStackItem *stackItem = NULL; + int64 stackId; + + /* + * Don't audit substatements. All the substatements we care about should + * be covered by the event triggers. + */ + if (context <= PROCESS_UTILITY_QUERY && !IsAbortedTransactionBlockState()) + { + /* Process top level utility statement */ + if (context == PROCESS_UTILITY_TOPLEVEL) + { + if (auditEventStack != NULL) + elog(ERROR, "pg_audit stack is not empty"); + + stackItem = stack_push(); + stackItem->auditEvent.paramList = params; + } + else + stackItem = stack_push(); + + stackId = stackItem->stackId; + stackItem->auditEvent.logStmtLevel = GetCommandLogLevel(parsetree); + stackItem->auditEvent.commandTag = nodeTag(parsetree); + stackItem->auditEvent.command = CreateCommandTag(parsetree); + stackItem->auditEvent.commandText = queryString; + + /* + * If this is a DO block log it before calling the next ProcessUtility + * hook. + */ + if (auditLogBitmap & LOG_FUNCTION && + stackItem->auditEvent.commandTag == T_DoStmt && + !IsAbortedTransactionBlockState()) + log_audit_event(stackItem); + } + + /* Call the standard process utility chain. */ + if (next_ProcessUtility_hook) + (*next_ProcessUtility_hook) (parsetree, queryString, context, + params, dest, completionTag); + else + standard_ProcessUtility(parsetree, queryString, context, + params, dest, completionTag); + + /* + * Process the audit event if there is one. Also check that this event was + * not popped off the stack by a memory context being free'd elsewhere. + */ + if (stackItem && !IsAbortedTransactionBlockState()) + { + /* + * Make sure the item we want to log is still on the stack - if not then + * something has gone wrong and an error will be raised. + */ + stack_valid(stackId); + + /* Log the utility command if logging is on, the command has not already + * been logged by another hook, and the transaction is not aborted. + */ + if (auditLogBitmap != 0 && !stackItem->auditEvent.logged) + log_audit_event(stackItem); + } +} + +/* + * Hook object_access_hook to provide fully-qualified object names for function + * calls. + */ +static void +pg_audit_object_access_hook(ObjectAccessType access, + Oid classId, + Oid objectId, + int subId, + void *arg) +{ + if (auditLogBitmap & LOG_FUNCTION && access == OAT_FUNCTION_EXECUTE && + auditEventStack && !IsAbortedTransactionBlockState()) + log_function_execute(objectId); + + if (next_object_access_hook) + (*next_object_access_hook) (access, classId, objectId, subId, arg); +} + +/* + * Event trigger functions + */ + +/* + * Supply additional data for (non drop) statements that have event trigger + * support and can be deparsed. + * + * Drop statements are handled below through the older sql_drop event trigger. + */ +Datum +pg_audit_ddl_command_end(PG_FUNCTION_ARGS) +{ + EventTriggerData *eventData; + int result, row; + TupleDesc spiTupDesc; + const char *query; + MemoryContext contextQuery; + MemoryContext contextOld; + + /* Continue only if session DDL logging is enabled */ + if (~auditLogBitmap & LOG_DDL) + PG_RETURN_NULL(); + + /* Be sure the module was loaded */ + if (!auditEventStack) + elog(ERROR, "pg_audit not loaded before call to " + "pg_audit_ddl_command_end()"); + + /* This is an internal statement - do not log it */ + internalStatement = true; + + /* Make sure the fuction was fired as a trigger */ + if (!CALLED_AS_EVENT_TRIGGER(fcinfo)) + elog(ERROR, "not fired by event trigger manager"); + + /* Switch memory context for query */ + contextQuery = AllocSetContextCreate( + CurrentMemoryContext, + "pg_audit_func_ddl_command_end temporary context", + ALLOCSET_DEFAULT_MINSIZE, + ALLOCSET_DEFAULT_INITSIZE, + ALLOCSET_DEFAULT_MAXSIZE); + contextOld = MemoryContextSwitchTo(contextQuery); + + /* Get information about triggered events */ + eventData = (EventTriggerData *) fcinfo->context; + + auditEventStack->auditEvent.logStmtLevel = + GetCommandLogLevel(eventData->parsetree); + auditEventStack->auditEvent.commandTag = + nodeTag(eventData->parsetree); + auditEventStack->auditEvent.command = + CreateCommandTag(eventData->parsetree); + + /* Return objects affected by the (non drop) DDL statement */ + query = "SELECT UPPER(object_type), object_identity\n" + " FROM pg_event_trigger_ddl_commands()"; + + /* Attempt to connect */ + result = SPI_connect(); + if (result < 0) + elog(ERROR, "pg_audit_ddl_command_end: SPI_connect returned %d", + result); + + /* Execute the query */ + result = SPI_execute(query, true, 0); + if (result != SPI_OK_SELECT) + elog(ERROR, "pg_audit_ddl_command_end: SPI_execute returned %d", + result); + + /* Iterate returned rows */ + spiTupDesc = SPI_tuptable->tupdesc; + for (row = 0; row < SPI_processed; row++) + { + HeapTuple spiTuple; + + spiTuple = SPI_tuptable->vals[row]; + + /* Supply object name and type for audit event */ + auditEventStack->auditEvent.objectType = + SPI_getvalue(spiTuple, spiTupDesc, 1); + auditEventStack->auditEvent.objectName = + SPI_getvalue(spiTuple, spiTupDesc, 2); + + /* Log the audit event */ + log_audit_event(auditEventStack); + } + + /* Complete the query */ + SPI_finish(); + + MemoryContextSwitchTo(contextOld); + MemoryContextDelete(contextQuery); + + /* No longer in an internal statement */ + internalStatement = false; + + PG_RETURN_NULL(); +} + +/* + * Supply additional data for drop statements that have event trigger support. + */ +Datum +pg_audit_sql_drop(PG_FUNCTION_ARGS) +{ + int result, row; + TupleDesc spiTupDesc; + const char *query; + MemoryContext contextQuery; + MemoryContext contextOld; + + if (~auditLogBitmap & LOG_DDL) + PG_RETURN_NULL(); + + /* Be sure the module was loaded */ + if (!auditEventStack) + elog(ERROR, "pg_audit not loaded before call to " + "pg_audit_sql_drop()"); + + /* This is an internal statement - do not log it */ + internalStatement = true; + + /* Make sure the fuction was fired as a trigger */ + if (!CALLED_AS_EVENT_TRIGGER(fcinfo)) + elog(ERROR, "not fired by event trigger manager"); + + /* Switch memory context for the query */ + contextQuery = AllocSetContextCreate( + CurrentMemoryContext, + "pg_audit_func_ddl_command_end temporary context", + ALLOCSET_DEFAULT_MINSIZE, + ALLOCSET_DEFAULT_INITSIZE, + ALLOCSET_DEFAULT_MAXSIZE); + contextOld = MemoryContextSwitchTo(contextQuery); + + /* Return objects affected by the drop statement */ + query = "SELECT UPPER(object_type),\n" + " object_identity\n" + " FROM pg_event_trigger_dropped_objects()\n" + " WHERE lower(object_type) <> 'type'\n" + " AND schema_name <> 'pg_toast'"; + + /* Attempt to connect */ + result = SPI_connect(); + if (result < 0) + elog(ERROR, "pg_audit_ddl_drop: SPI_connect returned %d", + result); + + /* Execute the query */ + result = SPI_execute(query, true, 0); + if (result != SPI_OK_SELECT) + elog(ERROR, "pg_audit_ddl_drop: SPI_execute returned %d", + result); + + /* Iterate returned rows */ + spiTupDesc = SPI_tuptable->tupdesc; + for (row = 0; row < SPI_processed; row++) + { + HeapTuple spiTuple; + + spiTuple = SPI_tuptable->vals[row]; + + auditEventStack->auditEvent.objectType = + SPI_getvalue(spiTuple, spiTupDesc, 1); + auditEventStack->auditEvent.objectName = + SPI_getvalue(spiTuple, spiTupDesc, 2); + + log_audit_event(auditEventStack); + } + + /* Complete the query */ + SPI_finish(); + + MemoryContextSwitchTo(contextOld); + MemoryContextDelete(contextQuery); + + /* No longer in an internal statement */ + internalStatement = false; + + PG_RETURN_NULL(); +} + +/* + * GUC check and assign functions + */ + +/* + * Take a pg_audit.log value such as "read, write, dml", verify that each of the + * comma-separated tokens corresponds to a LogClass value, and convert them into + * a bitmap that log_audit_event can check. + */ +static bool +check_pg_audit_log(char **newVal, void **extra, GucSource source) +{ + List *flagRawList; + char *rawVal; + ListCell *lt; + int *flags; + + /* Make sure newval is a comma-separated list of tokens. */ + rawVal = pstrdup(*newVal); + if (!SplitIdentifierString(rawVal, ',', &flagRawList)) + { + GUC_check_errdetail("List syntax is invalid"); + list_free(flagRawList); + pfree(rawVal); + return false; + } + + /* + * Check that we recognise each token, and add it to the bitmap we're + * building up in a newly-allocated int *f. + */ + if (!(flags = (int *)malloc(sizeof(int)))) + return false; + + *flags = 0; + + foreach(lt, flagRawList) + { + bool subtract = false; + int class; + + /* Retrieve a token */ + char *token = (char *)lfirst(lt); + + /* If token is preceded by -, then the token is subtractive */ + if (strstr(token, "-") == token) + { + token = token + 1; + subtract = true; + } + + /* Test each token */ + if (pg_strcasecmp(token, CLASS_NONE) == 0) + class = LOG_NONE; + else if (pg_strcasecmp(token, CLASS_ALL) == 0) + class = LOG_ALL; + else if (pg_strcasecmp(token, CLASS_DDL) == 0) + class = LOG_DDL; + else if (pg_strcasecmp(token, CLASS_FUNCTION) == 0) + class = LOG_FUNCTION; + else if (pg_strcasecmp(token, CLASS_MISC) == 0) + class = LOG_MISC; + else if (pg_strcasecmp(token, CLASS_READ) == 0) + class = LOG_READ; + else if (pg_strcasecmp(token, CLASS_ROLE) == 0) + class = LOG_ROLE; + else if (pg_strcasecmp(token, CLASS_WRITE) == 0) + class = LOG_WRITE; + else + { + free(flags); + pfree(rawVal); + list_free(flagRawList); + return false; + } + + /* Add or subtract class bits from the log bitmap */ + if (subtract) + *flags &= ~class; + else + *flags |= class; + } + + pfree(rawVal); + list_free(flagRawList); + + /* Store the bitmap for assign_pg_audit_log */ + *extra = flags; + + return true; +} + +/* + * Set pg_audit_log from extra (ignoring newVal, which has already been + * converted to a bitmap above). Note that extra may not be set if the + * assignment is to be suppressed. + */ +static void +assign_pg_audit_log(const char *newVal, void *extra) +{ + if (extra) + auditLogBitmap = *(int *)extra; +} + +/* + * Take a pg_audit.log_level value such as "debug" and check that is is valid. + * Return the enum value so it does not have to be checked again in the assign + * function. + */ +static bool +check_pg_audit_log_level(char **newVal, void **extra, GucSource source) +{ + int *logLevel; + + /* Allocate memory to store the log level */ + if (!(logLevel = (int *)malloc(sizeof(int)))) + return false; + + /* Find the log level enum */ + if (pg_strcasecmp(*newVal, "debug") == 0) + *logLevel = DEBUG2; + else if (pg_strcasecmp(*newVal, "debug5") == 0) + *logLevel = DEBUG5; + else if (pg_strcasecmp(*newVal, "debug4") == 0) + *logLevel = DEBUG4; + else if (pg_strcasecmp(*newVal, "debug3") == 0) + *logLevel = DEBUG3; + else if (pg_strcasecmp(*newVal, "debug2") == 0) + *logLevel = DEBUG2; + else if (pg_strcasecmp(*newVal, "debug1") == 0) + *logLevel = DEBUG1; + else if (pg_strcasecmp(*newVal, "info") == 0) + *logLevel = INFO; + else if (pg_strcasecmp(*newVal, "notice") == 0) + *logLevel = NOTICE; + else if (pg_strcasecmp(*newVal, "warning") == 0) + *logLevel = WARNING; + else if (pg_strcasecmp(*newVal, "error") == 0) + *logLevel = ERROR; + else if (pg_strcasecmp(*newVal, "log") == 0) + *logLevel = LOG; + else if (pg_strcasecmp(*newVal, "fatal") == 0) + *logLevel = FATAL; + else if (pg_strcasecmp(*newVal, "panic") == 0) + *logLevel = PANIC; + + /* Error if the log level enum is not found */ + else + { + free(logLevel); + return false; + } + + /* Return the log level enum */ + *extra = logLevel; + + return true; +} + +/* + * Set pg_audit_log from extra (ignoring newVal, which has already been + * converted to an enum above). Note that extra may not be set if the + * assignment is to be suppressed. + */ +static void +assign_pg_audit_log_level(const char *newVal, void *extra) +{ + if (extra) + auditLogLevel = *(int *)extra; +} + +/* + * Define GUC variables and install hooks upon module load. + */ +void +_PG_init(void) +{ + if (IsUnderPostmaster) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("pg_audit must be loaded via shared_preload_libraries"))); + + /* Define pg_audit.log */ + DefineCustomStringVariable( + "pg_audit.log", + + "Specifies which classes of statements will be logged by session audit " + "logging. Multiple classes can be provided using a comma-separated " + "list and classes can be subtracted by prefacing the class with a " + "- sign.", + + NULL, + &auditLog, + "none", + PGC_SUSET, + GUC_LIST_INPUT | GUC_NOT_IN_SAMPLE, + check_pg_audit_log, + assign_pg_audit_log, + NULL); + + /* Define pg_audit.log_catalog */ + DefineCustomBoolVariable( + "pg_audit.log_catalog", + + "Specifies that session logging should be enabled in the case where " + "all relations in a statement are in pg_catalog. Disabling this " + "setting will reduce noise in the log from tools like psql and PgAdmin " + "that query the catalog heavily.", + + NULL, + &auditLogCatalog, + true, + PGC_SUSET, + GUC_NOT_IN_SAMPLE, + NULL, NULL, NULL); + + /* Define pg_audit.log_level */ + DefineCustomStringVariable( + "pg_audit.log_level", + + "Specifies the log level that will be used for log entries. This " + "setting is used for regression testing and may also be useful to end " + "users for testing or other purposes. It is not intended to be used " + "in a production environment as it may leak which statements are being " + "logged to the user.", + + NULL, + &auditLogLevelString, + "log", + PGC_SUSET, + GUC_LIST_INPUT | GUC_NOT_IN_SAMPLE, + check_pg_audit_log_level, + assign_pg_audit_log_level, + NULL); + + /* Define pg_audit.log_parameter */ + DefineCustomBoolVariable( + "pg_audit.log_parameter", + + "Specifies that audit logging should include the parameters that were " + "passed with the statement. When parameters are present they will be " + "be included in CSV format after the statement text.", + + NULL, + &auditLogParameter, + false, + PGC_SUSET, + GUC_NOT_IN_SAMPLE, + NULL, NULL, NULL); + + /* Define pg_audit.log_relation */ + DefineCustomBoolVariable( + "pg_audit.log_relation", + + "Specifies whether session audit logging should create a separate log " + "entry for each relation referenced in a SELECT or DML statement. " + "This is a useful shortcut for exhaustive logging without using object " + "audit logging.", + + NULL, + &auditLogRelation, + false, + PGC_SUSET, + GUC_NOT_IN_SAMPLE, + NULL, NULL, NULL); + + /* Define pg_audit.log_statement_once */ + DefineCustomBoolVariable( + "pg_audit.log_statement_once", + + "Specifies whether logging will include the statement text and " + "parameters with the first log entry for a statement/substatement " + "combination or with every entry. Disabling this setting will result " + "in less verbose logging but may make it more difficult to determine " + "the statement that generated a log entry, though the " + "statement/substatement pair along with the process id should suffice " + "to identify the statement text logged with a previous entry.", + + NULL, + &auditLogStatementOnce, + false, + PGC_SUSET, + GUC_NOT_IN_SAMPLE, + NULL, NULL, NULL); + + /* Define pg_audit.role */ + DefineCustomStringVariable( + "pg_audit.role", + + "Specifies the master role to use for object audit logging. Muliple " + "audit roles can be defined by granting them to the master role. This " + "allows multiple groups to be in charge of different aspects of audit " + "logging.", + + NULL, + &auditRole, + "", + PGC_SUSET, + GUC_NOT_IN_SAMPLE, + NULL, NULL, NULL); + + /* + * Install our hook functions after saving the existing pointers to preserve + * the chains. + */ + next_ExecutorStart_hook = ExecutorStart_hook; + ExecutorStart_hook = pg_audit_ExecutorStart_hook; + + next_ExecutorCheckPerms_hook = ExecutorCheckPerms_hook; + ExecutorCheckPerms_hook = pg_audit_ExecutorCheckPerms_hook; + + next_ProcessUtility_hook = ProcessUtility_hook; + ProcessUtility_hook = pg_audit_ProcessUtility_hook; + + next_object_access_hook = object_access_hook; + object_access_hook = pg_audit_object_access_hook; +} diff --git a/contrib/pg_audit/pg_audit.conf b/contrib/pg_audit/pg_audit.conf new file mode 100644 index 0000000000..e9f4a2204f --- /dev/null +++ b/contrib/pg_audit/pg_audit.conf @@ -0,0 +1 @@ +shared_preload_libraries = pg_audit diff --git a/contrib/pg_audit/pg_audit.control b/contrib/pg_audit/pg_audit.control new file mode 100644 index 0000000000..6730c68916 --- /dev/null +++ b/contrib/pg_audit/pg_audit.control @@ -0,0 +1,5 @@ +# pg_audit extension +comment = 'provides auditing functionality' +default_version = '1.0.0' +module_pathname = '$libdir/pg_audit' +relocatable = true diff --git a/contrib/pg_audit/sql/pg_audit.sql b/contrib/pg_audit/sql/pg_audit.sql new file mode 100644 index 0000000000..2a63674c57 --- /dev/null +++ b/contrib/pg_audit/sql/pg_audit.sql @@ -0,0 +1,617 @@ +-- Load pg_audit module +create extension pg_audit; + +-- +-- Audit log fields are: +-- AUDIT_TYPE - SESSION or OBJECT +-- STATEMENT_ID - ID of the statement in the current backend +-- SUBSTATEMENT_ID - ID of the substatement in the current backend +-- CLASS - Class of statement being logged (e.g. ROLE, READ, WRITE) +-- COMMAND - e.g. SELECT, CREATE ROLE, UPDATE +-- OBJECT_TYPE - When available, type of object acted on (e.g. TABLE, VIEW) +-- OBJECT_NAME - When available, fully-qualified table of object +-- STATEMENT - The statement being logged +-- PARAMETER - If parameter logging is requested, they will follow the +-- statement + +-- +-- Create a superuser role that we know the name of for testing +CREATE USER super SUPERUSER; +ALTER ROLE super SET pg_audit.log = 'Role'; +ALTER ROLE super SET pg_audit.log_level = 'notice'; +\connect contrib_regression super; + +-- +-- Create auditor role +CREATE ROLE auditor; + +-- +-- Create first test user +CREATE USER user1; +ALTER ROLE user1 SET pg_audit.log = 'ddl, ROLE'; +ALTER ROLE user1 SET pg_audit.log_level = 'notice'; + +-- +-- Create, select, drop (select will not be audited) +\connect contrib_regression user1 +CREATE TABLE public.test (id INT); +SELECT * FROM test; +DROP TABLE test; + +-- +-- Create second test user +\connect contrib_regression super + +CREATE USER user2; +ALTER ROLE user2 SET pg_audit.log = 'Read, writE'; +ALTER ROLE user2 SET pg_audit.log_catalog = OFF; +ALTER ROLE user2 SET pg_audit.log_level = 'warning'; +ALTER ROLE user2 SET pg_audit.role = auditor; +ALTER ROLE user2 SET pg_audit.log_statement_once = ON; + +\connect contrib_regression user2 +CREATE TABLE test2 (id INT); +GRANT SELECT ON TABLE public.test2 TO auditor; + +-- +-- Role-based tests +CREATE TABLE test3 +( + id INT +); + +SELECT count(*) + FROM +( + SELECT relname + FROM pg_class + LIMIT 1 +) SUBQUERY; + +SELECT * + FROM test3, test2; + +GRANT INSERT + ON TABLE public.test3 + TO auditor; + +-- +-- Create a view to test logging +CREATE VIEW vw_test3 AS +SELECT * + FROM test3; + +GRANT SELECT + ON vw_test3 + TO auditor; + +-- +-- Object logged because of: +-- select on vw_test3 +-- select on test2 +SELECT * + FROM vw_test3, test2; + +-- +-- Object logged because of: +-- insert on test3 +-- select on test2 +WITH CTE AS +( + SELECT id + FROM test2 +) +INSERT INTO test3 +SELECT id + FROM cte; + +-- +-- Object logged because of: +-- insert on test3 +WITH CTE AS +( + INSERT INTO test3 VALUES (1) + RETURNING id +) +INSERT INTO test2 +SELECT id + FROM cte; + +GRANT UPDATE ON TABLE public.test2 TO auditor; + +-- +-- Object logged because of: +-- insert on test3 +-- update on test2 +WITH CTE AS +( + UPDATE test2 + SET id = 1 + RETURNING id +) +INSERT INTO test3 +SELECT id + FROM cte; + +-- +-- Object logged because of: +-- insert on test2 +WITH CTE AS +( + INSERT INTO test2 VALUES (1) + RETURNING id +) +UPDATE test3 + SET id = cte.id + FROM cte + WHERE test3.id <> cte.id; + +-- +-- Change permissions of user 2 so that only object logging will be done +\connect contrib_regression super +alter role user2 set pg_audit.log = 'NONE'; + +\connect contrib_regression user2 + +-- +-- Create test4 and add permissions +CREATE TABLE test4 +( + id int, + name text +); + +GRANT SELECT (name) + ON TABLE public.test4 + TO auditor; + +GRANT UPDATE (id) + ON TABLE public.test4 + TO auditor; + +GRANT insert (name) + ON TABLE public.test4 + TO auditor; + +-- +-- Not object logged +SELECT id + FROM public.test4; + +-- +-- Object logged because of: +-- select (name) on test4 +SELECT name + FROM public.test4; + +-- +-- Not object logged +INSERT INTO public.test4 (id) + VALUES (1); + +-- +-- Object logged because of: +-- insert (name) on test4 +INSERT INTO public.test4 (name) + VALUES ('test'); + +-- +-- Not object logged +UPDATE public.test4 + SET name = 'foo'; + +-- +-- Object logged because of: +-- update (id) on test4 +UPDATE public.test4 + SET id = 1; + +-- +-- Object logged because of: +-- update (name) on test4 +-- update (name) takes precedence over select (name) due to ordering +update public.test4 set name = 'foo' where name = 'bar'; + +-- +-- Drop test tables +DROP TABLE test2; +DROP VIEW vw_test3; +DROP TABLE test3; +DROP TABLE test4; + +-- +-- Change permissions of user 1 so that session logging will be done +\connect contrib_regression super +alter role user1 set pg_audit.log = 'DDL, READ'; +\connect contrib_regression user1 + +-- +-- Create table is session logged +CREATE TABLE public.account +( + id INT, + name TEXT, + password TEXT, + description TEXT +); + +-- +-- Select is session logged +SELECT * + FROM account; + +-- +-- Insert is not logged +INSERT INTO account (id, name, password, description) + VALUES (1, 'user1', 'HASH1', 'blah, blah'); + +-- +-- Change permissions of user 1 so that only object logging will be done +\connect contrib_regression super +alter role user1 set pg_audit.log = 'none'; +alter role user1 set pg_audit.role = 'auditor'; +\connect contrib_regression user1 + +-- +-- ROLE class not set, so auditor grants not logged +GRANT SELECT (password), + UPDATE (name, password) + ON TABLE public.account + TO auditor; + +-- +-- Not object logged +SELECT id, + name + FROM account; + +-- +-- Object logged because of: +-- select (password) on account +SELECT password + FROM account; + +-- +-- Not object logged +UPDATE account + SET description = 'yada, yada'; + +-- +-- Object logged because of: +-- update (password) on account +UPDATE account + SET password = 'HASH2'; + +-- +-- Change permissions of user 1 so that session relation logging will be done +\connect contrib_regression super +alter role user1 set pg_audit.log_relation = on; +alter role user1 set pg_audit.log = 'read, WRITE'; +\connect contrib_regression user1 + +-- +-- Not logged +create table ACCOUNT_ROLE_MAP +( + account_id INT, + role_id INT +); + +-- +-- ROLE class not set, so auditor grants not logged +GRANT SELECT + ON TABLE public.account_role_map + TO auditor; + +-- +-- Object logged because of: +-- select (password) on account +-- select on account_role_map +-- Session logged on all tables because log = read and log_relation = on +SELECT account.password, + account_role_map.role_id + FROM account + INNER JOIN account_role_map + on account.id = account_role_map.account_id; + +-- +-- Object logged because of: +-- select (password) on account +-- Session logged on all tables because log = read and log_relation = on +SELECT password + FROM account; + +-- +-- Not object logged +-- Session logged on all tables because log = read and log_relation = on +UPDATE account + SET description = 'yada, yada'; + +-- +-- Object logged because of: +-- select (password) on account (in the where clause) +-- Session logged on all tables because log = read and log_relation = on +UPDATE account + SET description = 'yada, yada' + where password = 'HASH2'; + +-- +-- Object logged because of: +-- update (password) on account +-- Session logged on all tables because log = read and log_relation = on +UPDATE account + SET password = 'HASH2'; + +-- +-- Change back to superuser to do exhaustive tests +\connect contrib_regression super +SET pg_audit.log = 'ALL'; +SET pg_audit.log_level = 'notice'; +SET pg_audit.log_relation = ON; +SET pg_audit.log_parameter = ON; + +-- +-- Simple DO block +DO $$ +BEGIN + raise notice 'test'; +END $$; + +-- +-- Create test schema +CREATE SCHEMA test; + +-- +-- Copy account to stdout +COPY account TO stdout; + +-- +-- Create a table from a query +CREATE TABLE test.account_copy AS +SELECT * + FROM account; + +-- +-- Copy from stdin to account copy +COPY test.account_copy from stdin; +1 user1 HASH2 yada, yada +\. + +-- +-- Test prepared statement +PREPARE pgclassstmt (oid) AS +SELECT * + FROM account + WHERE id = $1; + +EXECUTE pgclassstmt (1); +DEALLOCATE pgclassstmt; + +-- +-- Test cursor +BEGIN; + +DECLARE ctest SCROLL CURSOR FOR +SELECT count(*) + FROM +( + SELECT relname + FROM pg_class + LIMIT 1 + ) subquery; + +FETCH NEXT FROM ctest; +CLOSE ctest; +COMMIT; + +-- +-- Turn off log_catalog and pg_class will not be logged +SET pg_audit.log_catalog = OFF; + +SELECT count(*) + FROM +( + SELECT relname + FROM pg_class + LIMIT 1 + ) subquery; + +-- +-- Test prepared insert +CREATE TABLE test.test_insert +( + id INT +); + +PREPARE pgclassstmt (oid) AS +INSERT INTO test.test_insert (id) + VALUES ($1); +EXECUTE pgclassstmt (1); + +-- +-- Check that primary key creation is logged +CREATE TABLE public.test +( + id INT, + name TEXT, + description TEXT, + CONSTRAINT test_pkey PRIMARY KEY (id) +); + +-- +-- Check that analyze is logged +ANALYZE test; + +-- +-- Grants to public should not cause object logging (session logging will +-- still happen) +GRANT SELECT + ON TABLE public.test + TO PUBLIC; + +SELECT * + FROM test; + +-- Check that statements without columns log +SELECT + FROM test; + +SELECT 1, + current_user; + +DO $$ +DECLARE + test INT; +BEGIN + SELECT 1 + INTO test; +END $$; + +explain select 1; + +-- +-- Test that looks inside of do blocks log +INSERT INTO TEST (id) + VALUES (1); +INSERT INTO TEST (id) + VALUES (2); +INSERT INTO TEST (id) + VALUES (3); + +DO $$ +DECLARE + result RECORD; +BEGIN + FOR result IN + SELECT id + FROM test + LOOP + INSERT INTO test (id) + VALUES (result.id + 100); + END LOOP; +END $$; + +-- +-- Test obfuscated dynamic sql for clean logging +DO $$ +DECLARE + table_name TEXT = 'do_table'; +BEGIN + EXECUTE 'CREATE TABLE ' || table_name || ' ("weird name" INT)'; + EXECUTE 'DROP table ' || table_name; +END $$; + +-- +-- Generate an error and make sure the stack gets cleared +DO $$ +BEGIN + CREATE TABLE bogus.test_block + ( + id INT + ); +END $$; + +-- +-- Test alter table statements +ALTER TABLE public.test + DROP COLUMN description ; + +ALTER TABLE public.test + RENAME TO test2; + +ALTER TABLE public.test2 + SET SCHEMA test; + +ALTER TABLE test.test2 + ADD COLUMN description TEXT; + +ALTER TABLE test.test2 + DROP COLUMN description; + +DROP TABLE test.test2; + +-- +-- Test multiple statements with one semi-colon +CREATE SCHEMA foo + CREATE TABLE foo.bar (id int) + CREATE TABLE foo.baz (id int); + +-- +-- Test aggregate +CREATE FUNCTION public.int_add +( + a INT, + b INT +) + RETURNS INT LANGUAGE plpgsql AS $$ +BEGIN + return a + b; +END $$; + +SELECT int_add(1, 1); + +CREATE AGGREGATE public.sum_test(INT) (SFUNC=public.int_add, STYPE=INT, INITCOND='0'); +ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2; + +-- +-- Test conversion +CREATE CONVERSION public.conversion_test FOR 'SQL_ASCII' TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic; +ALTER CONVERSION public.conversion_test RENAME TO conversion_test2; + +-- +-- Test create/alter/drop database +CREATE DATABASE contrib_regression_pgaudit; +ALTER DATABASE contrib_regression_pgaudit RENAME TO contrib_regression_pgaudit2; +DROP DATABASE contrib_regression_pgaudit2; + +-- +-- Test that frees a memory context earlier than expected +CREATE TABLE hoge +( + id int +); + +CREATE FUNCTION test() + RETURNS INT AS $$ +DECLARE + cur1 cursor for select * from hoge; + tmp int; +BEGIN + OPEN cur1; + FETCH cur1 into tmp; + RETURN tmp; +END $$ +LANGUAGE plpgsql ; + +SELECT test(); + +-- +-- Delete all rows then delete 1 row +SET pg_audit.log = 'write'; +SET pg_audit.role = 'auditor'; + +create table bar +( + col int +); + +grant delete + on bar + to auditor; + +insert into bar (col) + values (1); +delete from bar; + +insert into bar (col) + values (1); +delete from bar + where col = 1; + +drop table bar; + +-- +-- Grant roles to each other +SET pg_audit.log = 'role'; +GRANT user1 TO user2; +REVOKE user1 FROM user2; diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml index 49a6ce8b0b..0a2bae8dad 100644 --- a/doc/src/sgml/contrib.sgml +++ b/doc/src/sgml/contrib.sgml @@ -124,6 +124,7 @@ CREATE EXTENSION module_name FROM unpackaged; <ree; &pageinspect; &passwordcheck; + &pgaudit; &pgbuffercache; &pgcrypto; &pgfreespacemap; diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml index 6268d5496b..03fea3217e 100644 --- a/doc/src/sgml/filelist.sgml +++ b/doc/src/sgml/filelist.sgml @@ -126,6 +126,7 @@ + diff --git a/doc/src/sgml/pgaudit.sgml b/doc/src/sgml/pgaudit.sgml new file mode 100644 index 0000000000..b8df0d50b8 --- /dev/null +++ b/doc/src/sgml/pgaudit.sgml @@ -0,0 +1,678 @@ + + + + pg_audit + + + pg_audit + + + + The pg_audit extension provides detailed session + and/or object audit logging via the standard logging facility. The goal + is to provide the tools needed to produce audit logs required to pass any + government, financial, or ISO certification audit. + + + + An audit is an official inspection of an individual's or organization's + accounts, typically by an independent body. The information gathered by + pg_audit is properly called an audit trail or audit + log. The term audit log is used in this documentation. + + + + Why <literal>pg_audit</>? + + + Basic statement logging can be provided by the standard logging facility + using log_statement = all. This is acceptable for monitoring + and other usages but does not provide the level of detail generally + required for an audit. It is not enough to have a list of all the + operations performed against the database. It must also be possible to + find particular statements that are of interest to an auditor. The + standard logging facility shows what the user requested, while + pg_audit focuses on the details of what happened while + the database was satisfying the request. + + + + For example, an auditor may want to verify that a particular table was + created inside a documented maintenance window. This might seem like a + simple job for grep, but what if you are presented with something like + this (intentionally obfuscated) example: + + + +DO $$ +BEGIN + EXECUTE 'CREATE TABLE import' || 'ant_table (id INT)'; +END $$; + + + + Standard logging will give you this: + + + +LOG: statement: DO $$ +BEGIN + EXECUTE 'CREATE TABLE import' || 'ant_table (id INT)'; +END $$; + + + + It appears that finding the table of interest may require some knowledge + of the code in cases where tables are created dynamically. This is not + ideal since it would be preferrable to just search on the table name. + This is where pg_audit comes in. For the same input, + it will produce this output in the log: + + + +AUDIT: SESSION,33,1,FUNCTION,DO,,,"DO $$ +BEGIN + EXECUTE 'CREATE TABLE import' || 'ant_table (id INT)'; +END $$;" +AUDIT: SESSION,33,2,DDL,CREATE TABLE,TABLE,public.important_table,CREATE TABLE important_table (id INT) + + + + Not only is the DO block logged, but substatement 2 contains + the full text of the CREATE TABLE with the statement type, + object type, and full-qualified name to make searches easy. + + + + When logging SELECT and DML statements, + pg_audit can be configured to log a separate entry for each + relation referenced in a statement. No parsing is required to find all + statements that touch a particular table. In fact, the goal is that the + statement text is provided primarily for deep forensics and should not be + the required for an audit. + + + + + Usage Considerations + + + Depending on settings, it is possible for pg_audit to + generate an enormous volume of logging. Be careful to determine + exactly what needs to be audit logged in your environment to avoid + logging too much. + + + + For example, when working in an OLAP environment it would probably not be + wise to audit log inserts into a large fact table. The size of the log + file will likely be many times the actual data size of the inserts because + the log file is expressed as text. Since logs are generally stored with + the OS this may lead to disk space being exhausted very + quickly. In cases where it is not possible to limit audit logging to + certain tables, be sure to assess the performance impact while testing + and allocate plenty of space on the log volume. This may also be true for + OLTP environments. Even if the insert volume is not as high, the + performance impact of audit logging may still noticeably affect latency. + + + + To limit the number of relations audit logged for SELECT + and DML statments, consider using object audit logging + (see ). Object audit logging + allows selection of the relations to be logged allowing for reduction + of the overall log volume. However, when new relations are added they + must be explicitly added to object audit logging. A programmatic + solution where specified tables are excluded from logging and all others + are included may be a good option in this case. + + + + + Settings + + + Settings may be modified only by a superuser. Allowing normal users to + change their settings would defeat the point of an audit log. + + + + Settings can be specified globally (in + postgresql.conf or using + ALTER SYSTEM ... SET), at the database level (using + ALTER DATABASE ... SET), or at the role level (using + ALTER ROLE ... SET). Note that settings are not + inherited through normal role inheritance and SET ROLE will + not alter a user's pg_audit settings. This is a limitation + of the roles system and not inherent to pg_audit. + + + + The pg_audit extension must be loaded in + . Otherwise, an error + will be raised at load time and no audit logging will occur. + + + + + pg_audit.log (string) + + pg_audit.log configuration parameter + + + + + Specifies which classes of statements will be logged by session + audit logging. Possible values are: + + + + + + READ - SELECT and + COPY when the source is a relation or a + query. + + + + + WRITE - INSERT, + UPDATE, DELETE, + TRUNCATE, and COPY when the + destination is a relation. + + + + + FUNCTION - Function calls and + DO blocks. + + + + + ROLE - Statements related to roles and + privileges: GRANT, + REVOKE, + CREATE/ALTER/DROP ROLE. + + + + + DDL - All DDL that is not included + in the ROLE class plus REINDEX. + + + + + MISC - Miscellaneous commands, e.g. + DISCARD, FETCH, + CHECKPOINT, VACUUM. + + + + + + Multiple classes can be provided using a comma-separated list and + classes can be subtracted by prefacing the class with a + - sign (see ). + The default is none. + + + + + + pg_audit.log_catalog (boolean) + + pg_audit.log_catalog configuration parameter + + + + + Specifies that session logging should be enabled in the case where all + relations in a statement are in pg_catalog. Disabling this setting + will reduce noise in the log from tools like psql and PgAdmin that query + the catalog heavily. The default is on. + + + + + + pg_audit.log_level (boolean) + + pg_audit.log_level configuration parameter + + + + + Specifies the log level that will be used for log entries (see + for valid levels). + This setting is used for regression testing and may also be useful + to end users for testing or other purposes. It is not intended to + be used in a production environment as it may leak which statements + are being logged to the user. The default is log. + + + + + + pg_audit.log_parameter (boolean) + + pg_audit.log_parameter configuration parameter + + + + + Specifies that audit logging should include the parameters that + were passed with the statement. When parameters are present they will + be included in CSV format after the statement text. The default is + off. + + + + + + pg_audit.log_relation (boolean) + + pg_audit.log_relation configuration parameter + + + + + Specifies whether session audit logging should create a separate + log entry for each relation (TABLE, VIEW, + etc.) referenced in a SELECT or DML + statement. This is a useful shortcut for exhaustive logging + without using object audit logging. The default is + off. + + + + + + pg_audit.log_statement_once (boolean) + + pg_audit.log_statement_once configuration parameter + + + + + Specifies whether logging will include the statement text and + parameters with the first log entry for a statement/substatement + combination or with every entry. Disabling this setting will + result in less verbose logging but may make it more difficult to + determine the statement that generated a log entry, though the + statement/substatement pair along with the process id should suffice + to identify the statement text logged with a previous entry. The + default is off. + + + + + + pg_audit.role (string) + + pg_audit.role configuration parameter + + + + + Specifies the master role to use for object audit logging. Muliple + audit roles can be defined by granting them to the master role. + This allows multiple groups to be in charge of different aspects + of audit logging. There is no default. + + + + + + + + Session Audit Logging + + + Session audit logging provides detailed logs of all statements executed + by a user in the backend. + + + + Configuration + + + Session logging is enabled with the + setting. + + Enable session logging for all DML and DDL and + log all relations in DML statements: + +set pg_audit.log = 'write, ddl'; +set pg_audit.log_relation = on; + + + + + Enable session logging for all commands except MISC and + raise audit log messages as NOTICE: + +set pg_audit.log = 'all, -misc'; +set pg_audit.log_notice = on; + + + + + + Example + + + In this example session audit logging is used for logging + DDL and SELECT statements. Note that the + insert statement is not logged since the WRITE class + is not enabled + + + + SQL: + + +set pg_audit.log = 'read, ddl'; + +create table account +( + id int, + name text, + password text, + description text +); + +insert into account (id, name, password, description) + values (1, 'user1', 'HASH1', 'blah, blah'); + +select * + from account; + + + + Log Output: + + + +AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.account,create table account +( + id int, + name text, + password text, + description text +); +AUDIT: SESSION,2,1,READ,SELECT,,,select * + from account + + + + + + Object Auditing + + + Object audit logging logs statements that affect a particular relation. + Only SELECT, INSERT, UPDATE and + DELETE commands are supported. TRUNCATE is not + included because there is no specific privilege for it. + + + + Object audit logging is intended to be a finer-grained replacement for + pg_audit.log = 'read, write'. As such, it may not + make sense to use them in conjunction but one possible scenario would + be to use session logging to capture each statement and then supplement + that with object logging to get more detail about specific relations. + + + + Configuration + + + Object-level audit logging is implemented via the roles system. The + setting defines the role that + will be used for audit logging. A relation (TABLE, + VIEW, etc.) will be audit logged when the audit role has + permissions for the command executed or inherits the permissions from + another role. This allows you to effectively have multiple audit roles + even though there is a single master role in any context. + + + + Set to auditor and + grant SELECT and DELETE privileges on the + account table. Any SELECT or + DELETE statements on account will now be + logged: + + + +set pg_audit.role = 'auditor'; + +grant select, delete + on public.account + to auditor; + + + + + Example + + + In this example object audit logging is used to illustrate how a + granular approach may be taken towards logging of SELECT + and DML statements. Note that logging on the + account table is controlled by column-level permissions, + while logging on account_role_map is table-level. + + + + SQL: + + + +set pg_audit.role = 'auditor'; + +create table account +( + id int, + name text, + password text, + description text +); + +grant select (password) + on public.account + to auditor; + +select id, name + from account; + +select password + from account; + +grant update (name, password) + on public.account + to auditor; + +update account + set description = 'yada, yada'; + +update account + set password = 'HASH2'; + +create table account_role_map +( + account_id int, + role_id int +); + +grant select + on public.account_role_map + to auditor; + +select account.password, + account_role_map.role_id + from account + inner join account_role_map + on account.id = account_role_map.account_id + + + + Log Output: + + + +AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account,select password + from account +AUDIT: OBJECT,2,1,WRITE,UPDATE,TABLE,public.account,update account + set password = 'HASH2' +AUDIT: OBJECT,3,1,READ,SELECT,TABLE,public.account,select account.password, + account_role_map.role_id + from account + inner join account_role_map + on account.id = account_role_map.account_id +AUDIT: OBJECT,3,1,READ,SELECT,TABLE,public.account_role_map,select account.password, + account_role_map.role_id + from account + inner join account_role_map + on account.id = account_role_map.account_id + + + + + + Format + + + Audit entries are written to the standard logging facility and contain + the following columns in comma-separated format: + + + + Output is compliant CSV format only if the log line prefix portion + of each log entry is removed. + + + + + + + AUDIT_TYPE - SESSION or + OBJECT. + + + + + STATEMENT_ID - Unique statement ID for this session. + Each statement ID represents a backend call. Statement IDs are + sequential even if some statements are not logged. There may be + multiple entries for a statement ID when more than one relation + is logged. + + + + + SUBSTATEMENT_ID - Sequential ID for each + substatement within the main statement. For example, calling + a function from a query. Substatement IDs are continuous + even if some substatements are not logged. There may be multiple + entries for a substatement ID when more than one relation is logged. + + + + + CLASS - e.g. (READ, + ROLE) (see ). + + + + + COMMAND - e.g. ALTER TABLE, + SELECT. + + + + + OBJECT_TYPE - TABLE, + INDEX, VIEW, etc. + Available for SELECT, DML and most + DDL statements. + + + + + OBJECT_NAME - The fully-qualified object name + (e.g. public.account). Available for SELECT, + DML and most DDL statements. + + + + + STATEMENT - Statement executed on the backend. + + + + + + + Use to add any other fields that + are needed to satisfy your audit log requirements. A typical log line + prefix might be '%m %u %d: ' which would provide the date/time, + user name, and database name for each audit log. + + + + + Caveats + + + + + Object renames are logged under the name they were renamed to. + For example, renaming a table will produce the following result: + + + +ALTER TABLE test RENAME TO test2; + +AUDIT: SESSION,36,1,DDL,ALTER TABLE,TABLE,public.test2,ALTER TABLE test RENAME TO test2 + + + + + + It is possible to have a command logged more than once. For example, + when a table is created with a primary key specified at creation time + the index for the primary key will be logged independently and another + audit log will be made for the index under the create entry. The + multiple entries will however be contained within one statement ID. + + + + + + Autovacuum and Autoanalyze are not logged, nor are they intended to be. + + + + + + Statements that are executed after a transaction enters an aborted state + will not be audit logged. However, the statement that caused the error + and any subsequent statements executed in the aborted transaction will + be logged as ERRORs by the standard logging facility. + + + + + + + Authors + + + Abhijit Menon-Sen ams@2ndQuadrant.com, Ian Barwick ian@2ndQuadrant.com, and David Steele david@pgmasters.net. + + +