pax_global_header00006660000000000000000000000064152637267650014535gustar00rootroot0000000000000052 comment=ac9127dfe0892579a42bd2255638f9948074a151 HexaCluster-credcheck-1c98317/000077500000000000000000000000001526372676500161565ustar00rootroot00000000000000HexaCluster-credcheck-1c98317/.github/000077500000000000000000000000001526372676500175165ustar00rootroot00000000000000HexaCluster-credcheck-1c98317/.github/workflows/000077500000000000000000000000001526372676500215535ustar00rootroot00000000000000HexaCluster-credcheck-1c98317/.github/workflows/main.yml000066400000000000000000000032041526372676500232210ustar00rootroot00000000000000# This is a basic workflow to help you get started with Actions name: CI # Controls when the workflow will run on: # Triggers the workflow on push or pull request events but only for the master branch push: branches: [ master ] pull_request: branches: [ master ] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: build: runs-on: ubuntu-latest defaults: run: shell: bash strategy: matrix: pgversion: - 13 - 14 - 15 - 16 - 17 - 18 - 19 env: PGVERSION: ${{ matrix.pgversion }} steps: - name: checkout uses: actions/checkout@main - name: Install GSSAPI development headers and TAP test Perl modules run: | sudo apt-get -y update sudo apt-get -y install libkrb5-dev libipc-run-perl - name: install pg run: | sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -v $PGVERSION -p -i sudo -u postgres createuser -s "$USER" - name: build run: | make PROFILE="-Werror" sudo -E make install - name: config run: | sudo pg_conftool $PGVERSION main set shared_preload_libraries credcheck sudo sed -i '1i local all all trust' /etc/postgresql/$PGVERSION/main/pg_hba.conf sudo service postgresql restart - name: test run: | make installcheck - name: show regression diffs if: ${{ failure() }} run: | cat regression.diffs HexaCluster-credcheck-1c98317/.gitignore000066400000000000000000000001101526372676500201360ustar00rootroot00000000000000*.o .deps *.so .cproject .settings .project *.bc results/* regression.* HexaCluster-credcheck-1c98317/ChangeLog000066400000000000000000000617531526372676500177440ustar00rootroot000000000000002026-10-14 - Version 6.0 This major release adds some new features and fix issues reported by users since last release. - Restrict use of pg_password_history to pg_read_all_stats and add missing entries in the 5.1.0 to 6.0.0 update SQL file. Thanks to Georgy Shelkovy for the report. - Allow integration of the max_auth_failure feature with pgbouncer. See https://github.com/HexaCluster/credcheck#using-max-auth-failure-with-pgpouncer for explanation. Thanks to sefabalica for the feature request. - Fix pgbouncer failure when password_change_first_login is enabled. When a role must change its password first, we only want to allow the password change itself (ALTER ROLE, handled by the caller) plus the handful of harmless session/transaction-control statements that clients and connection poolers unavoidably issue while establishing a session. Thanks to Cihan Baran for the report. - Add credcheck.password_valid_min setting to force a short VALID UNTIL window on newly created roles (CREATE ROLE) while keeping the longer credcheck.password_valid_until window when existing roles change their password. Useful to force new users to change their password quickly when the client (DBeaver, pgAdmin, ...) does not support credcheck.password_change_first_login. Thanks to remib-x for the feature request - Replace the dynahash-backed pg_password_history with an integer indexed hash table living entirely in a mmap file. Backward compatibility: v5.x dump file are loaded into memory BEFORE the mapping is initialised, re-inserts the entries into the fresh store and renames the old file to pg_password_history.legacy-migrated so the migration runs once. It runs identically on a standby, so the 5.x -> 6.0 upgrade restart preserves the history on both nodes. - Add an optional last-login history modelled on the Unix last(1)/wtmp. Records user sessions (name, login time, duration, backend pid, source IP and port, and optionally the last SQL query) plus PostgreSQL boot/shutdown/crash markers. New GUCs: credcheck.lastlog, credcheck.lastlog_max, credcheck.lastlog_query_size, credcheck.lastlog_track_query, credcheck.lastlog_flush_interval. New view pg_lastlog over credcheck_lastlog(). - Change the behavior of the credcheck.no_password_logging to not hide the error statement anymore but instead to replace every PASSWORD '' clause with dots inplace of the literal. Now when credcheck.no_password_logging is enabled (the default), credcheck masks the value of every PASSWORD '' clause (and the FDW/user-mapping "password ''" option) wherever it can reach the log. - Add function pg_check_password( IN username name, IN password text ) to check a candidate password (and the associated username) against the credcheck username/password policy without creating or altering a role. Raises the same error as CREATE/ALTER ROLE would when the policy is violated, and returns true when the password is accepted. Note: the password reuse policy is not evaluated here - Use palloc for transient strings to fix memory leaks and a heap overflow. - Remove test with SHOW password_encryption for PG13 compatibility - Change CI workflow to allow local connections on unix socket for test 08 - Add select test after force change password. - Fix privilege escalation via self ALTER ROLE when password_valid_until is set. Restrict the elevation to a pure password change on the caller's own role: the statement is only run with superuser privilege when its sole option is PASSWORD (plus the VALID UNTIL clause credcheck injects itself). Any other role option makes credcheck skip the elevation and let core enforce its normal permission checks, which reject the attempt. Thanks to Alexander Kukushkin for the patch. - Replace use of strtok with strtok_r. Thanks to pavagin for the patch. - Depending on the current month, time, and time zone, 60 days in the future is sometimes displayed as "2 mons" or "1 mon 30 days" or possibly something else. Relax to checking between 59 and 61 days ahead. Thanks to Christoph Berg for the patch. - Document that password history is replication-aware on PG 15+. Thanks to Aidar Imamov for the patch. - Remove redundant PG_MAJORVERSION variable. Thanks to Aidar Imamov for the patch. - Since the introduction of the custom WAL resource manager, every mutation of the password history file is shipped to standbys through normal WAL streaming. Mention this in the 'Password reuse policy' section so users are aware that the history stays consistent across a primary/replica setup and survives a failover. - Fix off-by-one buffer overflow in to_nlower(). Thanks to Aidar Imamov for the patch. 2026-06-17 - Version 5.0 This major release adds compatibility with PostgreSQL v19 and makes password history replication-aware. - Add compatibility with PG19. Thank to Devrim Gunduz for the report. - Assign stable custom WAL RMgr ID 150 to credcheck. Thanks to Aidar Imamov for the patch. - Make password history replication-aware via a custom WAL-rmgr. Thanks to Aidar Imamov for the patch. - Add PG19 to CI workflow - Fix workflow action to main - TAP test: skip gracefully on PostgreSQL < 15, thanks to Aidar Imamov for the patch. - Add information about credcheck.disallow_change_password and credcheck.superuser_nocheck use. - Disable any CREATE/ALTER ROLE checks for superusers when credcheck.superuser_nocheck is enabled. Thanks to sergei shkavrov for the report. - Add install of IPC::Run module in CI workflow - Fix parameter name is credcheck.disallow_change_password (not disallow_password_change). Thanks to Arindam Guha for the report. 2026-04-18 - Version 4.7.0 This release fixes issues reported by users since last release and adds two new features. * Allow no password policy checks at all for changes done by a superuser enabling new GUC credcheck.superuser_nocheck. Thanks to Jacute for the feature request. * Add feature "Disallow password change" to disallow users to change their password. This behavior is enabled by setting: credcheck.disallow_password_change=on It returns the following message when a user tries to change its password: ERROR: you are not allowed to change your password. Thanks to Jacute for the reature request. Other changes in this release: - Fix credcheck.password_valid_until when CREATE/ALTER ROLE is called from a plpgsql block. Thanks to Remi Bos for the report. - Fix password_valid_until / password_valid_max behavior. Thanks to wlkvv Daniel for the report. - Fix markdown formatting before "Warning before password expire" section. 2026-02-19 - Version 4.6.0 This is a maintenance release to fix a security issues introduced in previous release. - Fix security issue with ALTER ROLE current_role the allow to change superusers password. Thanks to Alexander Kukushkin for the patch. - Fix event trigger for password expiration warning when time diff are negative values. - Disable the login event trigger when credcheck.password_valid_warning is not defined or set to 0. Users of the v4.5 release must upgrade immediately. If you are using the password expiration warning feature you should execute the event_trigger.sql in each database where it is defined. 2026-01-27 - Version 4.5.0 This is a maintenance release to fix issues reported by users since last release. - Fix error cannot change data type of view column "roleid" from oid to regrole. Thanks to dbacvetkov for the report. - Add missing upgrade SQL file from 4.3 to 4.4. Thanks to dbacvetkov for the report. - Fix error when the user want to modify his password and credcheck.password_valid_until option is set. As we modify the VALID UNTIL clause it will generate an error message when the user changes his password: Only roles with the CREATEROLE attribute and the ADMIN option on role "..." may alter this role. Thanks to dwshin0416 for the report. - Set the tzp argument in timestamp2tm() call to apply timezone to converted timestamp. Thanks to Christoph Berg for the report. 2026-01-12 - Version 4.4.0 This is a maintenance release to fix issues reported by users since last release. - Fix backend crash when current_user is used in ALTER ROLE statement. Thanks to Andrii for the report. - Add the session timezone in the call to timestamp2tm(). Thanks to Christoph Berg for the report. - Move extension SQL files into a dedicated directory. 2025-12-23 - Version 4.3.0 This is a maintenance release to fix issues reported by users since last release. - Fix background workers crash when pg_cron is used and potentially others extension using background workers. Thanks to Anton Glushakov for the report. - Only execute processUtility hook if we are at top level (not SPI re-enter). Thanks to vidierr and Anton Glushakov for the report. - Fix the change password after first login feature that was not affected only to the new user but to all users connecting to the database. 2025-10-22 - Version 4.2.0 This is a quick maintenance release to fix a PG crash when parallel workers are in used with credcheck > 3.0. - Prevent credcheck code to be executed in parallel workers. Thanks to glushakov Anton for the report. 2025-10-19 - Version 4.1.0 This release fixes broken compilation with PostgreSQL below v17 and others bugs reported by users since last release. - Remove useless creation of the credcheck schema. Thanks to georg-h for the report - Fix unapplied patch to force valid until date. - No more error are thrown when no VALID UNTIL clause are used in the CREATE/ALTER ROLE statements. It is set automatically when configuration directive password_valid_until is set to a value greater than 0. Thanks to Kennycwc for the feature request. - Prevent first login feature to be appplied to white listed username. - Update documentation about event trigger change - Completely remove the on login even trigger DDL code from the extension SQL files. To enable this feature the trigger must be created manually. - Change the pg_banned_role view to display the rolename instead of the role oid. Thanks to Alexei Takaseev for the patch. - Update documentation about PG versions requirement and the on login event trigger feature. - Add CI tests to autommatically test credcheck with PostgreSQL > 13 - Fix support to PostgreSQL before v17. 2025-10-16 - Version 4.0.0 This major release adds new features and fixes bugs reported by users since last release. - When a user change its password and a VALID UNTIL clause is not set, credcheck will automatically set to now() + credcheck.password_valid_until - Extended the functionality of 'username_contain' and 'username_not_contain' gucs to allow users to use substrings instead of single characters only. Thanks to Palak Chaturvedi for the patch. - Add feature to send a warning to the user N days before his password expires. The number of days before can be set using the `credcheck.password_valid_warning` setting. Disabled by default. This is done using an event trigger up on login. The point is that the trigger must be set manually in all databases. - Add force password change at first logging. This feature allow to force the users to change their password after the account creation. This behavior is active when credcheck.password_change_first_login is enabled. It is also possible force any user to change his password at any time using: ALTER USER user1 SET credcheck_internal.force_change_password = true; - Fix license text to the corresponding PostgreSQL license. - Fix test/expected/06_reuse_interval_1.out. Thanks to Christoph Berg for the patch. 2024-12-31 - Version 3.0.0 This release adds a new feature to exclude a list of users from being banned and fixes bugs reported by users since last release. - Fix ban count with ssl. - Fix compilation with PG11. - Add new configuration variable to exclude some users from being banned. With credcheck.whitelist_auth_failure you can set a whitelist of usernames that must be excluded from this behavior. Example of use: credcheck.whitelist_auth_failure = 'appuser1,appuser2' Thanks to Kennycwc for the feature request. - Update regression test expected output. - Add a note about the \password command. Thanks to tsoulabail for the report. - Fix test for password reuse. - Fix pg_banned_role.roleid value. Thanks to Julien Rouhaud for the patch. - Move the project under HexaCluster GitHub account. - Update copyright year. 2024-08-03 - Version 2.8.0 This release adds the compatibility with PostgreSQL 17. Upgrade require a PostgreSQL restart to reload the credcheck library. - Remove call to tuplestore_donestoring() for compatibility with PG v17. - Show actual setted parameters in error messages. Thanks to coroneltortuga for the feature request. 2024-04-25 - Version 2.7.0 This release is maintenance a release to fix a problem with pgBackRest backup. - Change location of the pg_password_history file that used to be stored in the PGDATA/global/ subdirectory and now saved directly in the PGDATA. The old location was generating a checksum warning by pgBackRest. Thanks to glushakov for the report. Backward compatibility is preserved, the file will be relocated automatically. 2024-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. HexaCluster-credcheck-1c98317/LICENSE000066400000000000000000000020111526372676500171550ustar00rootroot00000000000000PostgreSQL License Copyright (c) 2021-2023 MigOps Copyright (c) 2023 Gilles Darold Copyright (c) 2024-2026 HexaCluster Corp Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies. IN NO EVENT SHALL HEXACLUSTER BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF HEXACLUSTER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND HEXACLUSTER HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. HexaCluster-credcheck-1c98317/Makefile000066400000000000000000000017721526372676500176250ustar00rootroot00000000000000EXTENSION = 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 -flto MODULE_big = credcheck OBJS = credcheck.o $(WIN32RES) PGFILEDESC = "credcheck - postgresql credential checker" DATA = $(wildcard updates/*--*.sql) 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 \ 08_first_login 09_plpgsql 10_no_password_logging \ 11_max_auth_pgbouncer REGRESS = $(patsubst test/sql/%.sql,%,$(TESTS)) PG_CONFIG = pg_config TAP_TESTS = 1 PGXS := $(shell $(PG_CONFIG) --pgxs) include $(PGXS) HexaCluster-credcheck-1c98317/README.md000066400000000000000000001125171526372676500174440ustar00rootroot00000000000000## 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) - [Force password change](#force-password-change) - [Prohibit password changes](#disallow-password-change) - [Warning before password expire](#warning-before-password-expire) - [Last login information](#last-login-information) - [Using max_auth_failure with PgBouncer](#using-max-auth-failure-with-pgpouncer) - [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 - force users to change their password after first login - throw a warning N days before when the password user is about to expire 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_length_su | password | minimum password length for a superuser | 8 | ✓ abcdefgh | ✘ abcdefgh | | 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 or set it automatically to now() + password_valid_until days in the CREATE/ALTER ROLE statements when the password is changed and no VALID UNTIL clause is present | 60 | ✓ CREATE ROLE abcd VALID UNTIL (now()+'3 months'::interval)::date | ✘ CREATE ROLE abcd LOGIN; | | password_valid_until_su | password | same as above but applies to superuser only | 30 | ✓ CREATE ROLE abcd VALID UNTIL (now()+'2 months'::interval)::date | ✘ CREATE ROLE abcd LOGIN; | | password_valid_min | password | number of days used for the VALID UNTIL clause of newly created roles (CREATE ROLE). When greater than zero it overrides password_valid_until at role creation only, so new roles can be forced to change their password quickly while existing roles that change their password keep the password_valid_until window. When set to 0 (default) CREATE ROLE keeps using password_valid_until | 0 (disabled) | ✓ CREATE ROLE abcd VALID UNTIL (now()+'5 days'::interval)::date | ✘ CREATE ROLE abcd VALID UNTIL (now()+'2 days'::interval)::date; | | 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; | | password_valid_warn | password | emit a warning N days before the password is about to expire | 0 (disabled) | | | | password_valid_warning | password | throw a warning N days before the password expires | 0 (disabled) | | | | password_change_first_login | password | force the user to change his password at first login | false (disabled) | | | 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 password change of users named `admin` or `supuser`. To disable password policy checks for changes done by a superuser, enable GUC `credcheck.superuser_nocheck`. ### [Last login information](#last-login-information) This feature is a last-login history modelled on the Unix last(1)/wtmp. Records user sessions (name, login time, duration, backend pid, source IP and port, and optionally the last SQL query) plus PostgreSQL boot/shutdown/crash markers. To enable this feature set `credcheck.lastlog` to `on`. The history ring lives in a mmap file in the data directory, so writes go through the kernel page cache and survive a postmaster crash. A dedicated background worker msync()s periodically at `credcheck.lastlog_flush_interval` seconds (default 0, at shutdown only) and writes the clean-shutdown marker on SIGTERM; an unclean previous shutdown is detected at startup and recorded as a crash record. Live sessions are kept in anonymous shared memory and finalized into the ring at backend exit. Number of records kept in the lastlog history ring is 1024, it should be upper than max_connections. New GUCs: credcheck.lastlog_query_size, credcheck.lastlog_track_query, A view `pg_lastlog` over `check_lastlog()` allows to look at the history. ``` contrib_regression=# select * from pg_lastlog; type | username | pid | client_addr | client_port | login_time | logout_time | duration | state | query ----------+----------+--------+-------------+-------------+-------------------------------+-------------------------------+-----------------+-----------------+------- user | gilles | 148483 | [local] | 0 | 2026-09-09 16:59:07.765016+02 | | 00:00:14.202368 | still connected | user | gilles | 148481 | [local] | 0 | 2026-09-09 16:59:07.171284+02 | 2026-09-09 16:59:07.174723+02 | 00:00:00.003439 | disconnected | [...] user | gilles | 145047 | [local] | 0 | 2026-09-09 15:36:34.346523+02 | 2026-09-09 15:36:34.368934+02 | 00:00:00.022411 | disconnected | user | postgres | 144982 | [local] | 0 | 2026-09-09 15:36:30.786325+02 | 2026-09-09 15:36:30.788083+02 | 00:00:00.001758 | disconnected | user | postgres | 144977 | [local] | 0 | 2026-09-09 15:36:30.273365+02 | 2026-09-09 15:36:30.275044+02 | 00:00:00.001679 | disconnected | user | postgres | 144974 | [local] | 0 | 2026-09-09 15:36:29.757562+02 | 2026-09-09 15:36:29.762573+02 | 00:00:00.005011 | disconnected | boot | | 144960 | | | 2026-09-09 15:36:28.674986+02 | | | boot | shutdown | | | | | 2026-09-09 15:36:28.490285+02 | | | shutdown | ``` It is also possible to store the last query executed by the user by enabling `credcheck.lastlog_track_query`. The query is truncated by default at 1024 bytes but this size can be increased using `credcheck.lastlog_query_size`. ### [Using max_auth_failure with PgBouncer](#using-max-auth-failure-with-pgpouncer) #### Why the counter is bypassed credcheck counts authentication failures only fires for connections that reach PostgreSQL. With `auth_query` (or a userlist), PgBouncer fetches the password hash and **verifies the password itself**; a wrong password fails inside PgBouncer and no client connection is ever attempted against PostgreSQL. The hook never fires, so nothing is counted and no ban is set. This cannot be intercepted from inside PostgreSQL — the failure happens in another process. The patch adds an integration surface so the PgBouncer edge can feed failures into credcheck's own ban state (one `credcheck.max_auth_failure` threshold governs both direct and pooled connections) and enforce bans through `auth_query`. #### SQL functions - `credcheck_record_auth_failure(username name) returns int` Report one failure; credcheck applies `credcheck.max_auth_failure` and bans at the threshold. Returns the new count. Superuser-only, REVOKEd from PUBLIC. - `credcheck_is_banned(username name) returns bool` True once the role is banned. Read-only; grant to the auth_query role. Existing `pg_banned_role` (view) and `pg_banned_role_reset(name)` are reused for inspection and unban. ##### 1. Enforce bans at the edge (auth_query) Make the auth_query function refuse a banned role so PgBouncer denies it: ```sql -- owned by a role allowed to read pg_authid (SECURITY DEFINER) CREATE FUNCTION public.pgbouncer_auth(i_user text) RETURNS TABLE(uname text, phash text) LANGUAGE sql SECURITY DEFINER AS $$ SELECT rolname::text, rolpassword::text FROM pg_authid WHERE rolname = i_user AND rolcanlogin AND NOT credcheck_is_banned(i_user::name) $$; REVOKE ALL ON FUNCTION public.pgbouncer_auth(text) FROM PUBLIC; GRANT EXECUTE ON FUNCTION public.pgbouncer_auth(text) TO ; GRANT EXECUTE ON FUNCTION credcheck_is_banned(name) TO ; ``` pgbouncer.ini: ``` auth_query = SELECT uname, phash FROM public.pgbouncer_auth($1) ``` Note: PgBouncer caches auth_query results, so enforcement takes effect on the next cache refresh / reconnect, not instantly. #### 2. Count failures at the edge (fail2ban example) credcheck cannot see the failures, so they must be reported from where they occur. Point fail2ban at the PgBouncer log. Filter (adjust the regex to your PgBouncer version's log format): ``` # /etc/fail2ban/filter.d/pgbouncer-credcheck.conf [Definition] failregex = login failed: .*"?[^"@ ]+"?@ password authentication failed for user "?[^"]+"? ``` Action — report each failure to credcheck (injection-safe via psql `:'u'`): ``` # /etc/fail2ban/action.d/credcheck-report.conf [Definition] actionban = psql "host=PGHOST port=5432 user=f2b_reporter dbname=postgres" \ -X -q -v ON_ERROR_STOP=1 \ -c "SELECT credcheck_record_auth_failure(:'u')" -v u="" actionunban = psql "host=PGHOST port=5432 user=f2b_reporter dbname=postgres" \ -X -q -c "SELECT pg_banned_role_reset(:'u')" -v u="" ``` Grant the reporter role: ```sql GRANT EXECUTE ON FUNCTION credcheck_record_auth_failure(name) TO f2b_reporter; -- superuser-only fn ``` (record_auth_failure enforces superuser() internally; run the reporter as a superuser, or wrap it in a SECURITY DEFINER function you own and grant that.) Set fail2ban `maxretry = 1` so every failure is forwarded and credcheck owns the threshold via `credcheck.max_auth_failure`. ### [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 ``` When a user change its password and a VALID UNTIL clause is not set, credcheck will set the valid until date to `now() + credcheck.password_valid_until` The `credcheck.password_valid_min` setting can be used to give newly created roles a shorter VALID UNTIL window than the one applied when an existing role changes its password. When it is greater than zero, it overrides `credcheck.password_valid_until` for `CREATE ROLE` statements only. This is handy to force new users to change their password within a few days, for example when the client tool (DBeaver, pgAdmin, ...) does not support `credcheck.password_change_first_login`: ``` postgres=# SET credcheck.password_valid_min = 5; -- new roles: 5 days postgres=# SET credcheck.password_valid_until = 365; -- password changes: 1 year postgres=# SET credcheck.password_valid_max = 370; -- never beyond ~1 year -- new role, no VALID UNTIL given: automatically set to now() + 5 days postgres=# CREATE USER remi PASSWORD 'FirstPass1'; -- when remi later changes its password without a VALID UNTIL clause, -- credcheck sets it to now() + 365 days instead of 5. postgres=# ALTER USER remi PASSWORD 'ChosenPass1'; ``` When `credcheck.password_valid_min` is left to 0 (the default), `CREATE ROLE` keeps using `credcheck.password_valid_until` exactly as before. 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/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. Starting from PostgreSQL 15, the password history is also replication-aware: a custom WAL resource manager ships every mutation (add, remove, rename, reset, timestamp update) to standbys through the usual WAL streaming, so the history stays consistent across a primary/replica setup and survives a failover. 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" ``` If you want to exclude some users from being banned, like application users for example, you can set a whitelist of username that must be excluded from this behavior. ``` credcheck.whitelist_auth_failure = 'appuser1,appuser2' ``` ### [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. ### [Force password change](#force-password-change) This feature allow to force the users to change their password after account creation. This behavior is enabled by setting `credcheck.password_change_first_login` to `true`. For example: ``` CREATE USER user1 PASSWORD 'Rkd89,34' VALID UNTIL '2050-12-31'; ``` Then connect to PostgreSQL using the new user: ``` $ psql -h localhost -d test -U user1 Password for user user1: psql (17.5 (Ubuntu 17.5-1.pgdg24.04+1)) SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off, ALPN: postgresql) Type "help" for help. test=> SELECT count(1) FROM pg_stat_activity; ERROR: you must change your password first. test=> ALTER USER user1 PASSWORD 'Zkd89,34'; ALTER ROLE test=> test=> SELECT count(1) FROM pg_stat_activity; count ------- 6 (1 row) ``` You can also force any user to change his password at any time using: ``` ALTER USER user1 SET credcheck_internal.force_change_password = true; ``` ### [Prohibit password changes](#disallow-password-change) This feature disallow users to change their password. This behavior is enabled by setting `credcheck.disallow_change_password` to `true`. For example: ``` $ psql -h localhost -d test -U user1 psql (17.5 (Ubuntu 17.5-1.pgdg24.04+1)) SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off, ALPN: postgresql) Type "help" for help. test=> ALTER ROLE 'user1' PASSWORD 'My-New-Pass#123'; ERROR: you are not allowed to change your password. ``` Even superusers can not change password when `credcheck.disallow_change_password` is enabled, to be able to change a password the superuser must execute `SET credcheck.superuser_nocheck = on;` before in the session. ### [Warning before password expire](#warning-before-password-expire) To send a warning to the user N days before his password expires you can set the number of days before using the `credcheck.password_valid_warning` setting. This feature is only available with PostgreSQL v17 and above as it uses on login event trigger that is not available before. The message will be: `WARNING: your password will expire in 7 days, please renew your password!` To enable this feature you must execute the following DDL in all databases as postgres supersuser. To create it automatically on new databases, apply this DDL to the template0 database. ``` -- Add event trigger for valid until warning CREATE OR REPLACE FUNCTION warning_valid_until() RETURNS event_trigger AS $$ DECLARE warn_days integer; BEGIN SELECT ((extract(epoch from valuntil) - extract(epoch from current_timestamp))/86400)::integer INTO warn_days FROM pg_catalog.pg_shadow WHERE usename = SESSION_USER ; IF ( warn_days <= current_setting('credcheck.password_valid_warning', true)::integer ) THEN RAISE WARNING 'Your password will expire in % days, please renew your password!', warn_days; END IF; END; $$ LANGUAGE plpgsql SECURITY DEFINER ; -- trigger definition CREATE EVENT TRIGGER valid_until_warning ON login EXECUTE FUNCTION warning_valid_until(); ALTER EVENT TRIGGER valid_until_warning ENABLE ALWAYS; ``` This DDL is also provided in the separated file `event_trigger.sql`. ### [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`. This also affect the `\password` psql command as it sends encrypted passord to the backend. 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-2026 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 HexaCluster-credcheck-1c98317/credcheck.c000066400000000000000000004150041526372676500202410ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * 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-2026: HexaCluster Corp * *------------------------------------------------------------------------- */ #include #include #include #ifdef USE_CRACKLIB #include #endif #include "postgres.h" #include "funcapi.h" #include "miscadmin.h" #include "access/genam.h" #include "access/heapam.h" #include "access/htup_details.h" #include "access/parallel.h" #if PG_VERSION_NUM >= 150000 #include "access/xlog.h" #include "access/xlog_internal.h" #include "access/xloginsert.h" #include "access/xlogreader.h" #endif #if PG_VERSION_NUM >= 190000 #include "port.h" #endif #include "catalog/catalog.h" #include "catalog/indexing.h" #include "catalog/pg_auth_members.h" #include "catalog/pg_authid.h" #include "catalog/pg_db_role_setting.h" #include "commands/user.h" #include "common/int.h" #if PG_VERSION_NUM >= 140000 #include "common/hmac.h" #include "common/hashfn.h" #else #include "access/hash.h" #endif #include "common/sha2.h" #include "executor/spi.h" #include "libpq/auth.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "nodes/nodes.h" #include "nodes/pg_list.h" #include "postmaster/postmaster.h" #include "postmaster/bgworker.h" #include "storage/latch.h" #include "storage/proc.h" #include "storage/procarray.h" #include "tcop/utility.h" #include "tcop/tcopprot.h" /* debug_query_string */ #ifndef WIN32 #include #endif #if PG_VERSION_NUM >= 190000 #include "storage/fd.h" #endif #include "storage/ipc.h" #include "storage/lwlock.h" #include "storage/shmem.h" #if PG_VERSION_NUM >= 140000 #include "utils/wait_event.h" #else #include "pgstat.h" #endif #include "libpq/pqsignal.h" #include "utils/acl.h" #include "utils/builtins.h" #include "utils/datetime.h" #include "utils/fmgroids.h" #include "utils/guc.h" #include "utils/rel.h" #include "utils/syscache.h" #include "utils/timestamp.h" #if PG_VERSION_NUM >= 190000 #include "utils/tuplestore.h" #endif #include "utils/varlena.h" #define NOT_IN_PARALLEL_WORKER (ParallelWorkerNumber < 0) /* 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_OLD "global/pg_password_history" #define PGPH_DUMP_FILE "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; /* * New (6.0) memory-mapped password history store. The dynahash is replaced by * an integer-indexed, separately-chained hash table living entirely inside a * mmap(MAP_SHARED) file (option C). No pointer is ever stored in the mapping: * buckets and chains reference slots by index. The legacy 5.x dump file (magic * PGPH_FILE_HEADER) is migrated into this store at startup. */ #define PGPH_MMAP_MAGIC 0x50484D50 /* "PHMP" */ #define PGPH_MMAP_VERSION 200 #define PGPH_TRANCHE_NAME "credcheck_history" #define PGAF_TRANCHE_NAME "credcheck_auth_failure" /* * --------------------------------------------------------------------------- * lastlog: utmp/wtmp-like login history persisted in a memory-mapped file. * * The runtime data lives in two areas: * - an anonymous shared memory control struct (LastlogShared) holding the * LWLock and the live (still connected) sessions array. This is volatile * and never persisted. * - a memory-mapped, fixed-size ring of LastlogEntry records (the history * plus boot/shutdown/crash markers). This *is* the file, the kernel * handles write-back; we only msync() periodically and at shutdown. * * Because the mapping base address differs between processes (EXEC_BACKEND, * ASLR), NOTHING in the mapped region may store a pointer: records are * addressed by integer index only. * --------------------------------------------------------------------------- */ #define LASTLOG_DUMP_FILE "credcheck.lastlog" #define LASTLOG_TRANCHE_NAME "credcheck_lastlog" static const uint32 LASTLOG_FILE_HEADER = 0x4C4C4743; /* "LLGC" */ static const uint32 LASTLOG_VERSION = 100; #define LASTLOG_HOST_LEN 64 typedef enum LastlogType { LL_USER = 0, /* a user session */ LL_BOOT, /* PostgreSQL (re)start */ LL_SHUTDOWN, /* clean shutdown */ LL_CRASH /* unclean previous shutdown detected at next boot */ } LastlogType; /* * A single fixed-size record. The query text is stored inline right after * the struct, capped at lastlog_query_size bytes; the real on-disk/in-mmap * stride is ll_record_stride (computed at startup and stored in the header), * so last_query must remain the final member. */ typedef struct LastlogEntry { uint64 seqno; /* monotonic; 0 == empty slot */ bool complete; /* written last, torn-write guard */ LastlogType type; char username[NAMEDATALEN]; int pid; char remote_host[LASTLOG_HOST_LEN]; /* IP or "[local]" */ int remote_port; TimestampTz login_time; TimestampTz logout_time; /* 0 while still connected */ bool active; /* still connected */ char last_query[FLEXIBLE_ARRAY_MEMBER]; /* size lastlog_query_size */ } LastlogEntry; #define LASTLOG_BASE_SIZE (offsetof(LastlogEntry, last_query)) /* On-disk/in-mmap header, kept at offset 0 of the mapping. */ typedef struct LastlogFileHeader { uint32 magic; uint32 version; uint32 record_stride; /* bytes per record incl. inline query */ uint32 capacity; /* number of record slots (= lastlog_max) */ uint32 head; /* index of next slot to (over)write */ uint32 nused; /* number of valid records (<= capacity) */ uint64 seqno; /* last allocated sequence number */ bool clean_shutdown; /* set true only on clean shutdown */ } LastlogFileHeader; /* * Live session slot, one per backend, addressed by MyProc->pgprocno. * Lives in anonymous shared memory, never persisted. */ typedef struct LastlogActive { bool in_use; int pid; char username[NAMEDATALEN]; char remote_host[LASTLOG_HOST_LEN]; int remote_port; TimestampTz login_time; char *last_query; /* points inside LastlogShared->query_area */ } LastlogActive; /* Anonymous shared control structure for lastlog. */ typedef struct LastlogShared { LWLock *lock; /* protects the mmap ring and active[] */ int nactive_slots; /* == MaxBackends */ LastlogActive active[FLEXIBLE_ARRAY_MEMBER]; /* query_area (nactive_slots * lastlog_query_size bytes) follows */ } LastlogShared; #if PG_VERSION_NUM >= 150000 /* * Custom WAL resource manager used to keep the password history file in * sync between the primary and its standbys. * * RM_CREDCHECK_ID (150) is a stable custom WAL resource manager ID * allocated to credcheck; see the PostgreSQL custom WAL Rmgr registry at * https://wiki.postgresql.org/wiki/CustomWALResourceManagers. */ #define RM_CREDCHECK_ID 150 #define CREDCHECK_RMGR_NAME "credcheck" #define XLOG_CREDCHECK_PWD_ADD 0x10 #define XLOG_CREDCHECK_PWD_REMOVE 0x20 #define XLOG_CREDCHECK_PWD_REMOVE_USER 0x30 #define XLOG_CREDCHECK_PWD_RENAME 0x40 #define XLOG_CREDCHECK_PWD_RESET 0x50 #define XLOG_CREDCHECK_PWD_TIMESTAMP 0x60 typedef struct xl_credcheck_pwd_add { char rolename[NAMEDATALEN]; char password_hash[PG_SHA256_DIGEST_STRING_LENGTH]; TimestampTz password_date; } xl_credcheck_pwd_add; typedef struct xl_credcheck_pwd_remove { char rolename[NAMEDATALEN]; char password_hash[PG_SHA256_DIGEST_STRING_LENGTH]; } xl_credcheck_pwd_remove; typedef struct xl_credcheck_pwd_remove_user { char rolename[NAMEDATALEN]; } xl_credcheck_pwd_remove_user; typedef struct xl_credcheck_pwd_rename { char oldname[NAMEDATALEN]; char newname[NAMEDATALEN]; } xl_credcheck_pwd_rename; typedef struct xl_credcheck_pwd_reset { bool has_user; char rolename[NAMEDATALEN]; /* valid iff has_user */ } xl_credcheck_pwd_reset; typedef struct xl_credcheck_pwd_timestamp { char rolename[NAMEDATALEN]; TimestampTz new_timestamp; } xl_credcheck_pwd_timestamp; static void credcheck_rmgr_redo(XLogReaderState *record); static void credcheck_rmgr_desc(StringInfo buf, XLogReaderState *record); static const char *credcheck_rmgr_identify(uint8 info); static RmgrData credcheck_rmgr = { .rm_name = CREDCHECK_RMGR_NAME, .rm_redo = credcheck_rmgr_redo, .rm_desc = credcheck_rmgr_desc, .rm_identify = credcheck_rmgr_identify, }; #endif /* PG_VERSION_NUM >= 150000 */ 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 ExecutorStart_hook_type prev_ExecutorStart = 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; /* On-disk/in-mmap header for the password history store. */ typedef struct PgphMmapHeader { uint32 magic; uint32 version; uint32 capacity; /* number of slots (= pgph_max) */ uint32 nbuckets; /* number of hash buckets */ int32 nused; /* slots in use */ int32 freehead; /* head of free-slot list, -1 if full */ } PgphMmapHeader; /* One slot of the chained hash table; addressed by integer index only. */ typedef struct PgphSlot { int32 next; /* next slot in bucket chain or free list */ int32 used; /* 1 = occupied, 0 = free */ pgphEntry entry; /* {key, password_date} */ } PgphSlot; /* 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; /* pgph memory-mapped store (process-local pointers into the mapping) */ static char *pgph_base = NULL; static PgphMmapHeader *pgph_hdr = NULL; static int32 *pgph_buckets = NULL; static PgphSlot *pgph_slots = NULL; static Size pgph_mmap_size = 0; static int pgph_max = 65535; static int pgaf_max = 1024; /* lastlog runtime state (process-local where noted) */ static LastlogShared *lastlog = NULL; /* anon shmem control */ static char *ll_mmap_base = NULL; /* process-local mmap base */ static LastlogFileHeader *ll_hdr = NULL; /* == (header *) ll_mmap_base */ static Size ll_mmap_size = 0; /* process-local mapped size */ static uint32 ll_record_stride = 0; /* bytes per record */ static int ll_my_slot = -1; /* this backend's active idx */ /* lastlog GUCs */ static bool lastlog_enabled = false; static int lastlog_max = 1024; static bool lastlog_track_query = false; static int lastlog_query_size = 1024; static int lastlog_flush_interval = 0; /* seconds; 0 = shutdown only */ 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 cc_ExecutorStart(QueryDesc *queryDesc, int eflags); static void flush_password_history(void); static pgphEntry *pgph_entry_alloc(pgphHashKey *key, TimestampTz password_date); /* pgph memory-mapped table API */ static void pgph_mmap_attach(void); static void pgph_tab_init(void); static pgphEntry *pgph_tab_find(pgphHashKey *key); static pgphEntry *pgph_tab_enter(pgphHashKey *key, bool *found); static void pgph_tab_remove(pgphHashKey *key); static void pgph_tab_rekey(pgphEntry *entry, pgphHashKey *newkey); static int32 pgph_tab_count(void); static void pgph_msync(int flags); static pgphEntry *pgph_slurp_legacy(int32 *n_out); 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); /* lastlog forward declarations */ static Size lastlog_memsize(void); static void lastlog_shmem_startup(void); static void lastlog_mmap_attach(void); static void lastlog_init_header(void); static void lastlog_append_marker(LastlogType type); static void lastlog_record_login(Port *port); static void lastlog_finalize_session(int code, Datum arg); static void lastlog_track_last_query(const char *query_string); static void lastlog_msync(int flags); PGDLLEXPORT void lastlog_bgworker_main(Datum main_arg); static void lastlog_bgworker_shutdown(int code, Datum arg); extern PGDLLEXPORT Datum credcheck_lastlog(PG_FUNCTION_ARGS); #if PG_VERSION_NUM >= 120000 static int entry_cmp(const void *lhs, const void *rhs); #endif 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(Port *port, Oid userid); static void remove_auth_failure(const char *username, Oid userid); static void pg_banned_role_internal(FunctionCallInfo fcinfo); static void set_force_change_password(Oid databaseid, Oid roleid, char *valuestr); /* 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; static char *max_auth_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_min = 0; static int password_valid_warning = 0; static int password_valid_max = 0; static int auth_delay_milliseconds = 0; static bool password_change_first_login = false; static bool force_change_password = false; static bool disallow_change_password = false; static bool superuser_nocheck = false; #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, char *whitelist); static char *to_nlower(const char *str, size_t max) { char *lower_str; int i = 0; lower_str = (char *) palloc(strlen(str) + 1); 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) { char *chars_copy; char *token; char *saveptr = NULL; bool found = false; /* Make a copy of chars since strtok_r modifies the string */ chars_copy = pstrdup(chars); token = strtok_r(chars_copy, ",", &saveptr); while (token != NULL) { if (strstr(str, token) != NULL) { found = true; break; } token = strtok_r(NULL, ",", &saveptr); } pfree(chars_copy); return found; } 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; /* 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 = pstrdup(password); tmp_user = pstrdup(username); tmp_contains = pstrdup(username_contain); tmp_not_contains = pstrdup(username_not_contain); } /* 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 (%d)"), "credcheck.username_min_length", 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: %s"), "credcheck.username_contain", tmp_contains))); 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: %s"), "credcheck.username_not_contain", tmp_not_contains))); 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 (%d)", "credcheck.username_min_upper", 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 (%d)", "credcheck.username_min_lower", 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 (%d)", "credcheck.username_min_digit", 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 (%d)", "credcheck.username_min_special", 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 (%d)"), "username", "credcheck.username_min_repeat", username_min_repeat))); goto clean; } } clean: /* tmp_pass is NULL when no password was given; pfree(NULL) is not allowed */ if (tmp_pass != NULL) pfree(tmp_pass); pfree(tmp_user); pfree(tmp_contains); pfree(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 *whitelist) { char *rawstring; List *elemlist; ListCell *l; int len = 0; Assert(username != NULL); Assert(whitelist != NULL); len = strlen(whitelist); if (len == 0) return false; /* Need a modifiable copy of string */ rawstring = palloc0(sizeof(char) * (len+1)); strcpy(rawstring, whitelist); /* Parse string into list of identifiers */ if (!SplitIdentifierString(rawstring, ',', &elemlist)) { ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg("username list is invalid: %s", 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 = pstrdup(password); tmp_user = pstrdup(username); tmp_contains = pstrdup(password_contain); tmp_not_contains = pstrdup(password_not_contain); } /* 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 (%d)"), "credcheck.password_min_length", 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: %s"), "credcheck.password_contain", tmp_contains))); 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: %s"), "credcheck.password_not_contain", tmp_not_contains))); 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 (%d)", "credcheck.password_min_upper", 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 (%d)", "credcheck.password_min_lower", 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 (%d)", "credcheck.password_min_digit", 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 (%d)", "credcheck.password_min_special", 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 (%d)", "password", "credcheck.password_min_repeat", password_min_repeat))); goto clean; } } clean: pfree(tmp_pass); pfree(tmp_user); pfree(tmp_contains); pfree(tmp_not_contains); } static void username_guc(void) { 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(void) { 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 or set it automatically to " " now() + password_valid_until days in the ALTER ROLE statement" " when the password is changed"), NULL, &password_valid_until, 0, 0, INT_MAX, PGC_SUSET, 0, NULL, NULL, NULL); DefineCustomIntVariable("credcheck.password_valid_min", gettext_noop("number of days used for the VALID UNTIL clause of newly" " created roles (CREATE ROLE). When greater than zero it overrides" " credcheck.password_valid_until at role creation only, so new roles" " can be forced to change their password quickly while existing roles" " that change their password keep the password_valid_until window." " When set to 0 (default) CREATE ROLE keeps using password_valid_until."), NULL, &password_valid_min, 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, 1, NULL, NULL, NULL); DefineCustomIntVariable("credcheck.password_valid_warning", gettext_noop("throw a warning N days before the password expires"), NULL, &password_valid_warning, 0, 0, INT_MAX, PGC_SUSET, 0, NULL, NULL, NULL); DefineCustomBoolVariable("credcheck.password_change_first_login", gettext_noop("force the user to change his password at first login"), NULL, &password_change_first_login, false, PGC_SUSET, 0, NULL, NULL, NULL); DefineCustomBoolVariable("credcheck_internal.force_change_password", gettext_noop("force the user to change his password"), NULL, &force_change_password, false, PGC_SUSET, 0, NULL, NULL, NULL); DefineCustomBoolVariable("credcheck.disallow_change_password", gettext_noop("prevent users to change their password"), NULL, &disallow_change_password, false, PGC_SUSET, 0, NULL, NULL, NULL); DefineCustomBoolVariable("credcheck.superuser_nocheck", gettext_noop("don't do password check if the logged user is a superuser"), NULL, &superuser_nocheck, false, PGC_SUSET, 0, NULL, NULL, NULL); } #if PG_VERSION_NUM >= 150000 /* * WAL emission helpers. Each one inserts a single fixed-size record * describing a mutation of the in-memory password history hash, so that * the change can be replayed on a standby through credcheck_rmgr_redo(). * * Callers must hold pgph->lock in exclusive mode. */ static void credcheck_xlog_pwd_add(const char *rolename, const char *password_hash, TimestampTz password_date) { xl_credcheck_pwd_add xlrec; memset(&xlrec, 0, sizeof(xlrec)); strlcpy(xlrec.rolename, rolename, NAMEDATALEN); strlcpy(xlrec.password_hash, password_hash, PG_SHA256_DIGEST_STRING_LENGTH); xlrec.password_date = password_date; XLogBeginInsert(); XLogRegisterData((char *) &xlrec, sizeof(xlrec)); (void) XLogInsert(RM_CREDCHECK_ID, XLOG_CREDCHECK_PWD_ADD); } static void credcheck_xlog_pwd_remove(const char *rolename, const char *password_hash) { xl_credcheck_pwd_remove xlrec; memset(&xlrec, 0, sizeof(xlrec)); strlcpy(xlrec.rolename, rolename, NAMEDATALEN); strlcpy(xlrec.password_hash, password_hash, PG_SHA256_DIGEST_STRING_LENGTH); XLogBeginInsert(); XLogRegisterData((char *) &xlrec, sizeof(xlrec)); (void) XLogInsert(RM_CREDCHECK_ID, XLOG_CREDCHECK_PWD_REMOVE); } static void credcheck_xlog_pwd_remove_user(const char *rolename) { xl_credcheck_pwd_remove_user xlrec; memset(&xlrec, 0, sizeof(xlrec)); strlcpy(xlrec.rolename, rolename, NAMEDATALEN); XLogBeginInsert(); XLogRegisterData((char *) &xlrec, sizeof(xlrec)); (void) XLogInsert(RM_CREDCHECK_ID, XLOG_CREDCHECK_PWD_REMOVE_USER); } static void credcheck_xlog_pwd_rename(const char *oldname, const char *newname) { xl_credcheck_pwd_rename xlrec; memset(&xlrec, 0, sizeof(xlrec)); strlcpy(xlrec.oldname, oldname, NAMEDATALEN); strlcpy(xlrec.newname, newname, NAMEDATALEN); XLogBeginInsert(); XLogRegisterData((char *) &xlrec, sizeof(xlrec)); (void) XLogInsert(RM_CREDCHECK_ID, XLOG_CREDCHECK_PWD_RENAME); } static void credcheck_xlog_pwd_reset(const char *rolename) { xl_credcheck_pwd_reset xlrec; memset(&xlrec, 0, sizeof(xlrec)); if (rolename) { xlrec.has_user = true; strlcpy(xlrec.rolename, rolename, NAMEDATALEN); } XLogBeginInsert(); XLogRegisterData((char *) &xlrec, sizeof(xlrec)); /* * pg_password_history_reset() is callable from a read-only SELECT, so * the surrounding transaction will not flush WAL on commit. Force a * flush so that walsenders (and pg_current_wal_lsn() observers) see the * record promptly. */ XLogFlush(XLogInsert(RM_CREDCHECK_ID, XLOG_CREDCHECK_PWD_RESET)); } static void credcheck_xlog_pwd_timestamp(const char *rolename, TimestampTz new_timestamp) { xl_credcheck_pwd_timestamp xlrec; memset(&xlrec, 0, sizeof(xlrec)); strlcpy(xlrec.rolename, rolename, NAMEDATALEN); xlrec.new_timestamp = new_timestamp; XLogBeginInsert(); XLogRegisterData((char *) &xlrec, sizeof(xlrec)); /* See credcheck_xlog_pwd_reset() for why we flush. */ XLogFlush(XLogInsert(RM_CREDCHECK_ID, XLOG_CREDCHECK_PWD_TIMESTAMP)); } /* * Replay a credcheck WAL record on a standby (or during crash recovery on * the primary). After applying the change to the in-memory hash we flush * the resulting state to the on-disk password history file, mirroring the * behaviour of the foreground code paths. */ static void credcheck_rmgr_redo(XLogReaderState *record) { uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK; char *rec = XLogRecGetData(record); if (!pgph || !pgph_hash) return; LWLockAcquire(pgph->lock, LW_EXCLUSIVE); switch (info) { case XLOG_CREDCHECK_PWD_ADD: { pgphHashKey key; xl_credcheck_pwd_add *xlrec = (xl_credcheck_pwd_add *) rec; memset(&key, 0, sizeof(key)); strlcpy(key.rolename, xlrec->rolename, NAMEDATALEN); strlcpy(key.password_hash, xlrec->password_hash, PG_SHA256_DIGEST_STRING_LENGTH); (void) pgph_entry_alloc(&key, xlrec->password_date); break; } case XLOG_CREDCHECK_PWD_REMOVE: { pgphHashKey key; xl_credcheck_pwd_remove *xlrec = (xl_credcheck_pwd_remove *) rec; memset(&key, 0, sizeof(key)); strlcpy(key.rolename, xlrec->rolename, NAMEDATALEN); strlcpy(key.password_hash, xlrec->password_hash, PG_SHA256_DIGEST_STRING_LENGTH); pgph_tab_remove(&key); break; } case XLOG_CREDCHECK_PWD_REMOVE_USER: { int32 _i; xl_credcheck_pwd_remove_user *xlrec = (xl_credcheck_pwd_remove_user *) rec; for (_i = 0; _i < (int32) pgph_hdr->capacity; _i++) { pgphEntry *entry; if (!pgph_slots[_i].used) continue; entry = &pgph_slots[_i].entry; if (strcmp(entry->key.rolename, xlrec->rolename) == 0) pgph_tab_remove(&entry->key); } break; } case XLOG_CREDCHECK_PWD_RENAME: { int32 _i; xl_credcheck_pwd_rename *xlrec = (xl_credcheck_pwd_rename *) rec; for (_i = 0; _i < (int32) pgph_hdr->capacity; _i++) { pgphEntry *entry; if (!pgph_slots[_i].used) continue; entry = &pgph_slots[_i].entry; if (strcmp(entry->key.rolename, xlrec->oldname) == 0) { pgphHashKey key; memset(&key, 0, sizeof(key)); strlcpy(key.rolename, xlrec->newname, NAMEDATALEN); strlcpy(key.password_hash, entry->key.password_hash, PG_SHA256_DIGEST_STRING_LENGTH); pgph_tab_rekey(entry, &key); } } break; } case XLOG_CREDCHECK_PWD_RESET: { int32 _i; xl_credcheck_pwd_reset *xlrec = (xl_credcheck_pwd_reset *) rec; for (_i = 0; _i < (int32) pgph_hdr->capacity; _i++) { pgphEntry *entry; if (!pgph_slots[_i].used) continue; entry = &pgph_slots[_i].entry; if (!xlrec->has_user || strcmp(entry->key.rolename, xlrec->rolename) == 0) pgph_tab_remove(&entry->key); } break; } case XLOG_CREDCHECK_PWD_TIMESTAMP: { int32 _i; xl_credcheck_pwd_timestamp *xlrec = (xl_credcheck_pwd_timestamp *) rec; for (_i = 0; _i < (int32) pgph_hdr->capacity; _i++) { pgphEntry *entry; if (!pgph_slots[_i].used) continue; entry = &pgph_slots[_i].entry; if (strcmp(entry->key.rolename, xlrec->rolename) == 0) entry->password_date = xlrec->new_timestamp; } break; } default: LWLockRelease(pgph->lock); elog(PANIC, "credcheck_rmgr_redo: unknown op code %u", info); } flush_password_history(); LWLockRelease(pgph->lock); } static void credcheck_rmgr_desc(StringInfo buf, XLogReaderState *record) { char *rec = XLogRecGetData(record); uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK; switch (info) { case XLOG_CREDCHECK_PWD_ADD: { xl_credcheck_pwd_add *xlrec = (xl_credcheck_pwd_add *) rec; appendStringInfo(buf, "add role \"%s\"", xlrec->rolename); break; } case XLOG_CREDCHECK_PWD_REMOVE: { xl_credcheck_pwd_remove *xlrec = (xl_credcheck_pwd_remove *) rec; appendStringInfo(buf, "remove role \"%s\"", xlrec->rolename); break; } case XLOG_CREDCHECK_PWD_REMOVE_USER: { xl_credcheck_pwd_remove_user *xlrec = (xl_credcheck_pwd_remove_user *) rec; appendStringInfo(buf, "remove user \"%s\"", xlrec->rolename); break; } case XLOG_CREDCHECK_PWD_RENAME: { xl_credcheck_pwd_rename *xlrec = (xl_credcheck_pwd_rename *) rec; appendStringInfo(buf, "rename \"%s\" to \"%s\"", xlrec->oldname, xlrec->newname); break; } case XLOG_CREDCHECK_PWD_RESET: { xl_credcheck_pwd_reset *xlrec = (xl_credcheck_pwd_reset *) rec; if (xlrec->has_user) appendStringInfo(buf, "reset user \"%s\"", xlrec->rolename); else appendStringInfoString(buf, "reset all"); break; } case XLOG_CREDCHECK_PWD_TIMESTAMP: { xl_credcheck_pwd_timestamp *xlrec = (xl_credcheck_pwd_timestamp *) rec; appendStringInfo(buf, "timestamp user \"%s\"", xlrec->rolename); break; } } } static const char * credcheck_rmgr_identify(uint8 info) { switch (info & ~XLR_INFO_MASK) { case XLOG_CREDCHECK_PWD_ADD: return "PWD_ADD"; case XLOG_CREDCHECK_PWD_REMOVE: return "PWD_REMOVE"; case XLOG_CREDCHECK_PWD_REMOVE_USER: return "PWD_REMOVE_USER"; case XLOG_CREDCHECK_PWD_RENAME: return "PWD_RENAME"; case XLOG_CREDCHECK_PWD_RESET: return "PWD_RESET"; case XLOG_CREDCHECK_PWD_TIMESTAMP: return "PWD_TIMESTAMP"; } return NULL; } #endif /* PG_VERSION_NUM >= 150000 */ #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 = 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 = pgph_tab_find(&key); 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) { #if PG_VERSION_NUM >= 150000 credcheck_xlog_pwd_add(username, encrypted_password, dt_now); #endif elog(DEBUG1, "entry added, flush change to disk"); flush_password_history(); } } LWLockRelease(pgph->lock); pfree(encrypted_password); } static void rename_user_in_history(const char *username, const char *newname) { pgphEntry *entry; int32 _i; 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); for (_i = 0; _i < (int32) pgph_hdr->capacity; _i++) { if (!pgph_slots[_i].used) continue; entry = &pgph_slots[_i].entry; /* update the key of matching entries */ if (strcmp(entry->key.rolename, username) == 0) { pgphHashKey key; memset(&key, 0, sizeof(key)); strcpy(key.rolename, newname) ; strcpy(key.password_hash, entry->key.password_hash); pgph_tab_rekey(entry, &key); num_changed++; } } if (num_changed > 0) { #if PG_VERSION_NUM >= 150000 credcheck_xlog_pwd_rename(username, newname); #endif 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 */ #if PG_VERSION_NUM >= 120000 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; } #endif 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; 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 = 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 = pgph_tab_count(); entries = palloc((num_entries > 0 ? num_entries : 1) * sizeof(pgphEntry *)); /* stores entries related to the username to be sorted by date */ { int32 _i; for (_i = 0; _i < (int32) pgph_hdr->capacity; _i++) { if (!pgph_slots[_i].used) continue; entry = &pgph_slots[_i].entry; 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) { #if PG_VERSION_NUM >= 150000 credcheck_xlog_pwd_remove(entries[i]->key.rolename, entries[i]->key.password_hash); #endif elog(DEBUG1, "removing entry %d from the history (%s, %s)", i, entries[i]->key.rolename, entries[i]->key.password_hash); pgph_tab_remove(&entries[i]->key); 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; int32 _i; 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); /* Scan all slots to find the entries to remove */ for (_i = 0; _i < (int32) pgph_hdr->capacity; _i++) { if (!pgph_slots[_i].used) continue; entry = &pgph_slots[_i].entry; if (strcmp(entry->key.rolename, username) == 0) { pgph_tab_remove(&entry->key); num_removed++; } } /* Flush the new entry to disk */ if (num_removed > 0) { #if PG_VERSION_NUM >= 150000 credcheck_xlog_pwd_remove_user(username); #endif 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; int32 _i; 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 = 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); for (_i = 0; _i < (int32) pgph_hdr->capacity; _i++) { if (!pgph_slots[_i].used) continue; entry = &pgph_slots[_i].entry; 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 against 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); pfree(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 /* don't do any password check if the role is whitelisted */ if (is_in_whitelist((char *)username, username_whitelist)) break; 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; } } /* * =========================================================================== * lastlog implementation * =========================================================================== */ /* Accessor: pointer to record slot i inside the mmap region (index-based). */ static inline LastlogEntry * ll_slot(uint32 i) { return (LastlogEntry *) (ll_mmap_base + MAXALIGN(sizeof(LastlogFileHeader)) + (Size) i * ll_record_stride); } /* * Estimate anonymous shared memory needed by lastlog (control struct + live * sessions array + per-session query buffers). The history ring itself does * NOT live here: it is the memory-mapped file. */ static Size lastlog_memsize(void) { Size size; if (!lastlog_enabled) return 0; size = offsetof(LastlogShared, active); size = add_size(size, mul_size(MaxBackends, sizeof(LastlogActive))); /* one query buffer per backend */ size = add_size(size, mul_size(MaxBackends, (Size) lastlog_query_size)); return MAXALIGN(size); } /* * Map (creating/sizing if necessary) the lastlog history file. Runs in every * process that executes the shmem startup hook; the mapping is process-local. */ static void lastlog_mmap_attach(void) { Size filesize; ll_record_stride = MAXALIGN(LASTLOG_BASE_SIZE + (Size) lastlog_query_size); filesize = MAXALIGN(sizeof(LastlogFileHeader)) + (Size) lastlog_max * ll_record_stride; /* round up to a multiple of BLCKSZ to please pgBackRest backups */ filesize = TYPEALIGN(BLCKSZ, filesize); ll_mmap_size = filesize; #ifndef WIN32 { int fd; fd = BasicOpenFile(LASTLOG_DUMP_FILE, O_RDWR | O_CREAT | PG_BINARY); if (fd < 0) ereport(FATAL, (errcode_for_file_access(), errmsg("could not open lastlog file \"%s\": %m", LASTLOG_DUMP_FILE))); if (ftruncate(fd, (off_t) filesize) != 0) { int save_errno = errno; close(fd); errno = save_errno; ereport(FATAL, (errcode_for_file_access(), errmsg("could not size lastlog file \"%s\": %m", LASTLOG_DUMP_FILE))); } ll_mmap_base = mmap(NULL, filesize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); close(fd); /* the mapping outlives the fd */ if (ll_mmap_base == MAP_FAILED) { ll_mmap_base = NULL; ereport(FATAL, (errcode_for_file_access(), errmsg("could not mmap lastlog file \"%s\": %m", LASTLOG_DUMP_FILE))); } } #else /* * Windows path: must be implemented with CreateFileMapping / * MapViewOfFile on a real file handle (not the paging file). Left as a * follow-up; mmap-based persistence is currently POSIX-only. */ ereport(FATAL, (errmsg("credcheck.lastlog is not yet supported on Windows"))); #endif ll_hdr = (LastlogFileHeader *) ll_mmap_base; } /* Initialise (or reset) the mmap header. Caller holds the lastlog lock. */ static void lastlog_init_header(void) { memset(ll_mmap_base, 0, ll_mmap_size); ll_hdr->magic = LASTLOG_FILE_HEADER; ll_hdr->version = LASTLOG_VERSION; ll_hdr->record_stride = ll_record_stride; ll_hdr->capacity = lastlog_max; ll_hdr->head = 0; ll_hdr->nused = 0; ll_hdr->seqno = 0; ll_hdr->clean_shutdown = false; } /* * Append a marker record (boot/shutdown/crash). Caller holds the lock * exclusively. Markers are tiny and rare, so we msync them immediately. */ static void lastlog_append_marker(LastlogType type) { LastlogEntry *e; if (!ll_hdr) return; e = ll_slot(ll_hdr->head); memset(e, 0, ll_record_stride); e->type = type; e->login_time = GetCurrentTimestamp(); e->pid = (type == LL_BOOT) ? PostmasterPid : 0; e->active = false; e->seqno = ++ll_hdr->seqno; pg_write_barrier(); e->complete = true; ll_hdr->head = (ll_hdr->head + 1) % ll_hdr->capacity; if (ll_hdr->nused < ll_hdr->capacity) ll_hdr->nused++; lastlog_msync(MS_ASYNC); } /* * Record a successful login in the caller's live-session slot. Called from * the ClientAuthentication hook (status == STATUS_OK). Also arms the * proc_exit callback that will move the session into the history ring. */ static void lastlog_record_login(Port *port) { LastlogActive *a; if (!lastlog_enabled || lastlog == NULL || MyProc == NULL) return; #if PG_VERSION_NUM < 170000 ll_my_slot = MyProc->pgprocno; #else ll_my_slot = MyProc->vxid.procNumber; #endif if (ll_my_slot < 0 || ll_my_slot >= lastlog->nactive_slots) { ll_my_slot = -1; return; } a = &lastlog->active[ll_my_slot]; LWLockAcquire(lastlog->lock, LW_EXCLUSIVE); a->in_use = true; a->pid = MyProcPid; strlcpy(a->username, port->user_name ? port->user_name : "", NAMEDATALEN); if (port->remote_host && port->remote_host[0] != '\0') strlcpy(a->remote_host, port->remote_host, LASTLOG_HOST_LEN); else strlcpy(a->remote_host, "[local]", LASTLOG_HOST_LEN); a->remote_port = (port->remote_port && port->remote_port[0] != '\0') ? atoi(port->remote_port) : 0; a->login_time = GetCurrentTimestamp(); if (a->last_query) a->last_query[0] = '\0'; LWLockRelease(lastlog->lock); /* Ensure the session is flushed to history on backend exit. */ before_shmem_exit(lastlog_finalize_session, (Datum) 0); } /* * proc_exit callback: turn this backend's live session into a finalized * history record (with logout time / duration), then free the live slot. */ static void lastlog_finalize_session(int code, Datum arg) { LastlogActive *a; LastlogEntry *e; if (!lastlog_enabled || lastlog == NULL || ll_hdr == NULL || ll_my_slot < 0) return; a = &lastlog->active[ll_my_slot]; LWLockAcquire(lastlog->lock, LW_EXCLUSIVE); if (!a->in_use) { LWLockRelease(lastlog->lock); ll_my_slot = -1; return; } e = ll_slot(ll_hdr->head); memset(e, 0, ll_record_stride); e->type = LL_USER; strlcpy(e->username, a->username, NAMEDATALEN); e->pid = a->pid; strlcpy(e->remote_host, a->remote_host, LASTLOG_HOST_LEN); e->remote_port = a->remote_port; e->login_time = a->login_time; e->logout_time = GetCurrentTimestamp(); e->active = false; if (lastlog_track_query && a->last_query) strlcpy(e->last_query, a->last_query, lastlog_query_size); e->seqno = ++ll_hdr->seqno; pg_write_barrier(); e->complete = true; ll_hdr->head = (ll_hdr->head + 1) % ll_hdr->capacity; if (ll_hdr->nused < ll_hdr->capacity) ll_hdr->nused++; a->in_use = false; LWLockRelease(lastlog->lock); ll_my_slot = -1; } /* Copy the current query text into this backend's live slot (ExecutorStart). */ static void lastlog_track_last_query(const char *query_string) { LastlogActive *a; if (!lastlog_enabled || !lastlog_track_query || lastlog == NULL || ll_my_slot < 0 || query_string == NULL) return; a = &lastlog->active[ll_my_slot]; if (a->last_query == NULL) return; LWLockAcquire(lastlog->lock, LW_EXCLUSIVE); if (a->in_use) strlcpy(a->last_query, query_string, lastlog_query_size); LWLockRelease(lastlog->lock); } /* Flush the mmap region to disk. */ static void lastlog_msync(int flags) { #ifndef WIN32 if (ll_mmap_base != NULL && ll_mmap_size > 0) (void) msync(ll_mmap_base, ll_mmap_size, flags); #endif } /* * shmem startup hook for lastlog: allocate the anonymous control area, attach * the mmap file, and (only in the first process) initialise the header, * detect a previous crash and write the boot marker. */ static void lastlog_shmem_startup(void) { bool found; Size qoff; char *query_area; int i; if (!lastlog_enabled) return; lastlog = NULL; ll_mmap_base = NULL; ll_hdr = NULL; LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE); lastlog = ShmemInitStruct("credcheck lastlog", lastlog_memsize(), &found); if (!found) { lastlog->lock = &(GetNamedLWLockTranche(LASTLOG_TRANCHE_NAME))->lock; lastlog->nactive_slots = MaxBackends; MemSet(lastlog->active, 0, mul_size(MaxBackends, sizeof(LastlogActive))); } /* wire each backend's query buffer into the area following active[] */ qoff = offsetof(LastlogShared, active) + mul_size(MaxBackends, sizeof(LastlogActive)); query_area = ((char *) lastlog) + qoff; for (i = 0; i < MaxBackends; i++) lastlog->active[i].last_query = query_area + (Size) i * lastlog_query_size; /* attach the memory-mapped history file (process-local) */ lastlog_mmap_attach(); if (!found) { bool prev_unclean; LWLockAcquire(lastlog->lock, LW_EXCLUSIVE); /* fresh / incompatible file -> (re)initialise */ if (ll_hdr->magic != LASTLOG_FILE_HEADER || ll_hdr->version != LASTLOG_VERSION || ll_hdr->record_stride != ll_record_stride || ll_hdr->capacity != (uint32) lastlog_max) { lastlog_init_header(); prev_unclean = false; } else { /* previous run crashed if it never set clean_shutdown */ prev_unclean = !ll_hdr->clean_shutdown; } if (prev_unclean) lastlog_append_marker(LL_CRASH); lastlog_append_marker(LL_BOOT); ll_hdr->clean_shutdown = false; /* cleared until clean exit */ lastlog_msync(MS_ASYNC); LWLockRelease(lastlog->lock); } LWLockRelease(AddinShmemInitLock); } /* * Background worker: periodic msync and, on SIGTERM, the clean-shutdown * marker + a final synchronous flush. This is the reliable place to catch * server shutdown (a plain backend's before_shmem_exit is not). */ void lastlog_bgworker_main(Datum main_arg) { pqsignal(SIGTERM, die); #if PG_VERSION_NUM >= 190000 pqsignal(SIGHUP, PG_SIG_IGN); #else pqsignal(SIGHUP, SIG_IGN); #endif BackgroundWorkerUnblockSignals(); /* write the clean-shutdown marker when we are asked to stop */ before_shmem_exit(lastlog_bgworker_shutdown, (Datum) 0); /* attach to the already-created shared structures */ for (;;) { int rc; long timeout_ms; CHECK_FOR_INTERRUPTS(); if (lastlog_flush_interval > 0) { lastlog_msync(MS_ASYNC); timeout_ms = (long) lastlog_flush_interval * 1000L; } else timeout_ms = 60000L; /* idle wakeup; shutdown flush only */ rc = WaitLatch(MyLatch, WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH, timeout_ms, PG_WAIT_EXTENSION); ResetLatch(MyLatch); if (rc & WL_LATCH_SET) CHECK_FOR_INTERRUPTS(); /* die() will longjmp out on SIGTERM */ } } /* * proc_exit-time handler for the bgworker: write the clean shutdown marker. * Registered via before_shmem_exit so it runs when die() unwinds. */ static void lastlog_bgworker_shutdown(int code, Datum arg) { if (!lastlog_enabled || ll_hdr == NULL) return; LWLockAcquire(lastlog->lock, LW_EXCLUSIVE); lastlog_append_marker(LL_SHUTDOWN); ll_hdr->clean_shutdown = true; LWLockRelease(lastlog->lock); lastlog_msync(MS_SYNC); } #define PG_LASTLOG_COLS 10 /* * SQL-callable SRF returning the merged view of live sessions (from active[]) * and the history ring (from the mmap file), newest first. */ PG_FUNCTION_INFO_V1(credcheck_lastlog); Datum credcheck_lastlog(PG_FUNCTION_ARGS) { ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; TupleDesc tupdesc; Tuplestorestate *tupstore; MemoryContext per_query_ctx; MemoryContext oldcontext; int i; if (!lastlog_enabled || lastlog == NULL || ll_hdr == NULL) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("credcheck.lastlog is disabled"))); if (rsinfo == NULL || !(rsinfo->allowedModes & SFRM_Materialize)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("set-valued function called in context that cannot accept a set"))); if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) elog(ERROR, "return type must be a row type"); per_query_ctx = rsinfo->econtext->ecxt_per_query_memory; oldcontext = MemoryContextSwitchTo(per_query_ctx); tupstore = tuplestore_begin_heap(true, false, work_mem); rsinfo->returnMode = SFRM_Materialize; rsinfo->setResult = tupstore; rsinfo->setDesc = tupdesc; MemoryContextSwitchTo(oldcontext); LWLockAcquire(lastlog->lock, LW_SHARED); /* live sessions first (still logged in) */ for (i = 0; i < lastlog->nactive_slots; i++) { Datum values[PG_LASTLOG_COLS]; bool nulls[PG_LASTLOG_COLS]; LastlogActive *a = &lastlog->active[i]; if (!a->in_use) continue; MemSet(values, 0, sizeof(values)); MemSet(nulls, 0, sizeof(nulls)); values[0] = CStringGetTextDatum("user"); values[1] = CStringGetTextDatum(a->username); values[2] = Int32GetDatum(a->pid); values[3] = CStringGetTextDatum(a->remote_host); values[4] = Int32GetDatum(a->remote_port); values[5] = TimestampTzGetDatum(a->login_time); nulls[6] = true; /* logout_time */ values[7] = DirectFunctionCall2(timestamptz_age, TimestampTzGetDatum(GetCurrentTimestamp()), TimestampTzGetDatum(a->login_time)); values[8] = CStringGetTextDatum("still connected"); if (lastlog_track_query && a->last_query && a->last_query[0] != '\0') values[9] = CStringGetTextDatum(a->last_query); else nulls[9] = true; tuplestore_putvalues(tupstore, tupdesc, values, nulls); } /* then the history ring, newest first */ if (ll_hdr->nused > 0) { uint32 n = ll_hdr->nused; uint32 idx = (ll_hdr->head + ll_hdr->capacity - 1) % ll_hdr->capacity; uint32 k; for (k = 0; k < n; k++) { LastlogEntry *e = ll_slot(idx); Datum values[PG_LASTLOG_COLS]; bool nulls[PG_LASTLOG_COLS]; const char *tname; if (e->complete && e->seqno != 0) { MemSet(values, 0, sizeof(values)); MemSet(nulls, 0, sizeof(nulls)); switch (e->type) { case LL_BOOT: tname = "boot"; break; case LL_SHUTDOWN: tname = "shutdown"; break; case LL_CRASH: tname = "crash"; break; default: tname = "user"; break; } values[0] = CStringGetTextDatum(tname); if (e->type == LL_USER) values[1] = CStringGetTextDatum(e->username); else nulls[1] = true; if (e->pid != 0) values[2] = Int32GetDatum(e->pid); else nulls[2] = true; if (e->type == LL_USER) { values[3] = CStringGetTextDatum(e->remote_host); values[4] = Int32GetDatum(e->remote_port); } else { nulls[3] = true; nulls[4] = true; } values[5] = TimestampTzGetDatum(e->login_time); if (e->logout_time != 0) { values[6] = TimestampTzGetDatum(e->logout_time); values[7] = DirectFunctionCall2(timestamptz_age, TimestampTzGetDatum(e->logout_time), TimestampTzGetDatum(e->login_time)); } else { nulls[6] = true; nulls[7] = true; } values[8] = CStringGetTextDatum( e->type == LL_USER ? "disconnected" : tname); if (e->type == LL_USER && lastlog_track_query && e->last_query[0] != '\0') values[9] = CStringGetTextDatum(e->last_query); else nulls[9] = true; tuplestore_putvalues(tupstore, tupdesc, values, nulls); } idx = (idx + ll_hdr->capacity - 1) % ll_hdr->capacity; } } LWLockRelease(lastlog->lock); return (Datum) 0; } 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.lastlog", gettext_noop("enable the last-login history (utmp-like)"), NULL, &lastlog_enabled, false, PGC_POSTMASTER, 0, NULL, NULL, NULL); DefineCustomIntVariable("credcheck.lastlog_max", gettext_noop("number of records kept in the lastlog history ring"), gettext_noop("Sizes the memory-mapped history file. Should be >= max_connections."), &lastlog_max, 1024, 16, (INT_MAX / 4096), PGC_POSTMASTER, 0, NULL, NULL, NULL); DefineCustomIntVariable("credcheck.lastlog_query_size", gettext_noop("maximum length stored for the last SQL query in lastlog"), NULL, &lastlog_query_size, 1024, 0, (1024 * 1024), PGC_POSTMASTER, 0, NULL, NULL, NULL); DefineCustomBoolVariable("credcheck.lastlog_track_query", gettext_noop("store the last executed SQL query in lastlog"), NULL, &lastlog_track_query, false, PGC_SIGHUP, 0, NULL, NULL, NULL); DefineCustomIntVariable("credcheck.lastlog_flush_interval", gettext_noop("seconds between lastlog msync flushes (0 = at shutdown only)"), NULL, &lastlog_flush_interval, 0, 0, 86400, PGC_SIGHUP, GUC_UNIT_S, NULL, NULL, NULL); /* Register the lastlog flush/shutdown background worker. */ if (lastlog_enabled) { BackgroundWorker worker; MemSet(&worker, 0, sizeof(BackgroundWorker)); worker.bgw_flags = BGWORKER_SHMEM_ACCESS; worker.bgw_start_time = BgWorkerStart_PostmasterStart; worker.bgw_restart_time = 5; snprintf(worker.bgw_library_name, BGW_MAXLEN, "credcheck"); snprintf(worker.bgw_function_name, BGW_MAXLEN, "lastlog_bgworker_main"); snprintf(worker.bgw_name, BGW_MAXLEN, "credcheck lastlog flusher"); snprintf(worker.bgw_type, BGW_MAXLEN, "credcheck lastlog"); worker.bgw_main_arg = (Datum) 0; worker.bgw_notify_pid = 0; RegisterBackgroundWorker(&worker); } #if PG_VERSION_NUM >= 150000 /* Register custom WAL rmgr that replicates the password history. */ RegisterCustomRmgr(RM_CREDCHECK_ID, &credcheck_rmgr); #endif } DefineCustomBoolVariable("credcheck.no_password_logging", gettext_noop("mask the password literal (PASSWORD '...') in statements" "written to the server log"), 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); DefineCustomStringVariable( "credcheck.whitelist_auth_failure", gettext_noop("comma separated list of username to exclude from max authentication failure check"), NULL, &max_auth_whitelist, "", PGC_SUSET, 0, check_whitelist, NULL, NULL); #if PG_VERSION_NUM < 150000 EmitWarningsOnPlaceholders("credcheck"); EmitWarningsOnPlaceholders("credcheck_internal"); /* * 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); if (lastlog_enabled) { RequestAddinShmemSpace(lastlog_memsize()); RequestNamedLWLockTranche(LASTLOG_TRANCHE_NAME, 1); } #else MarkGUCPrefixReserved("credcheck"); MarkGUCPrefixReserved("credcheck_internal"); #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; prev_ExecutorStart = ExecutorStart_hook; ExecutorStart_hook = cc_ExecutorStart; } 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; } /* * When a role must change its password first, we only want to allow the * password change itself (ALTER ROLE, handled by the caller) plus the handful * of harmless session/transaction-control statements that clients and * connection poolers unavoidably issue while establishing a session: * * - SET / RESET (T_VariableSetStmt) e.g. pgbouncer's * varcache_apply, and the * "SET extra_float_digits", * "SET application_name", * "SET client_encoding" * that libpq/JDBC/psql send * at connection time * - SHOW (T_VariableShowStmt) incl. the * "show password_encryption" * issued by \password * - BEGIN/COMMIT/ROLLBACK/... (T_TransactionStmt) * - DISCARD [ALL] (T_DiscardStmt) pgbouncer server_reset_query * * None of these can read or write data or escalate privileges, and actual * data access remains blocked in the ExecutorStart hook. Rejecting them here * broke session setup through pgbouncer: the error is raised during the * pooler's internal varcache_apply / setup phase, so pgbouncer closes the * server connection and only logs "varcache_apply failed: ... you must change * your password first." -- the client never receives it and just sees a failed * connection. Letting these through lets the session establish, after which the * user's first real query is blocked with an error that does reach them. */ static bool is_force_change_allowed_stmt(Node *parsetree) { switch (nodeTag(parsetree)) { case T_VariableSetStmt: { VariableSetStmt *setstmt = (VariableSetStmt *) parsetree; /* * Defense in depth: never let a forced session turn the flag off * with SET. Only a superuser could (the GUC is SUSET), but keep * the invariant explicit regardless. */ if (setstmt->name != NULL && pg_strcasecmp(setstmt->name, "credcheck_internal.force_change_password") == 0) return false; return true; } case T_VariableShowStmt: case T_TransactionStmt: case T_DiscardStmt: return true; default: return false; } } static void cc_ProcessUtility(PEL_PROCESSUTILITY_PROTO) { char load_roleid[NAMEDATALEN] = {0}; Oid roleid = InvalidOid; bool use_superuser_priv = false; Oid save_userid; int save_sec_context; elog(DEBUG1, "Start cc_ProcessUtility()"); /* If real user connection and top level (not SPI re-enter, etc) */ if (MyProcPort != NULL && NOT_IN_PARALLEL_WORKER && (context == PROCESS_UTILITY_TOPLEVEL || context == PROCESS_UTILITY_QUERY) ) { Node *parsetree = pstmt->utilityStmt; /* * no check at all if the user is superuser * and superuser_nocheck is enabled */ if (superuser() && superuser_nocheck) { if (prev_ProcessUtility) prev_ProcessUtility(PEL_PROCESSUTILITY_ARGS); else standard_ProcessUtility(PEL_PROCESSUTILITY_ARGS); return; } if (!is_in_whitelist(MyProcPort->user_name, username_whitelist)) { /* * When disallow_change_password is enable we return * an error to any usertrying to change his password. */ if (nodeTag(parsetree) == T_AlterRoleStmt && disallow_change_password) ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg(gettext_noop("you are not allowed to change password.")))); /* * When first login we don't allow anything else than the password * change (ALTER ROLE) and the harmless session/transaction-control * statements that drivers and poolers must issue to set up a * session (see is_force_change_allowed_stmt). The \password command * issues "show password_encryption" before prompting, which is now * covered by the T_VariableShowStmt case. \password itself is * rejected anyway because it sends an encrypted password. */ if (nodeTag(parsetree) != T_AlterRoleStmt && force_change_password && !is_force_change_allowed_stmt(parsetree)) ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg(gettext_noop("you must change your password first.")))); } 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, username_whitelist) || is_in_whitelist(stmt->subname, username_whitelist)) 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; bool has_other_option = false; DefElem *dvalidUntil = NULL; DefElem *dpassword = NULL; /* * Protect against attacks via ALTER ROLE current_role. * * The current user may later be switched to * BOOTSTRAP_SUPERUSERID, so we must ensure we do not * accidentally change the bootstrap superuser's password. * It is achived by making sure that roletype is always set to * ROLESPEC_CSTRING. */ if (stmt->role->roletype != ROLESPEC_CSTRING) { stmt->role->rolename = get_rolespec_name(stmt->role); stmt->role->roletype = ROLESPEC_CSTRING; } /* verify if the user in whitelisted or not */ if (is_in_whitelist(stmt->role->rolename, username_whitelist)) 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; } else { /* any other role option (SUPERUSER, CREATEROLE, ...) */ has_other_option = true; } } if (dpassword == NULL && force_change_password) ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg(gettext_noop("you must change your password first.")))); #if PG_VERSION_NUM >= 120000 /* check the password set */ if (dpassword && dpassword->arg) { password = strVal(dpassword->arg); save_password = check_password_reuse(stmt->role->rolename, password); } #endif /* * when the user change his password, automatically set the valid until * date to now() + password_valid_until days if password_valid_until is set. */ if (!dvalidUntil && password_valid_until > 0) { Timestamp dt_now = GetCurrentTimestamp(); struct pg_tm tt, *tm = &tt; fsec_t fsec; int julian; char *validuntil; int tz; if (timestamp2tm(dt_now, &tz, tm, &fsec, NULL, session_timezone) != 0) ereport(ERROR, (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), errmsg("timestamp out of range"))); /* * Add credcheck.password_valid_until days by converting to and from Julian. */ julian = date2j(tm->tm_year, tm->tm_mon, tm->tm_mday); if (pg_add_s32_overflow(julian, password_valid_until+1, &julian) || julian < 0) ereport(ERROR, (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), errmsg("timestamp out of range"))); j2date(julian, &tm->tm_year, &tm->tm_mon, &tm->tm_mday); validuntil = psprintf("%d-%d-%d", tm->tm_year, tm->tm_mon, tm->tm_mday); dvalidUntil = makeDefElem("validUntil", (Node *) makeString(validuntil), -1); ((AlterRoleStmt *)parsetree)->options = lappend(((AlterRoleStmt *)parsetree)->options, dvalidUntil); /* * As we modify the VALID UNTIL clause it will generate an error message * when the user changes his password: * Only roles with the CREATEROLE attribute and the ADMIN option * on role "..." may alter this role. * force use of the superuser privilege to modify the password user. * Restrict this elevation to a pure password change on the caller's * own role, otherwise privileged options (SUPERUSER, CREATEROLE, ...) * would run as the bootstrap superuser. */ if (!superuser() && dpassword != NULL && !has_other_option && get_rolespec_oid(stmt->role, false) == GetUserId()) use_superuser_priv = true; } /* when a valid until date is set check that it is > to password_valid_until */ 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))); } /* check that the valid until date is not under the limit of days */ if (dvalidUntil && dvalidUntil->arg) { int valid_max = check_valid_until(strVal(dvalidUntil->arg)); if (password_valid_until > 0 && valid_max < password_valid_until) ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg(gettext_noop("the VALID UNTIL option must have a date beyond %d days"), password_valid_until))); if (password_valid_max > 0 && 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))); } #if PG_VERSION_NUM >= 120000 /* The password can be saved into the history */ if (save_password) save_password_in_history(stmt->role->rolename, password); #endif if (force_change_password) { /* RESET variable, valuestr = NULL*/ set_force_change_password(InvalidOid, get_role_oid(stmt->role->rolename, true), NULL); force_change_password = false; } break; } case T_CreateRoleStmt: { CreateRoleStmt *stmt = (CreateRoleStmt *)parsetree; ListCell *option; int valid_until = 0; int valid_max = 0; bool has_valid_until = false; /* * At role creation credcheck.password_valid_min, when set, * takes precedence over credcheck.password_valid_until. This * lets a DBA force a short VALID UNTIL window on brand new * roles (so users must change their password quickly) while * keeping a longer password_valid_until window for existing * roles that change their password (see issue #77). */ int create_valid = (password_valid_min > 0) ? password_valid_min : password_valid_until; bool save_password = false; char *password; DefElem *dpassword = NULL; DefElem *dvalidUntil = NULL; if (is_in_whitelist(stmt->role, username_whitelist)) 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) { password = strVal(dpassword->arg); save_password = check_password_reuse(stmt->role, password); } #endif /* * At user creation automatically set the valid until date to * now() + create_valid days if a create-time window is set * (credcheck.password_valid_min, falling back to * credcheck.password_valid_until). */ if (!dvalidUntil && create_valid > 0) { Timestamp dt_now = GetCurrentTimestamp(); struct pg_tm tt, *tm = &tt; fsec_t fsec; int julian; char *validuntil; int tz; if (timestamp2tm(dt_now, &tz, tm, &fsec, NULL, session_timezone) != 0) ereport(ERROR, (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), errmsg("timestamp out of range"))); /* * Add credcheck.password_valid_until days by converting to and from Julian. */ julian = date2j(tm->tm_year, tm->tm_mon, tm->tm_mday); if (pg_add_s32_overflow(julian, create_valid+1, &julian) || julian < 0) ereport(ERROR, (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), errmsg("timestamp out of range"))); j2date(julian, &tm->tm_year, &tm->tm_mon, &tm->tm_mday); validuntil = psprintf("%d-%d-%d", tm->tm_year, tm->tm_mon, tm->tm_mday); dvalidUntil = makeDefElem("validUntil", (Node *) makeString(validuntil), 1); ((CreateRoleStmt *)parsetree)->options = lappend(((CreateRoleStmt *)parsetree)->options, dvalidUntil); } if (dvalidUntil && dvalidUntil->arg && create_valid > 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 && (create_valid > 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 (create_valid > 0 && valid_until < create_valid) ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg(gettext_noop("require a VALID UNTIL option with a date older than %d days"), create_valid))); /* check that we do not exceed the number of days for password validity */ 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 NOT beyond %d days"), password_valid_max))); #if PG_VERSION_NUM >= 120000 /* The password can be saved into the history */ if (save_password) save_password_in_history(stmt->role, password); #endif strcpy(load_roleid, stmt->role); 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; } } /* The password change must be done using SU privilege */ if (use_superuser_priv) { /* Get current user's Oid and security context */ GetUserIdAndSecContext(&save_userid, &save_sec_context); /* Become superuser */ SetUserIdAndSecContext(BOOTSTRAP_SUPERUSERID, save_sec_context | SECURITY_LOCAL_USERID_CHANGE | SECURITY_RESTRICTED_OPERATION); } if (prev_ProcessUtility) prev_ProcessUtility(PEL_PROCESSUTILITY_ARGS); else standard_ProcessUtility(PEL_PROCESSUTILITY_ARGS); /* Restore user's privileges */ if (use_superuser_priv) SetUserIdAndSecContext(save_userid, save_sec_context); if (MyProcPort != NULL && context == PROCESS_UTILITY_TOPLEVEL && NOT_IN_PARALLEL_WORKER) { if (load_roleid[0] != '\0') roleid = get_role_oid(load_roleid, true); /* set force change password option if password_change_first_login is set */ if (password_change_first_login && roleid != InvalidOid) set_force_change_password(InvalidOid, roleid, "true"); } elog(DEBUG1, "End cc_ProcessUtility()"); } #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 ****/ /* * =========================================================================== * Memory-mapped password history store (option C). * * Layout of the mapped file: * [ PgphMmapHeader ] * [ int32 bucket[nbuckets] ] (slot index of chain head, -1 = empty) * [ PgphSlot slot[capacity] ] * Everything is referenced by integer index, never by pointer, so the mapping * is valid regardless of the base address in each process. * =========================================================================== */ static inline Size pgph_bucket_off(void) { return MAXALIGN(sizeof(PgphMmapHeader)); } static inline Size pgph_slots_off(uint32 nbuckets) { return MAXALIGN(pgph_bucket_off() + (Size) nbuckets * sizeof(int32)); } /* Map (creating/sizing if needed) the password history file. Process-local. */ static void pgph_mmap_attach(void) { uint32 nbuckets = (uint32) pgph_max; Size filesize; filesize = pgph_slots_off(nbuckets) + (Size) pgph_max * sizeof(PgphSlot); filesize = TYPEALIGN(BLCKSZ, filesize); /* pgBackRest-friendly */ pgph_mmap_size = filesize; #ifndef WIN32 { int fd; fd = BasicOpenFile(PGPH_DUMP_FILE, O_RDWR | O_CREAT | PG_BINARY); if (fd < 0) ereport(FATAL, (errcode_for_file_access(), errmsg("could not open password history file \"%s\": %m", PGPH_DUMP_FILE))); if (ftruncate(fd, (off_t) filesize) != 0) { int save_errno = errno; close(fd); errno = save_errno; ereport(FATAL, (errcode_for_file_access(), errmsg("could not size password history file \"%s\": %m", PGPH_DUMP_FILE))); } pgph_base = mmap(NULL, filesize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); close(fd); if (pgph_base == MAP_FAILED) { pgph_base = NULL; ereport(FATAL, (errcode_for_file_access(), errmsg("could not mmap password history file \"%s\": %m", PGPH_DUMP_FILE))); } } #else ereport(FATAL, (errmsg("credcheck password history mmap store is not yet supported on Windows"))); #endif pgph_hdr = (PgphMmapHeader *) pgph_base; pgph_buckets = (int32 *) (pgph_base + pgph_bucket_off()); pgph_slots = (PgphSlot *) (pgph_base + pgph_slots_off(nbuckets)); /* liveness sentinel so the existing "if (!pgph_hash)" guards keep working */ pgph_hash = (HTAB *) pgph_base; } /* Initialise an empty table (first process / holds the lock). */ static void pgph_tab_init(void) { uint32 nbuckets = (uint32) pgph_max; int32 i; memset(pgph_base, 0, pgph_mmap_size); pgph_hdr->magic = PGPH_MMAP_MAGIC; pgph_hdr->version = PGPH_MMAP_VERSION; pgph_hdr->capacity = (uint32) pgph_max; pgph_hdr->nbuckets = nbuckets; pgph_hdr->nused = 0; for (i = 0; i < (int32) nbuckets; i++) pgph_buckets[i] = -1; for (i = 0; i < pgph_max; i++) { pgph_slots[i].used = 0; pgph_slots[i].next = (i + 1 < pgph_max) ? (i + 1) : -1; } pgph_hdr->freehead = (pgph_max > 0) ? 0 : -1; } static inline uint32 pgph_bucket_of(pgphHashKey *key) { uint32 h = hash_bytes((const unsigned char *) key, sizeof(pgphHashKey)); return h % pgph_hdr->nbuckets; } static pgphEntry * pgph_tab_find(pgphHashKey *key) { int32 idx; if (!pgph_hdr) return NULL; idx = pgph_buckets[pgph_bucket_of(key)]; while (idx != -1) { PgphSlot *s = &pgph_slots[idx]; if (s->used && memcmp(&s->entry.key, key, sizeof(pgphHashKey)) == 0) return &s->entry; idx = s->next; } return NULL; } static pgphEntry * pgph_tab_enter(pgphHashKey *key, bool *found) { uint32 b; int32 idx; pgphEntry *existing = pgph_tab_find(key); if (existing) { if (found) *found = true; return existing; } if (found) *found = false; if (pgph_hdr->freehead == -1) return NULL; /* table full */ idx = pgph_hdr->freehead; pgph_hdr->freehead = pgph_slots[idx].next; b = pgph_bucket_of(key); pgph_slots[idx].used = 1; pgph_slots[idx].entry.key = *key; pgph_slots[idx].entry.password_date = 0; pgph_slots[idx].next = pgph_buckets[b]; pgph_buckets[b] = idx; pgph_hdr->nused++; return &pgph_slots[idx].entry; } static bool pgph_unlink_from_bucket(uint32 b, int32 target) { int32 idx = pgph_buckets[b]; int32 prev = -1; while (idx != -1) { if (idx == target) { if (prev == -1) pgph_buckets[b] = pgph_slots[idx].next; else pgph_slots[prev].next = pgph_slots[idx].next; return true; } prev = idx; idx = pgph_slots[idx].next; } return false; } static void pgph_tab_remove(pgphHashKey *key) { uint32 b = pgph_bucket_of(key); int32 idx = pgph_buckets[b]; while (idx != -1) { PgphSlot *s = &pgph_slots[idx]; if (s->used && memcmp(&s->entry.key, key, sizeof(pgphHashKey)) == 0) { (void) pgph_unlink_from_bucket(b, idx); s->used = 0; s->next = pgph_hdr->freehead; pgph_hdr->freehead = idx; pgph_hdr->nused--; return; } idx = s->next; } } /* Change the key of an existing entry (ALTER ROLE ... RENAME). */ static void pgph_tab_rekey(pgphEntry *entry, pgphHashKey *newkey) { int32 idx = (int32) ((PgphSlot *) ((char *) entry - offsetof(PgphSlot, entry)) - pgph_slots); uint32 oldb = pgph_bucket_of(&pgph_slots[idx].entry.key); uint32 newb; (void) pgph_unlink_from_bucket(oldb, idx); pgph_slots[idx].entry.key = *newkey; newb = pgph_bucket_of(newkey); pgph_slots[idx].next = pgph_buckets[newb]; pgph_buckets[newb] = idx; } static int32 pgph_tab_count(void) { return pgph_hdr ? pgph_hdr->nused : 0; } static void pgph_msync(int flags) { #ifndef WIN32 if (pgph_base != NULL && pgph_mmap_size > 0) (void) msync(pgph_base, pgph_mmap_size, flags); #endif } /* * Backward compatibility: read a legacy 5.x dump file (magic PGPH_FILE_HEADER) * into a palloc'd array and rename it out of the way, so the caller can build * the fresh mmap store and re-insert the entries. Returns NULL (and *n_out = 0) * when there is no legacy file. MUST run before the mapping is initialised, * because pgph_tab_init() memset()s through the mmap and would otherwise wipe * the legacy bytes still sitting in the same file. * * Runs identically on a standby (the dump arrives via the base backup), so the * 5.x -> 6.0 upgrade restart preserves the history on both primary and standby. */ static pgphEntry * pgph_slurp_legacy(int32 *n_out) { FILE *f; uint32 header = 0; uint32 ver = 0; int32 num = 0; int32 i; pgphEntry *buf; *n_out = 0; f = AllocateFile(PGPH_DUMP_FILE, PG_BINARY_R); if (f == NULL) return NULL; if (fread(&header, sizeof(uint32), 1, f) != 1 || fread(&ver, sizeof(uint32), 1, f) != 1 || fread(&num, sizeof(int32), 1, f) != 1 || header != PGPH_FILE_HEADER || ver != PGPH_VERSION || num <= 0) { FreeFile(f); return NULL; /* not a legacy dump (e.g. our mmap store) */ } elog(LOG, "credcheck: migrating %d legacy password history entries to the mmap store", num); buf = (pgphEntry *) malloc((Size) num * sizeof(pgphEntry)); if (buf == NULL) { FreeFile(f); ereport(LOG, (errmsg("credcheck: out of memory migrating password history"))); return NULL; } for (i = 0; i < num; i++) { if (fread(&buf[i], sizeof(pgphEntry), 1, f) != 1) break; } FreeFile(f); *n_out = i; /* archive the legacy file so the mmap store can take the canonical name */ (void) durable_rename(PGPH_DUMP_FILE, PGPH_DUMP_FILE ".legacy-migrated", LOG); return buf; } /* * Estimate anonymous shared memory: only the control struct lives in shared * memory now; the hash table itself is the memory-mapped file. */ static Size pgph_memsize(void) { return MAXALIGN(sizeof(pgphSharedState)); } /* * 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); if (lastlog_enabled) { RequestAddinShmemSpace(lastlog_memsize()); RequestNamedLWLockTranche(LASTLOG_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(); lastlog_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; pgphEntry *migrated = NULL; int32 migrated_n = 0; /* reset in case this is a restart within the postmaster */ pgph = NULL; pgph_hash = NULL; pgph_base = NULL; pgph_hdr = NULL; /* Create or attach to the anonymous control struct (LWLock holder). */ LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE); pgph = ShmemInitStruct("pg_password_history", sizeof(pgphSharedState), &found); if (!found) pgph->lock = &(GetNamedLWLockTranche(PGPH_TRANCHE_NAME))->lock; /* * First process only: relocate an old global/ file, then slurp any legacy * 5.x dump into memory BEFORE we map and initialise the store (the init * memset writes through the mapping and would wipe the legacy bytes). */ if (!found) { FILE *oldf = AllocateFile(PGPH_DUMP_FILE_OLD, PG_BINARY_R); if (oldf != NULL) { FreeFile(oldf); (void) durable_rename(PGPH_DUMP_FILE_OLD, PGPH_DUMP_FILE, LOG); } migrated = pgph_slurp_legacy(&migrated_n); } /* Attach the memory-mapped store (every process maps it locally). */ pgph_mmap_attach(); if (!found) { LWLockAcquire(pgph->lock, LW_EXCLUSIVE); /* Keep an existing valid 6.0 store; otherwise start empty. */ if (!(pgph_hdr->magic == PGPH_MMAP_MAGIC && pgph_hdr->version == PGPH_MMAP_VERSION && pgph_hdr->capacity == (uint32) pgph_max)) pgph_tab_init(); /* Re-insert migrated legacy entries, if any. */ if (migrated != NULL) { int32 i; for (i = 0; i < migrated_n; i++) { pgphEntry *e = pgph_tab_enter(&migrated[i].key, NULL); if (e) e->password_date = migrated[i].password_date; } free(migrated); pgph_msync(MS_SYNC); } else pgph_msync(MS_ASYNC); LWLockRelease(pgph->lock); } LWLockRelease(AddinShmemInitLock); } static pgphEntry * pgph_entry_alloc(pgphHashKey *key, TimestampTz password_date) { pgphEntry *entry; bool found; entry = pgph_tab_enter(key, &found); if (entry == NULL) { 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; } /* 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) { /* * Always initialize banned_date: dynahash does not zero reused slots, * so a sub-threshold entry could otherwise inherit a stale timestamp * from a previously banned role and show up wrongly in pg_banned_role. */ entry->failure_count = failure_count; if (failure_count >= fail_max) entry->banned_date = GetCurrentTimestamp(); else entry->banned_date = 0; } return entry; } /* * Flush password history to disk. * * With the memory-mapped store (option C) the history *is* the file, so a * "flush" is just an msync(); the kernel handles the rest of the write-back. * Kept under this name because it is called from every mutation path and from * the WAL redo routine. The caller must hold pgph->lock exclusively. */ static void flush_password_history(void) { if (!pgph || !pgph_base) return; elog(DEBUG1, "flushing password history (msync) to file %s", PGPH_DUMP_FILE); pgph_msync(MS_ASYNC); } 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", #if PG_VERSION_NUM < 190000 pgaf_max, pgaf_max, #else pgaf_max, #endif &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; int32 _i; 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"))); /* * Mutations are replicated through the credcheck custom WAL resource * manager, so this function must not be invoked while in recovery. */ #if PG_VERSION_NUM >= 150000 if (RecoveryInProgress()) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("recovery is in progress"), errhint("pg_password_history_reset() cannot be executed during recovery."))); #endif /* 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); for (_i = 0; _i < (int32) pgph_hdr->capacity; _i++) { if (!pgph_slots[_i].used) continue; entry = &pgph_slots[_i].entry; if (username == NULL || strcmp(entry->key.rolename, username) == 0) { pgph_tab_remove(&entry->key); num_removed++; } } /* Flush the new entry to disk */ if (num_removed > 0) { #if PG_VERSION_NUM >= 150000 credcheck_xlog_pwd_reset(username); #endif 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; int32 _i; 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); for (_i = 0; _i < (int32) pgph_hdr->capacity; _i++) { Datum values[PG_PASSWORD_HISTORY_COLS]; bool nulls[PG_PASSWORD_HISTORY_COLS]; int i = 0; if (!pgph_slots[_i].used) continue; entry = &pgph_slots[_i].entry; 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); } 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; int32 _i; /* 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"))); /* * Mutations are replicated through the credcheck custom WAL resource * manager, so this function must not be invoked while in recovery. */ #if PG_VERSION_NUM >= 150000 if (RecoveryInProgress()) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("recovery is in progress"), errhint("pg_password_history_timestamp() cannot be executed during recovery."))); #endif /* Lookup the hash table entry with exclusive lock. */ LWLockAcquire(pgph->lock, LW_EXCLUSIVE); for (_i = 0; _i < (int32) pgph_hdr->capacity; _i++) { if (!pgph_slots[_i].used) continue; entry = &pgph_slots[_i].entry; if (strcmp(entry->key.rolename, username) == 0) { entry->password_date = new_timestamp; num_changed++; } } /* Flush the new entry to disk */ if (num_changed > 0) { #if PG_VERSION_NUM >= 150000 credcheck_xlog_pwd_timestamp(username, new_timestamp); #endif flush_password_history(); } LWLockRelease(pgph->lock); PG_RETURN_INT32(num_changed); } PG_FUNCTION_INFO_V1(pg_check_password); /* * SQL-callable wrapper exposing the internal check_password() routine so a * user can validate a candidate password against the configured credcheck * username/password policy *without* actually creating or altering a role. */ Datum pg_check_password(PG_FUNCTION_ARGS) { char *username; char *password; if (PG_ARGISNULL(0) || PG_ARGISNULL(1)) ereport(ERROR, (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), errmsg("username and password must not be NULL"))); username = NameStr(*(PG_GETARG_NAME(0))); password = text_to_cstring(PG_GETARG_TEXT_PP(1)); check_password(username, password, PASSWORD_TYPE_PLAINTEXT, (Datum) 0, true); PG_RETURN_BOOL(true); } /* * Cheap, case-insensitive test for the substring "passw", used to avoid * lexing every single log line when there cannot be a password to redact. */ static bool query_has_password(const char *s) { if (s == NULL) return false; for (; *s; s++) { if ((s[0] == 'p' || s[0] == 'P') && (s[1] == 'a' || s[1] == 'A') && (s[2] == 's' || s[2] == 'S') && (s[3] == 's' || s[3] == 'S') && (s[4] == 'w' || s[4] == 'W')) return true; } return false; } /* Advance past whitespace. */ static char * skip_ws(char *p) { while (*p && isspace((unsigned char) *p)) p++; return p; } /* Skip constant SQL string literal.*/ static char * skip_string(char *p, bool estr) { p++; /* skip opening quote */ while (*p) { if (estr && *p == '\\' && p[1] != '\0') { p += 2; continue; } if (*p == '\'') { if (p[1] == '\'') { p += 2; continue; } return p + 1; /* closing quote */ } p++; } return p; } /* * Overwrite the interior of an SQL string literal with '.', preserving the * quotes and the literal's overall length. 'p' points at the opening quote. * Returns the position just past the closing quote. */ static char * mask_string_literal(char *p, bool estr) { p++; /* keep opening quote */ while (*p) { if (estr && *p == '\\' && p[1] != '\0') { *p = '.'; p[1] = '.'; p += 2; continue; } if (*p == '\'') { if (p[1] == '\'') /* doubled quote -> embedded ' */ { *p = '.'; p[1] = '.'; p += 2; continue; } return p + 1; /* keep closing quote */ } *p = '.'; p++; } return p; } /* * Mask, in place, the value of every PASSWORD '' (and FDW-style * "password ''") clause found in the SQL text 's'. A small lexer is * used so that the PASSWORD keyword is only matched as real SQL: string * literals, dollar-quoted strings, quoted identifiers and comments are skipped * so the word "password" appearing inside ordinary data never triggers a * (corrupting) match. */ static void mask_passwords(char *s) { char *p = s; if (s == NULL) return; while (*p) { unsigned char c = (unsigned char) *p; /* line comment */ if (c == '-' && p[1] == '-') { p += 2; while (*p && *p != '\n') p++; continue; } /* block comment (PostgreSQL allows nesting) */ if (c == '/' && p[1] == '*') { int depth = 1; p += 2; while (*p && depth > 0) { if (p[0] == '/' && p[1] == '*') { depth++; p += 2; } else if (p[0] == '*' && p[1] == '/') { depth--; p += 2; } else p++; } continue; } /* quoted identifier */ if (c == '"') { p++; while (*p) { if (*p == '"') { if (p[1] == '"') { p += 2; continue; } p++; break; } p++; } continue; } /* dollar-quoted string: $tag$ ... $tag$ */ if (c == '$') { char *tagend = p + 1; while (*tagend && (*tagend == '_' || isalnum((unsigned char) *tagend))) tagend++; if (*tagend == '$') { size_t taglen = (size_t) (tagend - p) + 1; char *body = tagend + 1; for (; *body; body++) { if (*body == '$' && strncmp(body, p, taglen) == 0) { body += taglen; break; } } p = body; continue; } /* not a dollar quote */ p++; continue; } /* ordinary string literal */ if (c == '\'') { p = skip_string(p, false); continue; } /* identifier or keyword */ if (c == '_' || isalpha(c)) { char *start = p; size_t len; p++; while (*p == '_' || *p == '$' || isalnum((unsigned char) *p)) p++; len = (size_t) (p - start); /* E'...' / e'...' escape string constant */ if (len == 1 && (start[0] == 'E' || start[0] == 'e') && *p == '\'') { p = skip_string(p, true); continue; } if (len == 8 && pg_strncasecmp(start, "password", 8) == 0) { char *lit = skip_ws(p); bool estr = false; if ((*lit == 'E' || *lit == 'e') && lit[1] == '\'') { estr = true; lit++; } if (*lit == '\'') { /* PASSWORD 'secret' -> PASSWORD '......' */ p = mask_string_literal(lit, estr); continue; } /* PASSWORD NULL / PASSWORD $n / etc: nothing to mask */ } continue; } p++; } } static void fix_log(ErrorData *edata) { /* Do not expose the password in the log. */ if (no_password_logging) { /* * Mask the password literal anywhere it could reach the log, for every * log level: log_statement / log_min_duration_statement emit at LOG, * policy violations and other failures at ERROR. */ if (query_has_password(edata->message)) mask_passwords(edata->message); if (query_has_password(edata->detail)) mask_passwords(edata->detail); if (query_has_password(edata->detail_log)) mask_passwords(edata->detail_log); if (query_has_password(edata->context)) mask_passwords(edata->context); if (query_has_password(edata->internalquery)) mask_passwords(edata->internalquery); /* * The "STATEMENT: ..." line attached to an error is printed by * send_message_to_server_log() straight from debug_query_string, not * from edata, so mask it there too. The cast is safe: masking is * length-preserving and the backend resets this buffer to the next * statement's text on the following command. */ if (debug_query_string != NULL && query_has_password(debug_query_string)) mask_passwords((char *) debug_query_string); } /* 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); /* get out if the user is whitelisted for auth failure */ if (is_in_whitelist(port->user_name, max_auth_whitelist)) { if (prev_ClientAuthentication) prev_ClientAuthentication(port, status); return; } /* 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, 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); } } /* Record the successful login in the lastlog history. */ if (status == STATUS_OK) lastlog_record_login(port); 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(Port *port, Oid userid) { pgafHashKey key; pgafEntry *entry; float fail_cnt = 1; /* if (port->ssl_in_use) fail_cnt = 0.5; */ Assert(port->user_name != 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 (port->ssl_in_use) 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", port->user_name); hash_search(pgaf_hash, &entry->key, HASH_REMOVE, NULL); } elog(DEBUG1, "Add new entry in auth failure hash table for user %s (%d, %f)", port->user_name, 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(credcheck_record_auth_failure); /* * Record an authentication failure reported from outside the backend's own * ClientAuthentication hook. * * credcheck can only observe -- through the ClientAuthentication hook -- the * authentication attempts that actually reach PostgreSQL. When a connection * pooler such as PgBouncer verifies the password itself (auth_query / userlist), * a wrong password fails inside the pooler and no client connection is ever made * to PostgreSQL, so the hook never fires and credcheck.max_auth_failure cannot * count it. This function is the integration point for that case: an edge agent * (for instance a fail2ban action watching the pooler log, or the pooler itself) * reports each failure here, and credcheck applies exactly the same threshold * (credcheck.max_auth_failure) and shared ban state as the in-backend path, so a * single max_auth_failure setting governs both direct and pooled connections. * * Returns the new failure count for the role, or 0 when the feature is disabled, * the role is whitelisted, or the role does not exist. It is privileged (only a * superuser may call it) so that an unprivileged user cannot ban other roles; * the operator additionally REVOKEs it from PUBLIC and GRANTs EXECUTE to the * trusted reporting role only. */ Datum credcheck_record_auth_failure(PG_FUNCTION_ARGS) { char *username; Oid userid; pgafHashKey key; pgafEntry *entry; float fail_cnt = 1; if (!superuser()) ereport(ERROR, (errmsg("only superuser can record authentication failures"))); username = NameStr(*(PG_GETARG_NAME(0))); /* Feature disabled or shared state not ready: nothing to record. */ if (fail_max == 0 || !pgaf || !pgaf_hash) PG_RETURN_INT32(0); /* Honor the same whitelist as the in-backend hook. */ if (is_in_whitelist(username, max_auth_whitelist)) PG_RETURN_INT32(0); /* Mirror the hook: unknown roles are ignored. */ userid = get_role_oid(username, true); if (userid == InvalidOid) PG_RETURN_INT32(0); key.roleid = userid; LWLockAcquire(pgaf->lock, LW_EXCLUSIVE); entry = (pgafEntry *) hash_search(pgaf_hash, &key, HASH_FIND, NULL); if (entry) { /* * Already banned: do not keep incrementing past the threshold, exactly * as the hook rejects further attempts before counting them. */ if (entry->failure_count >= fail_max) { fail_cnt = entry->failure_count; LWLockRelease(pgaf->lock); PG_RETURN_INT32((int) fail_cnt); } fail_cnt = entry->failure_count + 1; hash_search(pgaf_hash, &entry->key, HASH_REMOVE, NULL); } /* (Re)create the entry; pgaf_entry_alloc arms banned_date at the threshold. */ entry = pgaf_entry_alloc(&key, fail_cnt); LWLockRelease(pgaf->lock); PG_RETURN_INT32((int) fail_cnt); } PG_FUNCTION_INFO_V1(credcheck_is_banned); /* * Return true when a role is currently banned for too many authentication * failures. Intended to be called from the pooler's auth_query function so that * a banned role can be refused at the pooler edge (where the ban would otherwise * never be enforced, since a pooler-terminated login never reaches the backend's * ClientAuthentication hook). Read-only and cheap; safe to grant to the role the * pooler uses for auth_query. */ Datum credcheck_is_banned(PG_FUNCTION_ARGS) { char *username; Oid userid; pgafHashKey key; pgafEntry *entry; bool banned = false; username = NameStr(*(PG_GETARG_NAME(0))); if (fail_max == 0 || !pgaf || !pgaf_hash) PG_RETURN_BOOL(false); userid = get_role_oid(username, true); if (userid == InvalidOid) PG_RETURN_BOOL(false); key.roleid = userid; LWLockAcquire(pgaf->lock, LW_SHARED); entry = (pgafEntry *) hash_search(pgaf_hash, &key, HASH_FIND, NULL); /* * A role is banned once its failure count reaches the threshold -- the same * condition the ClientAuthentication hook uses to reject a connection. We do * not test banned_date here because it is only meaningful at/after the * threshold. */ if (entry && entry->failure_count >= fail_max) banned = true; LWLockRelease(pgaf->lock); PG_RETURN_BOOL(banned); } 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++] = ObjectIdGetDatum(entry->key.roleid); #if PG_VERSION_NUM >= 190000 values[i++] = UInt8GetDatum(entry->failure_count); #else values[i++] = Int8GetDatum(entry->failure_count); #endif 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); } static void cc_ExecutorStart(QueryDesc *queryDesc, int eflags) { elog(DEBUG1, "cc_ExecutorStart()"); if (MyProcPort != NULL && NOT_IN_PARALLEL_WORKER) { /* * When first login we don't allow anything else than password change. * This rule is also checked in the ProcessUtility hook because we need * to allow the ALTER ROLE command to change the password. We must allow * SELECT CURRENT_USER which is sent by the \passwd command before * password change. */ if (debug_query_string != NULL && strcmp(debug_query_string, "SELECT CURRENT_USER") != 0) { if (!is_in_whitelist(MyProcPort->user_name, username_whitelist)) { if (queryDesc->operation != CMD_UTILITY && force_change_password) ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg(gettext_noop("you must change your password first.")))); } } } /* Remember the last executed query for the lastlog view. */ if (queryDesc->sourceText != NULL) lastlog_track_last_query(queryDesc->sourceText); /* Continue the normal behavior */ if (prev_ExecutorStart) prev_ExecutorStart(queryDesc, eflags); else standard_ExecutorStart(queryDesc, eflags); elog(DEBUG1, "End of cc_ExecutorStart()"); } static void set_force_change_password(Oid databaseid, Oid roleid, char *valuestr) { bool need_priv_escalation = !superuser(); /* we might be a SU */ Oid save_userid; int save_sec_context; HeapTuple tuple; Relation rel; ScanKeyData scankey[2]; SysScanDesc scan; if (roleid == InvalidOid) return; /* The setting reseet must be done as SU */ if (need_priv_escalation) { /* Get current user's Oid and security context */ GetUserIdAndSecContext(&save_userid, &save_sec_context); /* Become superuser */ SetUserIdAndSecContext(BOOTSTRAP_SUPERUSERID, save_sec_context | SECURITY_LOCAL_USERID_CHANGE | SECURITY_RESTRICTED_OPERATION); } /* Get the old tuple, if any. */ rel = table_open(DbRoleSettingRelationId, RowExclusiveLock); ScanKeyInit(&scankey[0], Anum_pg_db_role_setting_setdatabase, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(databaseid)); ScanKeyInit(&scankey[1], Anum_pg_db_role_setting_setrole, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(roleid)); scan = systable_beginscan(rel, DbRoleSettingDatidRolidIndexId, true, NULL, 2, scankey); tuple = systable_getnext(scan); if (HeapTupleIsValid(tuple)) { Datum repl_val[Natts_pg_db_role_setting]; bool repl_null[Natts_pg_db_role_setting]; bool repl_repl[Natts_pg_db_role_setting]; HeapTuple newtuple; Datum datum; bool isnull; ArrayType *a; memset(repl_repl, false, sizeof(repl_repl)); repl_repl[Anum_pg_db_role_setting_setconfig - 1] = true; repl_null[Anum_pg_db_role_setting_setconfig - 1] = false; /* Extract old value of setconfig */ datum = heap_getattr(tuple, Anum_pg_db_role_setting_setconfig, RelationGetDescr(rel), &isnull); a = isnull ? NULL : DatumGetArrayTypeP(datum); /* Update (valuestr is NULL in RESET cases) */ if (valuestr) a = GUCArrayAdd(a, "credcheck_internal.force_change_password", valuestr); else a = GUCArrayDelete(a, "credcheck_internal.force_change_password"); if (a) { repl_val[Anum_pg_db_role_setting_setconfig - 1] = PointerGetDatum(a); newtuple = heap_modify_tuple(tuple, RelationGetDescr(rel), repl_val, repl_null, repl_repl); CatalogTupleUpdate(rel, &tuple->t_self, newtuple); } else CatalogTupleDelete(rel, &tuple->t_self); } else if (valuestr) { /* non-null valuestr means it's not RESET, so insert a new tuple */ HeapTuple newtuple; Datum values[Natts_pg_db_role_setting]; bool nulls[Natts_pg_db_role_setting]; ArrayType *a; memset(nulls, false, sizeof(nulls)); a = GUCArrayAdd(NULL, "credcheck_internal.force_change_password", valuestr); values[Anum_pg_db_role_setting_setdatabase - 1] = ObjectIdGetDatum(databaseid); values[Anum_pg_db_role_setting_setrole - 1] = ObjectIdGetDatum(roleid); values[Anum_pg_db_role_setting_setconfig - 1] = PointerGetDatum(a); newtuple = heap_form_tuple(RelationGetDescr(rel), values, nulls); CatalogTupleInsert(rel, newtuple); } systable_endscan(scan); /* Close pg_db_role_setting, but keep lock till commit */ table_close(rel, NoLock); /* Restore user's privileges */ if (need_priv_escalation) SetUserIdAndSecContext(save_userid, save_sec_context); } HexaCluster-credcheck-1c98317/credcheck.control000066400000000000000000000002251526372676500214720ustar00rootroot00000000000000comment = 'credcheck - postgresql plain text credential checker' default_version = '6.0.0' module_pathname = '$libdir/credcheck' relocatable = false HexaCluster-credcheck-1c98317/event_trigger.sql000066400000000000000000000020561526372676500215460ustar00rootroot00000000000000 -- Drop the event trigger if present DROP EVENT TRIGGER IF EXISTS valid_until_warning CASCADE; -- Add event trigger for valid until warning CREATE OR REPLACE FUNCTION warning_valid_until() RETURNS event_trigger AS $$ DECLARE warn_days integer; valid_warning integer; BEGIN valid_warning := current_setting('credcheck.password_valid_warning', true)::integer; IF (valid_warning IS NOT NULL AND valid_warning > 0) THEN SELECT ((extract(epoch from valuntil) - extract(epoch from current_timestamp))/86400)::integer INTO warn_days FROM pg_catalog.pg_shadow WHERE usename = SESSION_USER AND valuntil IS NOT NULL AND valuntil > current_timestamp; IF (warn_days IS NOT NULL AND warn_days <= valid_warning) THEN RAISE WARNING 'your password will expire in % days, please renew your password!', warn_days; END IF; END IF; END; $$ LANGUAGE plpgsql SECURITY DEFINER ; -- trigger definition CREATE EVENT TRIGGER valid_until_warning ON login EXECUTE FUNCTION warning_valid_until(); ALTER EVENT TRIGGER valid_until_warning ENABLE ALWAYS; HexaCluster-credcheck-1c98317/sql/000077500000000000000000000000001526372676500167555ustar00rootroot00000000000000HexaCluster-credcheck-1c98317/sql/credcheck--0.1.0.sql000066400000000000000000000000001526372676500221060ustar00rootroot00000000000000HexaCluster-credcheck-1c98317/sql/credcheck--0.2.0.sql000066400000000000000000000000001526372676500221070ustar00rootroot00000000000000HexaCluster-credcheck-1c98317/sql/credcheck--1.0.0.sql000066400000000000000000000033651526372676500221270ustar00rootroot00000000000000-- 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; HexaCluster-credcheck-1c98317/sql/credcheck--1.1.0.sql000066400000000000000000000033651526372676500221300ustar00rootroot00000000000000-- 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; HexaCluster-credcheck-1c98317/sql/credcheck--1.2.0.sql000066400000000000000000000033651526372676500221310ustar00rootroot00000000000000-- 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; HexaCluster-credcheck-1c98317/sql/credcheck--2.0.0.sql000066400000000000000000000054441526372676500221300ustar00rootroot00000000000000-- 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; HexaCluster-credcheck-1c98317/sql/credcheck--2.1.0.sql000066400000000000000000000054441526372676500221310ustar00rootroot00000000000000-- 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; HexaCluster-credcheck-1c98317/sql/credcheck--2.2.0.sql000066400000000000000000000054441526372676500221320ustar00rootroot00000000000000-- 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; HexaCluster-credcheck-1c98317/sql/credcheck--2.3.0.sql000066400000000000000000000054441526372676500221330ustar00rootroot00000000000000-- 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; HexaCluster-credcheck-1c98317/sql/credcheck--2.4.0.sql000066400000000000000000000054331526372676500221320ustar00rootroot00000000000000-- 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; HexaCluster-credcheck-1c98317/sql/credcheck--2.5.0.sql000066400000000000000000000054331526372676500221330ustar00rootroot00000000000000-- 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; HexaCluster-credcheck-1c98317/sql/credcheck--2.6.0.sql000066400000000000000000000054331526372676500221340ustar00rootroot00000000000000-- 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; HexaCluster-credcheck-1c98317/sql/credcheck--2.7.0.sql000066400000000000000000000054331526372676500221350ustar00rootroot00000000000000-- 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; HexaCluster-credcheck-1c98317/sql/credcheck--2.8.0.sql000066400000000000000000000054331526372676500221360ustar00rootroot00000000000000-- 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; HexaCluster-credcheck-1c98317/sql/credcheck--3.0.0.sql000066400000000000000000000054331526372676500221270ustar00rootroot00000000000000-- 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; HexaCluster-credcheck-1c98317/sql/credcheck--4.0.0.sql000066400000000000000000000054401526372676500221260ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2021-2023 MigOps Inc -- Copyright (c) 2023 Gilles Darold -- Copyright (c) 2024-2026 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; HexaCluster-credcheck-1c98317/sql/credcheck--4.1.0.sql000066400000000000000000000054611526372676500221320ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2021-2023 MigOps Inc -- Copyright (c) 2023 Gilles Darold -- Copyright (c) 2024-2026 HexaCluster Corp -- 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 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 roleid::regrole, failure_count, banned_date 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; HexaCluster-credcheck-1c98317/sql/credcheck--4.2.0.sql000066400000000000000000000054611526372676500221330ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2021-2023 MigOps Inc -- Copyright (c) 2023 Gilles Darold -- Copyright (c) 2024-2026 HexaCluster Corp -- 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 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 roleid::regrole, failure_count, banned_date 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; HexaCluster-credcheck-1c98317/sql/credcheck--4.3.0--4.4.0.sql000066400000000000000000000002201526372676500226360ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2024-2026 HexaCluster Corp - All rights reserved. -- Nothing to do, only library change HexaCluster-credcheck-1c98317/sql/credcheck--4.3.0.sql000066400000000000000000000054611526372676500221340ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2021-2023 MigOps Inc -- Copyright (c) 2023 Gilles Darold -- Copyright (c) 2024-2026 HexaCluster Corp -- 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 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 roleid::regrole, failure_count, banned_date 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; HexaCluster-credcheck-1c98317/sql/credcheck--4.4.0.sql000066400000000000000000000054611526372676500221350ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2021-2023 MigOps Inc -- Copyright (c) 2023 Gilles Darold -- Copyright (c) 2024-2026 HexaCluster Corp -- 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 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 roleid::regrole, failure_count, banned_date 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; HexaCluster-credcheck-1c98317/sql/credcheck--4.5.0.sql000066400000000000000000000054611526372676500221360ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2021-2023 MigOps Inc -- Copyright (c) 2023 Gilles Darold -- Copyright (c) 2024-2026 HexaCluster Corp -- 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 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 roleid::regrole, failure_count, banned_date 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; HexaCluster-credcheck-1c98317/sql/credcheck--4.6.0.sql000066400000000000000000000054611526372676500221370ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2021-2023 MigOps Inc -- Copyright (c) 2023 Gilles Darold -- Copyright (c) 2024-2026 HexaCluster Corp -- 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 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 roleid::regrole, failure_count, banned_date 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; HexaCluster-credcheck-1c98317/sql/credcheck--4.7.0.sql000066400000000000000000000054611526372676500221400ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2021-2023 MigOps Inc -- Copyright (c) 2023 Gilles Darold -- Copyright (c) 2024-2026 HexaCluster Corp -- 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 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 roleid::regrole, failure_count, banned_date 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; HexaCluster-credcheck-1c98317/sql/credcheck--5.0.0.sql000066400000000000000000000054611526372676500221320ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2021-2023 MigOps Inc -- Copyright (c) 2023 Gilles Darold -- Copyright (c) 2024-2026 HexaCluster Corp -- 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 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 roleid::regrole, failure_count, banned_date 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; HexaCluster-credcheck-1c98317/sql/credcheck--5.1.0.sql000066400000000000000000000064401526372676500221310ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2021-2023 MigOps Inc -- Copyright (c) 2023 Gilles Darold -- Copyright (c) 2024-2026 HexaCluster Corp -- 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 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 roleid::regrole, failure_count, banned_date 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; ---- -- Check a candidate password (and the associated username) against the -- configured credcheck username/password policy without creating or -- altering a role. Raises the same error as CREATE/ALTER ROLE would when -- the policy is violated, and returns true when the password is accepted. -- Note: the password reuse policy is not evaluated here ---- CREATE FUNCTION pg_check_password( IN username name, IN password text ) RETURNS boolean AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; HexaCluster-credcheck-1c98317/sql/credcheck--6.0.0.sql000066400000000000000000000124141526372676500221270ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2021-2023 MigOps Inc -- Copyright (c) 2023 Gilles Darold -- Copyright (c) 2024-2026 HexaCluster Corp -- 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 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(); REVOKE ALL ON FUNCTION pg_password_history() FROM PUBLIC; REVOKE ALL ON pg_password_history FROM PUBLIC; GRANT SELECT ON pg_password_history TO pg_read_all_stats; ---- -- 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; -- 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 roleid::regrole, failure_count, banned_date 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; ---- -- Record an authentication failure reported from outside PostgreSQL, for -- environments where a connection pooler (e.g. PgBouncer with auth_query) -- verifies the password itself so the failure never reaches the backend's -- ClientAuthentication hook. credcheck applies the same credcheck.max_auth_failure -- threshold and shared ban state as for direct connections. Returns the new -- failure count for the role. ---- CREATE FUNCTION credcheck_record_auth_failure( IN username name ) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; ---- -- Return true when a role is currently banned. Intended to be called from the -- pooler's auth_query function so a banned role can be refused at the edge. ---- CREATE FUNCTION credcheck_is_banned( IN username name ) RETURNS boolean AS 'MODULE_PATHNAME' LANGUAGE C STRICT STABLE; -- record_auth_failure can ban any role, so keep it off PUBLIC: the operator -- GRANTs EXECUTE to the trusted reporting role only (e.g. the fail2ban DB user). REVOKE ALL ON FUNCTION credcheck_record_auth_failure(name) FROM PUBLIC; -- is_banned only reads state; the operator GRANTs it to the pooler auth_query role. REVOKE ALL ON FUNCTION credcheck_is_banned(name) FROM PUBLIC; ---- -- Check a candidate password (and the associated username) against the -- configured credcheck username/password policy without creating or -- altering a role. Raises the same error as CREATE/ALTER ROLE would when -- the policy is violated, and returns true when the password is accepted. -- Note: the password reuse policy is not evaluated here ---- CREATE FUNCTION pg_check_password( IN username name, IN password text ) RETURNS boolean AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; ---- -- lastlog: utmp/wtmp-like login history (see credcheck.lastlog GUC). ---- CREATE FUNCTION credcheck_lastlog ( OUT type text, OUT username text, OUT pid integer, OUT client_addr text, OUT client_port integer, OUT login_time timestamp with time zone, OUT logout_time timestamp with time zone, OUT duration interval, OUT state text, OUT query text ) RETURNS SETOF record AS 'MODULE_PATHNAME', 'credcheck_lastlog' LANGUAGE C VOLATILE; CREATE VIEW pg_lastlog AS SELECT * FROM credcheck_lastlog(); REVOKE ALL ON FUNCTION credcheck_lastlog() FROM PUBLIC; REVOKE ALL ON pg_lastlog FROM PUBLIC; GRANT SELECT ON pg_lastlog TO pg_read_all_stats; HexaCluster-credcheck-1c98317/t/000077500000000000000000000000001526372676500164215ustar00rootroot00000000000000HexaCluster-credcheck-1c98317/t/001_history_replication.pl000066400000000000000000000134051526372676500234330ustar00rootroot00000000000000 # Test that password history mutations done on the primary are shipped # through the credcheck custom WAL resource manager and applied on a # streaming standby, and that the history survives a standby promotion. use strict; use warnings FATAL => 'all'; use PostgreSQL::Test::Cluster; use PostgreSQL::Test::Utils; use Test::More; # Skip on PostgreSQL < 15: the Custom WAL Resource Manager API # (RegisterCustomRmgr) was introduced in PG 15. my ($pg_major) = (`pg_config --version` =~ /(\d+)/); plan skip_all => "this test requires PostgreSQL 15 or later" if $pg_major < 15; # Set up primary my $primary = PostgreSQL::Test::Cluster->new('primary'); $primary->init(allows_streaming => 1); $primary->append_conf( 'postgresql.conf', q{ shared_preload_libraries = 'credcheck' credcheck.password_reuse_history = 3 credcheck.password_min_length = 1 credcheck.password_min_lower = 0 credcheck.password_min_upper = 0 credcheck.password_min_digit = 0 credcheck.password_min_special = 0 credcheck.password_contain_username = false }); $primary->start; $primary->safe_psql('postgres', 'CREATE EXTENSION credcheck'); # Set up standby $primary->backup('bk1'); my $standby = PostgreSQL::Test::Cluster->new('standby'); $standby->init_from_backup($primary, 'bk1', has_streaming => 1); # credcheck must be preloaded on the standby as well, so that its custom # rmgr is registered before recovery starts replaying credcheck records. $standby->append_conf( 'postgresql.conf', q{ shared_preload_libraries = 'credcheck' credcheck.password_reuse_history = 3 }); $standby->start; # Confirm the custom rmgr is registered on both nodes. is( $primary->safe_psql( 'postgres', "SELECT count(*) FROM pg_get_wal_resource_managers() WHERE rm_name = 'credcheck'" ), '1', 'credcheck rmgr is registered on the primary'); is( $standby->safe_psql( 'postgres', "SELECT count(*) FROM pg_get_wal_resource_managers() WHERE rm_name = 'credcheck'" ), '1', 'credcheck rmgr is registered on the standby'); # Create users on the primary $primary->safe_psql('postgres', q{CREATE ROLE alice LOGIN PASSWORD 'pwd_alice_1'}); $primary->safe_psql('postgres', q{CREATE ROLE bob LOGIN PASSWORD 'pwd_bob_1'}); # Generate a few password updates so the history contains multiple entries. $primary->safe_psql('postgres', q{ALTER ROLE alice PASSWORD 'pwd_alice_2'}); $primary->safe_psql('postgres', q{ALTER ROLE alice PASSWORD 'pwd_alice_3'}); $primary->wait_for_catchup($standby); # Verify history content on both nodes my $sql_count = q{SELECT rolename, count(*) FROM pg_password_history GROUP BY rolename ORDER BY rolename}; my $primary_state = $primary->safe_psql('postgres', $sql_count); my $standby_state = $standby->safe_psql('postgres', $sql_count); is($standby_state, $primary_state, 'password history on standby matches the primary after WAL replay'); like($primary_state, qr/^alice\|3\nbob\|1$/m, 'primary contains the expected number of history entries'); # Rename a role $primary->safe_psql('postgres', q{ALTER ROLE bob RENAME TO bobby}); $primary->wait_for_catchup($standby); is( $standby->safe_psql('postgres', q{SELECT count(*) FROM pg_password_history WHERE rolename = 'bobby'}), '1', 'rename is replicated to the standby'); is( $standby->safe_psql('postgres', q{SELECT count(*) FROM pg_password_history WHERE rolename = 'bob'}), '0', 'old name is gone from the standby history'); # Drop a role $primary->safe_psql('postgres', q{DROP ROLE bobby}); $primary->wait_for_catchup($standby); is( $standby->safe_psql('postgres', q{SELECT count(*) FROM pg_password_history WHERE rolename = 'bobby'}), '0', 'DROP ROLE removes user history on the standby'); # Explicit reset $primary->safe_psql('postgres', q{SELECT pg_password_history_reset('alice')}); $primary->wait_for_catchup($standby); is( $standby->safe_psql('postgres', q{SELECT count(*) FROM pg_password_history}), '0', 'pg_password_history_reset() is replicated to the standby'); # Restart standby and re-check persistence: push another change, then # restart the standby so we exercise loading pg_password_history from # the file written by redo. $primary->safe_psql('postgres', q{CREATE ROLE carol LOGIN PASSWORD 'pwd_carol_1'}); $primary->wait_for_catchup($standby); $standby->restart; is( $standby->safe_psql('postgres', q{SELECT count(*) FROM pg_password_history WHERE rolename = 'carol'}), '1', 'history file persisted by redo is reloaded on standby restart'); # Promote the standby and check that the policy is still enforced. # One last password change on the primary so the promoted node has two # entries for carol to verify against. $primary->safe_psql('postgres', q{ALTER ROLE carol PASSWORD 'pwd_carol_2'}); $primary->wait_for_catchup($standby); $primary->stop; $standby->promote; $standby->poll_query_until('postgres', 'SELECT NOT pg_is_in_recovery()'); # carol has two entries replicated from the old primary: # pwd_carol_1 (CREATE ROLE) and pwd_carol_2 (ALTER ROLE). is( $standby->safe_psql('postgres', q{SELECT count(*) FROM pg_password_history WHERE rolename = 'carol'}), '2', 'promoted standby retains full history replicated from the old primary'); # pwd_carol_1 is still in the history window, so reusing it must be rejected. my ($ret, $stdout, $stderr) = $standby->psql('postgres', q{ALTER ROLE carol PASSWORD 'pwd_carol_1'}); isnt($ret, 0, 'promoted standby rejects a password that is in the replicated history'); like($stderr, qr/cannot use this credential following the password reuse policy/i, 'error message mentions reuse restriction'); # An unused password must be accepted. $standby->safe_psql('postgres', q{ALTER ROLE carol PASSWORD 'pwd_carol_new'}); is( $standby->safe_psql('postgres', q{SELECT count(*) FROM pg_password_history WHERE rolename = 'carol'}), '3', 'new password is stored in history on the promoted standby'); $standby->stop; done_testing(); HexaCluster-credcheck-1c98317/t/002_lastlog.pl000066400000000000000000000164411526372676500210120ustar00rootroot00000000000000 # Regression test for the "lastlog" feature: a utmp/wtmp-like login history # persisted in a memory-mapped file. Exercises the pieces that depend on the # server life cycle (and therefore cannot be checked with pg_regress): # - the boot marker written at startup; # - user sessions recorded at connection / finalized at disconnect; # - capture of the last executed query (credcheck.lastlog_track_query); # - the "still connected" live state for an open session; # - persistence of the history across a restart (mmap file reload); # - a clean restart must NOT produce a false crash marker, but must leave # a shutdown marker; # - an unclean stop (immediate) must be detected as a crash at next boot; # - the pg_lastlog view is readable by pg_read_all_stats but not PUBLIC. use strict; use warnings FATAL => 'all'; use PostgreSQL::Test::Cluster; use PostgreSQL::Test::Utils; use Test::More; # background_psql() (used for the "still connected" check) is available with # a stable API from PostgreSQL 15 on. my ($pg_major) = (`pg_config --version` =~ /(\d+)/); plan skip_all => "this test requires PostgreSQL 15 or later" if $pg_major < 15; my $node = PostgreSQL::Test::Cluster->new('lastlog'); $node->init; $node->append_conf( 'postgresql.conf', q{ shared_preload_libraries = 'credcheck' credcheck.lastlog = on credcheck.lastlog_max = 128 credcheck.lastlog_track_query = on credcheck.lastlog_flush_interval = 1 # keep the username/password policy out of the way of role creation credcheck.password_min_length = 1 credcheck.password_min_lower = 0 credcheck.password_min_upper = 0 credcheck.password_min_digit = 0 credcheck.password_min_special = 0 credcheck.password_contain_username = false }); $node->start; $node->safe_psql('postgres', 'CREATE EXTENSION credcheck'); # Small helper: count rows of pg_lastlog matching an optional WHERE clause. sub ll_count { my ($where) = @_; my $q = 'SELECT count(*) FROM pg_lastlog'; $q .= " WHERE $where" if defined $where; return $node->safe_psql('postgres', $q); } # --------------------------------------------------------------------------- # 1. The boot marker is written at startup. # --------------------------------------------------------------------------- ok(ll_count("type = 'boot'") >= 1, 'a boot marker is recorded at startup'); # --------------------------------------------------------------------------- # 2. A user session is recorded and finalized at disconnect. # --------------------------------------------------------------------------- $node->safe_psql('postgres', 'CREATE ROLE lluser LOGIN'); # connect as lluser, run a distinctive query, then disconnect $node->safe_psql('postgres', "SELECT 'llprobe_marker_42'", extra_params => [ '--username', 'lluser' ]); # the record is written during the backend's proc_exit, which races with # psql returning: poll until it shows up $node->poll_query_until('postgres', "SELECT count(*) > 0 FROM pg_lastlog " . "WHERE type = 'user' AND username = 'lluser' AND state = 'disconnected'", 't') or die "timed out waiting for the lluser session to be finalized"; is( $node->safe_psql( 'postgres', "SELECT count(*) FROM pg_lastlog " . "WHERE type = 'user' AND username = 'lluser' AND state = 'disconnected'" ) >= 1 ? 1 : 0, 1, 'a finished user session is recorded as disconnected'); # --------------------------------------------------------------------------- # 3. The last executed query is captured (lastlog_track_query = on). # --------------------------------------------------------------------------- is( $node->safe_psql( 'postgres', "SELECT count(*) FROM pg_lastlog " . "WHERE username = 'lluser' AND query LIKE '%llprobe_marker_42%'" ) >= 1 ? 1 : 0, 1, 'the last executed query of a session is captured'); # --------------------------------------------------------------------------- # 4. login_time and duration are populated for a finished session. # --------------------------------------------------------------------------- is( $node->safe_psql( 'postgres', "SELECT bool_and(login_time IS NOT NULL AND logout_time IS NOT NULL " . "AND duration IS NOT NULL) FROM pg_lastlog " . "WHERE username = 'lluser' AND state = 'disconnected'" ), 't', 'finished sessions carry login_time, logout_time and duration'); # --------------------------------------------------------------------------- # 5. An open session shows up as "still connected", then flips to # "disconnected" once it is closed. # --------------------------------------------------------------------------- my $bg = $node->background_psql('postgres', on_error_stop => 1); my $bgpid = $bg->query_safe('SELECT pg_backend_pid()'); $bgpid =~ s/\D//g; ok( $node->safe_psql( 'postgres', "SELECT count(*) FROM pg_lastlog " . "WHERE pid = $bgpid AND state = 'still connected'") >= 1, 'an open session is reported as still connected'); $bg->quit; $node->poll_query_until('postgres', "SELECT count(*) > 0 FROM pg_lastlog " . "WHERE pid = $bgpid AND state = 'disconnected'", 't') or die "timed out waiting for the background session to disconnect"; pass('a closed session flips to disconnected'); # --------------------------------------------------------------------------- # 6. A clean restart preserves the history (mmap reload), writes a shutdown # marker, and does NOT create a false crash marker. # --------------------------------------------------------------------------- my $users_before = ll_count("username = 'lluser'"); my $crashes_before = ll_count("type = 'crash'"); $node->restart; ok(ll_count("type = 'shutdown'") >= 1, 'a shutdown marker is recorded on clean stop'); is(ll_count("type = 'crash'"), $crashes_before, 'a clean restart does not create a crash marker'); ok(ll_count("username = 'lluser'") >= $users_before, 'history survives a restart (memory-mapped file reloaded)'); # a fresh boot marker must have been added by the restart ok(ll_count("type = 'boot'") >= 2, 'the restart adds a new boot marker'); # --------------------------------------------------------------------------- # 7. An unclean stop is detected as a crash at the next startup. # --------------------------------------------------------------------------- my $crashes_pre = ll_count("type = 'crash'"); $node->stop('immediate'); $node->start; ok(ll_count("type = 'crash'") > $crashes_pre, 'an unclean (immediate) stop is detected as a crash at next boot'); # --------------------------------------------------------------------------- # 8. Access control: pg_lastlog is readable by pg_read_all_stats members but # not by an unprivileged role. # --------------------------------------------------------------------------- $node->safe_psql('postgres', 'CREATE ROLE ll_nopriv LOGIN'); $node->safe_psql('postgres', 'GRANT EXECUTE ON FUNCTION credcheck_lastlog TO pg_read_all_stats'); $node->safe_psql('postgres', 'CREATE ROLE ll_priv LOGIN IN ROLE pg_read_all_stats'); my ($rc_no, $out_no, $err_no) = $node->psql( 'postgres', 'SELECT count(*) FROM pg_lastlog', extra_params => [ '--username', 'll_nopriv' ]); isnt($rc_no, 0, 'an unprivileged role cannot read pg_lastlog'); like($err_no, qr/permission denied/, 'the denial mentions permission denied'); my ($rc_yes, $out_yes, $err_yes) = $node->psql( 'postgres', 'SELECT count(*) FROM pg_lastlog', extra_params => [ '--username', 'll_priv' ]); is($rc_yes, 0, 'a pg_read_all_stats member can read pg_lastlog'); $node->stop; done_testing(); HexaCluster-credcheck-1c98317/test/000077500000000000000000000000001526372676500171355ustar00rootroot00000000000000HexaCluster-credcheck-1c98317/test/expected/000077500000000000000000000000001526372676500207365ustar00rootroot00000000000000HexaCluster-credcheck-1c98317/test/expected/01_username.out000066400000000000000000000143171526372676500236140ustar00rootroot00000000000000LOAD '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 (2) 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 (2) 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 (5) 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 (1) 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 (1) 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 (1) 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: a,b,c 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: x,z 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 (1) 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 (1) 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 (1) 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 (1) DROP USER aa; ERROR: role "aa" does not exist SET credcheck.username_contain TO 'ba,c'; DROP USER IF EXISTS "b$auser1"; NOTICE: role "b$auser1" does not exist, skipping CREATE USER "b$auser1" WITH PASSWORD 'dummy'; ERROR: username does not contain the configured credcheck.username_contain characters: ba,c DROP USER IF EXISTS "ba$user1"; NOTICE: role "ba$user1" does not exist, skipping CREATE USER "ba$user1" WITH PASSWORD 'dummy'; DROP USER IF EXISTS "ba$user1"; DROP USER IF EXISTS "c$user1"; NOTICE: role "c$user1" does not exist, skipping CREATE USER "c$user1" WITH PASSWORD 'dummy'; DROP USER IF EXISTS "c$user1"; HexaCluster-credcheck-1c98317/test/expected/02_password.out000066400000000000000000000121331526372676500236320ustar00rootroot00000000000000LOAD '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 (2) 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 (1) 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: a,b,c 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: a,b,c 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 (1) 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 (2) 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'; SET credcheck.whitelist = ''; SELECT pg_check_password('nocheck2', 'nocheck2'); ERROR: username should not contain password SELECT pg_check_password('nocheck2', 'aaaaaaaa'); ERROR: password does not contain the configured credcheck.password_min_digit characters (1) DROP USER IF EXISTS nocheck1; DROP USER IF EXISTS nocheck2; HexaCluster-credcheck-1c98317/test/expected/03_rename.out000066400000000000000000000057441526372676500232520ustar00rootroot00000000000000--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 (2) -- 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 (2) DROP USER bbb; CREATE USER b; ERROR: username length should match the configured credcheck.username_min_length (2) -- --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 (5) -- --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 (1) -- --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 (1) -- --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 (1) -- --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: a,b,c -- --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: x,z -- --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 (1) DROP USER aaa; HexaCluster-credcheck-1c98317/test/expected/04_alter_pwd.out000066400000000000000000000056211526372676500237570ustar00rootroot00000000000000CREATE 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 (2) -- --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 (1) -- --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 (1) -- --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 (1) -- --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: a,b,c -- --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: a,b,c -- -- 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 (1) -- --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 (2) DROP USER aaa; HexaCluster-credcheck-1c98317/test/expected/05_reuse_history.out000066400000000000000000000047601526372676500247060ustar00rootroot00000000000000DROP 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) HexaCluster-credcheck-1c98317/test/expected/05_reuse_history_1.out000066400000000000000000000047601526372676500251260ustar00rootroot00000000000000DROP 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) HexaCluster-credcheck-1c98317/test/expected/06_reuse_interval.out000066400000000000000000000103371526372676500250270ustar00rootroot00000000000000SET 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=6O1'; SELECT rolename, password_hash FROM pg_password_history WHERE rolename = 'credtest' ORDER BY password_date ; rolename | password_hash ----------+------------------------------------------------------------------ credtest | e61e58c22aa6bf31a92b385932f7d0e4dbaba24fa3fdb2982510d6c72a961335 credtest | a4a99db0eae794af3fdfeb6094da5cbd591b81cb7df6c82cdecc100a3b9ffe55 (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) HexaCluster-credcheck-1c98317/test/expected/06_reuse_interval_1.out000066400000000000000000000103371526372676500252470ustar00rootroot00000000000000SET 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=6O1'; SELECT rolename, password_hash FROM pg_password_history WHERE rolename = 'credtest' ORDER BY password_date ; rolename | password_hash ----------+------------------------------------------------------------------ credtest | c38cf85ca6c3e5ee72c09cf0bfb42fb29b0f0a3e8ba335637941d60f86512508 credtest | 9b8221d3b2db06d2b736bbf1d45d046adc7b476e2c20a61adc4dd1886cbbb1ed (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) HexaCluster-credcheck-1c98317/test/expected/07_valid_until.out000066400000000000000000000160421526372676500243120ustar00rootroot00000000000000LOAD '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; -- the VALID UNTIL clause must be present, if not it will be added automaticaly CREATE USER aaa PASSWORD 'DummY'; select count(*) from pg_shadow where usename = 'aaa' AND valuntil::date = (now()+'5 days'::interval)::date; count ------- 1 (1 row) DROP USER aaa; -- 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 NOT 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 aaa with login password 'password'; select rolname, rolvaliduntil between now() + '59 days'::interval and now() + '61 days'::interval from pg_roles WHERE rolname='aaa'; rolname | ?column? ---------+---------- aaa | t (1 row) -- History must have one entry SELECT count(*), '1' AS "expected" FROM pg_password_history ; count | expected -------+---------- 1 | 1 (1 row) DROP USER aaa; -- -- Privilege escalation regression: a non-superuser altering their own role must -- never be elevated to superuser just because credcheck auto-injects a VALID -- UNTIL clause. Only a pure password change on the caller's own role may be run -- with elevated privileges. -- CREATE USER low_priv; SET ROLE low_priv; -- Attempt to smuggle privileged options into a self ALTER ROLE; must not elevate DO $$ DECLARE escalated boolean; BEGIN BEGIN ALTER ROLE low_priv CREATEDB; EXCEPTION WHEN OTHERS THEN NULL; END; BEGIN ALTER ROLE low_priv CREATEROLE; EXCEPTION WHEN OTHERS THEN NULL; END; BEGIN ALTER ROLE low_priv BYPASSRLS; EXCEPTION WHEN OTHERS THEN NULL; END; BEGIN ALTER ROLE low_priv SUPERUSER; EXCEPTION WHEN OTHERS THEN NULL; END; BEGIN ALTER ROLE low_priv PASSWORD 'SomePass2' SUPERUSER; EXCEPTION WHEN OTHERS THEN NULL; END; SELECT rolsuper OR rolcreaterole OR rolcreatedb OR rolbypassrls INTO escalated FROM pg_roles WHERE rolname = current_role; IF escalated THEN RAISE NOTICE 'FAIL: low_priv gained privileges'; ELSE RAISE NOTICE 'PASS: no privilege escalation'; END IF; END $$; NOTICE: PASS: no privilege escalation -- A plain self password change must still be accepted (elevation still works) ALTER ROLE low_priv PASSWORD 'SomePass3'; RESET ROLE; DROP ROLE low_priv; -- -- issue #77: credcheck.password_valid_min gives newly created roles a short -- VALID UNTIL window (so users are forced to change their password quickly) -- while existing roles that change their password keep the longer -- password_valid_until window. -- SET credcheck.password_reuse_history TO 0; SET credcheck.password_reuse_interval TO 0; SET credcheck.password_valid_until TO 60; SET credcheck.password_valid_min TO 5; SET credcheck.password_valid_max TO 90; -- new role without VALID UNTIL: auto-set to now()+5 days (password_valid_min), not 60 CREATE USER remi PASSWORD 'DummY1'; SELECT rolname, rolvaliduntil between now() + '4 days'::interval and now() + '6 days'::interval AS min_window FROM pg_roles WHERE rolname='remi'; rolname | min_window ---------+------------ remi | t (1 row) -- existing role changing its password without VALID UNTIL: auto-set to now()+60 days (password_valid_until) ALTER USER remi PASSWORD 'DummY2'; SELECT rolname, rolvaliduntil between now() + '59 days'::interval and now() + '61 days'::interval AS until_window FROM pg_roles WHERE rolname='remi'; rolname | until_window ---------+-------------- remi | t (1 row) DROP USER remi; -- fail: a new role requesting a VALID UNTIL beyond password_valid_max (90 days) CREATE USER remi PASSWORD 'DummY1' VALID UNTIL '2999-01-01'; ERROR: require a VALID UNTIL option with a date NOT beyond 90 days -- fail: a new role requesting a VALID UNTIL below password_valid_min (5 days) CREATE USER remi PASSWORD 'DummY1' VALID UNTIL '2020-01-01'; ERROR: require a VALID UNTIL option with a date older than 5 days -- password_valid_min works even when password_valid_until is disabled (0) SET credcheck.password_valid_until TO 0; SET credcheck.password_valid_max TO 0; SET credcheck.password_valid_min TO 7; CREATE USER remi PASSWORD 'DummY1'; SELECT rolname, rolvaliduntil between now() + '6 days'::interval and now() + '8 days'::interval AS min_only_window FROM pg_roles WHERE rolname='remi'; rolname | min_only_window ---------+----------------- remi | t (1 row) DROP USER remi; -- non-regression: with password_valid_min explicitly disabled (0), CREATE ROLE -- must behave exactly as before issue #77 and fall back to password_valid_until. SET credcheck.password_valid_min TO 0; SET credcheck.password_valid_until TO 30; SET credcheck.password_valid_max TO 0; -- new role without VALID UNTIL: auto-set to now()+30 days (password_valid_until), the pre-#77 behaviour CREATE USER remi PASSWORD 'DummY1'; SELECT rolname, rolvaliduntil between now() + '29 days'::interval and now() + '31 days'::interval AS fallback_window FROM pg_roles WHERE rolname='remi'; rolname | fallback_window ---------+----------------- remi | t (1 row) DROP USER remi; -- and the password_valid_until floor still applies at CREATE when min is 0 CREATE USER remi PASSWORD 'DummY1' VALID UNTIL '2020-01-01'; ERROR: require a VALID UNTIL option with a date older than 30 days SET credcheck.password_valid_min TO DEFAULT; SET credcheck.password_valid_until TO DEFAULT; SET credcheck.password_valid_max TO DEFAULT; HexaCluster-credcheck-1c98317/test/expected/08_first_login.out000066400000000000000000000037731526372676500243270ustar00rootroot00000000000000LOAD 'credcheck'; SET credcheck.password_change_first_login = true; CREATE USER aaa PASSWORD 'DummY'; -- verify that credcheck_internal.force_change_password is present after user creation SELECT 1 FROM pg_catalog.pg_db_role_setting WHERE setrole='aaa'::regrole AND 'credcheck_internal.force_change_password=true'=ANY(setconfig); ?column? ---------- 1 (1 row) DROP USER aaa; -- Enforcement when a forced role actually logs in. -- Create a forced role and reconnect as it so that -- credcheck_internal.force_change_password is applied at login, exactly as in -- production (the flag comes from pg_db_role_setting, not from a manual SET). SET credcheck.password_change_first_login = true; CREATE USER forced_login PASSWORD 'DummY1passWORD!'; \c - forced_login -- Session/transaction-control statements that drivers and connection poolers -- must be able to issue while establishing a session (pgbouncer varcache_apply, -- pgbouncer server_reset_query = DISCARD ALL, JDBC/libpq startup SETs). These -- must be allowed even though the role has not changed its password yet. SET client_encoding = 'UTF8'; SET application_name = 'x'; BEGIN; COMMIT; DISCARD ALL; -- The flag stays armed after DISCARD ALL (its default comes from the role -- settings), so real data access is still blocked -- and, unlike the swallowed -- pooler-setup error, this one reaches the client. SELECT 1; ERROR: you must change your password first. -- DDL and other utility statements remain blocked too. CREATE TABLE t_force (i int); ERROR: you must change your password first. -- A forced session must not be able to clear the flag itself. SET credcheck_internal.force_change_password = false; ERROR: you must change your password first. -- The password change itself is allowed and clears the forced state. ALTER USER forced_login PASSWORD 'BrandNew3passWORD!'; SELECT 1 AS after_password_change; after_password_change ----------------------- 1 (1 row) -- Reconnect as the superuser to clean up. \c - postgres DROP USER forced_login; HexaCluster-credcheck-1c98317/test/expected/09_plpgsql.out000066400000000000000000000005571526372676500234700ustar00rootroot00000000000000LOAD 'credcheck'; SET credcheck.password_valid_until TO 4; SET credcheck.password_valid_max TO 0; DO $$ BEGIN CREATE ROLE test10 LOGIN PASSWORD '101!!010101010abc!' NOSUPERUSER INHERIT NOCREATEDB NOCREATEROLE NOREPLICATION; END $$; select 1 from pg_shadow where usename = 'test10' and valuntil IS NOT NULL; ?column? ---------- 1 (1 row) DROP ROLE test10; HexaCluster-credcheck-1c98317/test/expected/09_su_nocheck.out000066400000000000000000000005301526372676500241160ustar00rootroot00000000000000LOAD 'credcheck'; SET credcheck.password_min_upper To 4; CREATE USER aaa PASSWORD 'DuMmY4P'; -- must return an error ALTER ROLE aaa PASSWORD 'DummY2'; ERROR: password does not contain the configured credcheck.password_min_upper characters (4) SET credcheck.superuser_nocheck TO on; -- no error ALTER ROLE aaa PASSWORD 'DummY2'; DROP ROLE aaa; HexaCluster-credcheck-1c98317/test/expected/10_no_password_logging.out000066400000000000000000000043031526372676500260330ustar00rootroot00000000000000LOAD 'credcheck'; SET log_statement = 'all'; SET lc_messages = 'C'; SET credcheck.password_min_length = 8; SET credcheck.password_min_special = 0; SET credcheck.password_min_digit = 0; SET credcheck.password_min_upper = 0; SET credcheck.password_min_lower = 0; SET credcheck.password_contain_username = false; -- Enabled by default SHOW credcheck.no_password_logging; credcheck.no_password_logging ------------------------------- on (1 row) SET client_min_messages TO LOG; CREATE ROLE alice LOGIN PASSWORD 'Sup3rStr0ng!'; LOG: statement: CREATE ROLE alice LOGIN PASSWORD '............'; ALTER ROLE alice ENCRYPTED PASSWORD 'An0therGoodOne#'; LOG: statement: ALTER ROLE alice ENCRYPTED PASSWORD '...............'; -- Normal string literal with a doubled (embedded) quote. CREATE ROLE bob LOGIN PASSWORD 'pa''ss W0rdX1'; LOG: statement: CREATE ROLE bob LOGIN PASSWORD '.............'; -- Escape-string constant with a backslash escape. CREATE ROLE dave LOGIN PASSWORD E'pa\ss W0rdX2'; LOG: statement: CREATE ROLE dave LOGIN PASSWORD E'............'; -- Rejected password (too short) -> ERROR + "STATEMENT:" line. ALTER ROLE alice PASSWORD 'Zq3xK'; LOG: statement: ALTER ROLE alice PASSWORD '.....'; ERROR: password length should match the configured credcheck.password_min_length (8) -- Ordinary data mentioning "password", and a comment containing the word SELECT 'my password is hunter2' AS note; LOG: statement: SELECT 'my password is hunter2' AS note; note ------------------------ my password is hunter2 (1 row) /* set the password here */ CREATE ROLE carol LOGIN PASSWORD 'Comm3ntPwd!'; LOG: statement: /* set the password here */ CREATE ROLE carol LOGIN PASSWORD '...........'; -- Normal behavior when no_password_logging is disabled SET credcheck.no_password_logging = off; LOG: statement: SET credcheck.no_password_logging = off; ALTER ROLE alice PASSWORD 'Zq3xK'; LOG: statement: ALTER ROLE alice PASSWORD 'Zq3xK'; ERROR: password length should match the configured credcheck.password_min_length (8) DROP ROLE alice; LOG: statement: DROP ROLE alice; DROP ROLE bob; LOG: statement: DROP ROLE bob; DROP ROLE dave; LOG: statement: DROP ROLE dave; DROP ROLE carol; LOG: statement: DROP ROLE carol; HexaCluster-credcheck-1c98317/test/expected/11_max_auth_pgbouncer.out000066400000000000000000000054671526372676500256560ustar00rootroot00000000000000LOAD 'credcheck'; -- Integration points that let a connection pooler (e.g. PgBouncer with -- auth_query) feed authentication failures into credcheck's ban state, since -- pooler-terminated failures never reach the backend ClientAuthentication hook. SET credcheck.max_auth_failure = 3; CREATE ROLE ban_test LOGIN PASSWORD 'DummY1passWORD!'; SELECT pg_banned_role_reset('ban_test'); pg_banned_role_reset ---------------------- 0 (1 row) -- Not banned initially. SELECT credcheck_is_banned('ban_test'); credcheck_is_banned --------------------- f (1 row) -- Report failures through the integration point; credcheck applies its own -- credcheck.max_auth_failure threshold, exactly as for direct connections. SELECT credcheck_record_auth_failure('ban_test'); -- 1 credcheck_record_auth_failure ------------------------------- 1 (1 row) SELECT credcheck_record_auth_failure('ban_test'); -- 2 credcheck_record_auth_failure ------------------------------- 2 (1 row) SELECT credcheck_is_banned('ban_test'); -- still false (2 < 3) credcheck_is_banned --------------------- f (1 row) SELECT credcheck_record_auth_failure('ban_test'); -- 3 -> banned credcheck_record_auth_failure ------------------------------- 3 (1 row) SELECT credcheck_is_banned('ban_test'); -- true credcheck_is_banned --------------------- t (1 row) -- Does not keep incrementing once banned. SELECT credcheck_record_auth_failure('ban_test'); -- still 3 credcheck_record_auth_failure ------------------------------- 3 (1 row) -- Visible in the same shared ban view used by the in-backend path. SELECT failure_count, banned_date IS NOT NULL AS banned FROM pg_banned_role WHERE roleid='ban_test'::regrole; failure_count | banned ---------------+-------- 3 | t (1 row) -- Unknown roles are ignored (mirrors the hook). SELECT credcheck_record_auth_failure('does_not_exist'); credcheck_record_auth_failure ------------------------------- 0 (1 row) SELECT credcheck_is_banned('does_not_exist'); credcheck_is_banned --------------------- f (1 row) -- The existing reset function unbans the role. SELECT pg_banned_role_reset('ban_test'); pg_banned_role_reset ---------------------- 1 (1 row) SELECT credcheck_is_banned('ban_test'); credcheck_is_banned --------------------- f (1 row) -- Disabling the feature makes both functions inert. SET credcheck.max_auth_failure = 0; SELECT credcheck_record_auth_failure('ban_test'); credcheck_record_auth_failure ------------------------------- 0 (1 row) SELECT credcheck_is_banned('ban_test'); credcheck_is_banned --------------------- f (1 row) DROP ROLE ban_test; HexaCluster-credcheck-1c98317/test/sql/000077500000000000000000000000001526372676500177345ustar00rootroot00000000000000HexaCluster-credcheck-1c98317/test/sql/01_username.sql000077500000000000000000000072701526372676500226050ustar00rootroot00000000000000LOAD '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; SET credcheck.username_contain TO 'ba,c'; DROP USER IF EXISTS "b$auser1"; CREATE USER "b$auser1" WITH PASSWORD 'dummy'; DROP USER IF EXISTS "ba$user1"; CREATE USER "ba$user1" WITH PASSWORD 'dummy'; DROP USER IF EXISTS "ba$user1"; DROP USER IF EXISTS "c$user1"; CREATE USER "c$user1" WITH PASSWORD 'dummy'; DROP USER IF EXISTS "c$user1"; HexaCluster-credcheck-1c98317/test/sql/02_password.sql000077500000000000000000000072421526372676500226300ustar00rootroot00000000000000LOAD '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'; SET credcheck.whitelist = ''; SELECT pg_check_password('nocheck2', 'nocheck2'); SELECT pg_check_password('nocheck2', 'aaaaaaaa'); DROP USER IF EXISTS nocheck1; DROP USER IF EXISTS nocheck2; HexaCluster-credcheck-1c98317/test/sql/03_rename.sql000077500000000000000000000041041526372676500222300ustar00rootroot00000000000000--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; HexaCluster-credcheck-1c98317/test/sql/04_alter_pwd.sql000077500000000000000000000041071526372676500227460ustar00rootroot00000000000000CREATE 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; HexaCluster-credcheck-1c98317/test/sql/05_reuse_history.sql000066400000000000000000000024701526372676500236700ustar00rootroot00000000000000DROP 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(); HexaCluster-credcheck-1c98317/test/sql/06_reuse_interval.sql000066400000000000000000000042771526372676500240230ustar00rootroot00000000000000SET 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=6O1'; 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(); HexaCluster-credcheck-1c98317/test/sql/07_valid_until.sql000077500000000000000000000142461526372676500233070ustar00rootroot00000000000000LOAD '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; -- the VALID UNTIL clause must be present, if not it will be added automaticaly CREATE USER aaa PASSWORD 'DummY'; select count(*) from pg_shadow where usename = 'aaa' AND valuntil::date = (now()+'5 days'::interval)::date; DROP USER aaa; -- 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 aaa with login password 'password'; select rolname, rolvaliduntil between now() + '59 days'::interval and now() + '61 days'::interval from pg_roles WHERE rolname='aaa'; -- History must have one entry SELECT count(*), '1' AS "expected" FROM pg_password_history ; DROP USER aaa; -- -- Privilege escalation regression: a non-superuser altering their own role must -- never be elevated to superuser just because credcheck auto-injects a VALID -- UNTIL clause. Only a pure password change on the caller's own role may be run -- with elevated privileges. -- CREATE USER low_priv; SET ROLE low_priv; -- Attempt to smuggle privileged options into a self ALTER ROLE; must not elevate DO $$ DECLARE escalated boolean; BEGIN BEGIN ALTER ROLE low_priv CREATEDB; EXCEPTION WHEN OTHERS THEN NULL; END; BEGIN ALTER ROLE low_priv CREATEROLE; EXCEPTION WHEN OTHERS THEN NULL; END; BEGIN ALTER ROLE low_priv BYPASSRLS; EXCEPTION WHEN OTHERS THEN NULL; END; BEGIN ALTER ROLE low_priv SUPERUSER; EXCEPTION WHEN OTHERS THEN NULL; END; BEGIN ALTER ROLE low_priv PASSWORD 'SomePass2' SUPERUSER; EXCEPTION WHEN OTHERS THEN NULL; END; SELECT rolsuper OR rolcreaterole OR rolcreatedb OR rolbypassrls INTO escalated FROM pg_roles WHERE rolname = current_role; IF escalated THEN RAISE NOTICE 'FAIL: low_priv gained privileges'; ELSE RAISE NOTICE 'PASS: no privilege escalation'; END IF; END $$; -- A plain self password change must still be accepted (elevation still works) ALTER ROLE low_priv PASSWORD 'SomePass3'; RESET ROLE; DROP ROLE low_priv; -- -- issue #77: credcheck.password_valid_min gives newly created roles a short -- VALID UNTIL window (so users are forced to change their password quickly) -- while existing roles that change their password keep the longer -- password_valid_until window. -- SET credcheck.password_reuse_history TO 0; SET credcheck.password_reuse_interval TO 0; SET credcheck.password_valid_until TO 60; SET credcheck.password_valid_min TO 5; SET credcheck.password_valid_max TO 90; -- new role without VALID UNTIL: auto-set to now()+5 days (password_valid_min), not 60 CREATE USER remi PASSWORD 'DummY1'; SELECT rolname, rolvaliduntil between now() + '4 days'::interval and now() + '6 days'::interval AS min_window FROM pg_roles WHERE rolname='remi'; -- existing role changing its password without VALID UNTIL: auto-set to now()+60 days (password_valid_until) ALTER USER remi PASSWORD 'DummY2'; SELECT rolname, rolvaliduntil between now() + '59 days'::interval and now() + '61 days'::interval AS until_window FROM pg_roles WHERE rolname='remi'; DROP USER remi; -- fail: a new role requesting a VALID UNTIL beyond password_valid_max (90 days) CREATE USER remi PASSWORD 'DummY1' VALID UNTIL '2999-01-01'; -- fail: a new role requesting a VALID UNTIL below password_valid_min (5 days) CREATE USER remi PASSWORD 'DummY1' VALID UNTIL '2020-01-01'; -- password_valid_min works even when password_valid_until is disabled (0) SET credcheck.password_valid_until TO 0; SET credcheck.password_valid_max TO 0; SET credcheck.password_valid_min TO 7; CREATE USER remi PASSWORD 'DummY1'; SELECT rolname, rolvaliduntil between now() + '6 days'::interval and now() + '8 days'::interval AS min_only_window FROM pg_roles WHERE rolname='remi'; DROP USER remi; -- non-regression: with password_valid_min explicitly disabled (0), CREATE ROLE -- must behave exactly as before issue #77 and fall back to password_valid_until. SET credcheck.password_valid_min TO 0; SET credcheck.password_valid_until TO 30; SET credcheck.password_valid_max TO 0; -- new role without VALID UNTIL: auto-set to now()+30 days (password_valid_until), the pre-#77 behaviour CREATE USER remi PASSWORD 'DummY1'; SELECT rolname, rolvaliduntil between now() + '29 days'::interval and now() + '31 days'::interval AS fallback_window FROM pg_roles WHERE rolname='remi'; DROP USER remi; -- and the password_valid_until floor still applies at CREATE when min is 0 CREATE USER remi PASSWORD 'DummY1' VALID UNTIL '2020-01-01'; SET credcheck.password_valid_min TO DEFAULT; SET credcheck.password_valid_until TO DEFAULT; SET credcheck.password_valid_max TO DEFAULT; HexaCluster-credcheck-1c98317/test/sql/08_first_login.sql000077500000000000000000000034031526372676500233060ustar00rootroot00000000000000LOAD 'credcheck'; SET credcheck.password_change_first_login = true; CREATE USER aaa PASSWORD 'DummY'; -- verify that credcheck_internal.force_change_password is present after user creation SELECT 1 FROM pg_catalog.pg_db_role_setting WHERE setrole='aaa'::regrole AND 'credcheck_internal.force_change_password=true'=ANY(setconfig); DROP USER aaa; -- Enforcement when a forced role actually logs in. -- Create a forced role and reconnect as it so that -- credcheck_internal.force_change_password is applied at login, exactly as in -- production (the flag comes from pg_db_role_setting, not from a manual SET). SET credcheck.password_change_first_login = true; CREATE USER forced_login PASSWORD 'DummY1passWORD!'; \c - forced_login -- Session/transaction-control statements that drivers and connection poolers -- must be able to issue while establishing a session (pgbouncer varcache_apply, -- pgbouncer server_reset_query = DISCARD ALL, JDBC/libpq startup SETs). These -- must be allowed even though the role has not changed its password yet. SET client_encoding = 'UTF8'; SET application_name = 'x'; BEGIN; COMMIT; DISCARD ALL; -- The flag stays armed after DISCARD ALL (its default comes from the role -- settings), so real data access is still blocked -- and, unlike the swallowed -- pooler-setup error, this one reaches the client. SELECT 1; -- DDL and other utility statements remain blocked too. CREATE TABLE t_force (i int); -- A forced session must not be able to clear the flag itself. SET credcheck_internal.force_change_password = false; -- The password change itself is allowed and clears the forced state. ALTER USER forced_login PASSWORD 'BrandNew3passWORD!'; SELECT 1 AS after_password_change; -- Reconnect as the superuser to clean up. \c - postgres DROP USER forced_login; HexaCluster-credcheck-1c98317/test/sql/09_plpgsql.sql000077500000000000000000000005121526372676500224500ustar00rootroot00000000000000LOAD 'credcheck'; SET credcheck.password_valid_until TO 4; SET credcheck.password_valid_max TO 0; DO $$ BEGIN CREATE ROLE test10 LOGIN PASSWORD '101!!010101010abc!' NOSUPERUSER INHERIT NOCREATEDB NOCREATEROLE NOREPLICATION; END $$; select 1 from pg_shadow where usename = 'test10' and valuntil IS NOT NULL; DROP ROLE test10; HexaCluster-credcheck-1c98317/test/sql/10_no_password_logging.sql000066400000000000000000000022631526372676500250240ustar00rootroot00000000000000LOAD 'credcheck'; SET log_statement = 'all'; SET lc_messages = 'C'; SET credcheck.password_min_length = 8; SET credcheck.password_min_special = 0; SET credcheck.password_min_digit = 0; SET credcheck.password_min_upper = 0; SET credcheck.password_min_lower = 0; SET credcheck.password_contain_username = false; -- Enabled by default SHOW credcheck.no_password_logging; SET client_min_messages TO LOG; CREATE ROLE alice LOGIN PASSWORD 'Sup3rStr0ng!'; ALTER ROLE alice ENCRYPTED PASSWORD 'An0therGoodOne#'; -- Normal string literal with a doubled (embedded) quote. CREATE ROLE bob LOGIN PASSWORD 'pa''ss W0rdX1'; -- Escape-string constant with a backslash escape. CREATE ROLE dave LOGIN PASSWORD E'pa\ss W0rdX2'; -- Rejected password (too short) -> ERROR + "STATEMENT:" line. ALTER ROLE alice PASSWORD 'Zq3xK'; -- Ordinary data mentioning "password", and a comment containing the word SELECT 'my password is hunter2' AS note; /* set the password here */ CREATE ROLE carol LOGIN PASSWORD 'Comm3ntPwd!'; -- Normal behavior when no_password_logging is disabled SET credcheck.no_password_logging = off; ALTER ROLE alice PASSWORD 'Zq3xK'; DROP ROLE alice; DROP ROLE bob; DROP ROLE dave; DROP ROLE carol; HexaCluster-credcheck-1c98317/test/sql/11_max_auth_pgbouncer.sql000066400000000000000000000032301526372676500246260ustar00rootroot00000000000000LOAD 'credcheck'; -- Integration points that let a connection pooler (e.g. PgBouncer with -- auth_query) feed authentication failures into credcheck's ban state, since -- pooler-terminated failures never reach the backend ClientAuthentication hook. SET credcheck.max_auth_failure = 3; CREATE ROLE ban_test LOGIN PASSWORD 'DummY1passWORD!'; SELECT pg_banned_role_reset('ban_test'); -- Not banned initially. SELECT credcheck_is_banned('ban_test'); -- Report failures through the integration point; credcheck applies its own -- credcheck.max_auth_failure threshold, exactly as for direct connections. SELECT credcheck_record_auth_failure('ban_test'); -- 1 SELECT credcheck_record_auth_failure('ban_test'); -- 2 SELECT credcheck_is_banned('ban_test'); -- still false (2 < 3) SELECT credcheck_record_auth_failure('ban_test'); -- 3 -> banned SELECT credcheck_is_banned('ban_test'); -- true -- Does not keep incrementing once banned. SELECT credcheck_record_auth_failure('ban_test'); -- still 3 -- Visible in the same shared ban view used by the in-backend path. SELECT failure_count, banned_date IS NOT NULL AS banned FROM pg_banned_role WHERE roleid='ban_test'::regrole; -- Unknown roles are ignored (mirrors the hook). SELECT credcheck_record_auth_failure('does_not_exist'); SELECT credcheck_is_banned('does_not_exist'); -- The existing reset function unbans the role. SELECT pg_banned_role_reset('ban_test'); SELECT credcheck_is_banned('ban_test'); -- Disabling the feature makes both functions inert. SET credcheck.max_auth_failure = 0; SELECT credcheck_record_auth_failure('ban_test'); SELECT credcheck_is_banned('ban_test'); DROP ROLE ban_test; HexaCluster-credcheck-1c98317/updates/000077500000000000000000000000001526372676500176235ustar00rootroot00000000000000HexaCluster-credcheck-1c98317/updates/credcheck--0.1.0--0.1.1.sql000066400000000000000000000000001526372676500234640ustar00rootroot00000000000000HexaCluster-credcheck-1c98317/updates/credcheck--0.1.1--0.2.0.sql000066400000000000000000000000001526372676500234650ustar00rootroot00000000000000HexaCluster-credcheck-1c98317/updates/credcheck--0.2.0--1.0.0.sql000066400000000000000000000033101526372676500234730ustar00rootroot00000000000000-- 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; HexaCluster-credcheck-1c98317/updates/credcheck--1.0.0--1.1.0.sql000066400000000000000000000000001526372676500234640ustar00rootroot00000000000000HexaCluster-credcheck-1c98317/updates/credcheck--1.1.0--1.2.0.sql000066400000000000000000000000001526372676500234660ustar00rootroot00000000000000HexaCluster-credcheck-1c98317/updates/credcheck--1.2.0--2.0.0.sql000066400000000000000000000024351526372676500235040ustar00rootroot00000000000000-- 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; HexaCluster-credcheck-1c98317/updates/credcheck--2.0.0--2.1.0.sql000066400000000000000000000003121526372676500234740ustar00rootroot00000000000000-- 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 HexaCluster-credcheck-1c98317/updates/credcheck--2.1.0--2.2.0.sql000066400000000000000000000003121526372676500234760ustar00rootroot00000000000000-- 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 HexaCluster-credcheck-1c98317/updates/credcheck--2.2.0--2.3.0.sql000066400000000000000000000003121526372676500235000ustar00rootroot00000000000000-- 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 HexaCluster-credcheck-1c98317/updates/credcheck--2.3.0--2.4.0.sql000066400000000000000000000002201526372676500235000ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2024 HexaCluster Corp - All rights reserved. -- No SQL change to apply in this version HexaCluster-credcheck-1c98317/updates/credcheck--2.4.0--2.5.0.sql000066400000000000000000000002201526372676500235020ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2024 HexaCluster Corp - All rights reserved. -- No SQL change to apply in this version HexaCluster-credcheck-1c98317/updates/credcheck--2.5.0--2.6.0.sql000066400000000000000000000002201526372676500235040ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2024 HexaCluster Corp - All rights reserved. -- No SQL change to apply in this version HexaCluster-credcheck-1c98317/updates/credcheck--2.6.0--2.7.0.sql000066400000000000000000000002201526372676500235060ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2024 HexaCluster Corp - All rights reserved. -- No SQL change to apply in this version HexaCluster-credcheck-1c98317/updates/credcheck--2.7.0--2.8.0.sql000066400000000000000000000002201526372676500235100ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2024 HexaCluster Corp - All rights reserved. -- No SQL change to apply in this version HexaCluster-credcheck-1c98317/updates/credcheck--2.8.0--3.0.0.sql000066400000000000000000000002251526372676500235070ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2024-2026 HexaCluster Corp - All rights reserved. -- No SQL change to apply in this version HexaCluster-credcheck-1c98317/updates/credcheck--3.0.0--4.0.0.sql000066400000000000000000000002251526372676500235010ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2024-2026 HexaCluster Corp - All rights reserved. -- No SQL change to apply in this version HexaCluster-credcheck-1c98317/updates/credcheck--4.0.0--4.1.0.sql000066400000000000000000000005421526372676500235050ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2024-2026 HexaCluster Corp - All rights reserved. DROP SCHEMA credcheck; -- Show rolename instead of role oid DROP VIEW pg_banned_role; CREATE OR REPLACE VIEW pg_banned_role AS SELECT roleid::regrole, failure_count, banned_date FROM pg_banned_role(); GRANT SELECT ON pg_banned_role TO PUBLIC; HexaCluster-credcheck-1c98317/updates/credcheck--4.1.0--4.2.0.sql000066400000000000000000000002201526372676500235000ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2024-2026 HexaCluster Corp - All rights reserved. -- Nothing to do, only library change HexaCluster-credcheck-1c98317/updates/credcheck--4.2.0--4.3.0.sql000066400000000000000000000002201526372676500235020ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2024-2026 HexaCluster Corp - All rights reserved. -- Nothing to do, only library change HexaCluster-credcheck-1c98317/updates/credcheck--4.3.0--4.4.0.sql000066400000000000000000000002201526372676500235040ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2024-2026 HexaCluster Corp - All rights reserved. -- Nothing to do, only library change HexaCluster-credcheck-1c98317/updates/credcheck--4.4.0--4.5.0.sql000066400000000000000000000002201526372676500235060ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2024-2026 HexaCluster Corp - All rights reserved. -- Nothing to do, only library change HexaCluster-credcheck-1c98317/updates/credcheck--4.5.0--4.6.0.sql000066400000000000000000000002201526372676500235100ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2024-2026 HexaCluster Corp - All rights reserved. -- Nothing to do, only library change HexaCluster-credcheck-1c98317/updates/credcheck--4.6.0--4.7.0.sql000066400000000000000000000002201526372676500235120ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2024-2026 HexaCluster Corp - All rights reserved. -- Nothing to do, only library change HexaCluster-credcheck-1c98317/updates/credcheck--4.7.0--5.0.0.sql000066400000000000000000000002201526372676500235050ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2024-2026 HexaCluster Corp - All rights reserved. -- Nothing to do, only library change HexaCluster-credcheck-1c98317/updates/credcheck--5.0.0--5.1.0.sql000066400000000000000000000011301526372676500235010ustar00rootroot00000000000000-- credcheck extension for PostgreSQL -- Copyright (c) 2024-2026 HexaCluster Corp - All rights reserved. ---- -- Check a candidate password (and the associated username) against the -- configured credcheck username/password policy without creating or -- altering a role. Raises the same error as CREATE/ALTER ROLE would when -- the policy is violated, and returns true when the password is accepted. -- Note: the password reuse policy is not evaluated here ---- CREATE FUNCTION pg_check_password( IN username name, IN password text ) RETURNS boolean AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; HexaCluster-credcheck-1c98317/updates/credcheck--5.1.0--6.0.0.sql000066400000000000000000000043411526372676500235110ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via ALTER EXTENSION \echo Use "ALTER EXTENSION credcheck UPDATE TO '6.0.0'" to load this file. \quit ---- -- lastlog: utmp/wtmp-like login history. The data is kept in shared memory -- and a memory-mapped file managed by the C code; this only exposes it. ---- CREATE FUNCTION credcheck_lastlog ( OUT type text, OUT username text, OUT pid integer, OUT client_addr text, OUT client_port integer, OUT login_time timestamp with time zone, OUT logout_time timestamp with time zone, OUT duration interval, OUT state text, OUT query text ) RETURNS SETOF record AS 'MODULE_PATHNAME', 'credcheck_lastlog' LANGUAGE C VOLATILE; CREATE VIEW pg_lastlog AS SELECT * FROM credcheck_lastlog(); REVOKE ALL ON FUNCTION credcheck_lastlog() FROM PUBLIC; REVOKE ALL ON pg_lastlog FROM PUBLIC; GRANT SELECT ON pg_lastlog TO pg_read_all_stats; REVOKE ALL ON FUNCTION pg_password_history() FROM PUBLIC; REVOKE ALL ON pg_password_history FROM PUBLIC; GRANT SELECT ON pg_password_history TO pg_read_all_stats; ---- -- Check a candidate password (and the associated username) against the -- configured credcheck username/password policy without creating or -- altering a role. Raises the same error as CREATE/ALTER ROLE would when -- the policy is violated, and returns true when the password is accepted. -- Note: the password reuse policy is not evaluated here ---- CREATE FUNCTION pg_check_password( IN username name, IN password text ) RETURNS boolean AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; ---- -- lastlog: utmp/wtmp-like login history (see credcheck.lastlog GUC). ---- CREATE FUNCTION credcheck_lastlog ( OUT type text, OUT username text, OUT pid integer, OUT client_addr text, OUT client_port integer, OUT login_time timestamp with time zone, OUT logout_time timestamp with time zone, OUT duration interval, OUT state text, OUT query text ) RETURNS SETOF record AS 'MODULE_PATHNAME', 'credcheck_lastlog' LANGUAGE C VOLATILE; CREATE VIEW pg_lastlog AS SELECT * FROM credcheck_lastlog(); REVOKE ALL ON FUNCTION credcheck_lastlog() FROM PUBLIC; REVOKE ALL ON pg_lastlog FROM PUBLIC; GRANT SELECT ON pg_lastlog TO pg_read_all_stats;