Line data Source code
1 : /*-------------------------------------------------------------------------
2 : *
3 : * option.c
4 : * FDW and GUC option handling for postgres_fdw
5 : *
6 : * Portions Copyright (c) 2012-2026, PostgreSQL Global Development Group
7 : *
8 : * IDENTIFICATION
9 : * contrib/postgres_fdw/option.c
10 : *
11 : *-------------------------------------------------------------------------
12 : */
13 : #include "postgres.h"
14 :
15 : #include "access/reloptions.h"
16 : #include "catalog/pg_foreign_server.h"
17 : #include "catalog/pg_foreign_table.h"
18 : #include "catalog/pg_user_mapping.h"
19 : #include "commands/defrem.h"
20 : #include "commands/extension.h"
21 : #include "libpq/libpq-be.h"
22 : #include "postgres_fdw.h"
23 : #include "utils/guc.h"
24 : #include "utils/memutils.h"
25 : #include "utils/varlena.h"
26 :
27 : /*
28 : * Describes the valid options for objects that this wrapper uses.
29 : */
30 : typedef struct PgFdwOption
31 : {
32 : const char *keyword;
33 : Oid optcontext; /* OID of catalog in which option may appear */
34 : bool is_libpq_opt; /* true if it's used in libpq */
35 : } PgFdwOption;
36 :
37 : /*
38 : * Valid options for postgres_fdw.
39 : * Allocated and filled in InitPgFdwOptions.
40 : */
41 : static PgFdwOption *postgres_fdw_options;
42 :
43 : /*
44 : * GUC parameters
45 : */
46 : char *pgfdw_application_name = NULL;
47 :
48 : /*
49 : * Helper functions
50 : */
51 : static void InitPgFdwOptions(void);
52 : static bool is_valid_option(const char *keyword, Oid context);
53 : static bool is_libpq_option(const char *keyword);
54 :
55 : #include "miscadmin.h"
56 :
57 : /*
58 : * Validate the generic options given to a FOREIGN DATA WRAPPER, SERVER,
59 : * USER MAPPING or FOREIGN TABLE that uses postgres_fdw.
60 : *
61 : * Raise an ERROR if the option or its value is considered invalid.
62 : */
63 22 : PG_FUNCTION_INFO_V1(postgres_fdw_validator);
64 :
65 : Datum
66 328 : postgres_fdw_validator(PG_FUNCTION_ARGS)
67 : {
68 328 : List *options_list = untransformRelOptions(PG_GETARG_DATUM(0));
69 328 : Oid catalog = PG_GETARG_OID(1);
70 : ListCell *cell;
71 :
72 : /* Build our options lists if we didn't yet. */
73 328 : InitPgFdwOptions();
74 :
75 : /*
76 : * Check that only options supported by postgres_fdw, and allowed for the
77 : * current object type, are given.
78 : */
79 1061 : foreach(cell, options_list)
80 : {
81 749 : DefElem *def = (DefElem *) lfirst(cell);
82 :
83 749 : if (!is_valid_option(def->defname, catalog))
84 : {
85 : /*
86 : * Unknown option specified, complain about it. Provide a hint
87 : * with a valid option that looks similar, if there is one.
88 : */
89 : PgFdwOption *opt;
90 : const char *closest_match;
91 : ClosestMatchState match_state;
92 7 : bool has_valid_options = false;
93 :
94 7 : initClosestMatch(&match_state, def->defname, 4);
95 511 : for (opt = postgres_fdw_options; opt->keyword; opt++)
96 : {
97 504 : if (catalog == opt->optcontext)
98 : {
99 183 : has_valid_options = true;
100 183 : updateClosestMatch(&match_state, opt->keyword);
101 : }
102 : }
103 :
104 7 : closest_match = getClosestMatch(&match_state);
105 7 : ereport(ERROR,
106 : (errcode(ERRCODE_FDW_INVALID_OPTION_NAME),
107 : errmsg("invalid option \"%s\"", def->defname),
108 : has_valid_options ? closest_match ?
109 : errhint("Perhaps you meant the option \"%s\".",
110 : closest_match) : 0 :
111 : errhint("There are no valid options in this context.")));
112 : }
113 :
114 : /*
115 : * Validate option value, when we can do so without any context.
116 : */
117 742 : if (strcmp(def->defname, "use_remote_estimate") == 0 ||
118 703 : strcmp(def->defname, "updatable") == 0 ||
119 697 : strcmp(def->defname, "truncatable") == 0 ||
120 692 : strcmp(def->defname, "async_capable") == 0 ||
121 688 : strcmp(def->defname, "parallel_commit") == 0 ||
122 684 : strcmp(def->defname, "parallel_abort") == 0 ||
123 680 : strcmp(def->defname, "keep_connections") == 0 ||
124 662 : strcmp(def->defname, "restore_stats") == 0 ||
125 659 : strcmp(def->defname, "use_scram_passthrough") == 0)
126 : {
127 : /* these accept only boolean values */
128 87 : (void) defGetBoolean(def);
129 : }
130 655 : else if (strcmp(def->defname, "fdw_startup_cost") == 0 ||
131 646 : strcmp(def->defname, "fdw_tuple_cost") == 0)
132 15 : {
133 : /*
134 : * These must have a floating point value greater than or equal to
135 : * zero.
136 : */
137 : char *value;
138 : double real_val;
139 : bool is_parsed;
140 :
141 17 : value = defGetString(def);
142 17 : is_parsed = parse_real(value, &real_val, 0, NULL);
143 :
144 17 : if (!is_parsed)
145 2 : ereport(ERROR,
146 : (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
147 : errmsg("invalid value for floating point option \"%s\": %s",
148 : def->defname, value)));
149 :
150 15 : if (real_val < 0)
151 0 : ereport(ERROR,
152 : (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
153 : errmsg("\"%s\" must be a floating point value greater than or equal to zero",
154 : def->defname)));
155 : }
156 638 : else if (strcmp(def->defname, "extensions") == 0)
157 : {
158 : /* check list syntax, warn about uninstalled extensions */
159 33 : (void) ExtractExtensionList(defGetString(def), true);
160 : }
161 605 : else if (strcmp(def->defname, "fetch_size") == 0 ||
162 600 : strcmp(def->defname, "batch_size") == 0)
163 24 : {
164 : char *value;
165 : int int_val;
166 : bool is_parsed;
167 :
168 26 : value = defGetString(def);
169 26 : is_parsed = parse_int(value, &int_val, 0, NULL);
170 :
171 26 : if (!is_parsed)
172 2 : ereport(ERROR,
173 : (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
174 : errmsg("invalid value for integer option \"%s\": %s",
175 : def->defname, value)));
176 :
177 24 : if (int_val <= 0)
178 0 : ereport(ERROR,
179 : (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
180 : errmsg("\"%s\" must be an integer value greater than zero",
181 : def->defname)));
182 : }
183 579 : else if (strcmp(def->defname, "password_required") == 0)
184 : {
185 6 : bool pw_required = defGetBoolean(def);
186 :
187 : /*
188 : * Only the superuser may set this option on a user mapping, or
189 : * alter a user mapping on which this option is set. We allow a
190 : * user to clear this option if it's set - in fact, we don't have
191 : * a choice since we can't see the old mapping when validating an
192 : * alter.
193 : */
194 6 : if (!superuser() && !pw_required)
195 1 : ereport(ERROR,
196 : (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
197 : errmsg("password_required=false is superuser-only"),
198 : errhint("User mappings with the password_required option set to false may only be created or modified by the superuser.")));
199 : }
200 573 : else if (strcmp(def->defname, "sslcert") == 0 ||
201 563 : strcmp(def->defname, "sslkey") == 0)
202 : {
203 : /* similarly for sslcert / sslkey on user mapping */
204 20 : if (catalog == UserMappingRelationId && !superuser())
205 2 : ereport(ERROR,
206 : (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
207 : errmsg("sslcert and sslkey are superuser-only"),
208 : errhint("User mappings with the sslcert or sslkey options set may only be created or modified by the superuser.")));
209 : }
210 553 : else if (strcmp(def->defname, "analyze_sampling") == 0)
211 : {
212 : char *value;
213 :
214 7 : value = defGetString(def);
215 :
216 : /* we recognize off/auto/random/system/bernoulli */
217 7 : if (strcmp(value, "off") != 0 &&
218 5 : strcmp(value, "auto") != 0 &&
219 4 : strcmp(value, "random") != 0 &&
220 3 : strcmp(value, "system") != 0 &&
221 2 : strcmp(value, "bernoulli") != 0)
222 1 : ereport(ERROR,
223 : (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
224 : errmsg("invalid value for string option \"%s\": %s",
225 : def->defname, value)));
226 : }
227 : }
228 :
229 312 : PG_RETURN_VOID();
230 : }
231 :
232 : /*
233 : * Initialize option lists.
234 : */
235 : static void
236 520 : InitPgFdwOptions(void)
237 : {
238 : int num_libpq_opts;
239 : PQconninfoOption *libpq_options;
240 : PQconninfoOption *lopt;
241 : PgFdwOption *popt;
242 :
243 : /* non-libpq FDW-specific FDW options */
244 : static const PgFdwOption non_libpq_options[] = {
245 : {"schema_name", ForeignTableRelationId, false},
246 : {"table_name", ForeignTableRelationId, false},
247 : {"column_name", AttributeRelationId, false},
248 : /* use_remote_estimate is available on both server and table */
249 : {"use_remote_estimate", ForeignServerRelationId, false},
250 : {"use_remote_estimate", ForeignTableRelationId, false},
251 : /* cost factors */
252 : {"fdw_startup_cost", ForeignServerRelationId, false},
253 : {"fdw_tuple_cost", ForeignServerRelationId, false},
254 : /* shippable extensions */
255 : {"extensions", ForeignServerRelationId, false},
256 : /* updatable is available on both server and table */
257 : {"updatable", ForeignServerRelationId, false},
258 : {"updatable", ForeignTableRelationId, false},
259 : /* truncatable is available on both server and table */
260 : {"truncatable", ForeignServerRelationId, false},
261 : {"truncatable", ForeignTableRelationId, false},
262 : /* fetch_size is available on both server and table */
263 : {"fetch_size", ForeignServerRelationId, false},
264 : {"fetch_size", ForeignTableRelationId, false},
265 : /* batch_size is available on both server and table */
266 : {"batch_size", ForeignServerRelationId, false},
267 : {"batch_size", ForeignTableRelationId, false},
268 : /* async_capable is available on both server and table */
269 : {"async_capable", ForeignServerRelationId, false},
270 : {"async_capable", ForeignTableRelationId, false},
271 : {"parallel_commit", ForeignServerRelationId, false},
272 : {"parallel_abort", ForeignServerRelationId, false},
273 : {"keep_connections", ForeignServerRelationId, false},
274 : {"password_required", UserMappingRelationId, false},
275 :
276 : /* sampling is available on both server and table */
277 : {"analyze_sampling", ForeignServerRelationId, false},
278 : {"analyze_sampling", ForeignTableRelationId, false},
279 : /* restore_stats is available on both server and table */
280 : {"restore_stats", ForeignServerRelationId, false},
281 : {"restore_stats", ForeignTableRelationId, false},
282 :
283 : {"use_scram_passthrough", ForeignServerRelationId, false},
284 : {"use_scram_passthrough", UserMappingRelationId, false},
285 :
286 : /*
287 : * sslcert and sslkey are in fact libpq options, but we repeat them
288 : * here to allow them to appear in both foreign server context (when
289 : * we generate libpq options) and user mapping context (from here).
290 : */
291 : {"sslcert", UserMappingRelationId, true},
292 : {"sslkey", UserMappingRelationId, true},
293 :
294 : /*
295 : * gssdelegation is also a libpq option but should be allowed in a
296 : * user mapping context too
297 : */
298 : {"gssdelegation", UserMappingRelationId, true},
299 :
300 : {NULL, InvalidOid, false}
301 : };
302 :
303 : /* Prevent redundant initialization. */
304 520 : if (postgres_fdw_options)
305 487 : return;
306 :
307 : /*
308 : * Get list of valid libpq options.
309 : *
310 : * To avoid unnecessary work, we get the list once and use it throughout
311 : * the lifetime of this backend process. Hence, we'll allocate it in
312 : * TopMemoryContext.
313 : */
314 33 : libpq_options = PQconndefaults();
315 33 : if (!libpq_options) /* assume reason for failure is OOM */
316 0 : ereport(ERROR,
317 : (errcode(ERRCODE_FDW_OUT_OF_MEMORY),
318 : errmsg("out of memory"),
319 : errdetail("Could not get libpq's default connection options.")));
320 :
321 : /* Count how many libpq options are available. */
322 33 : num_libpq_opts = 0;
323 1749 : for (lopt = libpq_options; lopt->keyword; lopt++)
324 1716 : num_libpq_opts++;
325 :
326 : /*
327 : * Construct an array which consists of all valid options for
328 : * postgres_fdw, by appending FDW-specific options to libpq options.
329 : */
330 33 : postgres_fdw_options = (PgFdwOption *)
331 33 : MemoryContextAlloc(TopMemoryContext,
332 33 : sizeof(PgFdwOption) * num_libpq_opts +
333 : sizeof(non_libpq_options));
334 :
335 33 : popt = postgres_fdw_options;
336 1749 : for (lopt = libpq_options; lopt->keyword; lopt++)
337 : {
338 : /* Hide debug options, as well as settings we override internally. */
339 1716 : if (strchr(lopt->dispchar, 'D') ||
340 1584 : strcmp(lopt->keyword, "fallback_application_name") == 0 ||
341 1551 : strcmp(lopt->keyword, "client_encoding") == 0)
342 198 : continue;
343 :
344 : /*
345 : * Disallow OAuth options for now, since the builtin flow communicates
346 : * on stderr by default and can't cache tokens yet.
347 : */
348 1518 : if (strncmp(lopt->keyword, "oauth_", strlen("oauth_")) == 0)
349 165 : continue;
350 :
351 2706 : popt->keyword = MemoryContextStrdup(TopMemoryContext,
352 1353 : lopt->keyword);
353 :
354 : /*
355 : * "user" and any secret options are allowed only on user mappings.
356 : * Everything else is a server option.
357 : */
358 1353 : if (strcmp(lopt->keyword, "user") == 0 || strchr(lopt->dispchar, '*'))
359 99 : popt->optcontext = UserMappingRelationId;
360 : else
361 1254 : popt->optcontext = ForeignServerRelationId;
362 1353 : popt->is_libpq_opt = true;
363 :
364 1353 : popt++;
365 : }
366 :
367 : /* Done with libpq's output structure. */
368 33 : PQconninfoFree(libpq_options);
369 :
370 : /* Append FDW-specific options and dummy terminator. */
371 33 : memcpy(popt, non_libpq_options, sizeof(non_libpq_options));
372 : }
373 :
374 : /*
375 : * Check whether the given option is one of the valid postgres_fdw options.
376 : * context is the Oid of the catalog holding the object the option is for.
377 : */
378 : static bool
379 749 : is_valid_option(const char *keyword, Oid context)
380 : {
381 : PgFdwOption *opt;
382 :
383 : Assert(postgres_fdw_options); /* must be initialized already */
384 :
385 24822 : for (opt = postgres_fdw_options; opt->keyword; opt++)
386 : {
387 24815 : if (context == opt->optcontext && strcmp(opt->keyword, keyword) == 0)
388 742 : return true;
389 : }
390 :
391 7 : return false;
392 : }
393 :
394 : /*
395 : * Check whether the given option is one of the valid libpq options.
396 : */
397 : static bool
398 406 : is_libpq_option(const char *keyword)
399 : {
400 : PgFdwOption *opt;
401 :
402 : Assert(postgres_fdw_options); /* must be initialized already */
403 :
404 12141 : for (opt = postgres_fdw_options; opt->keyword; opt++)
405 : {
406 12018 : if (opt->is_libpq_opt && strcmp(opt->keyword, keyword) == 0)
407 283 : return true;
408 : }
409 :
410 123 : return false;
411 : }
412 :
413 : /*
414 : * Generate key-value arrays which include only libpq options from the
415 : * given list (which can contain any kind of options). Caller must have
416 : * allocated large-enough arrays. Returns number of options found.
417 : */
418 : int
419 192 : ExtractConnectionOptions(List *defelems, const char **keywords,
420 : const char **values)
421 : {
422 : ListCell *lc;
423 : int i;
424 :
425 : /* Build our options lists if we didn't yet. */
426 192 : InitPgFdwOptions();
427 :
428 192 : i = 0;
429 598 : foreach(lc, defelems)
430 : {
431 406 : DefElem *d = (DefElem *) lfirst(lc);
432 :
433 406 : if (is_libpq_option(d->defname))
434 : {
435 283 : keywords[i] = d->defname;
436 283 : values[i] = defGetString(d);
437 283 : i++;
438 : }
439 : }
440 192 : return i;
441 : }
442 :
443 : /*
444 : * Parse a comma-separated string and return a List of the OIDs of the
445 : * extensions named in the string. If any names in the list cannot be
446 : * found, report a warning if warnOnMissing is true, else just silently
447 : * ignore them.
448 : */
449 : List *
450 963 : ExtractExtensionList(const char *extensionsString, bool warnOnMissing)
451 : {
452 963 : List *extensionOids = NIL;
453 : List *extlist;
454 : ListCell *lc;
455 :
456 : /* SplitIdentifierString scribbles on its input, so pstrdup first */
457 963 : if (!SplitIdentifierString(pstrdup(extensionsString), ',', &extlist))
458 : {
459 : /* syntax error in name list */
460 1 : ereport(ERROR,
461 : (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
462 : errmsg("parameter \"%s\" must be a list of extension names",
463 : "extensions")));
464 : }
465 :
466 1925 : foreach(lc, extlist)
467 : {
468 963 : const char *extension_name = (const char *) lfirst(lc);
469 963 : Oid extension_oid = get_extension_oid(extension_name, true);
470 :
471 963 : if (OidIsValid(extension_oid))
472 : {
473 961 : extensionOids = lappend_oid(extensionOids, extension_oid);
474 : }
475 2 : else if (warnOnMissing)
476 : {
477 2 : ereport(WARNING,
478 : (errcode(ERRCODE_UNDEFINED_OBJECT),
479 : errmsg("extension \"%s\" is not installed",
480 : extension_name)));
481 : }
482 : }
483 :
484 962 : list_free(extlist);
485 962 : return extensionOids;
486 : }
487 :
488 : /*
489 : * Replace escape sequences beginning with % character in the given
490 : * application_name with status information, and return it.
491 : *
492 : * This function always returns a palloc'd string, so the caller is
493 : * responsible for pfreeing it.
494 : */
495 : char *
496 28 : process_pgfdw_appname(const char *appname)
497 : {
498 : const char *p;
499 : StringInfoData buf;
500 :
501 28 : initStringInfo(&buf);
502 :
503 389 : for (p = appname; *p != '\0'; p++)
504 : {
505 361 : if (*p != '%')
506 : {
507 : /* literal char, just copy */
508 352 : appendStringInfoChar(&buf, *p);
509 352 : continue;
510 : }
511 :
512 : /* must be a '%', so skip to the next char */
513 9 : p++;
514 9 : if (*p == '\0')
515 0 : break; /* format error - ignore it */
516 9 : else if (*p == '%')
517 : {
518 : /* string contains %% */
519 1 : appendStringInfoChar(&buf, '%');
520 1 : continue;
521 : }
522 :
523 : /* process the option */
524 8 : switch (*p)
525 : {
526 1 : case 'a':
527 1 : appendStringInfoString(&buf, application_name);
528 1 : break;
529 2 : case 'c':
530 2 : appendStringInfo(&buf, "%" PRIx64 ".%x", MyStartTime, MyProcPid);
531 2 : break;
532 2 : case 'C':
533 2 : appendStringInfoString(&buf, cluster_name);
534 2 : break;
535 1 : case 'd':
536 1 : if (MyProcPort)
537 : {
538 1 : const char *dbname = MyProcPort->database_name;
539 :
540 1 : if (dbname)
541 1 : appendStringInfoString(&buf, dbname);
542 : else
543 0 : appendStringInfoString(&buf, "[unknown]");
544 : }
545 1 : break;
546 1 : case 'p':
547 1 : appendStringInfo(&buf, "%d", MyProcPid);
548 1 : break;
549 1 : case 'u':
550 1 : if (MyProcPort)
551 : {
552 1 : const char *username = MyProcPort->user_name;
553 :
554 1 : if (username)
555 1 : appendStringInfoString(&buf, username);
556 : else
557 0 : appendStringInfoString(&buf, "[unknown]");
558 : }
559 1 : break;
560 0 : default:
561 : /* format error - ignore it */
562 0 : break;
563 : }
564 : }
565 :
566 28 : return buf.data;
567 : }
568 :
569 : /*
570 : * Module load callback
571 : */
572 : void
573 33 : _PG_init(void)
574 : {
575 : /*
576 : * Unlike application_name GUC, don't set GUC_IS_NAME flag nor check_hook
577 : * to allow postgres_fdw.application_name to be any string more than
578 : * NAMEDATALEN characters and to include non-ASCII characters. Instead,
579 : * remote server truncates application_name of remote connection to less
580 : * than NAMEDATALEN and replaces any non-ASCII characters in it with a '?'
581 : * character.
582 : */
583 33 : DefineCustomStringVariable("postgres_fdw.application_name",
584 : "Sets the application name to be used on the remote server.",
585 : NULL,
586 : &pgfdw_application_name,
587 : NULL,
588 : PGC_USERSET,
589 : 0,
590 : NULL,
591 : NULL,
592 : NULL);
593 :
594 33 : MarkGUCPrefixReserved("postgres_fdw");
595 33 : }
|