pax_global_header00006660000000000000000000000064145562662050014525gustar00rootroot0000000000000052 comment=3cc4f3b24788616383811e0e4462e8cc71f687ee credcheck-2.6/000077500000000000000000000000001455626620500133075ustar00rootroot00000000000000credcheck-2.6/.gitignore000066400000000000000000000001101455626620500152670ustar00rootroot00000000000000*.o .deps *.so .cproject .settings .project *.bc results/* regression.* credcheck-2.6/ChangeLog000066400000000000000000000270361455626620500150710ustar00rootroot000000000000002024-01-30 - Version 2.6.0 This release is the a quick maintenance release to fix path of pg_config in Makefile and an errata in the previous upgrade filename. Thanks to Devrim Gunduz for the report. 2024-01-30 - Version 2.5.0 This release is a quick maintenance release to fix compîlation issue with PostgreSQL prior 15.0 on custom variable prefix restriction. Thanks to Devrim Gunduz for the report. 2024-01-30 - Version 2.4.0 This release is a maintenance release to fix a major issue with the backup of the history file with pgBackRest and adds an authentication delay feature. - Add authentication delay feature to be able to add a pause on authentication failure. Setting `credcheck.auth_delay_ms` causes the server to pause for a given number of milliseconds before reporting authentication failure. This makes brute-force attacks on database passwords more difficult. This patch is purely a copy/paste from the auth_delay extension to avoid loading other extension. See https://www.postgresql.org/docs/current/auth-delay.html for more information about the origin of this feature. - Force size of file $PGDATA/global/pg_password_history to be a multiple of 8192 to fix pgBackRest error caused by the error message: "page misalignment in file /.../global/pg_password_history: file size 2604 is not divisible by page size 8192" Thanks to did16 for the report. 2023-11-03 - Version 2.3.0 This release is a maintenance release to fix a major issue with the "whitelist" feature. - Fix crash when length of the credcheck.whitelist value was > NAMEDATALEN. Thanks to zobnin for the report. Extension upgrade requires a PostgreSQL restart to reload the credcheck library. 2023-09-16 - Version 2.2.0 This release adds a new feature, fixes a major bug with null password and fixes some issues reported by users since last release. - Add new GUC variable credcheck.whitelist that can be used to set a comma separated list of username to exclude from the password policy check. For example: credcheck.whitelist = 'admin,supuser' will disable any credcheck policy for the user named admin and supuser. Thanks to Nikolai for the feature request. - Add -Wno-ignored-attributes to CPPFLAGS to avoid compilation warning on pg_vsnprintf call. - Fix PG crash when password was set to NULL. Thanks to ragaoua for the report. - Suppress "MD5 password cleared because of role rename" messages. This makes the tests pass on PG12 and 13. Thanks to Christoph Berg for the patch. - Use pg_regress' variant comparison files mechanism. Instead of manually selecting the tests to run on PG13 in the Makefile, simply let pg_regress choose the matching output file from .out and _1.out. Thanks to Christoph Berg for the patch. - Add missing file credcheck--2.1.0.sql. Thanks to Jeff Janes for the report. Extension upgrade requires a PostgreSQL restart to reload the credcheck library. 2023-07-15 - Version 2.1.0 This release adds a two new features and fix issues reported by users since last release. - Add custom configuration variable credcheck.encrypted_password allowed to allow the use of encrypted password in CREATE or ALTER ROLE statement. Default is to not allow encrypted password and to fire an error. Thanks to ragaoua for the feature request. - Add the possibility to check the easiness of a password by the use of the cracklib tool. This work is simply a integration of a copy/paste from the passwordcheck extension available in the contrib/ directory. Credits to the author Laurenz Albe. - Fix failure count issue when ssl is disabled. Thanks to yinzhishu for the report. Upgrade require a PostgreSQL restart to reload the credcheck library. 2023-06-10 - Version 2.0.0 This release adds a major feature called Authentication Failure Ban and the compatibility with PostgreSQL 16. Upgrade require a PostgreSQL restart to reload the credcheck library. - Add "Authentication failure ban" new feature PostgreSQL doesn't have any mechanism to limit the number of authentication failure attempt before the user being banned. With the credcheck extension, after an amount of authentication failure defined by configuration directive `credcheck.max_auth_failure` the user can be banned and never connect anymore even if it gives the right password later. This feature requires that the credcheck extension to be added to to `shared_preload_libraries` configuration option. All users authentication failures are registered in shared memory with the timestamps of when the user have been banned. The authentication failures history is saved into memory only, that mean that the history is lost at PostgreSQL restart. I have not seen the interest for the moment to restore the cache at startup. The authentication failure cache size is set to 1024 records by default and can be adjusted using the `credcheck.auth_failure_cache_size` configuration directive. Change of this GUC require a PostgreSQL restart. Two settings allow to control the behavior of this feature: * `credcheck.max_auth_failure`: number of authentication failure allowed for a user before being banned. * `credcheck.reset_superuser` : force superuser to not be banned or reset a banned superuser when set to true. The default value for the first setting is `0` which means that authentication failure ban feature is disabled. The default value for the second setting is `false` which means that `postgres` superuser can be banned. In case the `postgres` superuser was banned, he can not logged anymore. If there is no other superuser account that can be used to reset the record of the banned superuser, set the `credcheck.reset_superuser`configuration directive to `true` into postgresql.conf file and send the SIGHUP signal to the PostgreSQL process pid so that it will reread the configuration. Next time the superuser will try to connect, its authentication failure cache entry will be removed. - Fix Makefile for PG 16. Thanks to Devrim Gunduz for the report. - Add missing SQL file for version 1.2.0 2023-05-13 - Version 1.2.0 This release fixes a major bug reported by users since last release: Fix case where password was wrongly saved in the history after a VALID UNTIL min/max error. Add a regression test for this case. Thanks to Tushar Takate for the report. Upgrade require a PostgreSQL restart to reload the credcheck library. 2023-04-27 - Version 1.1.0 This release fixes some minor issues reported by users since last release and adds a new custom setting: - credcheck.password_valid_max to force use of VALID UNTIL clause in CREATE/ALTER ROLE statements with a maximum number of days. Thanks to Gabriel Leroux for the feature report. - Explicitely import unistd.h for unlink() calls. Thanks to Gabriel Leroux for the report. 2023-04-06 - Version 1.0.0 This release adds a major feature called Password Reuse Policy and the ability to force the use of an expiration date for a password. It also prevent PostgreSQL to expose the password in the logs in case of error and fixes some issues reported by users since the past 6 months. - Add Password Reuse Policy feature. This implementation use a dedicated shared memory storage to share the password history between all database. The module must be loaded by adding credcheck to shared_preload_libraries in postgresql.conf, because it requires additional shared memory. This means that a server restart is needed to add or remove the module. When credcheck is active, it stores password history across all databases of the server. To access and manipulate this history, the module provides a view pg_password_history and the utility functions pg_password_history_reset() and pg_password_history_timestamp(). These are not available globally but can be enabled for a specific database with CREATE EXTENSION credcheck. The password history is stored in share memory and written to disk in file $PGDATA/global/pg_password_history to be loaded at startup. The share memory history size is set to 65535 records by default and can be adjusted using the credcheck.history_max_size configuration directive. Change of this GUC require a PostgreSQL restart. One record in the history takes 144 bytes so the default is to allocate around 10 MB of additional shared memory for the password history. Two settings allow to control the behavior of this feature: - credcheck.password_reuse_history: number of distinct passwords set before a password can be reused. - credcheck.password_reuse_interval: amount of time it takes before a password can be reused again. The default value for these settings are 0 which means that all password reuse policies are disabled. The password history consists of passwords a user has been assigned in the past. credcheck can restrict new passwords from being chosen from this history: - If an account is restricted on the basis of number of password changes, a new password cannot be chosen from the password_reuse_history most recent passwords. For example, minimum number of password changes is set to 3, a new password cannot be the same as any of the most recent 3 passwords. - If an account is restricted based on time elapsed, a new password can't be chosen from those in the history that are newer than the number of day set to password_reuse_interval. For example, if the password reuse interval is set to 365, new password must not be among those previously chosen within the last year. Thanks to Umair Shahid and Gabi201265 for the feature request. - Force PostgreSQL to not expose the password in the log when an error in CREATE/ALTER role occurs. This behavior can be disabled by setting the custom variable credcheck.no_password_logging to off. - Add possibility to enforce the use of an expiration date for a password with a life time of a specific number of days. Ex: credcheck.password_valid_until = 60 the password life time must be at least of two months. Thanks to Umair Shahid for the feature request. - Allow credcheck to check the user name in CREATE USER statement without option PASSWORD. Thanks to freeDev84 for the feature report. - Force credcheck settings to be set/changed only by a superuser. This fix will break backward compatibility if you use SET credcheck.* on a non superuser connection. - Fix detection of the VALID UNTIL clause in CREATE ROLE. Thanks to did16 for the report. - Use errcode ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION (28000) for most error messages. 2021-09-20 - Version 0.2.0 This release adds support to PostgreSQL v14 and fix some minor issues reported by users since the last 3 months. - Remove SQL extension file as it is empty and not required. - Fix compilation error with PostgreSQL v14, thanks to Devrim Gunduz for the report. [ patch from Gilles Darold ] - Add upgrade SQL script for extension. [ patch from Gilles Darold ] - Ignore char repeat checks, if the string size is 1 also changing the comment style. [ patch from Dinesh Kumar ] - Adding file header content. [ patch from Dinesh Kumar ] - Typo fix in docs. [ patch from Dinesh Kumar ] 2021-06-25 - Version 0.1.1 This release adds minor fixes to ignore char repeat checks if the string size is 1 and also change the comment style in C code. 2021-06-24 - Version 0.1.0 This is the first release of credcheck extension, which is a credential checker for the PostgreSQL users. credcheck-2.6/LICENSE000066400000000000000000000021701455626620500143140ustar00rootroot00000000000000PostgreSQL License Copyright (c) 2021-2023 MigOps Copyright (c) 2023 Gilles Darold Copyright (c) 2024 HexaCluster Corp Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. credcheck-2.6/Makefile000066400000000000000000000016231455626620500147510ustar00rootroot00000000000000EXTENSION = credcheck EXTVERSION = $(shell grep default_version $(EXTENSION).control | \ sed -e "s/default_version[[:space:]]*=[[:space:]]*'\([^']*\)'/\1/") # Uncomment the following two lines to enable cracklib support, adapt the path # to the cracklib dictionary following your distribution #PG_CPPFLAGS = -DUSE_CRACKLIB '-DCRACKLIB_DICTPATH="/usr/lib/cracklib_dict"' #SHLIB_LINK = -lcrack PG_CPPFLAGS += -Wno-ignored-attributes MODULE_big = credcheck OBJS = credcheck.o $(WIN32RES) PGFILEDESC = "credcheck - postgresql credential checker" DATA = $(wildcard updates/*--*.sql) $(EXTENSION)--$(EXTVERSION).sql REGRESS_OPTS = --inputdir=test --load-extension=credcheck TESTS = 01_username 02_password 03_rename 04_alter_pwd \ 05_reuse_history 06_reuse_interval 07_valid_until REGRESS = $(patsubst test/sql/%.sql,%,$(TESTS)) PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) include $(PGXS) credcheck-2.6/README.md000066400000000000000000000574721455626620500146050ustar00rootroot00000000000000## credcheck - PostgreSQL username/password checks - [credcheck - PostgreSQL username/password checks](#credcheck---postgresql-usernamepassword-checks) - [Description](#description) - [Installation](#installation) - [Checks](#checks) - [Password reuse policy](#password-reuse-policy) - [Authentication failure ban](#authentication-failure-ban) - [Authentication delay](#authentication-delay) - [Examples](#examples) - [Limitations](#limitations) - [Authors](#authors) - [License](#license) - [Credits](#credits) ### [Description](#description) The `credcheck` PostgreSQL extension provides few general credential checks, which will be evaluated during the user creation, during the password change and user renaming. By using this extension, we can define a set of rules: - allow a specific set of credentials - reject a certain type of credentials - deny password that can be easily cracked - enforce use of an expiration date with a minimum of day for a password - define a password reuse policy - define the number of authentication failure allowed before a user is banned This extension provides all the checks as configurable parameters. The default configuration settings, will not enforce any complex checks and will try to allow most of the credentials. By using `SET credcheck. TO ;` command, enforce new settings for the credential checks. The settings can only be changed by a superuser. ### [Installation](#installation) To install the credcheck extension you need a PostgreSQL version upper than 10 but if you want to use the Password Reuse Policy feature the minimum version required is 12. This extension must be compiled with pgxs, so the `pg_config` tool must be available from your PATH environment variable. If you want to use the "deny password that can be easily cracked" feature you need to edit the `Makefile` to enable the following lines: #PG_CPPFLAGS = -DUSE_CRACKLIB '-DCRACKLIB_DICTPATH="/usr/lib/cracklib_dict"' #SHLIB_LINK = -lcrack Depending on your installation, you may need to install some devel packages. sudo yum -y install cracklib cracklib-devel cracklib-dicts words or sudo apt install libpam-cracklib libcrack2-dev You will also have to build the dictionary to be used, following your distribution: mkdict /usr/share/dict/* | sudo packer /usr/lib/cracklib_dict or cracklib-format /usr/share/dict/* | sudo cracklib-packer /usr/lib/cracklib_dic Once it is done, do "make", and then "sudo make install". Append `credcheck` to `shared_preload_libraries` configuration parameter in your `postgresql.conf` file then restart the PostgreSQL database to apply the changes. The regression tests can be run by using the `make installcheck` command. ### [Checks](#checks) Please find the below list of general checks, which we can enforce on credentials. | Check | Type | Description | Setting Value | Accepted | Not Accepted | |---------------------------|----------|-----------------------------------------------------|---------------|-----------------------------|------------------------------| | username_min_length | username | minimum length of a username | 4 | ✓ abcd | ✘ abc | | username_min_special | username | minimum number of special characters | 1 | ✓ a@bc | ✘ abcd | | username_min_digit | username | minimum number of digits | 1 | ✓ a1bc | ✘ abcd | | username_min_upper | username | minimum number of upper case | 2 | ✓ aBC | ✘ aBc | | username_min_lower | username | minimum number of lower case | 1 | ✓ aBC | ✘ ABC | | username_min_repeat | username | maximum number of times a character should repeat | 2 | ✓ aaBCa | ✘ aaaBCa | | username_contain_password | username | username should not contain password | on | ✓ username - password | ✘ username + password | | username_contain | username | username should contain one of these characters | a,b,c | ✓ ade | ✘ efg | | username_not_contain | username | username should not contain one of these characters | x,y,z | ✓ ade | ✘ axf | | username_ignore_case | username | ignore case while performing the above checks | on | ✓ Ade | ✘ aXf | | password_min_length | password | minimum length of a password | 4 | ✓ abcd | ✘ abc | | password_min_special | password | minimum number of special characters | 1 | ✓ a@bc | ✘ abc | | password_min_digit | password | minimum number of digits in a password | 1 | ✓ a1bc | ✘ abc | | password_min_upper | password | minimum number of uppercase characters | 1 | ✓ Abc | ✘ abc | | password_min_lower | password | minimum number of lowercase characters | 1 | ✓ aBC | ✘ ABC | | password_min_repeat | password | maximum number of times a character should repeat | 2 | ✓ aab | ✘ aaab | | password_contain_username | password | password should not contain password | on | ✓ password - username | ✘ password + username | | password_contain | password | password should contain these characters | a,b,c | ✓ ade | ✘ xfg | | password_not_contain | password | password should not contain these characters | x,y,z | ✓ abc | ✘ axf | | password_ignore_case | password | ignore case while performing above checks | on | ✓ Abc | ✘ aXf | | password_valid_until | password | force use of VALID UNTIL clause in CREATE ROLE statement with a minimum number of days | 60 | ✓ CREATE ROLE abcd VALID UNTIL (now()+'3 months'::interval)::date | ✘ CREATE ROLE abcd LOGIN; | | password_valid_max | password | force use of VALID UNTIL clause in CREATE ROLE statement with a maximum number of days | 365 | ✓ CREATE ROLE abcd VALID UNTIL (now()+'6 months'::interval)::date | ✘ CREATE ROLE abcd VALID UNTIL (now()+'2 years'::interval)::date; | There is also the `credcheck.whitelist` GUC that can be used to set a comma separated list of username to exclude from the password policy check. For example: ``` credcheck.whitelist = 'admin,supuser' ``` will disable any credcheck policy for the user named `admin` and `supuser`. ### [Examples](#examples) Let us start with a simple check as every username should be of length minimum 4 characters. ``` postgres=# SHOW credcheck.username_min_length; credcheck.username_min_length ------------------------------- 4 (1 row) postgres=# CREATE USER abc WITH PASSWORD 'pass'; ERROR: username length should match the configured credcheck.username_min_length postgres=# CREATE USER abcd WITH PASSWORD 'pass'; CREATE ROLE ``` Let us enforce an another check as every username should contain a special character in it. ``` postgres=# SHOW credcheck.username_min_special; credcheck.username_min_special -------------------------------- 1 (1 row) postgres=# CREATE USER abcd WITH PASSWORD 'pass'; ERROR: username does not contain the configured credcheck.username_min_special characters postgres=# CREATE USER abcd$ WITH PASSWORD 'pass'; CREATE ROLE ``` Let us add one more check to the username, where username should not contain more than 1 adjacent repeat character. ``` postgres=# show credcheck.username_min_repeat ; credcheck.username_min_repeat ------------------------------- 1 (1 row) postgres=# CREATE USER week$ WITH PASSWORD 'pass'; ERROR: username characters are repeated more than the configured credcheck.username_min_repeat times postgres=# CREATE USER weak$ WITH PASSWORD 'pass'; CREATE ROLE postgres=# SHOW credcheck.username_min_repeat ; credcheck.username_min_repeat ------------------------------- 2 (1 row) postgres=# CREATE USER week$ WITH PASSWORD 'pass'; CREATE ROLE ``` Now, let us add some checks for the password. Let us start with a check as a password should not contain these characters (!@=$#). ``` postgres=# SHOW credcheck.password_not_contain ; credcheck.password_not_contain -------------------------------- !@=$# (1 row) postgres=# CREATE USER abcd$ WITH PASSWORD 'p@ss'; ERROR: password does contain the configured credcheck.password_not_contain characters postgres=# CREATE USER abcd$ WITH PASSWORD 'pass'; CREATE ROLE ``` Let us add another check for the password as, the password should not contain username. ``` postgres=# SHOW credcheck.password_contain_username ; credcheck.password_contain_username ------------------------------------- on (1 row) postgres=# CREATE USER abcd$ WITH PASSWORD 'abcd$xyz'; ERROR: password should not contain username -- OK, ignore case is disabled postgres=# CREATE USER abcd$ WITH PASSWORD 'ABCD$xyz'; CREATE ROLE postgres=# CREATE USER abcd$ WITH PASSWORD 'axyz'; CREATE ROLE ``` Let us make checks as to ignore the case. ``` postgres=# SHOW credcheck.password_ignore_case; credcheck.password_ignore_case -------------------------------- on (1 row) postgres=# CREATE USER abcd$ WITH PASSWORD 'ABCD$xyz'; ERROR: password should not contain username postgres=# CREATE USER abcd$ WITH PASSWORD 'A$xyz'; CREATE ROLE ``` Let us add one final check to the password as the password should not contain any adjacent repeated characters. ``` postgres=# SHOW credcheck.password_min_repeat ; credcheck.password_min_repeat ------------------------------- 3 (1 row) postgres=# CREATE USER abcd$ WITH PASSWORD 'straaaangepaasssword'; ERROR: password characters are repeated more than the configured credcheck.password_min_repeat times postgres=# CREATE USER abcd$ WITH PASSWORD 'straaangepaasssword'; CREATE ROLE ``` credcheck can also enforce the use of an expiration date for the password by checking option VALID UNTIL used in CREATE or ALTER ROLE. ``` postgres=# SET credcheck.password_valid_until = 30; SET postgres=# SET credcheck.password_valid_max = 180; SET postgres=# CREATE USER abcd$; ERROR: require a VALID UNTIL option with a date older than 30 days postgres=# CREATE USER abcd$ VALID UNTIL '2022-12-21'; ERROR: require a VALID UNTIL option with a date older than 30 days postgres=# ALTER USER abcd$ VALID UNTIL '2022-12-21'; ERROR: require a VALID UNTIL option with a date older than 30 days postgres=# ALTER USER abcd$ VALID UNTIL '2025-12-21'; ERROR: require a VALID UNTIL option with a date not beyond 180 days ``` If you have enabled the use of cracklib to check the easiness of a password you could have this kind of messages: ``` postgres=# CREATE USER my_easy_password with password 'pass123'; ERROR: password is easily cracked ``` ### [Password reuse policy](#password-reuse-policy) PostgreSQL supports natively password expiration, all other kinds of password policy enforcement comes with extensions. With the credcheck extension, password can be forced to be of a certain length, contain amounts of various types of characters and be checked against the user account name itself. But one thing was missing, there was no password reuse policy enforcement. That mean that when user were required to change their password, they could just reuse their current password! The credcheck extension adds the "Password Reuse Policy" in release 1.0. To used this feature, the credcheck extension MUST be added to `shared_preload_libraries` configuration option. All users passwords are historicized in shared memory together with the timestamps of when these passwords were set. The passwords history is saved into a file named `$PGDATA/global/pg_password_history` to be reloaded in shared memory at startup. This file must be part of your backups if you don't want to loose the password history, hopefully pg_basebackup will take care of it. Passwords are stored and compared as sha256 hashes, never in plain text. The password history size is set to 65535 records by default and can be adjusted using the `credcheck.history_max_size` configuration directive. Change of this GUC require a PostgreSQL restart. One record in the history takes 144 bytes, so the default is to allocate around 10 MB of additional shared memory for the password history. Two settings allow to control the behavior of this feature: * `credcheck.password_reuse_history`: number of distinct passwords set before a password can be reused. * `credcheck.password_reuse_interval`: amount of time it takes before a password can be reused again. The default value for these settings are 0 which means that all password reuse policies are disabled. The password history consists of passwords a user has been assigned in the past. credcheck can restrict new passwords from being chosen from this history: * If an account is restricted on the basis of number of password changes, a new password cannot be chosen from the `password_reuse_history` most recent passwords. For example, minimum number of password changes is set to 3, a new password cannot be the same as any of the most recent 3 passwords. * If an account is restricted based on time elapsed, a new password cannot be chosen from passwords in the history that are newer than `password_reuse_interval` days. For example, if the password reuse interval is set to 365, a new password must not be among those previously chosen within the last year. To be able to list the content of the history a view is provided in the database you have created the credcheck extension. The view is named `public.pg_password_history`. This view is visible by everyone. A superuser can also reset the content of the password history by calling a function named `public.pg_password_history_reset()`. If it is called without an argument, all the passwords history will be cleared. To only remove the records registered for a single user, just pass his name as parameter. This function returns the number of records removed from the history. Example: ``` SET credcheck.password_reuse_history = 2; CREATE USER credtest WITH PASSWORD 'H8Hdre=S2'; ALTER USER credtest PASSWORD 'J8YuRe=6O'; SELECT rolename, password_hash FROM pg_password_history WHERE rolename = 'credtest' ORDER BY password_date; rolename | password_hash ----------+------------------------------------------------------------------ credtest | 7488570b80076cf9da26644d5eeb316c4768ff5bee7bf319344e7bb328032098 credtest | e61e58c22aa6bf31a92b385932f7d0e4dbaba24fa3fdb2982510d6c72a961335 (2 rows) -- fail, the credential is still in the history ALTER USER credtest PASSWORD 'J8YuRe=6O'; ERROR: Cannot use this credential following the password reuse policy -- Reset the password history SELECT pg_password_history_reset(); pg_password_history_reset --------------------------- 2 (1 row) ``` Example for password reuse interval: ``` SET credcheck.password_reuse_history = 1; SET credcheck.password_reuse_interval = 365; -- Add a new password in the history and set its age to 100 days ALTER USER credtest PASSWORD 'J8YuRe=6O'; SELECT pg_password_history_timestamp('credtest', now()::timestamp - '100 days'::interval); pg_password_history_timestamp ------------------------------- 1 (1 row) SELECT * FROM pg_password_history WHERE rolename = 'credtest'; rolename | password_date | password_hash ----------+-------------------------------+------------------------------------------------------------------ credtest | 2022-12-15 13:41:06.736775+03 | c38cf85ca6c3e5ee72c09cf0bfb42fb29b0f0a3e8ba335637941d60f86512508 (1 row) -- fail, the password is in the history for less than 1 year ALTER USER credtest PASSWORD 'J8YuRe=6O'; ERROR: Cannot use this credential following the password reuse policy -- Change the age of the password to exceed the 1 year interval SELECT pg_password_history_timestamp('credtest', now()::timestamp - '380 days'::interval); pg_password_history_timestamp ------------------------------- 2 (1 row) -- success, the old password present in the history has expired and will be removed ALTER USER credtest PASSWORD 'J8YuRe=6O'; SELECT rolename, password_hash FROM pg_password_history WHERE rolename = 'credtest'; rolename | password_date | password_hash ----------+-------------------------------+------------------------------------------------------------------ credtest | 2023-03-25 13:42:37.387629+03 | c38cf85ca6c3e5ee72c09cf0bfb42fb29b0f0a3e8ba335637941d60f86512508 (1 row) ``` Function `pg_password_history_timestamp()` is provided for testing purpose only and allow a superuer to change the timestamp of all registered passwords in the history. ### [Authentication failure ban](#authentication-failure-ban) PostgreSQL doesn't have any mechanism to limit the number of authentication failure attempt before the user being banned. With the credcheck extension, after an amount of authentication failure defined by configuration directive `credcheck.max_auth_failure` the user can be banned and never connect anymore even if it gives the right password later. The credcheck extension adds the "Authentication failure ban" feature in release 2.0. To used this feature, the credcheck extension MUST be added to `shared_preload_libraries` configuration option. All users authentication failures are registered in shared memory with the timestamps of when the user have been banned. The authentication failures history is saved into memory only, that mean that the history is lost at PostgreSQL restart. I have not seen the interest to restore the cache at startup The authentication failure cache size is set to 1024 records by default and can be adjusted using the `credcheck.auth_failure_cache_size` configuration directive. Change of this GUC require a PostgreSQL restart. Two settings allow to control the behavior of this feature: * `credcheck.max_auth_failure`: number of authentication failure allowed for a user before being banned. * `credcheck.reset_superuser` : force superuser to not be banned or reset a banned superuser when set to true. The default value for the first setting is `0` which means that the authentication failure ban feature is disabled. The default value for the second setting is `false` which means that `postgres` superuser can be banned. In case the `postgres` superuser was banned, he can not logged anymore. If there is no other superuser account that can be used to reset the record of the banned superuser, set the `credcheck.reset_superuser`configuration directive to `true` into postgresql.conf file and send the SIGHUP signal to the PostgreSQL process pid so that it will reread the configuration. Next time the superuser will try to connect, its authentication failure cache entry will be removed. Example: `kill -1 1234` A superuser can also reset the content of the banned user cache by calling a function named `public.pg_banned_role_reset()`. If it is called without an argument, all the banned cache will be cleared. To only remove the record registered for a single user, just pass his name as parameter. This function returns the number of records removed from the cache. A restart of PostgreSQL also clear the cache. Example: ``` $ psql -h localhost -U toban_user -d gilles Password for user toban_user: psql: error: connection to server at "localhost" (127.0.0.1), port 5432 failed: FATAL: password authentication failed for user "toban_user" connection to server at "localhost" (127.0.0.1), port 5432 failed: FATAL: password authentication failed for user "toban_user" $ psql -h localhost -U toban_user -d gilles Password for user toban_user: psql: error: connection to server at "localhost" (127.0.0.1), port 5432 failed: FATAL: password authentication failed for user "toban_user" connection to server at "localhost" (127.0.0.1), port 5432 failed: FATAL: password authentication failed for user "toban_user" $ psql -h localhost -U toban_user -d gilles Password for user toban_user: psql: error: connection to server at "localhost" (127.0.0.1), port 5432 failed: FATAL: rejecting connection, user 'toban_user' has been banned connection to server at "localhost" (127.0.0.1), port 5432 failed: FATAL: rejecting connection, user 'toban_user' has been banned ``` ``` test=# SELECT * FROM pg_banned_role; roleid | failure_count | banned_date --------+---------------+---------------------------- 250362 | 2 | 2023-06-09 20:33:58.490273 (1 row) test=# SELECT pg_banned_role_reset(); pg_banned_role_reset ---------------------- 1 (1 row) test=# SELECT * FROM pg_banned_role; roleid | failure_count | banned_date --------+---------------+------------- (0 rows) ``` and then another login attempt is allowed: ``` $ psql -h localhost -U toban_user -d gilles Password for user toban_user: psql: error: connection to server at "localhost" (127.0.0.1), port 5432 failed: FATAL: password authentication failed for user "toban_user" connection to server at "localhost" (127.0.0.1), port 5432 failed: FATAL: password authentication failed for user "toban_user" ``` ### [Authentication delay](#authentication-delay) This feature allow a pause on authentication failure. Setting `credcheck.auth_delay_ms` causes the server to pause for a given number of milliseconds before reporting authentication failure. This makes brute-force attacks on database passwords more difficult. ### [Limitations](#limitations) This extension only works for the plain text passwords. Example ``` postgres=# CREATE USER user1 PASSWORD 'this is some plain text'; CREATE ROLE ``` An error will report, if any user trying to create user with an ENCRYPTED password. Example ``` postgres=# CREATE USER user1 PASSWORD 'md55e4cc86d2d6a8b73bbefc4d5b91baa45'; ERROR: password type is not a plain text ``` To allow the use of encrypted password in CREATE or ALTER ROLE, enable configuration custom variable `credcheck.encrypted_password_allowed`. Username checks will not get enforced while create an user without password, and while renaming the user if the user doesn't have a password defined. Example (username checks won't invoke here) ``` postgres=# CREATE USER user1; ``` Example (username checks won't invoke here) ``` postgres=# ALTER USER user1 RENAME to test_user; ``` Example (username checks will invoke here and on the rename statement too) ``` postgres=# CREATE USER user1 PASSWORD 'this is some plain text'; CREATE ROLE postgres=# ALTER USER user1 RENAME to test_user; ``` ### [Authors](#authors) - Dinesh Kumar - Gilles Darold Maintainer: Gilles Darold ### [License](#license) This extension is free software distributed under the PostgreSQL License. Copyright (c) 2021-2023 MigOps Inc. Copyright (c) 2023 Gilles Darold Copyright (c) 2024 HexaCluster Corp ### [Credits](#credits) - Thanks to the [passwordcheck](https://www.postgresql.org/docs/current/passwordcheck.html) extension author - Thanks to the [password policy](https://github.com/eendroroy/passwordpolicy) extension author - Thanks to the [blog author](https://paquier.xyz/postgresql-2/postgres-module-highlight-customize-passwordcheck-to-secure-your-database/) Mickael Paquier credcheck-2.6/credcheck--0.1.0.sql000066400000000000000000000000001455626620500164400ustar00rootroot00000000000000credcheck-2.6/credcheck--0.2.0.sql000066400000000000000000000000001455626620500164410ustar00rootroot00000000000000credcheck-2.6/credcheck--1.0.0.sql000066400000000000000000000033651455626620500164610ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2021-2023 MigOps Inc - All rights reserved. -- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION credcheck" to load this file. \quit CREATE SCHEMA credcheck; ---- -- Remove all entries from password history. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_password_history_reset( ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C VOLATILE; ---- -- Remove entries of the specified user from password history. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_password_history_reset( IN username name ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; ---- -- Look at password history entries ---- CREATE FUNCTION pg_password_history ( OUT rolename name, OUT password_date timestamp with time zone, OUT password_hash text ) RETURNS SETOF record AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; -- Register a view on the function for ease of use. CREATE VIEW pg_password_history AS SELECT * FROM pg_password_history(); ---- -- Change password creation timestamp for all entries of the specified -- user in the password history. Proposed for testing purpose only. -- Returns the number of entries changed. ---- CREATE FUNCTION pg_password_history_timestamp( IN username name, IN new_timestamp timestamp with time zone) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; GRANT SELECT ON pg_password_history TO PUBLIC; -- Don't want this to be available to non-superusers. REVOKE ALL ON FUNCTION pg_password_history_reset() FROM PUBLIC; REVOKE ALL ON FUNCTION pg_password_history_reset(name) FROM PUBLIC; REVOKE ALL ON FUNCTION pg_password_history_timestamp(name, timestamp with time zone) FROM PUBLIC; credcheck-2.6/credcheck--1.1.0.sql000066400000000000000000000033651455626620500164620ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2021-2023 MigOps Inc - All rights reserved. -- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION credcheck" to load this file. \quit CREATE SCHEMA credcheck; ---- -- Remove all entries from password history. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_password_history_reset( ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C VOLATILE; ---- -- Remove entries of the specified user from password history. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_password_history_reset( IN username name ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; ---- -- Look at password history entries ---- CREATE FUNCTION pg_password_history ( OUT rolename name, OUT password_date timestamp with time zone, OUT password_hash text ) RETURNS SETOF record AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; -- Register a view on the function for ease of use. CREATE VIEW pg_password_history AS SELECT * FROM pg_password_history(); ---- -- Change password creation timestamp for all entries of the specified -- user in the password history. Proposed for testing purpose only. -- Returns the number of entries changed. ---- CREATE FUNCTION pg_password_history_timestamp( IN username name, IN new_timestamp timestamp with time zone) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; GRANT SELECT ON pg_password_history TO PUBLIC; -- Don't want this to be available to non-superusers. REVOKE ALL ON FUNCTION pg_password_history_reset() FROM PUBLIC; REVOKE ALL ON FUNCTION pg_password_history_reset(name) FROM PUBLIC; REVOKE ALL ON FUNCTION pg_password_history_timestamp(name, timestamp with time zone) FROM PUBLIC; credcheck-2.6/credcheck--1.2.0.sql000066400000000000000000000033651455626620500164630ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2021-2023 MigOps Inc - All rights reserved. -- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION credcheck" to load this file. \quit CREATE SCHEMA credcheck; ---- -- Remove all entries from password history. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_password_history_reset( ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C VOLATILE; ---- -- Remove entries of the specified user from password history. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_password_history_reset( IN username name ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; ---- -- Look at password history entries ---- CREATE FUNCTION pg_password_history ( OUT rolename name, OUT password_date timestamp with time zone, OUT password_hash text ) RETURNS SETOF record AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; -- Register a view on the function for ease of use. CREATE VIEW pg_password_history AS SELECT * FROM pg_password_history(); ---- -- Change password creation timestamp for all entries of the specified -- user in the password history. Proposed for testing purpose only. -- Returns the number of entries changed. ---- CREATE FUNCTION pg_password_history_timestamp( IN username name, IN new_timestamp timestamp with time zone) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; GRANT SELECT ON pg_password_history TO PUBLIC; -- Don't want this to be available to non-superusers. REVOKE ALL ON FUNCTION pg_password_history_reset() FROM PUBLIC; REVOKE ALL ON FUNCTION pg_password_history_reset(name) FROM PUBLIC; REVOKE ALL ON FUNCTION pg_password_history_timestamp(name, timestamp with time zone) FROM PUBLIC; credcheck-2.6/credcheck--2.0.0.sql000066400000000000000000000054441455626620500164620ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2021-2023 MigOps Inc - All rights reserved. -- Copyright (c) 2023 Gilles Darold - All rights reserved. -- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION credcheck" to load this file. \quit CREATE SCHEMA credcheck; ---- -- Remove all entries from password history. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_password_history_reset( ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C VOLATILE; ---- -- Remove entries of the specified user from password history. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_password_history_reset( IN username name ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; ---- -- Look at password history entries ---- CREATE FUNCTION pg_password_history ( OUT rolename name, OUT password_date timestamp with time zone, OUT password_hash text ) RETURNS SETOF record AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; -- Register a view on the function for ease of use. CREATE VIEW pg_password_history AS SELECT * FROM pg_password_history(); ---- -- Change password creation timestamp for all entries of the specified -- user in the password history. Proposed for testing purpose only. -- Returns the number of entries changed. ---- CREATE FUNCTION pg_password_history_timestamp( IN username name, IN new_timestamp timestamp with time zone) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; GRANT SELECT ON pg_password_history TO PUBLIC; -- Don't want this to be available to non-superusers. REVOKE ALL ON FUNCTION pg_password_history_reset() FROM PUBLIC; REVOKE ALL ON FUNCTION pg_password_history_reset(name) FROM PUBLIC; REVOKE ALL ON FUNCTION pg_password_history_timestamp(name, timestamp with time zone) FROM PUBLIC; ---- -- Remove all entries from authent failure cache. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_banned_role_reset( ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C VOLATILE; ---- -- Remove entries of the specified user from authent failure cache. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_banned_role_reset( IN username name ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; ---- -- Look at authent failure cache entries ---- CREATE FUNCTION pg_banned_role ( OUT roleid Oid, OUT failure_count integer, OUT banned_date timestamp ) RETURNS SETOF record AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; -- Register a view on the function for ease of use. CREATE VIEW pg_banned_role AS SELECT * FROM pg_banned_role(); GRANT SELECT ON pg_banned_role TO PUBLIC; -- Don't want this to be available to non-superusers. REVOKE ALL ON FUNCTION pg_banned_role_reset() FROM PUBLIC; REVOKE ALL ON FUNCTION pg_banned_role_reset(name) FROM PUBLIC; credcheck-2.6/credcheck--2.1.0.sql000066400000000000000000000054441455626620500164630ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2021-2023 MigOps Inc - All rights reserved. -- Copyright (c) 2023 Gilles Darold - All rights reserved. -- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION credcheck" to load this file. \quit CREATE SCHEMA credcheck; ---- -- Remove all entries from password history. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_password_history_reset( ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C VOLATILE; ---- -- Remove entries of the specified user from password history. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_password_history_reset( IN username name ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; ---- -- Look at password history entries ---- CREATE FUNCTION pg_password_history ( OUT rolename name, OUT password_date timestamp with time zone, OUT password_hash text ) RETURNS SETOF record AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; -- Register a view on the function for ease of use. CREATE VIEW pg_password_history AS SELECT * FROM pg_password_history(); ---- -- Change password creation timestamp for all entries of the specified -- user in the password history. Proposed for testing purpose only. -- Returns the number of entries changed. ---- CREATE FUNCTION pg_password_history_timestamp( IN username name, IN new_timestamp timestamp with time zone) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; GRANT SELECT ON pg_password_history TO PUBLIC; -- Don't want this to be available to non-superusers. REVOKE ALL ON FUNCTION pg_password_history_reset() FROM PUBLIC; REVOKE ALL ON FUNCTION pg_password_history_reset(name) FROM PUBLIC; REVOKE ALL ON FUNCTION pg_password_history_timestamp(name, timestamp with time zone) FROM PUBLIC; ---- -- Remove all entries from authent failure cache. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_banned_role_reset( ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C VOLATILE; ---- -- Remove entries of the specified user from authent failure cache. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_banned_role_reset( IN username name ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; ---- -- Look at authent failure cache entries ---- CREATE FUNCTION pg_banned_role ( OUT roleid Oid, OUT failure_count integer, OUT banned_date timestamp ) RETURNS SETOF record AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; -- Register a view on the function for ease of use. CREATE VIEW pg_banned_role AS SELECT * FROM pg_banned_role(); GRANT SELECT ON pg_banned_role TO PUBLIC; -- Don't want this to be available to non-superusers. REVOKE ALL ON FUNCTION pg_banned_role_reset() FROM PUBLIC; REVOKE ALL ON FUNCTION pg_banned_role_reset(name) FROM PUBLIC; credcheck-2.6/credcheck--2.2.0.sql000066400000000000000000000054441455626620500164640ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2021-2023 MigOps Inc - All rights reserved. -- Copyright (c) 2023 Gilles Darold - All rights reserved. -- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION credcheck" to load this file. \quit CREATE SCHEMA credcheck; ---- -- Remove all entries from password history. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_password_history_reset( ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C VOLATILE; ---- -- Remove entries of the specified user from password history. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_password_history_reset( IN username name ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; ---- -- Look at password history entries ---- CREATE FUNCTION pg_password_history ( OUT rolename name, OUT password_date timestamp with time zone, OUT password_hash text ) RETURNS SETOF record AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; -- Register a view on the function for ease of use. CREATE VIEW pg_password_history AS SELECT * FROM pg_password_history(); ---- -- Change password creation timestamp for all entries of the specified -- user in the password history. Proposed for testing purpose only. -- Returns the number of entries changed. ---- CREATE FUNCTION pg_password_history_timestamp( IN username name, IN new_timestamp timestamp with time zone) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; GRANT SELECT ON pg_password_history TO PUBLIC; -- Don't want this to be available to non-superusers. REVOKE ALL ON FUNCTION pg_password_history_reset() FROM PUBLIC; REVOKE ALL ON FUNCTION pg_password_history_reset(name) FROM PUBLIC; REVOKE ALL ON FUNCTION pg_password_history_timestamp(name, timestamp with time zone) FROM PUBLIC; ---- -- Remove all entries from authent failure cache. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_banned_role_reset( ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C VOLATILE; ---- -- Remove entries of the specified user from authent failure cache. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_banned_role_reset( IN username name ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; ---- -- Look at authent failure cache entries ---- CREATE FUNCTION pg_banned_role ( OUT roleid Oid, OUT failure_count integer, OUT banned_date timestamp ) RETURNS SETOF record AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; -- Register a view on the function for ease of use. CREATE VIEW pg_banned_role AS SELECT * FROM pg_banned_role(); GRANT SELECT ON pg_banned_role TO PUBLIC; -- Don't want this to be available to non-superusers. REVOKE ALL ON FUNCTION pg_banned_role_reset() FROM PUBLIC; REVOKE ALL ON FUNCTION pg_banned_role_reset(name) FROM PUBLIC; credcheck-2.6/credcheck--2.3.0.sql000066400000000000000000000054441455626620500164650ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2021-2023 MigOps Inc - All rights reserved. -- Copyright (c) 2023 Gilles Darold - All rights reserved. -- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION credcheck" to load this file. \quit CREATE SCHEMA credcheck; ---- -- Remove all entries from password history. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_password_history_reset( ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C VOLATILE; ---- -- Remove entries of the specified user from password history. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_password_history_reset( IN username name ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; ---- -- Look at password history entries ---- CREATE FUNCTION pg_password_history ( OUT rolename name, OUT password_date timestamp with time zone, OUT password_hash text ) RETURNS SETOF record AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; -- Register a view on the function for ease of use. CREATE VIEW pg_password_history AS SELECT * FROM pg_password_history(); ---- -- Change password creation timestamp for all entries of the specified -- user in the password history. Proposed for testing purpose only. -- Returns the number of entries changed. ---- CREATE FUNCTION pg_password_history_timestamp( IN username name, IN new_timestamp timestamp with time zone) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; GRANT SELECT ON pg_password_history TO PUBLIC; -- Don't want this to be available to non-superusers. REVOKE ALL ON FUNCTION pg_password_history_reset() FROM PUBLIC; REVOKE ALL ON FUNCTION pg_password_history_reset(name) FROM PUBLIC; REVOKE ALL ON FUNCTION pg_password_history_timestamp(name, timestamp with time zone) FROM PUBLIC; ---- -- Remove all entries from authent failure cache. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_banned_role_reset( ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C VOLATILE; ---- -- Remove entries of the specified user from authent failure cache. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_banned_role_reset( IN username name ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; ---- -- Look at authent failure cache entries ---- CREATE FUNCTION pg_banned_role ( OUT roleid Oid, OUT failure_count integer, OUT banned_date timestamp ) RETURNS SETOF record AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; -- Register a view on the function for ease of use. CREATE VIEW pg_banned_role AS SELECT * FROM pg_banned_role(); GRANT SELECT ON pg_banned_role TO PUBLIC; -- Don't want this to be available to non-superusers. REVOKE ALL ON FUNCTION pg_banned_role_reset() FROM PUBLIC; REVOKE ALL ON FUNCTION pg_banned_role_reset(name) FROM PUBLIC; credcheck-2.6/credcheck--2.4.0.sql000066400000000000000000000054331455626620500164640ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2021-2023 MigOps Inc -- Copyright (c) 2023 Gilles Darold -- Copyright (c) 2024 HexaCluster Corp -- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION credcheck" to load this file. \quit CREATE SCHEMA credcheck; ---- -- Remove all entries from password history. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_password_history_reset( ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C VOLATILE; ---- -- Remove entries of the specified user from password history. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_password_history_reset( IN username name ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; ---- -- Look at password history entries ---- CREATE FUNCTION pg_password_history ( OUT rolename name, OUT password_date timestamp with time zone, OUT password_hash text ) RETURNS SETOF record AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; -- Register a view on the function for ease of use. CREATE VIEW pg_password_history AS SELECT * FROM pg_password_history(); ---- -- Change password creation timestamp for all entries of the specified -- user in the password history. Proposed for testing purpose only. -- Returns the number of entries changed. ---- CREATE FUNCTION pg_password_history_timestamp( IN username name, IN new_timestamp timestamp with time zone) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; GRANT SELECT ON pg_password_history TO PUBLIC; -- Don't want this to be available to non-superusers. REVOKE ALL ON FUNCTION pg_password_history_reset() FROM PUBLIC; REVOKE ALL ON FUNCTION pg_password_history_reset(name) FROM PUBLIC; REVOKE ALL ON FUNCTION pg_password_history_timestamp(name, timestamp with time zone) FROM PUBLIC; ---- -- Remove all entries from authent failure cache. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_banned_role_reset( ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C VOLATILE; ---- -- Remove entries of the specified user from authent failure cache. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_banned_role_reset( IN username name ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; ---- -- Look at authent failure cache entries ---- CREATE FUNCTION pg_banned_role ( OUT roleid Oid, OUT failure_count integer, OUT banned_date timestamp ) RETURNS SETOF record AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; -- Register a view on the function for ease of use. CREATE VIEW pg_banned_role AS SELECT * FROM pg_banned_role(); GRANT SELECT ON pg_banned_role TO PUBLIC; -- Don't want this to be available to non-superusers. REVOKE ALL ON FUNCTION pg_banned_role_reset() FROM PUBLIC; REVOKE ALL ON FUNCTION pg_banned_role_reset(name) FROM PUBLIC; credcheck-2.6/credcheck--2.5.0.sql000066400000000000000000000054331455626620500164650ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2021-2023 MigOps Inc -- Copyright (c) 2023 Gilles Darold -- Copyright (c) 2024 HexaCluster Corp -- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION credcheck" to load this file. \quit CREATE SCHEMA credcheck; ---- -- Remove all entries from password history. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_password_history_reset( ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C VOLATILE; ---- -- Remove entries of the specified user from password history. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_password_history_reset( IN username name ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; ---- -- Look at password history entries ---- CREATE FUNCTION pg_password_history ( OUT rolename name, OUT password_date timestamp with time zone, OUT password_hash text ) RETURNS SETOF record AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; -- Register a view on the function for ease of use. CREATE VIEW pg_password_history AS SELECT * FROM pg_password_history(); ---- -- Change password creation timestamp for all entries of the specified -- user in the password history. Proposed for testing purpose only. -- Returns the number of entries changed. ---- CREATE FUNCTION pg_password_history_timestamp( IN username name, IN new_timestamp timestamp with time zone) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; GRANT SELECT ON pg_password_history TO PUBLIC; -- Don't want this to be available to non-superusers. REVOKE ALL ON FUNCTION pg_password_history_reset() FROM PUBLIC; REVOKE ALL ON FUNCTION pg_password_history_reset(name) FROM PUBLIC; REVOKE ALL ON FUNCTION pg_password_history_timestamp(name, timestamp with time zone) FROM PUBLIC; ---- -- Remove all entries from authent failure cache. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_banned_role_reset( ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C VOLATILE; ---- -- Remove entries of the specified user from authent failure cache. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_banned_role_reset( IN username name ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; ---- -- Look at authent failure cache entries ---- CREATE FUNCTION pg_banned_role ( OUT roleid Oid, OUT failure_count integer, OUT banned_date timestamp ) RETURNS SETOF record AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; -- Register a view on the function for ease of use. CREATE VIEW pg_banned_role AS SELECT * FROM pg_banned_role(); GRANT SELECT ON pg_banned_role TO PUBLIC; -- Don't want this to be available to non-superusers. REVOKE ALL ON FUNCTION pg_banned_role_reset() FROM PUBLIC; REVOKE ALL ON FUNCTION pg_banned_role_reset(name) FROM PUBLIC; credcheck-2.6/credcheck--2.6.0.sql000066400000000000000000000054331455626620500164660ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2021-2023 MigOps Inc -- Copyright (c) 2023 Gilles Darold -- Copyright (c) 2024 HexaCluster Corp -- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION credcheck" to load this file. \quit CREATE SCHEMA credcheck; ---- -- Remove all entries from password history. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_password_history_reset( ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C VOLATILE; ---- -- Remove entries of the specified user from password history. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_password_history_reset( IN username name ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; ---- -- Look at password history entries ---- CREATE FUNCTION pg_password_history ( OUT rolename name, OUT password_date timestamp with time zone, OUT password_hash text ) RETURNS SETOF record AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; -- Register a view on the function for ease of use. CREATE VIEW pg_password_history AS SELECT * FROM pg_password_history(); ---- -- Change password creation timestamp for all entries of the specified -- user in the password history. Proposed for testing purpose only. -- Returns the number of entries changed. ---- CREATE FUNCTION pg_password_history_timestamp( IN username name, IN new_timestamp timestamp with time zone) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; GRANT SELECT ON pg_password_history TO PUBLIC; -- Don't want this to be available to non-superusers. REVOKE ALL ON FUNCTION pg_password_history_reset() FROM PUBLIC; REVOKE ALL ON FUNCTION pg_password_history_reset(name) FROM PUBLIC; REVOKE ALL ON FUNCTION pg_password_history_timestamp(name, timestamp with time zone) FROM PUBLIC; ---- -- Remove all entries from authent failure cache. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_banned_role_reset( ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C VOLATILE; ---- -- Remove entries of the specified user from authent failure cache. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_banned_role_reset( IN username name ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; ---- -- Look at authent failure cache entries ---- CREATE FUNCTION pg_banned_role ( OUT roleid Oid, OUT failure_count integer, OUT banned_date timestamp ) RETURNS SETOF record AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; -- Register a view on the function for ease of use. CREATE VIEW pg_banned_role AS SELECT * FROM pg_banned_role(); GRANT SELECT ON pg_banned_role TO PUBLIC; -- Don't want this to be available to non-superusers. REVOKE ALL ON FUNCTION pg_banned_role_reset() FROM PUBLIC; REVOKE ALL ON FUNCTION pg_banned_role_reset(name) FROM PUBLIC; credcheck-2.6/credcheck.c000066400000000000000000002074271455626620500154020ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * credcheck.c: * This file has the general PostgreSQL credential checks. * * This program is open source, licensed under the PostgreSQL license. * For license terms, see the LICENSE file. * * Copyright (c) 2021-2023: MigOps Inc * Copyright (c) 2023: Gilles Darold * Copyright (c) 2024: HexaCluster Corp * *------------------------------------------------------------------------- */ #include #include #include #ifdef USE_CRACKLIB #include #endif #include "postgres.h" #include "funcapi.h" #include "miscadmin.h" #include "access/heapam.h" #include "access/htup_details.h" #include "catalog/catalog.h" #include "catalog/indexing.h" #include "catalog/pg_auth_members.h" #include "catalog/pg_authid.h" #include "commands/user.h" #if PG_VERSION_NUM >= 140000 #include "common/hmac.h" #endif #include "common/sha2.h" #include "executor/spi.h" #include "libpq/auth.h" #include "nodes/makefuncs.h" #include "nodes/nodes.h" #include "nodes/pg_list.h" #include "postmaster/postmaster.h" #include "tcop/utility.h" #include "storage/ipc.h" #include "storage/lwlock.h" #include "storage/shmem.h" #include "utils/acl.h" #include "utils/builtins.h" #include "utils/guc.h" #include "utils/rel.h" #include "utils/syscache.h" #include "utils/timestamp.h" #include "utils/varlena.h" /* Default passord encryption */ #define Password_encryption = PASSWORD_TYPE_SCRAM_SHA_256; /* Name of external file to store password history in the PGDATA */ #define PGPH_DUMP_FILE "global/pg_password_history" /* Number of output arguments (columns) in the pg_password_history pseudo table */ #define PG_PASSWORD_HISTORY_COLS 3 /* Number of output arguments (columns) in the pg_banned_role pseudo table */ #define PG_BANNED_ROLE_COLS 3 /* Magic number identifying the stats file format */ static const uint32 PGPH_FILE_HEADER = 0x48504750; /* credcheck password history version, changes in which invalidate all entries */ static const uint32 PGPH_VERSION = 100; #define PGPH_TRANCHE_NAME "credcheck_history" #define PGAF_TRANCHE_NAME "credcheck_auth_failure" static bool statement_has_password = false; static bool no_password_logging = true; #if PG_VERSION_NUM < 120000 #define table_open(r,l) heap_open(r,l) #define table_openrv(r,l) heap_openrv(r,l) #define table_close(r,l) heap_close(r,l) #endif #if PG_VERSION_NUM < 100000 #error Minimum version of PostgreSQL required is 10 #endif /* Define ProcessUtility hook proto/parameters following the PostgreSQL version */ #if PG_VERSION_NUM >= 140000 #define PEL_PROCESSUTILITY_PROTO PlannedStmt *pstmt, const char *queryString, \ bool readOnlyTree, \ ProcessUtilityContext context, ParamListInfo params, \ QueryEnvironment *queryEnv, DestReceiver *dest, \ QueryCompletion *qc #define PEL_PROCESSUTILITY_ARGS pstmt, queryString, readOnlyTree, context, params, queryEnv, dest, qc #else #if PG_VERSION_NUM >= 130000 #define PEL_PROCESSUTILITY_PROTO PlannedStmt *pstmt, const char *queryString, \ ProcessUtilityContext context, ParamListInfo params, \ QueryEnvironment *queryEnv, DestReceiver *dest, \ QueryCompletion *qc #define PEL_PROCESSUTILITY_ARGS pstmt, queryString, context, params, queryEnv, dest, qc #else #define PEL_PROCESSUTILITY_PROTO PlannedStmt *pstmt, const char *queryString, \ ProcessUtilityContext context, ParamListInfo params, \ QueryEnvironment *queryEnv, DestReceiver *dest, \ char *completionTag #define PEL_PROCESSUTILITY_ARGS pstmt, queryString, context, params, queryEnv, dest, completionTag #endif #endif PG_MODULE_MAGIC; /* Hooks */ static check_password_hook_type prev_check_password_hook = NULL; static ProcessUtility_hook_type prev_ProcessUtility = NULL; static shmem_startup_hook_type prev_shmem_startup_hook = NULL; #if PG_VERSION_NUM >= 150000 static shmem_request_hook_type prev_shmem_request_hook = NULL; #endif /* Hold previous client authent hook */ static ClientAuthentication_hook_type prev_ClientAuthentication = NULL; /* Hold previous logging hook */ static emit_log_hook_type prev_log_hook = NULL; /* In memory storage of password history */ typedef struct pgphHashKey { char rolename[NAMEDATALEN]; char password_hash[PG_SHA256_DIGEST_STRING_LENGTH]; } pgphHashKey; typedef struct pgphEntry { pgphHashKey key; /* hash key of entry - MUST BE FIRST */ TimestampTz password_date; } pgphEntry; /* Global shared state */ typedef struct pgphSharedState { LWLock *lock; /* protects hashtable search/modification */ int num_entries; /* number of entries in the password history */ } pgphSharedState; /* Links to shared memory state */ static pgphSharedState *pgph = NULL; static HTAB *pgph_hash = NULL; static int pgph_max = 65535; static int pgaf_max = 1024; static int fail_max = 0; static bool reset_superuser = false; static bool encrypted_password_allowed = false; /* In memory storage of auth failure history */ typedef struct pgafHashKey { Oid roleid; } pgafHashKey; typedef struct pgafEntry { pgafHashKey key; /* hash key of entry - MUST BE FIRST */ float failure_count; TimestampTz banned_date; } pgafEntry; /* Global shared state */ typedef struct pgafSharedState { LWLock *lock; /* protects hashtable search/modification */ int num_entries; /* number of entries in the auth failure history */ } pgafSharedState; static pgafSharedState *pgaf = NULL; static HTAB *pgaf_hash = NULL; /* Functions */ extern void _PG_init(void); extern void _PG_fini(void); static void cc_ProcessUtility(PEL_PROCESSUTILITY_PROTO); static void flush_password_history(void); static pgphEntry *pgph_entry_alloc(pgphHashKey *key, TimestampTz password_date); static pgafEntry *pgaf_entry_alloc(pgafHashKey *key, float failure_count); #if PG_VERSION_NUM >= 150000 static void pghist_shmem_request(void); #endif static void pghist_shmem_startup(void); static void pgph_shmem_startup(void); static void pgaf_shmem_startup(void); static int entry_cmp(const void *lhs, const void *rhs); static Size pgph_memsize(void); static void pg_password_history_internal(FunctionCallInfo fcinfo); static void fix_log(ErrorData *edata); static Size pgaf_memsize(void); static void credcheck_max_auth_failure(Port *port, int status); static float get_auth_failure(const char *username, Oid userid, int status); static float save_auth_failure(const char *username, Oid userid); static void remove_auth_failure(const char *username, Oid userid); static void pg_banned_role_internal(FunctionCallInfo fcinfo); /* Username flags*/ static int username_min_length = 1; static int username_min_special = 0; static int username_min_digit = 0; static int username_min_upper = 0; static int username_min_lower = 0; static int username_min_repeat = 0; static char *username_not_contain = NULL; static char *username_contain = NULL; static bool username_contain_password = true; static bool username_ignore_case = false; static char *username_whitelist = NULL; /* Password flags*/ static int password_min_length = 1; static int password_min_special = 0; static int password_min_digit = 0; static int password_min_upper = 0; static int password_min_lower = 0; static int password_min_repeat = 0; static char *password_not_contain = NULL; static char *password_contain = NULL; static bool password_contain_username = true; static bool password_ignore_case = false; static int password_valid_until = 0; static int password_valid_max = 0; static int auth_delay_milliseconds = 0; #if PG_VERSION_NUM >= 120000 /* password_reuse_history: number of distinct passwords set before a password can be reused. password_reuse_interval: amount of time it takes before a password can be reused again. */ static int password_reuse_history = 0; static int password_reuse_interval = 0; char *str_to_sha256(const char *str, const char *salt); #endif bool check_whitelist(char **newval, void **extra, GucSource source); bool is_in_whitelist(char *username); static char *to_nlower(const char *str, size_t max) { char *lower_str; int i = 0; lower_str = (char *)calloc(strlen(str), sizeof(char)); for (const char *p = str; *p && i < max; p++) { lower_str[i++] = tolower(*p); } lower_str[i] = '\0'; return lower_str; } static bool str_contains(const char *chars, const char *str) { for (const char *i = str; *i; i++) { for (const char *j = chars; *j; j++) { if (*i == *j) { return true; } } } return false; } static void check_str_counters(const char *str, int *lower, int *upper, int *digit, int *special) { for (const char *i = str; *i; i++) { if (islower(*i)) { (*lower)++; } else if (isupper(*i)) { (*upper)++; } else if (isdigit(*i)) { (*digit)++; } else { (*special)++; } } } static bool char_repeat_exceeds(const char *str, int max_repeat) { int occurred = 1; size_t len = strlen(str); /*if string has only one character, then no need to proceed further*/ if (len==1) { return false; } for (size_t i = 0; i < len;) { occurred = 1; /*first character = str[i] second character = str[i+1] search for an adjacent repeated characters for example, in this string "weekend summary" search for the series "ee", "mm" */ for (size_t j = (i + 1), k = 1; j < len; j++, k++) { /* character matched*/ if (str[i] == str[j]) { /* is the previous, current character positions are adjacent*/ if (i + k == j) { occurred++; if (occurred > max_repeat) { return true; } } } /* if we reach an end of the string, no need to process further*/ if (j + 1 == len) { return false; } /* if the characters are not equal then point "i" to "j"*/ if (str[i] != str[j]) { i = j; break; } } } return false; } static void username_check(const char *username, const char *password) { int user_total_special = 0; int user_total_digit = 0; int user_total_upper = 0; int user_total_lower = 0; char *tmp_pass = NULL; char *tmp_user = NULL; char *tmp_contains = NULL; char *tmp_not_contains = NULL; if (strcasestr(debug_query_string, "PASSWORD") != NULL) statement_has_password = true; /* checks has to be done by ignoring case */ if (username_ignore_case) { if (password != NULL && strlen(password) > 0) tmp_pass = to_nlower(password, INT_MAX); tmp_user = to_nlower(username, INT_MAX); tmp_contains = to_nlower(username_contain, INT_MAX); tmp_not_contains = to_nlower(username_not_contain, INT_MAX); } else { if (password != NULL && strlen(password) > 0) tmp_pass = strndup(password, INT_MAX); tmp_user = strndup(username, INT_MAX); tmp_contains = strndup(username_contain, INT_MAX); tmp_not_contains = strndup(username_not_contain, INT_MAX); } /* Rule 1: username length */ if (strnlen(tmp_user, INT_MAX) < username_min_length) { ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg(gettext_noop("username length should match the configured %s"), "credcheck.username_min_length"))); goto clean; } /* Rule 2: username contains password * Note: * tmp_pass is NULL for ALTER USER ... RENAME TO ...; * statement so this rule can not be applied. */ if (tmp_pass != NULL && username_contain_password) { if (strstr(tmp_user, tmp_pass)) { ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg(gettext_noop("username should not contain password")))); goto clean; } } /* Rule 3: contain characters */ if (tmp_contains != NULL && strlen(tmp_contains) > 0) { if (str_contains(tmp_contains, tmp_user) == false) { ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg(gettext_noop("username does not contain the configured %s characters"), "credcheck.username_contain"))); goto clean; } } /* Rule 4: not contain characters */ if (tmp_not_contains != NULL && strlen(tmp_not_contains) > 0) { if (str_contains(tmp_not_contains, tmp_user) == true) { ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg(gettext_noop("username contains the configured %s unauthorized characters"), "credcheck.username_not_contain"))); goto clean; } } check_str_counters(tmp_user, &user_total_lower, &user_total_upper, &user_total_digit, &user_total_special); /* Rule 5: total upper characters */ if (!username_ignore_case && user_total_upper < username_min_upper) { ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg("username does not contain the configured %s characters", "credcheck.username_min_upper"))); goto clean; } /* Rule 6: total lower characters */ if (!username_ignore_case && user_total_lower < username_min_lower) { ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg("username does not contain the configured %s characters", "credcheck.username_min_lower"))); goto clean; } /* Rule 7: total digits */ if (user_total_digit < username_min_digit) { ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg("username does not contain the configured %s characters", "credcheck.username_min_digit"))); goto clean; } /* Rule 8: total special */ if (user_total_special < username_min_special) { ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg("username does not contain the configured %s characters", "credcheck.username_min_special"))); goto clean; } /* Rule 9: minimum char repeat */ if (username_min_repeat) { if (char_repeat_exceeds(tmp_user, username_min_repeat)) { ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg(gettext_noop("%s characters are repeated more than the " "configured %s times"), "username", "credcheck.username_min_repeat"))); goto clean; } } clean: free(tmp_pass); free(tmp_user); free(tmp_contains); free(tmp_not_contains); } /* We just check that the list is valid, no username existing check */ bool check_whitelist(char **newval, void **extra, GucSource source) { char *rawstring; List *elemlist; /* Need a modifiable copy of string */ rawstring = pstrdup(*newval); /* Parse string into list of identifiers */ if (!SplitIdentifierString(rawstring, ',', &elemlist)) { /* syntax error in list */ GUC_check_errdetail("List syntax is invalid."); pfree(rawstring); list_free(elemlist); return false; } pfree(rawstring); list_free(elemlist); return true; } /* check if the username is in the whitelist */ bool is_in_whitelist(char *username) { char *rawstring; List *elemlist; ListCell *l; int len = strlen(username_whitelist); Assert(username != NULL); if (len == 0) return false; /* Need a modifiable copy of string */ rawstring = palloc0(sizeof(char) * (len+1)); strcpy(rawstring, username_whitelist); /* Parse string into list of identifiers */ if (!SplitIdentifierString(rawstring, ',', &elemlist)) { ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg("%s username list is invalid: %s", "credcheck.password_min_length", username_whitelist))); list_free(elemlist); pfree(rawstring); return false; } foreach(l, elemlist) { char *tok = (char *) lfirst(l); /* the username is in the list */ if (pg_strcasecmp(tok, username) == 0) { list_free(elemlist); pfree(rawstring); return true; } } list_free(elemlist); pfree(rawstring); return false; } static void password_check(const char *username, const char *password) { int pass_total_special = 0; int pass_total_digit = 0; int pass_total_upper = 0; int pass_total_lower = 0; char *tmp_pass = NULL; char *tmp_user = NULL; char *tmp_contains = NULL; char *tmp_not_contains = NULL; Assert(username != NULL); Assert(password != NULL); /* checks has to be done by ignoring case */ if (password_ignore_case) { tmp_pass = to_nlower(password, INT_MAX); tmp_user = to_nlower(username, INT_MAX); tmp_contains = to_nlower(password_contain, INT_MAX); tmp_not_contains = to_nlower(password_not_contain, INT_MAX); } else { tmp_pass = strndup(password, INT_MAX); tmp_user = strndup(username, INT_MAX); tmp_contains = strndup(password_contain, INT_MAX); tmp_not_contains = strndup(password_not_contain, INT_MAX); } /* Rule 1: password length */ if (strnlen(tmp_pass, INT_MAX) < password_min_length) { ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg(gettext_noop("password length should match the configured %s"), "credcheck.password_min_length"))); goto clean; } /* Rule 2: password contains username */ if (password_contain_username) { if (strstr(tmp_pass, tmp_user)) { ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg(gettext_noop("password should not contain username")))); goto clean; } } /* Rule 3: contain characters */ if (tmp_contains != NULL && strlen(tmp_contains) > 0) { if (str_contains(tmp_contains, tmp_pass) == false) { ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg(gettext_noop("password does not contain the configured %s characters"), "credcheck.password_contain"))); goto clean; } } /* Rule 4: not contain characters */ if (tmp_not_contains != NULL && strlen(tmp_not_contains) > 0) { if (str_contains(tmp_not_contains, tmp_pass) == true) { ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg(gettext_noop("password contains the configured %s unauthorized characters"), "credcheck.password_not_contain"))); goto clean; } } check_str_counters(tmp_pass, &pass_total_lower, &pass_total_upper, &pass_total_digit, &pass_total_special); /* Rule 5: total upper characters */ if (!password_ignore_case && pass_total_upper < password_min_upper) { ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg("password does not contain the configured %s characters", "credcheck.password_min_upper"))); goto clean; } /* Rule 6: total lower characters */ if (!password_ignore_case && pass_total_lower < password_min_lower) { ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg("password does not contain the configured %s characters", "credcheck.password_min_lower"))); goto clean; } /* Rule 7: total digits */ if (pass_total_digit < password_min_digit) { ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg("password does not contain the configured %s characters", "credcheck.password_min_digit"))); goto clean; } /* Rule 8: total special */ if (pass_total_special < password_min_special) { ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg("password does not contain the configured %s characters", "credcheck.password_min_special"))); goto clean; } /* Rule 9: minimum char repeat */ if (password_min_repeat) { if (char_repeat_exceeds(tmp_pass, password_min_repeat)) { ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg("%s characters are repeated more than the " "configured %s times", "password", "credcheck.password_min_repeat"))); goto clean; } } clean: free(tmp_pass); free(tmp_user); free(tmp_contains); free(tmp_not_contains); } static void username_guc() { DefineCustomIntVariable("credcheck.username_min_length", gettext_noop("minimum username length"), NULL, &username_min_length, 1, 1, INT_MAX, PGC_SUSET, 0, NULL, NULL, NULL); DefineCustomIntVariable("credcheck.username_min_special", gettext_noop("minimum username special characters"), NULL, &username_min_special, 0, 0, INT_MAX, PGC_SUSET, 0, NULL, NULL, NULL); DefineCustomIntVariable("credcheck.username_min_digit", gettext_noop("minimum username digits"), NULL, &username_min_digit, 0, 0, INT_MAX, PGC_SUSET, 0, NULL, NULL, NULL); DefineCustomIntVariable("credcheck.username_min_upper", gettext_noop("minimum username uppercase letters"), NULL, &username_min_upper, 0, 0, INT_MAX, PGC_SUSET, 0, NULL, NULL, NULL); DefineCustomIntVariable("credcheck.username_min_lower", gettext_noop("minimum username lowercase letters"), NULL, &username_min_lower, 0, 0, INT_MAX, PGC_SUSET, 0, NULL, NULL, NULL); DefineCustomIntVariable("credcheck.username_min_repeat", gettext_noop("minimum username characters repeat"), NULL, &username_min_repeat, 0, 0, INT_MAX, PGC_SUSET, 0, NULL, NULL, NULL); DefineCustomBoolVariable("credcheck.username_contain_password", gettext_noop("username contains password"), NULL, &username_contain_password, true, PGC_SUSET, 0, NULL, NULL, NULL); DefineCustomBoolVariable("credcheck.username_ignore_case", gettext_noop("ignore case while username checking"), NULL, &username_ignore_case, false, PGC_SUSET, 0, NULL, NULL, NULL); DefineCustomStringVariable( "credcheck.username_not_contain", gettext_noop("username should not contain these characters"), NULL, &username_not_contain, "", PGC_SUSET, 0, NULL, NULL, NULL); DefineCustomStringVariable( "credcheck.username_contain", gettext_noop("password should contain these characters"), NULL, &username_contain, "", PGC_SUSET, 0, NULL, NULL, NULL); } static void password_guc() { DefineCustomIntVariable("credcheck.password_min_length", gettext_noop("minimum password length"), NULL, &password_min_length, 1, 1, INT_MAX, PGC_SUSET, 0, NULL, NULL, NULL); DefineCustomIntVariable("credcheck.password_min_special", gettext_noop("minimum special characters"), NULL, &password_min_special, 0, 0, INT_MAX, PGC_SUSET, 0, NULL, NULL, NULL); DefineCustomIntVariable("credcheck.password_min_digit", gettext_noop("minimum password digits"), NULL, &password_min_digit, 0, 0, INT_MAX, PGC_SUSET, 0, NULL, NULL, NULL); DefineCustomIntVariable("credcheck.password_min_upper", gettext_noop("minimum password uppercase letters"), NULL, &password_min_upper, 0, 0, INT_MAX, PGC_SUSET, 0, NULL, NULL, NULL); DefineCustomIntVariable("credcheck.password_min_lower", gettext_noop("minimum password lowercase letters"), NULL, &password_min_lower, 0, 0, INT_MAX, PGC_SUSET, 0, NULL, NULL, NULL); DefineCustomIntVariable("credcheck.password_min_repeat", gettext_noop("minimum password characters repeat"), NULL, &password_min_repeat, 0, 0, INT_MAX, PGC_SUSET, 0, NULL, NULL, NULL); DefineCustomBoolVariable("credcheck.password_contain_username", gettext_noop("password contains username"), NULL, &password_contain_username, true, PGC_SUSET, 0, NULL, NULL, NULL); DefineCustomBoolVariable("credcheck.password_ignore_case", gettext_noop("ignore case while password checking"), NULL, &password_ignore_case, false, PGC_SUSET, 0, NULL, NULL, NULL); DefineCustomStringVariable( "credcheck.password_not_contain", gettext_noop("password should not contain these characters"), NULL, &password_not_contain, "", PGC_SUSET, 0, NULL, NULL, NULL); DefineCustomStringVariable( "credcheck.password_contain", gettext_noop("password should contain these characters"), NULL, &password_contain, "", PGC_SUSET, 0, NULL, NULL, NULL); #if PG_VERSION_NUM >= 120000 DefineCustomIntVariable("credcheck.password_reuse_history", gettext_noop("minimum number of password changes before permitting reuse"), NULL, &password_reuse_history, 0, 0, 100, PGC_SUSET, 0, NULL, NULL, NULL); DefineCustomIntVariable("credcheck.password_reuse_interval", gettext_noop("minimum number of days elapsed before permitting reuse"), NULL, &password_reuse_interval, 0, 0, 730, /* max 2 years */ PGC_SUSET, 0, NULL, NULL, NULL); #endif DefineCustomIntVariable("credcheck.password_valid_until", gettext_noop("force use of VALID UNTIL clause in CREATE ROLE statement" " with a minimum number of days"), NULL, &password_valid_until, 0, 0, INT_MAX, PGC_SUSET, 0, NULL, NULL, NULL); DefineCustomIntVariable("credcheck.password_valid_max", gettext_noop("force use of VALID UNTIL clause in CREATE ROLE statement" " with a maximum number of days"), NULL, &password_valid_max, 0, 0, INT_MAX, PGC_SUSET, 0, NULL, NULL, NULL); } #if PG_VERSION_NUM >= 120000 static void save_password_in_history(const char *username, const char *password) { char *encrypted_password; pgphHashKey key; pgphEntry *entry; TimestampTz dt_now = GetCurrentTimestamp(); Assert(username != NULL); Assert(password != NULL); if (password_reuse_history == 0 && password_reuse_interval == 0) return; /* Safety check... */ if (!pgph || !pgph_hash) return; /* Encrypt the password to the requested format. */ encrypted_password = strdup(str_to_sha256(password, username)); /* Store the password into share memory and password history file */ /* Set up key for hashtable search */ strcpy(key.rolename, username) ; strcpy(key.password_hash, encrypted_password); /* Lookup the hash table entry with exclusive lock. */ LWLockAcquire(pgph->lock, LW_EXCLUSIVE); /* Create new entry, if not present */ entry = (pgphEntry *) hash_search(pgph_hash, &key, HASH_FIND, NULL); if (!entry) { dt_now = GetCurrentTimestamp(); elog(DEBUG1, "Add new entry in history hash table: (%s, '%s', '%s')", username, encrypted_password, timestamptz_to_str(dt_now)); /* OK to create a new hashtable entry */ entry = pgph_entry_alloc(&key, dt_now); /* Flush the new entry to disk */ if (entry) { elog(DEBUG1, "entry added, flush change to disk"); flush_password_history(); } } LWLockRelease(pgph->lock); free(encrypted_password); } static void rename_user_in_history(const char *username, const char *newname) { pgphEntry *entry; HASH_SEQ_STATUS hash_seq; int num_changed = 0; if (password_reuse_history == 0 && password_reuse_interval == 0) return; Assert(username != NULL); Assert(newname != NULL); /* Safety check ... shouldn't get here unless shmem is set up. */ if (!pgph || !pgph_hash) return; elog(DEBUG1, "renaming user %s to %s into password history", username, newname); LWLockAcquire(pgph->lock, LW_EXCLUSIVE); hash_seq_init(&hash_seq, pgph_hash); while ((entry = hash_seq_search(&hash_seq)) != NULL) { /* update the key of matching entries */ if (strcmp(entry->key.rolename, username) == 0) { pgphHashKey key; strcpy(key.rolename, newname) ; strcpy(key.password_hash, entry->key.password_hash); hash_update_hash_key(pgph_hash, entry, &key); num_changed++; } } if (num_changed > 0) { elog(DEBUG1, "%d entries in paswword history hash table have been mofidied for user %s", num_changed, username); /* Flush the new entry to disk */ flush_password_history(); } LWLockRelease(pgph->lock); } /* * qsort comparator for sorting into increasing usage order */ static int entry_cmp(const void *lhs, const void *rhs) { TimestampTz l_password_date = (*(pgphEntry *const *) lhs)->password_date; TimestampTz r_password_date = (*(pgphEntry *const *) rhs)->password_date; if (l_password_date < r_password_date) return -1; else if (l_password_date > r_password_date) return +1; else return 0; } static void remove_password_from_history(const char *username, const char *password, int numentries) { char *encrypted_password; int32 num_entries; int32 num_user_entries = 0; int32 num_removed = 0; pgphEntry *entry; HASH_SEQ_STATUS hash_seq; pgphEntry **entries; int i = 0; if (password_reuse_history == 0 && password_reuse_interval == 0) return; Assert(username != NULL); Assert(password != NULL); /* Safety check ... shouldn't get here unless shmem is set up. */ if (!pgph || !pgph_hash) return; /* Encrypt the password to the requested format. */ encrypted_password = strdup(str_to_sha256(password, username)); elog(DEBUG1, "attempting to remove historized password = '%s' for user = '%s'", encrypted_password, username); LWLockAcquire(pgph->lock, LW_EXCLUSIVE); num_entries = hash_get_num_entries(pgph_hash); hash_seq_init(&hash_seq, pgph_hash); entries = palloc(num_entries * sizeof(pgphEntry *)); /* stores entries related to the username to be sorted by date */ while ((entry = hash_seq_search(&hash_seq)) != NULL) { if (strcmp(entry->key.rolename, username) == 0) entries[i++] = entry; } if (i == 0) { elog(DEBUG1, "no entry in the history for user: %s", username); LWLockRelease(pgph->lock); pfree(entries); return; } num_user_entries = i; /* Sort into increasing order by date */ qsort(entries, i, sizeof(pgphEntry *), entry_cmp); /* * Remove the oldest tuples when password_reuse_history is reached * until password_reuse_history size is respected for this user, * except if password_reuse_interval is enabled and not reached. * * A ascending index must exits on the date column of the table, * we use this index to treat the oldest entries first in the scan. */ for (i = 0; i < num_user_entries; i++) { bool keep = false; /* if we have a retention delay remove entries that has expired */ if (password_reuse_interval > 0) { TimestampTz dt_now = GetCurrentTimestamp(); float8 result; result = ((float8) (dt_now - entries[i]->password_date)) / 1000000.0; /* in seconds */ result /= 86400; /* in days */ elog(DEBUG1, "password_reuse_interval: %d, entry age: %d", password_reuse_interval, (int) result); /* * When the delay have not expired, keep the entry if the * number of entry exceed password_reuse_history */ if (password_reuse_interval >= (int) result) keep = true; else elog(DEBUG1, "remove_password_from_history(): this history entry has expired"); } if (!keep) { /* we need to remove the entries that exceed history size */ if ((num_user_entries - i) >= password_reuse_history) { elog(DEBUG1, "removing entry %d from the history (%s, %s)", i, entries[i]->key.rolename, entries[i]->key.password_hash); hash_search(pgph_hash, &entries[i]->key, HASH_REMOVE, NULL); num_removed++; } } } pfree(entries); /* Flush the new entry to disk */ if (num_removed > 0) flush_password_history(); LWLockRelease(pgph->lock); } static void remove_user_from_history(const char *username) { int32 num_removed = 0; pgphEntry *entry; HASH_SEQ_STATUS hash_seq; if (password_reuse_history == 0 && password_reuse_interval == 0) return; Assert(username != NULL); /* Safety check ... shouldn't get here unless shmem is set up. */ if (!pgph || !pgph_hash) return; elog(DEBUG1, "removing user %s from password history", username); /* Lookup the hash table entry with exclusive lock. */ LWLockAcquire(pgph->lock, LW_EXCLUSIVE); hash_seq_init(&hash_seq, pgph_hash); /* Sequential scan of the hash table to find the entries to remove */ while ((entry = hash_seq_search(&hash_seq)) != NULL) { if (strcmp(entry->key.rolename, username) == 0) { hash_search(pgph_hash, &entry->key, HASH_REMOVE, NULL); num_removed++; } } /* Flush the new entry to disk */ if (num_removed > 0) flush_password_history(); LWLockRelease(pgph->lock); } /* Check if the password can be reused */ static bool check_password_reuse(const char *username, const char *password) { int count_in_history = 0; pgphEntry *entry; bool found = false; char *encrypted_password; HASH_SEQ_STATUS hash_seq; Assert(username != NULL); if (password == NULL) return false; if (password_reuse_history == 0 && password_reuse_interval == 0) return false; /* Safety check... */ if (!pgph || !pgph_hash) return false; /* Encrypt the password to the requested format. */ encrypted_password = strdup(str_to_sha256(password, username)); elog(DEBUG1, "Looking for registered password = '%s' for username = '%s'", encrypted_password, username); /* Lookup the hash table entry with shared lock. */ LWLockAcquire(pgph->lock, LW_SHARED); hash_seq_init(&hash_seq, pgph_hash); while ((entry = hash_seq_search(&hash_seq)) != NULL) { if (strcmp(entry->key.rolename, username) == 0) { /* if the password is found in the history remove it if the interval is passed */ if (strcmp(encrypted_password, entry->key.password_hash) == 0) { elog(DEBUG1, "password found in history, username = '%s'," " password: '%s', saved at date: '%s'", username, entry->key.password_hash, timestamptz_to_str(entry->password_date)); /* mark that the password hash was found in the history */ found = true; /* Check the password age again the reuse interval */ if (password_reuse_interval > 0) { TimestampTz dt_now = GetCurrentTimestamp(); float8 result; result = ((float8) (dt_now - entry->password_date)) / 1000000.0; /* in seconds */ result /= 86400; /* in days */ elog(DEBUG1, "password_reuse_interval: %d, entry age: %d", password_reuse_interval, (int) result); /* * if the delay have expired skip the entry, it will be * removed later in remove_password_from_history() */ if (password_reuse_interval < (int) result) { elog(DEBUG1, "this history entry has expired"); found = false; count_in_history--; } } } /* * Even if the password was found we continue to count the number of * password stored in the history for this user. This count is used * to remove the oldest password that exceed the password_reuse_history */ count_in_history++; } } LWLockRelease(pgph->lock); free(encrypted_password); if (found) ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg(gettext_noop("Cannot use this credential following the password reuse policy")))); /* Password not found, remove passwords exceeding the history size */ remove_password_from_history(username, password, count_in_history); /* The password was not found, add the password to the history */ return true; } #endif /* Return the number of days between current timestamp and the date given as parameter */ static int check_valid_until(char *valid_until_date) { int days = 0; elog(DEBUG1, "option VALID UNTIL date: %s", valid_until_date); if (valid_until_date) { Datum validUntil_datum; TimestampTz dt_now = GetCurrentTimestamp(); TimestampTz valid_date; float8 result; validUntil_datum = DirectFunctionCall3(timestamptz_in, CStringGetDatum(valid_until_date), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); valid_date = DatumGetTimestampTz(validUntil_datum); result = ((float8) (valid_date - dt_now)) / 1000000.0; /* in seconds */ result /= 86400; /* in days */ days = (int) result; elog(DEBUG1, "option VALID UNTIL in days: %d", days); } return days; } static void check_password(const char *username, const char *password, PasswordType password_type, Datum validuntil_time, bool validuntil_null) { switch (password_type) { case PASSWORD_TYPE_PLAINTEXT: { #ifdef USE_CRACKLIB const char *reason; #endif if (is_in_whitelist((char *)username)) break; statement_has_password = true; username_check(username, password); if (password != NULL) { password_check(username, password); #ifdef USE_CRACKLIB /* call cracklib to check password */ if ((reason = FascistCheck(password, CRACKLIB_DICTPATH))) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("password is easily cracked"), errdetail_log("cracklib diagnostic: %s", reason))); #endif } break; } default: if (!encrypted_password_allowed) ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg(gettext_noop("password type is not a plain text")))); break; } } void _PG_init(void) { /* Defined GUCs */ username_guc(); password_guc(); if (process_shared_preload_libraries_in_progress) { DefineCustomIntVariable("credcheck.history_max_size", gettext_noop("maximum of entries in the password history"), NULL, &pgph_max, 65535, 1, (INT_MAX / 1024), PGC_POSTMASTER, 0, NULL, NULL, NULL); DefineCustomIntVariable("credcheck.auth_failure_cache_size", gettext_noop("maximum of entries in the auth failure cache"), NULL, &pgaf_max, 1024, 1, (INT_MAX / 1024), PGC_POSTMASTER, 0, NULL, NULL, NULL); } DefineCustomBoolVariable("credcheck.no_password_logging", gettext_noop("prevent exposing the password in error messages logged"), NULL, &no_password_logging, true, PGC_SUSET, 0, NULL, NULL, NULL); DefineCustomIntVariable("credcheck.max_auth_failure", gettext_noop("maximum number of authentication failure before" " the user loggin account be invalidated"), NULL, &fail_max, 0, 0, 64, PGC_SUSET, 0, NULL, NULL, NULL); DefineCustomBoolVariable("credcheck.reset_superuser", gettext_noop("restore superuser acces when he have been banned."), NULL, &reset_superuser, false, PGC_SIGHUP, 0, NULL, NULL, NULL); DefineCustomBoolVariable("credcheck.encrypted_password_allowed", gettext_noop("allow encrypted password to be used or throw an error"), NULL, &encrypted_password_allowed, false, PGC_SUSET, 0, NULL, NULL, NULL); DefineCustomStringVariable( "credcheck.whitelist", gettext_noop("comma separated list of username to exclude from password policy check"), NULL, &username_whitelist, "", PGC_SUSET, 0, check_whitelist, NULL, NULL); DefineCustomIntVariable("credcheck.auth_delay_ms", "Milliseconds to delay before reporting authentication failure", NULL, &auth_delay_milliseconds, 0, 0, INT_MAX / 1000, PGC_SIGHUP, GUC_UNIT_MS, NULL, NULL, NULL); #if PG_VERSION_NUM < 150000 EmitWarningsOnPlaceholders("credcheck"); /* * Request additional shared resources. (These are no-ops if we're not in * the postmaster process.) We'll allocate or attach to the shared * resources in pgph_shmem_startup(). */ RequestAddinShmemSpace(pgph_memsize()); RequestNamedLWLockTranche(PGPH_TRANCHE_NAME, 1); RequestAddinShmemSpace(pgaf_memsize()); RequestNamedLWLockTranche(PGAF_TRANCHE_NAME, 1); #else MarkGUCPrefixReserved("credcheck"); #endif /* Install hooks */ prev_ProcessUtility = ProcessUtility_hook; ProcessUtility_hook = cc_ProcessUtility; prev_check_password_hook = check_password_hook; check_password_hook = check_password; #if PG_VERSION_NUM >= 150000 prev_shmem_request_hook = shmem_request_hook; shmem_request_hook = pghist_shmem_request; #endif prev_shmem_startup_hook = shmem_startup_hook; shmem_startup_hook = pghist_shmem_startup; prev_log_hook = emit_log_hook; emit_log_hook = fix_log; prev_ClientAuthentication = ClientAuthentication_hook; ClientAuthentication_hook = credcheck_max_auth_failure; } void _PG_fini(void) { /* Uninstall hooks */ check_password_hook = prev_check_password_hook; ProcessUtility_hook = prev_ProcessUtility; emit_log_hook = prev_log_hook; #if PG_VERSION_NUM >= 150000 shmem_request_hook = prev_shmem_request_hook; #endif shmem_startup_hook = prev_shmem_startup_hook; ClientAuthentication_hook = prev_ClientAuthentication; } static void cc_ProcessUtility(PEL_PROCESSUTILITY_PROTO) { Node *parsetree = pstmt->utilityStmt; /* Execute the utility command before */ if (prev_ProcessUtility) prev_ProcessUtility(PEL_PROCESSUTILITY_ARGS); else standard_ProcessUtility(PEL_PROCESSUTILITY_ARGS); statement_has_password = false; switch (nodeTag(parsetree)) { /* Intercept ALTER USER .. RENAME statements */ case T_RenameStmt: { RenameStmt *stmt = (RenameStmt *)parsetree; /* We only take care of user renaming */ if (stmt->renameType == OBJECT_ROLE && stmt->newname != NULL) { if (is_in_whitelist(stmt->newname) || is_in_whitelist(stmt->subname)) break; /* check the validity of the username */ username_check(stmt->newname, NULL); #if PG_VERSION_NUM >= 120000 /* rename the user in the history table */ rename_user_in_history(stmt->subname, stmt->newname); #endif } break; } case T_AlterRoleStmt: { AlterRoleStmt *stmt = (AlterRoleStmt *)parsetree; ListCell *option; char *password; bool save_password = false; DefElem *dpassword = NULL; DefElem *dvalidUntil = NULL; if (is_in_whitelist(stmt->role->rolename)) break; /* Extract options from the statement node tree */ foreach(option, stmt->options) { DefElem *defel = (DefElem *) lfirst(option); if (strcmp(defel->defname, "password") == 0) { dpassword = defel; } else if (strcmp(defel->defname, "validUntil") == 0) { dvalidUntil = defel; } } #if PG_VERSION_NUM >= 120000 if (dpassword && dpassword->arg) { statement_has_password = true; password = strVal(dpassword->arg); save_password = check_password_reuse(stmt->role->rolename, password); } #endif if (dvalidUntil && dvalidUntil->arg && password_valid_until > 0) { int valid_until = check_valid_until(strVal(dvalidUntil->arg)); if (valid_until < password_valid_until) ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg(gettext_noop("the VALID UNTIL option must have a date older than %d days"), password_valid_until))); } if (dvalidUntil && dvalidUntil->arg && password_valid_max > 0) { int valid_max = check_valid_until(strVal(dvalidUntil->arg)); if (valid_max > password_valid_max) ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg(gettext_noop("the VALID UNTIL option must NOT have a date beyond %d days"), password_valid_max))); } /* The password can be saved into the history */ if (save_password) save_password_in_history(stmt->role->rolename, password); break; } case T_CreateRoleStmt: { CreateRoleStmt *stmt = (CreateRoleStmt *)parsetree; ListCell *option; int valid_until = 0; int valid_max = 0; bool has_valid_until = false; bool save_password = false; char *password; DefElem *dpassword = NULL; DefElem *dvalidUntil = NULL; if (is_in_whitelist(stmt->role)) break; /* check the validity of the username */ username_check(stmt->role, NULL); /* Extract options from the statement node tree */ foreach(option, stmt->options) { DefElem *defel = (DefElem *) lfirst(option); if (strcmp(defel->defname, "password") == 0) { dpassword = defel; } else if (strcmp(defel->defname, "validUntil") == 0) { dvalidUntil = defel; } } #if PG_VERSION_NUM >= 120000 if (dpassword && dpassword->arg) { statement_has_password = true; password = strVal(dpassword->arg); save_password = check_password_reuse(stmt->role, password); } #endif if (dvalidUntil && dvalidUntil->arg && password_valid_until > 0) { valid_until = check_valid_until(strVal(dvalidUntil->arg)); has_valid_until = true; } if (dvalidUntil && dvalidUntil->arg && password_valid_max > 0) { valid_max = check_valid_until(strVal(dvalidUntil->arg)); has_valid_until = true; } /* check that a VALID UNTIL option is present */ if ( !has_valid_until && (password_valid_until > 0 || password_valid_max > 0) ) ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg(gettext_noop("require a VALID UNTIL option")))); /* check that a minimum number of days for password validity is defined */ if (password_valid_until > 0 && valid_until < password_valid_until) ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg(gettext_noop("require a VALID UNTIL option with a date older than %d days"), password_valid_until))); /* check that a maximum number of days for password validity is defined */ if (password_valid_max > 0 && valid_max > password_valid_max) ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg(gettext_noop("require a VALID UNTIL option with a date beyond %d days"), password_valid_max))); /* The password can be saved into the history */ if (save_password) save_password_in_history(stmt->role, password); break; } #if PG_VERSION_NUM >= 120000 case T_DropRoleStmt: { DropRoleStmt *stmt = (DropRoleStmt *)parsetree; ListCell *item; foreach(item, stmt->roles) { RoleSpec *rolspec = lfirst(item); remove_user_from_history(rolspec->rolename); } break; } #endif default: break; } } #if PG_VERSION_NUM >= 120000 #if PG_VERSION_NUM >= 140000 char * str_to_sha256(const char *password, const char *salt) { int password_len = strlen(password); int saltlen = strlen(salt); uint8 checksumbuf[PG_SHA256_DIGEST_LENGTH]; char *result = palloc0(sizeof (char) * PG_SHA256_DIGEST_STRING_LENGTH); pg_hmac_ctx *hmac_ctx = pg_hmac_create(PG_SHA256); if (hmac_ctx == NULL) { pfree(result); elog(ERROR, gettext_noop("credcheck could not initialize checksum context")); } if (pg_hmac_init(hmac_ctx, (uint8 *) password, password_len) < 0 || pg_hmac_update(hmac_ctx, (uint8 *) salt, saltlen) < 0 || pg_hmac_final(hmac_ctx, checksumbuf, sizeof(checksumbuf)) < 0) { pfree(result); pg_hmac_free(hmac_ctx); elog(ERROR, gettext_noop("credcheck could not initialize checksum")); } hex_encode((char *) checksumbuf, sizeof checksumbuf, result); result[PG_SHA256_DIGEST_STRING_LENGTH - 1] = '\0'; pg_hmac_free(hmac_ctx); return result; } #else char * str_to_sha256(const char *password, const char *salt) { int password_len = strlen(password); uint8 checksumbuf[PG_SHA256_DIGEST_LENGTH]; char *result = palloc0(sizeof (char) * PG_SHA256_DIGEST_STRING_LENGTH); pg_sha256_ctx sha256_ctx; pg_sha256_init(&sha256_ctx); pg_sha256_update(&sha256_ctx, (uint8 *) password, password_len); pg_sha256_final(&sha256_ctx, checksumbuf); hex_encode((char *) checksumbuf, sizeof checksumbuf, result); result[PG_SHA256_DIGEST_STRING_LENGTH - 1] = '\0'; return result; } #endif #endif /**** * Password history feature ****/ /* * Estimate shared memory space needed for password history. */ static Size pgph_memsize(void) { Size size; size = MAXALIGN(sizeof(pgphSharedState)); size = add_size(size, hash_estimate_size(pgph_max, sizeof(pgphEntry))); return size; } /* * Estimate shared memory space needed for auth failure history. */ static Size pgaf_memsize(void) { Size size; size = MAXALIGN(sizeof(pgafSharedState)); size = add_size(size, hash_estimate_size(pgaf_max, sizeof(pgafEntry))); return size; } #if PG_VERSION_NUM >= 150000 static void pghist_shmem_request(void) { if (prev_shmem_request_hook) prev_shmem_request_hook(); /* * If you change code here, don't forget to also report the modifications in * _PG_init() for pg14 and below. */ RequestAddinShmemSpace(pgph_memsize()); RequestNamedLWLockTranche(PGPH_TRANCHE_NAME, 1); RequestAddinShmemSpace(pgaf_memsize()); RequestNamedLWLockTranche(PGAF_TRANCHE_NAME, 1); } #endif static void pghist_shmem_startup(void) { if (prev_shmem_startup_hook) prev_shmem_startup_hook(); pgph_shmem_startup(); pgaf_shmem_startup(); } /* * shmem_startup hook: allocate or attach to shared memory, * then load any pre-existing password history from text file * or create it (even if empty) while the module is enabled. */ static void pgph_shmem_startup(void) { bool found; HASHCTL info; FILE *file = NULL; uint32 header; int32 pgphver; int32 num; int32 i; /* reset in case this is a restart within the postmaster */ pgph = NULL; pgph_hash = NULL; /* * Create or attach to the shared memory state, including hash table */ LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE); pgph = ShmemInitStruct("pg_password_history", sizeof(pgphSharedState), &found); if (!found) { /* First time through ... */ pgph->lock = &(GetNamedLWLockTranche(PGPH_TRANCHE_NAME))->lock; } memset(&info, 0, sizeof(info)); info.keysize = sizeof(pgphHashKey); info.entrysize = sizeof(pgphEntry); pgph_hash = ShmemInitHash("pg_password_history hash", pgph_max, pgph_max, &info, HASH_ELEM | HASH_BLOBS); LWLockRelease(AddinShmemInitLock); /* * Done if some other process already completed our initialization. */ if (found) return; /* * Note: we don't bother with locks here, because there should be no other * processes running when this code is reached. */ /* * Attempt to load old history from the dump file. */ file = AllocateFile(PGPH_DUMP_FILE, PG_BINARY_R); if (file == NULL) { if (errno != ENOENT) goto read_error; /* No existing persisted stats file, so we're done */ return; } if (fread(&header, sizeof(uint32), 1, file) != 1 || fread(&pgphver, sizeof(uint32), 1, file) != 1 || fread(&num, sizeof(int32), 1, file) != 1) goto read_error; if (header != PGPH_FILE_HEADER || pgphver != PGPH_VERSION) goto data_error; for (i = 0; i < num; i++) { pgphEntry temp; pgphEntry *entry; if (fread(&temp, sizeof(pgphEntry), 1, file) != 1) { ereport(LOG, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("ignoring invalid data in pg_password_history file \"%s\"", PGPH_DUMP_FILE))); goto fail; } /* make the hashtable entry (discards old entries if too many) */ entry = pgph_entry_alloc(&temp.key, temp.password_date); if (!entry) goto fail; } FreeFile(file); pgph->num_entries = i + 1; return; read_error: ereport(LOG, (errcode_for_file_access(), errmsg("could not read pg_password_history file \"%s\": %m", PGPH_DUMP_FILE))); goto fail; data_error: ereport(LOG, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("ignoring invalid data in file \"%s\"", PGPH_DUMP_FILE))); fail: if (file) FreeFile(file); } static pgphEntry * pgph_entry_alloc(pgphHashKey *key, TimestampTz password_date) { pgphEntry *entry; bool found; if (hash_get_num_entries(pgph_hash) >= pgph_max) { ereport(LOG, (errcode(ERRCODE_OUT_OF_MEMORY), errmsg("can not allocate enough memory for new entry in password history cache."), errhint("You shoul increase credcheck.history_max_size."))); return NULL; } /* Find or create an entry with desired hash code */ entry = (pgphEntry *) hash_search(pgph_hash, key, HASH_ENTER, &found); /* New entry, set the timestamp */ if (!found) entry->password_date = password_date; return entry; } static pgafEntry * pgaf_entry_alloc(pgafHashKey *key, float failure_count) { pgafEntry *entry; bool found; if (hash_get_num_entries(pgaf_hash) >= pgph_max) { ereport(LOG, (errcode(ERRCODE_OUT_OF_MEMORY), errmsg("can not allocate enough memory for new entry in auth failure cache."), errhint("You shoul increase credcheck.history_max_size."))); return NULL; } /* Find or create an entry with desired hash code */ entry = (pgafEntry *) hash_search(pgaf_hash, key, HASH_ENTER, &found); /* New entry */ if (!found) { entry->failure_count = failure_count; if (failure_count >= fail_max) entry->banned_date = GetCurrentTimestamp(); } return entry; } /* * Flush password history to disk. * * IMPORTANT: the caller is responsible to emit * an exclusive lock on pgph->lock otherwise the * file can be corrupted. */ static void flush_password_history(void) { FILE *file; int32 num_entries; pgphEntry *entry; HASH_SEQ_STATUS hash_seq; /* Safety check ... shouldn't get here unless shmem is set up. */ if (!pgph || !pgph_hash) return; elog(DEBUG1, "flushing password history to file %s", PGPH_DUMP_FILE); file = AllocateFile(PGPH_DUMP_FILE ".tmp", PG_BINARY_W); if (file == NULL) goto error; if (fwrite(&PGPH_FILE_HEADER, sizeof(uint32), 1, file) != 1) goto error; if (fwrite(&PGPH_VERSION, sizeof(uint32), 1, file) != 1) goto error; num_entries = hash_get_num_entries(pgph_hash); if (fwrite(&num_entries, sizeof(int32), 1, file) != 1) goto error; hash_seq_init(&hash_seq, pgph_hash); while ((entry = hash_seq_search(&hash_seq)) != NULL) { if (fwrite(entry, sizeof(pgphEntry), 1, file) != 1) { /* note: we assume hash_seq_term won't change errno */ hash_seq_term(&hash_seq); goto error; } } /* * Fill the file until a size divisible by page size 8192 * to fix a complain of pgBackRest backup: file size X is * not divisible by page size 8192 */ fseek(file, 0, SEEK_END); while ((ftell(file) % BLCKSZ) != 0) putc(0, file); /* close the file */ if (FreeFile(file)) { file = NULL; goto error; } elog(DEBUG1, "history hash table written to disk"); /* * Rename file into place, so we atomically replace any old one. */ (void) durable_rename(PGPH_DUMP_FILE ".tmp", PGPH_DUMP_FILE, LOG); return; error: ereport(LOG, (errcode_for_file_access(), errmsg("could not write password history file \"%s\": %m", PGPH_DUMP_FILE ".tmp"))); if (file) FreeFile(file); unlink(PGPH_DUMP_FILE ".tmp"); } static void pgaf_shmem_startup(void) { bool found; HASHCTL info; /* reset in case this is a restart within the postmaster */ pgaf = NULL; pgaf_hash = NULL; /* * Create or attach to the shared memory state, including hash table */ LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE); pgaf = ShmemInitStruct("pg_auth_failure_history", sizeof(pgafSharedState), &found); if (!found) { /* First time through ... */ pgaf->lock = &(GetNamedLWLockTranche(PGAF_TRANCHE_NAME))->lock; } memset(&info, 0, sizeof(info)); info.keysize = sizeof(pgafHashKey); info.entrysize = sizeof(pgafEntry); pgaf_hash = ShmemInitHash("pg_auth_failure_history hash", pgaf_max, pgaf_max, &info, HASH_ELEM | HASH_BLOBS); LWLockRelease(AddinShmemInitLock); } PG_FUNCTION_INFO_V1(pg_password_history_reset); /* * Reset password history. */ Datum pg_password_history_reset(PG_FUNCTION_ARGS) { char *username; int num_removed = 0; HASH_SEQ_STATUS hash_seq; pgphEntry *entry; /* Safety check... */ if (!pgph || !pgph_hash) return 0; /* Only superusers can reset the history */ if (!superuser()) ereport(ERROR, (errmsg("only superuser can reset password history"))); /* Get the username to filter the entries to remove if one specified */ if (PG_NARGS() > 0) username = PG_GETARG_CSTRING(0); else username = NULL; /* Lookup the hash table entry with exclusive lock. */ LWLockAcquire(pgph->lock, LW_EXCLUSIVE); hash_seq_init(&hash_seq, pgph_hash); /* Sequential scan of the hash table to find the entries to remove */ while ((entry = hash_seq_search(&hash_seq)) != NULL) { if (username == NULL || strcmp(entry->key.rolename, username) == 0) { hash_search(pgph_hash, &entry->key, HASH_REMOVE, NULL); num_removed++; } } /* Flush the new entry to disk */ if (num_removed > 0) flush_password_history(); LWLockRelease(pgph->lock); PG_RETURN_INT32(num_removed); } PG_FUNCTION_INFO_V1(pg_password_history); /* * Show content of the password history. */ Datum pg_password_history(PG_FUNCTION_ARGS) { pg_password_history_internal(fcinfo); return (Datum) 0; } /* Common code for all versions of pg_password_history() */ static void pg_password_history_internal(FunctionCallInfo fcinfo) { ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; TupleDesc tupdesc; Tuplestorestate *tupstore; MemoryContext per_query_ctx; MemoryContext oldcontext; HASH_SEQ_STATUS hash_seq; pgphEntry *entry; /* Safety check... */ if (!pgph || !pgph_hash) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("credcheck must be loaded via shared_preload_libraries to use password history"))); /* check to see if caller supports us returning a tuplestore */ if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("set-valued function called in context that cannot accept a set"))); if (!(rsinfo->allowedModes & SFRM_Materialize)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("materialize mode required, but it is not allowed in this context"))); /* Switch into long-lived context to construct returned data structures */ per_query_ctx = rsinfo->econtext->ecxt_per_query_memory; oldcontext = MemoryContextSwitchTo(per_query_ctx); /* Build a tuple descriptor for our result type */ if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) elog(ERROR, "return type must be a row type"); tupstore = tuplestore_begin_heap(true, false, work_mem); rsinfo->returnMode = SFRM_Materialize; rsinfo->setResult = tupstore; rsinfo->setDesc = tupdesc; MemoryContextSwitchTo(oldcontext); /* * Get shared lock, iterate over the hashtable entries. * * With a large hash table, we might be holding the lock rather longer * than one could wish. However, this only blocks creation of new hash * table entries, and the larger the hash table the less likely that is to * be needed. */ LWLockAcquire(pgph->lock, LW_SHARED); hash_seq_init(&hash_seq, pgph_hash); while ((entry = hash_seq_search(&hash_seq)) != NULL) { Datum values[PG_PASSWORD_HISTORY_COLS]; bool nulls[PG_PASSWORD_HISTORY_COLS]; int i = 0; memset(values, 0, sizeof(values)); memset(nulls, 0, sizeof(nulls)); values[i++] = CStringGetDatum(entry->key.rolename); values[i++] = TimestampTzGetDatum(entry->password_date); values[i++] = CStringGetTextDatum(entry->key.password_hash); tuplestore_putvalues(tupstore, tupdesc, values, nulls); } /* clean up and return the tuplestore */ LWLockRelease(pgph->lock); tuplestore_donestoring(tupstore); } PG_FUNCTION_INFO_V1(pg_password_history_timestamp); /* * Change the password_date of all entries in password history * for a specified user. Proposed for testing purpose only. */ Datum pg_password_history_timestamp(PG_FUNCTION_ARGS) { char *username = PG_GETARG_CSTRING(0); TimestampTz new_timestamp = PG_GETARG_TIMESTAMPTZ(1); pgphEntry *entry; int num_changed = 0; HASH_SEQ_STATUS hash_seq; /* Safety check... */ if (!pgph || !pgph_hash) return 0; /* Only superusers can reset the history */ if (!superuser()) ereport(ERROR, (errmsg("only superuser can change timestamp in password history"))); /* Lookup the hash table entry with exclusive lock. */ LWLockAcquire(pgph->lock, LW_EXCLUSIVE); hash_seq_init(&hash_seq, pgph_hash); while ((entry = hash_seq_search(&hash_seq)) != NULL) { if (strcmp(entry->key.rolename, username) == 0) { entry->password_date = new_timestamp; num_changed++; } } /* Flush the new entry to disk */ if (num_changed > 0) flush_password_history(); LWLockRelease(pgph->lock); PG_RETURN_INT32(num_changed); } static void fix_log(ErrorData *edata) { if (edata->elevel != ERROR) { /* Continue chain to previous hook */ if (prev_log_hook) (*prev_log_hook) (edata); return; } /* * Error should not expose the password in the log. */ if (statement_has_password && no_password_logging) edata->hide_stmt = true; statement_has_password = false; /* Continue chain to previous hook */ if (prev_log_hook) (*prev_log_hook) (edata); } static void credcheck_max_auth_failure(Port *port, int status) { /* Inject a short delay if authentication failed. */ if (status != STATUS_OK) pg_usleep(1000L * auth_delay_milliseconds); /* check for max auth failure */ if (fail_max > 0 && status != STATUS_EOF) { Oid userOid = get_role_oid(port->user_name, true); if (userOid != InvalidOid) { float fail_num = get_auth_failure(port->user_name, userOid, status); /* register the auth failure if we not reach allowed max failure */ if (status == STATUS_ERROR && fail_num <= fail_max) fail_num = save_auth_failure(port->user_name, userOid); /* reject, this account has been banned */ if (fail_num >= fail_max) { /* * if superuser have been banned, restore the access if requested * through credcheck.reset_superuser and a configuration reload */ if (reset_superuser && userOid == 10) remove_auth_failure(port->user_name, userOid); else ereport(FATAL, (errmsg("rejecting connection, user '%s' has been banned", port->user_name))); } /* connection is ok and we have not reach the failure limit, let's reset the counter */ if (status == STATUS_OK && fail_num < fail_max) remove_auth_failure(port->user_name, userOid); } } if (prev_ClientAuthentication) prev_ClientAuthentication(port, status); } static float get_auth_failure(const char *username, Oid userid, int status) { pgafHashKey key; pgafEntry *entry; float fail_cnt = 0; Assert(username != NULL); if (fail_max == 0) return 0; /* Safety check... */ if (!pgaf || !pgaf_hash) return 0; /* Set up key for hashtable search */ key.roleid = userid ; /* Lookup the hash table entry with exclusive lock. */ LWLockAcquire(pgaf->lock, LW_EXCLUSIVE); /* Create new entry, if not present */ entry = (pgafEntry *) hash_search(pgaf_hash, &key, HASH_FIND, NULL); if (entry) fail_cnt = entry->failure_count; elog(DEBUG1, "Auth failure count for user %s is %f, fired by status: %d", username, fail_cnt, status); LWLockRelease(pgaf->lock); return fail_cnt; } static float save_auth_failure(const char *username, Oid userid) { pgafHashKey key; pgafEntry *entry; float fail_cnt = 0.5; if (!EnableSSL) fail_cnt = 1; Assert(username != NULL); if (fail_max == 0) return 0; /* Safety check... */ if (!pgaf || !pgaf_hash) return 0; /* Set up key for hashtable search */ key.roleid = userid ; /* Lookup the hash table entry with exclusive lock. */ LWLockAcquire(pgaf->lock, LW_EXCLUSIVE); /* Create new entry, if not present */ entry = (pgafEntry *) hash_search(pgaf_hash, &key, HASH_FIND, NULL); if (entry) { if (EnableSSL) fail_cnt = entry->failure_count + 0.5; else fail_cnt = entry->failure_count + 1; elog(DEBUG1, "Remove entry in auth failure hash table for user %s", username); hash_search(pgaf_hash, &entry->key, HASH_REMOVE, NULL); } elog(DEBUG1, "Add new entry in auth failure hash table for user %s (%d, %f)", username, userid, fail_cnt); /* OK to create a new hashtable entry */ entry = pgaf_entry_alloc(&key, fail_cnt); LWLockRelease(pgaf->lock); return fail_cnt; } static void remove_auth_failure(const char *username, Oid userid) { pgafHashKey key; Assert(username != NULL); if (fail_max == 0) return; /* Safety check... */ if (!pgaf || !pgaf_hash) return; /* Set up key for hashtable search */ key.roleid = userid; /* Lookup the hash table entry with exclusive lock. */ LWLockAcquire(pgaf->lock, LW_EXCLUSIVE); elog(DEBUG1, "Remove entry in auth failure hash table for user %s", username); hash_search(pgaf_hash, &key, HASH_REMOVE, NULL); LWLockRelease(pgaf->lock); } PG_FUNCTION_INFO_V1(pg_banned_role_reset); /* * Reset banned role cache. */ Datum pg_banned_role_reset(PG_FUNCTION_ARGS) { char *username; int num_removed = 0; HASH_SEQ_STATUS hash_seq; pgafEntry *entry; /* Safety check... */ if (!pgaf || !pgaf_hash) return 0; /* Only superusers can reset the history */ if (!superuser()) ereport(ERROR, (errmsg("only superuser can reset banned roles cache"))); /* Get the username to filter the entries to remove if one specified */ if (PG_NARGS() > 0) username = PG_GETARG_CSTRING(0); else username = NULL; /* Lookup the hash table entry with exclusive lock. */ LWLockAcquire(pgaf->lock, LW_EXCLUSIVE); hash_seq_init(&hash_seq, pgaf_hash); /* Sequential scan of the hash table to find the entries to remove */ while ((entry = hash_seq_search(&hash_seq)) != NULL) { if (username == NULL || (entry->key.roleid == get_role_oid(username, true))) { hash_search(pgaf_hash, &entry->key, HASH_REMOVE, NULL); num_removed++; } } LWLockRelease(pgaf->lock); PG_RETURN_INT32(num_removed); } PG_FUNCTION_INFO_V1(pg_banned_role); /* * Show list of the banned role */ Datum pg_banned_role(PG_FUNCTION_ARGS) { pg_banned_role_internal(fcinfo); return (Datum) 0; } /* Common code for all versions of pg_banned_role() */ static void pg_banned_role_internal(FunctionCallInfo fcinfo) { ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; TupleDesc tupdesc; Tuplestorestate *tupstore; MemoryContext per_query_ctx; MemoryContext oldcontext; HASH_SEQ_STATUS hash_seq; pgafEntry *entry; /* Safety check... */ if (!pgaf || !pgaf_hash) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("credcheck must be loaded via shared_preload_libraries to use auth failure feature"))); /* check to see if caller supports us returning a tuplestore */ if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("set-valued function called in context that cannot accept a set"))); if (!(rsinfo->allowedModes & SFRM_Materialize)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("materialize mode required, but it is not allowed in this context"))); /* Switch into long-lived context to construct returned data structures */ per_query_ctx = rsinfo->econtext->ecxt_per_query_memory; oldcontext = MemoryContextSwitchTo(per_query_ctx); /* Build a tuple descriptor for our result type */ if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) elog(ERROR, "return type must be a row type"); tupstore = tuplestore_begin_heap(true, false, work_mem); rsinfo->returnMode = SFRM_Materialize; rsinfo->setResult = tupstore; rsinfo->setDesc = tupdesc; MemoryContextSwitchTo(oldcontext); /* * Get shared lock, iterate over the hashtable entries. * * With a large hash table, we might be holding the lock rather longer * than one could wish. However, this only blocks creation of new hash * table entries, and the larger the hash table the less likely that is to * be needed. */ LWLockAcquire(pgaf->lock, LW_SHARED); hash_seq_init(&hash_seq, pgaf_hash); while ((entry = hash_seq_search(&hash_seq)) != NULL) { Datum values[PG_BANNED_ROLE_COLS]; bool nulls[PG_BANNED_ROLE_COLS]; int i = 0; memset(values, 0, sizeof(values)); memset(nulls, 0, sizeof(nulls)); values[i++] = Int8GetDatum(entry->key.roleid); values[i++] = Int8GetDatum(entry->failure_count); if (entry->banned_date) values[i++] = TimestampTzGetDatum(entry->banned_date); else nulls[i++] = true; tuplestore_putvalues(tupstore, tupdesc, values, nulls); } /* clean up and return the tuplestore */ LWLockRelease(pgaf->lock); tuplestore_donestoring(tupstore); } credcheck-2.6/credcheck.control000066400000000000000000000002251455626620500166230ustar00rootroot00000000000000comment = 'credcheck - postgresql plain text credential checker' default_version = '2.6.0' module_pathname = '$libdir/credcheck' relocatable = false credcheck-2.6/test/000077500000000000000000000000001455626620500142665ustar00rootroot00000000000000credcheck-2.6/test/expected/000077500000000000000000000000001455626620500160675ustar00rootroot00000000000000credcheck-2.6/test/expected/01_username.out000066400000000000000000000131301455626620500207350ustar00rootroot00000000000000LOAD 'credcheck'; -- --reset all settings -- SET credcheck.username_min_length TO DEFAULT; SET credcheck.username_min_special TO DEFAULT; SET credcheck.username_min_upper TO DEFAULT; SET credcheck.username_min_upper TO DEFAULT; SET credcheck.username_min_digit TO DEFAULT; SET credcheck.username_contain_password TO DEFAULT; SET credcheck.username_ignore_case TO DEFAULT; SET credcheck.username_contain TO DEFAULT; SET credcheck.username_not_contain TO DEFAULT; SET credcheck.username_min_repeat TO DEFAULT; SET credcheck.password_min_length TO DEFAULT; SET credcheck.password_min_special TO DEFAULT; SET credcheck.password_min_upper TO DEFAULT; SET credcheck.password_min_upper TO DEFAULT; SET credcheck.password_min_digit TO DEFAULT; SET credcheck.password_contain_username TO DEFAULT; SET credcheck.password_ignore_case TO DEFAULT; SET credcheck.password_contain TO DEFAULT; SET credcheck.password_not_contain TO DEFAULT; SET credcheck.password_min_repeat TO DEFAULT; --username checks -- --length must be >=2 -- SET credcheck.username_min_length TO 2; DROP USER IF EXISTS a; NOTICE: role "a" does not exist, skipping CREATE USER a WITH PASSWORD 'dummy'; ERROR: username length should match the configured credcheck.username_min_length DROP USER IF EXISTS a; NOTICE: role "a" does not exist, skipping CREATE USER a; ERROR: username length should match the configured credcheck.username_min_length DROP USER IF EXISTS a; NOTICE: role "a" does not exist, skipping -- --min user repeat -- SET credcheck.username_min_repeat TO 5; DROP USER IF EXISTS abbbaaaaaa; NOTICE: role "abbbaaaaaa" does not exist, skipping CREATE USER abbbaaaaaa WITH PASSWORD 'dummy'; ERROR: username characters are repeated more than the configured credcheck.username_min_repeat times DROP USER IF EXISTS abbbaaaaaa; NOTICE: role "abbbaaaaaa" does not exist, skipping -- --min special >= 1 -- SET credcheck.username_min_special TO 1; DROP USER IF EXISTS a$; NOTICE: role "a$" does not exist, skipping CREATE USER aa WITH PASSWORD 'dummy'; ERROR: username does not contain the configured credcheck.username_min_special characters CREATE USER a$ WITH PASSWORD 'dummy'; DROP USER IF EXISTS a$; -- --min upper >=1 -- SET credcheck.username_min_upper TO 1; DROP USER IF EXISTS "aA$"; NOTICE: role "aA$" does not exist, skipping CREATE USER "aa$" WITH PASSWORD 'dummy'; ERROR: username does not contain the configured credcheck.username_min_upper characters CREATE USER "aA$" WITH PASSWORD 'dummy'; DROP USER IF EXISTS "aA$"; -- --min lower >=2 -- SET credcheck.username_min_lower TO 1; DROP USER IF EXISTS "AAA$"; NOTICE: role "AAA$" does not exist, skipping CREATE USER "AAA$" WITH PASSWORD 'dummy'; ERROR: username does not contain the configured credcheck.username_min_lower characters DROP USER IF EXISTS "aaA$"; NOTICE: role "aaA$" does not exist, skipping CREATE USER "aaA$" WITH PASSWORD 'dummy'; DROP USER IF EXISTS "aaA$"; -- --must contain one of the characters 'a','b','c' -- SET credcheck.username_contain TO 'a,b,c'; DROP USER IF EXISTS "pA$user"; NOTICE: role "pA$user" does not exist, skipping CREATE USER "pA$user" WITH PASSWORD 'dummy'; ERROR: username does not contain the configured credcheck.username_contain characters DROP USER IF EXISTS "aA$user"; NOTICE: role "aA$user" does not exist, skipping CREATE USER "aA$user" WITH PASSWORD 'dummy'; DROP USER IF EXISTS "aA$user"; -- --must not contain one of the characters 'x','z' -- SET credcheck.username_not_contain TO 'x,z'; DROP USER IF EXISTS "xaA$user"; NOTICE: role "xaA$user" does not exist, skipping CREATE USER "xaA$user" WITH PASSWORD 'dummy'; ERROR: username contains the configured credcheck.username_not_contain unauthorized characters DROP USER IF EXISTS "aaA$user"; NOTICE: role "aaA$user" does not exist, skipping CREATE USER "aaA$user" WITH PASSWORD 'dummy'; DROP USER IF EXISTS "aaA$user"; -- --username contain password -- SET credcheck.username_contain_password TO on; DROP USER IF EXISTS "aaA$dummy"; NOTICE: role "aaA$dummy" does not exist, skipping CREATE USER "aaA$dummy" WITH PASSWORD 'dummy'; ERROR: username should not contain password DROP USER IF EXISTS "aaA$usernopass"; NOTICE: role "aaA$usernopass" does not exist, skipping CREATE USER "aaA$usernopass" WITH PASSWORD 'dummy'; DROP USER IF EXISTS "aaA$usernopass"; -- --ignore case while performing checks -- SET credcheck.username_ignore_case TO on; DROP USER IF EXISTS "aa$user_dummy"; NOTICE: role "aa$user_dummy" does not exist, skipping CREATE USER "aa$user_dummy" WITH PASSWORD 'DUMMY'; ERROR: username should not contain password DROP USER IF EXISTS "aa$user_DUMMY"; NOTICE: role "aa$user_DUMMY" does not exist, skipping CREATE USER "aa$user_DUMMY" WITH PASSWORD 'dummy'; ERROR: username should not contain password DROP USER IF EXISTS "aa$user_dummy"; NOTICE: role "aa$user_dummy" does not exist, skipping -- --min digit >=1 -- SET credcheck.username_min_digit TO 1; DROP USER IF EXISTS aa; NOTICE: role "aa" does not exist, skipping CREATE USER aa WITH PASSWORD 'dummy'; ERROR: username does not contain the configured credcheck.username_min_digit characters DROP USER IF EXISTS aa2; NOTICE: role "aa2" does not exist, skipping CREATE USER aa2 WITH PASSWORD 'dummy'; ERROR: username does not contain the configured credcheck.username_min_special characters DROP USER IF EXISTS aa2; NOTICE: role "aa2" does not exist, skipping CREATE USER "a$user1" WITH PASSWORD ''; ERROR: password length should match the configured credcheck.password_min_length DROP USER "a$user1"; ERROR: role "a$user1" does not exist CREATE USER aa; ERROR: username does not contain the configured credcheck.username_min_digit characters DROP USER aa; ERROR: role "aa" does not exist credcheck-2.6/test/expected/02_password.out000066400000000000000000000114611455626620500207660ustar00rootroot00000000000000LOAD 'credcheck'; -- --reset all settings -- SET credcheck.username_min_length TO DEFAULT; SET credcheck.username_min_special TO DEFAULT; SET credcheck.username_min_upper TO DEFAULT; SET credcheck.username_min_upper TO DEFAULT; SET credcheck.username_min_digit TO DEFAULT; SET credcheck.username_contain_password TO DEFAULT; SET credcheck.username_ignore_case TO DEFAULT; SET credcheck.username_contain TO DEFAULT; SET credcheck.username_not_contain TO DEFAULT; SET credcheck.username_min_repeat TO DEFAULT; SET credcheck.password_min_length TO DEFAULT; SET credcheck.password_min_special TO DEFAULT; SET credcheck.password_min_upper TO DEFAULT; SET credcheck.password_min_upper TO DEFAULT; SET credcheck.password_min_digit TO DEFAULT; SET credcheck.password_contain_username TO DEFAULT; SET credcheck.password_ignore_case TO DEFAULT; SET credcheck.password_contain TO DEFAULT; SET credcheck.password_not_contain TO DEFAULT; SET credcheck.password_min_repeat TO DEFAULT; --password checks -- --length must be >=2 -- SET credcheck.password_min_length TO 2; DROP USER IF EXISTS aa; NOTICE: role "aa" does not exist, skipping CREATE USER aa WITH PASSWORD 'd'; ERROR: password length should match the configured credcheck.password_min_length CREATE USER aa WITH PASSWORD 'dd'; DROP USER IF EXISTS aa; -- --min special >= 1 -- SET credcheck.password_min_special TO 1; DROP USER IF EXISTS aa; NOTICE: role "aa" does not exist, skipping CREATE USER aa WITH PASSWORD 'aa'; ERROR: username should not contain password CREATE USER aa WITH PASSWORD 'a$'; DROP USER IF EXISTS aa; -- --min upper >=1 -- SET credcheck.password_min_upper TO 1; DROP USER IF EXISTS "aa"; NOTICE: role "aa" does not exist, skipping CREATE USER "aa" WITH PASSWORD 'aa$'; ERROR: password should not contain username CREATE USER "aa" WITH PASSWORD 'aA$'; DROP USER IF EXISTS "aa"; -- --min lower >=2 -- SET credcheck.password_min_lower TO 1; DROP USER IF EXISTS "aa"; NOTICE: role "aa" does not exist, skipping CREATE USER "aa" WITH PASSWORD 'AA$'; ERROR: password does not contain the configured credcheck.password_min_lower characters CREATE USER "aa" WITH PASSWORD 'aA$'; DROP USER IF EXISTS "aa"; -- --must contain one of the characters 'a','b','c' -- SET credcheck.password_contain TO 'a,b,c'; DROP USER IF EXISTS "aa"; NOTICE: role "aa" does not exist, skipping CREATE USER "aa" WITH PASSWORD 'dddU$'; ERROR: password does not contain the configured credcheck.password_contain characters CREATE USER "aa" WITH PASSWORD 'ddaU$'; DROP USER IF EXISTS "aa"; -- --must not contain one of the characters 'x','z' -- SET credcheck.password_not_contain TO 'x,z'; DROP USER IF EXISTS "aa"; NOTICE: role "aa" does not exist, skipping CREATE USER "aa" WITH PASSWORD 'Ax$'; ERROR: password does not contain the configured credcheck.password_contain characters CREATE USER "aa" WITH PASSWORD 'Ab$'; DROP USER IF EXISTS "aa"; -- --passord contain username -- SET credcheck.password_contain_username TO on; DROP USER IF EXISTS "aa"; NOTICE: role "aa" does not exist, skipping CREATE USER "aa" WITH PASSWORD 'aa$'; ERROR: password should not contain username CREATE USER "aa" WITH PASSWORD 'Ab$'; DROP USER IF EXISTS "aa"; -- --ignore case while performing checks -- SET credcheck.password_ignore_case TO on; DROP USER IF EXISTS "aa"; NOTICE: role "aa" does not exist, skipping CREATE USER "aa" WITH PASSWORD 'random_AA$'; ERROR: password should not contain username DROP USER IF EXISTS "aa"; NOTICE: role "aa" does not exist, skipping -- --min digit >=1 -- SET credcheck.password_min_digit TO 1; DROP USER IF EXISTS aa; NOTICE: role "aa" does not exist, skipping CREATE USER aa WITH PASSWORD 'a@a'; ERROR: password does not contain the configured credcheck.password_min_digit characters CREATE USER aa WITH PASSWORD 'a@1'; DROP USER IF EXISTS aa; -- --min password repeat 2 -- SET credcheck.password_min_repeat TO 2; DROP USER IF EXISTS aa; NOTICE: role "aa" does not exist, skipping CREATE USER aa WITH PASSWORD '1a@bbb'; ERROR: password characters are repeated more than the configured credcheck.password_min_repeat times CREATE USER aa WITH PASSWORD '1a@a'; DROP USER IF EXISTS aa; -- -- Check NULL password -- CREATE USER aa WITH PASSWORD '1a@bcg'; ALTER USER aa PASSWORD NULL; DROP USER IF EXISTS aa; CREATE USER aa PASSWORD NULL; DROP USER IF EXISTS aa; -- -- Check whitlisted users SET credcheck.password_min_repeat TO 2; SET credcheck.whitelist = 'nocheck1,nocheck2,aaaaaaaa,bbbbbbbb,cccccccc,dddddddd,eeeeeeee,ffffffff,gggggggg'; DROP USER IF EXISTS nocheck1; NOTICE: role "nocheck1" does not exist, skipping CREATE USER nocheck1 WITH PASSWORD 'aaaa'; DROP USER IF EXISTS nocheck1; CREATE USER nocheck1; DROP USER IF EXISTS nocheck2; NOTICE: role "nocheck2" does not exist, skipping CREATE USER nocheck2 WITH PASSWORD 'aaaa'; ALTER USER nocheck2 WITH PASSWORD 'bbbb'; DROP USER IF EXISTS nocheck1; DROP USER IF EXISTS nocheck2; credcheck-2.6/test/expected/03_rename.out000066400000000000000000000056701455626620500204010ustar00rootroot00000000000000--suppress "MD5 password cleared because of role rename" messages SET client_min_messages TO warning; CREATE USER aaa PASSWORD 'DummY'; CREATE USER bbb; LOAD 'credcheck'; -- --reset all settings -- SET credcheck.username_min_length TO DEFAULT; SET credcheck.username_min_special TO DEFAULT; SET credcheck.username_min_upper TO DEFAULT; SET credcheck.username_min_upper TO DEFAULT; SET credcheck.username_min_digit TO DEFAULT; SET credcheck.username_contain_password TO DEFAULT; SET credcheck.username_ignore_case TO DEFAULT; SET credcheck.username_contain TO DEFAULT; SET credcheck.username_not_contain TO DEFAULT; SET credcheck.username_min_repeat TO DEFAULT; SET credcheck.password_min_length TO DEFAULT; SET credcheck.password_min_special TO DEFAULT; SET credcheck.password_min_upper TO DEFAULT; SET credcheck.password_min_upper TO DEFAULT; SET credcheck.password_min_digit TO DEFAULT; SET credcheck.password_contain_username TO DEFAULT; SET credcheck.password_ignore_case TO DEFAULT; SET credcheck.password_contain TO DEFAULT; SET credcheck.password_not_contain TO DEFAULT; SET credcheck.password_min_repeat TO DEFAULT; -- --length must be >=2 -- SET credcheck.username_min_length TO 2; ALTER USER aaa RENAME TO b; ERROR: username length should match the configured credcheck.username_min_length -- Check that renaiming a user without password also invoke the extension ALTER USER bbb RENAME TO b; ERROR: username length should match the configured credcheck.username_min_length DROP USER bbb; CREATE USER b; ERROR: username length should match the configured credcheck.username_min_length -- --min user repeat -- SET credcheck.username_min_repeat TO 5; ALTER USER aaa RENAME TO abbbaaaaaa; ERROR: username characters are repeated more than the configured credcheck.username_min_repeat times -- --min special >= 1 -- SET credcheck.username_min_special TO 1; ALTER USER aaa RENAME TO bbb; ERROR: username does not contain the configured credcheck.username_min_special characters -- --min upper >=1 -- SET credcheck.username_min_upper TO 1; ALTER USER aaa RENAME TO "b$bb"; ERROR: username does not contain the configured credcheck.username_min_upper characters -- --min lower >=2 -- SET credcheck.username_min_lower TO 1; ALTER USER aaa RENAME TO "B$BB"; ERROR: username does not contain the configured credcheck.username_min_lower characters -- --must contain one of the characters 'a','b','c' -- SET credcheck.username_contain TO 'a,b,c'; ALTER USER aaa RENAME TO "d$eF"; ERROR: username does not contain the configured credcheck.username_contain characters -- --must not contain one of the characters 'x','z' -- SET credcheck.username_not_contain TO 'x,z'; ALTER USER aaa RENAME TO "a$exF"; ERROR: username contains the configured credcheck.username_not_contain unauthorized characters -- --min digit >=1 -- SET credcheck.username_min_digit TO 1; ALTER USER aaa RENAME TO "a$eFD"; ERROR: username does not contain the configured credcheck.username_min_digit characters DROP USER aaa; credcheck-2.6/test/expected/04_alter_pwd.out000066400000000000000000000055531455626620500211140ustar00rootroot00000000000000CREATE USER aaa PASSWORD 'DummY'; LOAD 'credcheck'; -- --reset all settings -- SET credcheck.username_min_length TO DEFAULT; SET credcheck.username_min_special TO DEFAULT; SET credcheck.username_min_upper TO DEFAULT; SET credcheck.username_min_upper TO DEFAULT; SET credcheck.username_min_digit TO DEFAULT; SET credcheck.username_contain_password TO DEFAULT; SET credcheck.username_ignore_case TO DEFAULT; SET credcheck.username_contain TO DEFAULT; SET credcheck.username_not_contain TO DEFAULT; SET credcheck.username_min_repeat TO DEFAULT; SET credcheck.password_min_length TO DEFAULT; SET credcheck.password_min_special TO DEFAULT; SET credcheck.password_min_upper TO DEFAULT; SET credcheck.password_min_upper TO DEFAULT; SET credcheck.password_min_digit TO DEFAULT; SET credcheck.password_contain_username TO DEFAULT; SET credcheck.password_ignore_case TO DEFAULT; SET credcheck.password_contain TO DEFAULT; SET credcheck.password_not_contain TO DEFAULT; SET credcheck.password_min_repeat TO DEFAULT; --password checks -- --length must be >=2 -- SET credcheck.password_min_length TO 2; ALTER USER aaa PASSWORD 'd'; ERROR: password length should match the configured credcheck.password_min_length -- --min special >= 1 -- SET credcheck.password_min_special TO 1; ALTER USER aaa PASSWORD 'dd'; ERROR: password does not contain the configured credcheck.password_min_special characters -- --min upper >=1 -- SET credcheck.password_min_upper TO 1; ALTER USER aaa PASSWORD 'dd$'; ERROR: password does not contain the configured credcheck.password_min_upper characters -- --min lower >=2 -- SET credcheck.password_min_lower TO 1; ALTER USER aaa PASSWORD 'DD$'; ERROR: password does not contain the configured credcheck.password_min_lower characters -- --must contain one of the characters 'a','b','c' -- SET credcheck.password_contain TO 'a,b,c'; ALTER USER aaa PASSWORD 'DD$d'; ERROR: password does not contain the configured credcheck.password_contain characters -- --must not contain one of the characters 'x','z' -- SET credcheck.password_not_contain TO 'x,z'; ALTER USER aaa PASSWORD 'DD$dx'; ERROR: password does not contain the configured credcheck.password_contain characters -- -- password contain username -- SET credcheck.password_contain_username TO on; ALTER USER aaa PASSWORD 'DD$dxaaa'; ERROR: password should not contain username -- --ignore case while performing checks -- SET credcheck.password_ignore_case TO on; ALTER USER aaa PASSWORD 'DD$dxAAA'; ERROR: password should not contain username -- --min digit >=1 -- SET credcheck.password_min_digit TO 1; ALTER USER aaa PASSWORD 'DD$dA'; ERROR: password does not contain the configured credcheck.password_min_digit characters -- --min password repeat 2 -- SET credcheck.password_min_repeat TO 2; ALTER USER aaa PASSWORD 'DD$dccc1'; ERROR: password characters are repeated more than the configured credcheck.password_min_repeat times DROP USER aaa; credcheck-2.6/test/expected/05_reuse_history.out000066400000000000000000000047601455626620500220370ustar00rootroot00000000000000DROP USER IF EXISTS credtest; NOTICE: role "credtest" does not exist, skipping DROP EXTENSION credcheck CASCADE; CREATE EXTENSION credcheck; SELECT pg_password_history_reset(); pg_password_history_reset --------------------------- 0 (1 row) SELECT * FROM pg_password_history WHERE rolename = 'credtest'; rolename | password_date | password_hash ----------+---------------+--------------- (0 rows) SET credcheck.password_reuse_history = 2; -- When creating user the password must be stored in the history CREATE USER credtest WITH PASSWORD 'H8Hdre=S2'; ALTER USER credtest PASSWORD 'J8YuRe=6O'; SELECT rolename, password_hash FROM pg_password_history WHERE rolename = 'credtest' ORDER BY password_date; rolename | password_hash ----------+------------------------------------------------------------------ credtest | 7488570b80076cf9da26644d5eeb316c4768ff5bee7bf319344e7bb328032098 credtest | e61e58c22aa6bf31a92b385932f7d0e4dbaba24fa3fdb2982510d6c72a961335 (2 rows) -- fail, the credential is still in the history ALTER USER credtest PASSWORD 'J8YuRe=6O'; ERROR: Cannot use this credential following the password reuse policy -- eject the first credential from the history and add a new one ALTER USER credtest PASSWORD 'AJ8YuRe=6O0'; SELECT rolename, password_hash FROM pg_password_history WHERE rolename = 'credtest' ORDER BY password_date ; rolename | password_hash ----------+------------------------------------------------------------------ credtest | e61e58c22aa6bf31a92b385932f7d0e4dbaba24fa3fdb2982510d6c72a961335 credtest | 79320cea69ba581d5e17255c02ae08060f412f79a7c14d0e24ffca51fc03ec74 (2 rows) -- fail, the credential is still in the history ALTER USER credtest PASSWORD 'J8YuRe=6O'; ERROR: Cannot use this credential following the password reuse policy -- success, eject the second credential from the history and reuse the first one ALTER USER credtest PASSWORD 'H8Hdre=S2'; -- success, the second credential has been removed from the history ALTER USER credtest PASSWORD 'J8YuRe=6O'; -- Dropping the user must empty the record in history table DROP USER credtest; SELECT * FROM pg_password_history WHERE rolename = 'credtest'; rolename | password_date | password_hash ----------+---------------+--------------- (0 rows) -- Reset the password history SELECT pg_password_history_reset(); pg_password_history_reset --------------------------- 0 (1 row) credcheck-2.6/test/expected/05_reuse_history_1.out000066400000000000000000000047601455626620500222570ustar00rootroot00000000000000DROP USER IF EXISTS credtest; NOTICE: role "credtest" does not exist, skipping DROP EXTENSION credcheck CASCADE; CREATE EXTENSION credcheck; SELECT pg_password_history_reset(); pg_password_history_reset --------------------------- 0 (1 row) SELECT * FROM pg_password_history WHERE rolename = 'credtest'; rolename | password_date | password_hash ----------+---------------+--------------- (0 rows) SET credcheck.password_reuse_history = 2; -- When creating user the password must be stored in the history CREATE USER credtest WITH PASSWORD 'H8Hdre=S2'; ALTER USER credtest PASSWORD 'J8YuRe=6O'; SELECT rolename, password_hash FROM pg_password_history WHERE rolename = 'credtest' ORDER BY password_date; rolename | password_hash ----------+------------------------------------------------------------------ credtest | 5302ee28c0fde94ab3a23a6f660d5983bf8147397def105427d0f37e810c134c credtest | c38cf85ca6c3e5ee72c09cf0bfb42fb29b0f0a3e8ba335637941d60f86512508 (2 rows) -- fail, the credential is still in the history ALTER USER credtest PASSWORD 'J8YuRe=6O'; ERROR: Cannot use this credential following the password reuse policy -- eject the first credential from the history and add a new one ALTER USER credtest PASSWORD 'AJ8YuRe=6O0'; SELECT rolename, password_hash FROM pg_password_history WHERE rolename = 'credtest' ORDER BY password_date ; rolename | password_hash ----------+------------------------------------------------------------------ credtest | c38cf85ca6c3e5ee72c09cf0bfb42fb29b0f0a3e8ba335637941d60f86512508 credtest | c0b37cb82bc2b8a2aae606362754072224fe01651aabc688c4aa240ab450f916 (2 rows) -- fail, the credential is still in the history ALTER USER credtest PASSWORD 'J8YuRe=6O'; ERROR: Cannot use this credential following the password reuse policy -- success, eject the second credential from the history and reuse the first one ALTER USER credtest PASSWORD 'H8Hdre=S2'; -- success, the second credential has been removed from the history ALTER USER credtest PASSWORD 'J8YuRe=6O'; -- Dropping the user must empty the record in history table DROP USER credtest; SELECT * FROM pg_password_history WHERE rolename = 'credtest'; rolename | password_date | password_hash ----------+---------------+--------------- (0 rows) -- Reset the password history SELECT pg_password_history_reset(); pg_password_history_reset --------------------------- 0 (1 row) credcheck-2.6/test/expected/06_reuse_interval.out000066400000000000000000000103371455626620500221600ustar00rootroot00000000000000SET client_min_messages TO warning; DROP USER IF EXISTS credtest; DROP EXTENSION credcheck CASCADE; CREATE EXTENSION credcheck; SELECT pg_password_history_reset(); pg_password_history_reset --------------------------- 0 (1 row) SELECT * FROM pg_password_history WHERE rolename = 'credtest'; rolename | password_date | password_hash ----------+---------------+--------------- (0 rows) -- no password in the history, settings password_reuse_history -- or password_reuse_interval are not set yet CREATE USER credtest WITH PASSWORD 'AJ8YuRe=6O0'; SET credcheck.password_reuse_history = 1; SET credcheck.password_reuse_interval = 365; SELECT rolename, password_hash FROM pg_password_history WHERE rolename = 'credtest' ORDER BY password_date ; rolename | password_hash ----------+--------------- (0 rows) -- Add a new password in the history and set its age to 100 days ALTER USER credtest PASSWORD 'J8YuRe=6O'; SELECT pg_password_history_timestamp('credtest', now()::timestamp - '100 days'::interval); pg_password_history_timestamp ------------------------------- 1 (1 row) SELECT rolename, password_hash FROM pg_password_history WHERE rolename = 'credtest' ORDER BY password_date ; rolename | password_hash ----------+------------------------------------------------------------------ credtest | e61e58c22aa6bf31a92b385932f7d0e4dbaba24fa3fdb2982510d6c72a961335 (1 row) -- fail, the password is in the history for less than 1 year ALTER USER credtest PASSWORD 'J8YuRe=6O'; ERROR: Cannot use this credential following the password reuse policy SELECT rolename, password_hash FROM pg_password_history WHERE rolename = 'credtest' ORDER BY password_date ; rolename | password_hash ----------+------------------------------------------------------------------ credtest | e61e58c22aa6bf31a92b385932f7d0e4dbaba24fa3fdb2982510d6c72a961335 (1 row) -- success, but the old password must be kept in the history (interval not reached) ALTER USER credtest PASSWORD 'AJ8YuRe=6O0'; SELECT rolename, password_hash FROM pg_password_history WHERE rolename = 'credtest' ORDER BY password_date ; rolename | password_hash ----------+------------------------------------------------------------------ credtest | e61e58c22aa6bf31a92b385932f7d0e4dbaba24fa3fdb2982510d6c72a961335 credtest | 79320cea69ba581d5e17255c02ae08060f412f79a7c14d0e24ffca51fc03ec74 (2 rows) -- fail, the password is still present in the history ALTER USER credtest PASSWORD 'J8YuRe=6O'; ERROR: Cannot use this credential following the password reuse policy -- Change the age of the password to exceed the 1 year interval SELECT pg_password_history_timestamp('credtest', now()::timestamp - '380 days'::interval); pg_password_history_timestamp ------------------------------- 2 (1 row) -- success, the old password present in the history has expired ALTER USER credtest PASSWORD 'J8YuRe=6O'; SELECT rolename, password_hash FROM pg_password_history WHERE rolename = 'credtest' ORDER BY password_date ; rolename | password_hash ----------+------------------------------------------------------------------ credtest | e61e58c22aa6bf31a92b385932f7d0e4dbaba24fa3fdb2982510d6c72a961335 (1 row) -- Rename user, all entries in the history table must follow the change ALTER USER credtest RENAME TO credtest2; SELECT rolename, password_hash FROM pg_password_history WHERE rolename = 'credtest2' ORDER BY password_date ; rolename | password_hash -----------+------------------------------------------------------------------ credtest2 | e61e58c22aa6bf31a92b385932f7d0e4dbaba24fa3fdb2982510d6c72a961335 (1 row) -- Dropping the user must empty the record in history table DROP USER credtest2; SELECT * FROM pg_password_history WHERE rolename = 'credtest2'; rolename | password_date | password_hash ----------+---------------+--------------- (0 rows) -- Reset the password history SELECT pg_password_history_reset(); pg_password_history_reset --------------------------- 0 (1 row) credcheck-2.6/test/expected/06_reuse_interval_1.out000066400000000000000000000103371455626620500224000ustar00rootroot00000000000000SET client_min_messages TO warning; DROP USER IF EXISTS credtest; DROP EXTENSION credcheck CASCADE; CREATE EXTENSION credcheck; SELECT pg_password_history_reset(); pg_password_history_reset --------------------------- 0 (1 row) SELECT * FROM pg_password_history WHERE rolename = 'credtest'; rolename | password_date | password_hash ----------+---------------+--------------- (0 rows) -- no password in the history, settings password_reuse_history -- or password_reuse_interval are not set yet CREATE USER credtest WITH PASSWORD 'AJ8YuRe=6O0'; SET credcheck.password_reuse_history = 1; SET credcheck.password_reuse_interval = 365; SELECT rolename, password_hash FROM pg_password_history WHERE rolename = 'credtest' ORDER BY password_date ; rolename | password_hash ----------+--------------- (0 rows) -- Add a new password in the history and set its age to 100 days ALTER USER credtest PASSWORD 'J8YuRe=6O'; SELECT pg_password_history_timestamp('credtest', now()::timestamp - '100 days'::interval); pg_password_history_timestamp ------------------------------- 1 (1 row) SELECT rolename, password_hash FROM pg_password_history WHERE rolename = 'credtest' ORDER BY password_date ; rolename | password_hash ----------+------------------------------------------------------------------ credtest | c38cf85ca6c3e5ee72c09cf0bfb42fb29b0f0a3e8ba335637941d60f86512508 (1 row) -- fail, the password is in the history for less than 1 year ALTER USER credtest PASSWORD 'J8YuRe=6O'; ERROR: Cannot use this credential following the password reuse policy SELECT rolename, password_hash FROM pg_password_history WHERE rolename = 'credtest' ORDER BY password_date ; rolename | password_hash ----------+------------------------------------------------------------------ credtest | c38cf85ca6c3e5ee72c09cf0bfb42fb29b0f0a3e8ba335637941d60f86512508 (1 row) -- success, but the old password must be kept in the history (interval not reached) ALTER USER credtest PASSWORD 'AJ8YuRe=6O0'; SELECT rolename, password_hash FROM pg_password_history WHERE rolename = 'credtest' ORDER BY password_date ; rolename | password_hash ----------+------------------------------------------------------------------ credtest | c38cf85ca6c3e5ee72c09cf0bfb42fb29b0f0a3e8ba335637941d60f86512508 credtest | c0b37cb82bc2b8a2aae606362754072224fe01651aabc688c4aa240ab450f916 (2 rows) -- fail, the password is still present in the history ALTER USER credtest PASSWORD 'J8YuRe=6O'; ERROR: Cannot use this credential following the password reuse policy -- Change the age of the password to exceed the 1 year interval SELECT pg_password_history_timestamp('credtest', now()::timestamp - '380 days'::interval); pg_password_history_timestamp ------------------------------- 2 (1 row) -- success, the old password present in the history has expired ALTER USER credtest PASSWORD 'J8YuRe=6O'; SELECT rolename, password_hash FROM pg_password_history WHERE rolename = 'credtest' ORDER BY password_date ; rolename | password_hash ----------+------------------------------------------------------------------ credtest | c38cf85ca6c3e5ee72c09cf0bfb42fb29b0f0a3e8ba335637941d60f86512508 (1 row) -- Rename user, all entries in the history table must follow the change ALTER USER credtest RENAME TO credtest2; SELECT rolename, password_hash FROM pg_password_history WHERE rolename = 'credtest2' ORDER BY password_date ; rolename | password_hash -----------+------------------------------------------------------------------ credtest2 | c38cf85ca6c3e5ee72c09cf0bfb42fb29b0f0a3e8ba335637941d60f86512508 (1 row) -- Dropping the user must empty the record in history table DROP USER credtest2; SELECT * FROM pg_password_history WHERE rolename = 'credtest2'; rolename | password_date | password_hash ----------+---------------+--------------- (0 rows) -- Reset the password history SELECT pg_password_history_reset(); pg_password_history_reset --------------------------- 0 (1 row) credcheck-2.6/test/expected/07_valid_until.out000066400000000000000000000046431455626620500214470ustar00rootroot00000000000000LOAD 'credcheck'; -- --reset all settings -- SET credcheck.username_min_length TO DEFAULT; SET credcheck.username_min_special TO DEFAULT; SET credcheck.username_min_upper TO DEFAULT; SET credcheck.username_min_upper TO DEFAULT; SET credcheck.username_min_digit TO DEFAULT; SET credcheck.username_contain_password TO DEFAULT; SET credcheck.username_ignore_case TO DEFAULT; SET credcheck.username_contain TO DEFAULT; SET credcheck.username_not_contain TO DEFAULT; SET credcheck.username_min_repeat TO DEFAULT; SET credcheck.password_min_length TO DEFAULT; SET credcheck.password_min_special TO DEFAULT; SET credcheck.password_min_upper TO DEFAULT; SET credcheck.password_min_upper TO DEFAULT; SET credcheck.password_min_digit TO DEFAULT; SET credcheck.password_contain_username TO DEFAULT; SET credcheck.password_ignore_case TO DEFAULT; SET credcheck.password_contain TO DEFAULT; SET credcheck.password_not_contain TO DEFAULT; SET credcheck.password_min_repeat TO DEFAULT; SET credcheck.password_reuse_history = 0; SET credcheck.password_reuse_interval = 0; -- VALID UNTIL clause checks SET credcheck.password_valid_until TO 4; SET credcheck.password_valid_max TO 0; -- fail, the VALID UNTIL clause must be present CREATE USER aaa PASSWORD 'DummY'; ERROR: require a VALID UNTIL option -- Success, the VALID UNTIL clause is present and respect the delay CREATE USER aaa PASSWORD 'DummY' VALID UNTIL '2050-01-01 00:00:00'; -- fail, the VALID UNTIL clause does not respect the delay ALTER USER aaa PASSWORD 'DummY2' VALID UNTIL '2022-01-01 00:00:00'; ERROR: the VALID UNTIL option must have a date older than 4 days SET credcheck.password_valid_max TO 180; -- fail, the VALID UNTIL clause can not exceed a maximum of 180 days ALTER USER aaa PASSWORD 'DummY2' VALID UNTIL '2050-01-01 00:00:00'; ERROR: the VALID UNTIL option must NOT have a date beyond 180 days -- Clear the user DROP USER aaa; -- fail, the VALID UNTIL clause can not exceed a maximum of 180 days CREATE USER aaa PASSWORD 'DummY2' VALID UNTIL '2050-01-01 00:00:00'; ERROR: require a VALID UNTIL option with a date beyond 180 days SET credcheck.password_valid_until to 60; SET credcheck.password_reuse_interval to 15; SET credcheck.password_reuse_history to 4; CREATE role credcheck_test with login password 'password'; ERROR: require a VALID UNTIL option -- History must be empty SELECT count(*), '0' AS "expected" FROM pg_password_history ; count | expected -------+---------- 0 | 0 (1 row) credcheck-2.6/test/sql/000077500000000000000000000000001455626620500150655ustar00rootroot00000000000000credcheck-2.6/test/sql/01_username.sql000077500000000000000000000065461455626620500177430ustar00rootroot00000000000000LOAD 'credcheck'; -- --reset all settings -- SET credcheck.username_min_length TO DEFAULT; SET credcheck.username_min_special TO DEFAULT; SET credcheck.username_min_upper TO DEFAULT; SET credcheck.username_min_upper TO DEFAULT; SET credcheck.username_min_digit TO DEFAULT; SET credcheck.username_contain_password TO DEFAULT; SET credcheck.username_ignore_case TO DEFAULT; SET credcheck.username_contain TO DEFAULT; SET credcheck.username_not_contain TO DEFAULT; SET credcheck.username_min_repeat TO DEFAULT; SET credcheck.password_min_length TO DEFAULT; SET credcheck.password_min_special TO DEFAULT; SET credcheck.password_min_upper TO DEFAULT; SET credcheck.password_min_upper TO DEFAULT; SET credcheck.password_min_digit TO DEFAULT; SET credcheck.password_contain_username TO DEFAULT; SET credcheck.password_ignore_case TO DEFAULT; SET credcheck.password_contain TO DEFAULT; SET credcheck.password_not_contain TO DEFAULT; SET credcheck.password_min_repeat TO DEFAULT; --username checks -- --length must be >=2 -- SET credcheck.username_min_length TO 2; DROP USER IF EXISTS a; CREATE USER a WITH PASSWORD 'dummy'; DROP USER IF EXISTS a; CREATE USER a; DROP USER IF EXISTS a; -- --min user repeat -- SET credcheck.username_min_repeat TO 5; DROP USER IF EXISTS abbbaaaaaa; CREATE USER abbbaaaaaa WITH PASSWORD 'dummy'; DROP USER IF EXISTS abbbaaaaaa; -- --min special >= 1 -- SET credcheck.username_min_special TO 1; DROP USER IF EXISTS a$; CREATE USER aa WITH PASSWORD 'dummy'; CREATE USER a$ WITH PASSWORD 'dummy'; DROP USER IF EXISTS a$; -- --min upper >=1 -- SET credcheck.username_min_upper TO 1; DROP USER IF EXISTS "aA$"; CREATE USER "aa$" WITH PASSWORD 'dummy'; CREATE USER "aA$" WITH PASSWORD 'dummy'; DROP USER IF EXISTS "aA$"; -- --min lower >=2 -- SET credcheck.username_min_lower TO 1; DROP USER IF EXISTS "AAA$"; CREATE USER "AAA$" WITH PASSWORD 'dummy'; DROP USER IF EXISTS "aaA$"; CREATE USER "aaA$" WITH PASSWORD 'dummy'; DROP USER IF EXISTS "aaA$"; -- --must contain one of the characters 'a','b','c' -- SET credcheck.username_contain TO 'a,b,c'; DROP USER IF EXISTS "pA$user"; CREATE USER "pA$user" WITH PASSWORD 'dummy'; DROP USER IF EXISTS "aA$user"; CREATE USER "aA$user" WITH PASSWORD 'dummy'; DROP USER IF EXISTS "aA$user"; -- --must not contain one of the characters 'x','z' -- SET credcheck.username_not_contain TO 'x,z'; DROP USER IF EXISTS "xaA$user"; CREATE USER "xaA$user" WITH PASSWORD 'dummy'; DROP USER IF EXISTS "aaA$user"; CREATE USER "aaA$user" WITH PASSWORD 'dummy'; DROP USER IF EXISTS "aaA$user"; -- --username contain password -- SET credcheck.username_contain_password TO on; DROP USER IF EXISTS "aaA$dummy"; CREATE USER "aaA$dummy" WITH PASSWORD 'dummy'; DROP USER IF EXISTS "aaA$usernopass"; CREATE USER "aaA$usernopass" WITH PASSWORD 'dummy'; DROP USER IF EXISTS "aaA$usernopass"; -- --ignore case while performing checks -- SET credcheck.username_ignore_case TO on; DROP USER IF EXISTS "aa$user_dummy"; CREATE USER "aa$user_dummy" WITH PASSWORD 'DUMMY'; DROP USER IF EXISTS "aa$user_DUMMY"; CREATE USER "aa$user_DUMMY" WITH PASSWORD 'dummy'; DROP USER IF EXISTS "aa$user_dummy"; -- --min digit >=1 -- SET credcheck.username_min_digit TO 1; DROP USER IF EXISTS aa; CREATE USER aa WITH PASSWORD 'dummy'; DROP USER IF EXISTS aa2; CREATE USER aa2 WITH PASSWORD 'dummy'; DROP USER IF EXISTS aa2; CREATE USER "a$user1" WITH PASSWORD ''; DROP USER "a$user1"; CREATE USER aa; DROP USER aa; credcheck-2.6/test/sql/02_password.sql000077500000000000000000000070401455626620500177550ustar00rootroot00000000000000LOAD 'credcheck'; -- --reset all settings -- SET credcheck.username_min_length TO DEFAULT; SET credcheck.username_min_special TO DEFAULT; SET credcheck.username_min_upper TO DEFAULT; SET credcheck.username_min_upper TO DEFAULT; SET credcheck.username_min_digit TO DEFAULT; SET credcheck.username_contain_password TO DEFAULT; SET credcheck.username_ignore_case TO DEFAULT; SET credcheck.username_contain TO DEFAULT; SET credcheck.username_not_contain TO DEFAULT; SET credcheck.username_min_repeat TO DEFAULT; SET credcheck.password_min_length TO DEFAULT; SET credcheck.password_min_special TO DEFAULT; SET credcheck.password_min_upper TO DEFAULT; SET credcheck.password_min_upper TO DEFAULT; SET credcheck.password_min_digit TO DEFAULT; SET credcheck.password_contain_username TO DEFAULT; SET credcheck.password_ignore_case TO DEFAULT; SET credcheck.password_contain TO DEFAULT; SET credcheck.password_not_contain TO DEFAULT; SET credcheck.password_min_repeat TO DEFAULT; --password checks -- --length must be >=2 -- SET credcheck.password_min_length TO 2; DROP USER IF EXISTS aa; CREATE USER aa WITH PASSWORD 'd'; CREATE USER aa WITH PASSWORD 'dd'; DROP USER IF EXISTS aa; -- --min special >= 1 -- SET credcheck.password_min_special TO 1; DROP USER IF EXISTS aa; CREATE USER aa WITH PASSWORD 'aa'; CREATE USER aa WITH PASSWORD 'a$'; DROP USER IF EXISTS aa; -- --min upper >=1 -- SET credcheck.password_min_upper TO 1; DROP USER IF EXISTS "aa"; CREATE USER "aa" WITH PASSWORD 'aa$'; CREATE USER "aa" WITH PASSWORD 'aA$'; DROP USER IF EXISTS "aa"; -- --min lower >=2 -- SET credcheck.password_min_lower TO 1; DROP USER IF EXISTS "aa"; CREATE USER "aa" WITH PASSWORD 'AA$'; CREATE USER "aa" WITH PASSWORD 'aA$'; DROP USER IF EXISTS "aa"; -- --must contain one of the characters 'a','b','c' -- SET credcheck.password_contain TO 'a,b,c'; DROP USER IF EXISTS "aa"; CREATE USER "aa" WITH PASSWORD 'dddU$'; CREATE USER "aa" WITH PASSWORD 'ddaU$'; DROP USER IF EXISTS "aa"; -- --must not contain one of the characters 'x','z' -- SET credcheck.password_not_contain TO 'x,z'; DROP USER IF EXISTS "aa"; CREATE USER "aa" WITH PASSWORD 'Ax$'; CREATE USER "aa" WITH PASSWORD 'Ab$'; DROP USER IF EXISTS "aa"; -- --passord contain username -- SET credcheck.password_contain_username TO on; DROP USER IF EXISTS "aa"; CREATE USER "aa" WITH PASSWORD 'aa$'; CREATE USER "aa" WITH PASSWORD 'Ab$'; DROP USER IF EXISTS "aa"; -- --ignore case while performing checks -- SET credcheck.password_ignore_case TO on; DROP USER IF EXISTS "aa"; CREATE USER "aa" WITH PASSWORD 'random_AA$'; DROP USER IF EXISTS "aa"; -- --min digit >=1 -- SET credcheck.password_min_digit TO 1; DROP USER IF EXISTS aa; CREATE USER aa WITH PASSWORD 'a@a'; CREATE USER aa WITH PASSWORD 'a@1'; DROP USER IF EXISTS aa; -- --min password repeat 2 -- SET credcheck.password_min_repeat TO 2; DROP USER IF EXISTS aa; CREATE USER aa WITH PASSWORD '1a@bbb'; CREATE USER aa WITH PASSWORD '1a@a'; DROP USER IF EXISTS aa; -- -- Check NULL password -- CREATE USER aa WITH PASSWORD '1a@bcg'; ALTER USER aa PASSWORD NULL; DROP USER IF EXISTS aa; CREATE USER aa PASSWORD NULL; DROP USER IF EXISTS aa; -- -- Check whitlisted users SET credcheck.password_min_repeat TO 2; SET credcheck.whitelist = 'nocheck1,nocheck2,aaaaaaaa,bbbbbbbb,cccccccc,dddddddd,eeeeeeee,ffffffff,gggggggg'; DROP USER IF EXISTS nocheck1; CREATE USER nocheck1 WITH PASSWORD 'aaaa'; DROP USER IF EXISTS nocheck1; CREATE USER nocheck1; DROP USER IF EXISTS nocheck2; CREATE USER nocheck2 WITH PASSWORD 'aaaa'; ALTER USER nocheck2 WITH PASSWORD 'bbbb'; DROP USER IF EXISTS nocheck1; DROP USER IF EXISTS nocheck2; credcheck-2.6/test/sql/03_rename.sql000077500000000000000000000041041455626620500173610ustar00rootroot00000000000000--suppress "MD5 password cleared because of role rename" messages SET client_min_messages TO warning; CREATE USER aaa PASSWORD 'DummY'; CREATE USER bbb; LOAD 'credcheck'; -- --reset all settings -- SET credcheck.username_min_length TO DEFAULT; SET credcheck.username_min_special TO DEFAULT; SET credcheck.username_min_upper TO DEFAULT; SET credcheck.username_min_upper TO DEFAULT; SET credcheck.username_min_digit TO DEFAULT; SET credcheck.username_contain_password TO DEFAULT; SET credcheck.username_ignore_case TO DEFAULT; SET credcheck.username_contain TO DEFAULT; SET credcheck.username_not_contain TO DEFAULT; SET credcheck.username_min_repeat TO DEFAULT; SET credcheck.password_min_length TO DEFAULT; SET credcheck.password_min_special TO DEFAULT; SET credcheck.password_min_upper TO DEFAULT; SET credcheck.password_min_upper TO DEFAULT; SET credcheck.password_min_digit TO DEFAULT; SET credcheck.password_contain_username TO DEFAULT; SET credcheck.password_ignore_case TO DEFAULT; SET credcheck.password_contain TO DEFAULT; SET credcheck.password_not_contain TO DEFAULT; SET credcheck.password_min_repeat TO DEFAULT; -- --length must be >=2 -- SET credcheck.username_min_length TO 2; ALTER USER aaa RENAME TO b; -- Check that renaiming a user without password also invoke the extension ALTER USER bbb RENAME TO b; DROP USER bbb; CREATE USER b; -- --min user repeat -- SET credcheck.username_min_repeat TO 5; ALTER USER aaa RENAME TO abbbaaaaaa; -- --min special >= 1 -- SET credcheck.username_min_special TO 1; ALTER USER aaa RENAME TO bbb; -- --min upper >=1 -- SET credcheck.username_min_upper TO 1; ALTER USER aaa RENAME TO "b$bb"; -- --min lower >=2 -- SET credcheck.username_min_lower TO 1; ALTER USER aaa RENAME TO "B$BB"; -- --must contain one of the characters 'a','b','c' -- SET credcheck.username_contain TO 'a,b,c'; ALTER USER aaa RENAME TO "d$eF"; -- --must not contain one of the characters 'x','z' -- SET credcheck.username_not_contain TO 'x,z'; ALTER USER aaa RENAME TO "a$exF"; -- --min digit >=1 -- SET credcheck.username_min_digit TO 1; ALTER USER aaa RENAME TO "a$eFD"; DROP USER aaa; credcheck-2.6/test/sql/04_alter_pwd.sql000077500000000000000000000041071455626620500200770ustar00rootroot00000000000000CREATE USER aaa PASSWORD 'DummY'; LOAD 'credcheck'; -- --reset all settings -- SET credcheck.username_min_length TO DEFAULT; SET credcheck.username_min_special TO DEFAULT; SET credcheck.username_min_upper TO DEFAULT; SET credcheck.username_min_upper TO DEFAULT; SET credcheck.username_min_digit TO DEFAULT; SET credcheck.username_contain_password TO DEFAULT; SET credcheck.username_ignore_case TO DEFAULT; SET credcheck.username_contain TO DEFAULT; SET credcheck.username_not_contain TO DEFAULT; SET credcheck.username_min_repeat TO DEFAULT; SET credcheck.password_min_length TO DEFAULT; SET credcheck.password_min_special TO DEFAULT; SET credcheck.password_min_upper TO DEFAULT; SET credcheck.password_min_upper TO DEFAULT; SET credcheck.password_min_digit TO DEFAULT; SET credcheck.password_contain_username TO DEFAULT; SET credcheck.password_ignore_case TO DEFAULT; SET credcheck.password_contain TO DEFAULT; SET credcheck.password_not_contain TO DEFAULT; SET credcheck.password_min_repeat TO DEFAULT; --password checks -- --length must be >=2 -- SET credcheck.password_min_length TO 2; ALTER USER aaa PASSWORD 'd'; -- --min special >= 1 -- SET credcheck.password_min_special TO 1; ALTER USER aaa PASSWORD 'dd'; -- --min upper >=1 -- SET credcheck.password_min_upper TO 1; ALTER USER aaa PASSWORD 'dd$'; -- --min lower >=2 -- SET credcheck.password_min_lower TO 1; ALTER USER aaa PASSWORD 'DD$'; -- --must contain one of the characters 'a','b','c' -- SET credcheck.password_contain TO 'a,b,c'; ALTER USER aaa PASSWORD 'DD$d'; -- --must not contain one of the characters 'x','z' -- SET credcheck.password_not_contain TO 'x,z'; ALTER USER aaa PASSWORD 'DD$dx'; -- -- password contain username -- SET credcheck.password_contain_username TO on; ALTER USER aaa PASSWORD 'DD$dxaaa'; -- --ignore case while performing checks -- SET credcheck.password_ignore_case TO on; ALTER USER aaa PASSWORD 'DD$dxAAA'; -- --min digit >=1 -- SET credcheck.password_min_digit TO 1; ALTER USER aaa PASSWORD 'DD$dA'; -- --min password repeat 2 -- SET credcheck.password_min_repeat TO 2; ALTER USER aaa PASSWORD 'DD$dccc1'; DROP USER aaa; credcheck-2.6/test/sql/05_reuse_history.sql000066400000000000000000000024701455626620500210210ustar00rootroot00000000000000DROP USER IF EXISTS credtest; DROP EXTENSION credcheck CASCADE; CREATE EXTENSION credcheck; SELECT pg_password_history_reset(); SELECT * FROM pg_password_history WHERE rolename = 'credtest'; SET credcheck.password_reuse_history = 2; -- When creating user the password must be stored in the history CREATE USER credtest WITH PASSWORD 'H8Hdre=S2'; ALTER USER credtest PASSWORD 'J8YuRe=6O'; SELECT rolename, password_hash FROM pg_password_history WHERE rolename = 'credtest' ORDER BY password_date; -- fail, the credential is still in the history ALTER USER credtest PASSWORD 'J8YuRe=6O'; -- eject the first credential from the history and add a new one ALTER USER credtest PASSWORD 'AJ8YuRe=6O0'; SELECT rolename, password_hash FROM pg_password_history WHERE rolename = 'credtest' ORDER BY password_date ; -- fail, the credential is still in the history ALTER USER credtest PASSWORD 'J8YuRe=6O'; -- success, eject the second credential from the history and reuse the first one ALTER USER credtest PASSWORD 'H8Hdre=S2'; -- success, the second credential has been removed from the history ALTER USER credtest PASSWORD 'J8YuRe=6O'; -- Dropping the user must empty the record in history table DROP USER credtest; SELECT * FROM pg_password_history WHERE rolename = 'credtest'; -- Reset the password history SELECT pg_password_history_reset(); credcheck-2.6/test/sql/06_reuse_interval.sql000066400000000000000000000042771455626620500211540ustar00rootroot00000000000000SET client_min_messages TO warning; DROP USER IF EXISTS credtest; DROP EXTENSION credcheck CASCADE; CREATE EXTENSION credcheck; SELECT pg_password_history_reset(); SELECT * FROM pg_password_history WHERE rolename = 'credtest'; -- no password in the history, settings password_reuse_history -- or password_reuse_interval are not set yet CREATE USER credtest WITH PASSWORD 'AJ8YuRe=6O0'; SET credcheck.password_reuse_history = 1; SET credcheck.password_reuse_interval = 365; SELECT rolename, password_hash FROM pg_password_history WHERE rolename = 'credtest' ORDER BY password_date ; -- Add a new password in the history and set its age to 100 days ALTER USER credtest PASSWORD 'J8YuRe=6O'; SELECT pg_password_history_timestamp('credtest', now()::timestamp - '100 days'::interval); SELECT rolename, password_hash FROM pg_password_history WHERE rolename = 'credtest' ORDER BY password_date ; -- fail, the password is in the history for less than 1 year ALTER USER credtest PASSWORD 'J8YuRe=6O'; SELECT rolename, password_hash FROM pg_password_history WHERE rolename = 'credtest' ORDER BY password_date ; -- success, but the old password must be kept in the history (interval not reached) ALTER USER credtest PASSWORD 'AJ8YuRe=6O0'; SELECT rolename, password_hash FROM pg_password_history WHERE rolename = 'credtest' ORDER BY password_date ; -- fail, the password is still present in the history ALTER USER credtest PASSWORD 'J8YuRe=6O'; -- Change the age of the password to exceed the 1 year interval SELECT pg_password_history_timestamp('credtest', now()::timestamp - '380 days'::interval); -- success, the old password present in the history has expired ALTER USER credtest PASSWORD 'J8YuRe=6O'; SELECT rolename, password_hash FROM pg_password_history WHERE rolename = 'credtest' ORDER BY password_date ; -- Rename user, all entries in the history table must follow the change ALTER USER credtest RENAME TO credtest2; SELECT rolename, password_hash FROM pg_password_history WHERE rolename = 'credtest2' ORDER BY password_date ; -- Dropping the user must empty the record in history table DROP USER credtest2; SELECT * FROM pg_password_history WHERE rolename = 'credtest2'; -- Reset the password history SELECT pg_password_history_reset(); credcheck-2.6/test/sql/07_valid_until.sql000077500000000000000000000041311455626620500204300ustar00rootroot00000000000000LOAD 'credcheck'; -- --reset all settings -- SET credcheck.username_min_length TO DEFAULT; SET credcheck.username_min_special TO DEFAULT; SET credcheck.username_min_upper TO DEFAULT; SET credcheck.username_min_upper TO DEFAULT; SET credcheck.username_min_digit TO DEFAULT; SET credcheck.username_contain_password TO DEFAULT; SET credcheck.username_ignore_case TO DEFAULT; SET credcheck.username_contain TO DEFAULT; SET credcheck.username_not_contain TO DEFAULT; SET credcheck.username_min_repeat TO DEFAULT; SET credcheck.password_min_length TO DEFAULT; SET credcheck.password_min_special TO DEFAULT; SET credcheck.password_min_upper TO DEFAULT; SET credcheck.password_min_upper TO DEFAULT; SET credcheck.password_min_digit TO DEFAULT; SET credcheck.password_contain_username TO DEFAULT; SET credcheck.password_ignore_case TO DEFAULT; SET credcheck.password_contain TO DEFAULT; SET credcheck.password_not_contain TO DEFAULT; SET credcheck.password_min_repeat TO DEFAULT; SET credcheck.password_reuse_history = 0; SET credcheck.password_reuse_interval = 0; -- VALID UNTIL clause checks SET credcheck.password_valid_until TO 4; SET credcheck.password_valid_max TO 0; -- fail, the VALID UNTIL clause must be present CREATE USER aaa PASSWORD 'DummY'; -- Success, the VALID UNTIL clause is present and respect the delay CREATE USER aaa PASSWORD 'DummY' VALID UNTIL '2050-01-01 00:00:00'; -- fail, the VALID UNTIL clause does not respect the delay ALTER USER aaa PASSWORD 'DummY2' VALID UNTIL '2022-01-01 00:00:00'; SET credcheck.password_valid_max TO 180; -- fail, the VALID UNTIL clause can not exceed a maximum of 180 days ALTER USER aaa PASSWORD 'DummY2' VALID UNTIL '2050-01-01 00:00:00'; -- Clear the user DROP USER aaa; -- fail, the VALID UNTIL clause can not exceed a maximum of 180 days CREATE USER aaa PASSWORD 'DummY2' VALID UNTIL '2050-01-01 00:00:00'; SET credcheck.password_valid_until to 60; SET credcheck.password_reuse_interval to 15; SET credcheck.password_reuse_history to 4; CREATE role credcheck_test with login password 'password'; -- History must be empty SELECT count(*), '0' AS "expected" FROM pg_password_history ; credcheck-2.6/updates/000077500000000000000000000000001455626620500147545ustar00rootroot00000000000000credcheck-2.6/updates/credcheck--0.1.0--0.1.1.sql000066400000000000000000000000001455626620500206150ustar00rootroot00000000000000credcheck-2.6/updates/credcheck--0.1.1--0.2.0.sql000066400000000000000000000000001455626620500206160ustar00rootroot00000000000000credcheck-2.6/updates/credcheck--0.2.0--1.0.0.sql000066400000000000000000000033101455626620500206240ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2021-2023 MigOps Inc - All rights reserved. -- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION credcheck" to load this file. \quit CREATE SCHEMA credcheck; ---- -- Remove all entries from password history. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_password_history_reset( ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C VOLATILE; ---- -- Remove entries of the specified user from password history. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_password_history_reset( IN username name ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; ---- -- Look at password history entries ---- CREATE FUNCTION pg_password_history ( OUT rolename name, OUT password_date timestamp, OUT password_hash text ) RETURNS SETOF record AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; -- Register a view on the function for ease of use. CREATE VIEW pg_password_history AS SELECT * FROM pg_password_history(); ---- -- Change password creation timestamp for all entries of the specified -- user in the password history. Proposed for testing purpose only. -- Returns the number of entries changed. ---- CREATE FUNCTION pg_password_history_timestamp( IN username name, IN new_timestamp timestamp) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; GRANT SELECT ON pg_password_history TO PUBLIC; -- Don't want this to be available to non-superusers. REVOKE ALL ON FUNCTION pg_password_history_reset() FROM PUBLIC; REVOKE ALL ON FUNCTION pg_password_history_reset(name) FROM PUBLIC; REVOKE ALL ON FUNCTION pg_password_history_timestamp(name, timestamp) FROM PUBLIC; credcheck-2.6/updates/credcheck--1.0.0--1.1.0.sql000066400000000000000000000000001455626620500206150ustar00rootroot00000000000000credcheck-2.6/updates/credcheck--1.1.0--1.2.0.sql000066400000000000000000000000001455626620500206170ustar00rootroot00000000000000credcheck-2.6/updates/credcheck--1.2.0--2.0.0.sql000066400000000000000000000024351455626620500206350ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2021-2023 MigOps Inc - All rights reserved. -- Copyright (c) 2023 Gilles Darold - All rights reserved. -- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION credcheck" to load this file. \quit ---- -- Remove all entries from authent failure cache. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_banned_role_reset( ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C VOLATILE; ---- -- Remove entries of the specified user from authent failure cache. -- Returns the number of entries removed. ---- CREATE FUNCTION pg_banned_role_reset( IN username name ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; ---- -- Look at authent failure cache entries ---- CREATE FUNCTION pg_banned_role ( OUT roleid Oid, OUT failure_count integer, OUT banned_date timestamp ) RETURNS SETOF record AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; -- Register a view on the function for ease of use. CREATE VIEW pg_banned_role AS SELECT * FROM pg_banned_role(); GRANT SELECT ON pg_banned_role TO PUBLIC; -- Don't want this to be available to non-superusers. REVOKE ALL ON FUNCTION pg_banned_role_reset() FROM PUBLIC; REVOKE ALL ON FUNCTION pg_banned_role_reset(name) FROM PUBLIC; credcheck-2.6/updates/credcheck--2.0.0--2.1.0.sql000066400000000000000000000003121455626620500206250ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2021-2023 MigOps Inc - All rights reserved. -- Copyright (c) 2023 Gilles Darold - All rights reserved. -- No SQL change to apply in this version credcheck-2.6/updates/credcheck--2.1.0--2.2.0.sql000066400000000000000000000003121455626620500206270ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2021-2023 MigOps Inc - All rights reserved. -- Copyright (c) 2023 Gilles Darold - All rights reserved. -- No SQL change to apply in this version credcheck-2.6/updates/credcheck--2.2.0--2.3.0.sql000066400000000000000000000003121455626620500206310ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2021-2023 MigOps Inc - All rights reserved. -- Copyright (c) 2023 Gilles Darold - All rights reserved. -- No SQL change to apply in this version credcheck-2.6/updates/credcheck--2.3.0--2.4.0.sql000066400000000000000000000002201455626620500206310ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2024 HexaCluster Corp - All rights reserved. -- No SQL change to apply in this version credcheck-2.6/updates/credcheck--2.4.0--2.5.0.sql000066400000000000000000000002201455626620500206330ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2024 HexaCluster Corp - All rights reserved. -- No SQL change to apply in this version credcheck-2.6/updates/credcheck--2.5.0--2.6.0.sql000066400000000000000000000002201455626620500206350ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2024 HexaCluster Corp - All rights reserved. -- No SQL change to apply in this version