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