From: David Steele <david@pgmasters.net>
To: Stephen Frost <sfrost@snowman.net>
Cc: pgsql-hackers@postgresql.org, Abhijit Menon-Sen <ams@2ndquadrant.com>
Subject: Re: Auditing extension for PostgreSQL (Take 2)
Date: Tue, 24 Feb 2015 11:22:41 -0500
Message-ID: <54ECA551.8060306@pgmasters.net> (raw)
In-Reply-To: <54EB4E56.7050102@pgmasters.net>
References: <54E005CC.1060605@pgmasters.net>
<20150217153402.GQ6717@tamriel.snowman.net>
<54EB4E56.7050102@pgmasters.net>
List-Unsubscribe: <mailto:majordomo@postgresql.org?body=unsub%20pgsql-hackers>
On 2/23/15 10:59 AM, David Steele wrote:
> On 2/17/15 10:34 AM, Stephen Frost wrote:
>> There seems to be a number of places which are 'pgaudit' and a bunch
>> that are 'pg_audit'. I'm guessing you were thinking 'pg_audit', but
>> it'd be good to clean up and make them all consistent.
>
> Fixed, though I still left the file name as pgaudit.sgml since all but
> one module follow that convention.
It turns out there are a few places where _ is not allowed. I've
reverted a few places to fix the doc build while maintaining the name as
pg_audit in the visible docs.
>> Perhaps I missed it, but it'd be good to point out that GUCs can be set
>> at various levels. I know we probably say that somewhere else, but it's
>> particularly relevant for this.
I added notes as suggested.
Patch v3 is attached.
--
- David Steele
david@pgmasters.net
Attachments:pg_audit-v3.patch (text/plain, 78.2K) downloaddiff --git a/contrib/Makefile b/contrib/Makefileindex 195d447..d8e75f4 100644--- a/contrib/Makefile+++ b/contrib/Makefile@@ -29,6 +29,7 @@ SUBDIRS = \
pageinspect \
passwordcheck \
pg_archivecleanup \
+ pg_audit \
pg_buffercache \
pg_freespacemap \
pg_prewarm \
diff --git a/contrib/pg_audit/Makefile b/contrib/pg_audit/Makefilenew file mode 100644index 0000000..32bc6d9--- /dev/null+++ b/contrib/pg_audit/Makefile@@ -0,0 +1,20 @@+# pg_audit/Makefile++MODULE = pg_audit+MODULE_big = pg_audit+OBJS = pg_audit.o++EXTENSION = pg_audit++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+endifdiff --git a/contrib/pg_audit/pg_audit--1.0.0.sql b/contrib/pg_audit/pg_audit--1.0.0.sqlnew file mode 100644index 0000000..2eee3b9--- /dev/null+++ b/contrib/pg_audit/pg_audit--1.0.0.sql@@ -0,0 +1,4 @@+/* 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.\quitdiff --git a/contrib/pg_audit/pg_audit.c b/contrib/pg_audit/pg_audit.cnew file mode 100644index 0000000..ead65a8--- /dev/null+++ b/contrib/pg_audit/pg_audit.c@@ -0,0 +1,1102 @@+/*------------------------------------------------------------------------------+ * pg_audit.c+ *+ * An auditing extension for PostgreSQL. Improves on standard statement logging+ * by adding more logging classes, object level logging, and providing+ * fully-qualified object names for all DML and many DDL statements (See+ * pg_audit.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);++/*+ * auditRole is the string value of the pgaudit.role GUC, which contains the+ * role for grant-based auditing.+ */+char *auditRole = NULL;++/*+ * auditLog is the string value of the pgaudit.log GUC, e.g. "read, write, ddl"+ * (it's not used by the module but is required by DefineCustomStringVariable).+ * Each token corresponds to a flag in enum LogClass below. We convert the list+ * of tokens into a bitmap in auditLogBitmap for internal use.+ */+char *auditLog = NULL;+static uint64 auditLogBitmap = 0;++/*+ * String constants for audit types - used when logging to distinguish session+ * vs. object auditing.+ */+#define AUDIT_TYPE_OBJECT "OBJECT"+#define AUDIT_TYPE_SESSION "SESSION"++/*+ * String constants for log classes - used when processing tokens in the+ * pgaudit.log GUC.+ */+#define CLASS_DDL "DDL"+#define CLASS_FUNCTION "FUNCTION"+#define CLASS_MISC "MISC"+#define CLASS_READ "READ"+#define CLASS_WRITE "WRITE"++#define CLASS_ALL "ALL"+#define CLASS_NONE "NONE"++/* Log class enum used to represent bits in auditLogBitmap */+enum LogClass+{+ LOG_NONE = 0,++ /* SELECT */+ LOG_READ = (1 << 0),++ /* INSERT, UPDATE, DELETE, TRUNCATE */+ LOG_WRITE = (1 << 1),++ /* DDL: CREATE/DROP/ALTER */+ LOG_DDL = (1 << 2),++ /* Function execution */+ LOG_FUNCTION = (1 << 4),++ /* Function execution */+ LOG_MISC = (1 << 5),++ /* Absolutely everything */+ LOG_ALL = ~(uint64)0+};++/* String constants for logging commands */+#define COMMAND_DELETE "DELETE"+#define COMMAND_EXECUTE "EXECUTE"+#define COMMAND_INSERT "INSERT"+#define COMMAND_UPDATE "UPDATE"+#define COMMAND_SELECT "SELECT"++#define COMMAND_UNKNOWN "UNKNOWN"++/* String constants for logging object types */+#define OBJECT_TYPE_COMPOSITE_TYPE "COMPOSITE TYPE"+#define OBJECT_TYPE_FOREIGN_TABLE "FOREIGN TABLE"+#define OBJECT_TYPE_FUNCTION "FUNCTION"+#define OBJECT_TYPE_INDEX "INDEX"+#define OBJECT_TYPE_TABLE "TABLE"+#define OBJECT_TYPE_TOASTVALUE "TOASTVALUE"+#define OBJECT_TYPE_MATVIEW "MATERIALIZED VIEW"+#define OBJECT_TYPE_SEQUENCE "SEQUENCE"+#define OBJECT_TYPE_VIEW "VIEW"++#define OBJECT_TYPE_UNKNOWN "UNKNOWN"++/*+ * An AuditEvent represents an operation that potentially affects a single+ * object. If an underlying command affects multiple objects multiple+ * AuditEvents must be created to represent it.+ */+typedef struct+{+ LogStmtLevel logStmtLevel;+ NodeTag commandTag;+ const char *command;+ const char *objectType;+ char *objectName;+ const char *commandText;+ bool granted;+} AuditEvent;++/*+ * Set if a function below log_utility_command() has logged the event - prevents+ * more than one function from logging when the event could be logged in+ * multiple places.+ */+bool utilityCommandLogged = false;+bool utilityCommandInProgress = false;+AuditEvent utilityAuditEvent;++/*+ * Takes an AuditEvent and returns true or false depending on whether the event+ * should be logged according to the pgaudit.roles/log settings. If it returns+ * true, also fills in the name of the LogClass which it is logged under.+ */+static bool+log_check(AuditEvent *e, const char **classname)+{+ enum LogClass class = LOG_NONE;++ /* By default put everything in the MISC class. */+ *classname = CLASS_MISC;+ class = LOG_MISC;++ /*+ * Look at the type of the command and decide what LogClass needs to be+ * enabled for the command to be logged.+ */+ switch (e->logStmtLevel)+ {+ case LOGSTMT_MOD:+ *classname = CLASS_WRITE;+ class = LOG_WRITE;+ break;++ case LOGSTMT_DDL:+ *classname = CLASS_DDL;+ class = LOG_DDL;++ case LOGSTMT_ALL:+ switch (e->commandTag)+ {+ case T_CopyStmt:+ case T_SelectStmt:+ case T_PrepareStmt:+ case T_PlannedStmt:+ case T_ExecuteStmt:+ *classname = CLASS_READ;+ class = LOG_READ;+ break;++ case T_VacuumStmt:+ case T_ReindexStmt:+ *classname = CLASS_DDL;+ class = LOG_DDL;+ break;++ case T_DoStmt:+ *classname = CLASS_FUNCTION;+ class = LOG_FUNCTION;+ break;++ default:+ break;+ }+ break;++ case LOGSTMT_NONE:+ break;+ }++ /*+ * We log audit events under the following conditions:+ *+ * 1. If the audit role has been explicitly granted permission for+ * an operation.+ */+ if (e->granted)+ {+ return true;+ }++ /* 2. If the event belongs to a class covered by pgaudit.log. */+ if ((auditLogBitmap & class) == class)+ {+ return true;+ }++ return false;+}++/*+ * Takes an AuditEvent and, if it log_check(), writes it to the audit log. The+ * AuditEvent is assumed to be completely filled in by the caller (unknown+ * values must be set to "" so that they can be logged without error checking).+ */+static void+log_audit_event(AuditEvent *e)+{+ const char *classname;++ /* Check that this event should be logged. */+ if (!log_check(e, &classname))+ return;++ /* Log via ereport(). */+ ereport(LOG,+ (errmsg("AUDIT: %s,%s,%s,%s,%s,%s",+ e->granted ? AUDIT_TYPE_OBJECT : AUDIT_TYPE_SESSION,+ classname, e->command, e->objectType, e->objectName,+ e->commandText),+ errhidestmt(true)));+}++/*+ * 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+log_acl_check(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 */+ 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+log_relation_check(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));++ /* Return false if tuple is not valid */+ if (!HeapTupleIsValid(tuple))+ return false;++ /* Get the relation's ACL */+ aclDatum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_relacl,+ &isNull);++ /* If not null then test */+ if (!isNull)+ result = log_acl_check(aclDatum, auditOid, mask);++ /* Free the relation tuple */+ ReleaseSysCache(tuple);++ return result;+}++/*+ * Check if a role has any of the permissions in the mask on an attribute.+ */+static bool+log_attribute_check(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));++ /* Return false if attribute is invalid */+ if (!HeapTupleIsValid(attTuple))+ return false;++ /* Only process attribute that have not been dropped */+ if (!((Form_pg_attribute) GETSTRUCT(attTuple))->attisdropped)+ {+ aclDatum = SysCacheGetAttr(ATTNUM, attTuple, Anum_pg_attribute_attacl,+ &isNull);++ if (!isNull)+ result = log_acl_check(aclDatum, auditOid, mask);+ }++ /* Free attribute */+ ReleaseSysCache(attTuple);++ return result;+}++/*+ * Check if a role has any of the permissions in the mask on an attribute in+ * the provided set. If the set is empty, then all valid attributes in the+ * relation will be tested.+ */+static bool+log_attribute_check_any(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 attribute */+ 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 (log_attribute_check(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 &&+ log_attribute_check(relOid, col, auditOid, mode))+ {+ result = true;+ break;+ }+ }++ /* Free the column set */+ bms_free(tmpSet);++ return result;+}++/*+ * Create AuditEvents for DML operations via executor permissions checks.+ */+static void+log_dml(Oid auditOid, List *rangeTabls)+{+ ListCell *lr;+ bool first = true;+ AuditEvent auditEvent;++ 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;++ /*+ * Filter out any system relations+ */+ relOid = rte->relid;+ rel = relation_open(relOid, NoLock);++ if (IsSystemNamespace(RelationGetNamespace(rel)))+ {+ relation_close(rel, NoLock);+ return;+ }++ /*+ * 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.+ */+ auditEvent.logStmtLevel = LOGSTMT_MOD;++ if (rte->requiredPerms & ACL_INSERT)+ {+ auditEvent.commandTag = T_InsertStmt;+ auditEvent.command = COMMAND_INSERT;+ }+ else if (rte->requiredPerms & ACL_UPDATE)+ {+ auditEvent.commandTag = T_UpdateStmt;+ auditEvent.command = COMMAND_UPDATE;+ }+ else if (rte->requiredPerms & ACL_DELETE)+ {+ auditEvent.commandTag = T_DeleteStmt;+ auditEvent.command = COMMAND_DELETE;+ }+ else if (rte->requiredPerms & ACL_SELECT)+ {+ auditEvent.logStmtLevel = LOGSTMT_ALL;+ auditEvent.commandTag = T_SelectStmt;+ auditEvent.command = COMMAND_SELECT;+ }+ else+ {+ auditEvent.commandTag = T_Invalid;+ auditEvent.command = COMMAND_UNKNOWN;+ }++ /*+ * Fill values in the event struct that are required for session+ * logging.+ */+ auditEvent.granted = false;+ auditEvent.commandText = debug_query_string;++ /* If this is the first rte then session log */+ if (first)+ {+ auditEvent.objectName = "";+ auditEvent.objectType = "";++ log_audit_event(&auditEvent);++ first = false;+ }++ /* Get the relation type */+ switch (rte->relkind)+ {+ case RELKIND_RELATION:+ auditEvent.objectType = OBJECT_TYPE_TABLE;+ break;++ case RELKIND_INDEX:+ auditEvent.objectType = OBJECT_TYPE_INDEX;+ break;++ case RELKIND_SEQUENCE:+ auditEvent.objectType = OBJECT_TYPE_SEQUENCE;+ break;++ case RELKIND_TOASTVALUE:+ auditEvent.objectType = OBJECT_TYPE_TOASTVALUE;+ break;++ case RELKIND_VIEW:+ auditEvent.objectType = OBJECT_TYPE_VIEW;+ break;++ case RELKIND_COMPOSITE_TYPE:+ auditEvent.objectType = OBJECT_TYPE_COMPOSITE_TYPE;+ break;++ case RELKIND_FOREIGN_TABLE:+ auditEvent.objectType = OBJECT_TYPE_FOREIGN_TABLE;+ break;++ case RELKIND_MATVIEW:+ auditEvent.objectType = OBJECT_TYPE_MATVIEW;+ break;++ default:+ auditEvent.objectType = OBJECT_TYPE_UNKNOWN;+ break;+ }++ /* Get the relation name */+ 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) &+ rte->requiredPerms;++ /*+ * If any of the required permissions for the relation are granted+ * to the audit role then audit the relation+ */+ if (log_relation_check(relOid, auditOid, auditPerms))+ {+ 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 to see if the audit role has+ * priveleges on any of them.+ */+ if (auditPerms & ACL_SELECT)+ {+ auditEvent.granted =+ log_attribute_check_any(relOid, auditOid,+ rte->selectedCols,+ ACL_SELECT);+ }++ /*+ * Check the modified columns to see if the audit role has+ * privileges on any of them.+ */+ if (!auditEvent.granted)+ {+ auditPerms &= (ACL_INSERT | ACL_UPDATE);++ if (auditPerms)+ {+ auditEvent.granted =+ log_attribute_check_any(relOid, auditOid,+ rte->modifiedCols,+ auditPerms);+ }+ }+ }+ }++ /* Only do relation level logging if a grant was found. */+ if (auditEvent.granted)+ {+ log_audit_event(&auditEvent);+ }++ pfree(auditEvent.objectName);+ }++ /*+ * If the first flag was never set to false, then rangeTabls was empty. In+ * this case log a session select statement.+ */+ if (first && !utilityCommandInProgress)+ {+ auditEvent.logStmtLevel = LOGSTMT_ALL;+ auditEvent.commandTag = T_SelectStmt;+ auditEvent.command = COMMAND_SELECT;+ auditEvent.granted = false;+ auditEvent.commandText = debug_query_string;+ auditEvent.objectName = "";+ auditEvent.objectType = "";++ log_audit_event(&auditEvent);+ }+}++/*+ * Create AuditEvents for certain kinds of CREATE, ALTER, and DELETE statements+ * where the object can be logged.+ */+static void+log_create_alter_drop(Oid classId,+ Oid objectId)+{+ /* Only perform when class is relation */+ if (classId == RelationRelationId)+ {+ Relation rel;+ Form_pg_class class;++ /* Open the relation */+ rel = relation_open(objectId, NoLock);++ /* Filter out any system relations */+ if (IsToastNamespace(RelationGetNamespace(rel)))+ {+ relation_close(rel, NoLock);+ return;+ }++ /* Get rel information and close it */+ class = RelationGetForm(rel);+ utilityAuditEvent.objectName =+ quote_qualified_identifier(get_namespace_name(+ RelationGetNamespace(rel)),+ RelationGetRelationName(rel));+ relation_close(rel, NoLock);++ /* Set object type based on relkind */+ switch (class->relkind)+ {+ case RELKIND_RELATION:+ utilityAuditEvent.objectType = OBJECT_TYPE_TABLE;+ break;++ case RELKIND_INDEX:+ utilityAuditEvent.objectType = OBJECT_TYPE_INDEX;+ break;++ case RELKIND_SEQUENCE:+ utilityAuditEvent.objectType = OBJECT_TYPE_SEQUENCE;+ break;++ case RELKIND_VIEW:+ utilityAuditEvent.objectType = OBJECT_TYPE_VIEW;+ break;++ case RELKIND_COMPOSITE_TYPE:+ utilityAuditEvent.objectType = OBJECT_TYPE_COMPOSITE_TYPE;+ break;++ case RELKIND_FOREIGN_TABLE:+ utilityAuditEvent.objectType = OBJECT_TYPE_FOREIGN_TABLE;+ break;++ case RELKIND_MATVIEW:+ utilityAuditEvent.objectType = OBJECT_TYPE_MATVIEW;+ break;++ /*+ * Any other cases will be handled by log_utility_command().+ */+ default:+ return;+ break;+ }++ /* Log the event */+ log_audit_event(&utilityAuditEvent);+ utilityCommandLogged = true;+ }+}++/*+ * 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;++ /* 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;+ }++ /* Generate the fully-qualified function name. */+ utilityAuditEvent.objectName =+ quote_qualified_identifier(get_namespace_name(proc->pronamespace),+ NameStr(proc->proname));+ ReleaseSysCache(proctup);++ /* Log the event */+ utilityAuditEvent.logStmtLevel = LOGSTMT_ALL;+ utilityAuditEvent.commandTag = T_DoStmt;+ utilityAuditEvent.command = COMMAND_EXECUTE;+ utilityAuditEvent.objectType = OBJECT_TYPE_FUNCTION;+ utilityAuditEvent.commandText = debug_query_string;++ log_audit_event(&utilityAuditEvent);+ utilityCommandLogged = true;+}++/*+ * Log object accesses (which is more about DDL than DML, even though it+ * sounds like the latter).+ */+static void+log_object_access(ObjectAccessType access,+ Oid classId,+ Oid objectId,+ int subId,+ void *arg)+{+ switch (access)+ {+ /* Log execute. */+ case OAT_FUNCTION_EXECUTE:+ log_function_execute(objectId);+ break;++ /* Log create. */+ case OAT_POST_CREATE:+ {+ ObjectAccessPostCreate *pc = arg;++ if (pc->is_internal)+ return;++ log_create_alter_drop(classId, objectId);+ }+ break;++ /* Log alter. */+ case OAT_POST_ALTER:+ {+ ObjectAccessPostAlter *pa = arg;++ if (pa->is_internal)+ return;++ log_create_alter_drop(classId, objectId);+ }+ break;++ /* Log drop. */+ case OAT_DROP:+ {+ ObjectAccessDrop *drop = arg;++ if (drop->dropflags & PERFORM_DELETION_INTERNAL)+ return;++ log_create_alter_drop(classId, objectId);+ }+ break;++ /* All others processed by log_utility_command() */+ default:+ break;+ }+}++/*+ * 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;++/*+ * Hook ExecutorCheckPerms to do session and object auditing for DML.+ */+static bool+pgaudit_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_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+pgaudit_ProcessUtility_hook(Node *parsetree,+ const char *queryString,+ ProcessUtilityContext context,+ ParamListInfo params,+ DestReceiver *dest,+ char *completionTag)+{+ /* Create the utility audit event. */+ utilityCommandLogged = false;+ utilityCommandInProgress = true;++ utilityAuditEvent.logStmtLevel = GetCommandLogLevel(parsetree);+ utilityAuditEvent.commandTag = nodeTag(parsetree);+ utilityAuditEvent.command = CreateCommandTag(parsetree);+ utilityAuditEvent.objectName = "";+ utilityAuditEvent.objectType = "";+ utilityAuditEvent.commandText = debug_query_string;+ utilityAuditEvent.granted = false;++ /* 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);++ /* 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 && !utilityCommandLogged &&+ !IsAbortedTransactionBlockState())+ {+ log_audit_event(&utilityAuditEvent);+ }++ utilityCommandInProgress = false;+}++/*+ * Hook object_access_hook to provide fully-qualified object names for execute,+ * create, drop, and alter commands. Most of the audit information is filled in+ * by log_utility_command().+ */+static void+pgaudit_object_access_hook(ObjectAccessType access,+ Oid classId,+ Oid objectId,+ int subId,+ void *arg)+{+ if (auditLogBitmap != 0 && !IsAbortedTransactionBlockState())+ log_object_access(access, classId, objectId, subId, arg);++ if (next_object_access_hook)+ (*next_object_access_hook) (access, classId, objectId, subId, arg);+}++/*+ * 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_pgaudit_log(char **newval, void **extra, GucSource source)+{+ List *flags;+ char *rawval;+ ListCell *lt;+ uint64 *f;++ /* Make sure newval is a comma-separated list of tokens. */+ rawval = pstrdup(*newval);+ if (!SplitIdentifierString(rawval, ',', &flags))+ {+ GUC_check_errdetail("List syntax is invalid");+ list_free(flags);+ pfree(rawval);+ return false;+ }++ /*+ * Check that we recognise each token, and add it to the bitmap we're+ * building up in a newly-allocated uint64 *f.+ */+ f = (uint64 *) malloc(sizeof(uint64));+ if (!f)+ return false;+ *f = 0;++ foreach(lt, flags)+ {+ bool subtract = false;+ uint64 class;++ /* Retrieve a token */+ char *token = (char *)lfirst(lt);++ /* If token is preceded by -, then then 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_WRITE) == 0)+ class = LOG_WRITE;+ else+ {+ free(f);+ pfree(rawval);+ list_free(flags);+ return false;+ }++ /* Add or subtract class bits from the log bitmap. */+ if (subtract)+ *f &= ~class;+ else+ *f |= class;+ }++ pfree(rawval);+ list_free(flags);++ /*+ * Store the bitmap for assign_pgaudit_log.+ */+ *extra = f;++ return true;+}++/*+ * Set pgaudit_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_pgaudit_log(const char *newval, void *extra)+{+ if (extra)+ auditLogBitmap = *(uint64 *)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")));++ /*+ * pg_audit.role = "audit"+ *+ * This variable defines a role to be used for auditing.+ */+ DefineCustomStringVariable("pg_audit.role",+ "Enable auditing for role",+ NULL,+ &auditRole,+ "",+ PGC_SUSET,+ GUC_LIST_INPUT | GUC_NOT_IN_SAMPLE,+ NULL, NULL, NULL);++ /*+ * pg_audit.log = "read, write, ddl"+ *+ * This variables controls what classes of commands are logged.+ */+ DefineCustomStringVariable("pg_audit.log",+ "Enable auditing for classes of commands",+ NULL,+ &auditLog,+ "none",+ PGC_SUSET,+ GUC_LIST_INPUT | GUC_NOT_IN_SAMPLE,+ check_pgaudit_log,+ assign_pgaudit_log,+ NULL);++ /*+ * Install our hook functions after saving the existing pointers to preserve+ * the chain.+ */+ next_ExecutorCheckPerms_hook = ExecutorCheckPerms_hook;+ ExecutorCheckPerms_hook = pgaudit_ExecutorCheckPerms_hook;++ next_ProcessUtility_hook = ProcessUtility_hook;+ ProcessUtility_hook = pgaudit_ProcessUtility_hook;++ next_object_access_hook = object_access_hook;+ object_access_hook = pgaudit_object_access_hook;+}diff --git a/contrib/pg_audit/pg_audit.control b/contrib/pg_audit/pg_audit.controlnew file mode 100644index 0000000..0b39082--- /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/pgaudit'+relocatable = truediff --git a/contrib/pg_audit/test/test.pl b/contrib/pg_audit/test/test.plnew file mode 100755index 0000000..dcb9fda--- /dev/null+++ b/contrib/pg_audit/test/test.pl@@ -0,0 +1,1243 @@+#!/usr/bin/perl+################################################################################+# test.pl - pg_audit Unit Tests+################################################################################++################################################################################+# Perl includes+################################################################################+use strict;+use warnings;+use Carp;++use Getopt::Long;+use Pod::Usage;+use DBI;+use Cwd qw(abs_path);+use IPC::System::Simple qw(capture);++################################################################################+# Constants+################################################################################+use constant+{+ true => 1,+ false => 0+};++use constant+{+ CONTEXT_GLOBAL => 'GLOBAL',+ CONTEXT_DATABASE => 'DATABASE',+ CONTEXT_ROLE => 'ROLE'+};++use constant+{+ CLASS => 'CLASS',++ CLASS_DDL => 'DDL',+ CLASS_FUNCTION => 'FUNCTION',+ CLASS_MISC => 'MISC',+ CLASS_READ => 'READ',+ CLASS_WRITE => 'WRITE',++ CLASS_ALL => 'ALL',+ CLASS_NONE => 'NONE'+};++use constant+{+ COMMAND => 'COMMAND',+ COMMAND_LOG => 'COMMAND_LOG',++ COMMAND_ANALYZE => 'ANALYZE',+ COMMAND_ALTER_AGGREGATE => 'ALTER AGGREGATE',+ COMMAND_ALTER_COLLATION => 'ALTER COLLATION',+ COMMAND_ALTER_CONVERSION => 'ALTER CONVERSION',+ COMMAND_ALTER_DATABASE => 'ALTER DATABASE',+ COMMAND_ALTER_ROLE => 'ALTER ROLE',+ COMMAND_ALTER_ROLE_SET => 'ALTER ROLE SET',+ COMMAND_ALTER_TABLE => 'ALTER TABLE',+ COMMAND_ALTER_TABLE_INDEX => 'ALTER TABLE INDEX',+ COMMAND_BEGIN => 'BEGIN',+ COMMAND_CLOSE => 'CLOSE CURSOR',+ COMMAND_COMMIT => 'COMMIT',+ COMMAND_COPY => 'COPY',+ COMMAND_COPY_TO => 'COPY TO',+ COMMAND_COPY_FROM => 'COPY FROM',+ COMMAND_CREATE_AGGREGATE => 'CREATE AGGREGATE',+ COMMAND_CREATE_COLLATION => 'CREATE COLLATION',+ COMMAND_CREATE_CONVERSION => 'CREATE CONVERSION',+ COMMAND_CREATE_DATABASE => 'CREATE DATABASE',+ COMMAND_CREATE_INDEX => 'CREATE INDEX',+ COMMAND_DEALLOCATE => 'DEALLOCATE',+ COMMAND_DECLARE_CURSOR => 'DECLARE CURSOR',+ COMMAND_DO => 'DO',+ COMMAND_DISCARD_ALL => 'DISCARD ALL',+ COMMAND_CREATE_FUNCTION => 'CREATE FUNCTION',+ COMMAND_CREATE_ROLE => 'CREATE ROLE',+ COMMAND_CREATE_SCHEMA => 'CREATE SCHEMA',+ COMMAND_CREATE_TABLE => 'CREATE TABLE',+ COMMAND_CREATE_TABLE_AS => 'CREATE TABLE AS',+ COMMAND_DROP_DATABASE => 'DROP DATABASE',+ COMMAND_DROP_SCHEMA => 'DROP SCHEMA',+ COMMAND_DROP_TABLE => 'DROP TABLE',+ COMMAND_DROP_TABLE_INDEX => 'DROP TABLE INDEX',+ COMMAND_DROP_TABLE_TYPE => 'DROP TABLE TYPE',+ COMMAND_EXECUTE => 'EXECUTE',+ COMMAND_EXECUTE_READ => 'EXECUTE READ',+ COMMAND_EXECUTE_WRITE => 'EXECUTE WRITE',+ COMMAND_EXECUTE_FUNCTION => 'EXECUTE FUNCTION',+ COMMAND_EXPLAIN => 'EXPLAIN',+ COMMAND_FETCH => 'FETCH',+ COMMAND_GRANT => 'GRANT',+ COMMAND_INSERT => 'INSERT',+ COMMAND_PREPARE => 'PREPARE',+ COMMAND_PREPARE_READ => 'PREPARE READ',+ COMMAND_PREPARE_WRITE => 'PREPARE WRITE',+ COMMAND_REVOKE => 'REVOKE',+ COMMAND_SELECT => 'SELECT',+ COMMAND_SET => 'SET',+ COMMAND_UPDATE => 'UPDATE'+};++use constant+{+ TYPE => 'TYPE',+ TYPE_NONE => '',++ TYPE_FUNCTION => 'FUNCTION',+ TYPE_INDEX => 'INDEX',+ TYPE_TABLE => 'TABLE',+ TYPE_TYPE => 'TYPE'+};++use constant+{+ NAME => 'NAME'+};++################################################################################+# Command line parameters+################################################################################+my $strPgSqlBin = '../../../../bin/bin'; # Path of PG binaries to use for+ # this test+my $strTestPath = '../../../../data'; # Path where testing will occur+my $iDefaultPort = 6000; # Default port to run Postgres on+my $bHelp = false; # Display help+my $bQuiet = false; # Supress output except for errors+my $bNoCleanup = false; # Cleanup database on exit++GetOptions ('q|quiet' => \$bQuiet,+ 'no-cleanup' => \$bNoCleanup,+ 'help' => \$bHelp,+ 'pgsql-bin=s' => \$strPgSqlBin,+ 'test-path=s' => \$strTestPath)+ or pod2usage(2);++# Display version and exit if requested+if ($bHelp)+{+ print 'pg_audit unit test\n\n';+ pod2usage();++ exit 0;+}++################################################################################+# Global variables+################################################################################+my $hDb; # Connection to Postgres+my $strLogExpected = ''; # The expected log compared with grepping AUDIT+ # entries from the postgres log.++my $strDatabase = 'postgres'; # Connected database (modified by PgSetDatabase)+my $strUser = 'postgres'; # Connected user (modified by PgSetUser)+my $strAuditRole = 'audit'; # Role to use for auditing++my %oAuditLogHash; # Hash to store pg_audit.log GUCS+my %oAuditGrantHash; # Hash to store pg_audit grants++my $strCurrentAuditLog; # pg_audit.log setting was Postgres was started with+my $strTemporaryAuditLog; # pg_audit.log setting that was set hot++################################################################################+# Stores the mapping between commands, classes, and types+################################################################################+my %oCommandHash =+(&COMMAND_ANALYZE => {+ &CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},+ &COMMAND_ALTER_AGGREGATE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},+ &COMMAND_ALTER_DATABASE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},+ &COMMAND_ALTER_COLLATION => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},+ &COMMAND_ALTER_CONVERSION => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},+ &COMMAND_ALTER_ROLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},+ &COMMAND_ALTER_ROLE_SET => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE,+ &COMMAND => &COMMAND_ALTER_ROLE},+ &COMMAND_ALTER_TABLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},+ &COMMAND_ALTER_TABLE_INDEX => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_INDEX,+ &COMMAND => &COMMAND_ALTER_TABLE},+ &COMMAND_BEGIN => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},+ &COMMAND_CLOSE => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},+ &COMMAND_COMMIT => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},+ &COMMAND_COPY_FROM => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE,+ &COMMAND => &COMMAND_COPY},+ &COMMAND_COPY_TO => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE,+ &COMMAND => &COMMAND_COPY},+ &COMMAND_CREATE_AGGREGATE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},+ &COMMAND_CREATE_CONVERSION => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},+ &COMMAND_CREATE_COLLATION => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},+ &COMMAND_CREATE_DATABASE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},+ &COMMAND_CREATE_INDEX => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_INDEX},+ &COMMAND_DEALLOCATE => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},+ &COMMAND_DECLARE_CURSOR => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE},+ &COMMAND_DO => {&CLASS => &CLASS_FUNCTION, &TYPE => &TYPE_NONE},+ &COMMAND_DISCARD_ALL => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},+ &COMMAND_CREATE_FUNCTION => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},+ &COMMAND_CREATE_ROLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},+ &COMMAND_CREATE_SCHEMA => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},+ &COMMAND_CREATE_TABLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},+ &COMMAND_CREATE_TABLE_AS => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},+ &COMMAND_DROP_DATABASE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},+ &COMMAND_DROP_SCHEMA => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},+ &COMMAND_DROP_TABLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},+ &COMMAND_DROP_TABLE_INDEX => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_INDEX,+ &COMMAND => &COMMAND_DROP_TABLE},+ &COMMAND_DROP_TABLE_TYPE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TYPE,+ &COMMAND => &COMMAND_DROP_TABLE},+ &COMMAND_EXECUTE_READ => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE,+ &COMMAND => &COMMAND_EXECUTE},+ &COMMAND_EXECUTE_WRITE => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE,+ &COMMAND => &COMMAND_EXECUTE},+ &COMMAND_EXECUTE_FUNCTION => {&CLASS => &CLASS_FUNCTION,+ &TYPE => &TYPE_FUNCTION, &COMMAND => &COMMAND_EXECUTE},+ &COMMAND_EXPLAIN => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},+ &COMMAND_FETCH => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},+ &COMMAND_GRANT => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},+ &COMMAND_PREPARE_READ => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE,+ &COMMAND => &COMMAND_PREPARE},+ &COMMAND_PREPARE_WRITE => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE,+ &COMMAND => &COMMAND_PREPARE},+ &COMMAND_INSERT => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE},+ &COMMAND_REVOKE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},+ &COMMAND_SELECT => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE},+ &COMMAND_SET => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},+ &COMMAND_UPDATE => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE}+);++################################################################################+# CommandExecute+################################################################################+sub CommandExecute+{+ my $strCommand = shift;+ my $bSuppressError = shift;++ # Set default+ $bSuppressError = defined($bSuppressError) ? $bSuppressError : false;++ # Run the command+ my $iResult = system($strCommand);++ if ($iResult != 0 && !$bSuppressError)+ {+ confess "command '${strCommand}' failed with error ${iResult}";+ }+}++################################################################################+# log+################################################################################+sub log+{+ my $strMessage = shift;+ my $bError = shift;++ # Set default+ $bError = defined($bError) ? $bError : false;++ if (!$bQuiet)+ {+ print "${strMessage}\n";+ }++ if ($bError)+ {+ exit 1;+ }+}++################################################################################+# ArrayToString+################################################################################+sub ArrayToString+{+ my @stryArray = @_;++ my $strResult = '';++ for (my $iIndex = 0; $iIndex < @stryArray; $iIndex++)+ {+ if ($iIndex != 0)+ {+ $strResult .= ', ';+ }++ $strResult .= $stryArray[$iIndex];+ }++ return $strResult;+}++################################################################################+# BuildModule+################################################################################+sub BuildModule+{+ capture('cd ..;make');+ CommandExecute("cp ../pg_audit.so" .+ " ${strPgSqlBin}/../lib/postgresql");+ CommandExecute("cp ../pg_audit.control" .+ " ${strPgSqlBin}/../share/postgresql/extension");+ CommandExecute("cp ../pg_audit--1.0.0.sql" .+ " ${strPgSqlBin}/../share/postgresql/extension");+}++################################################################################+# PgConnect+################################################################################+sub PgConnect+{+ my $iPort = shift;++ # Set default+ $iPort = defined($iPort) ? $iPort : $iDefaultPort;++ # Log Connection+ &log(" DB: connect user ${strUser}, database ${strDatabase}");++ # Disconnect user session+ PgDisconnect();++ # Connect to the db+ $hDb = DBI->connect("dbi:Pg:dbname=${strDatabase};port=${iPort};host=/tmp",+ $strUser, undef,+ {AutoCommit => 1, RaiseError => 1});+}++################################################################################+# PgDisconnect+################################################################################+sub PgDisconnect+{+ # Connect to the db (whether it is local or remote)+ if (defined($hDb))+ {+ $hDb->disconnect;+ undef($hDb);+ }+}++################################################################################+# PgExecute+################################################################################+sub PgExecute+{+ my $strSql = shift;++ # Log the statement+ &log(" SQL: ${strSql}");++ # Execute the statement+ my $hStatement = $hDb->prepare($strSql);++ $hStatement->execute();+ $hStatement->finish();+}++################################################################################+# PgExecuteOnly+################################################################################+sub PgExecuteOnly+{+ my $strSql = shift;++ # Log the statement+ &log(" SQL: ${strSql}");++ # Execute the statement+ $hDb->do($strSql);+}++################################################################################+# PgSetDatabase+################################################################################+sub PgSetDatabase+{+ my $strDatabaseParam = shift;++ # Stop and start the database to reset pgconf entries+ PgStop();+ PgStart();++ # Execute the statement+ $strDatabase = $strDatabaseParam;+ PgConnect();+}++################################################################################+# PgSetUser+################################################################################+sub PgSetUser+{+ my $strUserParam = shift;++ $strUser = $strUserParam;++ # Stop and start the database to reset pgconf entries+ if ((defined($strTemporaryAuditLog) && !defined($strCurrentAuditLog)) ||+ (defined($strCurrentAuditLog) && !defined($strTemporaryAuditLog)) ||+ $strCurrentAuditLog ne $strTemporaryAuditLog)+ {+ $strCurrentAuditLog = $strTemporaryAuditLog;++ PgStop();+ PgStart();+ }+ else+ {+ # Execute the statement+ PgConnect();+ }+}++################################################################################+# SaveString+################################################################################+sub SaveString+{+ my $strFile = shift;+ my $strString = shift;++ # Open the file for writing+ my $hFile;++ open($hFile, '>', $strFile)+ or confess "unable to open ${strFile}";++ if ($strString ne '')+ {+ syswrite($hFile, $strString)+ or confess "unable to write to ${strFile}: $!";+ }++ close($hFile);+}++################################################################################+# PgLogExecute+################################################################################+sub PgLogExecute+{+ my $strCommand = shift;+ my $strSql = shift;+ my $oData = shift;+ my $bExecute = shift;+ my $bWait = shift;+ my $bLogSql = shift;++ # Set defaults+ $bExecute = defined($bExecute) ? $bExecute : true;+ $bWait = defined($bWait) ? $bWait : true;+ $bLogSql = defined($bLogSql) ? $bLogSql : true;++ if ($bExecute)+ {+ PgExecuteOnly($strSql);+ }++ PgLogExpect($strCommand, $bLogSql ? $strSql : '', $oData);++ if ($bWait)+ {+ PgLogWait();+ }+}++################################################################################+# PgLogExpect+################################################################################+sub PgLogExpect+{+ my $strCommand = shift;+ my $strSql = shift;+ my $oData = shift;++ # If oData is false then no logging+ if (defined($oData) && ref($oData) eq '' && !$oData)+ {+ return;+ }++ # Log based on session+ if (PgShouldLog($strCommand))+ {+ # Make sure class is defined+ my $strClass = $oCommandHash{$strCommand}{&CLASS};++ if (!defined($strClass))+ {+ confess "class is not defined for command ${strCommand}";+ }++ # Make sure object type is defined+ my $strObjectType = $oCommandHash{$strCommand}{&TYPE};++ if (!defined($strObjectType))+ {+ confess "object type is not defined for command ${strCommand}";+ }++ # Check for command override+ my $strCommandLog = $strCommand;++ if ($oCommandHash{$strCommand}{&COMMAND})+ {+ $strCommandLog = $oCommandHash{$strCommand}{&COMMAND};+ }++ my $strObjectName = '';++ if (defined($oData) && ref($oData) ne 'ARRAY')+ {+ $strObjectName = $oData;+ }++ my $strLog .= "SESSION,${strClass},${strCommandLog}," .+ "${strObjectType},${strObjectName},${strSql}";+ &log("AUDIT: ${strLog}");++ $strLogExpected .= "${strLog}\n";+ }++ # Log based on grants+ if (ref($oData) eq 'ARRAY' && ($strCommand eq COMMAND_SELECT ||+ $oCommandHash{$strCommand}{&CLASS} eq CLASS_WRITE))+ {+ foreach my $oTableHash (@{$oData})+ {+ my $strObjectName = ${$oTableHash}{&NAME};+ my $strCommandLog = ${$oTableHash}{&COMMAND};++ if (defined($oAuditGrantHash{$strAuditRole}+ {$strObjectName}{$strCommandLog}))+ {+ my $strCommandLog = defined(${$oTableHash}{&COMMAND_LOG}) ?+ ${$oTableHash}{&COMMAND_LOG} : $strCommandLog;+ my $strClass = $oCommandHash{$strCommandLog}{&CLASS};+ my $strObjectType = ${$oTableHash}{&TYPE};++ my $strLog .= "OBJECT,${strClass},${strCommandLog}," .+ "${strObjectType},${strObjectName},${strSql}";+ &log("AUDIT: ${strLog}");++ $strLogExpected .= "${strLog}\n";+ }+ }++ $oData = undef;+ }+}++################################################################################+# PgShouldLog+################################################################################+sub PgShouldLog+{+ my $strCommand = shift;++ # Make sure class is defined+ my $strClass = $oCommandHash{$strCommand}{&CLASS};++ if (!defined($strClass))+ {+ confess "class is not defined for command ${strCommand}";+ }++ # Check logging for the role+ my $bLog = undef;++ if (defined($oAuditLogHash{&CONTEXT_ROLE}{$strUser}))+ {+ $bLog = $oAuditLogHash{&CONTEXT_ROLE}{$strUser}{$strClass};+ }++ # Else check logging for the db+ elsif (defined($oAuditLogHash{&CONTEXT_DATABASE}{$strDatabase}))+ {+ $bLog = $oAuditLogHash{&CONTEXT_DATABASE}{$strDatabase}{$strClass};+ }++ # Else check logging for global+ elsif (defined($oAuditLogHash{&CONTEXT_GLOBAL}{&CONTEXT_GLOBAL}))+ {+ $bLog = $oAuditLogHash{&CONTEXT_GLOBAL}{&CONTEXT_GLOBAL}{$strClass};+ }++ return defined($bLog) ? true : false;+}++################################################################################+# PgLogWait+################################################################################+sub PgLogWait+{+ my $strLogActual;++ # Run in an eval block since grep returns 1 when nothing was found+ eval+ {+ $strLogActual = capture("grep 'LOG: AUDIT: '" .+ " ${strTestPath}/postgresql.log");+ };++ # If an error was returned, continue if it was 1, otherwise confess+ if ($@)+ {+ my $iExitStatus = $? >> 8;++ if ($iExitStatus != 1)+ {+ confess "grep returned ${iExitStatus}";+ }++ $strLogActual = '';+ }++ # Strip the AUDIT and timestamp from the actual log+ $strLogActual =~ s/prefix LOG: AUDIT\: //g;++ # Save the logs+ SaveString("${strTestPath}/audit.actual", $strLogActual);+ SaveString("${strTestPath}/audit.expected", $strLogExpected);++ CommandExecute("diff ${strTestPath}/audit.expected" .+ " ${strTestPath}/audit.actual");+}++################################################################################+# PgDrop+################################################################################+sub PgDrop+{+ my $strPath = shift;++ # Set default+ $strPath = defined($strPath) ? $strPath : $strTestPath;++ # Stop the cluster+ PgStop(true, $strPath);++ # Remove the directory+ CommandExecute("rm -rf ${strTestPath}");+}++################################################################################+# PgCreate+################################################################################+sub PgCreate+{+ my $strPath = shift;++ # Set default+ $strPath = defined($strPath) ? $strPath : $strTestPath;++ CommandExecute("${strPgSqlBin}/initdb -D ${strPath} -U ${strUser}" .+ ' -A trust > /dev/null');+}++################################################################################+# PgStop+################################################################################+sub PgStop+{+ my $bImmediate = shift;+ my $strPath = shift;++ # Set default+ $strPath = defined($strPath) ? $strPath : $strTestPath;+ $bImmediate = defined($bImmediate) ? $bImmediate : false;++ # Disconnect user session+ PgDisconnect();++ # If postmaster process is running then stop the cluster+ if (-e $strPath . '/postmaster.pid')+ {+ CommandExecute("${strPgSqlBin}/pg_ctl stop -D ${strPath} -w -s -m " .+ ($bImmediate ? 'immediate' : 'fast'));+ }+}++################################################################################+# PgStart+################################################################################+sub PgStart+{+ my $iPort = shift;+ my $strPath = shift;++ # Set default+ $iPort = defined($iPort) ? $iPort : $iDefaultPort;+ $strPath = defined($strPath) ? $strPath : $strTestPath;++ # Make sure postgres is not running+ if (-e $strPath . '/postmaster.pid')+ {+ confess "${strPath}/postmaster.pid exists, cannot start";+ }++ # Start the cluster+ CommandExecute("${strPgSqlBin}/pg_ctl start -o \"" .+ "-c port=${iPort}" .+ " -c unix_socket_directories='/tmp'" .+ " -c shared_preload_libraries='pg_audit'" .+ " -c log_min_messages=debug1" .+ " -c log_line_prefix='prefix '" .+ # " -c log_destination='stderr,csvlog'" .+ # " -c logging_collector=on" .+ (defined($strCurrentAuditLog) ?+ " -c pg_audit.log='${strCurrentAuditLog}'" : '') .+ " -c pg_audit.role='${strAuditRole}'" .+ " -c log_connections=on" .+ "\" -D ${strPath} -l ${strPath}/postgresql.log -w -s");++ # Connect user session+ PgConnect();+}++################################################################################+# PgAuditLogSet+################################################################################+sub PgAuditLogSet+{+ my $strContext = shift;+ my $strName = shift;+ my @stryClass = @_;++ # Create SQL to set the GUC+ my $strCommand;+ my $strSql;++ if ($strContext eq CONTEXT_GLOBAL)+ {+ $strCommand = COMMAND_SET;+ $strSql = "set pg_audit.log = '" .+ ArrayToString(@stryClass) . "'";+ $strTemporaryAuditLog = ArrayToString(@stryClass);+ }+ elsif ($strContext eq CONTEXT_ROLE)+ {+ $strCommand = COMMAND_ALTER_ROLE_SET;+ $strSql = "alter role ${strName} set pg_audit.log = '" .+ ArrayToString(@stryClass) . "'";+ }+ else+ {+ confess "unable to set pg_audit.log for context ${strContext}";+ }++ # Reset the audit log+ if ($strContext eq CONTEXT_GLOBAL)+ {+ delete($oAuditLogHash{$strContext});+ $strName = CONTEXT_GLOBAL;+ }+ else+ {+ delete($oAuditLogHash{$strContext}{$strName});+ }++ # Store all the classes in the hash and build the GUC+ foreach my $strClass (@stryClass)+ {+ if ($strClass eq CLASS_ALL)+ {+ $oAuditLogHash{$strContext}{$strName}{&CLASS_DDL} = true;+ $oAuditLogHash{$strContext}{$strName}{&CLASS_FUNCTION} = true;+ $oAuditLogHash{$strContext}{$strName}{&CLASS_MISC} = true;+ $oAuditLogHash{$strContext}{$strName}{&CLASS_READ} = true;+ $oAuditLogHash{$strContext}{$strName}{&CLASS_WRITE} = true;+ }++ if (index($strClass, '-') == 0)+ {+ $strClass = substr($strClass, 1);++ delete($oAuditLogHash{$strContext}{$strName}{$strClass});+ }+ else+ {+ $oAuditLogHash{$strContext}{$strName}{$strClass} = true;+ }+ }++ PgLogExecute($strCommand, $strSql);+}++################################################################################+# PgAuditGrantSet+################################################################################+sub PgAuditGrantSet+{+ my $strRole = shift;+ my $strPrivilege = shift;+ my $strObject = shift;+ my $strColumn = shift;++ # Create SQL to set the grant+ PgLogExecute(COMMAND_GRANT, "grant " . lc(${strPrivilege}) .+ (defined($strColumn) ? " (${strColumn})" : '') .+ " on ${strObject} to ${strRole}");++ $oAuditGrantHash{$strRole}{$strObject}{$strPrivilege} = true;+}++################################################################################+# PgAuditGrantReset+################################################################################+sub PgAuditGrantReset+{+ my $strRole = shift;+ my $strPrivilege = shift;+ my $strObject = shift;+ my $strColumn = shift;++ # Create SQL to set the grant+ PgLogExecute(COMMAND_REVOKE, "revoke " . lc(${strPrivilege}) .+ (defined($strColumn) ? " (${strColumn})" : '') .+ " on ${strObject} from ${strRole}");++ delete($oAuditGrantHash{$strRole}{$strObject}{$strPrivilege});+}++################################################################################+# Main+################################################################################+my @oyTable; # Store table info for select, insert, update, delete++# Drop the old cluster, build the code, and create a new cluster+PgDrop();+BuildModule();+PgCreate();+PgStart();++PgExecute("create extension pg_audit");++# Create test users and the audit role+PgExecute("create user user1");+PgExecute("create user user2");+PgExecute("create role ${strAuditRole}");++PgAuditLogSet(CONTEXT_GLOBAL, undef, (CLASS_DDL));++PgAuditLogSet(CONTEXT_ROLE, 'user2', (CLASS_READ, CLASS_WRITE));++# User1 follows the global log settings+PgSetUser('user1');+PgLogExecute(COMMAND_CREATE_TABLE, 'create table test (id int)', 'public.test');+PgLogExecute(COMMAND_SELECT, 'select * from test');++PgLogExecute(COMMAND_DROP_TABLE, 'drop table test', 'public.test');++PgSetUser('user2');+PgLogExecute(COMMAND_CREATE_TABLE,+ 'create table test2 (id int)', 'public.test2');+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.test2');+PgLogExecute(COMMAND_CREATE_TABLE,+ 'create table test3 (id int)', 'public.test2');++# Catalog select should not log+PgLogExecute(COMMAND_SELECT, 'select * from pg_class limit 1',+ false);++# Multi-table select+@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE,+ &COMMAND => &COMMAND_SELECT},+ {&NAME => 'public.test2', &TYPE => &TYPE_TABLE,+ &COMMAND => &COMMAND_SELECT});+PgLogExecute(COMMAND_SELECT, 'select * from test3, test2',+ \@oyTable);++# Various CTE combinations+PgAuditGrantSet($strAuditRole, &COMMAND_INSERT, 'public.test3');++@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE,+ &COMMAND => &COMMAND_INSERT},+ {&NAME => 'public.test2', &TYPE => &TYPE_TABLE,+ &COMMAND => &COMMAND_SELECT});+PgLogExecute(COMMAND_INSERT,+ 'with cte as (select id from test2)' .+ ' insert into test3 select id from cte',+ \@oyTable);++@oyTable = ({&NAME => 'public.test2', &TYPE => &TYPE_TABLE,+ &COMMAND => &COMMAND_INSERT},+ {&NAME => 'public.test3', &TYPE => &TYPE_TABLE,+ &COMMAND => &COMMAND_INSERT});+PgLogExecute(COMMAND_INSERT,+ 'with cte as (insert into test3 values (1) returning id)' .+ ' insert into test2 select id from cte',+ \@oyTable);++PgAuditGrantSet($strAuditRole, &COMMAND_UPDATE, 'public.test2');++@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE,+ &COMMAND => &COMMAND_INSERT},+ {&NAME => 'public.test2', &TYPE => &TYPE_TABLE,+ &COMMAND => &COMMAND_UPDATE});+PgLogExecute(COMMAND_INSERT,+ 'with cte as (update test2 set id = 1 returning id)' .+ ' insert into test3 select id from cte',+ \@oyTable);++@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE,+ &COMMAND => &COMMAND_UPDATE},+ {&NAME => 'public.test2', &TYPE => &TYPE_TABLE,+ &COMMAND => &COMMAND_INSERT},+ {&NAME => 'public.test2', &TYPE => &TYPE_TABLE,+ &COMMAND => &COMMAND_SELECT, &COMMAND_LOG => &COMMAND_INSERT});+PgLogExecute(COMMAND_UPDATE,+ 'with cte as (insert into test2 values (1) returning id)' .+ ' update test3 set id = cte.id' .+ ' from cte where test3.id <> cte.id',+ \@oyTable);++PgSetUser('postgres');+PgAuditLogSet(CONTEXT_ROLE, 'user2', (CLASS_NONE));+PgSetUser('user2');++# Column-based audits+PgLogExecute(COMMAND_CREATE_TABLE,+ 'create table test4 (id int, name text)', 'public.test4');+PgAuditGrantSet($strAuditRole, COMMAND_SELECT, 'public.test4', 'name');+PgAuditGrantSet($strAuditRole, COMMAND_UPDATE, 'public.test4', 'id');+PgAuditGrantSet($strAuditRole, COMMAND_INSERT, 'public.test4', 'name');++# Select+@oyTable = ();+PgLogExecute(COMMAND_SELECT, 'select id from public.test4',+ \@oyTable);++@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE,+ &COMMAND => &COMMAND_SELECT});+PgLogExecute(COMMAND_SELECT, 'select name from public.test4',+ \@oyTable);++# Insert+@oyTable = ();+PgLogExecute(COMMAND_INSERT, 'insert into public.test4 (id) values (1)',+ \@oyTable);++@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE,+ &COMMAND => &COMMAND_INSERT});+PgLogExecute(COMMAND_INSERT, "insert into public.test4 (name) values ('test')",+ \@oyTable);++# Update+@oyTable = ();+PgLogExecute(COMMAND_UPDATE, "update public.test4 set name = 'foo'",+ \@oyTable);++@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE,+ &COMMAND => &COMMAND_UPDATE});+PgLogExecute(COMMAND_UPDATE, "update public.test4 set id = 1",+ \@oyTable);++@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE,+ &COMMAND => &COMMAND_SELECT, &COMMAND_LOG => &COMMAND_UPDATE});+PgLogExecute(COMMAND_UPDATE,+ "update public.test4 set name = 'foo' where name = 'bar'",+ \@oyTable);++# Drop test tables+PgLogExecute(COMMAND_DROP_TABLE, "drop table test2", 'public.test2');+PgLogExecute(COMMAND_DROP_TABLE, "drop table test3", 'public.test3');+PgLogExecute(COMMAND_DROP_TABLE, "drop table test4", 'public.test4');+++# Make sure there are no more audit events pending in the postgres log+PgLogWait();++# Now create some email friendly tests. These first tests are session logging+# only.+PgSetUser('postgres');++&log("\nExamples:");++&log("\nSession Audit:\n");++PgAuditLogSet(CONTEXT_GLOBAL, undef, (CLASS_DDL, CLASS_READ));++# !!! Trying to build test to exclude function calls (did not work)+# PgLogExecute(COMMAND_CREATE_ROLE, 'create role func_owner');+# PgAuditLogSet(CONTEXT_ROLE, 'func_owner');+# PgLogExecute(COMMAND_SET, 'set role func_owner');+# PgLogExecute(COMMAND_CREATE_FUNCTION, 'CREATE FUNCTION func_test(a int)' .+# ' returns int as $$ begin return a + 1;' .+# ' end $$language plpgsql security definer');+# PgLogExecute(COMMAND_GRANT, 'grant execute on function func_test(int) to user1');++PgSetUser('user1');++PgLogExecute(COMMAND_CREATE_TABLE,+ 'create table account (id int, name text, password text,' .+ ' description text)', 'public.account');+PgLogExecute(COMMAND_SELECT,+ 'select * from account');+PgLogExecute(COMMAND_INSERT,+ "insert into account (id, name, password, description)" .+ " values (1, 'user1', 'HASH1', 'blah, blah')");+&log("AUDIT: <nothing logged>");++# Test function without auditing (did not work)+# PgLogExecute(COMMAND_SELECT, 'select func_test(1)');++# Now tests for object logging+&log("\nObject Audit:\n");++PgSetUser('postgres');+PgAuditLogSet(CONTEXT_GLOBAL, undef, (CLASS_NONE));+PgExecute("set pg_audit.role = 'audit'");+PgSetUser('user1');++PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.account', 'password');++@oyTable = ();+PgLogExecute(COMMAND_SELECT, 'select id, name from account',+ \@oyTable);+&log("AUDIT: <nothing logged>");++@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,+ &COMMAND => &COMMAND_SELECT});+PgLogExecute(COMMAND_SELECT, 'select password from account',+ \@oyTable);++PgAuditGrantSet($strAuditRole, &COMMAND_UPDATE,+ 'public.account', 'name, password');++@oyTable = ();+PgLogExecute(COMMAND_UPDATE, "update account set description = 'yada, yada'",+ \@oyTable);+&log("AUDIT: <nothing logged>");++@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,+ &COMMAND => &COMMAND_UPDATE});+PgLogExecute(COMMAND_UPDATE, "update account set password = 'HASH2'",+ \@oyTable);++# Now tests for session/object logging+&log("\nSession/Object Audit:\n");++PgSetUser('postgres');+PgAuditLogSet(CONTEXT_ROLE, 'user1', (CLASS_READ, CLASS_WRITE));+PgSetUser('user1');++PgLogExecute(COMMAND_CREATE_TABLE,+ 'create table account_role_map (account_id int, role_id int)',+ 'public.account_role_map');+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.account_role_map');++@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,+ &COMMAND => &COMMAND_SELECT},+ {&NAME => 'public.account_role_map', &TYPE => &TYPE_TABLE,+ &COMMAND => &COMMAND_SELECT});+PgLogExecute(COMMAND_SELECT,+ 'select account.password, account_role_map.role_id from account' .+ ' inner join account_role_map' .+ ' on account.id = account_role_map.account_id',+ \@oyTable);++@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,+ &COMMAND => &COMMAND_SELECT});+PgLogExecute(COMMAND_SELECT, 'select password from account',+ \@oyTable);++@oyTable = ();+PgLogExecute(COMMAND_UPDATE, "update account set description = 'yada, yada'",+ \@oyTable);+&log("AUDIT: <nothing logged>");++@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,+ &COMMAND => &COMMAND_SELECT, &COMMAND_LOG => &COMMAND_UPDATE});+PgLogExecute(COMMAND_UPDATE,+ "update account set description = 'yada, yada'" .+ " where password = 'HASH2'",+ \@oyTable);++@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,+ &COMMAND => &COMMAND_UPDATE});+PgLogExecute(COMMAND_UPDATE, "update account set password = 'HASH2'",+ \@oyTable);++# Test all sql commands+&log("\nExhaustive Command Tests:\n");++PgSetUser('postgres');++PgAuditLogSet(CONTEXT_GLOBAL, undef, (CLASS_ALL));+PgLogExecute(COMMAND_SET, "set pg_audit.role = 'audit'");++PgLogExecute(COMMAND_DO, "do \$\$\ begin raise notice 'test'; end; \$\$;");+PgLogExecute(COMMAND_CREATE_SCHEMA, "create schema test");++# Test COPY+PgLogExecute(COMMAND_COPY_TO,+ "COPY pg_class to '" . abs_path($strTestPath) . "/class.out'");+PgLogExecute(COMMAND_CREATE_TABLE_AS,+ "CREATE TABLE test.pg_class as select * from pg_class",+ 'test.pg_class', true, false);+PgLogExecute(COMMAND_INSERT,+ "CREATE TABLE test.pg_class as select * from pg_class",+ undef, false, true);+PgLogExecute(COMMAND_INSERT,+ "COPY test.pg_class from '" . abs_path($strTestPath) .+ "/class.out'", undef, true, false);+PgLogExecute(COMMAND_COPY_FROM,+ "COPY test.pg_class from '" . abs_path($strTestPath) .+ "/class.out'", undef, false, true);++# Test prepared SELECT+PgLogExecute(COMMAND_PREPARE_READ,+ 'PREPARE pgclassstmt (oid) as select *' .+ ' from pg_class where oid = $1');+PgLogExecute(COMMAND_EXECUTE_READ,+ 'EXECUTE pgclassstmt (1)');+PgLogExecute(COMMAND_DEALLOCATE,+ 'DEALLOCATE pgclassstmt');++# Test cursor+PgLogExecute(COMMAND_BEGIN,+ 'BEGIN');+PgLogExecute(COMMAND_DECLARE_CURSOR,+ 'DECLARE ctest SCROLL CURSOR FOR SELECT * FROM pg_class');+PgLogExecute(COMMAND_FETCH,+ 'FETCH NEXT FROM ctest');+PgLogExecute(COMMAND_CLOSE,+ 'CLOSE ctest');+PgLogExecute(COMMAND_COMMIT,+ 'COMMIT');++# Test prepared INSERT+PgLogExecute(COMMAND_CREATE_TABLE,+ 'create table test.test_insert (id int)', 'test.test_insert');+PgLogExecute(COMMAND_PREPARE_WRITE,+ 'PREPARE pgclassstmt (oid) as insert' .+ ' into test.test_insert (id) values ($1)');+PgLogExecute(COMMAND_INSERT,+ 'EXECUTE pgclassstmt (1)', undef, true, false);+PgLogExecute(COMMAND_EXECUTE_WRITE,+ 'EXECUTE pgclassstmt (1)', undef, false, true);++# Create a table with a primary key+PgLogExecute(COMMAND_CREATE_TABLE,+ 'create table test (id int primary key, name text,' .+ 'description text)',+ 'public.test', true, false);+PgLogExecute(COMMAND_CREATE_INDEX,+ 'create table test (id int primary key, name text,' .+ 'description text)',+ 'public.test_pkey', false, true);+PgLogExecute(COMMAND_ANALYZE, 'analyze test');++# Grant select to public - this should have no affect on auditing+PgLogExecute(COMMAND_GRANT, 'grant select on public.test to public');+PgLogExecute(COMMAND_SELECT, 'select * from test');++# Now grant select to audit and it should be logged+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.test');+@oyTable = ({&NAME => 'public.test', &TYPE => &TYPE_TABLE,+ &COMMAND => &COMMAND_SELECT});+PgLogExecute(COMMAND_SELECT, 'select * from test', \@oyTable);++# Check columns granted to public and make sure they do not log+PgAuditGrantReset($strAuditRole, &COMMAND_SELECT, 'public.test');+PgLogExecute(COMMAND_GRANT, 'grant select (name) on public.test to public');+PgLogExecute(COMMAND_SELECT, 'select * from test');+PgLogExecute(COMMAND_SELECT, 'select from test');++# Try a select that does not reference any tables+PgLogExecute(COMMAND_SELECT, 'select 1, current_timestamp');++# Try explain+PgLogExecute(COMMAND_EXPLAIN, 'explain select 1');++# Now set grant to a specific column to audit and make sure it logs+# Make sure the the converse is true+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.test',+ 'name, description');+PgLogExecute(COMMAND_SELECT, 'select id from test');++@oyTable = ({&NAME => 'public.test', &TYPE => &TYPE_TABLE,+ &COMMAND => &COMMAND_SELECT});+PgLogExecute(COMMAND_SELECT, 'select name from test', \@oyTable);++PgLogExecute(COMMAND_ALTER_TABLE,+ 'alter table test drop description', 'public.test');+@oyTable = ({&NAME => 'public.test', &TYPE => &TYPE_TABLE,+ &COMMAND => &COMMAND_SELECT});+PgLogExecute(COMMAND_SELECT, 'select from test', \@oyTable);++PgLogExecute(COMMAND_ALTER_TABLE,+ 'alter table test rename to test2', 'public.test');+PgLogExecute(COMMAND_ALTER_TABLE,+ 'alter table test2 set schema test', 'public.test2', true, false);+PgLogExecute(COMMAND_ALTER_TABLE_INDEX, 'alter table test2 set schema test',+ 'public.test_pkey', false, true);+PgLogExecute(COMMAND_ALTER_TABLE, 'alter table test.test2 add description text',+ 'test.test2');+PgLogExecute(COMMAND_ALTER_TABLE, 'alter table test.test2 drop description',+ 'test.test2');+PgLogExecute(COMMAND_DROP_TABLE_INDEX, 'drop table test.test2',+ 'test.test_pkey', false, false);+PgLogExecute(COMMAND_DROP_TABLE, 'drop table test.test2',+ 'test.test2', true, true);++PgLogExecute(COMMAND_CREATE_FUNCTION, 'CREATE FUNCTION int_add(a int, b int)' .+ ' returns int as $$ begin return a + b;' .+ ' end $$language plpgsql');+PgLogExecute(COMMAND_SELECT, "select int_add(1, 1)",+ undef, true, false);+PgLogExecute(COMMAND_EXECUTE_FUNCTION, "select int_add(1, 1)",+ 'public.int_add', false, true);++PgLogExecute(COMMAND_CREATE_AGGREGATE, "CREATE AGGREGATE sum_test (int)" .+ " (sfunc = int_add, stype = int, initcond = 0)");+PgLogExecute(COMMAND_ALTER_AGGREGATE,+ "ALTER AGGREGATE sum_test (int) rename to sum_test2");++PgLogExecute(COMMAND_CREATE_COLLATION,+ "CREATE COLLATION collation_test FROM \"de_DE\"");+PgLogExecute(COMMAND_ALTER_COLLATION,+ "ALTER COLLATION collation_test rename to collation_test2");++PgLogExecute(COMMAND_CREATE_CONVERSION,+ "CREATE CONVERSION conversion_test FOR 'SQL_ASCII' TO".+ " 'MULE_INTERNAL' FROM ascii_to_mic");+PgLogExecute(COMMAND_ALTER_CONVERSION,+ "ALTER CONVERSION conversion_test rename to conversion_test2");++PgLogExecute(COMMAND_CREATE_DATABASE, "CREATE DATABASE database_test");+PgLogExecute(COMMAND_ALTER_DATABASE,+ "ALTER DATABASE database_test rename to database_test2");+PgLogExecute(COMMAND_DROP_DATABASE, "DROP DATABASE database_test2");++# Make sure there are no more audit events pending in the postgres log+PgLogWait();++# Stop the database+if (!$bNoCleanup)+{+ PgDrop();+}diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgmlindex a698d0f..5b247a9 100644--- a/doc/src/sgml/contrib.sgml+++ b/doc/src/sgml/contrib.sgml@@ -124,6 +124,7 @@ CREATE EXTENSION <replaceable>module_name</> FROM unpackaged;
<ree;
&pageinspect;
&passwordcheck;
+ &pgaudit;
&pgbuffercache;
&pgcrypto;
&pgfreespacemap;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgmlindex f03b72a..e4f0bdc 100644--- a/doc/src/sgml/filelist.sgml+++ b/doc/src/sgml/filelist.sgml@@ -124,6 +124,7 @@
<!ENTITY oid2name SYSTEM "oid2name.sgml">
<!ENTITY pageinspect SYSTEM "pageinspect.sgml">
<!ENTITY passwordcheck SYSTEM "passwordcheck.sgml">
+<!ENTITY pgaudit SYSTEM "pgaudit.sgml">
<!ENTITY pgbench SYSTEM "pgbench.sgml">
<!ENTITY pgarchivecleanup SYSTEM "pgarchivecleanup.sgml">
<!ENTITY pgbuffercache SYSTEM "pgbuffercache.sgml">
diff --git a/doc/src/sgml/pgaudit.sgml b/doc/src/sgml/pgaudit.sgmlnew file mode 100644index 0000000..d62d45f--- /dev/null+++ b/doc/src/sgml/pgaudit.sgml@@ -0,0 +1,330 @@+<!-- doc/src/sgml/pgaudit.sgml -->++<sect1 id="pgaudit" xreflabel="pgaudit">+ <title>pg_audit</title>++ <indexterm zone="pgaudit">+ <primary>pg_audit</primary>+ </indexterm>++ <para>+ The <filename>pg_audit</filename> module provides session and object+ auditing via the standard logging facility. Session and object auditing are+ completely independent and can be combined.+ </para>++ <sect2>+ <title>Session Auditing</title>++ <para>+ Session auditing allows the logging of all commands that are executed by+ a user in the backend. Each command is logged with a single entry and+ includes the audit type (e.g. <literal>SESSION</literal>), command type+ (e.g. <literal>CREATE TABLE</literal>, <literal>SELECT</literal>) and+ statement (e.g. <literal>"select * from test"</literal>).++ Fully-qualified names and object types will be logged for+ <literal>CREATE</literal>, <literal>UPDATE</literal>, and+ <literal>DROP</literal> commands on <literal>TABLE</literal>,+ <literal>MATVIEW</literal>, <literal>VIEW</literal>,+ <literal>INDEX</literal>, <literal>FOREIGN TABLE</literal>,+ <literal>COMPOSITE TYPE</literal>, <literal>INDEX</literal>, and+ <literal>SEQUENCE</literal> objects as well as function calls.+ </para>++ <sect3>+ <title>Configuration</title>++ <para>+ Session logging is controlled by the <literal>pg_audit.log</literal>+ GUC. There are five classes of commands that are recognized:++ <itemizedlist>+ <listitem>+ <para>+ <literal>READ</literal> - <literal>SELECT</literal> and+ <literal>COPY</literal> when the source is a table or query.+ </para>+ </listitem>+ <listitem>+ <para>+ <literal>WRITE</literal> - <literal>INSERT</literal>,+ <literal>UPDATE</literal>, <literal>DELETE</literal>,+ <literal>TRUNCATE</literal>, and <literal>COPY</literal> when the+ destination is a table.+ </para>+ </listitem>+ <listitem>+ <para>+ <literal>FUNCTION</literal> - Function calls and+ <literal>DO</literal> blocks.+ </para>+ </listitem>+ <listitem>+ <para>+ <literal>DDL</literal> - DDL, plus <literal>VACUUM</literal>,+ <literal>REINDEX</literal>, and <literal>ANALYZE</literal>.+ </para>+ </listitem>+ <listitem>+ <para>+ <literal>MISC</literal> - Miscellaneous commands, e.g.+ <literal>DISCARD</literal>, <literal>FETCH</literal>,+ <literal>CHECKPOINT</literal>.+ </para>+ </listitem>+ </itemizedlist>+ </para>++ <para>+ Enable session logging for all writes and DDL:+ <programlisting>+pg_audit.log = 'write, ddl'+ </programlisting>+ </para>++ <para>+ Enable session logging for all commands except miscellaneous:+ <programlisting>+pg_audit.log = 'all, -misc'+ </programlisting>+ </para>+ + <para>+ Note that <literal>pg_audit.log</literal> can be set globally (in + <filename>postgresql.conf</filename>), at the database level (using+ <literal>alter database ... set</literal>), or at the role level (using+ <literal>alter role ... set</literal>).+ </para>+ </sect3>++ <sect3>+ <title>Examples</title>++ <para>+ Set <literal>pg_audit.log = 'read, ddl'</literal> in+ <literal>postgresql.conf</literal>.+ </para>++ <para>+ SQL:+ </para>++ <programlisting>+create table account+(+ id int,+ name text,+ password text,+ description text+);++select *+ from account;++insert into account (id, name, password, description)+ values (1, 'user1', 'HASH1', 'blah, blah');+ </programlisting>++ <para>+ Log Output:+ </para>++ <programlisting>+AUDIT: SESSION,DDL,CREATE TABLE,TABLE,public.account,create table account+(+ id int,+ name text,+ password text,+ description text+);+AUDIT: SESSION,READ,SELECT,,,select *+ from account+ </programlisting>+ </sect3>+ </sect2>++ <sect2>+ <title>Object Auditing</title>++ <para>+ Object auditing logs commands that affect a particular object. Only+ <literal>SELECT</literal>, <literal>INSERT</literal>,+ <literal>UPDATE</literal> and <literal>DELETE</literal> commands are+ supported.+ </para>++ <sect3>+ <title>Configuration</title>++ <para>+ Object-level auditing is implemented via the roles system. The+ <literal>pg_audit.role</literal> GUC defines the role that will be used+ for auditing. An object will be audited when the audit role has+ permissions for the command executed or inherits the permissions from+ another role.+ </para>++ <programlisting>+postresql.conf: pg_audit.role = 'audit'++grant select, delete+ on public.account;+ </programlisting>++ <para>+ Note that <literal>pg_audit.role</literal> can be set globally (in + <filename>postgresql.conf</filename>), at the database level (using+ <literal>alter database ... set</literal>), or at the role level (using+ <literal>alter role ... set</literal>).+ </para>+ </sect3>++ <sect3>+ <title>Examples</title>++ <para>+ Set <literal>pg_audit.role = 'audit'</literal> in+ <literal>postgresql.conf</literal>.+ </para>++ <para>+ SQL:+ </para>++ <programlisting>+create table account+(+ id int,+ name text,+ password text,+ description text+);++grant select (password)+ on public.account+ to audit;++select id, name+ from account;++select password+ from account;++grant update (name, password)+ on public.account+ to audit;++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 audit;++select account.password,+ account_role_map.role_id+ from account+ inner join account_role_map+ on account.id = account_role_map.account_id+ </programlisting>++ <para>+ Log Output:+ </para>++ <programlisting>+AUDIT: OBJECT,READ,SELECT,TABLE,public.account,select password+ from account+AUDIT: OBJECT,WRITE,UPDATE,TABLE,public.account,update account+ set password = 'HASH2'+AUDIT: OBJECT,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,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+ </programlisting>+ </sect3>+ </sect2>++ <sect2>+ <title>Format</title>++ <para>+ Audit entries are written to the standard logging facility and contain+ the following columns in comma-separated format:++ <note>+ <para>+ Output is not in compliant CSV format. If machine-readability is+ required then consider setting+ <literal>log_destination = 'csvlog'</literal>.+ </para>+ </note>++ <itemizedlist>+ <listitem>+ <para>+ <literal>AUDIT_TYPE</literal> - <literal>SESSION</literal> or+ <literal>OBJECT</literal>.+ </para>+ </listitem>+ <listitem>+ <para>+ <literal>CLASS</literal> - <literal>READ</literal>,+ <literal>WRITE</literal>, <literal>FUNCTION</literal>,+ <literal>DDL</literal>, or <literal>MISC</literal>.+ </para>+ </listitem>+ <listitem>+ <para>+ <literal>COMMAND</literal> - <literal>ALTER TABLE</literal>,+ <literal>SELECT</literal>, <literal>CREATE INDEX</literal>,+ <literal>UPDATE</literal>, etc.+ </para>+ </listitem>+ <listitem>+ <para>+ <literal>OBJECT_TYPE</literal> - <literal>TABLE</literal>,+ <literal>INDEX</literal>, <literal>VIEW</literal>, etc. Only+ available for DML and certain DDL commands.+ </para>+ </listitem>+ <listitem>+ <para>+ <literal>OBJECT_NAME</literal> - The fully-qualified object name+ (e.g. public.account). Only available for DML and certain DDL+ commands.+ </para>+ </listitem>+ <listitem>+ <para>+ <literal>STATEMENT</literal> - Statement execute on the backend.+ </para>+ </listitem>+ </itemizedlist>+ </para>+ </sect2>++ <sect2>+ <title>Authors</title>++ <para>+ Abhijit Menon-Sen <email>ams@2ndQuadrant.com</email>, Ian Barwick <email>ian@2ndQuadrant.com</email>, and David Steele <email>david@pgmasters.net</email>.+ </para>+ </sect2>+</sect1>
signature.asc (application/pgp-signature, 818B) download
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: pgsql-hackers@postgresql.org
Cc: david@pgmasters.net, sfrost@snowman.net, ams@2ndquadrant.com
Subject: Re: Auditing extension for PostgreSQL (Take 2)
In-Reply-To: <54ECA551.8060306@pgmasters.net>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by DDX for PostgreSQL; see mirroring instructions
for how to clone and mirror all data and code used for this inbox