Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix shadow auth for users defined under [users] section #706

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions include/bouncer.h
Expand Up @@ -326,6 +326,7 @@ struct PgUser {
uint8_t scram_ServerKey[32];
bool has_scram_keys; /* true if the above two are valid */
bool mock_auth; /* not a real user, only for mock auth */
bool is_preconfigured; /* true if user is created from pre-configuration parsing */
int pool_mode;
int max_user_connections; /* how much server connections are allowed */
int connection_count; /* how much connections are used by user now */
Expand Down
1 change: 1 addition & 0 deletions include/objects.h
Expand Up @@ -47,6 +47,7 @@ void disconnect_client(PgSocket *client, bool notify, const char *reason, ...) _

PgDatabase * add_database(const char *name) _MUSTCHECK;
PgDatabase *register_auto_database(const char *name);
PgUser * find_original_user(PgUser *login_user) _MUSTCHECK;
PgUser * add_user(const char *name, const char *passwd) _MUSTCHECK;
PgUser * add_db_user(PgDatabase *db, const char *name, const char *passwd) _MUSTCHECK;
PgUser * force_user(PgDatabase *db, const char *username, const char *passwd) _MUSTCHECK;
Expand Down
1 change: 1 addition & 0 deletions include/server.h
Expand Up @@ -24,3 +24,4 @@ int pool_min_pool_size(PgPool *pool) _MUSTCHECK;
int pool_res_pool_size(PgPool *pool) _MUSTCHECK;
int database_max_connections(PgDatabase *db) _MUSTCHECK;
int user_max_connections(PgUser *user) _MUSTCHECK;
bool user_is_authenticated(PgUser *user) _MUSTCHECK;
13 changes: 6 additions & 7 deletions src/client.c
Expand Up @@ -206,7 +206,7 @@ static bool finish_set_pool(PgSocket *client, bool takeover)
if (client->db->forced_user)
pool_user = client->db->forced_user;
else
pool_user = client->login_user;
pool_user = find_original_user(client->login_user);

client->pool = get_pool(client->db, pool_user);
if (!client->pool) {
Expand Down Expand Up @@ -343,13 +343,12 @@ bool set_pool(PgSocket *client, const char *dbname, const char *username, const
}
} else {
client->login_user = find_user(username);
if (!client->login_user) {
if (!client->login_user || !user_is_authenticated(client->login_user)) {
/*
* If the login user specified by the client
* does not exist, check if an auth_user is
* set and if so send off an auth_query. If
* no auth_user is set for the db, see if the
* global auth_user is set and use that.
* If the login user specified by the client does not exist or only exists as
* a pre-configuration, check if an auth_user is set and if so send off an
* auth_query. If no auth_user is set for the db, see if the global auth_user
* is set and use that.
*/
if (!client->db->auth_user && cf_auth_user) {
client->db->auth_user = find_user(cf_auth_user);
Expand Down
2 changes: 2 additions & 0 deletions src/loader.c
Expand Up @@ -407,11 +407,13 @@ bool parse_user(void *base, const char *name, const char *connstr)

user = find_user(name);
if (!user) {
/* represents a user pre-configuration, not a connected logged-in user */
user = add_user(name, "");
if (!user) {
log_error("cannot create user, no memory?");
goto fail;
}
user->is_preconfigured = true;
}

user->pool_mode = pool_mode;
Expand Down
30 changes: 30 additions & 0 deletions src/objects.c
Expand Up @@ -387,6 +387,36 @@ PgUser *add_user(const char *name, const char *passwd)
return user;
}

/* find original or preconfigured user given the login user */
PgUser *find_original_user(PgUser *login_user)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function name isn't really descriptive of what this function is doing, especially the put_in_order call.

{
/*
* The original user will either be the current login user, a preconfigured user via
* ini file [users] section, a preconfigured user via global auth_file, a PAM user,
* a user attached to a database via auth_query, a forced user with user= entry via
* ini file [databases] section, or not exist yet if the user has never connected.
*/
PgUser *original_user = find_user(login_user->name);
char *tmp_passwd;

/* add new login user to reload their configurations at runtime */
if (original_user == NULL) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand what this is needed for. Can you add a test showing what you mean with the RELOAD behaviour that you want to support? i.e. one that fails without this put_in_order code.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

find_original_user no longer exists now.

put_in_order(&login_user->head, &user_list, cmp_user);
aatree_insert(&user_tree, (uintptr_t)login_user->name, &login_user->tree_node);
return login_user;
}

/* original user is no longer preconfigured and won't send auth queries on future login */
if (original_user->is_preconfigured)
original_user->is_preconfigured = false;

tmp_passwd = strdup(login_user->passwd);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this strdup seems unnecessary, you should be able to copy the password directly from user to user.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

find_original_user no longer exists now.

safe_strcpy(original_user->passwd, tmp_passwd, sizeof(original_user->passwd));
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy over password of new login user to account for a single user that may have different passwords for multiple databases.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So you are copying every time in case a user has a different password in different databases? I think even with this code that could lead to problems, because the original_user is shared between clients of different databases. So maybe we should just document that a user should not have different passwords for different databases when using auth_query/auth_user.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the PR. find_original_user no longer exists. To support a user logging into different databases with different passwords (usually when databases are on different hosts), each PgDatabase* now stores a list of user passwords.

struct PgDatabase {
	struct List head;
	char name[MAX_DBNAME];	/* db name for clients */
	struct AATree user_passwds;	/* user passwords for this database from auth_query */
...
struct PgUserPassword {
	struct AANode tree_node;	/* used to attach user password to tree */
	char username[MAX_USERNAME];
	char passwd[MAX_PASSWORD];
};

Semantically, this should be clearer since it models reality, where each database may have different passwords per user. PgUser* still retains a password field to store a password from auth_file which will be used (forced) by default if it exists, when connecting to any database.

Storing user passwords in each PgDatabase* also conveniently frees (purges) passwords whenever the database is killed in method kill_database to prevent stale logins. This is preferred over storing a list of passwords in PgUser* (which would require another scan to free users' passwords if belonging to a database killed).
Screen+Shot+2022-07-12+at+5 58 05+PM

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change seems to somehow have been reverted on your branch. Possibly due to a rebase.

free(tmp_passwd);

return original_user;
}

/* add or update db users */
PgUser *add_db_user(PgDatabase *db, const char *name, const char *passwd)
{
Expand Down
8 changes: 8 additions & 0 deletions src/server.c
Expand Up @@ -243,6 +243,14 @@ int user_max_connections(PgUser *user)
}
}

bool user_is_authenticated(PgUser *user)
{
/* authenticated users cannot be in a non-logged in or preconfigured state */
return cf_auth_type == AUTH_TRUST ||
(cf_auth_user != NULL && strcmp(cf_auth_user, user->name) == 0) ||
!user->is_preconfigured;
}

/* process packets on logged in connection */
static bool handle_server_work(PgSocket *server, PktHdr *pkt)
{
Expand Down
1 change: 1 addition & 0 deletions test/test.ini
Expand Up @@ -33,6 +33,7 @@ hostlist2 = port=6666 host=127.0.0.1,127.0.0.1 dbname=p0 user=bouncer

[users]
maxedout = max_user_connections=3
shadowuser2 = max_user_connections=3

[pgbouncer]
logfile = test.log
Expand Down
45 changes: 45 additions & 0 deletions test/test.sh
Expand Up @@ -185,6 +185,8 @@ psql -X -p $PG_PORT -d p0 -c "select * from pg_user" | grep pswcheck > /dev/null
psql -X -o /dev/null -p $PG_PORT -c "create user pswcheck with superuser createdb password 'pgbouncer-check';" p0 || exit 1
psql -X -o /dev/null -p $PG_PORT -c "create user someuser with password 'anypasswd';" p0 || exit 1
psql -X -o /dev/null -p $PG_PORT -c "create user maxedout;" p0 || exit 1
psql -X -o /dev/null -p $PG_PORT -c "create user shadowuser1 with password 'bar';" p0 || exit 1
psql -X -o /dev/null -p $PG_PORT -c "create user shadowuser2 with password 'foo';" p0 || exit 1
psql -X -o /dev/null -p $PG_PORT -c "create user longpass with password '$long_password';" p0 || exit 1
if $pg_supports_scram; then
psql -X -o /dev/null -p $PG_PORT -c "set password_encryption = 'md5'; create user muser1 password 'foo';" p0 || exit 1
Expand Down Expand Up @@ -931,6 +933,48 @@ test_password_server() {
return 0
}

# test password authentication from PgBouncer to PostgreSQL server using
# auth_query to retrieve user's shadow password from pg_shadow
test_shadow_password_server_login() {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These test cases are reproducible examples of bug (they fail without the fix): #484

$have_getpeereid || return 77

admin "set auth_type='md5'"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it makes sense to include the same tests also for scram auth?

admin "set auth_user='pswcheck'"

# plain-text password of auth_user in userlist.txt
curuser=`psql -X -d "dbname=authdb user=pswcheck password=pgbouncer-check" -tAq -c "select current_user;"`
echo "curuser=$curuser"
test "$curuser" = "pswcheck" || return 1

# user with correct password from PostgreSQL server
curuser=`psql -X -d "dbname=authdb user=shadowuser1 password=bar" -tAq -c "select current_user;"`
echo "curuser=$curuser"
test "$curuser" = "shadowuser1" || return 1
# user with wrong password from PostgreSQL server
curuser=`psql -X -d "dbname=authdb user=shadowuser1 password=badpasswd" -tAq -c "select current_user;"`
echo "curuser=$curuser"
test "$curuser" = "" || return 1

# user defined in ini [users] section with correct password from PostgreSQL server
curuser=`psql -X -d "dbname=authdb user=shadowuser2 password=foo" -tAq -c "select current_user;"`
echo "curuser=$curuser"
test "$curuser" = "shadowuser2" || return 1
# user defined in ini [users] section with wrong password from PostgreSQL server
curuser2=`psql -X -d "dbname=authdb user=shadowuser2 password=badpasswd" -tAq -c "select current_user;"`
echo "curuser2=$curuser2"
test "$curuser2" = "" || return 1

# auth_user defined in ini [users] section with correct password from PostgreSQL server
admin "set auth_user='shadowuser2'"
curuser=`psql -X -d "dbname=authdb user=shadowuser2 password=foo" -tAq -c "select current_user;"`
echo "curuser=$curuser"
test "$curuser" = "shadowuser2" || return 1

admin "set auth_type='trust'"

return 0
}

# test plain-text password authentication from client to PgBouncer
test_password_client() {
$have_getpeereid || return 77
Expand Down Expand Up @@ -1445,6 +1489,7 @@ test_server_lifetime
test_server_idle_timeout
test_query_timeout
test_idle_transaction_timeout
test_shadow_password_server_login
test_server_connect_timeout_establish
test_server_connect_timeout_reject
test_server_check_delay
Expand Down