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