diff --git a/contrib/Makefile b/contrib/Makefile index 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/Makefile new file mode 100644 index 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 +endif diff --git a/contrib/pg_audit/pg_audit--1.0.0.sql b/contrib/pg_audit/pg_audit--1.0.0.sql new file mode 100644 index 0000000..2eee3b9 --- /dev/null +++ b/contrib/pg_audit/pg_audit--1.0.0.sql @@ -0,0 +1,4 @@ +/* pg_audit/pg_audit--1.0.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION pg_audit" to load this file.\quit diff --git a/contrib/pg_audit/pg_audit.c b/contrib/pg_audit/pg_audit.c new file mode 100644 index 0000000..b3914ac --- /dev/null +++ b/contrib/pg_audit/pg_audit.c @@ -0,0 +1,1099 @@ +/*------------------------------------------------------------------------------ + * 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. + * + * Copyright (c) 2014-2015, PostgreSQL Global Development Group + * + * IDENTIFICATION + * contrib/pg_prewarm/pg_prewarm.c + *------------------------------------------------------------------------------ + */ +#include "postgres.h" + +#include "access/htup_details.h" +#include "access/sysattr.h" +#include "access/xact.h" +#include "catalog/catalog.h" +#include "catalog/objectaccess.h" +#include "catalog/pg_class.h" +#include "catalog/namespace.h" +#include "commands/dbcommands.h" +#include "catalog/pg_proc.h" +#include "commands/event_trigger.h" +#include "executor/executor.h" +#include "executor/spi.h" +#include "miscadmin.h" +#include "libpq/auth.h" +#include "nodes/nodes.h" +#include "tcop/utility.h" +#include "utils/acl.h" +#include "utils/builtins.h" +#include "utils/guc.h" +#include "utils/lsyscache.h" +#include "utils/memutils.h" +#include "utils/rel.h" +#include "utils/syscache.h" +#include "utils/timestamp.h" + +PG_MODULE_MAGIC; + +void _PG_init(void); + +/* + * auditRole is the string value of the pgaudit.role GUC, which contains the + * role for grant-based auditing. + */ +char *auditRole = NULL; + +/* + * auditLog is the string value of the pgaudit.log GUC, e.g. "read, write, ddl" + * (it's not used by the module but is required by DefineCustomStringVariable). + * Each token corresponds to a flag in enum LogClass below. We convert the list + * of tokens into a bitmap in auditLogBitmap for internal use. + */ +char *auditLog = NULL; +static uint64 auditLogBitmap = 0; + +/* + * String contants for audit types - used when logging to distinguish session + * vs. object auditing. + */ +#define AUDIT_TYPE_OBJECT "OBJECT" +#define AUDIT_TYPE_SESSION "SESSION" + +/* + * String contants for log classes - used when processing tokens in the + * pgaudit.log GUC. + */ +#define CLASS_DDL "DDL" +#define CLASS_FUNCTION "FUNCTION" +#define CLASS_MISC "MISC" +#define CLASS_READ "READ" +#define CLASS_WRITE "WRITE" + +#define CLASS_ALL "ALL" +#define CLASS_NONE "NONE" + +/* Log class enum used to represent bits in auditLogBitmap */ +enum LogClass +{ + LOG_NONE = 0, + + /* SELECT */ + LOG_READ = (1 << 0), + + /* INSERT, UPDATE, DELETE, TRUNCATE */ + LOG_WRITE = (1 << 1), + + /* DDL: CREATE/DROP/ALTER */ + LOG_DDL = (1 << 2), + + /* Function execution */ + LOG_FUNCTION = (1 << 4), + + /* Function execution */ + LOG_MISC = (1 << 5), + + /* Absolutely everything */ + LOG_ALL = ~(uint64)0 +}; + +/* String contants 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" + +/* + * This module collects AuditEvents from various sources (event triggers, and + * executor/utility hooks) and passes them to the log_audit_event() function. + * + * An AuditEvent represents an operation that potentially affects a single + * object. If an underlying command affects multiple objects multiple + * AuditEvents must be created to represent it. + */ +typedef struct +{ + LogStmtLevel logStmtLevel; + NodeTag commandTag; + const char *command; + const char *objectType; + char *objectName; + const char *commandText; + bool granted; +} AuditEvent; + +/* + * Set if a function below log_utility_command() has logged the event - prevents + * more than one function from logging when the event could be logged in + * multiple places. + */ +bool utilityCommandLogged = false; +AuditEvent utilityAuditEvent; + +/* + * Returns the oid of the role specified in pgaudit.role. + */ +static Oid +audit_role_oid() +{ + HeapTuple roleTup; + Oid oid = InvalidOid; + + roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(auditRole)); + + if (HeapTupleIsValid(roleTup)) + { + oid = HeapTupleGetOid(roleTup); + ReleaseSysCache(roleTup); + } + + return oid; +} + +/* + * Takes an AuditEvent and returns true or false depending on whether the event + * should be logged according to the pgaudit.roles/log settings. If it returns + * true, also fills in the name of the LogClass which it is logged under. + */ +static bool +log_check(AuditEvent *e, const char **classname) +{ + enum LogClass class = LOG_NONE; + + /* By default put everything in the MISC class. */ + *classname = CLASS_MISC; + class = LOG_MISC; + + /* + * Look at the type of the command and decide what LogClass needs to be + * enabled for the command to be logged. + */ + switch (e->logStmtLevel) + { + case LOGSTMT_MOD: + *classname = CLASS_WRITE; + class = LOG_WRITE; + break; + + case LOGSTMT_DDL: + *classname = CLASS_DDL; + class = LOG_DDL; + + case LOGSTMT_ALL: + switch (e->commandTag) + { + case T_CopyStmt: + case T_SelectStmt: + case T_PrepareStmt: + case T_PlannedStmt: + case T_ExecuteStmt: + *classname = CLASS_READ; + class = LOG_READ; + break; + + case T_VacuumStmt: + case T_ReindexStmt: + *classname = CLASS_DDL; + class = LOG_DDL; + break; + + case T_DoStmt: + *classname = CLASS_FUNCTION; + class = LOG_FUNCTION; + break; + + default: + break; + } + break; + + case LOGSTMT_NONE: + break; + } + + /* + * We log audit events under the following conditions: + * + * 1. If the audit role has been explicitly granted permission for + * an operation. + */ + if (e->granted) + { + return true; + } + + /* 2. If the event belongs to a class covered by pgaudit.log. */ + if ((auditLogBitmap & class) == class) + { + return true; + } + + return false; +} + +/* + * Takes an AuditEvent and, if it log_check(), writes it to the audit log. The + * AuditEvent is assumed to be completely filled in by the caller (unknown + * values must be set to "" so that they can be logged without error checking). + */ +static void +log_audit_event(AuditEvent *e) +{ + const char *classname; + + /* Check that this event should be logged. */ + if (!log_check(e, &classname)) + return; + + /* Log via ereport(). */ + ereport(LOG, + (errmsg("AUDIT: %s,%s,%s,%s,%s,%s", + e->granted ? AUDIT_TYPE_OBJECT : AUDIT_TYPE_SESSION, + classname, e->command, e->objectType, e->objectName, + e->commandText), + errhidestmt(true))); +} + +/* + * Check if the role or any inherited role has any permission in the mask. The + * public role is excluded from this check and superuser permissions are not + * considered. + */ +static bool +log_acl_check(Datum aclDatum, Oid auditOid, AclMode mask) +{ + bool result = false; + Acl *acl; + AclItem *aclItemData; + int aclIndex; + int aclTotal; + + /* Detoast column's ACL if necessary */ + acl = DatumGetAclP(aclDatum); + + /* Get the acl list and total */ + aclTotal = ACL_NUM(acl); + aclItemData = ACL_DAT(acl); + + /* Check privileges granted directly to auditOid */ + for (aclIndex = 0; aclIndex < aclTotal; aclIndex++) + { + AclItem *aclItem = &aclItemData[aclIndex]; + + if (aclItem->ai_grantee == auditOid && + aclItem->ai_privs & mask) + { + result = true; + break; + } + } + + /* + * Check privileges granted indirectly via role memberships. We do this in + * a separate pass to minimize expensive indirect membership tests. In + * particular, it's worth testing whether a given ACL entry grants any + * privileges still of interest before we perform the has_privs_of_role + * test. + */ + if (!result) + { + for (aclIndex = 0; aclIndex < aclTotal; aclIndex++) + { + AclItem *aclItem = &aclItemData[aclIndex]; + + /* Don't test public or auditOid (it has been tested already) */ + if (aclItem->ai_grantee == ACL_ID_PUBLIC || + aclItem->ai_grantee == auditOid) + continue; + + /* + * Check that the role has the required privileges and that it is + * inherited by auditOid. + */ + if (aclItem->ai_privs & mask && + has_privs_of_role(auditOid, aclItem->ai_grantee)) + { + result = true; + break; + } + } + } + + /* if we have a detoasted copy, free it */ + if (acl && (Pointer) acl != DatumGetPointer(aclDatum)) + pfree(acl); + + return result; +} + +/* + * Check if a role has any of the permissions in the mask on a relation. + */ +static bool +log_relation_check(Oid relOid, + Oid auditOid, + AclMode mask) +{ + bool result = false; + HeapTuple tuple; + Datum aclDatum; + bool isNull; + + /* Get relation tuple from pg_class */ + tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid)); + + /* Return false if tuple is not valid */ + if (!HeapTupleIsValid(tuple)) + return false; + + /* Get the relation's ACL */ + aclDatum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_relacl, + &isNull); + + /* If not null then test */ + if (!isNull) + result = log_acl_check(aclDatum, auditOid, mask); + + /* Free the relation tuple */ + ReleaseSysCache(tuple); + + return result; +} + +/* + * Check if a role has any of the permissions in the mask on an attribute. + */ +static bool +log_attribute_check(Oid relOid, + AttrNumber attNum, + Oid auditOid, + AclMode mask) +{ + bool result = false; + HeapTuple attTuple; + Datum aclDatum; + bool isNull; + + /* Get the attribute's ACL */ + attTuple = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(relOid), + Int16GetDatum(attNum)); + + /* Return false if attribute is invalid */ + if (!HeapTupleIsValid(attTuple)) + return false; + + /* Only process attribute that have not been dropped */ + if (!((Form_pg_attribute) GETSTRUCT(attTuple))->attisdropped) + { + aclDatum = SysCacheGetAttr(ATTNUM, attTuple, Anum_pg_attribute_attacl, + &isNull); + + if (!isNull) + result = log_acl_check(aclDatum, auditOid, mask); + } + + /* Free attribute */ + ReleaseSysCache(attTuple); + + return result; +} + +/* + * Check if a role has any of the permissions in the mask on an attribute in + * the provided set. If the set is empty, then all valid attributes in the + * relation will be tested. + */ +static bool +log_attribute_check_any(Oid relOid, + Oid auditOid, + Bitmapset *attributeSet, + AclMode mode) +{ + bool result = false; + AttrNumber col; + Bitmapset *tmpSet; + + /* If bms is empty then check for any column match */ + if (bms_is_empty(attributeSet)) + { + HeapTuple classTuple; + AttrNumber nattrs; + AttrNumber curr_att; + + /* Get relation to determine total attribute */ + classTuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid)); + + if (!HeapTupleIsValid(classTuple)) + return false; + + nattrs = ((Form_pg_class) GETSTRUCT(classTuple))->relnatts; + ReleaseSysCache(classTuple); + + /* Check each column */ + for (curr_att = 1; curr_att <= nattrs; curr_att++) + { + if (log_attribute_check(relOid, curr_att, auditOid, mode)) + return true; + } + } + + /* Make a copy of the column set */ + tmpSet = bms_copy(attributeSet); + + /* Check each column */ + while ((col = bms_first_member(tmpSet)) >= 0) + { + col += FirstLowInvalidHeapAttributeNumber; + + if (col != InvalidAttrNumber && + log_attribute_check(relOid, col, auditOid, mode)) + { + result = true; + break; + } + } + + /* Free the column set */ + bms_free(tmpSet); + + return result; +} + +/* + * Create AuditEvents for DML operations via executor permissions checks. + */ +static void +log_dml(Oid auditOid, List *rangeTabls) +{ + ListCell *lr; + bool first = true; + + foreach(lr, rangeTabls) + { + Oid relOid; + Relation rel; + RangeTblEntry *rte = lfirst(lr); + AuditEvent auditEvent; + + /* We only care about tables, and can ignore subqueries etc. */ + if (rte->rtekind != RTE_RELATION) + continue; + + /* + * Filter out any system relations + */ + relOid = rte->relid; + rel = relation_open(relOid, NoLock); + + if (IsSystemNamespace(RelationGetNamespace(rel))) + { + relation_close(rel, NoLock); + return; + } + + /* + * We don't have access to the parsetree here, so we have to generate + * the node type, object type, and command tag by decoding + * rte->requiredPerms and rte->relkind. + */ + auditEvent.logStmtLevel = LOGSTMT_MOD; + + if (rte->requiredPerms & ACL_INSERT) + { + auditEvent.commandTag = T_InsertStmt; + auditEvent.command = COMMAND_INSERT; + } + else if (rte->requiredPerms & ACL_UPDATE) + { + auditEvent.commandTag = T_UpdateStmt; + auditEvent.command = COMMAND_UPDATE; + } + else if (rte->requiredPerms & ACL_DELETE) + { + auditEvent.commandTag = T_DeleteStmt; + auditEvent.command = COMMAND_DELETE; + } + else if (rte->requiredPerms & ACL_SELECT) + { + auditEvent.logStmtLevel = LOGSTMT_ALL; + auditEvent.commandTag = T_SelectStmt; + auditEvent.command = COMMAND_SELECT; + } + else + { + auditEvent.commandTag = T_Invalid; + auditEvent.command = COMMAND_UNKNOWN; + } + + /* + * Fill values in the event struct that are required for session + * logging. + */ + auditEvent.granted = false; + auditEvent.commandText = debug_query_string; + + /* If this is the first rte then session log */ + if (first) + { + auditEvent.objectName = ""; + auditEvent.objectType = ""; + + log_audit_event(&auditEvent); + + first = false; + } + + /* Get the relation type */ + switch (rte->relkind) + { + case RELKIND_RELATION: + auditEvent.objectType = OBJECT_TYPE_TABLE; + break; + + case RELKIND_INDEX: + auditEvent.objectType = OBJECT_TYPE_INDEX; + break; + + case RELKIND_SEQUENCE: + auditEvent.objectType = OBJECT_TYPE_SEQUENCE; + break; + + case RELKIND_TOASTVALUE: + auditEvent.objectType = OBJECT_TYPE_TOASTVALUE; + break; + + case RELKIND_VIEW: + auditEvent.objectType = OBJECT_TYPE_VIEW; + break; + + case RELKIND_COMPOSITE_TYPE: + auditEvent.objectType = OBJECT_TYPE_COMPOSITE_TYPE; + break; + + case RELKIND_FOREIGN_TABLE: + auditEvent.objectType = OBJECT_TYPE_FOREIGN_TABLE; + break; + + case RELKIND_MATVIEW: + auditEvent.objectType = OBJECT_TYPE_MATVIEW; + break; + + default: + auditEvent.objectType = OBJECT_TYPE_UNKNOWN; + break; + } + + /* Get the relation name */ + auditEvent.objectName = + quote_qualified_identifier(get_namespace_name( + RelationGetNamespace(rel)), + RelationGetRelationName(rel)); + relation_close(rel, NoLock); + + + /* Perform object auditing only if the audit role is valid */ + if (auditOid != InvalidOid) + { + AclMode auditPerms = (ACL_SELECT | ACL_UPDATE | ACL_INSERT) & + rte->requiredPerms; + + /* + * If any of the required permissions for the relation are granted + * to the audit role then audit the relation + */ + if (log_relation_check(relOid, auditOid, auditPerms)) + { + auditEvent.granted = true; + } + + /* + * Else check if the audit role has column-level permissions for + * select, insert, or update. + */ + else if (auditPerms != 0) + { + /* + * Check the select columns to see if the audit role has + * priveleges on any of them. + */ + if (auditPerms & ACL_SELECT) + { + auditEvent.granted = + log_attribute_check_any(relOid, auditOid, + rte->selectedCols, + ACL_SELECT); + } + + /* + * Check the modified columns to see if the audit role has + * privileges on any of them. + */ + if (!auditEvent.granted) + { + auditPerms &= (ACL_INSERT | ACL_UPDATE); + + if (auditPerms) + { + auditEvent.granted = + log_attribute_check_any(relOid, auditOid, + rte->modifiedCols, + auditPerms); + } + } + } + } + + /* Only do relation level logging if a grant was found. */ + if (auditEvent.granted) + { + log_audit_event(&auditEvent); + } + + pfree(auditEvent.objectName); + } +} + +/* + * Create AuditEvents for certain kinds of CREATE, ALTER, and DELETE statements + * where the object can be logged. + */ +static void +log_create_alter_drop(Oid classId, + Oid objectId) +{ + /* Only perform when class is relation */ + if (classId == RelationRelationId) + { + Relation rel; + Form_pg_class class; + + /* Open the relation */ + rel = relation_open(objectId, NoLock); + + /* Filter out any system relations */ + if (IsToastNamespace(RelationGetNamespace(rel))) + { + relation_close(rel, NoLock); + return; + } + + /* Get rel information and close it */ + class = RelationGetForm(rel); + utilityAuditEvent.objectName = + quote_qualified_identifier(get_namespace_name( + RelationGetNamespace(rel)), + RelationGetRelationName(rel)); + relation_close(rel, NoLock); + + /* Set object type based on relkind */ + switch (class->relkind) + { + case RELKIND_RELATION: + utilityAuditEvent.objectType = OBJECT_TYPE_TABLE; + break; + + case RELKIND_INDEX: + utilityAuditEvent.objectType = OBJECT_TYPE_INDEX; + break; + + case RELKIND_SEQUENCE: + utilityAuditEvent.objectType = OBJECT_TYPE_SEQUENCE; + break; + + case RELKIND_VIEW: + utilityAuditEvent.objectType = OBJECT_TYPE_VIEW; + break; + + case RELKIND_COMPOSITE_TYPE: + utilityAuditEvent.objectType = OBJECT_TYPE_COMPOSITE_TYPE; + break; + + case RELKIND_FOREIGN_TABLE: + utilityAuditEvent.objectType = OBJECT_TYPE_FOREIGN_TABLE; + break; + + case RELKIND_MATVIEW: + utilityAuditEvent.objectType = OBJECT_TYPE_MATVIEW; + break; + + /* + * Any other cases will be handled by log_utility_command(). + */ + default: + return; + break; + } + + /* Log the event */ + log_audit_event(&utilityAuditEvent); + utilityCommandLogged = true; + } +} + +/* + * Create AuditEvents for non-catalog function execution, as detected by + * log_object_access() below. + */ +static void +log_function_execute(Oid objectId) +{ + HeapTuple proctup; + Form_pg_proc proc; + + /* Get info about the function. */ + proctup = SearchSysCache1(PROCOID, ObjectIdGetDatum(objectId)); + + if (!proctup) + elog(ERROR, "cache lookup failed for function %u", objectId); + proc = (Form_pg_proc) GETSTRUCT(proctup); + + /* + * Logging execution of all pg_catalog functions would make the log + * unusably noisy. + */ + if (IsSystemNamespace(proc->pronamespace)) + { + ReleaseSysCache(proctup); + return; + } + + /* Generate the fully-qualified function name. */ + utilityAuditEvent.objectName = + quote_qualified_identifier(get_namespace_name(proc->pronamespace), + NameStr(proc->proname)); + ReleaseSysCache(proctup); + + /* Log the event */ + utilityAuditEvent.logStmtLevel = LOGSTMT_ALL; + utilityAuditEvent.commandTag = T_DoStmt; + utilityAuditEvent.command = COMMAND_EXECUTE; + utilityAuditEvent.objectType = OBJECT_TYPE_FUNCTION; + utilityAuditEvent.commandText = debug_query_string; + + log_audit_event(&utilityAuditEvent); + utilityCommandLogged = true; +} + +/* + * Log object accesses (which is more about DDL than DML, even though it + * sounds like the latter). + */ +static void +log_object_access(ObjectAccessType access, + Oid classId, + Oid objectId, + int subId, + void *arg) +{ + switch (access) + { + /* Log execute. */ + case OAT_FUNCTION_EXECUTE: + log_function_execute(objectId); + break; + + /* Log create. */ + case OAT_POST_CREATE: + { + ObjectAccessPostCreate *pc = arg; + + if (pc->is_internal) + return; + + log_create_alter_drop(classId, objectId); + } + break; + + /* Log alter. */ + case OAT_POST_ALTER: + { + ObjectAccessPostAlter *pa = arg; + + if (pa->is_internal) + return; + + log_create_alter_drop(classId, objectId); + } + break; + + /* Log drop. */ + case OAT_DROP: + { + ObjectAccessDrop *drop = arg; + + if (drop->dropflags & PERFORM_DELETION_INTERNAL) + return; + + log_create_alter_drop(classId, objectId); + } + break; + + /* All others processed by log_utility_command() */ + default: + break; + } +} + +/* + * Hook functions + */ +static ExecutorCheckPerms_hook_type next_ExecutorCheckPerms_hook = NULL; +static ProcessUtility_hook_type next_ProcessUtility_hook = NULL; +static object_access_hook_type next_object_access_hook = NULL; + +/* + * Hook ExecutorCheckPerms to do session and object auditing for DML. + */ +static bool +pgaudit_ExecutorCheckPerms_hook(List *rangeTabls, bool abort) +{ + Oid auditOid = audit_role_oid(); + + if ((auditOid != InvalidOid || auditLogBitmap != 0) && + !IsAbortedTransactionBlockState()) + log_dml(auditOid, rangeTabls); + + if (next_ExecutorCheckPerms_hook && + !(*next_ExecutorCheckPerms_hook) (rangeTabls, abort)) + return false; + + return true; +} + +/* + * Hook ProcessUtility to do session auditing for DDL and utility commands. + */ +static void +pgaudit_ProcessUtility_hook(Node *parsetree, + const char *queryString, + ProcessUtilityContext context, + ParamListInfo params, + DestReceiver *dest, + char *completionTag) +{ + /* Create the utility audit event. */ + utilityCommandLogged = false; + + utilityAuditEvent.logStmtLevel = GetCommandLogLevel(parsetree); + utilityAuditEvent.commandTag = nodeTag(parsetree); + utilityAuditEvent.command = CreateCommandTag(parsetree); + utilityAuditEvent.objectName = ""; + utilityAuditEvent.objectType = ""; + utilityAuditEvent.commandText = debug_query_string; + utilityAuditEvent.granted = false; + + /* Call the standard process utility chain. */ + if (next_ProcessUtility_hook) + (*next_ProcessUtility_hook) (parsetree, queryString, context, + params, dest, completionTag); + else + standard_ProcessUtility(parsetree, queryString, context, + params, dest, completionTag); + + /* Log the utility command if logging is on, the command has not already + * been logged by another hook, and the transaction is not aborted */ + if (auditLogBitmap != 0 && !utilityCommandLogged && + !IsAbortedTransactionBlockState()) + { + log_audit_event(&utilityAuditEvent); + } +} + +/* + * Hook object_access_hook to provide fully-qualified object names for execute, + * create, drop, and alter commands. Most of the audit information is filled in + * by log_utility_command(). + */ +static void +pgaudit_object_access_hook(ObjectAccessType access, + Oid classId, + Oid objectId, + int subId, + void *arg) +{ + if (auditLogBitmap != 0 && !IsAbortedTransactionBlockState()) + log_object_access(access, classId, objectId, subId, arg); + + if (next_object_access_hook) + (*next_object_access_hook) (access, classId, objectId, subId, arg); +} + +/* + * GUC check and assign functions + */ + +/* + * Take a pgaudit.log value such as "read, write, dml", verify that each of the + * comma-separated tokens corresponds to a LogClass value, and convert them into + * a bitmap that log_audit_event can check. + */ +static bool +check_pgaudit_log(char **newval, void **extra, GucSource source) +{ + List *flags; + char *rawval; + ListCell *lt; + uint64 *f; + + /* Make sure newval is a comma-separated list of tokens. */ + rawval = pstrdup(*newval); + if (!SplitIdentifierString(rawval, ',', &flags)) + { + GUC_check_errdetail("List syntax is invalid"); + list_free(flags); + pfree(rawval); + return false; + } + + /* + * Check that we recognise each token, and add it to the bitmap we're + * building up in a newly-allocated uint64 *f. + */ + f = (uint64 *) malloc(sizeof(uint64)); + if (!f) + return false; + *f = 0; + + foreach(lt, flags) + { + bool subtract = false; + uint64 class; + + /* Retrieve a token */ + char *token = (char *)lfirst(lt); + + /* If token is preceded by -, then then token is subtractive. */ + if (strstr(token, "-") == token) + { + token = token + 1; + subtract = true; + } + + /* Test each token. */ + if (pg_strcasecmp(token, CLASS_NONE) == 0) + class = LOG_NONE; + else if (pg_strcasecmp(token, CLASS_ALL) == 0) + class = LOG_ALL; + else if (pg_strcasecmp(token, CLASS_DDL) == 0) + class = LOG_DDL; + else if (pg_strcasecmp(token, CLASS_FUNCTION) == 0) + class = LOG_FUNCTION; + else if (pg_strcasecmp(token, CLASS_MISC) == 0) + class = LOG_MISC; + else if (pg_strcasecmp(token, CLASS_READ) == 0) + class = LOG_READ; + else if (pg_strcasecmp(token, CLASS_WRITE) == 0) + class = LOG_WRITE; + else + { + free(f); + pfree(rawval); + list_free(flags); + return false; + } + + /* Add or subtract class bits from the log bitmap. */ + if (subtract) + *f &= ~class; + else + *f |= class; + } + + pfree(rawval); + list_free(flags); + + /* + * Store the bitmap for assign_pgaudit_log. + */ + *extra = f; + + return true; +} + +/* + * Set pgaudit_log from extra (ignoring newval, which has already been converted + * to a bitmap above). Note that extra may not be set if the assignment is to be + * suppressed. + */ +static void +assign_pgaudit_log(const char *newval, void *extra) +{ + if (extra) + auditLogBitmap = *(uint64 *)extra; +} + +/* + * Define GUC variables and install hooks upon module load. + */ +void +_PG_init(void) +{ + if (IsUnderPostmaster) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("pgaudit must be loaded via shared_preload_libraries"))); + + /* + * pgaudit.role = "role1" + * + * This variable defines a role to be used for auditing. + */ + DefineCustomStringVariable("pgaudit.role", + "Enable auditing for role", + NULL, + &auditRole, + "", + PGC_SUSET, + GUC_LIST_INPUT | GUC_NOT_IN_SAMPLE, + NULL, NULL, NULL); + + /* + * pgaudit.log = "read, write, ddl" + * + * This variables controls what classes of commands are logged. + */ + DefineCustomStringVariable("pgaudit.log", + "Enable auditing for classes of commands", + NULL, + &auditLog, + "none", + PGC_SUSET, + GUC_LIST_INPUT | GUC_NOT_IN_SAMPLE, + check_pgaudit_log, + assign_pgaudit_log, + NULL); + + /* + * Install our hook functions after saving the existing pointers to preserve + * the chain. + */ + next_ExecutorCheckPerms_hook = ExecutorCheckPerms_hook; + ExecutorCheckPerms_hook = pgaudit_ExecutorCheckPerms_hook; + + next_ProcessUtility_hook = ProcessUtility_hook; + ProcessUtility_hook = pgaudit_ProcessUtility_hook; + + next_object_access_hook = object_access_hook; + object_access_hook = pgaudit_object_access_hook; +} diff --git a/contrib/pg_audit/pg_audit.control b/contrib/pg_audit/pg_audit.control new file mode 100644 index 0000000..0b39082 --- /dev/null +++ b/contrib/pg_audit/pg_audit.control @@ -0,0 +1,5 @@ +# pg_audit extension +comment = 'provides auditing functionality' +default_version = '1.0.0' +module_pathname = '$libdir/pgaudit' +relocatable = true diff --git a/contrib/pg_audit/test/test.pl b/contrib/pg_audit/test/test.pl new file mode 100755 index 0000000..3a89d50 --- /dev/null +++ b/contrib/pg_audit/test/test.pl @@ -0,0 +1,1220 @@ +#!/usr/bin/perl +################################################################################ +# test.pl - pgAudit Unit Tests +################################################################################ + +################################################################################ +# Perl includes +################################################################################ +use strict; +use warnings; +use Carp; + +use Getopt::Long; +use Pod::Usage; +use DBI; +use Cwd qw(abs_path); +use IPC::System::Simple qw(capture); + +################################################################################ +# Constants +################################################################################ +use constant +{ + true => 1, + false => 0 +}; + +use constant +{ + CONTEXT_GLOBAL => 'GLOBAL', + CONTEXT_DATABASE => 'DATABASE', + CONTEXT_ROLE => 'ROLE' +}; + +use constant +{ + CLASS => 'CLASS', + + CLASS_DDL => 'DDL', + CLASS_FUNCTION => 'FUNCTION', + CLASS_MISC => 'MISC', + CLASS_READ => 'READ', + CLASS_WRITE => 'WRITE', + + CLASS_ALL => 'ALL', + CLASS_NONE => 'NONE' +}; + +use constant +{ + COMMAND => 'COMMAND', + COMMAND_LOG => 'COMMAND_LOG', + + COMMAND_ANALYZE => 'ANALYZE', + COMMAND_ALTER_AGGREGATE => 'ALTER AGGREGATE', + COMMAND_ALTER_COLLATION => 'ALTER COLLATION', + COMMAND_ALTER_CONVERSION => 'ALTER CONVERSION', + COMMAND_ALTER_DATABASE => 'ALTER DATABASE', + COMMAND_ALTER_ROLE => 'ALTER ROLE', + COMMAND_ALTER_ROLE_SET => 'ALTER ROLE SET', + COMMAND_ALTER_TABLE => 'ALTER TABLE', + COMMAND_ALTER_TABLE_INDEX => 'ALTER TABLE INDEX', + COMMAND_BEGIN => 'BEGIN', + COMMAND_CLOSE => 'CLOSE CURSOR', + COMMAND_COMMIT => 'COMMIT', + COMMAND_COPY => 'COPY', + COMMAND_COPY_TO => 'COPY TO', + COMMAND_COPY_FROM => 'COPY FROM', + COMMAND_CREATE_AGGREGATE => 'CREATE AGGREGATE', + COMMAND_CREATE_COLLATION => 'CREATE COLLATION', + COMMAND_CREATE_CONVERSION => 'CREATE CONVERSION', + COMMAND_CREATE_DATABASE => 'CREATE DATABASE', + COMMAND_CREATE_INDEX => 'CREATE INDEX', + COMMAND_DEALLOCATE => 'DEALLOCATE', + COMMAND_DECLARE_CURSOR => 'DECLARE CURSOR', + COMMAND_DO => 'DO', + COMMAND_DISCARD_ALL => 'DISCARD ALL', + COMMAND_CREATE_FUNCTION => 'CREATE FUNCTION', + COMMAND_CREATE_ROLE => 'CREATE ROLE', + COMMAND_CREATE_SCHEMA => 'CREATE SCHEMA', + COMMAND_CREATE_TABLE => 'CREATE TABLE', + COMMAND_CREATE_TABLE_AS => 'CREATE TABLE AS', + COMMAND_DROP_DATABASE => 'DROP DATABASE', + COMMAND_DROP_SCHEMA => 'DROP SCHEMA', + COMMAND_DROP_TABLE => 'DROP TABLE', + COMMAND_DROP_TABLE_INDEX => 'DROP TABLE INDEX', + COMMAND_DROP_TABLE_TYPE => 'DROP TABLE TYPE', + COMMAND_EXECUTE => 'EXECUTE', + COMMAND_EXECUTE_READ => 'EXECUTE READ', + COMMAND_EXECUTE_WRITE => 'EXECUTE WRITE', + COMMAND_EXECUTE_FUNCTION => 'EXECUTE FUNCTION', + COMMAND_FETCH => 'FETCH', + COMMAND_GRANT => 'GRANT', + COMMAND_INSERT => 'INSERT', + COMMAND_PREPARE => 'PREPARE', + COMMAND_PREPARE_READ => 'PREPARE READ', + COMMAND_PREPARE_WRITE => 'PREPARE WRITE', + COMMAND_REVOKE => 'REVOKE', + COMMAND_SELECT => 'SELECT', + COMMAND_SET => 'SET', + COMMAND_UPDATE => 'UPDATE' +}; + +use constant +{ + TYPE => 'TYPE', + TYPE_NONE => '', + + TYPE_FUNCTION => 'FUNCTION', + TYPE_INDEX => 'INDEX', + TYPE_TABLE => 'TABLE', + TYPE_TYPE => 'TYPE' +}; + +use constant +{ + NAME => 'NAME' +}; + +################################################################################ +# Command line parameters +################################################################################ +my $strPgSqlBin = '../../../../bin/bin'; # Path of PG binaries to use for + # this test +my $strTestPath = '../../../../data'; # Path where testing will occur +my $iDefaultPort = 6000; # Default port to run Postgres on +my $bHelp = false; # Display help +my $bQuiet = false; # Supress output except for errors +my $bNoCleanup = false; # Cleanup database on exit + +GetOptions ('q|quiet' => \$bQuiet, + 'no-cleanup' => \$bNoCleanup, + 'help' => \$bHelp, + 'pgsql-bin=s' => \$strPgSqlBin, + 'test-path=s' => \$strTestPath) + or pod2usage(2); + +# Display version and exit if requested +if ($bHelp) +{ + print 'pg_audit unit test\n\n'; + pod2usage(); + + exit 0; +} + +################################################################################ +# Global variables +################################################################################ +my $hDb; # Connection to Postgres +my $strLogExpected = ''; # The expected log compared with grepping AUDIT + # entries from the postgres log. + +my $strDatabase = 'postgres'; # Connected database (modified by PgSetDatabase) +my $strUser = 'postgres'; # Connected user (modified by PgSetUser) +my $strAuditRole = 'audit'; # Role to use for auditing + +my %oAuditLogHash; # Hash to store pgaudit.log GUCS +my %oAuditGrantHash; # Hash to store pgaudit grants + +my $strCurrentAuditLog; # pgaudit.log setting that Postgres was started with +my $strTemporaryAuditLog; # pgaudit.log setting that was set hot + +################################################################################ +# Stores the mapping between commands, classes, and types +################################################################################ +my %oCommandHash = +(&COMMAND_ANALYZE => { + &CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE}, + &COMMAND_ALTER_AGGREGATE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE}, + &COMMAND_ALTER_DATABASE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE}, + &COMMAND_ALTER_COLLATION => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE}, + &COMMAND_ALTER_CONVERSION => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE}, + &COMMAND_ALTER_ROLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE}, + &COMMAND_ALTER_ROLE_SET => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE, + &COMMAND => &COMMAND_ALTER_ROLE}, + &COMMAND_ALTER_TABLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE}, + &COMMAND_ALTER_TABLE_INDEX => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_INDEX, + &COMMAND => &COMMAND_ALTER_TABLE}, + &COMMAND_BEGIN => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE}, + &COMMAND_CLOSE => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE}, + &COMMAND_COMMIT => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE}, + &COMMAND_COPY_FROM => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE, + &COMMAND => &COMMAND_COPY}, + &COMMAND_COPY_TO => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE, + &COMMAND => &COMMAND_COPY}, + &COMMAND_CREATE_AGGREGATE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE}, + &COMMAND_CREATE_CONVERSION => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE}, + &COMMAND_CREATE_COLLATION => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE}, + &COMMAND_CREATE_DATABASE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE}, + &COMMAND_CREATE_INDEX => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_INDEX}, + &COMMAND_DEALLOCATE => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE}, + &COMMAND_DECLARE_CURSOR => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE}, + &COMMAND_DO => {&CLASS => &CLASS_FUNCTION, &TYPE => &TYPE_NONE}, + &COMMAND_DISCARD_ALL => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE}, + &COMMAND_CREATE_FUNCTION => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE}, + &COMMAND_CREATE_ROLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE}, + &COMMAND_CREATE_SCHEMA => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE}, + &COMMAND_CREATE_TABLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE}, + &COMMAND_CREATE_TABLE_AS => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE}, + &COMMAND_DROP_DATABASE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE}, + &COMMAND_DROP_SCHEMA => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE}, + &COMMAND_DROP_TABLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE}, + &COMMAND_DROP_TABLE_INDEX => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_INDEX, + &COMMAND => &COMMAND_DROP_TABLE}, + &COMMAND_DROP_TABLE_TYPE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TYPE, + &COMMAND => &COMMAND_DROP_TABLE}, + &COMMAND_EXECUTE_READ => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE, + &COMMAND => &COMMAND_EXECUTE}, + &COMMAND_EXECUTE_WRITE => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE, + &COMMAND => &COMMAND_EXECUTE}, + &COMMAND_EXECUTE_FUNCTION => {&CLASS => &CLASS_FUNCTION, + &TYPE => &TYPE_FUNCTION, &COMMAND => &COMMAND_EXECUTE}, + &COMMAND_FETCH => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE}, + &COMMAND_GRANT => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE}, + &COMMAND_PREPARE_READ => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE, + &COMMAND => &COMMAND_PREPARE}, + &COMMAND_PREPARE_WRITE => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE, + &COMMAND => &COMMAND_PREPARE}, + &COMMAND_INSERT => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE}, + &COMMAND_REVOKE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE}, + &COMMAND_SELECT => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE}, + &COMMAND_SET => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE}, + &COMMAND_UPDATE => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE} +); + +################################################################################ +# CommandExecute +################################################################################ +sub CommandExecute +{ + my $strCommand = shift; + my $bSuppressError = shift; + + # Set default + $bSuppressError = defined($bSuppressError) ? $bSuppressError : false; + + # Run the command + my $iResult = system($strCommand); + + if ($iResult != 0 && !$bSuppressError) + { + confess "command '${strCommand}' failed with error ${iResult}"; + } +} + +################################################################################ +# log +################################################################################ +sub log +{ + my $strMessage = shift; + my $bError = shift; + + # Set default + $bError = defined($bError) ? $bError : false; + + if (!$bQuiet) + { + print "${strMessage}\n"; + } + + if ($bError) + { + exit 1; + } +} + +################################################################################ +# ArrayToString +################################################################################ +sub ArrayToString +{ + my @stryArray = @_; + + my $strResult = ''; + + for (my $iIndex = 0; $iIndex < @stryArray; $iIndex++) + { + if ($iIndex != 0) + { + $strResult .= ', '; + } + + $strResult .= $stryArray[$iIndex]; + } + + return $strResult; +} + +################################################################################ +# BuildModule +################################################################################ +sub BuildModule +{ + capture('cd ..;make'); + CommandExecute("cp ../pg_audit.so" . + " ${strPgSqlBin}/../lib/postgresql"); + CommandExecute("cp ../pg_audit.control" . + " ${strPgSqlBin}/../share/postgresql/extension"); + CommandExecute("cp ../pg_audit--1.0.0.sql" . + " ${strPgSqlBin}/../share/postgresql/extension"); +} + +################################################################################ +# PgConnect +################################################################################ +sub PgConnect +{ + my $iPort = shift; + + # Set default + $iPort = defined($iPort) ? $iPort : $iDefaultPort; + + # Log Connection + &log(" DB: connect user ${strUser}, database ${strDatabase}"); + + # Disconnect user session + PgDisconnect(); + + # Connect to the db + $hDb = DBI->connect("dbi:Pg:dbname=${strDatabase};port=${iPort};host=/tmp", + $strUser, undef, + {AutoCommit => 1, RaiseError => 1}); +} + +################################################################################ +# PgDisconnect +################################################################################ +sub PgDisconnect +{ + # Connect to the db (whether it is local or remote) + if (defined($hDb)) + { + $hDb->disconnect; + undef($hDb); + } +} + +################################################################################ +# PgExecute +################################################################################ +sub PgExecute +{ + my $strSql = shift; + + # Log the statement + &log(" SQL: ${strSql}"); + + # Execute the statement + my $hStatement = $hDb->prepare($strSql); + + $hStatement->execute(); + $hStatement->finish(); +} + +################################################################################ +# PgExecuteOnly +################################################################################ +sub PgExecuteOnly +{ + my $strSql = shift; + + # Log the statement + &log(" SQL: ${strSql}"); + + # Execute the statement + $hDb->do($strSql); +} + +################################################################################ +# PgSetDatabase +################################################################################ +sub PgSetDatabase +{ + my $strDatabaseParam = shift; + + # Stop and start the database to reset pgconf entries + PgStop(); + PgStart(); + + # Execute the statement + $strDatabase = $strDatabaseParam; + PgConnect(); +} + +################################################################################ +# PgSetUser +################################################################################ +sub PgSetUser +{ + my $strUserParam = shift; + + $strUser = $strUserParam; + + # Stop and start the database to reset pgconf entries + if ((defined($strTemporaryAuditLog) && !defined($strCurrentAuditLog)) || + (defined($strCurrentAuditLog) && !defined($strTemporaryAuditLog)) || + $strCurrentAuditLog ne $strTemporaryAuditLog) + { + $strCurrentAuditLog = $strTemporaryAuditLog; + + PgStop(); + PgStart(); + } + else + { + # Execute the statement + PgConnect(); + } +} + +################################################################################ +# SaveString +################################################################################ +sub SaveString +{ + my $strFile = shift; + my $strString = shift; + + # Open the file for writing + my $hFile; + + open($hFile, '>', $strFile) + or confess "unable to open ${strFile}"; + + if ($strString ne '') + { + syswrite($hFile, $strString) + or confess "unable to write to ${strFile}: $!"; + } + + close($hFile); +} + +################################################################################ +# PgLogExecute +################################################################################ +sub PgLogExecute +{ + my $strCommand = shift; + my $strSql = shift; + my $oData = shift; + my $bExecute = shift; + my $bWait = shift; + my $bLogSql = shift; + + # Set defaults + $bExecute = defined($bExecute) ? $bExecute : true; + $bWait = defined($bWait) ? $bWait : true; + $bLogSql = defined($bLogSql) ? $bLogSql : true; + + if ($bExecute) + { + PgExecuteOnly($strSql); + } + + PgLogExpect($strCommand, $bLogSql ? $strSql : '', $oData); + + if ($bWait) + { + PgLogWait(); + } +} + +################################################################################ +# PgLogExpect +################################################################################ +sub PgLogExpect +{ + my $strCommand = shift; + my $strSql = shift; + my $oData = shift; + + # If oData is false then no logging + if (defined($oData) && ref($oData) eq '' && !$oData) + { + return; + } + + # Log based on session + if (PgShouldLog($strCommand)) + { + # Make sure class is defined + my $strClass = $oCommandHash{$strCommand}{&CLASS}; + + if (!defined($strClass)) + { + confess "class is not defined for command ${strCommand}"; + } + + # Make sure object type is defined + my $strObjectType = $oCommandHash{$strCommand}{&TYPE}; + + if (!defined($strObjectType)) + { + confess "object type is not defined for command ${strCommand}"; + } + + # Check for command override + my $strCommandLog = $strCommand; + + if ($oCommandHash{$strCommand}{&COMMAND}) + { + $strCommandLog = $oCommandHash{$strCommand}{&COMMAND}; + } + + my $strObjectName = ''; + + if (defined($oData) && ref($oData) ne 'ARRAY') + { + $strObjectName = $oData; + } + + my $strLog .= "SESSION,${strClass},${strCommandLog}," . + "${strObjectType},${strObjectName},${strSql}"; + &log("AUDIT: ${strLog}"); + + $strLogExpected .= "${strLog}\n"; + } + + # Log based on grants + if (ref($oData) eq 'ARRAY' && ($strCommand eq COMMAND_SELECT || + $oCommandHash{$strCommand}{&CLASS} eq CLASS_WRITE)) + { + foreach my $oTableHash (@{$oData}) + { + my $strObjectName = ${$oTableHash}{&NAME}; + my $strCommandLog = ${$oTableHash}{&COMMAND}; + + if (defined($oAuditGrantHash{$strAuditRole} + {$strObjectName}{$strCommandLog})) + { + my $strCommandLog = defined(${$oTableHash}{&COMMAND_LOG}) ? + ${$oTableHash}{&COMMAND_LOG} : $strCommandLog; + my $strClass = $oCommandHash{$strCommandLog}{&CLASS}; + my $strObjectType = ${$oTableHash}{&TYPE}; + + my $strLog .= "OBJECT,${strClass},${strCommandLog}," . + "${strObjectType},${strObjectName},${strSql}"; + &log("AUDIT: ${strLog}"); + + $strLogExpected .= "${strLog}\n"; + } + } + + $oData = undef; + } +} + +################################################################################ +# PgShouldLog +################################################################################ +sub PgShouldLog +{ + my $strCommand = shift; + + # Make sure class is defined + my $strClass = $oCommandHash{$strCommand}{&CLASS}; + + if (!defined($strClass)) + { + confess "class is not defined for command ${strCommand}"; + } + + # Check logging for the role + my $bLog = undef; + + if (defined($oAuditLogHash{&CONTEXT_ROLE}{$strUser})) + { + $bLog = $oAuditLogHash{&CONTEXT_ROLE}{$strUser}{$strClass}; + } + + # Else check logging for the db + elsif (defined($oAuditLogHash{&CONTEXT_DATABASE}{$strDatabase})) + { + $bLog = $oAuditLogHash{&CONTEXT_DATABASE}{$strDatabase}{$strClass}; + } + + # Else check logging for global + elsif (defined($oAuditLogHash{&CONTEXT_GLOBAL}{&CONTEXT_GLOBAL})) + { + $bLog = $oAuditLogHash{&CONTEXT_GLOBAL}{&CONTEXT_GLOBAL}{$strClass}; + } + + return defined($bLog) ? true : false; +} + +################################################################################ +# PgLogWait +################################################################################ +sub PgLogWait +{ + my $strLogActual; + + # Run in an eval block since grep returns 1 when nothing was found + eval + { + $strLogActual = capture("grep 'LOG: AUDIT: '" . + " ${strTestPath}/postgresql.log"); + }; + + # If an error was returned, continue if it was 1, otherwise confess + if ($@) + { + my $iExitStatus = $? >> 8; + + if ($iExitStatus != 1) + { + confess "grep returned ${iExitStatus}"; + } + + $strLogActual = ''; + } + + # Strip the AUDIT and timestamp from the actual log + $strLogActual =~ s/prefix LOG: AUDIT\: //g; + + # Save the logs + SaveString("${strTestPath}/audit.actual", $strLogActual); + SaveString("${strTestPath}/audit.expected", $strLogExpected); + + CommandExecute("diff ${strTestPath}/audit.expected" . + " ${strTestPath}/audit.actual"); +} + +################################################################################ +# PgDrop +################################################################################ +sub PgDrop +{ + my $strPath = shift; + + # Set default + $strPath = defined($strPath) ? $strPath : $strTestPath; + + # Stop the cluster + PgStop(true, $strPath); + + # Remove the directory + CommandExecute("rm -rf ${strTestPath}"); +} + +################################################################################ +# PgCreate +################################################################################ +sub PgCreate +{ + my $strPath = shift; + + # Set default + $strPath = defined($strPath) ? $strPath : $strTestPath; + + CommandExecute("${strPgSqlBin}/initdb -D ${strPath} -U ${strUser}" . + ' -A trust > /dev/null'); +} + +################################################################################ +# PgStop +################################################################################ +sub PgStop +{ + my $bImmediate = shift; + my $strPath = shift; + + # Set default + $strPath = defined($strPath) ? $strPath : $strTestPath; + $bImmediate = defined($bImmediate) ? $bImmediate : false; + + # Disconnect user session + PgDisconnect(); + + # If postmaster process is running then stop the cluster + if (-e $strPath . '/postmaster.pid') + { + CommandExecute("${strPgSqlBin}/pg_ctl stop -D ${strPath} -w -s -m " . + ($bImmediate ? 'immediate' : 'fast')); + } +} + +################################################################################ +# PgStart +################################################################################ +sub PgStart +{ + my $iPort = shift; + my $strPath = shift; + + # Set default + $iPort = defined($iPort) ? $iPort : $iDefaultPort; + $strPath = defined($strPath) ? $strPath : $strTestPath; + + # Make sure postgres is not running + if (-e $strPath . '/postmaster.pid') + { + confess "${strPath}/postmaster.pid exists, cannot start"; + } + + # Start the cluster + CommandExecute("${strPgSqlBin}/pg_ctl start -o \"" . + "-c port=${iPort}" . + " -c unix_socket_directories='/tmp'" . + " -c shared_preload_libraries='pg_audit'" . + " -c log_min_messages=debug1" . + " -c log_line_prefix='prefix '" . + # " -c log_destination='stderr,csvlog'" . + # " -c logging_collector=on" . + (defined($strCurrentAuditLog) ? + " -c pgaudit.log='${strCurrentAuditLog}'" : '') . + " -c pgaudit.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 pgaudit.log = '" . + ArrayToString(@stryClass) . "'"; + $strTemporaryAuditLog = ArrayToString(@stryClass); + } + elsif ($strContext eq CONTEXT_ROLE) + { + $strCommand = COMMAND_ALTER_ROLE_SET; + $strSql = "alter role ${strName} set pgaudit.log = '" . + ArrayToString(@stryClass) . "'"; + } + else + { + confess "unable to set pgaudit.log for context ${strContext}"; + } + + # Reset the audit log + if ($strContext eq CONTEXT_GLOBAL) + { + delete($oAuditLogHash{$strContext}); + $strName = CONTEXT_GLOBAL; + } + else + { + delete($oAuditLogHash{$strContext}{$strName}); + } + + # Store all the classes in the hash and build the GUC + foreach my $strClass (@stryClass) + { + if ($strClass eq CLASS_ALL) + { + $oAuditLogHash{$strContext}{$strName}{&CLASS_DDL} = true; + $oAuditLogHash{$strContext}{$strName}{&CLASS_FUNCTION} = true; + $oAuditLogHash{$strContext}{$strName}{&CLASS_MISC} = true; + $oAuditLogHash{$strContext}{$strName}{&CLASS_READ} = true; + $oAuditLogHash{$strContext}{$strName}{&CLASS_WRITE} = true; + } + + if (index($strClass, '-') == 0) + { + $strClass = substr($strClass, 1); + + delete($oAuditLogHash{$strContext}{$strName}{$strClass}); + } + else + { + $oAuditLogHash{$strContext}{$strName}{$strClass} = true; + } + } + + PgLogExecute($strCommand, $strSql); +} + +################################################################################ +# PgAuditGrantSet +################################################################################ +sub PgAuditGrantSet +{ + my $strRole = shift; + my $strPrivilege = shift; + my $strObject = shift; + my $strColumn = shift; + + # Create SQL to set the grant + PgLogExecute(COMMAND_GRANT, "grant " . lc(${strPrivilege}) . + (defined($strColumn) ? " (${strColumn})" : '') . + " on ${strObject} to ${strRole}"); + + $oAuditGrantHash{$strRole}{$strObject}{$strPrivilege} = true; +} + +################################################################################ +# PgAuditGrantReset +################################################################################ +sub PgAuditGrantReset +{ + my $strRole = shift; + my $strPrivilege = shift; + my $strObject = shift; + my $strColumn = shift; + + # Create SQL to set the grant + PgLogExecute(COMMAND_REVOKE, "revoke " . lc(${strPrivilege}) . + (defined($strColumn) ? " (${strColumn})" : '') . + " on ${strObject} from ${strRole}"); + + delete($oAuditGrantHash{$strRole}{$strObject}{$strPrivilege}); +} + +################################################################################ +# Main +################################################################################ +my @oyTable; # Store table info for select, insert, update, delete + +# Drop the old cluster, build the code, and create a new cluster +PgDrop(); +BuildModule(); +PgCreate(); +PgStart(); + +PgExecute("create extension pg_audit"); + +# Create test users and the audit role +PgExecute("create user user1"); +PgExecute("create user user2"); +PgExecute("create role ${strAuditRole}"); + +PgAuditLogSet(CONTEXT_GLOBAL, undef, (CLASS_DDL)); + +PgAuditLogSet(CONTEXT_ROLE, 'user2', (CLASS_READ, CLASS_WRITE)); + +# User1 follows the global log settings +PgSetUser('user1'); +PgLogExecute(COMMAND_CREATE_TABLE, 'create table test (id int)', 'public.test'); +PgLogExecute(COMMAND_SELECT, 'select * from test'); + +PgLogExecute(COMMAND_DROP_TABLE, 'drop table test', 'public.test'); + +PgSetUser('user2'); +PgLogExecute(COMMAND_CREATE_TABLE, + 'create table test2 (id int)', 'public.test2'); +PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.test2'); +PgLogExecute(COMMAND_CREATE_TABLE, + 'create table test3 (id int)', 'public.test2'); + +# Catalog select should not log +PgLogExecute(COMMAND_SELECT, 'select * from pg_class limit 1', + false); + +# Multi-table select +@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE, + &COMMAND => &COMMAND_SELECT}, + {&NAME => 'public.test2', &TYPE => &TYPE_TABLE, + &COMMAND => &COMMAND_SELECT}); +PgLogExecute(COMMAND_SELECT, 'select * from test3, test2', + \@oyTable); + +# Various CTE combinations +PgAuditGrantSet($strAuditRole, &COMMAND_INSERT, 'public.test3'); + +@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE, + &COMMAND => &COMMAND_INSERT}, + {&NAME => 'public.test2', &TYPE => &TYPE_TABLE, + &COMMAND => &COMMAND_SELECT}); +PgLogExecute(COMMAND_INSERT, + 'with cte as (select id from test2)' . + ' insert into test3 select id from cte', + \@oyTable); + +@oyTable = ({&NAME => 'public.test2', &TYPE => &TYPE_TABLE, + &COMMAND => &COMMAND_INSERT}, + {&NAME => 'public.test3', &TYPE => &TYPE_TABLE, + &COMMAND => &COMMAND_INSERT}); +PgLogExecute(COMMAND_INSERT, + 'with cte as (insert into test3 values (1) returning id)' . + ' insert into test2 select id from cte', + \@oyTable); + +PgAuditGrantSet($strAuditRole, &COMMAND_UPDATE, 'public.test2'); + +@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE, + &COMMAND => &COMMAND_INSERT}, + {&NAME => 'public.test2', &TYPE => &TYPE_TABLE, + &COMMAND => &COMMAND_UPDATE}); +PgLogExecute(COMMAND_INSERT, + 'with cte as (update test2 set id = 1 returning id)' . + ' insert into test3 select id from cte', + \@oyTable); + +@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE, + &COMMAND => &COMMAND_UPDATE}, + {&NAME => 'public.test2', &TYPE => &TYPE_TABLE, + &COMMAND => &COMMAND_INSERT}, + {&NAME => 'public.test2', &TYPE => &TYPE_TABLE, + &COMMAND => &COMMAND_SELECT, &COMMAND_LOG => &COMMAND_INSERT}); +PgLogExecute(COMMAND_UPDATE, + 'with cte as (insert into test2 values (1) returning id)' . + ' update test3 set id = cte.id' . + ' from cte where test3.id <> cte.id', + \@oyTable); + +PgSetUser('postgres'); +PgAuditLogSet(CONTEXT_ROLE, 'user2', (CLASS_NONE)); +PgSetUser('user2'); + +# Column-based audits +PgLogExecute(COMMAND_CREATE_TABLE, + 'create table test4 (id int, name text)', 'public.test4'); +PgAuditGrantSet($strAuditRole, COMMAND_SELECT, 'public.test4', 'name'); +PgAuditGrantSet($strAuditRole, COMMAND_UPDATE, 'public.test4', 'id'); +PgAuditGrantSet($strAuditRole, COMMAND_INSERT, 'public.test4', 'name'); + +# Select +@oyTable = (); +PgLogExecute(COMMAND_SELECT, 'select id from public.test4', + \@oyTable); + +@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE, + &COMMAND => &COMMAND_SELECT}); +PgLogExecute(COMMAND_SELECT, 'select name from public.test4', + \@oyTable); + +# Insert +@oyTable = (); +PgLogExecute(COMMAND_INSERT, 'insert into public.test4 (id) values (1)', + \@oyTable); + +@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE, + &COMMAND => &COMMAND_INSERT}); +PgLogExecute(COMMAND_INSERT, "insert into public.test4 (name) values ('test')", + \@oyTable); + +# Update +@oyTable = (); +PgLogExecute(COMMAND_UPDATE, "update public.test4 set name = 'foo'", + \@oyTable); + +@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE, + &COMMAND => &COMMAND_UPDATE}); +PgLogExecute(COMMAND_UPDATE, "update public.test4 set id = 1", + \@oyTable); + +@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE, + &COMMAND => &COMMAND_SELECT, &COMMAND_LOG => &COMMAND_UPDATE}); +PgLogExecute(COMMAND_UPDATE, + "update public.test4 set name = 'foo' where name = 'bar'", + \@oyTable); + +# Drop test tables +PgLogExecute(COMMAND_DROP_TABLE, "drop table test2", 'public.test2'); +PgLogExecute(COMMAND_DROP_TABLE, "drop table test3", 'public.test3'); +PgLogExecute(COMMAND_DROP_TABLE, "drop table test4", 'public.test4'); + + +# Make sure there are no more audit events pending in the postgres log +PgLogWait(); + +# Now create some email friendly tests. These first tests are session logging +# only. +PgSetUser('postgres'); + +&log("\nExamples:"); + +&log("\nSession Audit:\n"); + +PgAuditLogSet(CONTEXT_GLOBAL, undef, (CLASS_DDL, CLASS_READ)); +PgSetUser('user1'); + +PgLogExecute(COMMAND_CREATE_TABLE, + 'create table account (id int, name text, password text,' . + ' description text)', 'public.account'); +PgLogExecute(COMMAND_SELECT, + 'select * from account'); +PgLogExecute(COMMAND_INSERT, + "insert into account (id, name, password, description)" . + " values (1, 'user1', 'HASH1', 'blah, blah')"); +&log("AUDIT: "); + +# Now tests for object logging +&log("\nObject Audit:\n"); + +PgSetUser('postgres'); +PgAuditLogSet(CONTEXT_GLOBAL, undef, (CLASS_NONE)); +PgExecute("set pgaudit.role = 'audit'"); +PgSetUser('user1'); + +PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.account', 'password'); + +@oyTable = (); +PgLogExecute(COMMAND_SELECT, 'select id, name from account', + \@oyTable); +&log("AUDIT: "); + +@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: "); + +@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: "); + +@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 pgaudit.role = 'audit'"); + +PgLogExecute(COMMAND_DO, "do \$\$\ begin raise notice 'test'; end; \$\$;"); +PgLogExecute(COMMAND_CREATE_SCHEMA, "create schema test"); + +# Test COPY +PgLogExecute(COMMAND_COPY_TO, + "COPY pg_class to '" . abs_path($strTestPath) . "/class.out'"); +PgLogExecute(COMMAND_CREATE_TABLE_AS, + "CREATE TABLE test.pg_class as select * from pg_class", + 'test.pg_class', true, false); +PgLogExecute(COMMAND_INSERT, + "CREATE TABLE test.pg_class as select * from pg_class", + undef, false, true); +PgLogExecute(COMMAND_INSERT, + "COPY test.pg_class from '" . abs_path($strTestPath) . + "/class.out'", undef, true, false); +PgLogExecute(COMMAND_COPY_FROM, + "COPY test.pg_class from '" . abs_path($strTestPath) . + "/class.out'", undef, false, true); + +# Test prepared SELECT +PgLogExecute(COMMAND_PREPARE_READ, + 'PREPARE pgclassstmt (oid) as select *' . + ' from pg_class where oid = $1'); +PgLogExecute(COMMAND_EXECUTE_READ, + 'EXECUTE pgclassstmt (1)'); +PgLogExecute(COMMAND_DEALLOCATE, + 'DEALLOCATE pgclassstmt'); + +# Test cursor +PgLogExecute(COMMAND_BEGIN, + 'BEGIN'); +PgLogExecute(COMMAND_DECLARE_CURSOR, + 'DECLARE ctest SCROLL CURSOR FOR SELECT * FROM pg_class'); +PgLogExecute(COMMAND_FETCH, + 'FETCH NEXT FROM ctest'); +PgLogExecute(COMMAND_CLOSE, + 'CLOSE ctest'); +PgLogExecute(COMMAND_COMMIT, + 'COMMIT'); + +# Test prepared INSERT +PgLogExecute(COMMAND_CREATE_TABLE, + 'create table test.test_insert (id int)', 'test.test_insert'); +PgLogExecute(COMMAND_PREPARE_WRITE, + 'PREPARE pgclassstmt (oid) as insert' . + ' into test.test_insert (id) values ($1)'); +PgLogExecute(COMMAND_INSERT, + 'EXECUTE pgclassstmt (1)', undef, true, false); +PgLogExecute(COMMAND_EXECUTE_WRITE, + 'EXECUTE pgclassstmt (1)', undef, false, true); + +# Create a table with a primary key +PgLogExecute(COMMAND_CREATE_TABLE, + 'create table test (id int primary key, name text,' . + 'description text)', + 'public.test', true, false); +PgLogExecute(COMMAND_CREATE_INDEX, + 'create table test (id int primary key, name text,' . + 'description text)', + 'public.test_pkey', false, true); +PgLogExecute(COMMAND_ANALYZE, 'analyze test'); + +# Grant select to public - this should have no affect on auditing +PgLogExecute(COMMAND_GRANT, 'grant select on public.test to public'); +PgLogExecute(COMMAND_SELECT, 'select * from test'); + +# Now grant select to audit and it should be logged +PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.test'); +@oyTable = ({&NAME => 'public.test', &TYPE => &TYPE_TABLE, + &COMMAND => &COMMAND_SELECT}); +PgLogExecute(COMMAND_SELECT, 'select * from test', \@oyTable); + +# Check columns granted to public and make sure they do not log +PgAuditGrantReset($strAuditRole, &COMMAND_SELECT, 'public.test'); +PgLogExecute(COMMAND_GRANT, 'grant select (name) on public.test to public'); +PgLogExecute(COMMAND_SELECT, 'select * from test'); +PgLogExecute(COMMAND_SELECT, 'select from test'); + +# Now set grant to a specific column to audit and make sure it logs +# Make sure the the converse is true +PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.test', + 'name, description'); +PgLogExecute(COMMAND_SELECT, 'select id from test'); + +@oyTable = ({&NAME => 'public.test', &TYPE => &TYPE_TABLE, + &COMMAND => &COMMAND_SELECT}); +PgLogExecute(COMMAND_SELECT, 'select name from test', \@oyTable); + +PgLogExecute(COMMAND_ALTER_TABLE, + 'alter table test drop description', 'public.test'); +@oyTable = ({&NAME => 'public.test', &TYPE => &TYPE_TABLE, + &COMMAND => &COMMAND_SELECT}); +PgLogExecute(COMMAND_SELECT, 'select from test', \@oyTable); + +PgLogExecute(COMMAND_ALTER_TABLE, + 'alter table test rename to test2', 'public.test'); +PgLogExecute(COMMAND_ALTER_TABLE, + 'alter table test2 set schema test', 'public.test2', true, false); +PgLogExecute(COMMAND_ALTER_TABLE_INDEX, 'alter table test2 set schema test', + 'public.test_pkey', false, true); +PgLogExecute(COMMAND_ALTER_TABLE, 'alter table test.test2 add description text', + 'test.test2'); +PgLogExecute(COMMAND_ALTER_TABLE, 'alter table test.test2 drop description', + 'test.test2'); +PgLogExecute(COMMAND_DROP_TABLE_INDEX, 'drop table test.test2', + 'test.test_pkey', false, false); +PgLogExecute(COMMAND_DROP_TABLE, 'drop table test.test2', + 'test.test2', true, true); + +PgLogExecute(COMMAND_CREATE_FUNCTION, 'CREATE FUNCTION int_add(a int, b int)' . + ' returns int as $$ begin return a + b;' . + ' end $$language plpgsql'); +PgLogExecute(COMMAND_EXECUTE_FUNCTION, "select int_add(1, 1)", + 'public.int_add'); + +PgLogExecute(COMMAND_CREATE_AGGREGATE, "CREATE AGGREGATE sum_test (int)" . + " (sfunc = int_add, stype = int, initcond = 0)"); +PgLogExecute(COMMAND_ALTER_AGGREGATE, + "ALTER AGGREGATE sum_test (int) rename to sum_test2"); + +PgLogExecute(COMMAND_CREATE_COLLATION, + "CREATE COLLATION collation_test FROM \"de_DE\""); +PgLogExecute(COMMAND_ALTER_COLLATION, + "ALTER COLLATION collation_test rename to collation_test2"); + +PgLogExecute(COMMAND_CREATE_CONVERSION, + "CREATE CONVERSION conversion_test FOR 'SQL_ASCII' TO". + " 'MULE_INTERNAL' FROM ascii_to_mic"); +PgLogExecute(COMMAND_ALTER_CONVERSION, + "ALTER CONVERSION conversion_test rename to conversion_test2"); + +PgLogExecute(COMMAND_CREATE_DATABASE, "CREATE DATABASE database_test"); +PgLogExecute(COMMAND_ALTER_DATABASE, + "ALTER DATABASE database_test rename to database_test2"); +PgLogExecute(COMMAND_DROP_DATABASE, "DROP DATABASE database_test2"); + +# Make sure there are no more audit events pending in the postgres log +PgLogWait(); + +# Stop the database +if (!$bNoCleanup) +{ + PgDrop(); +} diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml index a698d0f..5b247a9 100644 --- a/doc/src/sgml/contrib.sgml +++ b/doc/src/sgml/contrib.sgml @@ -124,6 +124,7 @@ CREATE EXTENSION module_name FROM unpackaged; <ree; &pageinspect; &passwordcheck; + &pgaudit; &pgbuffercache; &pgcrypto; &pgfreespacemap; diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml index f03b72a..e4f0bdc 100644 --- a/doc/src/sgml/filelist.sgml +++ b/doc/src/sgml/filelist.sgml @@ -124,6 +124,7 @@ + diff --git a/doc/src/sgml/pgaudit.sgml b/doc/src/sgml/pgaudit.sgml new file mode 100644 index 0000000..f3f4ab9 --- /dev/null +++ b/doc/src/sgml/pgaudit.sgml @@ -0,0 +1,316 @@ + + + + pg_audit + + + pg_audit + + + + The pg_audit module provides session and object + auditing via the standard logging facility. Session and object auditing are + completely independent and can be combined. + + + + Session Auditing + + + 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. SESSION), command type + (e.g. CREATE TABLE, SELECT) and + statement (e.g. "select * from test"). + + Fully-qualified names and object types will be logged for + CREATE, UPDATE, and + DROP commands on TABLE, + MATVIEW, VIEW, + INDEX, FOREIGN TABLE, + COMPOSITE TYPE, INDEX, and + SEQUENCE objects as well as function calls. + + + + Configuration + + + Session logging is controlled by the pgaudit.log GUC. + There are five classes of commands that are recognized: + + + + + READ - SELECT and + COPY when the source is a table or query. + + + + + WRITE - INSERT, + UPDATE, DELETE, + TRUNCATE, and COPY when the + destination is a table. + + + + + FUNCTION - Function calls and + DO blocks. + + + + + DDL - DDL, plus VACUUM, + REINDEX, and ANALYZE. + + + + + MISC - Miscellaneous commands, e.g. + DISCARD, FETCH, + CHECKPOINT. + + + + + + + Enable session logging for all writes and DDL: + +pgaudit.log = 'write, ddl' + + + + + Enable session logging for all commands except miscellaneous: + +pgaudit.log = 'all, -misc' + + + + + + Examples + + + Set pgaudit.log = 'read, ddl' in + postgresql.conf. + + + + SQL: + + + +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'); + + + + Log Output: + + + +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 + + + + + + Object Auditing + + + Object auditing logs commands that affect a particular object. Only + SELECT, INSERT, + UPDATE and DELETE commands are + supported. + + + + Configuration + + + Object-level auditing is implemented via the roles system. The + pgaudit.role 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. + + + +postresql.conf: pgaudit.role = 'audit' + +grant select, delete + on public.account; + + + + + Examples + + + Set pgaudit.role = 'audit' in + postgresql.conf. + + + + SQL: + + + +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 + + + + Log Output: + + + +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 + + + + + + Format + + + Audit entries are written to the standard logging facility and contain + the following columns in comma-separated format: + + + + Output is not in compliant CSV format. If machine-readability is + required then consider setting + log_destination = 'csvlog'. + + + + + + + AUDIT_TYPE - SESSION or + OBJECT. + + + + + CLASS - READ, + WRITE, FUNCTION, + DDL, or MISC. + + + + + COMMAND - ALTER TABLE, + SELECT, CREATE INDEX, + UPDATE, etc. + + + + + OBJECT_TYPE - TABLE, + INDEX, VIEW, etc. Only + available for DML and certain DDL commands. + + + + + OBJECT_NAME - The fully-qualified object name + (e.g. public.account). Only available for DML and certain DDL + commands. + + + + + STATEMENT - Statement execute on the backend. + + + + + + + + Author + + + David Steele david@pgmasters.net + + +