From: David Steele <david@pgmasters.net>
To: Sawada Masahiko <sawada.mshk@gmail.com>
Cc: Alvaro Herrera <alvherre@2ndquadrant.com>
Cc: Abhijit Menon-Sen <ams@2ndquadrant.com>
Cc: Stephen Frost <sfrost@snowman.net>
Cc: Fujii Masao <masao.fujii@gmail.com>
Cc: PostgreSQL-development <pgsql-hackers@postgresql.org>
Subject: Re: Auditing extension for PostgreSQL (Take 2)
Date: Wed, 01 Apr 2015 13:46:51 -0400
Message-ID: <551C2F0B.3010305@pgmasters.net> (raw)
In-Reply-To: <5512B71C.7030808@pgmasters.net>
References: <54E005CC.1060605@pgmasters.net>
<20150217153402.GQ6717@tamriel.snowman.net>
<54EB4E56.7050102@pgmasters.net>
<54ECA551.8060306@pgmasters.net>
<20150323053146.GA1919@toroid.org>
<55104204.80008@pgmasters.net>
<CAD21AoBBQXPBiOncyvZdtOfSzr121GyDoARc4jXo2v-JM_45EQ@mail.gmail.com>
<20150323181724.GG3636@alvh.no-ip.org>
<CAD21AoA1-UVvjt0q8wvXBRc83RRwDhBgtRTR5vXByPdKb=e6-g@mail.gmail.com>
<55118509.8020009@pgmasters.net>
<CAD21AoD+MFYjjTz08gZtyDNQ77kt71ckyKvo7W+QuPq4icMjQA@mail.gmail.com>
<55122A1E.7030401@pgmasters.net>
<CAD21AoBFs2sA31Y+zgioQYfiXc0vGEOM3GcgouNc_AH00QCCow@mail.gmail.com>
<5512B71C.7030808@pgmasters.net>
List-Unsubscribe: <mailto:majordomo@postgresql.org?body=unsub%20pgsql-hackers>
Hi Sawada,
On 3/25/15 9:24 AM, David Steele wrote:
> On 3/25/15 7:46 AM, Sawada Masahiko wrote:
>> 2.
>> I got ERROR when executing function uses cursor.
>>
>> 1) create empty table (hoge table)
>> 2) create test function as follows.
>>
>> 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 ;
>>
>> 3) execute test function (got ERROR)
>> =# select test();
>> LOG: AUDIT: SESSION,6,1,READ,SELECT,,,selecT test();
>> LOG: AUDIT: SESSION,6,2,FUNCTION,EXECUTE,FUNCTION,public.test,selecT test();
>> LOG: AUDIT: SESSION,6,3,READ,SELECT,,,select * from hoge
>> CONTEXT: PL/pgSQL function test() line 6 at OPEN
>> ERROR: pg_audit stack is already empty
>> STATEMENT: selecT test();
>>
>> It seems like that the item in stack is already freed by deleting
>> pg_audit memory context (in MemoryContextDelete()),
>> before calling stack_pop in dropping of top-level Portal.
This has been fixed and I have attached a new patch. I've seen this
with cursors before where the parent MemoryContext is freed before
control is returned to ProcessUtility. I think that's strange behavior
but there's not a lot I can do about it.
The code I put in to deal with this situation was not quite robust
enough so I had to harden it a bit more.
Let me know if you see any other issues.
Thanks,
--
- David Steele
david@pgmasters.net
Attachments:pg_audit-v6.patch (text/plain, 101.9K) 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..9d9ee83--- /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.cnew file mode 100644index 0000000..b34df5a--- /dev/null+++ b/contrib/pg_audit/pg_audit.c@@ -0,0 +1,1688 @@+/*------------------------------------------------------------------------------+ * 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);++/*+ * Event trigger prototypes+ */+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);++/*+ * auditRole is the string value of the pg_audit.role GUC, which contains the+ * role for grant-based auditing.+ */+char *auditRole = NULL;++/*+ * auditLog is the string value of the pg_audit.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+ * pg_audit.log GUC.+ */+#define CLASS_DDL "DDL"+#define CLASS_FUNCTION "FUNCTION"+#define CLASS_MISC "MISC"+#define CLASS_PARAMETER "PARAMETER"+#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,++ /* DDL: CREATE/DROP/ALTER */+ LOG_DDL = (1 << 1),++ /* Function execution */+ LOG_FUNCTION = (1 << 2),++ /* Statements not covered by another class */+ LOG_MISC = (1 << 3),++ /* Function execution */+ LOG_PARAMETER = (1 << 4),++ /* SELECT */+ LOG_READ = (1 << 5),++ /* INSERT, UPDATE, DELETE, TRUNCATE */+ LOG_WRITE = (1 << 6),++ /* 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 a statement affects multiple objects multiple AuditEvents must be+ * created to represent it.+ */+typedef struct+{+ int64 statementId;+ int64 substatementId;++ LogStmtLevel logStmtLevel;+ NodeTag commandTag;+ const char *command;+ const char *objectType;+ char *objectName;+ const char *commandText;+ ParamListInfo paramList;++ bool granted;+ bool logged;+} 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;++/*+ * Track when an internal statement is running so it is not logged+ */+static bool internalStatement = false;++/*+ * Track running total for statements and substatements and whether or not+ * anything has been logged since this 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 in case of error */+ internalStatement = false;++ /* Reset sub statement total */+ substatementTotal = 0;++ /* Reset statement logged flag total */+ statementLogged = false;+ }++ return;+ }++ /* Still looking, test the next item */+ 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 */+ contextAudit = AllocSetContextCreate(CurrentMemoryContext,+ "pg_audit stack context",+ ALLOCSET_DEFAULT_MINSIZE,+ ALLOCSET_DEFAULT_INITSIZE,+ ALLOCSET_DEFAULT_MAXSIZE);+ contextOld = MemoryContextSwitchTo(contextAudit);++ /* Allocate the stack item */+ stackItem = palloc0(sizeof(AuditEventStackItem));++ /* Store memory contexts */+ stackItem->contextAudit = contextAudit;++ /* If item already on stack then push it down */+ if (auditEventStack != NULL)+ stackItem->next = auditEventStack;+ else+ stackItem->next = NULL;++ /*+ * Create the unique stackId - used to keep the stack sane when memory+ * contexts are freed unexpectedly.+ */+ 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 item on the stack */+ auditEventStack = stackItem;++ /* Return to the old memory context */+ MemoryContextSwitchTo(contextOld);++ /* Return the stack item */+ 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);+ }+}++/*+ * 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 return. 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 permissions were granted+ * via roles or if the statement belongs in a class that is being logged.+ */+static void+log_audit_event(AuditEventStackItem *stackItem)+{+ MemoryContext contextOld;+ StringInfoData auditStr;++ /* By default put everything in the MISC class. */+ enum LogClass class = LOG_MISC;+ const char *className = CLASS_MISC;++ /* Classify the statement using log stmt level and the command tag */+ switch (stackItem->auditEvent.logStmtLevel)+ {+ case LOGSTMT_MOD:+ className = CLASS_WRITE;+ class = LOG_WRITE;+ break;++ case LOGSTMT_DDL:+ className = CLASS_DDL;+ class = LOG_DDL;++ case LOGSTMT_ALL:+ switch (stackItem->auditEvent.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;+ }++ /*+ * Only log the statement if:+ *+ * 1. If permissions were granted via roles+ * 2. The statement belongs to a class that is being logged+ */+ if (!stackItem->auditEvent.granted && !(auditLogBitmap & class))+ return;++ /* Use audit memory context in case something is not freed */+ 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 string */+ 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);+ appendStringInfoCharMacro(&auditStr, ',');++ append_valid_csv(&auditStr, stackItem->auditEvent.commandText);++ /* If parameter logging is turned on and there are parameters to log */+ if (auditLogBitmap & LOG_PARAMETER &&+ stackItem->auditEvent.paramList != NULL &&+ stackItem->auditEvent.paramList->numParams > 0 &&+ !IsAbortedTransactionBlockState())+ {+ ParamListInfo paramList = stackItem->auditEvent.paramList;+ int paramIdx;++ /* Iterate through all params */+ for (paramIdx = 0; paramIdx < paramList->numParams; paramIdx++)+ {+ ParamExternData *prm = ¶mList->params[paramIdx];+ Oid typeOutput;+ bool typeIsVarLena;+ char *paramStr;++ /* Add a comma for each param */+ appendStringInfoCharMacro(&auditStr, ',');++ /* Skip this param 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(&auditStr, paramStr);+ pfree(paramStr);+ }+ }++ /* Log the audit string */+ ereport(LOG,+ (errmsg("AUDIT: %s,%ld,%ld,%s,%s",+ stackItem->auditEvent.granted ?+ AUDIT_TYPE_OBJECT : AUDIT_TYPE_SESSION,+ stackItem->auditEvent.statementId,+ stackItem->auditEvent.substatementId,+ className, auditStr.data),+ errhidestmt(true)));++ /* Mark the audit event as logged */+ stackItem->auditEvent.logged = true;++ /* Switch back to the old memory context */+ 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 */+ 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));++ /* 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 = 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 an attribute.+ */+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));++ /* 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 = 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 an attribute in+ * the provided set. If the set is empty, then all valid attributes 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 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 (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;+ }+ }++ /* Free the column set */+ 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;++ /*+ * Filter out any system relations+ */+ relOid = rte->relid;+ rel = relation_open(relOid, NoLock);++ if (IsSystemNamespace(RelationGetNamespace(rel)))+ {+ relation_close(rel, NoLock);+ continue;+ }++ /*+ * 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;+ }++ /*+ * Fill values in the event struct that are required for session+ * logging.+ */+ auditEventStack->auditEvent.granted = false;++ /* If this is the first rte then session log */+ if (first)+ {+ auditEventStack->auditEvent.objectName = "";+ auditEventStack->auditEvent.objectType = "";++ log_audit_event(auditEventStack);++ first = false;+ }++ /* Get the relation 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 the relation 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) &+ 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 to see if the audit role has+ * priveleges on any of them.+ */+ if (auditPerms & ACL_SELECT)+ {+ auditEventStack->auditEvent.granted =+ audit_on_any_attribute(relOid, auditOid,+ rte->selectedCols,+ ACL_SELECT);+ }++ /*+ * Check the modified columns to see if the audit role has+ * privileges on any of them.+ */+ if (!auditEventStack->auditEvent.granted)+ {+ auditPerms &= (ACL_INSERT | ACL_UPDATE);++ if (auditPerms)+ {+ auditEventStack->auditEvent.granted =+ audit_on_any_attribute(relOid, auditOid,+ rte->modifiedCols,+ auditPerms);+ }+ }+ }+ }++ /* Only do relation level logging if a grant was found. */+ if (auditEventStack->auditEvent.granted)+ {+ auditEventStack->auditEvent.logged = 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 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);+ auditEventStack->auditEvent.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:+ 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_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;++ /*+ * Any other cases will be handled by log_utility_command().+ */+ default:+ return;+ break;+ }+ }+}++/*+ * 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);+}++/*+ * 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:+ if (auditLogBitmap & LOG_FUNCTION)+ log_function_execute(objectId);+ break;++ /* Log create */+ case OAT_POST_CREATE:+ if (auditLogBitmap & LOG_DDL)+ {+ ObjectAccessPostCreate *pc = arg;++ if (pc->is_internal)+ return;++ log_create_alter_drop(classId, objectId);+ }+ break;++ /* Log alter */+ case OAT_POST_ALTER:+ if (auditLogBitmap & LOG_DDL)+ {+ ObjectAccessPostAlter *pa = arg;++ if (pa->is_internal)+ return;++ log_create_alter_drop(classId, objectId);+ }+ break;++ /* Log drop */+ case OAT_DROP:+ if (auditLogBitmap & LOG_DDL)+ {+ 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;+static ExecutorStart_hook_type next_ExecutorStart_hook = NULL;+static ExecutorEnd_hook_type next_ExecutorEnd_hook = NULL;++/*+ * Hook ExecutorStart to get the query text and basic command type for queries+ * that do not contain a table so can't be idenitified accurately in+ * ExecutorCheckPerms.+ */+static void+pg_audit_ExecutorStart_hook(QueryDesc *queryDesc, int eflags)+{+ AuditEventStackItem *stackItem = NULL;++ if (!internalStatement)+ {+ /* Allocate the audit event */+ stackItem = stack_push();++ /* Initialize command */+ 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.objectName = "";+ stackItem->auditEvent.objectType = "";+ 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);+}++/*+ * 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 ExecutorEnd to pop statement audit event off the stack.+ */+static void+pg_audit_ExecutorEnd_hook(QueryDesc *queryDesc)+{+ /* Call the next hook or standard function */+ if (next_ExecutorEnd_hook)+ next_ExecutorEnd_hook(queryDesc);+ else+ standard_ExecutorEnd(queryDesc);++ /* Pop the audit event off the stack */+ if (!internalStatement)+ {+ stack_pop(auditEventStack->stackId);+ }+}++/*+ * 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;++ /* Allocate the audit event */+ if (!IsAbortedTransactionBlockState())+ {+ /* Process top level utility statement */+ if (context == PROCESS_UTILITY_TOPLEVEL)+ {+ if (auditEventStack != NULL)+ elog(ERROR, "pg_audit stack is not empty");++ /* Set params */+ 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.objectName = "";+ stackItem->auditEvent.objectType = "";+ stackItem->auditEvent.commandText = queryString;++ /*+ * If this is a DO block log it before calling the next ProcessUtility+ * hook.+ */+ if (auditLogBitmap != 0 &&+ 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. */+ if (stackItem != NULL)+ {+ /* 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 &&+ !IsAbortedTransactionBlockState())+ log_audit_event(stackItem);++ stack_pop(stackId);+ }+}++/*+ * 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+pg_audit_object_access_hook(ObjectAccessType access,+ Oid classId,+ Oid objectId,+ int subId,+ void *arg)+{+ if (auditLogBitmap != 0 && !IsAbortedTransactionBlockState() &&+ auditLogBitmap & (LOG_DDL | LOG_FUNCTION))+ log_object_access(access, classId, objectId, subId, arg);++ 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.+ */+Datum+pg_audit_ddl_command_end(PG_FUNCTION_ARGS)+{+ /* Continue only if session logging is enabled */+ if (auditLogBitmap != LOG_DDL)+ {+ EventTriggerData *eventData;+ int result, row;+ TupleDesc spiTupDesc;+ const char *query;+ MemoryContext contextQuery;+ MemoryContext contextOld;++ /* 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 */+ 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;++ /* Return objects affected by the (non drop) DDL statement */+ query = "SELECT classid, objid, objsubid, UPPER(object_type), schema,\n"+ " identity, command\n"+ " FROM pg_event_trigger_get_creation_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;+ bool isNull;++ spiTuple = SPI_tuptable->vals[row];++ /* Supply addition data to current audit event */+ auditEventStack->auditEvent.logStmtLevel =+ GetCommandLogLevel(eventData->parsetree);+ auditEventStack->auditEvent.commandTag =+ nodeTag(eventData->parsetree);+ auditEventStack->auditEvent.command =+ CreateCommandTag(eventData->parsetree);+ auditEventStack->auditEvent.objectName =+ SPI_getvalue(spiTuple, spiTupDesc, 6);+ auditEventStack->auditEvent.objectType =+ SPI_getvalue(spiTuple, spiTupDesc, 4);+ auditEventStack->auditEvent.commandText =+ TextDatumGetCString(+ DirectFunctionCall1(pg_event_trigger_expand_command,+ SPI_getbinval(spiTuple, spiTupDesc,+ 7, &isNull)));++ /* Log the audit event */+ log_audit_event(auditEventStack);+ }++ /* Complete the query */+ SPI_finish();++ /* Switch to the old memory context */+ 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)+{+ if (auditLogBitmap & LOG_DDL)+ {+ int result, row;+ TupleDesc spiTupDesc;+ const char *query;+ MemoryContext contextQuery;+ MemoryContext contextOld;++ /* 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 */+ 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 classid, objid, objsubid, UPPER(object_type),\n"+ " schema_name, object_name, object_identity\n"+ " FROM pg_event_trigger_dropped_objects()";++ /* 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;+ char *schemaName;++ spiTuple = SPI_tuptable->vals[row];++ auditEventStack->auditEvent.objectType =+ SPI_getvalue(spiTuple, spiTupDesc, 4);+ schemaName = SPI_getvalue(spiTuple, spiTupDesc, 5);++ if (!(pg_strcasecmp(auditEventStack->auditEvent.objectType,+ "TYPE") == 0 ||+ pg_strcasecmp(schemaName, "pg_toast") == 0))+ {+ auditEventStack->auditEvent.objectName =+ SPI_getvalue(spiTuple, spiTupDesc, 7);++ log_audit_event(auditEventStack);+ }+ }++ /* Complete the query */+ SPI_finish();++ /* Switch to the old memory context */+ 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 *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_PARAMETER) == 0)+ class = LOG_PARAMETER;+ 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_pg_audit_log.+ */+ *extra = f;++ 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 = *(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_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_pg_audit_log,+ assign_pg_audit_log,+ NULL);++ /*+ * Install our hook functions after saving the existing pointers to preserve+ * the chain.+ */+ next_ExecutorStart_hook = ExecutorStart_hook;+ ExecutorStart_hook = pg_audit_ExecutorStart_hook;++ next_ExecutorCheckPerms_hook = ExecutorCheckPerms_hook;+ ExecutorCheckPerms_hook = pg_audit_ExecutorCheckPerms_hook;++ next_ExecutorEnd_hook = ExecutorEnd_hook;+ ExecutorEnd_hook = pg_audit_ExecutorEnd_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.control b/contrib/pg_audit/pg_audit.controlnew file mode 100644index 0000000..6730c68--- /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 = truediff --git a/contrib/pg_audit/test/test.pl b/contrib/pg_audit/test/test.plnew file mode 100755index 0000000..b3f2443--- /dev/null+++ b/contrib/pg_audit/test/test.pl@@ -0,0 +1,1433 @@+#!/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_PARAMETER => 'PARAMETER',+ 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_COLUMN => 'ALTER TABLE COLUMN',+ 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_CONSTRAINT => 'DROP TABLE CONSTRAINT',+ COMMAND_DROP_TABLE_INDEX => 'DROP TABLE INDEX',+ COMMAND_DROP_TABLE_TOAST => 'DROP TABLE TOAST',+ 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_PARAMETER => 'PARAMETER',+ # COMMAND_PARAMETER_READ => 'PARAMETER_READ',+ # COMMAND_PARAMETER_WRITE => 'PARAMETER_WRITE',+ 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_AGGREGATE => 'AGGREGATE',+ TYPE_COLLATION => 'COLLATION',+ TYPE_CONVERSION => 'CONVERSION',+ TYPE_SCHEMA => 'SCHEMA',+ TYPE_FUNCTION => 'FUNCTION',+ TYPE_INDEX => 'INDEX',+ TYPE_TABLE => 'TABLE',+ TYPE_TABLE_COLUMN => 'TABLE COLUMN',+ TYPE_TABLE_CONSTRAINT => 'TABLE CONSTRAINT',+ TYPE_TABLE_TOAST => 'TABLE TOAST',+ 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_AGGREGATE},+ &COMMAND_ALTER_DATABASE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},+ &COMMAND_ALTER_COLLATION => {&CLASS => &CLASS_DDL,+ &TYPE => &TYPE_COLLATION},+ &COMMAND_ALTER_CONVERSION => {&CLASS => &CLASS_DDL,+ &TYPE => &TYPE_CONVERSION},+ &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_COLUMN => {&CLASS => &CLASS_DDL,+ &TYPE => &TYPE_TABLE_COLUMN, &COMMAND => &COMMAND_ALTER_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_AGGREGATE},+ &COMMAND_CREATE_CONVERSION => {&CLASS => &CLASS_DDL,+ &TYPE => &TYPE_CONVERSION},+ &COMMAND_CREATE_COLLATION => {&CLASS => &CLASS_DDL,+ &TYPE => &TYPE_COLLATION},+ &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_FUNCTION},+ &COMMAND_CREATE_ROLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},+ &COMMAND_CREATE_SCHEMA => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_SCHEMA},+ &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_CONSTRAINT => {&CLASS => &CLASS_DDL,+ &TYPE => &TYPE_TABLE_CONSTRAINT, &COMMAND => &COMMAND_DROP_TABLE},+ &COMMAND_DROP_TABLE_INDEX => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_INDEX,+ &COMMAND => &COMMAND_DROP_TABLE},+ &COMMAND_DROP_TABLE_TOAST => {&CLASS => &CLASS_DDL,+ &TYPE => &TYPE_TABLE_TOAST, &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_TABLE},+ &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_TABLE},+ &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;+ my $strParameter = shift;+ my $bExpectError = shift;++ # Set defaults+ $bExecute = defined($bExecute) ? $bExecute : true;+ $bWait = defined($bWait) ? $bWait : true;+ $bLogSql = defined($bLogSql) ? $bLogSql : true;++ if ($bExecute)+ {+ eval+ {+ PgExecuteOnly($strSql);+ };++ if ($@ && !$bExpectError)+ {+ confess $@;+ }+ }++ PgLogExpect($strCommand, $bLogSql ? $strSql : '', $strParameter, $oData);++ if ($bWait)+ {+ PgLogWait();+ }+}++################################################################################+# QuoteCSV+################################################################################+sub QuoteCSV+{+ my $strCSV = shift;++ if (defined($strCSV) &&+ (index($strCSV, ',') >= 0 || index($strCSV, '"') > 0 ||+ index($strCSV, "\n") > 0 || index($strCSV, "\r") >= 0))+ {+ $strCSV =~ s/"/""/g;+ $strCSV = "\"${strCSV}\"";+ }++ return $strCSV;+}++################################################################################+# PgLogExpect+################################################################################+sub PgLogExpect+{+ my $strCommand = shift;+ my $strSql = shift;+ my $strParameter = shift;+ my $oData = shift;++ # If oData is false then no logging+ if (defined($oData) && ref($oData) eq '' && !$oData)+ {+ return;+ }++ # Quote SQL if needs to be quoted+ $strSql = QuoteCSV($strSql);++ if (defined($strParameter))+ {+ $strSql .= ",${strParameter}";+ }++ # 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 = QuoteCSV($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 = QuoteCSV(${$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;+ $strLogActual =~ s/SESSION,[0-9]+,[0-9]+,/SESSION,/g;+ $strLogActual =~ s/OBJECT,[0-9]+,[0-9]+,/OBJECT,/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_statement=all" .+ (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 " .+ (defined($strColumn) ?+ lc(${strPrivilege}) ." (${strColumn})" :+ uc(${strPrivilege})) .+ " ON TABLE ${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 " . uc(${strPrivilege}) .+ (defined($strColumn) ? " (${strColumn})" : '') .+ " ON TABLE ${strObject} FROM ${strRole} ");++ delete($oAuditGrantHash{$strRole}{$strObject}{$strPrivilege});+}++################################################################################+# Main+################################################################################+my @oyTable; # Store table info for select, insert, update, delete+my $strSql; # Hold Sql commands++# 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');++$strSql = 'CREATE TABLE public.test (id pg_catalog.int4 )' .+ ' WITH (oids=OFF) ';+PgLogExecute(COMMAND_CREATE_TABLE, $strSql, 'public.test');+PgLogExecute(COMMAND_SELECT, 'select * from test');++$strSql = 'drop table test';+PgLogExecute(COMMAND_DROP_TABLE, $strSql, '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();++# 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));++PgSetUser('user1');++$strSql = 'CREATE TABLE public.account (id pg_catalog.int4 ,' .+ ' name pg_catalog.text COLLATE pg_catalog."default", ' .+ 'password pg_catalog.text COLLATE pg_catalog."default", '.+ 'description pg_catalog.text COLLATE pg_catalog."default") '.+ 'WITH (oids=OFF) ';+PgLogExecute(COMMAND_CREATE_TABLE, $strSql, '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>");++# 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; \$\$;");++$strSql = 'CREATE SCHEMA test ';+PgLogExecute(COMMAND_CREATE_SCHEMA, $strSql, 'test');++# Test COPY+PgLogExecute(COMMAND_COPY_TO,+ "COPY pg_class to '" . abs_path($strTestPath) . "/class.out'");++$strSql = 'CREATE TABLE test.pg_class WITH (oids=OFF) AS SELECT relname,' .+ ' relnamespace, reltype, reloftype, relowner, relam, relfilenode, ' .+ 'reltablespace, relpages, reltuples, relallvisible, reltoastrelid, ' .+ 'relhasindex, relisshared, relpersistence, relkind, relnatts, ' .+ 'relchecks, relhasoids, relhaspkey, relhasrules, relhastriggers, ' .+ 'relhassubclass, relrowsecurity, relispopulated, relreplident, ' .+ 'relfrozenxid, relminmxid, relacl, reloptions ' .+ 'FROM pg_catalog.pg_class ';+PgLogExecute(COMMAND_INSERT, $strSql, undef, true, false);+PgLogExecute(COMMAND_CREATE_TABLE_AS, $strSql, 'test.pg_class', false, true);++$strSql = "COPY test.pg_class from '" . abs_path($strTestPath) . "/class.out'";+PgLogExecute(COMMAND_INSERT, $strSql);+#PgLogExecute(COMMAND_COPY_FROM, $strSql, 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+$strSql = 'CREATE TABLE test.test_insert (id pg_catalog.int4 ) ' .+ 'WITH (oids=OFF) ';+PgLogExecute(COMMAND_CREATE_TABLE, $strSql, 'test.test_insert');++$strSql = 'PREPARE pgclassstmt (oid) as insert into test.test_insert (id) ' .+ 'values ($1)';+PgLogExecute(COMMAND_PREPARE_WRITE, $strSql);+PgLogExecute(COMMAND_INSERT, $strSql, undef, false, false, undef, "1");++$strSql = 'EXECUTE pgclassstmt (1)';+PgLogExecute(COMMAND_EXECUTE_WRITE, $strSql, undef, true, true);++# Create a table with a primary key+$strSql = 'CREATE TABLE public.test (id pg_catalog.int4 , ' .+ 'name pg_catalog.text COLLATE pg_catalog."default", description ' .+ 'pg_catalog.text COLLATE pg_catalog."default", CONSTRAINT ' .+ 'test_pkey PRIMARY KEY (id)) WITH (oids=OFF) ';+PgLogExecute(COMMAND_CREATE_INDEX, $strSql, 'public.test_pkey', true, false);+PgLogExecute(COMMAND_CREATE_TABLE, $strSql, 'public.test', false, true);++PgLogExecute(COMMAND_ANALYZE, 'analyze test');++# Grant select to public - this should have no affect on auditing+$strSql = 'GRANT SELECT ON TABLE public.test TO PUBLIC ';+PgLogExecute(COMMAND_GRANT, $strSql);++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');++$strSql = 'GRANT select (name) ON TABLE public.test TO PUBLIC ';+PgLogExecute(COMMAND_GRANT, $strSql);++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');++# Now try the same in a do block+$strSql = 'do $$ declare test int; begin select 1 into test; end $$';+PgLogExecute(COMMAND_DO, $strSql, undef, true, false);++$strSql = 'select 1';+PgLogExecute(COMMAND_SELECT, $strSql, undef, false, true);++# Insert some data into test and try a loop in a do block+PgLogExecute(COMMAND_INSERT, 'insert into test (id) values (1)');+PgLogExecute(COMMAND_INSERT, 'insert into test (id) values (2)');+PgLogExecute(COMMAND_INSERT, 'insert into test (id) values (3)');++$strSql = 'do $$ ' .+ 'declare ' .+ ' result record;' .+ 'begin ' .+ ' for result in select id from test loop ' .+ ' insert into test (id) values (result.id + 100); ' .+ ' end loop; ' .+ 'end; $$';++PgLogExecute(COMMAND_DO, $strSql, undef, true, false);++$strSql = 'select id from test';+PgLogExecute(COMMAND_SELECT, $strSql, undef, false, false);++$strSql = 'insert into test (id) values (result.id + 100)';+PgLogExecute(COMMAND_INSERT, $strSql, undef, false, false, undef, ",,");++PgLogExecute(COMMAND_INSERT, $strSql, undef, false, false, undef, ",,");++PgLogExecute(COMMAND_INSERT, $strSql, undef, false, false, undef, ",,");++# Test EXECUTE with bind+$strSql = "select * from test where id = ?";+my $hStatement = $hDb->prepare($strSql);++$strSql = "select * from test where id = \$1";+$hStatement->bind_param(1, 101);+$hStatement->execute();++PgLogExecute(COMMAND_SELECT, $strSql, undef, false, false, undef, "101");++$hStatement->bind_param(1, 103);+$hStatement->execute();++PgLogExecute(COMMAND_SELECT, $strSql, undef, false, false, undef, "103");++$hStatement->finish();++# Cursors in a function block+$strSql = "CREATE FUNCTION public.test() RETURNS pg_catalog.int4 LANGUAGE " .+ "plpgsql VOLATILE CALLED ON NULL INPUT SECURITY INVOKER COST 100 " .+ " AS ' declare cur1 cursor for select * from hoge; tmp int; begin " .+ "create table hoge (id int); open cur1; fetch cur1 into tmp; close " .+ "cur1; return tmp; end'";++PgLogExecute(COMMAND_CREATE_FUNCTION, $strSql, 'public.test()');++$strSql = 'select public.test()';+PgLogExecute(COMMAND_SELECT, $strSql, undef, true, false);+PgLogExecute(COMMAND_EXECUTE_FUNCTION, $strSql, 'public.test', false, false);++$strSql = 'CREATE TABLE public.hoge (id pg_catalog.int4 )' .+ ' WITH (oids=OFF) ';+PgLogExecute(COMMAND_CREATE_TABLE, $strSql, 'public.hoge', false, false);++$strSql = 'select * from hoge';+PgLogExecute(COMMAND_SELECT, $strSql, undef, false, true);+#PgLogExecute(COMMAND_SELECT, 'select public.test()');++# Now try some DDL in a do block+$strSql = 'do $$ ' .+ 'begin ' .+ ' create table test_block (id int); ' .+ ' drop table test_block; ' .+ 'end; $$';++PgLogExecute(COMMAND_DO, $strSql, undef, true, false);++$strSql = 'CREATE TABLE public.test_block (id pg_catalog.int4 ) ' .+ 'WITH (oids=OFF) ';+PgLogExecute(COMMAND_CREATE_TABLE, $strSql, 'public.test_block', false, false);++$strSql = 'drop table test_block';+PgLogExecute(COMMAND_DROP_TABLE, $strSql, 'public.test_block', false, false);++# Generate an error in a do block and make sure the stack gets cleaned up+$strSql = 'do $$ ' .+ 'begin ' .+ ' create table bobus.test_block (id int); ' .+ 'end; $$';++PgLogExecute(COMMAND_DO, $strSql, undef, undef, undef, undef, undef, true);+# PgLogExecute(COMMAND_SELECT, 'select 1');+# exit 0;++# Try explain+PgLogExecute(COMMAND_SELECT, 'explain select 1', undef, true, false);+PgLogExecute(COMMAND_EXPLAIN, 'explain select 1', undef, false, true);++# 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);++# Test alter and drop table statements+$strSql = 'ALTER TABLE public.test DROP COLUMN description ';+PgLogExecute(COMMAND_ALTER_TABLE_COLUMN,+ $strSql, 'public.test.description', true, false);+PgLogExecute(COMMAND_ALTER_TABLE,+ $strSql, 'public.test', false, true);+@oyTable = ({&NAME => 'public.test', &TYPE => &TYPE_TABLE,+ &COMMAND => &COMMAND_SELECT});+PgLogExecute(COMMAND_SELECT, 'select from test', \@oyTable);++$strSql = 'ALTER TABLE public.test RENAME TO test2';+PgLogExecute(COMMAND_ALTER_TABLE, $strSql, 'public.test2');++$strSql = 'ALTER TABLE public.test2 SET SCHEMA test';+PgLogExecute(COMMAND_ALTER_TABLE, $strSql, 'test.test2');++$strSql = 'ALTER TABLE test.test2 ADD COLUMN description pg_catalog.text ' .+ 'COLLATE pg_catalog."default"';+PgLogExecute(COMMAND_ALTER_TABLE, $strSql, 'test.test2');++$strSql = 'ALTER TABLE test.test2 DROP COLUMN description ';+PgLogExecute(COMMAND_ALTER_TABLE_COLUMN, $strSql,+ 'test.test2.description', true, false);+PgLogExecute(COMMAND_ALTER_TABLE, $strSql,+ 'test.test2', false, true);++$strSql = 'drop table test.test2';+PgLogExecute(COMMAND_DROP_TABLE, $strSql, 'test.test2', true, false);+PgLogExecute(COMMAND_DROP_TABLE_CONSTRAINT, $strSql, 'test_pkey on test.test2',+ false, false);+PgLogExecute(COMMAND_DROP_TABLE_INDEX, $strSql, 'test.test_pkey', false, true);++$strSql = "CREATE FUNCTION public.int_add(IN a pg_catalog.int4 , IN b " .+ "pg_catalog.int4 ) RETURNS pg_catalog.int4 LANGUAGE plpgsql " .+ "VOLATILE CALLED ON NULL INPUT SECURITY INVOKER COST 100 AS '" .+ " begin return a + b; end '";+PgLogExecute(COMMAND_CREATE_FUNCTION, $strSql,+ 'public.int_add(integer,integer)');+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);++$strSql = "CREATE AGGREGATE public.sum_test( pg_catalog.int4) " .+ "(SFUNC=public.int_add, STYPE=pg_catalog.int4, INITCOND='0')";+PgLogExecute(COMMAND_CREATE_AGGREGATE, $strSql, 'public.sum_test(integer)');++# There's a bug here in deparse:+$strSql = "ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2";+PgLogExecute(COMMAND_ALTER_AGGREGATE, $strSql, 'public.sum_test2(integer)');++$strSql = "CREATE COLLATION public.collation_test (LC_COLLATE = 'de_DE', " .+ "LC_CTYPE = 'de_DE')";+PgLogExecute(COMMAND_CREATE_COLLATION, $strSql, 'public.collation_test');++$strSql = "ALTER COLLATION public.collation_test RENAME TO collation_test2";+PgLogExecute(COMMAND_ALTER_COLLATION, $strSql, 'public.collation_test2');++$strSql = "CREATE CONVERSION public.conversion_test FOR 'SQL_ASCII' " .+ "TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic";+PgLogExecute(COMMAND_CREATE_CONVERSION, $strSql, 'public.conversion_test');++$strSql = "ALTER CONVERSION public.conversion_test RENAME TO conversion_test2";+PgLogExecute(COMMAND_ALTER_CONVERSION, $strSql, 'public.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 89fff77..6b0b407 100644--- a/doc/src/sgml/filelist.sgml+++ b/doc/src/sgml/filelist.sgml@@ -125,6 +125,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..4588619--- /dev/null+++ b/doc/src/sgml/pgaudit.sgml@@ -0,0 +1,347 @@+<!-- 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 six 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>PARAMETER</literal> - Parameters that were passed for the statement. Parameters immediately follow the statement text.+ </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 compliant CSV format only if the log line prefix portion+ of each log entry is removed.+ </para>+ </note>++ <itemizedlist>+ <listitem>+ <para>+ <literal>AUDIT_TYPE</literal> - <literal>SESSION</literal> or+ <literal>OBJECT</literal>.+ </para>+ </listitem>+ <listitem>+ <para>+ <literal>STATEMENT_ID</literal> - Unique statement ID for this+ session. Each statement ID represents a backend call.+ </para>+ </listitem>+ <listitem>+ <para>+ <literal>SUBSTATEMENT_ID</literal> - Sequential ID for each+ substatement within the main statement. For example, calling+ a function from a query.+ </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, sawada.mshk@gmail.com, alvherre@2ndquadrant.com, ams@2ndquadrant.com, sfrost@snowman.net, masao.fujii@gmail.com
Subject: Re: Auditing extension for PostgreSQL (Take 2)
In-Reply-To: <551C2F0B.3010305@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