v1-0001-oauth-provider-support.patch
application/x-patch
Filename: v1-0001-oauth-provider-support.patch
Type: application/x-patch
Part: 0
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: unified
Series: patch v1-0001
| File | + | − |
|---|---|---|
| src/backend/libpq/auth.c | 8 | 2 |
| src/backend/libpq/auth-oauth.c | 104 | 213 |
| src/backend/libpq/hba.c | 33 | 0 |
| src/include/libpq/auth.h | 25 | 0 |
| src/include/libpq/hba.h | 1 | 0 |
| src/interfaces/libpq/fe-auth-oauth.c | 31 | 5 |
| src/interfaces/libpq/fe-connect.c | 6 | 0 |
| src/interfaces/libpq/libpq-int.h | 1 | 0 |
diff --git a/src/backend/libpq/auth-oauth.c b/src/backend/libpq/auth-oauth.c
index c47211132c..86f820482b 100644
--- a/src/backend/libpq/auth-oauth.c
+++ b/src/backend/libpq/auth-oauth.c
@@ -24,7 +24,9 @@
#include "libpq/hba.h"
#include "libpq/oauth.h"
#include "libpq/sasl.h"
+#include "miscadmin.h"
#include "storage/fd.h"
+#include "utils/memutils.h"
/* GUC */
char *oauth_validator_command;
@@ -34,6 +36,13 @@ static void *oauth_init(Port *port, const char *selected_mech, const char *shado
static int oauth_exchange(void *opaq, const char *input, int inputlen,
char **output, int *outputlen, char **logdetail);
+/*----------------------------------------------------------------
+ * OAuth Authentication
+ *----------------------------------------------------------------
+ */
+static List *oauth_providers = NIL;
+static OAuthProvider* oauth_provider = NULL;
+
/* Mechanism declaration */
const pg_be_sasl_mech pg_be_oauth_mech = {
oauth_get_mechanisms,
@@ -63,15 +72,90 @@ static char *sanitize_char(char c);
static char *parse_kvpairs_for_auth(char **input);
static void generate_error_response(struct oauth_ctx *ctx, char **output, int *outputlen);
static bool validate(Port *port, const char *auth, char **logdetail);
-static bool run_validator_command(Port *port, const char *token);
+static const char* run_validator_command(Port *port, const char *token);
static bool check_exit(FILE **fh, const char *command);
static bool unset_cloexec(int fd);
-static bool username_ok_for_shell(const char *username);
#define KVSEP 0x01
#define AUTH_KEY "auth"
#define BEARER_SCHEME "Bearer "
+/*----------------------------------------------------------------
+ * OAuth Token Validator
+ *----------------------------------------------------------------
+ */
+
+/*
+ * RegisterOAuthProvider registers a OAuth Token Validator to be
+ * used for oauth token validation. It validates the token and adds the valiator
+ * name and it's hooks to a list of loaded token validator. The right validator's
+ * hooks can then be called based on the validator name specified in
+ * pg_hba.conf.
+ *
+ * This function should be called in _PG_init() by any extension looking to
+ * add a custom authentication method.
+ */
+void
+RegisterOAuthProvider(
+ const char *provider_name,
+ OAuthProviderCheck_hook_type OAuthProviderCheck_hook,
+ OAuthProviderError_hook_type OAuthProviderError_hook
+)
+{
+ if (!process_shared_preload_libraries_in_progress)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("RegisterOAuthProvider can only be called by a shared_preload_library")));
+ return;
+ }
+
+ MemoryContext oldcxt;
+ if (oauth_provider == NULL)
+ {
+ oldcxt = MemoryContextSwitchTo(TopMemoryContext);
+ oauth_provider = palloc(sizeof(OAuthProvider));
+ oauth_provider->name = pstrdup(provider_name);
+ oauth_provider->oauth_provider_hook = OAuthProviderCheck_hook;
+ oauth_provider->oauth_error_hook = OAuthProviderError_hook;
+ oauth_providers = lappend(oauth_providers, oauth_provider);
+ MemoryContextSwitchTo(oldcxt);
+ }
+ else
+ {
+ if (oauth_provider && oauth_provider->name)
+ {
+ ereport(ERROR,
+ (errmsg("OAuth provider \"%s\" is already loaded.",
+ oauth_provider->name)));
+ }
+ else
+ {
+ ereport(ERROR,
+ (errmsg("OAuth provider is already loaded.")));
+ }
+ }
+}
+
+/*
+ * Returns the oauth provider (which includes it's
+ * callback functions) based on name specified.
+ */
+OAuthProvider *get_provider_by_name(const char *name)
+{
+ ListCell *lc;
+ foreach(lc, oauth_providers)
+ {
+ OAuthProvider *provider = (OAuthProvider *) lfirst(lc);
+ if (strcmp(provider->name, name) == 0)
+ {
+ return provider;
+ }
+ }
+
+ return NULL;
+}
+
static void
oauth_get_mechanisms(Port *port, StringInfo buf)
{
@@ -494,17 +578,17 @@ validate(Port *port, const char *auth, char **logdetail)
}
/* Have the validator check the token. */
- if (!run_validator_command(port, token))
+ if (run_validator_command(port, token) == NULL)
return false;
-
+
if (port->hba->oauth_skip_usermap)
{
/*
- * If the validator is our authorization authority, we're done.
- * Authentication may or may not have been performed depending on the
- * validator implementation; all that matters is that the validator says
- * the user can log in with the target role.
- */
+ * If the validator is our authorization authority, we're done.
+ * Authentication may or may not have been performed depending on the
+ * validator implementation; all that matters is that the validator says
+ * the user can log in with the target role.
+ */
return true;
}
@@ -524,193 +608,26 @@ validate(Port *port, const char *auth, char **logdetail)
return (ret == STATUS_OK);
}
-static bool
+static const char*
run_validator_command(Port *port, const char *token)
{
- bool success = false;
- int rc;
- int pipefd[2];
- int rfd = -1;
- int wfd = -1;
-
- StringInfoData command = { 0 };
- char *p;
- FILE *fh = NULL;
-
- ssize_t written;
- char *line = NULL;
- size_t size = 0;
- ssize_t len;
-
- Assert(oauth_validator_command);
-
- if (!oauth_validator_command[0])
- {
- ereport(COMMERROR,
- (errmsg("oauth_validator_command is not set"),
- errhint("To allow OAuth authenticated connections, set "
- "oauth_validator_command in postgresql.conf.")));
- return false;
- }
-
- /*
- * Since popen() is unidirectional, open up a pipe for the other direction.
- * Use CLOEXEC to ensure that our write end doesn't accidentally get copied
- * into child processes, which would prevent us from closing it cleanly.
- *
- * XXX this is ugly. We should just read from the child process's stdout,
- * but that's a lot more code.
- * XXX by bypassing the popen API, we open the potential of process
- * deadlock. Clearly document child process requirements (i.e. the child
- * MUST read all data off of the pipe before writing anything).
- * TODO: port to Windows using _pipe().
- */
- rc = pipe2(pipefd, O_CLOEXEC);
- if (rc < 0)
+ if(oauth_provider->oauth_provider_hook == NULL)
{
- ereport(COMMERROR,
- (errcode_for_file_access(),
- errmsg("could not create child pipe: %m")));
return false;
}
- rfd = pipefd[0];
- wfd = pipefd[1];
-
- /* Allow the read pipe be passed to the child. */
- if (!unset_cloexec(rfd))
+ char *id = oauth_provider->
+ oauth_provider_hook(port, token);
+ if(id == NULL)
{
- /* error message was already logged */
- goto cleanup;
- }
-
- /*
- * Construct the command, substituting any recognized %-specifiers:
- *
- * %f: the file descriptor of the input pipe
- * %r: the role that the client wants to assume (port->user_name)
- * %%: a literal '%'
- */
- initStringInfo(&command);
-
- for (p = oauth_validator_command; *p; p++)
- {
- if (p[0] == '%')
- {
- switch (p[1])
- {
- case 'f':
- appendStringInfo(&command, "%d", rfd);
- p++;
- break;
- case 'r':
- /*
- * TODO: decide how this string should be escaped. The role
- * is controlled by the client, so if we don't escape it,
- * command injections are inevitable.
- *
- * This is probably an indication that the role name needs
- * to be communicated to the validator process in some other
- * way. For this proof of concept, just be incredibly strict
- * about the characters that are allowed in user names.
- */
- if (!username_ok_for_shell(port->user_name))
- goto cleanup;
-
- appendStringInfoString(&command, port->user_name);
- p++;
- break;
- case '%':
- appendStringInfoChar(&command, '%');
- p++;
- break;
- default:
- appendStringInfoChar(&command, p[0]);
- }
- }
- else
- appendStringInfoChar(&command, p[0]);
- }
-
- /* Execute the command. */
- fh = OpenPipeStream(command.data, "re");
- /* TODO: handle failures */
-
- /* We don't need the read end of the pipe anymore. */
- close(rfd);
- rfd = -1;
-
- /* Give the command the token to validate. */
- written = write(wfd, token, strlen(token));
- if (written != strlen(token))
- {
- /* TODO must loop for short writes, EINTR et al */
- ereport(COMMERROR,
- (errcode_for_file_access(),
- errmsg("could not write token to child pipe: %m")));
- goto cleanup;
- }
-
- close(wfd);
- wfd = -1;
-
- /*
- * Read the command's response.
- *
- * TODO: getline() is probably too new to use, unfortunately.
- * TODO: loop over all lines
- */
- if ((len = getline(&line, &size, fh)) >= 0)
- {
- /* TODO: fail if the authn_id doesn't end with a newline */
- if (len > 0)
- line[len - 1] = '\0';
-
- set_authn_id(port, line);
- }
- else if (ferror(fh))
- {
- ereport(COMMERROR,
- (errcode_for_file_access(),
- errmsg("could not read from command \"%s\": %m",
- command.data)));
- goto cleanup;
- }
-
- /* Make sure the command exits cleanly. */
- if (!check_exit(&fh, command.data))
- {
- /* error message already logged */
- goto cleanup;
- }
-
- /* Done. */
- success = true;
-
-cleanup:
- if (line)
- free(line);
-
- /*
- * In the successful case, the pipe fds are already closed. For the error
- * case, always close out the pipe before waiting for the command, to
- * prevent deadlock.
- */
- if (rfd >= 0)
- close(rfd);
- if (wfd >= 0)
- close(wfd);
-
- if (fh)
- {
- Assert(!success);
- check_exit(&fh, command.data);
+ ereport(LOG,
+ (errmsg("OAuth bearer token validation failed" )));
+ return NULL;
}
- if (command.data)
- pfree(command.data);
-
- return success;
+ set_authn_id(port, id);
+
+ return id;
}
static bool
@@ -769,29 +686,3 @@ unset_cloexec(int fd)
return true;
}
-
-/*
- * XXX This should go away eventually and be replaced with either a proper
- * escape or a different strategy for communication with the validator command.
- */
-static bool
-username_ok_for_shell(const char *username)
-{
- /* This set is borrowed from fe_utils' appendShellStringNoError(). */
- static const char * const allowed = "abcdefghijklmnopqrstuvwxyz"
- "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
- "0123456789-_./:";
- size_t span;
-
- Assert(username && username[0]); /* should have already been checked */
-
- span = strspn(username, allowed);
- if (username[span] != '\0')
- {
- ereport(COMMERROR,
- (errmsg("PostgreSQL user name contains unsafe characters and cannot be passed to the OAuth validator")));
- return false;
- }
-
- return true;
-}
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 333051ad3c..0bbcf231d2 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -296,8 +296,14 @@ auth_failed(Port *port, int status, const char *logdetail)
errstr = gettext_noop("RADIUS authentication failed for user \"%s\"");
break;
case uaOAuth:
- errstr = gettext_noop("OAuth bearer authentication failed for user \"%s\"");
- break;
+ {
+ OAuthProvider *provider = get_provider_by_name(port->hba->oauth_provider);
+ if(provider->oauth_error_hook)
+ errstr = provider->oauth_error_hook(port);
+ else
+ errstr = gettext_noop("OAuth bearer authentication failed for user \"%s\"");
+ break;
+ }
default:
errstr = gettext_noop("authentication failed for user \"%s\": invalid authentication method");
break;
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 943e78ddff..94fb5d434d 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -1663,6 +1663,14 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
parsedline->clientcert = clientCertFull;
}
+ /*
+ * Ensure that the token validation provider name is specified as provider for oauth method.
+ */
+ if (parsedline->auth_method == uaOAuth)
+ {
+ MANDATORY_AUTH_ARG(parsedline->oauth_provider, "provider", "oauth");
+ }
+
return parsedline;
}
@@ -2095,6 +2103,31 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
else
hbaline->oauth_skip_usermap = false;
}
+ else if (strcmp(name, "provider") == 0)
+ {
+ REQUIRE_AUTH_OPTION(uaOAuth, "provider", "oauth");
+ if (hbaline->auth_method != uaOAuth)
+ INVALID_AUTH_OPTION("provider", gettext_noop("oauth"));
+ /*
+ * Verify that the token validation mentioned is loaded via shared_preload_libraries.
+ */
+ if (get_provider_by_name(val) == NULL)
+ {
+ ereport(elevel,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("cannot use oauth provider %s",val),
+ errhint("Load provider token validation via shared_preload_libraries."),
+ errcontext("line %d of configuration file \"%s\"",
+ line_num, HbaFileName)));
+ *err_msg = psprintf("cannot use oauth provider %s", val);
+
+ return false;
+ }
+ else
+ {
+ hbaline->oauth_provider = pstrdup(val);
+ }
+ }
else
{
ereport(elevel,
diff --git a/src/include/libpq/auth.h b/src/include/libpq/auth.h
index 485e48970e..938ac399dc 100644
--- a/src/include/libpq/auth.h
+++ b/src/include/libpq/auth.h
@@ -44,4 +44,29 @@ extern void set_authn_id(Port *port, const char *id);
typedef void (*ClientAuthentication_hook_type) (Port *, int);
extern PGDLLIMPORT ClientAuthentication_hook_type ClientAuthentication_hook;
+/* Declarations for oAuth authentication providers */
+typedef const char* (*OAuthProviderCheck_hook_type) (Port *, const char*);
+
+/* Hook for plugins to report error messages in validation_failed() */
+typedef const char * (*OAuthProviderError_hook_type) (Port *);
+
+/* Hook for plugins to validate oauth provider options */
+typedef bool (*OAuthProviderValidateOptions_hook_type)
+ (char *, char *, HbaLine *, char **);
+
+typedef struct OAuthProvider
+{
+ const char *name;
+ OAuthProviderCheck_hook_type oauth_provider_hook;
+ OAuthProviderError_hook_type oauth_error_hook;
+} OAuthProvider;
+
+extern void RegisterOAuthProvider
+ (const char *provider_name,
+ OAuthProviderCheck_hook_type OAuthProviderCheck_hook,
+ OAuthProviderError_hook_type OAuthProviderError_hook
+ );
+
+extern OAuthProvider *get_provider_by_name(const char *name);
+
#endif /* AUTH_H */
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index c1b1313989..d65395cc22 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -123,6 +123,7 @@ typedef struct HbaLine
char *radiusports_s;
char *oauth_issuer;
char *oauth_scope;
+ char *oauth_provider;
bool oauth_skip_usermap;
} HbaLine;
diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c
index 91d2c69f16..61a0b80b7e 100644
--- a/src/interfaces/libpq/fe-auth-oauth.c
+++ b/src/interfaces/libpq/fe-auth-oauth.c
@@ -174,6 +174,16 @@ get_auth_token(PGconn *conn)
if (!token_buf)
goto cleanup;
+ if(conn->oauth_bearer_token)
+ {
+ appendPQExpBufferStr(token_buf, "Bearer ");
+ appendPQExpBufferStr(token_buf, conn->oauth_bearer_token);
+ if (PQExpBufferBroken(token_buf))
+ goto cleanup;
+ token = strdup(token_buf->data);
+ goto cleanup;
+ }
+
err = i_set_str_parameter(&session, I_OPT_OPENID_CONFIG_ENDPOINT, conn->oauth_discovery_uri);
if (err)
{
@@ -201,18 +211,22 @@ get_auth_token(PGconn *conn)
libpq_gettext("issuer does not support device authorization"));
goto cleanup;
}
+
+ //default device flow
+ int session_response_type = I_RESPONSE_TYPE_DEVICE_CODE;
+ auth_method = I_TOKEN_AUTH_METHOD_NONE;
+ if (conn->oauth_client_secret && *conn->oauth_client_secret)
+ {
+ auth_method = I_TOKEN_AUTH_METHOD_SECRET_BASIC;
+ }
- err = i_set_response_type(&session, I_RESPONSE_TYPE_DEVICE_CODE);
+ err = i_set_response_type(&session, session_response_type);
if (err)
{
iddawc_error(conn, err, "failed to set device code response type");
goto cleanup;
}
- auth_method = I_TOKEN_AUTH_METHOD_NONE;
- if (conn->oauth_client_secret && *conn->oauth_client_secret)
- auth_method = I_TOKEN_AUTH_METHOD_SECRET_BASIC;
-
err = i_set_parameter_list(&session,
I_OPT_CLIENT_ID, conn->oauth_client_id,
I_OPT_CLIENT_SECRET, conn->oauth_client_secret,
@@ -250,6 +264,18 @@ get_auth_token(PGconn *conn)
goto cleanup;
}
+ if (conn->oauth_client_secret && *conn->oauth_client_secret)
+ {
+ session_response_type = I_RESPONSE_TYPE_CLIENT_CREDENTIALS;
+ }
+
+ err = i_set_response_type(&session, session_response_type);
+ if (err)
+ {
+ iddawc_error(conn, err, "failed to set session response type");
+ goto cleanup;
+ }
+
/*
* Poll the token endpoint until either the user logs in and authorizes the
* use of a token, or a hard failure occurs. We perform one ping _before_
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 2ff450ce05..5d804c8c0d 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -361,6 +361,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
"OAuth-Scope", "", 15,
offsetof(struct pg_conn, oauth_scope)},
+ {"oauth_bearer_token", NULL, NULL, NULL,
+ "OAuth-Bearer", "", 20,
+ offsetof(struct pg_conn, oauth_bearer_token)},
+
/* Terminating entry --- MUST BE LAST */
{NULL, NULL, NULL, NULL,
NULL, NULL, 0}
@@ -4200,6 +4204,8 @@ freePGconn(PGconn *conn)
free(conn->oauth_discovery_uri);
if (conn->oauth_client_id)
free(conn->oauth_client_id);
+ if(conn->oauth_bearer_token)
+ free(conn->oauth_bearer_token);
if (conn->oauth_client_secret)
free(conn->oauth_client_secret);
if (conn->oauth_scope)
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 1b4de3dff0..91e71afe14 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -402,6 +402,7 @@ struct pg_conn
char *oauth_client_id; /* client identifier */
char *oauth_client_secret; /* client secret */
char *oauth_scope; /* access token scope */
+ char *oauth_bearer_token; /* oauth token */
bool oauth_want_retry; /* should we retry on failure? */
/* Optional file to write trace info to */