From e723c2040405237155eeca58cab675866763b876 Mon Sep 17 00:00:00 2001 From: Jacob Champion Date: Fri, 7 Feb 2025 14:23:40 -0800 Subject: [PATCH v49 2/4] v48 fixup patches! Add OAUTHBEARER SASL mechanism --- doc/src/sgml/libpq.sgml | 40 +++++++++++++++- doc/src/sgml/oauth-validators.sgml | 31 ++++++++---- src/backend/libpq/auth-oauth.c | 20 ++++++-- src/include/libpq/oauth.h | 48 ++++++++++++++++++- src/interfaces/libpq/fe-auth-oauth-curl.c | 14 +++++- .../modules/oauth_validator/fail_validator.c | 15 ++++-- .../modules/oauth_validator/t/001_server.pl | 11 ++++- src/test/modules/oauth_validator/validator.c | 28 +++++++---- 8 files changed, 175 insertions(+), 32 deletions(-) diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index a51355e238f..b2abae8deee 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -10133,8 +10133,46 @@ void PQinitSSL(int do_ssl); OAuth Support - TODO + libpq implements support for the OAuth v2 Device Authorization client flow, + documented in + RFC 8628, + which it will attempt to use by default if the server + requests a bearer token during + authentication. This flow can be utilized even if the system running the + client application does not have a usable web browser, for example when + running a client via SSH. Client applications may implement their own flows + instead; see . + + The builtin flow will, by default, print a URL to visit and a user code to + enter there: + +$ psql 'dbname=postgres oauth_issuer=https://example.com oauth_client_id=...' +Visit https://example.com/device and enter the code: ABCD-EFGH + + (This prompt may be + customized.) + You will then log into your OAuth provider, which will ask whether you want + to allow libpq and the server to perform actions on your behalf. It is always + a good idea to carefully review the URL and permissions displayed, to ensure + they match your expectations, before continuing. Do not give permissions to + untrusted third parties. + + + For an OAuth client flow to be usable, the connection string must at minimum + contain and + . (These settings are + determined by your organization's OAuth provider.) The builtin flow + additionally requires the OAuth authorization server to publish a device + authorization endpoint. + + + + + The builtin Device Authorization flow is not currently supported on Windows. + Custom client flows may still be implemented. + + Authdata Hooks diff --git a/doc/src/sgml/oauth-validators.sgml b/doc/src/sgml/oauth-validators.sgml index d0bca9196d9..eb8c4431c2d 100644 --- a/doc/src/sgml/oauth-validators.sgml +++ b/doc/src/sgml/oauth-validators.sgml @@ -41,7 +41,9 @@ Validator Responsibilities - TODO + Although different modules may take very different approaches to token + validation, implementations generally need to perform three separate + actions: @@ -121,6 +123,11 @@ if users are not prompted for additional scopes. + + Even if authorization fails, a module may choose to continue to pull + authentication information from the token for use in auditing and + debugging. + @@ -290,13 +297,15 @@ validator module a function named _PG_oauth_validator_module_init must be provided. The return value of the function must be a pointer to a struct of type - OAuthValidatorCallbacks, which contains pointers to - the module's token validation functions. The returned + OAuthValidatorCallbacks, which contains a magic + number and pointers to the module's token validation functions. The returned pointer must be of server lifetime, which is typically achieved by defining it as a static const variable in global scope. typedef struct OAuthValidatorCallbacks { + uint32 magic; /* must be set to PG_OAUTH_VALIDATOR_MAGIC */ + ValidatorStartupCB startup_cb; ValidatorShutdownCB shutdown_cb; ValidatorValidateCB validate_cb; @@ -341,14 +350,16 @@ typedef void (*ValidatorStartupCB) (ValidatorModuleState *state); previous calls will be available in state->private_data. -typedef ValidatorModuleResult *(*ValidatorValidateCB) (ValidatorModuleState *state, const char *token, const char *role); +typedef bool (*ValidatorValidateCB) (const ValidatorModuleState *state, + const char *token, const char *role, + ValidatorModuleResult *result); token will contain the bearer token to validate. The server has ensured that the token is well-formed syntactically, but no other validation has been performed. role will contain the role the user has requested to log in as. The callback must - return a palloc'd ValidatorModuleResult struct, which is + set output parameters in the result struct, which is defined as below: @@ -368,17 +379,17 @@ typedef struct ValidatorModuleResult determined. - The caller assumes ownership of the returned memory allocation, the - validator module should not in any way access the memory after it has been - returned. A validator may instead return NULL to signal an internal - error. + A validator may return false to signal an internal error, + in which case any result parameters are ignored and the connection fails. + Otherwise the validator should return true to indicate + that it has processed the token and made an authorization decision. The behavior after validate_cb returns depends on the specific HBA setup. Normally, the authn_id user name must exactly match the role that the user is logging in as. (This behavior may be modified with a usermap.) But when authenticating against - an HBA rule with trust_validator_authz turned on, the + an HBA rule with delegate_ident_mapping turned on, the server will not perform any checks on the value of authn_id at all; in this case it is up to the validator to ensure that the token carries enough privileges for the user to diff --git a/src/backend/libpq/auth-oauth.c b/src/backend/libpq/auth-oauth.c index aa16977c643..e2b5d1ed913 100644 --- a/src/backend/libpq/auth-oauth.c +++ b/src/backend/libpq/auth-oauth.c @@ -656,9 +656,9 @@ validate(Port *port, const char *auth) errmsg("validation of OAuth token requested without a validator loaded")); /* Call the validation function from the validator module */ - ret = ValidatorCallbacks->validate_cb(validator_module_state, - token, port->user_name); - if (ret == NULL) + ret = palloc0(sizeof(ValidatorModuleResult)); + if (!ValidatorCallbacks->validate_cb(validator_module_state, token, + port->user_name, ret)) { ereport(LOG, errmsg("internal error in OAuth validator module")); return false; @@ -756,8 +756,22 @@ load_validator_library(const char *libname) ValidatorCallbacks = (*validator_init) (); Assert(ValidatorCallbacks); + /* + * Check the magic number, to protect against break-glass scenarios where + * the ABI must change within a major version. load_external_function() + * already checks for compatibility across major versions. + */ + if (ValidatorCallbacks->magic != PG_OAUTH_VALIDATOR_MAGIC) + ereport(ERROR, + errmsg("%s module \"%s\": magic number mismatch", + "OAuth validator", libname), + errdetail("Server has magic number 0x%08X, module has 0x%08X.", + PG_OAUTH_VALIDATOR_MAGIC, ValidatorCallbacks->magic)); + /* Allocate memory for validator library private state data */ validator_module_state = (ValidatorModuleState *) palloc0(sizeof(ValidatorModuleState)); + validator_module_state->sversion = PG_VERSION_NUM; + if (ValidatorCallbacks->startup_cb != NULL) ValidatorCallbacks->startup_cb(validator_module_state); diff --git a/src/include/libpq/oauth.h b/src/include/libpq/oauth.h index 4fcdda74305..7e249613e10 100644 --- a/src/include/libpq/oauth.h +++ b/src/include/libpq/oauth.h @@ -20,26 +20,72 @@ extern PGDLLIMPORT char *oauth_validator_libraries_string; typedef struct ValidatorModuleState { + /* Holds the server's PG_VERSION_NUM. Reserved for future extensibility. */ + int sversion; + + /* + * Private data pointer for use by a validator module. This can be used to + * store state for the module that will be passed to each of its + * callbacks. + */ void *private_data; } ValidatorModuleState; typedef struct ValidatorModuleResult { + /* + * Should be set to true if the token carries sufficient permissions for + * the bearer to connect. + */ bool authorized; + + /* + * If the token authenticates the user, this should be set to a palloc'd + * string containing the SYSTEM_USER to use for HBA mapping. Consider + * setting this even if result->authorized is false so that DBAs may use + * the logs to match end users to token failures. + * + * This is required if the module is not configured for ident mapping + * delegation. See the validator module documentation for details. + */ char *authn_id; } ValidatorModuleResult; +/* + * Validator module callbacks + * + * These callback functions should be defined by validator modules and returned + * via _PG_oauth_validator_module_init(). ValidatorValidateCB is the only + * required callback. For more information about the purpose of each callback, + * refer to the OAuth validator modules documentation. + */ typedef void (*ValidatorStartupCB) (ValidatorModuleState *state); typedef void (*ValidatorShutdownCB) (ValidatorModuleState *state); -typedef ValidatorModuleResult *(*ValidatorValidateCB) (ValidatorModuleState *state, const char *token, const char *role); +typedef bool (*ValidatorValidateCB) (const ValidatorModuleState *state, + const char *token, const char *role, + ValidatorModuleResult *result); + +/* + * Identifies the compiled ABI version of the validator module. Since the server + * already enforces the PG_MODULE_MAGIC number for modules across major + * versions, this is reserved for emergency use within a stable release line. + * May it never need to change. + */ +#define PG_OAUTH_VALIDATOR_MAGIC 0x20250207 typedef struct OAuthValidatorCallbacks { + uint32 magic; /* must be set to PG_OAUTH_VALIDATOR_MAGIC */ + ValidatorStartupCB startup_cb; ValidatorShutdownCB shutdown_cb; ValidatorValidateCB validate_cb; } OAuthValidatorCallbacks; +/* + * Type of the shared library symbol _PG_oauth_validator_module_init that is + * looked up when loading a validator module. + */ typedef const OAuthValidatorCallbacks *(*OAuthValidatorModuleInit) (void); extern PGDLLEXPORT const OAuthValidatorCallbacks *_PG_oauth_validator_module_init(void); diff --git a/src/interfaces/libpq/fe-auth-oauth-curl.c b/src/interfaces/libpq/fe-auth-oauth-curl.c index 2179bb89800..74323de309a 100644 --- a/src/interfaces/libpq/fe-auth-oauth-curl.c +++ b/src/interfaces/libpq/fe-auth-oauth-curl.c @@ -32,7 +32,19 @@ #include "libpq-int.h" #include "mb/pg_wchar.h" -#define MAX_OAUTH_RESPONSE_SIZE (1024 * 1024) +/* + * It's generally prudent to set a maximum response size to buffer in memory, + * but it's less clear what size to choose. The biggest of our expected + * responses is the server metadata JSON, which will only continue to grow in + * size; the number of IANA-registered parameters in that document is up to 78 + * as of February 2025. + * + * Even if every single parameter were to take up 2k on average (a previously + * common limit on the size of a URL), 256k gives us 128 parameter values before + * we give up. (That's almost certainly complete overkill in practice; 2-4k + * appears to be common among popular providers at the moment.) + */ +#define MAX_OAUTH_RESPONSE_SIZE (256 * 1024) /* * Parsed JSON Representations diff --git a/src/test/modules/oauth_validator/fail_validator.c b/src/test/modules/oauth_validator/fail_validator.c index f77a3e115c6..7b1e69518d9 100644 --- a/src/test/modules/oauth_validator/fail_validator.c +++ b/src/test/modules/oauth_validator/fail_validator.c @@ -19,12 +19,15 @@ PG_MODULE_MAGIC; -static ValidatorModuleResult *fail_token(ValidatorModuleState *state, - const char *token, - const char *role); +static bool fail_token(const ValidatorModuleState *state, + const char *token, + const char *role, + ValidatorModuleResult *result); /* Callback implementations (we only need the main one) */ static const OAuthValidatorCallbacks validator_callbacks = { + PG_OAUTH_VALIDATOR_MAGIC, + .validate_cb = fail_token, }; @@ -34,8 +37,10 @@ _PG_oauth_validator_module_init(void) return &validator_callbacks; } -static ValidatorModuleResult * -fail_token(ValidatorModuleState *state, const char *token, const char *role) +static bool +fail_token(const ValidatorModuleState *state, + const char *token, const char *role, + ValidatorModuleResult *res) { elog(FATAL, "fail_validator: sentinel error"); pg_unreachable(); diff --git a/src/test/modules/oauth_validator/t/001_server.pl b/src/test/modules/oauth_validator/t/001_server.pl index f0b918390fd..d2dda62a2d4 100644 --- a/src/test/modules/oauth_validator/t/001_server.pl +++ b/src/test/modules/oauth_validator/t/001_server.pl @@ -14,12 +14,18 @@ use MIME::Base64 qw(encode_base64); use PostgreSQL::Test::Cluster; use PostgreSQL::Test::Utils; use Test::More; +use Config; use FindBin; use lib $FindBin::RealBin; use OAuth::Server; +if ($Config{osname} eq 'MSWin32') +{ + plan skip_all => 'OAuth server-side tests are not supported on Windows'; +} + if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\boauth\b/) { plan skip_all => @@ -402,7 +408,10 @@ note "running '" . join("' '", @cmd) . "'"; my ($stdout, $stderr) = run_command(\@cmd); like($stdout, qr/connection succeeded/, "stress-async: stdout matches"); -unlike($stderr, qr/connection to database failed/, "stress-async: stderr matches"); +unlike( + $stderr, + qr/connection to database failed/, + "stress-async: stderr matches"); # # This section of tests reconfigures the validator module itself, rather than diff --git a/src/test/modules/oauth_validator/validator.c b/src/test/modules/oauth_validator/validator.c index ef9bbb2866f..e218f5c8902 100644 --- a/src/test/modules/oauth_validator/validator.c +++ b/src/test/modules/oauth_validator/validator.c @@ -23,12 +23,15 @@ PG_MODULE_MAGIC; static void validator_startup(ValidatorModuleState *state); static void validator_shutdown(ValidatorModuleState *state); -static ValidatorModuleResult *validate_token(ValidatorModuleState *state, - const char *token, - const char *role); +static bool validate_token(const ValidatorModuleState *state, + const char *token, + const char *role, + ValidatorModuleResult *result); /* Callback implementations (exercise all three) */ static const OAuthValidatorCallbacks validator_callbacks = { + PG_OAUTH_VALIDATOR_MAGIC, + .startup_cb = validator_startup, .shutdown_cb = validator_shutdown, .validate_cb = validate_token @@ -89,6 +92,13 @@ _PG_oauth_validator_module_init(void) static void validator_startup(ValidatorModuleState *state) { + /* + * Make sure the server is correctly setting sversion. (Real modules + * should not do this; it would defeat upgrade compatibility.) + */ + if (state->sversion != PG_VERSION_NUM) + elog(ERROR, "oauth_validator: sversion set to %d", state->sversion); + state->private_data = PRIVATE_COOKIE; } @@ -108,18 +118,16 @@ 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. */ -static ValidatorModuleResult * -validate_token(ValidatorModuleState *state, const char *token, const char *role) +static bool +validate_token(const ValidatorModuleState *state, + const char *token, const char *role, + ValidatorModuleResult *res) { - ValidatorModuleResult *res; - /* Check to make sure our private state still exists. */ if (state->private_data != PRIVATE_COOKIE) elog(ERROR, "oauth_validator: private state cookie changed to %p in validate", state->private_data); - res = palloc(sizeof(ValidatorModuleResult)); - elog(LOG, "oauth_validator: token=\"%s\", role=\"%s\"", token, role); elog(LOG, "oauth_validator: issuer=\"%s\", scope=\"%s\"", MyProcPort->hba->oauth_issuer, @@ -131,5 +139,5 @@ validate_token(ValidatorModuleState *state, const char *token, const char *role) else res->authn_id = pstrdup(role); - return res; + return true; } -- 2.39.3 (Apple Git-146)