Line data Source code
1 : /*--------------------------------------------------------------------------
2 : *
3 : * test_oat_hooks.c
4 : * Code for testing mandatory access control (MAC) using object access hooks.
5 : *
6 : * Copyright (c) 2015-2026, PostgreSQL Global Development Group
7 : *
8 : * IDENTIFICATION
9 : * src/test/modules/test_oat_hooks/test_oat_hooks.c
10 : *
11 : * -------------------------------------------------------------------------
12 : */
13 :
14 : #include "postgres.h"
15 :
16 : #include "access/parallel.h"
17 : #include "catalog/dependency.h"
18 : #include "catalog/objectaccess.h"
19 : #include "executor/executor.h"
20 : #include "fmgr.h"
21 : #include "miscadmin.h"
22 : #include "tcop/utility.h"
23 :
24 0 : PG_MODULE_MAGIC;
25 :
26 : /*
27 : * GUCs controlling which operations to deny
28 : */
29 : static bool REGRESS_deny_set_variable = false;
30 : static bool REGRESS_deny_alter_system = false;
31 : static bool REGRESS_deny_object_access = false;
32 : static bool REGRESS_deny_exec_perms = false;
33 : static bool REGRESS_deny_utility_commands = false;
34 : static bool REGRESS_audit = false;
35 :
36 : /*
37 : * GUCs for testing privileges on USERSET and SUSET variables,
38 : * with and without privileges granted prior to module load.
39 : */
40 : static bool REGRESS_userset_variable1 = false;
41 : static bool REGRESS_userset_variable2 = false;
42 : static bool REGRESS_suset_variable1 = false;
43 : static bool REGRESS_suset_variable2 = false;
44 :
45 : /* Saved hook values */
46 : static object_access_hook_type next_object_access_hook = NULL;
47 : static object_access_hook_type_str next_object_access_hook_str = NULL;
48 : static ExecutorCheckPerms_hook_type next_exec_check_perms_hook = NULL;
49 : static ProcessUtility_hook_type next_ProcessUtility_hook = NULL;
50 :
51 : /* Test Object Access Type Hook hooks */
52 : static void REGRESS_object_access_hook_str(ObjectAccessType access,
53 : Oid classId, const char *objName,
54 : int subId, void *arg);
55 : static void REGRESS_object_access_hook(ObjectAccessType access, Oid classId,
56 : Oid objectId, int subId, void *arg);
57 : static bool REGRESS_exec_check_perms(List *rangeTabls, List *rteperminfos, bool do_abort);
58 : static void REGRESS_utility_command(PlannedStmt *pstmt,
59 : const char *queryString, bool readOnlyTree,
60 : ProcessUtilityContext context,
61 : ParamListInfo params,
62 : QueryEnvironment *queryEnv,
63 : DestReceiver *dest, QueryCompletion *qc);
64 :
65 : /* Helper functions */
66 : static char *accesstype_to_string(ObjectAccessType access, int subId);
67 : static char *accesstype_arg_to_string(ObjectAccessType access, void *arg);
68 :
69 :
70 : /*
71 : * Module load callback
72 : */
73 : void
74 0 : _PG_init(void)
75 : {
76 : /*
77 : * test_oat_hooks.deny_set_variable = (on|off)
78 : */
79 0 : DefineCustomBoolVariable("test_oat_hooks.deny_set_variable",
80 : "Deny non-superuser set permissions",
81 : NULL,
82 : ®RESS_deny_set_variable,
83 : false,
84 : PGC_SUSET,
85 : GUC_NOT_IN_SAMPLE,
86 : NULL,
87 : NULL,
88 : NULL);
89 :
90 : /*
91 : * test_oat_hooks.deny_alter_system = (on|off)
92 : */
93 0 : DefineCustomBoolVariable("test_oat_hooks.deny_alter_system",
94 : "Deny non-superuser alter system set permissions",
95 : NULL,
96 : ®RESS_deny_alter_system,
97 : false,
98 : PGC_SUSET,
99 : GUC_NOT_IN_SAMPLE,
100 : NULL,
101 : NULL,
102 : NULL);
103 :
104 : /*
105 : * test_oat_hooks.deny_object_access = (on|off)
106 : */
107 0 : DefineCustomBoolVariable("test_oat_hooks.deny_object_access",
108 : "Deny non-superuser object access permissions",
109 : NULL,
110 : ®RESS_deny_object_access,
111 : false,
112 : PGC_SUSET,
113 : GUC_NOT_IN_SAMPLE,
114 : NULL,
115 : NULL,
116 : NULL);
117 :
118 : /*
119 : * test_oat_hooks.deny_exec_perms = (on|off)
120 : */
121 0 : DefineCustomBoolVariable("test_oat_hooks.deny_exec_perms",
122 : "Deny non-superuser exec permissions",
123 : NULL,
124 : ®RESS_deny_exec_perms,
125 : false,
126 : PGC_SUSET,
127 : GUC_NOT_IN_SAMPLE,
128 : NULL,
129 : NULL,
130 : NULL);
131 :
132 : /*
133 : * test_oat_hooks.deny_utility_commands = (on|off)
134 : */
135 0 : DefineCustomBoolVariable("test_oat_hooks.deny_utility_commands",
136 : "Deny non-superuser utility commands",
137 : NULL,
138 : ®RESS_deny_utility_commands,
139 : false,
140 : PGC_SUSET,
141 : GUC_NOT_IN_SAMPLE,
142 : NULL,
143 : NULL,
144 : NULL);
145 :
146 : /*
147 : * test_oat_hooks.audit = (on|off)
148 : */
149 0 : DefineCustomBoolVariable("test_oat_hooks.audit",
150 : "Turn on/off debug audit messages",
151 : NULL,
152 : ®RESS_audit,
153 : false,
154 : PGC_SUSET,
155 : GUC_NOT_IN_SAMPLE,
156 : NULL,
157 : NULL,
158 : NULL);
159 :
160 : /*
161 : * test_oat_hooks.user_var{1,2} = (on|off)
162 : */
163 0 : DefineCustomBoolVariable("test_oat_hooks.user_var1",
164 : "Dummy parameter settable by public",
165 : NULL,
166 : ®RESS_userset_variable1,
167 : false,
168 : PGC_USERSET,
169 : GUC_NOT_IN_SAMPLE,
170 : NULL,
171 : NULL,
172 : NULL);
173 :
174 0 : DefineCustomBoolVariable("test_oat_hooks.user_var2",
175 : "Dummy parameter settable by public",
176 : NULL,
177 : ®RESS_userset_variable2,
178 : false,
179 : PGC_USERSET,
180 : GUC_NOT_IN_SAMPLE,
181 : NULL,
182 : NULL,
183 : NULL);
184 :
185 : /*
186 : * test_oat_hooks.super_var{1,2} = (on|off)
187 : */
188 0 : DefineCustomBoolVariable("test_oat_hooks.super_var1",
189 : "Dummy parameter settable by superuser",
190 : NULL,
191 : ®RESS_suset_variable1,
192 : false,
193 : PGC_SUSET,
194 : GUC_NOT_IN_SAMPLE,
195 : NULL,
196 : NULL,
197 : NULL);
198 :
199 0 : DefineCustomBoolVariable("test_oat_hooks.super_var2",
200 : "Dummy parameter settable by superuser",
201 : NULL,
202 : ®RESS_suset_variable2,
203 : false,
204 : PGC_SUSET,
205 : GUC_NOT_IN_SAMPLE,
206 : NULL,
207 : NULL,
208 : NULL);
209 :
210 0 : MarkGUCPrefixReserved("test_oat_hooks");
211 :
212 : /* Object access hook */
213 0 : next_object_access_hook = object_access_hook;
214 0 : object_access_hook = REGRESS_object_access_hook;
215 :
216 : /* Object access hook str */
217 0 : next_object_access_hook_str = object_access_hook_str;
218 0 : object_access_hook_str = REGRESS_object_access_hook_str;
219 :
220 : /* DML permission check */
221 0 : next_exec_check_perms_hook = ExecutorCheckPerms_hook;
222 0 : ExecutorCheckPerms_hook = REGRESS_exec_check_perms;
223 :
224 : /* ProcessUtility hook */
225 0 : next_ProcessUtility_hook = ProcessUtility_hook;
226 0 : ProcessUtility_hook = REGRESS_utility_command;
227 0 : }
228 :
229 : static void
230 0 : emit_audit_message(const char *type, const char *hook, char *action, char *objName)
231 : {
232 : /*
233 : * Ensure that audit messages are not duplicated by only emitting them
234 : * from a leader process, not a worker process. This makes the test
235 : * results deterministic even if run with debug_parallel_query = regress.
236 : */
237 0 : if (REGRESS_audit && !IsParallelWorker())
238 : {
239 0 : const char *who = superuser_arg(GetUserId()) ? "superuser" : "non-superuser";
240 :
241 0 : if (objName)
242 0 : ereport(NOTICE,
243 : (errcode(ERRCODE_INTERNAL_ERROR),
244 : errmsg("in %s: %s %s %s [%s]", hook, who, type, action, objName)));
245 : else
246 0 : ereport(NOTICE,
247 : (errcode(ERRCODE_INTERNAL_ERROR),
248 : errmsg("in %s: %s %s %s", hook, who, type, action)));
249 0 : }
250 :
251 0 : if (action)
252 0 : pfree(action);
253 0 : if (objName)
254 0 : pfree(objName);
255 0 : }
256 :
257 : static void
258 0 : audit_attempt(const char *hook, char *action, char *objName)
259 : {
260 0 : emit_audit_message("attempting", hook, action, objName);
261 0 : }
262 :
263 : static void
264 0 : audit_success(const char *hook, char *action, char *objName)
265 : {
266 0 : emit_audit_message("finished", hook, action, objName);
267 0 : }
268 :
269 : static void
270 0 : audit_failure(const char *hook, char *action, char *objName)
271 : {
272 0 : emit_audit_message("denied", hook, action, objName);
273 0 : }
274 :
275 : static void
276 0 : REGRESS_object_access_hook_str(ObjectAccessType access, Oid classId, const char *objName, int subId, void *arg)
277 : {
278 0 : audit_attempt("object_access_hook_str",
279 0 : accesstype_to_string(access, subId),
280 0 : pstrdup(objName));
281 :
282 0 : if (next_object_access_hook_str)
283 : {
284 0 : (*next_object_access_hook_str) (access, classId, objName, subId, arg);
285 0 : }
286 :
287 0 : switch (access)
288 : {
289 : case OAT_POST_ALTER:
290 0 : if ((subId & ACL_SET) && (subId & ACL_ALTER_SYSTEM))
291 : {
292 0 : if (REGRESS_deny_set_variable && !superuser_arg(GetUserId()))
293 0 : ereport(ERROR,
294 : (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
295 : errmsg("permission denied: all privileges %s", objName)));
296 0 : }
297 0 : else if (subId & ACL_SET)
298 : {
299 0 : if (REGRESS_deny_set_variable && !superuser_arg(GetUserId()))
300 0 : ereport(ERROR,
301 : (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
302 : errmsg("permission denied: set %s", objName)));
303 0 : }
304 0 : else if (subId & ACL_ALTER_SYSTEM)
305 : {
306 0 : if (REGRESS_deny_alter_system && !superuser_arg(GetUserId()))
307 0 : ereport(ERROR,
308 : (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
309 : errmsg("permission denied: alter system set %s", objName)));
310 0 : }
311 : else
312 0 : elog(ERROR, "Unknown ParameterAclRelationId subId: %d", subId);
313 0 : break;
314 : default:
315 0 : break;
316 : }
317 :
318 0 : audit_success("object_access_hook_str",
319 0 : accesstype_to_string(access, subId),
320 0 : pstrdup(objName));
321 0 : }
322 :
323 : static void
324 0 : REGRESS_object_access_hook(ObjectAccessType access, Oid classId, Oid objectId, int subId, void *arg)
325 : {
326 0 : audit_attempt("object access",
327 0 : accesstype_to_string(access, 0),
328 0 : accesstype_arg_to_string(access, arg));
329 :
330 0 : if (REGRESS_deny_object_access && !superuser_arg(GetUserId()))
331 0 : ereport(ERROR,
332 : (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
333 : errmsg("permission denied: %s [%s]",
334 : accesstype_to_string(access, 0),
335 : accesstype_arg_to_string(access, arg))));
336 :
337 : /* Forward to next hook in the chain */
338 0 : if (next_object_access_hook)
339 0 : (*next_object_access_hook) (access, classId, objectId, subId, arg);
340 :
341 0 : audit_success("object access",
342 0 : accesstype_to_string(access, 0),
343 0 : accesstype_arg_to_string(access, arg));
344 0 : }
345 :
346 : static bool
347 0 : REGRESS_exec_check_perms(List *rangeTabls, List *rteperminfos, bool do_abort)
348 : {
349 0 : bool am_super = superuser_arg(GetUserId());
350 0 : bool allow = true;
351 :
352 0 : audit_attempt("executor check perms", pstrdup("execute"), NULL);
353 :
354 : /* Perform our check */
355 0 : allow = !REGRESS_deny_exec_perms || am_super;
356 0 : if (do_abort && !allow)
357 0 : ereport(ERROR,
358 : (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
359 : errmsg("permission denied: %s", "execute")));
360 :
361 : /* Forward to next hook in the chain */
362 0 : if (next_exec_check_perms_hook &&
363 0 : !(*next_exec_check_perms_hook) (rangeTabls, rteperminfos, do_abort))
364 0 : allow = false;
365 :
366 0 : if (allow)
367 0 : audit_success("executor check perms",
368 0 : pstrdup("execute"),
369 : NULL);
370 : else
371 0 : audit_failure("executor check perms",
372 0 : pstrdup("execute"),
373 : NULL);
374 :
375 0 : return allow;
376 0 : }
377 :
378 : static void
379 0 : REGRESS_utility_command(PlannedStmt *pstmt,
380 : const char *queryString,
381 : bool readOnlyTree,
382 : ProcessUtilityContext context,
383 : ParamListInfo params,
384 : QueryEnvironment *queryEnv,
385 : DestReceiver *dest,
386 : QueryCompletion *qc)
387 : {
388 0 : Node *parsetree = pstmt->utilityStmt;
389 0 : const char *action = GetCommandTagName(CreateCommandTag(parsetree));
390 :
391 0 : audit_attempt("process utility",
392 0 : pstrdup(action),
393 : NULL);
394 :
395 : /* Check permissions */
396 0 : if (REGRESS_deny_utility_commands && !superuser_arg(GetUserId()))
397 0 : ereport(ERROR,
398 : (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
399 : errmsg("permission denied: %s", action)));
400 :
401 : /* Forward to next hook in the chain */
402 0 : if (next_ProcessUtility_hook)
403 0 : (*next_ProcessUtility_hook) (pstmt, queryString, readOnlyTree,
404 0 : context, params, queryEnv,
405 0 : dest, qc);
406 : else
407 0 : standard_ProcessUtility(pstmt, queryString, readOnlyTree,
408 0 : context, params, queryEnv,
409 0 : dest, qc);
410 :
411 : /* We're done */
412 0 : audit_success("process utility",
413 0 : pstrdup(action),
414 : NULL);
415 0 : }
416 :
417 : static char *
418 0 : accesstype_to_string(ObjectAccessType access, int subId)
419 : {
420 0 : const char *type;
421 :
422 0 : switch (access)
423 : {
424 : case OAT_POST_CREATE:
425 0 : type = "create";
426 0 : break;
427 : case OAT_DROP:
428 0 : type = "drop";
429 0 : break;
430 : case OAT_POST_ALTER:
431 0 : type = "alter";
432 0 : break;
433 : case OAT_NAMESPACE_SEARCH:
434 0 : type = "namespace search";
435 0 : break;
436 : case OAT_FUNCTION_EXECUTE:
437 0 : type = "execute";
438 0 : break;
439 : case OAT_TRUNCATE:
440 0 : type = "truncate";
441 0 : break;
442 : default:
443 0 : type = "UNRECOGNIZED ObjectAccessType";
444 0 : }
445 :
446 0 : if ((subId & ACL_SET) && (subId & ACL_ALTER_SYSTEM))
447 0 : return psprintf("%s (subId=0x%x, all privileges)", type, subId);
448 0 : if (subId & ACL_SET)
449 0 : return psprintf("%s (subId=0x%x, set)", type, subId);
450 0 : if (subId & ACL_ALTER_SYSTEM)
451 0 : return psprintf("%s (subId=0x%x, alter system)", type, subId);
452 :
453 0 : return psprintf("%s (subId=0x%x)", type, subId);
454 0 : }
455 :
456 : static char *
457 0 : accesstype_arg_to_string(ObjectAccessType access, void *arg)
458 : {
459 0 : if (arg == NULL)
460 0 : return pstrdup("extra info null");
461 :
462 0 : switch (access)
463 : {
464 : case OAT_POST_CREATE:
465 : {
466 0 : ObjectAccessPostCreate *pc_arg = (ObjectAccessPostCreate *) arg;
467 :
468 0 : return pstrdup(pc_arg->is_internal ? "internal" : "explicit");
469 0 : }
470 : break;
471 : case OAT_DROP:
472 : {
473 0 : ObjectAccessDrop *drop_arg = (ObjectAccessDrop *) arg;
474 :
475 0 : return psprintf("%s%s%s%s%s%s",
476 0 : ((drop_arg->dropflags & PERFORM_DELETION_INTERNAL)
477 : ? "internal action," : ""),
478 0 : ((drop_arg->dropflags & PERFORM_DELETION_CONCURRENTLY)
479 : ? "concurrent drop," : ""),
480 0 : ((drop_arg->dropflags & PERFORM_DELETION_QUIETLY)
481 : ? "suppress notices," : ""),
482 0 : ((drop_arg->dropflags & PERFORM_DELETION_SKIP_ORIGINAL)
483 : ? "keep original object," : ""),
484 0 : ((drop_arg->dropflags & PERFORM_DELETION_SKIP_EXTENSIONS)
485 : ? "keep extensions," : ""),
486 0 : ((drop_arg->dropflags & PERFORM_DELETION_CONCURRENT_LOCK)
487 : ? "normal concurrent drop," : ""));
488 0 : }
489 : break;
490 : case OAT_POST_ALTER:
491 : {
492 0 : ObjectAccessPostAlter *pa_arg = (ObjectAccessPostAlter *) arg;
493 :
494 0 : return psprintf("%s %s auxiliary object",
495 0 : (pa_arg->is_internal ? "internal" : "explicit"),
496 0 : (OidIsValid(pa_arg->auxiliary_id) ? "with" : "without"));
497 0 : }
498 : break;
499 : case OAT_NAMESPACE_SEARCH:
500 : {
501 0 : ObjectAccessNamespaceSearch *ns_arg = (ObjectAccessNamespaceSearch *) arg;
502 :
503 0 : return psprintf("%s, %s",
504 0 : (ns_arg->ereport_on_violation ? "report on violation" : "no report on violation"),
505 0 : (ns_arg->result ? "allowed" : "denied"));
506 0 : }
507 : break;
508 : case OAT_TRUNCATE:
509 : case OAT_FUNCTION_EXECUTE:
510 : /* hook takes no arg. */
511 0 : return pstrdup("unexpected extra info pointer received");
512 : default:
513 0 : return pstrdup("cannot parse extra info for unrecognized access type");
514 : }
515 :
516 : return pstrdup("unknown");
517 0 : }
|