v2-0002-multiple-passwords-work-with-scram-and-md5.patch
application/x-patch
Filename: v2-0002-multiple-passwords-work-with-scram-and-md5.patch
Type: application/x-patch
Part: 2
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: format-patch
Series: patch v2-0002
Subject: multiple passwords work with scram and md5
| File | + | − |
|---|---|---|
| src/backend/commands/user.c | 166 | 33 |
| src/backend/libpq/auth.c | 40 | 54 |
| src/backend/libpq/auth-sasl.c | 2 | 2 |
| src/backend/libpq/auth-scram.c | 108 | 76 |
| src/backend/libpq/crypt.c | 29 | 22 |
| src/backend/parser/gram.y | 14 | 2 |
| src/backend/utils/cache/catcache.c | 1 | 1 |
| src/backend/utils/cache/syscache.c | 3 | 3 |
| src/include/catalog/pg_auth_password.h | 5 | 4 |
| src/include/commands/user.h | 1 | 1 |
| src/include/libpq/crypt.h | 1 | 1 |
| src/include/libpq/sasl.h | 2 | 2 |
| src/include/libpq/scram.h | 1 | 1 |
| src/include/parser/kwlist.h | 1 | 0 |
| src/include/utils/syscache.h | 1 | 1 |
| src/test/regress/expected/password.out | 1 | 1 |
From 446f4216b55db0b3ae1a90e46a9ef1983634e240 Mon Sep 17 00:00:00 2001
From: Joshua Brindle <joshua.brindle@crunchydata.com>
Date: Tue, 1 Feb 2022 14:32:55 -0800
Subject: [PATCH v2 2/3] multiple passwords work with scram and md5
also renaming roles, dropping roles, switching between
password_encryption
Signed-off-by: Joshua Brindle <joshua.brindle@crunchydata.com>
---
src/backend/commands/user.c | 199 +++++++++++++++++++++----
src/backend/libpq/auth-sasl.c | 4 +-
src/backend/libpq/auth-scram.c | 184 +++++++++++++----------
src/backend/libpq/auth.c | 94 +++++-------
src/backend/libpq/crypt.c | 51 ++++---
src/backend/parser/gram.y | 16 +-
src/backend/utils/cache/catcache.c | 2 +-
src/backend/utils/cache/syscache.c | 6 +-
src/include/catalog/pg_auth_password.h | 9 +-
src/include/commands/user.h | 2 +-
src/include/libpq/crypt.h | 2 +-
src/include/libpq/sasl.h | 4 +-
src/include/libpq/scram.h | 2 +-
src/include/parser/kwlist.h | 1 +
src/include/utils/syscache.h | 2 +-
src/test/regress/expected/password.out | 2 +-
16 files changed, 376 insertions(+), 204 deletions(-)
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b6ce37f1bf..4117e58840a 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -18,6 +18,7 @@
#include "access/xact.h"
#include "catalog/binary_upgrade.h"
#include "catalog/catalog.h"
+
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/objectaccess.h"
@@ -31,11 +32,14 @@
#include "commands/defrem.h"
#include "commands/seclabel.h"
#include "commands/user.h"
+#include "common/scram-common.h"
#include "libpq/crypt.h"
+#include "libpq/scram.h"
#include "miscadmin.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
#include "utils/builtins.h"
+#include "utils/catcache.h"
#include "utils/fmgroids.h"
#include "utils/syscache.h"
#include "utils/timestamp.h"
@@ -47,6 +51,9 @@ Oid binary_upgrade_next_pg_authid_oid = InvalidOid;
/* GUC parameter */
int Password_encryption = PASSWORD_TYPE_SCRAM_SHA_256;
+/* default password name */
+const char* default_passname = "__def__";
+
/* Hook to check passwords in CreateRole() and AlterRole() */
check_password_hook_type check_password_hook = NULL;
@@ -69,7 +76,7 @@ have_createrole_privilege(void)
* Is the role able to log in
*/
bool
-is_role_valid(const char *rolename, char **logdetail)
+is_role_valid(const char *rolename, const char **logdetail)
{
HeapTuple roleTup;
Datum datum;
@@ -102,6 +109,64 @@ is_role_valid(const char *rolename, char **logdetail)
}
+static
+bool
+validate_and_get_salt(char *rolename, char **salt, const char **logdetail)
+{
+ char **current_secrets;
+ char *salt1, *salt2 = NULL;
+ int i, num;
+ PasswordType passtype;
+
+ if (Password_encryption == PASSWORD_TYPE_MD5)
+ {
+ *salt = rolename; /* md5 always uses role name, no need to look through the passwords */
+ return true;
+ }
+ else if (Password_encryption == PASSWORD_TYPE_PLAINTEXT)
+ {
+ *salt = NULL; /* Plaintext does not have a salt */
+ return true;
+ }
+
+ current_secrets = get_role_passwords(rolename, logdetail, &num);
+ if (num == 0)
+ {
+ *salt = NULL; /* No existing passwords, allow one to be generated */
+ return true;
+ }
+ for (i = 0; i < num; i++) {
+ passtype = get_password_type(current_secrets[i]);
+ if (passtype == PASSWORD_TYPE_MD5 || passtype == PASSWORD_TYPE_PLAINTEXT)
+ continue; /* md5 uses rolename as salt so it is always the same and plaintext has no salt */
+ else if (passtype == PASSWORD_TYPE_SCRAM_SHA_256) {
+ int iterations;
+ uint8 stored_key[SCRAM_KEY_LEN];
+ uint8 server_key[SCRAM_KEY_LEN];
+
+ parse_scram_secret(current_secrets[i], &iterations, &salt1,
+ stored_key, server_key);
+
+ if (salt2 != NULL) {
+ if (strcmp(salt1, salt2)) {
+ *logdetail = psprintf(_("inconsistent salts, clearing password"));
+ *salt = NULL;
+ return false;
+ }
+ }
+ else
+ salt2 = salt1;
+ }
+
+ }
+ for (i = 0; i < num; i++)
+ pfree(current_secrets[i]);
+ pfree(current_secrets);
+ if (salt2)
+ *salt = pstrdup(salt2);
+ return true;
+}
+
/*
* CREATE ROLE
*/
@@ -117,6 +182,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
ListCell *item;
ListCell *option;
char *password = NULL; /* user password */
+ char *passname = NULL; /* name of the password for managing multiple passwords */
bool issuper = false; /* Make the user a superuser? */
bool inherit = true; /* Auto inherit privileges? */
bool createrole = false; /* Can this user create roles? */
@@ -144,6 +210,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
DefElem *dadminmembers = NULL;
DefElem *dvalidUntil = NULL;
DefElem *dbypassRLS = NULL;
+ DefElem *dpassName = NULL;
+
/* The defaults can vary depending on the original statement type */
switch (stmt->stmt_type)
@@ -246,6 +314,13 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
errorConflictingDefElem(defel, pstate);
dbypassRLS = defel;
}
+ else if (strcmp(defel->defname, "passname") == 0)
+ {
+ if (dpassName)
+ errorConflictingDefElem(defel, pstate);
+ dpassName = defel;
+ }
+
else
elog(ERROR, "option \"%s\" not recognized",
defel->defname);
@@ -283,6 +358,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
validUntil = strVal(dvalidUntil->arg);
if (dbypassRLS)
bypassrls = boolVal(dbypassRLS->arg);
+ if (dpassName)
+ passname = strVal(dpassName->arg);
/* Check some permissions first */
if (issuper)
@@ -424,8 +501,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
if (password)
{
- char *shadow_pass;
- char *logdetail;
+ char *shadow_pass, *salt;
+ const char *logdetail;
Datum new_password_record[Natts_pg_auth_password];
bool new_password_record_nulls[Natts_pg_auth_password];
Relation pg_auth_password_rel;
@@ -453,12 +530,18 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
else
{
/* Encrypt the password to the requested format. */
- shadow_pass = encrypt_password(Password_encryption, stmt->role,
+ validate_and_get_salt(stmt->role, &salt, &logdetail);
+ shadow_pass = encrypt_password(Password_encryption, salt,
password);
MemSet(new_password_record, 0, sizeof(new_password_record));
MemSet(new_password_record_nulls, false, sizeof(new_password_record_nulls));
-
+ if (passname != NULL)
+ new_password_record[Anum_pg_auth_password_name - 1] =
+ DirectFunctionCall1(namein, CStringGetDatum(passname));
+ else
+ new_password_record[Anum_pg_auth_password_name - 1] =
+ DirectFunctionCall1(namein, CStringGetDatum(default_passname));
/* open password table and insert it. */
pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
pg_auth_password_dsc = RelationGetDescr(pg_auth_password_rel);
@@ -560,6 +643,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
ListCell *option;
char *rolename;
char *password = NULL; /* user password */
+ char *passname = NULL; /* password name for managing multiple passwords */
int connlimit = -1; /* maximum connections allowed */
char *validUntil = NULL; /* time the login is valid until */
Datum validUntil_datum; /* same, as timestamptz Datum */
@@ -575,6 +659,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
DefElem *drolemembers = NULL;
DefElem *dvalidUntil = NULL;
DefElem *dbypassRLS = NULL;
+ DefElem *dpassName = NULL;
Oid roleid;
check_rolespec_name(stmt->role,
@@ -652,6 +737,12 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
errorConflictingDefElem(defel, pstate);
dbypassRLS = defel;
}
+ else if (strcmp(defel->defname, "passname") == 0)
+ {
+ if (dpassName)
+ errorConflictingDefElem(defel, pstate);
+ dpassName = defel;
+ }
else
elog(ERROR, "option \"%s\" not recognized",
defel->defname);
@@ -669,6 +760,8 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
}
if (dvalidUntil)
validUntil = strVal(dvalidUntil->arg);
+ if (dpassName)
+ passname = strVal(dpassName->arg);
/*
* Scan the pg_authid relation to be certain the user exists.
@@ -805,8 +898,9 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
/* password */
if (password)
{
- char *shadow_pass;
- const char *logdetail = NULL;
+ char *salt = NULL;
+ const char *logdetail = NULL;
+ char *shadow_pass;
/* Like in CREATE USER, don't allow an empty password. */
if (password[0] == '\0' ||
@@ -818,13 +912,26 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
}
else
{
- /* Encrypt the password to the requested format. */
- shadow_pass = encrypt_password(Password_encryption, rolename,
- password);
- new_password_record[Anum_pg_auth_password_password - 1] =
- CStringGetTextDatum(shadow_pass);
+ if (!validate_and_get_salt(rolename, &salt, &logdetail))
+ new_password_record_nulls[Anum_pg_auth_password_password - 1] = true;
+ else
+ {
+ /* Encrypt the password to the requested format. */
+ shadow_pass = encrypt_password(Password_encryption, salt,
+ password);
+ new_password_record[Anum_pg_auth_password_password - 1] =
+ CStringGetTextDatum(shadow_pass);
+
+ new_password_record_repl[Anum_pg_auth_password_password - 1] = true;
+
+ if (passname)
+ new_password_record[Anum_pg_auth_password_name - 1] =
+ DirectFunctionCall1(namein, CStringGetDatum(passname));
+ else
+ new_password_record[Anum_pg_auth_password_name - 1] =
+ DirectFunctionCall1(namein, CStringGetDatum(default_passname));
+ }
}
- new_password_record_repl[Anum_pg_auth_password_password - 1] = true;
}
/* unset password */
@@ -859,7 +966,10 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
pg_auth_password_dsc = RelationGetDescr(pg_auth_password_rel);
- password_tuple = SearchSysCache1(AUTHPASSWORD, ObjectIdGetDatum(roleid));
+ if (dpassName)
+ password_tuple = SearchSysCache2(AUTHPASSWORDNAME, ObjectIdGetDatum(roleid), CStringGetDatum(passname));
+ else
+ password_tuple = SearchSysCache2(AUTHPASSWORDNAME, ObjectIdGetDatum(roleid), CStringGetDatum(default_passname));
if (new_password_record_nulls[Anum_pg_auth_password_password - 1] == true) /* delete existing password */
{
@@ -1157,14 +1267,23 @@ DropRole(DropRoleStmt *stmt)
DeleteSharedSecurityLabel(roleid, AuthIdRelationId);
/*
- * Drop password
+ * Drop password(s)
*/
- tuple = SearchSysCache1(AUTHPASSWORD, roleid);
- if (HeapTupleIsValid(tuple)) {
- CatalogTupleDelete(pg_auth_password_rel, &tuple->t_self);
- ReleaseSysCache(tuple);
+ ScanKeyInit(&scankey,
+ Anum_pg_auth_password_roleid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(roleid));
+
+ sscan = systable_beginscan(pg_auth_password_rel, AuthPasswordRoleOidIndexId,
+ true, NULL, 1, &scankey);
+
+ while (HeapTupleIsValid(tmp_tuple = systable_getnext(sscan)))
+ {
+ CatalogTupleDelete(pg_auth_password_rel, &tmp_tuple->t_self);
}
+ systable_endscan(sscan);
+
/*
* Remove settings for this role.
*/
@@ -1201,6 +1320,9 @@ RenameRole(const char *oldname, const char *newname)
newtuple,
passtuple;
TupleDesc dsc;
+ ScanKeyData scankey;
+ SysScanDesc sscan;
+
Relation rel;
Datum datum;
bool isnull = true;
@@ -1211,6 +1333,7 @@ RenameRole(const char *oldname, const char *newname)
Oid roleid;
ObjectAddress address;
Form_pg_authid authform;
+ Relation pg_auth_password_rel;
rel = table_open(AuthIdRelationId, RowExclusiveLock);
dsc = RelationGetDescr(rel);
@@ -1301,28 +1424,38 @@ RenameRole(const char *oldname, const char *newname)
CStringGetDatum(newname));
repl_null[Anum_pg_authid_rolname - 1] = false;
- passtuple = SearchSysCache1(AUTHPASSWORD, roleid);
+ ScanKeyInit(&scankey,
+ Anum_pg_auth_password_roleid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(roleid));
+
+ pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
- if (HeapTupleIsValid(passtuple))
- datum = SysCacheGetAttr(AUTHPASSWORD, passtuple,
- Anum_pg_auth_password_password, &isnull);
+ sscan = systable_beginscan(pg_auth_password_rel, AuthPasswordRoleOidIndexId,
+ true, NULL, 1, &scankey);
- if (!isnull && get_password_type(TextDatumGetCString(datum)) == PASSWORD_TYPE_MD5)
+ while (HeapTupleIsValid(passtuple = systable_getnext(sscan)))
{
- Relation pg_auth_password_rel;
+ datum = SysCacheGetAttr(AUTHPASSWORDNAME, passtuple,
+ Anum_pg_auth_password_password, &isnull);
- /* MD5 uses the username as salt, so just clear it on a rename */
- pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
+ if (!isnull && get_password_type(TextDatumGetCString(datum)) == PASSWORD_TYPE_MD5)
+ {
- if (HeapTupleIsValid(passtuple)) {
- CatalogTupleDelete(pg_auth_password_rel, &passtuple->t_self);
- ReleaseSysCache(passtuple);
+ /* MD5 uses the username as salt, so just clear it on a rename */
+
+ if (HeapTupleIsValid(passtuple)) {
+ CatalogTupleDelete(pg_auth_password_rel, &passtuple->t_self);
+ ereport(NOTICE,
+ (errmsg("MD5 password cleared because of role rename")));
+
+ }
}
- table_close(pg_auth_password_rel, NoLock);
- ereport(NOTICE,
- (errmsg("MD5 password cleared because of role rename")));
}
+ systable_endscan(sscan);
+ table_close(pg_auth_password_rel, NoLock);
+
newtuple = heap_modify_tuple(oldtuple, dsc, repl_val, repl_null, repl_repl);
CatalogTupleUpdate(rel, &oldtuple->t_self, newtuple);
diff --git a/src/backend/libpq/auth-sasl.c b/src/backend/libpq/auth-sasl.c
index 805b3695b78..652c18fa94a 100644
--- a/src/backend/libpq/auth-sasl.c
+++ b/src/backend/libpq/auth-sasl.c
@@ -49,7 +49,7 @@
* should just pass NULL.
*/
int
-CheckSASLAuth(const pg_be_sasl_mech *mech, Port *port, char *shadow_pass,
+CheckSASLAuth(const pg_be_sasl_mech *mech, Port *port, const char **passwords, int num,
const char **logdetail)
{
StringInfoData sasl_mechs;
@@ -136,7 +136,7 @@ CheckSASLAuth(const pg_be_sasl_mech *mech, Port *port, char *shadow_pass,
* This is because we don't want to reveal to an attacker what
* usernames are valid, nor which users have a valid password.
*/
- opaq = mech->init(port, selected_mech, shadow_pass);
+ opaq = mech->init(port, selected_mech, passwords, num);
inputlen = pq_getmsgint(&buf, 4);
if (inputlen == -1)
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index 795f1cba555..95db4aaf958 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -109,7 +109,7 @@
static void scram_get_mechanisms(Port *port, StringInfo buf);
static void *scram_init(Port *port, const char *selected_mech,
- const char *shadow_pass);
+ const char **secrets, const int num_secrets);
static int scram_exchange(void *opaq, const char *input, int inputlen,
char **output, int *outputlen,
const char **logdetail);
@@ -132,6 +132,13 @@ typedef enum
SCRAM_AUTH_FINISHED
} scram_state_enum;
+typedef struct
+{
+ uint8 StoredKey[SCRAM_KEY_LEN];
+ uint8 ServerKey[SCRAM_KEY_LEN];
+} scram_secret;
+
+
typedef struct
{
scram_state_enum state;
@@ -141,10 +148,16 @@ typedef struct
Port *port;
bool channel_binding_in_use;
+ /*
+ * The salt and iterations must be the same for all
+ * secrets since they are sent as part of the initial message
+ */
int iterations;
char *salt; /* base64-encoded */
- uint8 StoredKey[SCRAM_KEY_LEN];
- uint8 ServerKey[SCRAM_KEY_LEN];
+ /* Array of possible secrets */
+ scram_secret *secrets;
+ int num_secrets;
+ int chosen_secret; /* secret chosen during final client message */
/* Fields of the first message from client */
char cbind_flag;
@@ -220,17 +233,20 @@ scram_get_mechanisms(Port *port, StringInfo buf)
* It should be one of the mechanisms that we support, as returned by
* scram_get_mechanisms().
*
- * 'shadow_pass' is the role's stored secret, from pg_auth_password.password.
+ * 'passwords' are the role's stored secrets, from pg_auth_password.password.
* The username was provided by the client in the startup message, and is
* available in port->user_name. If 'shadow_pass' is NULL, we still perform
* an authentication exchange, but it will fail, as if an incorrect password
* was given.
*/
static void *
-scram_init(Port *port, const char *selected_mech, const char *shadow_pass)
+scram_init(Port *port, const char *selected_mech, const char **secrets, const int num_secrets)
{
scram_state *state;
- bool got_secret;
+ bool got_secret = false;
+ int i;
+ int iterations;
+ char *salt = NULL; /* base64-encoded */
state = (scram_state *) palloc0(sizeof(scram_state));
state->port = port;
@@ -260,46 +276,48 @@ scram_init(Port *port, const char *selected_mech, const char *shadow_pass)
/*
* Parse the stored secret.
*/
- if (shadow_pass)
+ if (secrets)
{
- int password_type = get_password_type(shadow_pass);
-
- if (password_type == PASSWORD_TYPE_SCRAM_SHA_256)
+ state->secrets = palloc0(sizeof(scram_secret) * num_secrets);
+ state->num_secrets = num_secrets;
+ for (i = 0; i < num_secrets; i++)
{
- if (parse_scram_secret(shadow_pass, &state->iterations, &state->salt,
- state->StoredKey, state->ServerKey))
- got_secret = true;
- else
+ int password_type = get_password_type(secrets[i]);
+
+ if (password_type == PASSWORD_TYPE_SCRAM_SHA_256)
{
- /*
- * The password looked like a SCRAM secret, but could not be
- * parsed.
- */
- ereport(LOG,
- (errmsg("invalid SCRAM secret for user \"%s\"",
- state->port->user_name)));
- got_secret = false;
+
+ if (parse_scram_secret(secrets[i], &state->iterations, &state->salt,
+ state->secrets[i].StoredKey, state->secrets[i].ServerKey))
+ {
+ if (salt) {
+ /* The stored iterations and salt must match or we cannot proceed, allow failure via mock */
+ if (strcmp(salt, state->salt) || iterations != state->iterations) {
+ ereport(WARNING, (errmsg("inconsistent salt or iterations for user \"%s\"",
+ state->port->user_name)));
+ got_secret = false; /* fail and allow mock creditials to be created */
+ break;
+ }
+ }
+ else
+ {
+ salt = state->salt;
+ iterations = state->iterations;
+ got_secret = true; /* We got at least one good SCRAM secret */
+ }
+ }
+ else
+ {
+ /*
+ * The password looked like a SCRAM secret, but could not be
+ * parsed.
+ */
+ ereport(LOG,
+ (errmsg("invalid SCRAM secret for user \"%s\"",
+ state->port->user_name)));
+ }
}
}
- else
- {
- /*
- * The user doesn't have SCRAM secret. (You cannot do SCRAM
- * authentication with an MD5 hash.)
- */
- state->logdetail = psprintf(_("User \"%s\" does not have a valid SCRAM secret."),
- state->port->user_name);
- got_secret = false;
- }
- }
- else
- {
- /*
- * The caller requested us to perform a dummy authentication. This is
- * considered normal, since the caller requested it, so don't set log
- * detail.
- */
- got_secret = false;
}
/*
@@ -310,8 +328,10 @@ scram_init(Port *port, const char *selected_mech, const char *shadow_pass)
*/
if (!got_secret)
{
+ state->secrets = palloc0(sizeof(scram_secret));
+
mock_scram_secret(state->port->user_name, &state->iterations,
- &state->salt, state->StoredKey, state->ServerKey);
+ &state->salt, state->secrets[0].StoredKey, state->secrets[0].ServerKey);
state->doomed = true;
}
@@ -459,7 +479,7 @@ scram_exchange(void *opaq, const char *input, int inputlen,
* The result is palloc'd, so caller is responsible for freeing it.
*/
char *
-pg_be_scram_build_secret(const char *password)
+pg_be_scram_build_secret(const char *password, const char *salt)
{
char *prep_password;
pg_saslprep_rc rc;
@@ -476,12 +496,14 @@ pg_be_scram_build_secret(const char *password)
if (rc == SASLPREP_SUCCESS)
password = (const char *) prep_password;
- /* Generate random salt */
- if (!pg_strong_random(saltbuf, SCRAM_DEFAULT_SALT_LEN))
+ /* Use passed in salt or generate random salt */
+ if (!salt && !pg_strong_random(saltbuf, SCRAM_DEFAULT_SALT_LEN))
ereport(ERROR,
(errcode(ERRCODE_INTERNAL_ERROR),
errmsg("could not generate random salt")));
-
+ else if (salt)
+ pg_b64_decode(salt, strlen(salt), saltbuf, SCRAM_DEFAULT_SALT_LEN);
+
result = scram_build_secret(saltbuf, SCRAM_DEFAULT_SALT_LEN,
SCRAM_DEFAULT_ITERATIONS, password,
&errstr);
@@ -1114,47 +1136,57 @@ verify_client_proof(scram_state *state)
uint8 ClientSignature[SCRAM_KEY_LEN];
uint8 ClientKey[SCRAM_KEY_LEN];
uint8 client_StoredKey[SCRAM_KEY_LEN];
- pg_hmac_ctx *ctx = pg_hmac_create(PG_SHA256);
- int i;
+ pg_hmac_ctx *ctx;
+ int i, j;
const char *errstr = NULL;
-
/*
* Calculate ClientSignature. Note that we don't log directly a failure
* here even when processing the calculations as this could involve a mock
* authentication.
*/
- if (pg_hmac_init(ctx, state->StoredKey, SCRAM_KEY_LEN) < 0 ||
- pg_hmac_update(ctx,
- (uint8 *) state->client_first_message_bare,
- strlen(state->client_first_message_bare)) < 0 ||
- pg_hmac_update(ctx, (uint8 *) ",", 1) < 0 ||
- pg_hmac_update(ctx,
- (uint8 *) state->server_first_message,
- strlen(state->server_first_message)) < 0 ||
- pg_hmac_update(ctx, (uint8 *) ",", 1) < 0 ||
- pg_hmac_update(ctx,
- (uint8 *) state->client_final_message_without_proof,
- strlen(state->client_final_message_without_proof)) < 0 ||
- pg_hmac_final(ctx, ClientSignature, sizeof(ClientSignature)) < 0)
+ for (j = 0; j < state->num_secrets; j++)
{
- elog(ERROR, "could not calculate client signature: %s",
- pg_hmac_error(ctx));
- }
+ ctx = pg_hmac_create(PG_SHA256);
+ elog(LOG, "Trying to verify password %d", j);
+
+ if (pg_hmac_init(ctx, state->secrets[j].StoredKey, SCRAM_KEY_LEN) < 0 ||
+ pg_hmac_update(ctx,
+ (uint8 *) state->client_first_message_bare,
+ strlen(state->client_first_message_bare)) < 0 ||
+ pg_hmac_update(ctx, (uint8 *) ",", 1) < 0 ||
+ pg_hmac_update(ctx,
+ (uint8 *) state->server_first_message,
+ strlen(state->server_first_message)) < 0 ||
+ pg_hmac_update(ctx, (uint8 *) ",", 1) < 0 ||
+ pg_hmac_update(ctx,
+ (uint8 *) state->client_final_message_without_proof,
+ strlen(state->client_final_message_without_proof)) < 0 ||
+ pg_hmac_final(ctx, ClientSignature, sizeof(ClientSignature)) < 0)
+ {
+ elog(LOG, "could not calculate client signature");
+ pg_hmac_free(ctx);
+ continue;
+ }
- pg_hmac_free(ctx);
+ elog(LOG, "success on %d", j);
- /* Extract the ClientKey that the client calculated from the proof */
- for (i = 0; i < SCRAM_KEY_LEN; i++)
- ClientKey[i] = state->ClientProof[i] ^ ClientSignature[i];
+ pg_hmac_free(ctx);
- /* Hash it one more time, and compare with StoredKey */
- if (scram_H(ClientKey, SCRAM_KEY_LEN, client_StoredKey, &errstr) < 0)
- elog(ERROR, "could not hash stored key: %s", errstr);
+ for (i = 0; i < SCRAM_KEY_LEN; i++)
+ ClientKey[i] = state->ClientProof[i] ^ ClientSignature[i];
- if (memcmp(client_StoredKey, state->StoredKey, SCRAM_KEY_LEN) != 0)
- return false;
+ /* Hash it one more time, and compare with StoredKey */
+ if (scram_H(ClientKey, SCRAM_KEY_LEN, client_StoredKey, &errstr) < 0)
+ elog(ERROR, "could not hash stored key: %s", errstr);
- return true;
+ if (memcmp(client_StoredKey, state->secrets[j].StoredKey, SCRAM_KEY_LEN) == 0) {
+ elog(LOG, "Moving forward with Password %d", j);
+ state->chosen_secret = j;
+ return true;
+ }
+ }
+
+ return false;
}
/*
@@ -1380,7 +1412,7 @@ build_server_final_message(scram_state *state)
pg_hmac_ctx *ctx = pg_hmac_create(PG_SHA256);
/* calculate ServerSignature */
- if (pg_hmac_init(ctx, state->ServerKey, SCRAM_KEY_LEN) < 0 ||
+ if (pg_hmac_init(ctx, state->secrets[state->chosen_secret].ServerKey, SCRAM_KEY_LEN) < 0 ||
pg_hmac_update(ctx,
(uint8 *) state->client_first_message_bare,
strlen(state->client_first_message_bare)) < 0 ||
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 5cfde1eaa13..5569599ab31 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -58,8 +58,7 @@ static void set_authn_id(Port *port, const char *id);
static int CheckPasswordAuth(Port *port, const char **logdetail);
static int CheckPWChallengeAuth(Port *port, const char **logdetail);
-static int CheckMD5Auth(Port *port, char *shadow_pass,
- const char **logdetail);
+static int CheckMD5Auth(Port *port, const char **passwords, int num, const char **logdetail);
/*----------------------------------------------------------------
@@ -790,8 +789,9 @@ static int
CheckPasswordAuth(Port *port, const char **logdetail)
{
char *passwd;
- int result;
- char *shadow_pass;
+ int result = STATUS_ERROR;
+ int i, num;
+ char **passwords;
sendAuthRequest(port, AUTH_REQ_PASSWORD, NULL, 0);
@@ -799,17 +799,21 @@ CheckPasswordAuth(Port *port, const char **logdetail)
if (passwd == NULL)
return STATUS_EOF; /* client wouldn't send password */
- shadow_pass = get_role_password(port->user_name, logdetail);
- if (shadow_pass)
- {
- result = plain_crypt_verify(port->user_name, shadow_pass, passwd,
- logdetail);
- }
- else
- result = STATUS_ERROR;
+ passwords = get_role_passwords(port->user_name, logdetail, &num);
+ if (passwords != NULL) {
+ for (i = 0; i < num; i++)
+ {
+ result = plain_crypt_verify(port->user_name, passwords[i], passwd,
+ logdetail);
+ if (result == STATUS_OK)
+ break; /* Found a matching password, no need to try any others */
+ }
+ for (i = 0; passwords[i] != NULL; i++)
+ pfree(passwords[i]);
- if (shadow_pass)
- pfree(shadow_pass);
+ pfree(passwords);
+ }
+
pfree(passwd);
if (result == STATUS_OK)
@@ -824,29 +828,15 @@ CheckPasswordAuth(Port *port, const char **logdetail)
static int
CheckPWChallengeAuth(Port *port, const char **logdetail)
{
- int auth_result;
- char *shadow_pass;
- PasswordType pwtype;
+ int auth_result = STATUS_ERROR;
+ int i, num;
+ char **passwords;
Assert(port->hba->auth_method == uaSCRAM ||
port->hba->auth_method == uaMD5);
- /* First look up the user's password. */
- shadow_pass = get_role_password(port->user_name, logdetail);
-
- /*
- * If the user does not exist, or has no password or it's expired, we
- * still go through the motions of authentication, to avoid revealing to
- * the client that the user didn't exist. If 'md5' is allowed, we choose
- * whether to use 'md5' or 'scram-sha-256' authentication based on current
- * password_encryption setting. The idea is that most genuine users
- * probably have a password of that type, and if we pretend that this user
- * had a password of that type, too, it "blends in" best.
- */
- if (!shadow_pass)
- pwtype = Password_encryption;
- else
- pwtype = get_password_type(shadow_pass);
+ /* First look up the user's passwords. */
+ passwords = get_role_passwords(port->user_name, logdetail, &num);
/*
* If 'md5' authentication is allowed, decide whether to perform 'md5' or
@@ -858,23 +848,17 @@ CheckPWChallengeAuth(Port *port, const char **logdetail)
* had an MD5 password, CheckSASLAuth() with the SCRAM mechanism will
* fail.
*/
- if (port->hba->auth_method == uaMD5 && pwtype == PASSWORD_TYPE_MD5)
- auth_result = CheckMD5Auth(port, shadow_pass, logdetail);
- else
- auth_result = CheckSASLAuth(&pg_be_scram_mech, port, shadow_pass,
- logdetail);
+ if (passwords != NULL) {
+ if (port->hba->auth_method == uaMD5)
+ auth_result = CheckMD5Auth(port, (const char **) passwords, num, logdetail);
+ else
+ auth_result = CheckSASLAuth(&pg_be_scram_mech, port, (const char **) passwords, num,
+ logdetail);
- if (shadow_pass)
- pfree(shadow_pass);
+ for (i = 0; i < num; i++)
+ pfree(passwords[i]);
- /*
- * If get_role_password() returned error, return error, even if the
- * authentication succeeded.
- */
- if (!shadow_pass)
- {
- Assert(auth_result != STATUS_OK);
- return STATUS_ERROR;
+ pfree(passwords);
}
if (auth_result == STATUS_OK)
@@ -884,11 +868,12 @@ CheckPWChallengeAuth(Port *port, const char **logdetail)
}
static int
-CheckMD5Auth(Port *port, char *shadow_pass, const char **logdetail)
+CheckMD5Auth(Port *port, const char **passwords, int num, const char **logdetail)
{
char md5Salt[4]; /* Password salt */
char *passwd;
int result;
+ int i;
if (Db_user_namespace)
ereport(FATAL,
@@ -909,12 +894,13 @@ CheckMD5Auth(Port *port, char *shadow_pass, const char **logdetail)
if (passwd == NULL)
return STATUS_EOF; /* client wouldn't send password */
- if (shadow_pass)
- result = md5_crypt_verify(port->user_name, shadow_pass, passwd,
+ for (i = 0; i < num; i++)
+ {
+ result = md5_crypt_verify(port->user_name, passwords[i], passwd,
md5Salt, 4, logdetail);
- else
- result = STATUS_ERROR;
-
+ if (result == STATUS_OK)
+ break;
+ }
pfree(passwd);
return result;
diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index 745e61034c2..09b6bebf40c 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -23,6 +23,7 @@
#include "libpq/scram.h"
#include "miscadmin.h"
#include "utils/builtins.h"
+#include "utils/catcache.h"
#include "utils/syscache.h"
#include "utils/timestamp.h"
@@ -34,13 +35,15 @@
* for the postmaster log, in *logdetail. The error reason should *not* be
* sent to the client, to avoid giving away user information!
*/
-char *
-get_role_password(const char *role, const char **logdetail)
+char **
+get_role_passwords(const char *role, const char **logdetail, int *num)
{
- HeapTuple roleTup, passTup;
+ HeapTuple roleTup;
Datum datum;
bool isnull;
- char *shadow_pass;
+ char **passwords;
+ CatCList *passlist;
+ int i;
/* Get role info from pg_authid */
roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(role));
@@ -52,30 +55,34 @@ get_role_password(const char *role, const char **logdetail)
}
datum = SysCacheGetAttr(AUTHNAME, roleTup, Anum_pg_authid_oid, &isnull);
-
- passTup = SearchSysCache1(AUTHPASSWORD, datum);
+ ReleaseSysCache(roleTup);
+ /* Find any existing password that is not the one being updated to get the salt */
+ passlist = SearchSysCacheList1(AUTHPASSWORDNAME, datum);
+ *num = passlist->n_members;
- if (!HeapTupleIsValid(passTup))
+ if (passlist->n_members == 0)
{
+ *num = 0;
*logdetail = psprintf(_("User \"%s\" has no password assigned."),
role);
+ ReleaseCatCacheList(passlist);
return NULL; /* user has no password */
}
- datum = SysCacheGetAttr(AUTHPASSWORD, passTup,
- Anum_pg_auth_password_password, &isnull);
- ReleaseSysCache(roleTup);
- if (isnull) /* this should not happen any more but just in case */
+ passwords = palloc0(sizeof(char *) * passlist->n_members);
+
+ for (i = 0; i < passlist->n_members; i++)
{
- ReleaseSysCache(passTup);
- *logdetail = psprintf(_("User \"%s\" has no password assigned."),
- role);
- return NULL; /* user has no password */
+ HeapTuple tup = &passlist->members[i]->tuple;
+
+ datum = SysCacheGetAttr(AUTHPASSWORDNAME, tup,
+ Anum_pg_auth_password_password, &isnull);
+ passwords[i] = TextDatumGetCString(datum);
}
- shadow_pass = TextDatumGetCString(datum);
- ReleaseSysCache(passTup);
- return shadow_pass;
+ ReleaseCatCacheList(passlist);
+
+ return passwords;
}
/*
@@ -107,7 +114,7 @@ get_password_type(const char *shadow_pass)
* hash, so it is stored as it is regardless of the requested type.
*/
char *
-encrypt_password(PasswordType target_type, const char *role,
+encrypt_password(PasswordType target_type, const char *salt,
const char *password)
{
PasswordType guessed_type = get_password_type(password);
@@ -128,13 +135,13 @@ encrypt_password(PasswordType target_type, const char *role,
case PASSWORD_TYPE_MD5:
encrypted_password = palloc(MD5_PASSWD_LEN + 1);
- if (!pg_md5_encrypt(password, role, strlen(role),
+ if (!pg_md5_encrypt(password, salt, strlen(salt),
encrypted_password, &errstr))
- elog(ERROR, "password encryption failed: %s", errstr);
+ elog(ERROR, "password encryption failed %s", errstr);
return encrypted_password;
case PASSWORD_TYPE_SCRAM_SHA_256:
- return pg_be_scram_build_secret(password);
+ return pg_be_scram_build_secret(password, salt);
case PASSWORD_TYPE_PLAINTEXT:
elog(ERROR, "cannot encrypt password with 'plaintext'");
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c9941d9cb4f..c3cb6c76aa5 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -826,8 +826,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
ORDER ORDINALITY OTHERS OUT_P OUTER_P
OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER
- PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD PATH
- PLACING PLAN PLANS POLICY
+
+ PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSNAME PASSWORD
+ PATH PLACING PLAN PLANS POLICY
POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION
@@ -1256,6 +1257,15 @@ AlterOptRoleElem:
errhint("Remove UNENCRYPTED to store the password in encrypted form instead."),
parser_errposition(@1)));
}
+ | PASSNAME Sconst
+ {
+ $$ = makeDefElem("passname",
+ (Node *)makeString($2), @1);
+ }
+ | VALID FOR Sconst
+ {
+ $$ = makeDefElem("validFor", (Node *)makeString($3), @1);
+ }
| INHERIT
{
$$ = makeDefElem("inherit", (Node *)makeBoolean(true), @1);
@@ -17082,6 +17092,7 @@ unreserved_keyword:
| PARTIAL
| PARTITION
| PASSING
+ | PASSNAME
| PASSWORD
| PATH
| PLAN
@@ -17700,6 +17711,7 @@ bare_label_keyword:
| PARTIAL
| PARTITION
| PASSING
+ | PASSNAME
| PASSWORD
| PATH
| PLACING
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index 356e9cde6fb..c421ac0b19f 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -1104,7 +1104,7 @@ IndexScanOK(CatCache *cache, ScanKey cur_skey)
case AUTHNAME:
case AUTHOID:
- case AUTHPASSWORD:
+ case AUTHPASSWORDNAME:
case AUTHMEMMEMROLE:
case DATABASEOID:
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index de2b3ec1ebf..9879120bf76 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -256,12 +256,12 @@ static const struct cachedesc cacheinfo[] = {
},
8
},
- {AuthPasswordRelationId, /* AUTHPASSWORD */
+ {AuthPasswordRelationId, /* AUTHPASSWORDNAME */
AuthPasswordRoleOidIndexId,
- 1,
+ 2,
{
Anum_pg_auth_password_roleid,
- 0,
+ Anum_pg_auth_password_name,
0,
0
},
diff --git a/src/include/catalog/pg_auth_password.h b/src/include/catalog/pg_auth_password.h
index beaa2d40b90..4181caad30b 100644
--- a/src/include/catalog/pg_auth_password.h
+++ b/src/include/catalog/pg_auth_password.h
@@ -24,11 +24,13 @@
* typedef struct FormData_pg_auth_password
* ----------------
*/
-CATALOG(pg_auth_password,4548,AuthPasswordRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(4549,AuthPasswordRelation_Rowtype_Id) BKI_SCHEMA_MACRO
+CATALOG(pg_auth_password,4551,AuthPasswordRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(4552,AuthPasswordRelation_Rowtype_Id) BKI_SCHEMA_MACRO
{
Oid roleid BKI_LOOKUP(pg_authid); /* ID of a role */
+ NameData name; /* name of password for multiple password support */
+
#ifdef CATALOG_VARLEN /* variable-length fields start here */
- text password; /* password */
+ text password BKI_FORCE_NOT_NULL; /* password */
#endif
} FormData_pg_auth_password;
@@ -44,7 +46,6 @@ DECLARE_TOAST(pg_auth_password, 4175, 4176);
typedef FormData_pg_auth_password *Form_pg_auth_password;
-DECLARE_UNIQUE_INDEX_PKEY(pg_auth_password_roleoid_index, 4550, AuthPasswordRoleOidIndexId, on pg_auth_password using btree(roleid oid_ops));
-
+DECLARE_UNIQUE_INDEX_PKEY(pg_auth_password_roleoid_index, 4553, AuthPasswordRoleOidIndexId, on pg_auth_password using btree(roleid oid_ops, name name_ops));
#endif /* PG_AUTH_PASSWORD_H */
diff --git a/src/include/commands/user.h b/src/include/commands/user.h
index 3e1aae57854..1c14f60a05b 100644
--- a/src/include/commands/user.h
+++ b/src/include/commands/user.h
@@ -24,7 +24,7 @@ typedef void (*check_password_hook_type) (const char *username, const char *shad
extern PGDLLIMPORT check_password_hook_type check_password_hook;
-extern bool is_role_valid(const char *rolename, char **logdetail);
+extern bool is_role_valid(const char *rolename, const char **logdetail);
extern Oid CreateRole(ParseState *pstate, CreateRoleStmt *stmt);
extern Oid AlterRole(ParseState *pstate, AlterRoleStmt *stmt);
extern Oid AlterRoleSet(AlterRoleSetStmt *stmt);
diff --git a/src/include/libpq/crypt.h b/src/include/libpq/crypt.h
index b8ff8ccb417..84c9c4b4f48 100644
--- a/src/include/libpq/crypt.h
+++ b/src/include/libpq/crypt.h
@@ -35,7 +35,7 @@ extern PasswordType get_password_type(const char *shadow_pass);
extern char *encrypt_password(PasswordType target_type, const char *role,
const char *password);
-extern char *get_role_password(const char *role, const char **logdetail);
+extern char **get_role_passwords(const char *role, const char **logdetail, int *num);
extern int md5_crypt_verify(const char *role, const char *shadow_pass,
const char *client_pass, const char *md5_salt,
diff --git a/src/include/libpq/sasl.h b/src/include/libpq/sasl.h
index 39ccf8f0e31..27b4e570333 100644
--- a/src/include/libpq/sasl.h
+++ b/src/include/libpq/sasl.h
@@ -77,7 +77,7 @@ typedef struct pg_be_sasl_mech
* disclosing valid user names.
*---------
*/
- void *(*init) (Port *port, const char *mech, const char *shadow_pass);
+ void *(*init) (Port *port, const char *mech, const char **secrets, const int num_secrets);
/*---------
* exchange()
@@ -131,6 +131,6 @@ typedef struct pg_be_sasl_mech
/* Common implementation for auth.c */
extern int CheckSASLAuth(const pg_be_sasl_mech *mech, Port *port,
- char *shadow_pass, const char **logdetail);
+ const char **passwords, int num, const char **logdetail);
#endif /* PG_SASL_H */
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index c51e848c24d..70b01b511f0 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -21,7 +21,7 @@
extern PGDLLIMPORT const pg_be_sasl_mech pg_be_scram_mech;
/* Routines to handle and check SCRAM-SHA-256 secret */
-extern char *pg_be_scram_build_secret(const char *password);
+extern char *pg_be_scram_build_secret(const char *password, const char *salt);
extern bool parse_scram_secret(const char *secret, int *iterations, char **salt,
uint8 *stored_key, uint8 *server_key);
extern bool scram_verify_plain_password(const char *username,
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 8a2ab405a28..bf84593a61c 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -334,6 +334,7 @@ PG_KEYWORD("parser", PARSER, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("partial", PARTIAL, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("passname", PASSNAME, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("path", PATH, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index c7bc648f46a..a543c181889 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -43,7 +43,7 @@ enum SysCacheIdentifier
AUTHMEMROLEMEM,
AUTHNAME,
AUTHOID,
- AUTHPASSWORD,
+ AUTHPASSWORDNAME,
CASTSOURCETARGET,
CLAAMNAMENSP,
CLAOID,
diff --git a/src/test/regress/expected/password.out b/src/test/regress/expected/password.out
index 4ffc41a5455..c6a84f86a84 100644
--- a/src/test/regress/expected/password.out
+++ b/src/test/regress/expected/password.out
@@ -120,7 +120,7 @@ CREATE ROLE regress_passwd_sha_len2 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941
-- should not contain the original salt.
SELECT rolname, password not like '%A6xHKoH/494E941doaPOYg==%' as is_rolpassword_rehashed
FROM pg_authid
- LEFT JOIN pg_auth_password p
+ LEFT JOIN pg_auth_password p
ON pg_authid.oid = p.roleid
WHERE rolname LIKE 'regress_passwd_sha_len%'
ORDER BY rolname;
--
2.35.1