From 6e272f44b48ce9749468f6289b23d0f7569f7862 Mon Sep 17 00:00:00 2001 From: Gurjeet Singh Date: Mon, 9 Oct 2023 11:54:11 -0700 Subject: [PATCH v3 3/3] Added SQL support for ALTER ROLE to manage two passwords --- src/backend/commands/user.c | 252 +++++++++++++++++- src/backend/parser/gram.y | 53 +++- .../regress/expected/password_rollover.out | 140 ++++++++++ src/test/regress/parallel_schedule | 5 + src/test/regress/sql/password_rollover.sql | 107 ++++++++ 5 files changed, 544 insertions(+), 13 deletions(-) create mode 100644 src/test/regress/expected/password_rollover.out create mode 100644 src/test/regress/sql/password_rollover.sql diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c index 9ad02e4092..4721185e71 100644 --- a/src/backend/commands/user.c +++ b/src/backend/commands/user.c @@ -721,11 +721,16 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) ListCell *option; char *rolename; char *password = NULL; /* user password */ + char *second_password = NULL; /* user's second password */ int connlimit = -1; /* maximum connections allowed */ - char *validUntil = NULL; /* time the login is valid until */ - Datum validUntil_datum; /* same, as timestamptz Datum */ + char *validUntil = NULL; /* time the password is valid until */ + Datum validUntil_datum; /* validUntil, as timestamptz Datum */ bool validUntil_null; + char *secondValidUntil = NULL;/* time the second password is valid until */ + Datum secondValidUntil_datum; /* secondValidUntil, as timestamptz Datum */ + bool secondValidUntil_null; DefElem *dpassword = NULL; + DefElem *dsecondpassword = NULL; DefElem *dissuper = NULL; DefElem *dinherit = NULL; DefElem *dcreaterole = NULL; @@ -735,10 +740,18 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) DefElem *dconnlimit = NULL; DefElem *drolemembers = NULL; DefElem *dvalidUntil = NULL; + DefElem *dfirstValidUntil = NULL; + DefElem *dsecondValidUntil = NULL; DefElem *dbypassRLS = NULL; Oid roleid; Oid currentUserId = GetUserId(); GrantRoleOptions popt; + bool overwriteFirstPassword = false; + bool addFirstPassword = false; + bool addSecondPassword = false; + bool dropFirstPassword = false; + bool dropSecondPassword = false; + bool dropAllPasswords = false; check_rolespec_name(stmt->role, _("Cannot alter reserved roles.")); @@ -750,9 +763,95 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) if (strcmp(defel->defname, "password") == 0) { - if (dpassword) + if (overwriteFirstPassword || addFirstPassword) errorConflictingDefElem(defel, pstate); dpassword = defel; + overwriteFirstPassword = true; + + if (dpassword->arg != NULL) + { + /* PASSWORD 'sometext' syntax was used */ + + /* + * Adding and dropping passwords in the same command is not + * supported. + */ + if (dropFirstPassword || dropSecondPassword || dropAllPasswords) + errorConflictingDefElem(defel, pstate); + } + else + { + /* PASSWORD NULL syntax was used */ + + if (dropFirstPassword) + errorConflictingDefElem(defel, pstate); + + /* + * Adding and dropping passwords in the same command is not + * supported. + */ + if (addFirstPassword || addSecondPassword) + errorConflictingDefElem(defel, pstate); + + dropFirstPassword = true; + } + } + else if (strcmp(defel->defname, "add-first-password") == 0) + { + if (addFirstPassword || overwriteFirstPassword) + errorConflictingDefElem(defel, pstate); + dpassword = defel; + addFirstPassword = true; + + /* + * Adding and dropping passwords in the same command is not + * supported. + */ + if (dropFirstPassword || dropSecondPassword || dropAllPasswords) + errorConflictingDefElem(defel, pstate); + } + else if (strcmp(defel->defname, "add-second-password") == 0) + { + if (dsecondpassword) + errorConflictingDefElem(defel, pstate); + dsecondpassword = defel; + addSecondPassword = true; + /* + * Adding and dropping passwords in the same command is not + * supported. + */ + if (dropFirstPassword || dropSecondPassword || dropAllPasswords) + errorConflictingDefElem(defel, pstate); + } + else if (strcmp(defel->defname, "drop-password") == 0) + { + char *which = strVal(defel->arg); + + if (strcmp(which, "first") == 0) + { + if (dropFirstPassword || dropAllPasswords) + errorConflictingDefElem(defel, pstate); + dropFirstPassword = true; + } + else if (strcmp(which, "second") == 0) + { + if (dropSecondPassword || dropAllPasswords) + errorConflictingDefElem(defel, pstate); + dropSecondPassword = true; + } + else + { + if (dropAllPasswords || dropFirstPassword || dropSecondPassword) + errorConflictingDefElem(defel, pstate); + dropAllPasswords = true; + } + + /* + * Adding and dropping passwords in the same command is not + * supported. + */ + if (addFirstPassword || addSecondPassword || overwriteFirstPassword) + errorConflictingDefElem(defel, pstate); } else if (strcmp(defel->defname, "superuser") == 0) { @@ -809,6 +908,18 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) errorConflictingDefElem(defel, pstate); dvalidUntil = defel; } + else if (strcmp(defel->defname, "first-password-valid-until") == 0) + { + if (dfirstValidUntil) + errorConflictingDefElem(defel, pstate); + dfirstValidUntil = defel; + } + else if (strcmp(defel->defname, "second-password-valid-until") == 0) + { + if (dsecondValidUntil) + errorConflictingDefElem(defel, pstate); + dsecondValidUntil = defel; + } else if (strcmp(defel->defname, "bypassrls") == 0) { if (dbypassRLS) @@ -822,6 +933,8 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) if (dpassword && dpassword->arg) password = strVal(dpassword->arg); + if (dsecondpassword) + second_password = strVal(dsecondpassword->arg); if (dconnlimit) { connlimit = intVal(dconnlimit->arg); @@ -830,8 +943,30 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid connection limit: %d", connlimit))); } + + /* + * Disallow mixing VALID UNTIL with ADD FIRST/SECOND PASSWORD. + * + * VALID UNTIL and FIRST PASSWORD VALID UNTIL are functionally identical, + * but we track them separately to prevent the confusing invocation like the + * following. + * + * ALTER ROLE x ADD SECOND PASSWORD 'y' VALID UNTIL '2020/01/01'; + * + * In the above command the user may expect the expiration of the _second_ + * password to be set to '2020/01/01', but it will lead to second password's + * expiration set to NULL and first password's expiration set to + * '2020/01/01', because a plain VALIF UNTIL applies to the _first_ + * password. + */ + if (dvalidUntil && (addFirstPassword || addSecondPassword)) + errorConflictingDefElem(dvalidUntil, pstate); + dvalidUntil = dfirstValidUntil; + if (dvalidUntil) validUntil = strVal(dvalidUntil->arg); + if (dsecondValidUntil) + secondValidUntil = strVal(dsecondValidUntil->arg); /* * Scan the pg_authid relation to be certain the user exists. @@ -867,7 +1002,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) { /* things an unprivileged user certainly can't do */ if (dinherit || dcreaterole || dcreatedb || dcanlogin || dconnlimit || - dvalidUntil || disreplication || dbypassRLS) + dvalidUntil || dsecondValidUntil || disreplication || dbypassRLS) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("permission denied to alter role"), @@ -875,7 +1010,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) "CREATEROLE", "ADMIN", rolename))); /* an unprivileged user can change their own password */ - if (dpassword && roleid != currentUserId) + if ((dpassword || dsecondpassword) && roleid != currentUserId) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("permission denied to alter role"), @@ -934,15 +1069,42 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) &validUntil_null); } + /* Convert secondvaliduntil to internal form */ + if (dsecondValidUntil) + { + secondValidUntil_datum = DirectFunctionCall3(timestamptz_in, + CStringGetDatum(secondValidUntil), + ObjectIdGetDatum(InvalidOid), + Int32GetDatum(-1)); + secondValidUntil_null = false; + } + else + { + /* fetch existing setting in case hook needs it */ + secondValidUntil_datum = SysCacheGetAttr(AUTHNAME, tuple, + Anum_pg_authid_rolsecondvaliduntil, + &secondValidUntil_null); + } + /* * Call the password checking hook if there is one defined */ - if (check_password_hook && password) - (*check_password_hook) (rolename, - password, - get_password_type(password), - validUntil_datum, - validUntil_null); + if (check_password_hook) + { + if (password) + (*check_password_hook) (rolename, + password, + get_password_type(password), + validUntil_datum, + validUntil_null); + + if (second_password) + (*check_password_hook) (rolename, + second_password, + get_password_type(second_password), + secondValidUntil_datum, + secondValidUntil_null); + } /* * Build an updated tuple, perusing the information just obtained @@ -1008,6 +1170,20 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) char *shadow_pass; const char *logdetail = NULL; + if (addFirstPassword) + { + bool firstPassword_null; + + SysCacheGetAttr(AUTHNAME, tuple, + Anum_pg_authid_rolpassword, + &firstPassword_null); + + if (!firstPassword_null) + ereport(ERROR, + (errmsg("'first' password is already in use"), + errdetail("Use ALTER ROLE DROP FIRST PASSWORD"))); + } + /* Like in CREATE USER, don't allow an empty password. */ if (password[0] == '\0' || plain_crypt_verify(rolename, password, "", &logdetail) == STATUS_OK) @@ -1034,17 +1210,69 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) new_record_repl[Anum_pg_authid_rolpassword - 1] = true; } + /* second password */ + if (second_password) + { + char *shadow_pass; + const char *logdetail = NULL; + bool secondPassword_null; + + SysCacheGetAttr(AUTHNAME, tuple, + Anum_pg_authid_rolsecondpassword, + &secondPassword_null); + + if (!secondPassword_null) + ereport(ERROR, + (errmsg("'second' password is already in use"), + errdetail("Use ALTER ROLE DROP SECOND PASSWORD"))); + + /* Like in CREATE USER, don't allow an empty password. */ + if (second_password[0] == '\0' || + plain_crypt_verify(rolename, second_password, "", &logdetail) == STATUS_OK) + { + ereport(NOTICE, + (errmsg("empty string is not a valid password, clearing password"))); + new_record_nulls[Anum_pg_authid_rolsecondpassword - 1] = true; + } + else + { + char *salt; + + if (!get_salt(rolename, &salt, &logdetail)) + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("could not get a valid salt for password"), + errdetail("%s", logdetail))); + + /* Encrypt the password to the requested format. */ + shadow_pass = encrypt_password(Password_encryption, salt, second_password); + new_record[Anum_pg_authid_rolsecondpassword - 1] = + CStringGetTextDatum(shadow_pass); + } + new_record_repl[Anum_pg_authid_rolsecondpassword - 1] = true; + } + /* unset password */ - if (dpassword && dpassword->arg == NULL) + if (dropFirstPassword || dropAllPasswords) { new_record_repl[Anum_pg_authid_rolpassword - 1] = true; new_record_nulls[Anum_pg_authid_rolpassword - 1] = true; } + if (dropSecondPassword || dropAllPasswords) + { + new_record_repl[Anum_pg_authid_rolsecondpassword - 1] = true; + new_record_nulls[Anum_pg_authid_rolsecondpassword - 1] = true; + } + /* valid until */ new_record[Anum_pg_authid_rolvaliduntil - 1] = validUntil_datum; new_record_nulls[Anum_pg_authid_rolvaliduntil - 1] = validUntil_null; new_record_repl[Anum_pg_authid_rolvaliduntil - 1] = true; + /* second password valid until */ + new_record[Anum_pg_authid_rolsecondvaliduntil - 1] = secondValidUntil_datum; + new_record_nulls[Anum_pg_authid_rolsecondvaliduntil - 1] = secondValidUntil_null; + new_record_repl[Anum_pg_authid_rolsecondvaliduntil - 1] = true; if (dbypassRLS) { diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index e56cbe77cb..6447ac4056 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -361,7 +361,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type opt_nowait_or_skip %type OptRoleList AlterOptRoleList -%type CreateOptRoleElem AlterOptRoleElem +%type CreateOptRoleElem AlterOptRoleElem AlterOnlyOptRoleElem +%type OptFirstOrSecond %type opt_type %type foreign_server_version opt_foreign_server_version @@ -1168,6 +1169,7 @@ OptRoleList: AlterOptRoleList: AlterOptRoleList AlterOptRoleElem { $$ = lappend($1, $2); } + | AlterOptRoleList AlterOnlyOptRoleElem { $$ = lappend($1, $2); } | /* EMPTY */ { $$ = NIL; } ; @@ -1263,6 +1265,55 @@ AlterOptRoleElem: } ; +OptFirstOrSecond: + FIRST_P { $$ = true; } + | SECOND_P { $$ = false; } + ; + +/* + * AlterOnlyOptRoleElem is separate from AlterOptRoleElem because these options + * are not available to the CREATE ROLE command. + */ +AlterOnlyOptRoleElem: + ADD_P OptFirstOrSecond PASSWORD Sconst + { + bool first = $2; + + if (first) + $$ = makeDefElem("add-first-password", + (Node *) makeString($4), @1); + else + $$ = makeDefElem("add-second-password", + (Node *) makeString($4), @1); + } + | DROP OptFirstOrSecond PASSWORD + { + bool first = $2; + + if (first) + $$ = makeDefElem("drop-password", + (Node *) makeString("first"), @1); + else + $$ = makeDefElem("drop-password", + (Node *) makeString("second"), @1); + } + | DROP ALL PASSWORD + { + $$ = makeDefElem("drop-all-password", (Node *) NULL, @1); + } + | OptFirstOrSecond PASSWORD VALID UNTIL Sconst + { + bool first = $1; + + if (first) + $$ = makeDefElem("first-password-valid-until", + (Node *) makeString($5), @1); + else + $$ = makeDefElem("second-password-valid-until", + (Node *) makeString($5), @1); + } + ; + CreateOptRoleElem: AlterOptRoleElem { $$ = $1; } /* The following are not supported by ALTER ROLE/USER/GROUP */ diff --git a/src/test/regress/expected/password_rollover.out b/src/test/regress/expected/password_rollover.out new file mode 100644 index 0000000000..bad6d01b61 --- /dev/null +++ b/src/test/regress/expected/password_rollover.out @@ -0,0 +1,140 @@ +-- +-- Tests for password rollovers +-- +SET password_encryption = 'md5'; +-- Create a user, as usual +CREATE ROLE regress_password_rollover1 PASSWORD 'p1' LOGIN; +-- the rolpassword field should be non-null, and others should be null +SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil + FROM pg_authid + WHERE rolname LIKE 'regress_password_rollover%'; + rolname | rolpassword | rolvaliduntil | rolsecondpassword | rolsecondvaliduntil +----------------------------+-------------------------------------+---------------+-------------------+--------------------- + regress_password_rollover1 | md54ec11153dc2e0022e0d556740a238e94 | | | +(1 row) + +-- Add another password that the user can use for authentication. +ALTER ROLE regress_password_rollover1 ADD SECOND PASSWORD 'p2'; +-- the rolpassword and rolsecondpassword fields should be non-null, and others should be null +SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil + FROM pg_authid + WHERE rolname LIKE 'regress_password_rollover%'; + rolname | rolpassword | rolvaliduntil | rolsecondpassword | rolsecondvaliduntil +----------------------------+-------------------------------------+---------------+-------------------------------------+--------------------- + regress_password_rollover1 | md54ec11153dc2e0022e0d556740a238e94 | | md5c72e860974ea678511e200ded12780b6 | +(1 row) + +-- Set second password's expiration time. +ALTER ROLE regress_password_rollover1 SECOND PASSWORD VALID UNTIL '2021/01/01'; +-- the rolvaliduntil field should be null, and other should be non-null +SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil + FROM pg_authid + WHERE rolname LIKE 'regress_password_rollover%'; + rolname | rolpassword | rolvaliduntil | rolsecondpassword | rolsecondvaliduntil +----------------------------+-------------------------------------+---------------+-------------------------------------+------------------------------ + regress_password_rollover1 | md54ec11153dc2e0022e0d556740a238e94 | | md5c72e860974ea678511e200ded12780b6 | Fri Jan 01 00:00:00 2021 PST +(1 row) + +ALTER ROLE regress_password_rollover1 FIRST PASSWORD VALID UNTIL '2022/01/01'; +-- All fields should be non-null +SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil + FROM pg_authid + WHERE rolname LIKE 'regress_password_rollover%'; + rolname | rolpassword | rolvaliduntil | rolsecondpassword | rolsecondvaliduntil +----------------------------+-------------------------------------+------------------------------+-------------------------------------+------------------------------ + regress_password_rollover1 | md54ec11153dc2e0022e0d556740a238e94 | Sat Jan 01 00:00:00 2022 PST | md5c72e860974ea678511e200ded12780b6 | Fri Jan 01 00:00:00 2021 PST +(1 row) + +-- Setting a password to null does not set its expiration time to null +ALTER ROLE regress_password_rollover1 PASSWORD NULL; +-- the rolpassword field should be null, and others should be non-null +SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil + FROM pg_authid + WHERE rolname LIKE 'regress_password_rollover%'; + rolname | rolpassword | rolvaliduntil | rolsecondpassword | rolsecondvaliduntil +----------------------------+-------------+------------------------------+-------------------------------------+------------------------------ + regress_password_rollover1 | | Sat Jan 01 00:00:00 2022 PST | md5c72e860974ea678511e200ded12780b6 | Fri Jan 01 00:00:00 2021 PST +(1 row) + +-- If, for some reason, the user wants to get rid of the latest password added. +ALTER ROLE regress_password_rollover1 DROP SECOND PASSWORD; +-- the rolpassword and rolsecondpassword fields should be null, and others should be non-null +SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil + FROM pg_authid + WHERE rolname LIKE 'regress_password_rollover%'; + rolname | rolpassword | rolvaliduntil | rolsecondpassword | rolsecondvaliduntil +----------------------------+-------------+------------------------------+-------------------+------------------------------ + regress_password_rollover1 | | Sat Jan 01 00:00:00 2022 PST | | Fri Jan 01 00:00:00 2021 PST +(1 row) + +-- Add a new password in 'second' slot +ALTER ROLE regress_password_rollover1 ADD SECOND PASSWORD 'p3' SECOND PASSWORD VALID UNTIL '2023/01/01'; +-- the rolpassword field should be null, and others should be non-null +SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil + FROM pg_authid + WHERE rolname LIKE 'regress_password_rollover%'; + rolname | rolpassword | rolvaliduntil | rolsecondpassword | rolsecondvaliduntil +----------------------------+-------------+------------------------------+-------------------------------------+------------------------------ + regress_password_rollover1 | | Sat Jan 01 00:00:00 2022 PST | md53dff5d9eee2beb63399f1900a2371fcb | Sun Jan 01 00:00:00 2023 PST +(1 row) + +-- VALID UNTIL must not be allowed when ADDing a password, to avoid the +-- confusing invocation where the command may seem to do one thing but actually +-- does something else. The following may seem like it will add a 'second' +-- password with a new expiration, but, if allowed, this will set the expiration +-- time on the _first_ password. +ALTER USER regress_password_rollover1 ADD SECOND PASSWORD 'p4' VALID UNTIL '2023/01/01'; +ERROR: conflicting or redundant options +LINE 1: ...gress_password_rollover1 ADD SECOND PASSWORD 'p4' VALID UNTI... + ^ +-- Even though both, the password and the expiration, refer to the first +-- password, we disallow it to be consistent with the previous command's +-- behaviour. +ALTER USER regress_password_rollover1 ADD FIRST PASSWORD 'p4' VALID UNTIL '2023/01/01'; +ERROR: conflicting or redundant options +LINE 1: ...egress_password_rollover1 ADD FIRST PASSWORD 'p4' VALID UNTI... + ^ +-- Set the first password +ALTER ROLE regress_password_rollover1 ADD FIRST PASSWORD 'p5'; +-- Attempting to add a password while the respective slot is occupied +-- results in error +ALTER ROLE regress_password_rollover1 ADD FIRST PASSWORD 'p6'; +ERROR: 'first' password is already in use +DETAIL: Use ALTER ROLE DROP FIRST PASSWORD +ALTER ROLE regress_password_rollover1 ADD SECOND PASSWORD 'p6'; +ERROR: 'second' password is already in use +DETAIL: Use ALTER ROLE DROP SECOND PASSWORD +ALTER ROLE regress_password_rollover1 DROP SECOND PASSWORD; +-- the rolsecondpassword field should be null, and others should be non-null +SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil + FROM pg_authid + WHERE rolname LIKE 'regress_password_rollover%'; + rolname | rolpassword | rolvaliduntil | rolsecondpassword | rolsecondvaliduntil +----------------------------+-------------------------------------+------------------------------+-------------------+------------------------------ + regress_password_rollover1 | md5cc8c5dac5560a2fead71cfba4625a2c7 | Sat Jan 01 00:00:00 2022 PST | | Sun Jan 01 00:00:00 2023 PST +(1 row) + +-- Use scram-sha-256 for password storage +SET password_encryption = 'scram-sha-256'; +ALTER USER regress_password_rollover1 ADD SECOND PASSWORD 'p7' + SECOND PASSWORD VALID UNTIL 'Infinity'; +-- the rolsecondpassword field should now contain a SCRAM secret +SELECT rolname, rolpassword, rolvaliduntil, regexp_replace(rolsecondpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:$:') as rolsecondpassword_masked, rolsecondvaliduntil + FROM pg_authid + WHERE rolname LIKE 'regress_password_rollover%'; + rolname | rolpassword | rolvaliduntil | rolsecondpassword_masked | rolsecondvaliduntil +----------------------------+-------------------------------------+------------------------------+---------------------------------------------------+--------------------- + regress_password_rollover1 | md5cc8c5dac5560a2fead71cfba4625a2c7 | Sat Jan 01 00:00:00 2022 PST | SCRAM-SHA-256$4096:$: | infinity +(1 row) + +-- Drop the less secure, md5, password +ALTER USER regress_password_rollover1 DROP FIRST PASSWORD; +-- the rolpassword field should now be null +SELECT rolname, rolpassword, rolvaliduntil, regexp_replace(rolsecondpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:$:') as rolsecondpassword_masked, rolsecondvaliduntil + FROM pg_authid + WHERE rolname LIKE 'regress_password_rollover%'; + rolname | rolpassword | rolvaliduntil | rolsecondpassword_masked | rolsecondvaliduntil +----------------------------+-------------+------------------------------+---------------------------------------------------+--------------------- + regress_password_rollover1 | | Sat Jan 01 00:00:00 2022 PST | SCRAM-SHA-256$4096:$: | infinity +(1 row) + diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 4df9d8503b..5efad7f3ad 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -68,6 +68,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi # ---------- test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash +# ---------- +# Another group of parallel tests +# ---------- +test: password_rollover + # ---------- # Additional BRIN tests # ---------- diff --git a/src/test/regress/sql/password_rollover.sql b/src/test/regress/sql/password_rollover.sql new file mode 100644 index 0000000000..73a42f97ab --- /dev/null +++ b/src/test/regress/sql/password_rollover.sql @@ -0,0 +1,107 @@ +-- +-- Tests for password rollovers +-- + +SET password_encryption = 'md5'; + +-- Create a user, as usual +CREATE ROLE regress_password_rollover1 PASSWORD 'p1' LOGIN; + +-- the rolpassword field should be non-null, and others should be null +SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil + FROM pg_authid + WHERE rolname LIKE 'regress_password_rollover%'; + +-- Add another password that the user can use for authentication. +ALTER ROLE regress_password_rollover1 ADD SECOND PASSWORD 'p2'; + +-- the rolpassword and rolsecondpassword fields should be non-null, and others should be null +SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil + FROM pg_authid + WHERE rolname LIKE 'regress_password_rollover%'; + +-- Set second password's expiration time. +ALTER ROLE regress_password_rollover1 SECOND PASSWORD VALID UNTIL '2021/01/01'; + +-- the rolvaliduntil field should be null, and other should be non-null +SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil + FROM pg_authid + WHERE rolname LIKE 'regress_password_rollover%'; + +ALTER ROLE regress_password_rollover1 FIRST PASSWORD VALID UNTIL '2022/01/01'; + +-- All fields should be non-null +SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil + FROM pg_authid + WHERE rolname LIKE 'regress_password_rollover%'; + +-- Setting a password to null does not set its expiration time to null +ALTER ROLE regress_password_rollover1 PASSWORD NULL; + +-- the rolpassword field should be null, and others should be non-null +SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil + FROM pg_authid + WHERE rolname LIKE 'regress_password_rollover%'; + +-- If, for some reason, the user wants to get rid of the latest password added. +ALTER ROLE regress_password_rollover1 DROP SECOND PASSWORD; + +-- the rolpassword and rolsecondpassword fields should be null, and others should be non-null +SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil + FROM pg_authid + WHERE rolname LIKE 'regress_password_rollover%'; + +-- Add a new password in 'second' slot +ALTER ROLE regress_password_rollover1 ADD SECOND PASSWORD 'p3' SECOND PASSWORD VALID UNTIL '2023/01/01'; + +-- the rolpassword field should be null, and others should be non-null +SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil + FROM pg_authid + WHERE rolname LIKE 'regress_password_rollover%'; + +-- VALID UNTIL must not be allowed when ADDing a password, to avoid the +-- confusing invocation where the command may seem to do one thing but actually +-- does something else. The following may seem like it will add a 'second' +-- password with a new expiration, but, if allowed, this will set the expiration +-- time on the _first_ password. +ALTER USER regress_password_rollover1 ADD SECOND PASSWORD 'p4' VALID UNTIL '2023/01/01'; + +-- Even though both, the password and the expiration, refer to the first +-- password, we disallow it to be consistent with the previous command's +-- behaviour. +ALTER USER regress_password_rollover1 ADD FIRST PASSWORD 'p4' VALID UNTIL '2023/01/01'; + +-- Set the first password +ALTER ROLE regress_password_rollover1 ADD FIRST PASSWORD 'p5'; + +-- Attempting to add a password while the respective slot is occupied +-- results in error +ALTER ROLE regress_password_rollover1 ADD FIRST PASSWORD 'p6'; + +ALTER ROLE regress_password_rollover1 ADD SECOND PASSWORD 'p6'; + +ALTER ROLE regress_password_rollover1 DROP SECOND PASSWORD; + +-- the rolsecondpassword field should be null, and others should be non-null +SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil + FROM pg_authid + WHERE rolname LIKE 'regress_password_rollover%'; + +-- Use scram-sha-256 for password storage +SET password_encryption = 'scram-sha-256'; + +ALTER USER regress_password_rollover1 ADD SECOND PASSWORD 'p7' + SECOND PASSWORD VALID UNTIL 'Infinity'; + +-- the rolsecondpassword field should now contain a SCRAM secret +SELECT rolname, rolpassword, rolvaliduntil, regexp_replace(rolsecondpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:$:') as rolsecondpassword_masked, rolsecondvaliduntil + FROM pg_authid + WHERE rolname LIKE 'regress_password_rollover%'; + +-- Drop the less secure, md5, password +ALTER USER regress_password_rollover1 DROP FIRST PASSWORD; + +-- the rolpassword field should now be null +SELECT rolname, rolpassword, rolvaliduntil, regexp_replace(rolsecondpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:$:') as rolsecondpassword_masked, rolsecondvaliduntil + FROM pg_authid + WHERE rolname LIKE 'regress_password_rollover%'; -- 2.41.0