diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index a347ee18980..bca09a80416 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -2532,6 +2532,45 @@ host ... radius radiusservers="server1,server2" radiussecrets="""secret one"",""
+
+
+ validator.option
+
+
+
+ Validator modules may define
+ additional configuration options for oauth
+ HBA entries. These validator-specific options are accessible via the
+ validator.* "namespace". For example, a module may
+ register the validator.foo and
+ validator.bar options and define their effects on
+ authentication.
+
+
+ The name, syntax, and behavior of each option
+ are not determined by PostgreSQL; consult the
+ documentation for the validator module in use.
+
+
+
+ A limitation of the current implementation is that unrecognized
+ option names will not be caught until
+ connection time. A pg_ctl reload will succeed, but
+ matching connections will fail:
+
+LOG: connection received: host=[local]
+WARNING: unrecognized authentication option name: "validator.bad"
+DETAIL: The installed validator module ("my_validator") did not define an option named "bad".
+HINT: All OAuth connections matching this line will fail. Correct the option and reload the server configuration.
+CONTEXT: line 2 of configuration file "data/pg_hba.conf"
+
+ Use caution when making changes to validator-specific HBA options in
+ production systems.
+
+
+
+
+
map
diff --git a/doc/src/sgml/oauth-validators.sgml b/doc/src/sgml/oauth-validators.sgml
index 5f29f2be186..7c9e3dd931a 100644
--- a/doc/src/sgml/oauth-validators.sgml
+++ b/doc/src/sgml/oauth-validators.sgml
@@ -251,6 +251,11 @@
delegate_ident_mapping=1 mode, and what additional
configuration is required in order to do so.
+
+ If an implementation provides custom
+ HBA options, the names and syntax of those options should be
+ documented as well.
+
@@ -343,7 +348,8 @@ typedef const OAuthValidatorCallbacks *(*OAuthValidatorModuleInit) (void);
Startup Callback
The startup_cb callback is executed directly after
- loading the module. This callback can be used to set up local state and
+ loading the module. This callback can be used to set up local state,
+ define custom HBA options, and
perform additional initialization if required. If the validator module
has state it can use state->private_data to
store it.
@@ -432,4 +438,217 @@ typedef void (*ValidatorShutdownCB) (ValidatorModuleState *state);
+
+
+ Custom HBA Options
+
+
+ Like other preloaded libraries, validator modules may define
+ custom GUC parameters for user
+ configuration in postgresql.conf. However, it may be
+ desirable to configure behavior at a more granular level (say, for a
+ particular issuer or a group of users) instead of globally.
+
+
+
+ Beginning in PostgreSQL 19, validator
+ implementations may define custom options for use inside
+ pg_hba.conf. These options are then
+ made available to the user
+ as validator.option. The API
+ for registering and retrieving custom options is described below.
+
+
+
+ Options API
+
+ Modules register custom HBA option names during the startup_cb
+ callback, using RegisterOAuthHBAOptions():
+
+
+/*
+ * Register a list of custom option names for use in pg_hba.conf. For each name
+ * "foo" registered here, that option will be provided as "validator.foo" in
+ * the HBA.
+ *
+ * Valid option names consist of alphanumeric ASCII, underscore (_), and hyphen
+ * (-). Invalid option names will be ignored with a WARNING logged at
+ * connection time.
+ *
+ * This function may only be called during the startup_cb callback. Multiple
+ * calls are permitted, which will append to the existing list of registered
+ * options; options cannot be unregistered.
+ *
+ * Parameters:
+ *
+ * - state: the state pointer passed to the startup_cb callback
+ * - num: the number of options in the opts array
+ * - opts: an array of null-terminated option names to register
+ *
+ * The list of option names is copied internally, and the opts array is not
+ * required to remain valid after the call.
+ */
+void RegisterOAuthHBAOptions(ValidatorModuleState *state, int num,
+ const char *opts[]);
+
+
+
+
+ Each option's value, if set, may be later retrieved using
+ GetOAuthHBAOption():
+
+
+/*
+ * Retrieve the string value of an HBA option which was registered via
+ * RegisterOAuthHBAOptions(). Usable only during validate_cb or shutdown_cb.
+ *
+ * If the user has set the corresponding option in pg_hba.conf, this function
+ * returns that value as a null-terminated string, which must not be modified
+ * or freed. NULL is returned instead if the user has not set this option, if
+ * the option name was not registered, or if this function is incorrectly called
+ * during the startup_cb.
+ *
+ * Parameters:
+ *
+ * - state: the state pointer passed to the validate_cb/shutdown_cb callback
+ * - optname: the name of the option to retrieve
+ */
+const char *GetOAuthHBAOption(const ValidatorModuleState *state,
+ const char *optname);
+
+
+
+
+ See for sample usage.
+
+
+
+
+ Limitations
+
+
+
+
+ Option names are limited to ASCII alphanumeric characters,
+ underscores (_), and hyphens (-).
+
+
+
+
+ Option values are always freeform strings (in contrast to custom GUCs,
+ which support numerics, booleans, and enums).
+
+
+
+
+ Option names and values cannot be checked by the server during a reload of
+ the configuration. Any unregistered options in pg_hba.conf
+ will instead result in connection failures. It is the responsibility of
+ each module to document and verify the syntax of option values as needed.
+
+
+ If a module finds an invalid option value during validate_cb,
+ it's recommended to signal
+ an internal error by setting result->error_detail
+ to a description of the problem and returning false.
+
+
+
+
+
+
+
+
+
+ Example Usage
+
+
+ For a hypothetical module, the options foo and
+ bar could be registered as follows:
+
+
+static void
+validator_startup(ValidatorModuleState *state)
+{
+ static const char *opts[] = {
+ "foo", /* description of access privileges */
+ "bar", /* magic URL for additional administrator powers */
+ };
+
+ RegisterOAuthHBAOptions(state, lengthof(opts), opts);
+
+ /* ...other setup... */
+}
+
+
+
+
+ The following sample entries in pg_hba.conf can then
+ make use of these options:
+
+
+# TYPE DATABASE USER ADDRESS METHOD
+hostssl postgres admin 0.0.0.0/0 oauth issuer=https://admin.example.com \
+ scope="pg-admin openid email" \
+ map=oauth-email \
+ validator.foo="admin access" \
+ validator.bar=https://magic.example.com
+
+hostssl postgres all 0.0.0.0/0 oauth issuer=https://www.example.com \
+ scope="pg-user openid email" \
+ map=oauth-email \
+ validator.foo="user access"
+
+
+
+
+ The module can retrieve the option settings from the HBA during validation:
+
+
+static bool
+validate_token(const ValidatorModuleState *state,
+ const char *token, const char *role,
+ ValidatorModuleResult *res)
+{
+ const char *foo = GetOAuthHBAOption(state, "foo"); /* "admin access" or "user access" */
+ const char *bar = GetOAuthHBAOption(state, "bar"); /* "https://magic.example.com" or NULL */
+
+ if (bar && !is_valid_url(bar))
+ {
+ res->error_detail = psprintf("validator.bar (\"%s\") is not a valid URL.", bar);
+ return false;
+ }
+
+ /* proceed to validate token */
+}
+
+
+
+
+ When multiple validators are in use, their registered option lists remain
+ independent:
+
+
+in postgresql.conf:
+oauth_validator_libraries = 'example_org, my_validator'
+
+in pg_hba.conf:
+# TYPE DATABASE USER ADDRESS METHOD
+hostssl postgres admin 0.0.0.0/0 oauth issuer=https://admin.example.com \
+ scope="pg-admin openid email" \
+ map=oauth-email \
+ validator=my_validator \
+ validator.foo="admin access" \
+ validator.bar=https://magic.example.com
+
+hostssl postgres all 0.0.0.0/0 oauth issuer=https://www.example.org \
+ scope="pg-user openid profile" \
+ validator=example_org \
+ delegate_ident_mapping=1 \
+ validator.magic=on \
+ validator.more_magic=off
+
+
+
+
diff --git a/src/backend/libpq/auth-oauth.c b/src/backend/libpq/auth-oauth.c
index 6a75b79efbf..ea34ebdb733 100644
--- a/src/backend/libpq/auth-oauth.c
+++ b/src/backend/libpq/auth-oauth.c
@@ -25,6 +25,7 @@
#include "libpq/hba.h"
#include "libpq/oauth.h"
#include "libpq/sasl.h"
+#include "miscadmin.h"
#include "storage/fd.h"
#include "storage/ipc.h"
#include "utils/json.h"
@@ -40,10 +41,15 @@ static int oauth_exchange(void *opaq, const char *input, int inputlen,
static void load_validator_library(const char *libname);
static void shutdown_validator_library(void *arg);
+static bool check_validator_hba_options(Port *port, const char **logdetail);
static ValidatorModuleState *validator_module_state;
static const OAuthValidatorCallbacks *ValidatorCallbacks;
+static MemoryContext ValidatorMemoryContext;
+static List *ValidatorOptions;
+static bool ValidatorOptionsChecked;
+
/* Mechanism declaration */
const pg_be_sasl_mech pg_be_oauth_mech = {
.get_mechanisms = oauth_get_mechanisms,
@@ -109,6 +115,9 @@ oauth_init(Port *port, const char *selected_mech, const char *shadow_pass)
errcode(ERRCODE_PROTOCOL_VIOLATION),
errmsg("client selected an invalid SASL authentication mechanism"));
+ /* Save our memory context for later use by client API calls. */
+ ValidatorMemoryContext = CurrentMemoryContext;
+
ctx = palloc0_object(struct oauth_ctx);
ctx->state = OAUTH_STATE_INIT;
@@ -293,6 +302,16 @@ oauth_exchange(void *opaq, const char *input, int inputlen,
errmsg("malformed OAUTHBEARER message"),
errdetail("Message contains additional data after the final terminator."));
+ /*
+ * Make sure all custom HBA options are understood by the validator before
+ * continuing, since we couldn't check them during server start/reload.
+ */
+ if (!check_validator_hba_options(ctx->port, logdetail))
+ {
+ ctx->state = OAUTH_STATE_FINISHED;
+ return PG_SASL_EXCHANGE_FAILURE;
+ }
+
if (auth[0] == '\0')
{
/*
@@ -822,6 +841,9 @@ shutdown_validator_library(void *arg)
{
if (ValidatorCallbacks->shutdown_cb != NULL)
ValidatorCallbacks->shutdown_cb(validator_module_state);
+
+ /* The backing memory for this is about to disappear. */
+ ValidatorOptions = NIL;
}
/*
@@ -907,3 +929,206 @@ done:
return (*err_msg == NULL);
}
+
+/*
+ * Client APIs for validator implementations
+ *
+ * Since we're currently not threaded, we only allow one validator in the
+ * process at a time. So we can make use of globals for now instead of looking
+ * up information using the state pointer. We probably shouldn't assume that the
+ * module hasn't temporarily changed memory contexts on us, though; functions
+ * here should defensively use an appropriate context when making global
+ * allocations.
+ */
+
+/*
+ * Adds to the list of allowed validator.* HBA options. Used during the
+ * startup_cb.
+ */
+void
+RegisterOAuthHBAOptions(ValidatorModuleState *state, int num,
+ const char *opts[])
+{
+ MemoryContext oldcontext;
+
+ if (!state)
+ {
+ Assert(false);
+ return;
+ }
+
+ oldcontext = MemoryContextSwitchTo(ValidatorMemoryContext);
+
+ for (int i = 0; i < num; i++)
+ {
+ if (!valid_oauth_hba_option_name(opts[i]))
+ {
+ /*
+ * The user can't set this option in the HBA, so GetOAuthHBAOption
+ * would always return NULL.
+ */
+ ereport(WARNING,
+ errmsg("HBA option name \"%s\" is invalid and will be ignored",
+ opts[i]),
+ /* translator: the second %s is a function name */
+ errcontext("validator module \"%s\", in call to %s",
+ MyProcPort->hba->oauth_validator,
+ "RegisterOAuthHBAOptions"));
+ continue;
+ }
+
+ ValidatorOptions = lappend(ValidatorOptions, pstrdup(opts[i]));
+ }
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * Wait to validate the HBA against the registered options until later
+ * (see check_validator_hba_options()).
+ *
+ * Delaying allows the validator to make multiple registration calls, to
+ * append to the list; it lets us make the check in a place where we can
+ * report the error without leaking details to the client; and it avoids
+ * exporting the order of operations between HBA matching and the
+ * startup_cb call as an API guarantee. (The last issue may become
+ * relevant with a threaded model.)
+ */
+}
+
+/*
+ * Restrict the names available to custom HBA options, so that we don't
+ * accidentally prevent future syntax extensions to HBA files.
+ */
+bool
+valid_oauth_hba_option_name(const char *name)
+{
+ /*
+ * This list is not incredibly principled, since the goal is just to bound
+ * compatibility guarantees for our HBA parser. Alphanumerics seem
+ * obviously fine, and it's difficult to argue against the punctuation
+ * that's already included in some HBA option names and identifiers.
+ */
+ static const char *name_allowed_set =
+ "abcdefghijklmnopqrstuvwxyz"
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "0123456789_-";
+
+ size_t span;
+
+ if (!name[0])
+ return false;
+
+ span = strspn(name, name_allowed_set);
+ return name[span] == '\0';
+}
+
+/*
+ * Verifies that all validator.* HBA options specified by the user were actually
+ * registered by the validator library in use.
+ */
+static bool
+check_validator_hba_options(Port *port, const char **logdetail)
+{
+ HbaLine *hba = port->hba;
+
+ foreach_ptr(char, key, hba->oauth_opt_keys)
+ {
+ bool found = false;
+
+ /* O(n^2) shouldn't be a problem here in practice. */
+ foreach_ptr(char, optname, ValidatorOptions)
+ {
+ if (strcmp(key, optname) == 0)
+ {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ {
+ /*
+ * Unknown option name. Mirror the error messages in hba.c here,
+ * keeping in mind that the original "validator." prefix was
+ * stripped from the key during parsing.
+ *
+ * Since this is affecting live connections, which is unusual for
+ * HBA, be noisy with a WARNING. (Warnings aren't sent to clients
+ * prior to successful authentication, so this won't disclose the
+ * server config.) It'll duplicate some of the information in the
+ * logdetail, but that should make it hard to miss the connection
+ * between the two.
+ */
+ char *name = psprintf("validator.%s", key);
+
+ *logdetail = psprintf(_("unrecognized authentication option name: \"%s\""),
+ name);
+ ereport(WARNING,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("unrecognized authentication option name: \"%s\"",
+ name),
+ /* translator: the first %s is the name of the module */
+ errdetail("The installed validator module (\"%s\") did not define an option named \"%s\".",
+ hba->oauth_validator, key),
+ errhint("All OAuth connections matching this line will fail. Correct the option and reload the server configuration."),
+ errcontext("line %d of configuration file \"%s\"",
+ hba->linenumber, hba->sourcefile));
+
+ return false;
+ }
+ }
+
+ ValidatorOptionsChecked = true; /* unfetter GetOAuthHBAOption() */
+ return true;
+}
+
+/*
+ * Retrieves the setting for a validator.* HBA option, or NULL if not found.
+ * This may only be used during the validate_cb and shutdown_cb.
+ */
+const char *
+GetOAuthHBAOption(const ValidatorModuleState *state, const char *optname)
+{
+ HbaLine *hba = MyProcPort->hba;
+ ListCell *lc_k;
+ ListCell *lc_v;
+ const char *ret = NULL;
+
+ if (!ValidatorOptionsChecked)
+ {
+ /*
+ * Prevent the startup_cb from retrieving HBA options that it has just
+ * registered. This probably seems strange -- why refuse to hand out
+ * information we already know? -- but this lets us reserve the
+ * ability to perform the startup_cb call earlier, before we know
+ * which HBA line is matched by a connection, without breaking this
+ * API.
+ */
+ return NULL;
+ }
+
+ if (!state || !hba)
+ {
+ Assert(false);
+ return NULL;
+ }
+
+ Assert(list_length(hba->oauth_opt_keys) == list_length(hba->oauth_opt_vals));
+
+ forboth(lc_k, hba->oauth_opt_keys, lc_v, hba->oauth_opt_vals)
+ {
+ const char *key = lfirst(lc_k);
+ const char *val = lfirst(lc_v);
+
+ if (strcmp(key, optname) == 0)
+ {
+ /*
+ * Don't return yet -- when regular HBA options are specified more
+ * than once, the last one wins. Do the same for these options.
+ */
+ ret = val;
+ }
+ }
+
+ return ret;
+}
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 87ee541e880..7694506aaf7 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -2497,6 +2497,32 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
REQUIRE_AUTH_OPTION(uaOAuth, "validator", "oauth");
hbaline->oauth_validator = pstrdup(val);
}
+ else if (strncmp(name, "validator.", strlen("validator.")) == 0)
+ {
+ const char *key = name + strlen("validator.");
+
+ REQUIRE_AUTH_OPTION(uaOAuth, name, "oauth");
+
+ /*
+ * Validator modules may register their own per-HBA-line options.
+ * Unfortunately, since we don't want to require these modules to be
+ * loaded into the postmaster, we don't know if the options are valid
+ * yet and must store them for later. Perform only a basic syntax
+ * check here.
+ */
+ if (!valid_oauth_hba_option_name(key))
+ {
+ ereport(elevel,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("invalid OAuth validator option name: \"%s\"", name),
+ errcontext("line %d of configuration file \"%s\"",
+ line_num, file_name)));
+ return false;
+ }
+
+ hbaline->oauth_opt_keys = lappend(hbaline->oauth_opt_keys, pstrdup(key));
+ hbaline->oauth_opt_vals = lappend(hbaline->oauth_opt_vals, pstrdup(val));
+ }
else if (strcmp(name, "delegate_ident_mapping") == 0)
{
REQUIRE_AUTH_OPTION(uaOAuth, "delegate_ident_mapping", "oauth");
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index c4570ce9b3f..e8898561c8c 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -140,6 +140,8 @@ typedef struct HbaLine
char *oauth_scope;
char *oauth_validator;
bool oauth_skip_usermap;
+ List *oauth_opt_keys;
+ List *oauth_opt_vals;
} HbaLine;
typedef struct IdentLine
diff --git a/src/include/libpq/oauth.h b/src/include/libpq/oauth.h
index 60f493acddd..86f463a284e 100644
--- a/src/include/libpq/oauth.h
+++ b/src/include/libpq/oauth.h
@@ -96,6 +96,17 @@ typedef struct OAuthValidatorCallbacks
ValidatorValidateCB validate_cb;
} OAuthValidatorCallbacks;
+/*
+ * A validator can register a list of custom option names during its startup_cb,
+ * then later retrieve the user settings for each during validation. This
+ * enables per-HBA-line configuration. For more information, refer to the OAuth
+ * validator modules documentation.
+ */
+extern void RegisterOAuthHBAOptions(ValidatorModuleState *state, int num,
+ const char *opts[]);
+extern const char *GetOAuthHBAOption(const ValidatorModuleState *state,
+ const char *optname);
+
/*
* Type of the shared library symbol _PG_oauth_validator_module_init which is
* required for all validator modules. This function will be invoked during
@@ -107,9 +118,7 @@ extern PGDLLEXPORT const OAuthValidatorCallbacks *_PG_oauth_validator_module_ini
/* Implementation */
extern PGDLLIMPORT const pg_be_sasl_mech pg_be_oauth_mech;
-/*
- * Ensure a validator named in the HBA is permitted by the configuration.
- */
extern bool check_oauth_validator(HbaLine *hbaline, int elevel, char **err_msg);
+extern bool valid_oauth_hba_option_name(const char *name);
#endif /* PG_OAUTH_H */
diff --git a/src/test/modules/oauth_validator/t/001_server.pl b/src/test/modules/oauth_validator/t/001_server.pl
index 37c9d511b4b..eb2566c3775 100644
--- a/src/test/modules/oauth_validator/t/001_server.pl
+++ b/src/test/modules/oauth_validator/t/001_server.pl
@@ -620,10 +620,29 @@ $node->connect_fails(
$bgconn->query_safe("ALTER SYSTEM RESET oauth_validator.error_detail");
$bgconn->query_safe("ALTER SYSTEM RESET oauth_validator.internal_error");
+
+# We complain when bad option names are registered, but connections may proceed
+# (since users can't set those options in the HBA anyway).
+$bgconn->query_safe("ALTER SYSTEM RESET oauth_validator.authn_id");
+$bgconn->query_safe("ALTER SYSTEM RESET oauth_validator.authorize_tokens");
+$bgconn->query_safe("ALTER SYSTEM SET oauth_validator.invalid_hba TO true");
+
$node->reload;
$log_start =
$node->wait_for_log(qr/reloading configuration files/, $log_start);
+$node->connect_ok(
+ "$common_connstr user=test",
+ "bad registered HBA option",
+ expected_stderr =>
+ qr@Visit https://example\.com/ and enter the code: postgresuser@,
+ log_like => [
+ qr/WARNING:\s+HBA option name "bad option name" is invalid and will be ignored/,
+ qr/CONTEXT:\s+validator module "validator", in call to RegisterOAuthHBAOptions/,
+ ]);
+
+$bgconn->query_safe("ALTER SYSTEM RESET oauth_validator.invalid_hba");
+
#
# Test user mapping.
#
@@ -692,6 +711,84 @@ $node->reload;
$log_start =
$node->wait_for_log(qr/reloading configuration files/, $log_start);
+$bgconn->quit; # the tests below restart the server
+
+#
+# Test validator-specific HBA options.
+#
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf(
+ 'pg_hba.conf', qq{
+local all test oauth issuer="$issuer" scope="openid postgres" delegate_ident_mapping=1 \\
+ validator.authn_id="ignored" validator.authn_id="other-identity"
+local all testalt oauth issuer="$issuer" scope="openid postgres" validator.log="testalt message"
+});
+
+$node->reload;
+$log_start =
+ $node->wait_for_log(qr/reloading configuration files/, $log_start);
+
+$node->connect_ok(
+ "$common_connstr user=test",
+ "custom HBA setting (test)",
+ expected_stderr =>
+ qr@Visit https://example\.com/ and enter the code: postgresuser@,
+ log_like => [qr/connection authenticated: identity="other-identity"/]);
+$node->connect_ok(
+ "$common_connstr user=testalt",
+ "custom HBA setting (testalt)",
+ expected_stderr =>
+ qr@Visit https://example\.com/ and enter the code: postgresuser@,
+ log_like => [
+ qr/LOG:\s+testalt message/,
+ qr/connection authenticated: identity="testalt"/,
+ ]);
+
+# bad syntax
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf(
+ 'pg_hba.conf', qq{
+local all testalt oauth issuer="$issuer" scope="openid postgres" validator.=1
+});
+
+$log_start = -s $node->logfile;
+$node->restart(fail_ok => 1);
+$node->log_check("empty HBA option name",
+ $log_start,
+ log_like => [qr/invalid OAuth validator option name: "validator\."/]);
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf(
+ 'pg_hba.conf', qq{
+local all testalt oauth issuer="$issuer" scope="openid postgres" validator.@@=1
+});
+
+$log_start = -s $node->logfile;
+$node->restart(fail_ok => 1);
+$node->log_check("invalid HBA option name",
+ $log_start,
+ log_like => [qr/invalid OAuth validator option name: "validator\.@@"/]);
+
+# unknown settings (validation is deferred to connect time)
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf(
+ 'pg_hba.conf', qq{
+local all testalt oauth issuer="$issuer" scope="openid postgres" \\
+ validator.log=ignored validator.bad=1
+});
+$node->restart;
+
+$node->connect_fails(
+ "$common_connstr user=testalt",
+ "bad HBA setting",
+ expected_stderr => qr/OAuth bearer authentication failed/,
+ log_like => [
+ qr/WARNING:\s+unrecognized authentication option name: "validator\.bad"/,
+ qr/FATAL:\s+OAuth bearer authentication failed/,
+ qr/DETAIL:\s+unrecognized authentication option name: "validator\.bad"/,
+ ]);
+
#
# Test multiple validators.
#
diff --git a/src/test/modules/oauth_validator/validator.c b/src/test/modules/oauth_validator/validator.c
index 353e0e0d32a..85fb4c08bf2 100644
--- a/src/test/modules/oauth_validator/validator.c
+++ b/src/test/modules/oauth_validator/validator.c
@@ -42,13 +42,21 @@ static char *authn_id = NULL;
static bool authorize_tokens = true;
static char *error_detail = NULL;
static bool internal_error = false;
+static bool invalid_hba = false;
+
+/* HBA options */
+static const char *hba_opts[] = {
+ "authn_id", /* overrides the default authn_id */
+ "log", /* logs an arbitrary string */
+};
/*---
* Extension entry point. Sets up GUCs for use by tests:
*
* - oauth_validator.authn_id Sets the user identifier to return during token
* validation. Defaults to the username in the
- * startup packet.
+ * startup packet, or the validator.authn_id HBA
+ * option if it is set.
*
* - oauth_validator.authorize_tokens
* Sets whether to successfully validate incoming
@@ -96,6 +104,14 @@ _PG_init(void)
PGC_SIGHUP,
0,
NULL, NULL, NULL);
+ DefineCustomBoolVariable("oauth_validator.invalid_hba",
+ "Should the validator register an invalid option?",
+ NULL,
+ &invalid_hba,
+ false,
+ PGC_SIGHUP,
+ 0,
+ NULL, NULL, NULL);
MarkGUCPrefixReserved("oauth_validator");
}
@@ -124,6 +140,29 @@ validator_startup(ValidatorModuleState *state)
if (state->sversion != PG_VERSION_NUM)
elog(ERROR, "oauth_validator: sversion set to %d", state->sversion);
+ /*
+ * Test the behavior of custom HBA options. Registered options should not
+ * be retrievable during startup (we want to discourage modules from
+ * relying on the relative order of client connections and the
+ * startup_cb).
+ */
+ RegisterOAuthHBAOptions(state, lengthof(hba_opts), hba_opts);
+ for (int i = 0; i < lengthof(hba_opts); i++)
+ {
+ if (GetOAuthHBAOption(state, hba_opts[i]))
+ elog(ERROR,
+ "oauth_validator: GetOAuthValidatorOption(\"%s\") was non-NULL during startup_cb",
+ hba_opts[i]);
+ }
+
+ if (invalid_hba)
+ {
+ /* Register a bad option, which should print a WARNING to the logs. */
+ const char *invalid = "bad option name";
+
+ RegisterOAuthHBAOptions(state, 1, &invalid);
+ }
+
state->private_data = PRIVATE_COOKIE;
}
@@ -141,7 +180,7 @@ validator_shutdown(ValidatorModuleState *state)
/*
* Validator implementation. Logs the incoming data and authorizes the token by
- * default; the behavior can be modified via the module's GUC settings.
+ * default; the behavior can be modified via the module's GUC and HBA settings.
*/
static bool
validate_token(const ValidatorModuleState *state,
@@ -153,6 +192,9 @@ validate_token(const ValidatorModuleState *state,
elog(ERROR, "oauth_validator: private state cookie changed to %p in validate",
state->private_data);
+ if (GetOAuthHBAOption(state, "log"))
+ elog(LOG, "%s", GetOAuthHBAOption(state, "log"));
+
elog(LOG, "oauth_validator: token=\"%s\", role=\"%s\"", token, role);
elog(LOG, "oauth_validator: issuer=\"%s\", scope=\"%s\"",
MyProcPort->hba->oauth_issuer,
@@ -165,6 +207,8 @@ validate_token(const ValidatorModuleState *state,
res->authorized = authorize_tokens;
if (authn_id)
res->authn_id = pstrdup(authn_id);
+ else if (GetOAuthHBAOption(state, "authn_id"))
+ res->authn_id = pstrdup(GetOAuthHBAOption(state, "authn_id"));
else
res->authn_id = pstrdup(role);