From 8617ec4642a83f599b085dc9a18e91a1c2c4bf9d Mon Sep 17 00:00:00 2001
From: btlqql <2977859784@qq.com>
Date: Sat, 8 Aug 2026 15:34:16 +0800
Subject: [PATCH] Add configurable password complexity checks
Require uppercase, lowercase, digit, and punctuation categories for plaintext passwords while allowing superusers to configure each policy. Extend regression coverage and document the new settings.
Refs IvorySQL/IvorySQL#1102
Assisted-by: OpenAI:GPT-5
Percentage of AI-generated code: 100%
---
.../passwordcheck/expected/passwordcheck.out | 40 +++++++++-
.../expected/passwordcheck_1.out | 40 +++++++++-
contrib/passwordcheck/passwordcheck.c | 80 ++++++++++++++++++-
contrib/passwordcheck/sql/passwordcheck.sql | 40 +++++++++-
doc/src/sgml/passwordcheck.sgml | 72 +++++++++++++++++
5 files changed, 264 insertions(+), 8 deletions(-)
diff --git a/contrib/passwordcheck/expected/passwordcheck.out b/contrib/passwordcheck/expected/passwordcheck.out
index 83472c76d27..fc7562fe414 100644
--- a/contrib/passwordcheck/expected/passwordcheck.out
+++ b/contrib/passwordcheck/expected/passwordcheck.out
@@ -2,20 +2,56 @@ SET md5_password_warnings = off;
LOAD 'passwordcheck';
CREATE USER regress_passwordcheck_user1;
-- ok
-ALTER USER regress_passwordcheck_user1 PASSWORD 'a_nice_long_password';
+ALTER USER regress_passwordcheck_user1 PASSWORD 'A_nice_long_password1';
-- error: too short
ALTER USER regress_passwordcheck_user1 PASSWORD 'tooshrt';
ERROR: password is too short
DETAIL: password must be at least "passwordcheck.min_password_length" (8) bytes long
-- ok
SET passwordcheck.min_password_length = 6;
-ALTER USER regress_passwordcheck_user1 PASSWORD 'v_shrt';
+ALTER USER regress_passwordcheck_user1 PASSWORD 'V_shrt1';
-- error: contains user name
ALTER USER regress_passwordcheck_user1 PASSWORD 'xyzregress_passwordcheck_user1';
ERROR: password must not contain user name
-- error: contains only letters
ALTER USER regress_passwordcheck_user1 PASSWORD 'alessnicelongpassword';
ERROR: password must contain both letters and nonletters
+-- error: no uppercase letter
+ALTER USER regress_passwordcheck_user1 PASSWORD 'a_nice_long_password1';
+ERROR: password must contain an uppercase letter
+-- error: no lowercase letter
+ALTER USER regress_passwordcheck_user1 PASSWORD 'A_NICE_LONG_PASSWORD1';
+ERROR: password must contain a lowercase letter
+-- error: no digit
+ALTER USER regress_passwordcheck_user1 PASSWORD 'A_nice_long_password';
+ERROR: password must contain a digit
+-- error: no special character
+ALTER USER regress_passwordcheck_user1 PASSWORD 'Anicelongpassword1';
+ERROR: password must contain a special character
+-- ok: uppercase check can be configured
+SET passwordcheck.require_uppercase = off;
+ALTER USER regress_passwordcheck_user1 PASSWORD 'another_long_password1';
+SET passwordcheck.require_uppercase = on;
+ALTER USER regress_passwordcheck_user1 PASSWORD 'another_long_password1';
+ERROR: password must contain an uppercase letter
+-- ok: lowercase check can be configured
+SET passwordcheck.require_lowercase = off;
+ALTER USER regress_passwordcheck_user1 PASSWORD 'ANOTHER_LONG_PASSWORD1';
+SET passwordcheck.require_lowercase = on;
+ALTER USER regress_passwordcheck_user1 PASSWORD 'ANOTHER_LONG_PASSWORD1';
+ERROR: password must contain a lowercase letter
+-- ok: digit check can be configured
+SET passwordcheck.require_digit = off;
+ALTER USER regress_passwordcheck_user1 PASSWORD 'Another_long_password';
+SET passwordcheck.require_digit = on;
+ALTER USER regress_passwordcheck_user1 PASSWORD 'Another_long_password';
+ERROR: password must contain a digit
+-- ok: special-character check can be configured
+SET passwordcheck.require_special = off;
+ALTER USER regress_passwordcheck_user1 PASSWORD 'Anotherlongpassword1';
+SET passwordcheck.require_special = on;
+ALTER USER regress_passwordcheck_user1 PASSWORD 'Anotherlongpassword1';
+ERROR: password must contain a special character
-- encrypted ok (password is "secret")
ALTER USER regress_passwordcheck_user1 PASSWORD 'md592350e12ac34e52dd598f90893bb3ae7';
-- error: password is user name
diff --git a/contrib/passwordcheck/expected/passwordcheck_1.out b/contrib/passwordcheck/expected/passwordcheck_1.out
index fb12ec45cc4..0fb55f30d47 100644
--- a/contrib/passwordcheck/expected/passwordcheck_1.out
+++ b/contrib/passwordcheck/expected/passwordcheck_1.out
@@ -2,20 +2,56 @@ SET md5_password_warnings = off;
LOAD 'passwordcheck';
CREATE USER regress_passwordcheck_user1;
-- ok
-ALTER USER regress_passwordcheck_user1 PASSWORD 'a_nice_long_password';
+ALTER USER regress_passwordcheck_user1 PASSWORD 'A_nice_long_password1';
-- error: too short
ALTER USER regress_passwordcheck_user1 PASSWORD 'tooshrt';
ERROR: password is too short
DETAIL: password must be at least "passwordcheck.min_password_length" (8) bytes long
-- ok
SET passwordcheck.min_password_length = 6;
-ALTER USER regress_passwordcheck_user1 PASSWORD 'v_shrt';
+ALTER USER regress_passwordcheck_user1 PASSWORD 'V_shrt1';
-- error: contains user name
ALTER USER regress_passwordcheck_user1 PASSWORD 'xyzregress_passwordcheck_user1';
ERROR: password must not contain user name
-- error: contains only letters
ALTER USER regress_passwordcheck_user1 PASSWORD 'alessnicelongpassword';
ERROR: password must contain both letters and nonletters
+-- error: no uppercase letter
+ALTER USER regress_passwordcheck_user1 PASSWORD 'a_nice_long_password1';
+ERROR: password must contain an uppercase letter
+-- error: no lowercase letter
+ALTER USER regress_passwordcheck_user1 PASSWORD 'A_NICE_LONG_PASSWORD1';
+ERROR: password must contain a lowercase letter
+-- error: no digit
+ALTER USER regress_passwordcheck_user1 PASSWORD 'A_nice_long_password';
+ERROR: password must contain a digit
+-- error: no special character
+ALTER USER regress_passwordcheck_user1 PASSWORD 'Anicelongpassword1';
+ERROR: password must contain a special character
+-- ok: uppercase check can be configured
+SET passwordcheck.require_uppercase = off;
+ALTER USER regress_passwordcheck_user1 PASSWORD 'another_long_password1';
+SET passwordcheck.require_uppercase = on;
+ALTER USER regress_passwordcheck_user1 PASSWORD 'another_long_password1';
+ERROR: password must contain an uppercase letter
+-- ok: lowercase check can be configured
+SET passwordcheck.require_lowercase = off;
+ALTER USER regress_passwordcheck_user1 PASSWORD 'ANOTHER_LONG_PASSWORD1';
+SET passwordcheck.require_lowercase = on;
+ALTER USER regress_passwordcheck_user1 PASSWORD 'ANOTHER_LONG_PASSWORD1';
+ERROR: password must contain a lowercase letter
+-- ok: digit check can be configured
+SET passwordcheck.require_digit = off;
+ALTER USER regress_passwordcheck_user1 PASSWORD 'Another_long_password';
+SET passwordcheck.require_digit = on;
+ALTER USER regress_passwordcheck_user1 PASSWORD 'Another_long_password';
+ERROR: password must contain a digit
+-- ok: special-character check can be configured
+SET passwordcheck.require_special = off;
+ALTER USER regress_passwordcheck_user1 PASSWORD 'Anotherlongpassword1';
+SET passwordcheck.require_special = on;
+ALTER USER regress_passwordcheck_user1 PASSWORD 'Anotherlongpassword1';
+ERROR: password must contain a special character
-- encrypted ok (password is "secret")
ALTER USER regress_passwordcheck_user1 PASSWORD 'md592350e12ac34e52dd598f90893bb3ae7';
-- error: password is user name
diff --git a/contrib/passwordcheck/passwordcheck.c b/contrib/passwordcheck/passwordcheck.c
index 13fd5c976a0..543fcff7353 100644
--- a/contrib/passwordcheck/passwordcheck.c
+++ b/contrib/passwordcheck/passwordcheck.c
@@ -35,6 +35,10 @@ static check_password_hook_type prev_check_password_hook = NULL;
/* GUC variables */
static int min_password_length = 8;
+static bool require_uppercase = true;
+static bool require_lowercase = true;
+static bool require_digit = true;
+static bool require_special = true;
/*
* check_password
@@ -91,7 +95,11 @@ check_password(const char *username,
int pwdlen = strlen(password);
int i;
bool pwd_has_letter,
- pwd_has_nonletter;
+ pwd_has_nonletter,
+ pwd_has_uppercase,
+ pwd_has_lowercase,
+ pwd_has_digit,
+ pwd_has_special;
#ifdef USE_CRACKLIB
const char *reason;
#endif
@@ -113,22 +121,54 @@ check_password(const char *username,
/* check if the password contains both letters and non-letters */
pwd_has_letter = false;
pwd_has_nonletter = false;
+ pwd_has_uppercase = false;
+ pwd_has_lowercase = false;
+ pwd_has_digit = false;
+ pwd_has_special = false;
for (i = 0; i < pwdlen; i++)
{
+ unsigned char ch = (unsigned char) password[i];
+
/*
* isalpha() does not work for multibyte encodings but let's
* consider non-ASCII characters non-letters
*/
- if (isalpha((unsigned char) password[i]))
+ if (isalpha(ch))
pwd_has_letter = true;
else
pwd_has_nonletter = true;
+
+ if (isupper(ch))
+ pwd_has_uppercase = true;
+ if (islower(ch))
+ pwd_has_lowercase = true;
+ if (isdigit(ch))
+ pwd_has_digit = true;
+ if (ispunct(ch))
+ pwd_has_special = true;
}
if (!pwd_has_letter || !pwd_has_nonletter)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("password must contain both letters and nonletters")));
+ if (require_uppercase && !pwd_has_uppercase)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("password must contain an uppercase letter")));
+ if (require_lowercase && !pwd_has_lowercase)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("password must contain a lowercase letter")));
+ if (require_digit && !pwd_has_digit)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("password must contain a digit")));
+ if (require_special && !pwd_has_special)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("password must contain a special character")));
+
#ifdef USE_CRACKLIB
/* call cracklib to check password */
if ((reason = FascistCheck(password, CRACKLIB_DICTPATH)))
@@ -159,6 +199,42 @@ _PG_init(void)
GUC_UNIT_BYTE,
NULL, NULL, NULL);
+ DefineCustomBoolVariable("passwordcheck.require_uppercase",
+ "Require at least one uppercase ASCII letter.",
+ NULL,
+ &require_uppercase,
+ true,
+ PGC_SUSET,
+ 0,
+ NULL, NULL, NULL);
+
+ DefineCustomBoolVariable("passwordcheck.require_lowercase",
+ "Require at least one lowercase ASCII letter.",
+ NULL,
+ &require_lowercase,
+ true,
+ PGC_SUSET,
+ 0,
+ NULL, NULL, NULL);
+
+ DefineCustomBoolVariable("passwordcheck.require_digit",
+ "Require at least one ASCII digit.",
+ NULL,
+ &require_digit,
+ true,
+ PGC_SUSET,
+ 0,
+ NULL, NULL, NULL);
+
+ DefineCustomBoolVariable("passwordcheck.require_special",
+ "Require at least one ASCII punctuation character.",
+ NULL,
+ &require_special,
+ true,
+ PGC_SUSET,
+ 0,
+ NULL, NULL, NULL);
+
MarkGUCPrefixReserved("passwordcheck");
/* activate password checks when the module is loaded */
diff --git a/contrib/passwordcheck/sql/passwordcheck.sql b/contrib/passwordcheck/sql/passwordcheck.sql
index 21ad8d452b5..f524a63cd7d 100644
--- a/contrib/passwordcheck/sql/passwordcheck.sql
+++ b/contrib/passwordcheck/sql/passwordcheck.sql
@@ -4,14 +4,14 @@ LOAD 'passwordcheck';
CREATE USER regress_passwordcheck_user1;
-- ok
-ALTER USER regress_passwordcheck_user1 PASSWORD 'a_nice_long_password';
+ALTER USER regress_passwordcheck_user1 PASSWORD 'A_nice_long_password1';
-- error: too short
ALTER USER regress_passwordcheck_user1 PASSWORD 'tooshrt';
-- ok
SET passwordcheck.min_password_length = 6;
-ALTER USER regress_passwordcheck_user1 PASSWORD 'v_shrt';
+ALTER USER regress_passwordcheck_user1 PASSWORD 'V_shrt1';
-- error: contains user name
ALTER USER regress_passwordcheck_user1 PASSWORD 'xyzregress_passwordcheck_user1';
@@ -19,6 +19,42 @@ ALTER USER regress_passwordcheck_user1 PASSWORD 'xyzregress_passwordcheck_user1'
-- error: contains only letters
ALTER USER regress_passwordcheck_user1 PASSWORD 'alessnicelongpassword';
+-- error: no uppercase letter
+ALTER USER regress_passwordcheck_user1 PASSWORD 'a_nice_long_password1';
+
+-- error: no lowercase letter
+ALTER USER regress_passwordcheck_user1 PASSWORD 'A_NICE_LONG_PASSWORD1';
+
+-- error: no digit
+ALTER USER regress_passwordcheck_user1 PASSWORD 'A_nice_long_password';
+
+-- error: no special character
+ALTER USER regress_passwordcheck_user1 PASSWORD 'Anicelongpassword1';
+
+-- ok: uppercase check can be configured
+SET passwordcheck.require_uppercase = off;
+ALTER USER regress_passwordcheck_user1 PASSWORD 'another_long_password1';
+SET passwordcheck.require_uppercase = on;
+ALTER USER regress_passwordcheck_user1 PASSWORD 'another_long_password1';
+
+-- ok: lowercase check can be configured
+SET passwordcheck.require_lowercase = off;
+ALTER USER regress_passwordcheck_user1 PASSWORD 'ANOTHER_LONG_PASSWORD1';
+SET passwordcheck.require_lowercase = on;
+ALTER USER regress_passwordcheck_user1 PASSWORD 'ANOTHER_LONG_PASSWORD1';
+
+-- ok: digit check can be configured
+SET passwordcheck.require_digit = off;
+ALTER USER regress_passwordcheck_user1 PASSWORD 'Another_long_password';
+SET passwordcheck.require_digit = on;
+ALTER USER regress_passwordcheck_user1 PASSWORD 'Another_long_password';
+
+-- ok: special-character check can be configured
+SET passwordcheck.require_special = off;
+ALTER USER regress_passwordcheck_user1 PASSWORD 'Anotherlongpassword1';
+SET passwordcheck.require_special = on;
+ALTER USER regress_passwordcheck_user1 PASSWORD 'Anotherlongpassword1';
+
-- encrypted ok (password is "secret")
ALTER USER regress_passwordcheck_user1 PASSWORD 'md592350e12ac34e52dd598f90893bb3ae7';
diff --git a/doc/src/sgml/passwordcheck.sgml b/doc/src/sgml/passwordcheck.sgml
index 7ea32410463..e0295137ad2 100644
--- a/doc/src/sgml/passwordcheck.sgml
+++ b/doc/src/sgml/passwordcheck.sgml
@@ -83,8 +83,76 @@
+
+
+
+ passwordcheck.require_uppercase (boolean)
+
+ passwordcheck.require_uppercase configuration parameter
+
+
+
+
+ Require at least one uppercase ASCII letter. The default is
+ on. Only superusers can change this setting.
+
+
+
+
+
+
+ passwordcheck.require_lowercase (boolean)
+
+ passwordcheck.require_lowercase configuration parameter
+
+
+
+
+ Require at least one lowercase ASCII letter. The default is
+ on. Only superusers can change this setting.
+
+
+
+
+
+
+ passwordcheck.require_digit (boolean)
+
+ passwordcheck.require_digit configuration parameter
+
+
+
+
+ Require at least one ASCII digit. The default is
+ on. Only superusers can change this setting.
+
+
+
+
+
+
+ passwordcheck.require_special (boolean)
+
+ passwordcheck.require_special configuration parameter
+
+
+
+
+ Require at least one ASCII punctuation character. The default is
+ on. Only superusers can change this setting.
+
+
+
+
+
+ Password complexity parameters only apply when a password is supplied in
+ plain text. For a pre-encrypted password, the module can only check
+ whether it equals the user name.
+
+
+
In ordinary usage, this parameter is set in
postgresql.conf, but superusers can alter it on-the-fly
@@ -94,6 +162,10 @@
# postgresql.conf
passwordcheck.min_password_length = 12
+passwordcheck.require_uppercase = on
+passwordcheck.require_lowercase = on
+passwordcheck.require_digit = on
+passwordcheck.require_special = on