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