/*-------------------------------------------------------------------------- * * test_oat_hooks.c * Code for testing mandatory access control (MAC) using object access hooks. * * Copyright (c) 2015-2024, PostgreSQL Global Development Group * * IDENTIFICATION * src/test/modules/test_oat_hooks/test_oat_hooks.c * * ------------------------------------------------------------------------- */ #include "postgres.h" #include "access/parallel.h" #include "catalog/dependency.h" #include "catalog/objectaccess.h" #include "catalog/pg_proc.h" #include "executor/executor.h" #include "fmgr.h" #include "miscadmin.h" #include "tcop/utility.h" PG_MODULE_MAGIC; /* * GUCs controlling which operations to deny */ static bool REGRESS_deny_set_variable = false; static bool REGRESS_deny_alter_system = false; static bool REGRESS_deny_object_access = false; static bool REGRESS_deny_exec_perms = false; static bool REGRESS_deny_utility_commands = false; static bool REGRESS_audit = false; /* * GUCs for testing privileges on USERSET and SUSET variables, * with and without privileges granted prior to module load. */ static bool REGRESS_userset_variable1 = false; static bool REGRESS_userset_variable2 = false; static bool REGRESS_suset_variable1 = false; static bool REGRESS_suset_variable2 = false; /* Saved hook values */ static object_access_hook_type next_object_access_hook = NULL; static object_access_hook_type_str next_object_access_hook_str = NULL; static ExecutorCheckPerms_hook_type next_exec_check_perms_hook = NULL; static ProcessUtility_hook_type next_ProcessUtility_hook = NULL; /* Test Object Access Type Hook hooks */ static void REGRESS_object_access_hook_str(ObjectAccessType access, Oid classId, const char *objName, int subId, void *arg); static void REGRESS_object_access_hook(ObjectAccessType access, Oid classId, Oid objectId, int subId, void *arg); static bool REGRESS_exec_check_perms(List *rangeTabls, List *rteperminfos, bool do_abort); static void REGRESS_utility_command(PlannedStmt *pstmt, const char *queryString, bool readOnlyTree, ProcessUtilityContext context, ParamListInfo params, QueryEnvironment *queryEnv, DestReceiver *dest, QueryCompletion *qc); /* Helper functions */ static char *accesstype_to_string(ObjectAccessType access, int subId); static char *accesstype_arg_to_string(ObjectAccessType access, void *arg); /* * Module load callback */ void _PG_init(void) { /* * test_oat_hooks.deny_set_variable = (on|off) */ DefineCustomBoolVariable("test_oat_hooks.deny_set_variable", "Deny non-superuser set permissions", NULL, ®RESS_deny_set_variable, false, PGC_SUSET, GUC_NOT_IN_SAMPLE, NULL, NULL, NULL); /* * test_oat_hooks.deny_alter_system = (on|off) */ DefineCustomBoolVariable("test_oat_hooks.deny_alter_system", "Deny non-superuser alter system set permissions", NULL, ®RESS_deny_alter_system, false, PGC_SUSET, GUC_NOT_IN_SAMPLE, NULL, NULL, NULL); /* * test_oat_hooks.deny_object_access = (on|off) */ DefineCustomBoolVariable("test_oat_hooks.deny_object_access", "Deny non-superuser object access permissions", NULL, ®RESS_deny_object_access, false, PGC_SUSET, GUC_NOT_IN_SAMPLE, NULL, NULL, NULL); /* * test_oat_hooks.deny_exec_perms = (on|off) */ DefineCustomBoolVariable("test_oat_hooks.deny_exec_perms", "Deny non-superuser exec permissions", NULL, ®RESS_deny_exec_perms, false, PGC_SUSET, GUC_NOT_IN_SAMPLE, NULL, NULL, NULL); /* * test_oat_hooks.deny_utility_commands = (on|off) */ DefineCustomBoolVariable("test_oat_hooks.deny_utility_commands", "Deny non-superuser utility commands", NULL, ®RESS_deny_utility_commands, false, PGC_SUSET, GUC_NOT_IN_SAMPLE, NULL, NULL, NULL); /* * test_oat_hooks.audit = (on|off) */ DefineCustomBoolVariable("test_oat_hooks.audit", "Turn on/off debug audit messages", NULL, ®RESS_audit, false, PGC_SUSET, GUC_NOT_IN_SAMPLE, NULL, NULL, NULL); /* * test_oat_hooks.user_var{1,2} = (on|off) */ DefineCustomBoolVariable("test_oat_hooks.user_var1", "Dummy parameter settable by public", NULL, ®RESS_userset_variable1, false, PGC_USERSET, GUC_NOT_IN_SAMPLE, NULL, NULL, NULL); DefineCustomBoolVariable("test_oat_hooks.user_var2", "Dummy parameter settable by public", NULL, ®RESS_userset_variable2, false, PGC_USERSET, GUC_NOT_IN_SAMPLE, NULL, NULL, NULL); /* * test_oat_hooks.super_var{1,2} = (on|off) */ DefineCustomBoolVariable("test_oat_hooks.super_var1", "Dummy parameter settable by superuser", NULL, ®RESS_suset_variable1, false, PGC_SUSET, GUC_NOT_IN_SAMPLE, NULL, NULL, NULL); DefineCustomBoolVariable("test_oat_hooks.super_var2", "Dummy parameter settable by superuser", NULL, ®RESS_suset_variable2, false, PGC_SUSET, GUC_NOT_IN_SAMPLE, NULL, NULL, NULL); MarkGUCPrefixReserved("test_oat_hooks"); /* Object access hook */ next_object_access_hook = object_access_hook; object_access_hook = REGRESS_object_access_hook; /* Object access hook str */ next_object_access_hook_str = object_access_hook_str; object_access_hook_str = REGRESS_object_access_hook_str; /* DML permission check */ next_exec_check_perms_hook = ExecutorCheckPerms_hook; ExecutorCheckPerms_hook = REGRESS_exec_check_perms; /* ProcessUtility hook */ next_ProcessUtility_hook = ProcessUtility_hook; ProcessUtility_hook = REGRESS_utility_command; } static void emit_audit_message(const char *type, const char *hook, char *action, char *objName) { /* * Ensure that audit messages are not duplicated by only emitting them * from a leader process, not a worker process. This makes the test * results deterministic even if run with debug_parallel_query = regress. */ if (REGRESS_audit && !IsParallelWorker()) { const char *who = superuser_arg(GetUserId()) ? "superuser" : "non-superuser"; if (objName) ereport(NOTICE, (errcode(ERRCODE_INTERNAL_ERROR), errmsg("in %s: %s %s %s [%s]", hook, who, type, action, objName))); else ereport(NOTICE, (errcode(ERRCODE_INTERNAL_ERROR), errmsg("in %s: %s %s %s", hook, who, type, action))); } if (action) pfree(action); if (objName) pfree(objName); } static void audit_attempt(const char *hook, char *action, char *objName) { emit_audit_message("attempting", hook, action, objName); } static void audit_success(const char *hook, char *action, char *objName) { emit_audit_message("finished", hook, action, objName); } static void audit_failure(const char *hook, char *action, char *objName) { emit_audit_message("denied", hook, action, objName); } static void REGRESS_object_access_hook_str(ObjectAccessType access, Oid classId, const char *objName, int subId, void *arg) { audit_attempt("object_access_hook_str", accesstype_to_string(access, subId), pstrdup(objName)); if (next_object_access_hook_str) { (*next_object_access_hook_str) (access, classId, objName, subId, arg); } switch (access) { case OAT_POST_ALTER: if ((subId & ACL_SET) && (subId & ACL_ALTER_SYSTEM)) { if (REGRESS_deny_set_variable && !superuser_arg(GetUserId())) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("permission denied: all privileges %s", objName))); } else if (subId & ACL_SET) { if (REGRESS_deny_set_variable && !superuser_arg(GetUserId())) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("permission denied: set %s", objName))); } else if (subId & ACL_ALTER_SYSTEM) { if (REGRESS_deny_alter_system && !superuser_arg(GetUserId())) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("permission denied: alter system set %s", objName))); } else elog(ERROR, "Unknown ParameterAclRelationId subId: %d", subId); break; default: break; } audit_success("object_access_hook_str", accesstype_to_string(access, subId), pstrdup(objName)); } static void REGRESS_object_access_hook(ObjectAccessType access, Oid classId, Oid objectId, int subId, void *arg) { audit_attempt("object access", accesstype_to_string(access, 0), accesstype_arg_to_string(access, arg)); if (REGRESS_deny_object_access && !superuser_arg(GetUserId())) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("permission denied: %s [%s]", accesstype_to_string(access, 0), accesstype_arg_to_string(access, arg)))); /* Forward to next hook in the chain */ if (next_object_access_hook) (*next_object_access_hook) (access, classId, objectId, subId, arg); audit_success("object access", accesstype_to_string(access, 0), accesstype_arg_to_string(access, arg)); } static bool REGRESS_exec_check_perms(List *rangeTabls, List *rteperminfos, bool do_abort) { bool am_super = superuser_arg(GetUserId()); bool allow = true; audit_attempt("executor check perms", pstrdup("execute"), NULL); /* Perform our check */ allow = !REGRESS_deny_exec_perms || am_super; if (do_abort && !allow) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("permission denied: %s", "execute"))); /* Forward to next hook in the chain */ if (next_exec_check_perms_hook && !(*next_exec_check_perms_hook) (rangeTabls, rteperminfos, do_abort)) allow = false; if (allow) audit_success("executor check perms", pstrdup("execute"), NULL); else audit_failure("executor check perms", pstrdup("execute"), NULL); return allow; } static void REGRESS_utility_command(PlannedStmt *pstmt, const char *queryString, bool readOnlyTree, ProcessUtilityContext context, ParamListInfo params, QueryEnvironment *queryEnv, DestReceiver *dest, QueryCompletion *qc) { Node *parsetree = pstmt->utilityStmt; const char *action = GetCommandTagName(CreateCommandTag(parsetree)); audit_attempt("process utility", pstrdup(action), NULL); /* Check permissions */ if (REGRESS_deny_utility_commands && !superuser_arg(GetUserId())) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("permission denied: %s", action))); /* Forward to next hook in the chain */ if (next_ProcessUtility_hook) (*next_ProcessUtility_hook) (pstmt, queryString, readOnlyTree, context, params, queryEnv, dest, qc); else standard_ProcessUtility(pstmt, queryString, readOnlyTree, context, params, queryEnv, dest, qc); /* We're done */ audit_success("process utility", pstrdup(action), NULL); } static char * accesstype_to_string(ObjectAccessType access, int subId) { const char *type; switch (access) { case OAT_POST_CREATE: type = "create"; break; case OAT_DROP: type = "drop"; break; case OAT_POST_ALTER: type = "alter"; break; case OAT_NAMESPACE_SEARCH: type = "namespace search"; break; case OAT_FUNCTION_EXECUTE: type = "execute"; break; case OAT_TRUNCATE: type = "truncate"; break; default: type = "UNRECOGNIZED ObjectAccessType"; } if ((subId & ACL_SET) && (subId & ACL_ALTER_SYSTEM)) return psprintf("%s (subId=0x%x, all privileges)", type, subId); if (subId & ACL_SET) return psprintf("%s (subId=0x%x, set)", type, subId); if (subId & ACL_ALTER_SYSTEM) return psprintf("%s (subId=0x%x, alter system)", type, subId); return psprintf("%s (subId=0x%x)", type, subId); } static char * accesstype_arg_to_string(ObjectAccessType access, void *arg) { if (arg == NULL) return pstrdup("extra info null"); switch (access) { case OAT_POST_CREATE: { ObjectAccessPostCreate *pc_arg = (ObjectAccessPostCreate *) arg; return pstrdup(pc_arg->is_internal ? "internal" : "explicit"); } break; case OAT_DROP: { ObjectAccessDrop *drop_arg = (ObjectAccessDrop *) arg; return psprintf("%s%s%s%s%s%s", ((drop_arg->dropflags & PERFORM_DELETION_INTERNAL) ? "internal action," : ""), ((drop_arg->dropflags & PERFORM_DELETION_CONCURRENTLY) ? "concurrent drop," : ""), ((drop_arg->dropflags & PERFORM_DELETION_QUIETLY) ? "suppress notices," : ""), ((drop_arg->dropflags & PERFORM_DELETION_SKIP_ORIGINAL) ? "keep original object," : ""), ((drop_arg->dropflags & PERFORM_DELETION_SKIP_EXTENSIONS) ? "keep extensions," : ""), ((drop_arg->dropflags & PERFORM_DELETION_CONCURRENT_LOCK) ? "normal concurrent drop," : "")); } break; case OAT_POST_ALTER: { ObjectAccessPostAlter *pa_arg = (ObjectAccessPostAlter *) arg; return psprintf("%s %s auxiliary object", (pa_arg->is_internal ? "internal" : "explicit"), (OidIsValid(pa_arg->auxiliary_id) ? "with" : "without")); } break; case OAT_NAMESPACE_SEARCH: { ObjectAccessNamespaceSearch *ns_arg = (ObjectAccessNamespaceSearch *) arg; return psprintf("%s, %s", (ns_arg->ereport_on_violation ? "report on violation" : "no report on violation"), (ns_arg->result ? "allowed" : "denied")); } break; case OAT_TRUNCATE: case OAT_FUNCTION_EXECUTE: /* hook takes no arg. */ return pstrdup("unexpected extra info pointer received"); default: return pstrdup("cannot parse extra info for unrecognized access type"); } return pstrdup("unknown"); }