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-2024, 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 4 : 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 4 : _PG_init(void)
75 : {
76 : /*
77 : * test_oat_hooks.deny_set_variable = (on|off)
78 : */
79 4 : 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 4 : 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 4 : 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 4 : 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 4 : 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 4 : 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 4 : 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 4 : 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 4 : 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 4 : 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 4 : MarkGUCPrefixReserved("test_oat_hooks");
211 :
212 : /* Object access hook */
213 4 : next_object_access_hook = object_access_hook;
214 4 : object_access_hook = REGRESS_object_access_hook;
215 :
216 : /* Object access hook str */
217 4 : next_object_access_hook_str = object_access_hook_str;
218 4 : object_access_hook_str = REGRESS_object_access_hook_str;
219 :
220 : /* DML permission check */
221 4 : next_exec_check_perms_hook = ExecutorCheckPerms_hook;
222 4 : ExecutorCheckPerms_hook = REGRESS_exec_check_perms;
223 :
224 : /* ProcessUtility hook */
225 4 : next_ProcessUtility_hook = ProcessUtility_hook;
226 4 : ProcessUtility_hook = REGRESS_utility_command;
227 4 : }
228 :
229 : static void
230 616 : 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 616 : if (REGRESS_audit && !IsParallelWorker())
238 : {
239 580 : const char *who = superuser_arg(GetUserId()) ? "superuser" : "non-superuser";
240 :
241 580 : if (objName)
242 330 : ereport(NOTICE,
243 : (errcode(ERRCODE_INTERNAL_ERROR),
244 : errmsg("in %s: %s %s %s [%s]", hook, who, type, action, objName)));
245 : else
246 250 : ereport(NOTICE,
247 : (errcode(ERRCODE_INTERNAL_ERROR),
248 : errmsg("in %s: %s %s %s", hook, who, type, action)));
249 : }
250 :
251 616 : if (action)
252 616 : pfree(action);
253 616 : if (objName)
254 346 : pfree(objName);
255 616 : }
256 :
257 : static void
258 320 : audit_attempt(const char *hook, char *action, char *objName)
259 : {
260 320 : emit_audit_message("attempting", hook, action, objName);
261 320 : }
262 :
263 : static void
264 296 : audit_success(const char *hook, char *action, char *objName)
265 : {
266 296 : emit_audit_message("finished", hook, action, objName);
267 296 : }
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 48 : REGRESS_object_access_hook_str(ObjectAccessType access, Oid classId, const char *objName, int subId, void *arg)
277 : {
278 48 : audit_attempt("object_access_hook_str",
279 : accesstype_to_string(access, subId),
280 : pstrdup(objName));
281 :
282 48 : if (next_object_access_hook_str)
283 : {
284 0 : (*next_object_access_hook_str) (access, classId, objName, subId, arg);
285 : }
286 :
287 48 : switch (access)
288 : {
289 48 : case OAT_POST_ALTER:
290 48 : 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 : }
297 48 : else if (subId & ACL_SET)
298 : {
299 40 : if (REGRESS_deny_set_variable && !superuser_arg(GetUserId()))
300 2 : ereport(ERROR,
301 : (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
302 : errmsg("permission denied: set %s", objName)));
303 : }
304 8 : else if (subId & ACL_ALTER_SYSTEM)
305 : {
306 8 : 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 : }
311 : else
312 0 : elog(ERROR, "Unknown ParameterAclRelationId subId: %d", subId);
313 46 : break;
314 0 : default:
315 0 : break;
316 : }
317 :
318 46 : audit_success("object_access_hook_str",
319 : accesstype_to_string(access, subId),
320 : pstrdup(objName));
321 46 : }
322 :
323 : static void
324 126 : REGRESS_object_access_hook(ObjectAccessType access, Oid classId, Oid objectId, int subId, void *arg)
325 : {
326 126 : audit_attempt("object access",
327 : accesstype_to_string(access, 0),
328 : accesstype_arg_to_string(access, arg));
329 :
330 126 : 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 126 : if (next_object_access_hook)
339 0 : (*next_object_access_hook) (access, classId, objectId, subId, arg);
340 :
341 126 : audit_success("object access",
342 : accesstype_to_string(access, 0),
343 : accesstype_arg_to_string(access, arg));
344 126 : }
345 :
346 : static bool
347 12 : REGRESS_exec_check_perms(List *rangeTabls, List *rteperminfos, bool do_abort)
348 : {
349 12 : bool am_super = superuser_arg(GetUserId());
350 12 : bool allow = true;
351 :
352 12 : audit_attempt("executor check perms", pstrdup("execute"), NULL);
353 :
354 : /* Perform our check */
355 12 : allow = !REGRESS_deny_exec_perms || am_super;
356 12 : 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 12 : if (next_exec_check_perms_hook &&
363 0 : !(*next_exec_check_perms_hook) (rangeTabls, rteperminfos, do_abort))
364 0 : allow = false;
365 :
366 12 : if (allow)
367 12 : audit_success("executor check perms",
368 : pstrdup("execute"),
369 : NULL);
370 : else
371 0 : audit_failure("executor check perms",
372 : pstrdup("execute"),
373 : NULL);
374 :
375 12 : return allow;
376 : }
377 :
378 : static void
379 134 : 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 134 : Node *parsetree = pstmt->utilityStmt;
389 134 : const char *action = GetCommandTagName(CreateCommandTag(parsetree));
390 :
391 134 : audit_attempt("process utility",
392 : pstrdup(action),
393 : NULL);
394 :
395 : /* Check permissions */
396 134 : 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 134 : if (next_ProcessUtility_hook)
403 0 : (*next_ProcessUtility_hook) (pstmt, queryString, readOnlyTree,
404 : context, params, queryEnv,
405 : dest, qc);
406 : else
407 134 : standard_ProcessUtility(pstmt, queryString, readOnlyTree,
408 : context, params, queryEnv,
409 : dest, qc);
410 :
411 : /* We're done */
412 112 : audit_success("process utility",
413 : pstrdup(action),
414 : NULL);
415 112 : }
416 :
417 : static char *
418 346 : accesstype_to_string(ObjectAccessType access, int subId)
419 : {
420 : const char *type;
421 :
422 346 : switch (access)
423 : {
424 68 : case OAT_POST_CREATE:
425 68 : type = "create";
426 68 : break;
427 48 : case OAT_DROP:
428 48 : type = "drop";
429 48 : break;
430 142 : case OAT_POST_ALTER:
431 142 : type = "alter";
432 142 : break;
433 88 : case OAT_NAMESPACE_SEARCH:
434 88 : type = "namespace search";
435 88 : break;
436 0 : case OAT_FUNCTION_EXECUTE:
437 0 : type = "execute";
438 0 : break;
439 0 : case OAT_TRUNCATE:
440 0 : type = "truncate";
441 0 : break;
442 0 : default:
443 0 : type = "UNRECOGNIZED ObjectAccessType";
444 : }
445 :
446 346 : if ((subId & ACL_SET) && (subId & ACL_ALTER_SYSTEM))
447 0 : return psprintf("%s (subId=0x%x, all privileges)", type, subId);
448 346 : if (subId & ACL_SET)
449 78 : return psprintf("%s (subId=0x%x, set)", type, subId);
450 268 : if (subId & ACL_ALTER_SYSTEM)
451 16 : return psprintf("%s (subId=0x%x, alter system)", type, subId);
452 :
453 252 : return psprintf("%s (subId=0x%x)", type, subId);
454 : }
455 :
456 : static char *
457 252 : accesstype_arg_to_string(ObjectAccessType access, void *arg)
458 : {
459 252 : if (arg == NULL)
460 0 : return pstrdup("extra info null");
461 :
462 252 : switch (access)
463 : {
464 68 : case OAT_POST_CREATE:
465 : {
466 68 : ObjectAccessPostCreate *pc_arg = (ObjectAccessPostCreate *) arg;
467 :
468 68 : return pstrdup(pc_arg->is_internal ? "internal" : "explicit");
469 : }
470 : break;
471 48 : case OAT_DROP:
472 : {
473 48 : ObjectAccessDrop *drop_arg = (ObjectAccessDrop *) arg;
474 :
475 288 : return psprintf("%s%s%s%s%s%s",
476 48 : ((drop_arg->dropflags & PERFORM_DELETION_INTERNAL)
477 : ? "internal action," : ""),
478 48 : ((drop_arg->dropflags & PERFORM_DELETION_CONCURRENTLY)
479 : ? "concurrent drop," : ""),
480 48 : ((drop_arg->dropflags & PERFORM_DELETION_QUIETLY)
481 : ? "suppress notices," : ""),
482 48 : ((drop_arg->dropflags & PERFORM_DELETION_SKIP_ORIGINAL)
483 : ? "keep original object," : ""),
484 48 : ((drop_arg->dropflags & PERFORM_DELETION_SKIP_EXTENSIONS)
485 : ? "keep extensions," : ""),
486 48 : ((drop_arg->dropflags & PERFORM_DELETION_CONCURRENT_LOCK)
487 : ? "normal concurrent drop," : ""));
488 : }
489 : break;
490 48 : case OAT_POST_ALTER:
491 : {
492 48 : ObjectAccessPostAlter *pa_arg = (ObjectAccessPostAlter *) arg;
493 :
494 96 : return psprintf("%s %s auxiliary object",
495 48 : (pa_arg->is_internal ? "internal" : "explicit"),
496 48 : (OidIsValid(pa_arg->auxiliary_id) ? "with" : "without"));
497 : }
498 : break;
499 88 : case OAT_NAMESPACE_SEARCH:
500 : {
501 88 : ObjectAccessNamespaceSearch *ns_arg = (ObjectAccessNamespaceSearch *) arg;
502 :
503 176 : return psprintf("%s, %s",
504 88 : (ns_arg->ereport_on_violation ? "report on violation" : "no report on violation"),
505 88 : (ns_arg->result ? "allowed" : "denied"));
506 : }
507 : break;
508 0 : case OAT_TRUNCATE:
509 : case OAT_FUNCTION_EXECUTE:
510 : /* hook takes no arg. */
511 0 : return pstrdup("unexpected extra info pointer received");
512 0 : default:
513 0 : return pstrdup("cannot parse extra info for unrecognized access type");
514 : }
515 :
516 : return pstrdup("unknown");
517 : }
|