Add a hook to CREATE/ALTER ROLE to allow an external module to check the
authorTom Lane
Wed, 18 Nov 2009 21:57:56 +0000 (21:57 +0000)
committerTom Lane
Wed, 18 Nov 2009 21:57:56 +0000 (21:57 +0000)
strength of database passwords, and create a sample implementation of
such a hook as a new contrib module "passwordcheck".

Laurenz Albe, reviewed by Takahiro Itagaki

contrib/Makefile
contrib/README
contrib/passwordcheck/Makefile [new file with mode: 0644]
contrib/passwordcheck/passwordcheck.c [new file with mode: 0644]
doc/src/sgml/contrib.sgml
doc/src/sgml/filelist.sgml
doc/src/sgml/passwordcheck.sgml [new file with mode: 0644]
src/backend/commands/user.c
src/include/commands/user.h

index 8543b5287fe57c4bf267706eff389d0ef608d304..0b208851c16543ad32f3502664f1681654d04f23 100644 (file)
@@ -1,4 +1,4 @@
-# $PostgreSQL: pgsql/contrib/Makefile,v 1.89 2009/08/18 10:34:39 teodor Exp $
+# $PostgreSQL: pgsql/contrib/Makefile,v 1.90 2009/11/18 21:57:56 tgl Exp $
 
 subdir = contrib
 top_builddir = ..
@@ -25,6 +25,7 @@ SUBDIRS = \
        ltree       \
        oid2name    \
        pageinspect \
+       passwordcheck   \
        pg_buffercache  \
        pg_freespacemap \
        pg_standby  \
index a8396a5bfadf513ab5133da0d70a27a71ca5f961..ff35c08a700501cd9583218fe4c609765c21d5d1 100644 (file)
@@ -104,6 +104,10 @@ pageinspect -
    Allows inspection of database pages
    Heikki Linnakangas 
 
+passwordcheck -
+   Simple password strength checker
+   Laurenz Albe 
+
 pg_buffercache -
    Real time queries on the shared buffer cache
    by Mark Kirkwood 
diff --git a/contrib/passwordcheck/Makefile b/contrib/passwordcheck/Makefile
new file mode 100644 (file)
index 0000000..1d2c8b1
--- /dev/null
@@ -0,0 +1,19 @@
+# $PostgreSQL: pgsql/contrib/passwordcheck/Makefile,v 1.1 2009/11/18 21:57:56 tgl Exp $
+
+MODULE_big = passwordcheck
+OBJS = passwordcheck.o
+
+# uncomment the following two lines to enable cracklib support
+# PG_CPPFLAGS = -DUSE_CRACKLIB '-DCRACKLIB_DICTPATH="/usr/lib/cracklib_dict"'
+# SHLIB_LINK = -lcrack
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/passwordcheck
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/contrib/passwordcheck/passwordcheck.c b/contrib/passwordcheck/passwordcheck.c
new file mode 100644 (file)
index 0000000..88055e3
--- /dev/null
@@ -0,0 +1,147 @@
+/*-------------------------------------------------------------------------
+ *
+ * passwordcheck.c
+ *
+ *
+ * Copyright (c) 2009, PostgreSQL Global Development Group
+ *
+ * Author: Laurenz Albe 
+ *
+ * IDENTIFICATION
+ *   $PostgreSQL: pgsql/contrib/passwordcheck/passwordcheck.c,v 1.1 2009/11/18 21:57:56 tgl Exp $
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include 
+
+#ifdef USE_CRACKLIB
+#include 
+#endif
+
+#include "commands/user.h"
+#include "fmgr.h"
+#include "libpq/md5.h"
+
+
+PG_MODULE_MAGIC;
+
+/* passwords shorter than this will be rejected */
+#define MIN_PWD_LENGTH 8
+
+extern void _PG_init(void);
+
+/*
+ * check_password
+ *
+ * performs checks on an encrypted or unencrypted password
+ * ereport's if not acceptable
+ *
+ * username: name of role being created or changed
+ * password: new password (possibly already encrypted)
+ * password_type: PASSWORD_TYPE_PLAINTEXT or PASSWORD_TYPE_MD5 (there
+ *         could be other encryption schemes in future)
+ * validuntil_time: password expiration time, as a timestamptz Datum
+ * validuntil_null: true if password expiration time is NULL
+ *
+ * This sample implementation doesn't pay any attention to the password
+ * expiration time, but you might wish to insist that it be non-null and
+ * not too far in the future.
+ */
+static void
+check_password(const char *username,
+              const char *password,
+              int password_type,
+              Datum validuntil_time,
+              bool validuntil_null)
+{
+   int         namelen = strlen(username);
+   int         pwdlen = strlen(password);
+   char        encrypted[MD5_PASSWD_LEN + 1];
+   int         i;
+   bool        pwd_has_letter,
+               pwd_has_nonletter;
+
+   switch (password_type)
+   {
+       case PASSWORD_TYPE_MD5:
+           /*
+            * Unfortunately we cannot perform exhaustive checks on
+            * encrypted passwords - we are restricted to guessing.
+            * (Alternatively, we could insist on the password being
+            * presented non-encrypted, but that has its own security
+            * disadvantages.)
+            *
+            * We only check for username = password.
+            */
+           if (!pg_md5_encrypt(username, username, namelen, encrypted))
+               elog(ERROR, "password encryption failed");
+           if (strcmp(password, encrypted) == 0)
+               ereport(ERROR,
+                       (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                        errmsg("password must not contain user name")));
+           break;
+
+       case PASSWORD_TYPE_PLAINTEXT:
+           /*
+            * For unencrypted passwords we can perform better checks
+            */
+
+           /* enforce minimum length */
+           if (pwdlen < MIN_PWD_LENGTH)
+               ereport(ERROR,
+                       (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                        errmsg("password is too short")));
+
+           /* check if the password contains the username */
+           if (strstr(password, username))
+               ereport(ERROR,
+                       (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                        errmsg("password must not contain user name")));
+
+           /* check if the password contains both letters and non-letters */
+           pwd_has_letter = false;
+           pwd_has_nonletter = false;
+           for (i = 0; i < pwdlen; i++)
+           {
+               /*
+                * isalpha() does not work for multibyte encodings
+                * but let's consider non-ASCII characters non-letters
+                */
+               if (isalpha((unsigned char) password[i]))
+                   pwd_has_letter = true;
+               else
+                   pwd_has_nonletter = true;
+           }
+           if (!pwd_has_letter || !pwd_has_nonletter)
+               ereport(ERROR,
+                       (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                        errmsg("password must contain both letters and nonletters")));
+
+#ifdef USE_CRACKLIB
+           /* call cracklib to check password */
+           if (FascistCheck(password, CRACKLIB_DICTPATH))
+               ereport(ERROR,
+                       (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                        errmsg("password is easily cracked")));
+#endif
+           break;
+
+       default:
+           elog(ERROR, "unrecognized password type: %d", password_type);
+           break;
+   }
+
+   /* all checks passed, password is ok */
+}
+
+/*
+ * Module initialization function
+ */
+void
+_PG_init(void)
+{
+   /* activate password checks when the module is loaded */
+   check_password_hook = check_password;
+}
index cffbc55249c8e111cbcfa93e6abd7ed5c88d33bf..2895e6c170fcace2e6a4de00d4e6a73c961bf026 100644 (file)
@@ -1,4 +1,4 @@
-
+
 
 
  Additional Supplied Modules
@@ -98,6 +98,7 @@ psql -d dbname -f SHAREDIR/contrib/module.sql
  <ree;
  &oid2name;
  &pageinspect;
+ &passwordcheck;
  &pgbench;
  &pgbuffercache;
  &pgcrypto;
index bee66008b6695a4dd99d5f48980a56ae810f052a..2ceee79cb98f14f2f970524df343d02c66e49d2f 100644 (file)
@@ -1,4 +1,4 @@
-
+
 
 
 
 
 
 
+
 
 
 
diff --git a/doc/src/sgml/passwordcheck.sgml b/doc/src/sgml/passwordcheck.sgml
new file mode 100644 (file)
index 0000000..e46e3df
--- /dev/null
@@ -0,0 +1,62 @@
+
+
+
passwordcheck
+
+  passwordcheck
+
+  The passwordcheck module checks users' passwords
+  whenever they are set with
+   or
+  .
+  If a password is considered too weak, it will be rejected and
+  the command will terminate with an error.
+
+  To enable this module, add '$libdir/passwordcheck'
+  to  in
+  postgresql.conf, then restart the server.
+
+  You can adapt this module to your needs by changing the source code.
+  For example, you can use
+  CrackLib
+  to check passwords — this only requires uncommenting
+  two lines in the Makefile and rebuilding the
+  module.  (We cannot include CrackLib
+  by default for license reasons.)
+  Without CrackLib, the module enforces a few
+  simple rules for password strength, which you can modify or extend
+  as you see fit.
+
+  
+   To prevent unencrypted passwords from being sent across the network,
+   written to the server log or otherwise stolen by a database administrator,
+   PostgreSQL allows the user to supply
+   pre-encrypted passwords. Many client programs make use of this
+   functionality and encrypt the password before sending it to the server.
+  
+  
+   This limits the usefulness of the passwordcheck
+   module, because in that case it can only try to guess the password.
+   For this reason, passwordcheck is not
+   recommendable if your security requirements are high.
+   It is more secure to use an external authentication method such as Kerberos
+   (see ) than to rely on
+   passwords within the database.
+  
+  
+   Alternatively, you could modify passwordcheck
+   to reject pre-encrypted passwords, but forcing users to set their
+   passwords in clear text carries its own security risks.
+  
+
+
index ef546cf3602b9572e8091c2f804efc724847b7fc..66560d7a5b7176413b596ce60cbc446c236bbf72 100644 (file)
@@ -6,7 +6,7 @@
  * Portions Copyright (c) 1996-2009, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
  *
- * $PostgreSQL: pgsql/src/backend/commands/user.c,v 1.189 2009/10/07 22:14:19 alvherre Exp $
+ * $PostgreSQL: pgsql/src/backend/commands/user.c,v 1.190 2009/11/18 21:57:56 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
 #include "utils/tqual.h"
 
 
+/* GUC parameter */
 extern bool Password_encryption;
 
+/* Hook to check passwords in CreateRole() and AlterRole() */
+check_password_hook_type check_password_hook = NULL;
+
 static List *roleNamesToIds(List *memberNames);
 static void AddRoleMems(const char *rolename, Oid roleid,
            List *memberNames, List *memberIds,
@@ -96,6 +100,8 @@ CreateRole(CreateRoleStmt *stmt)
    List       *rolemembers = NIL;      /* roles to be members of this role */
    List       *adminmembers = NIL;     /* roles to be admins of this role */
    char       *validUntil = NULL;      /* time the login is valid until */
+   Datum       validUntil_datum;       /* same, as timestamptz Datum */
+   bool        validUntil_null;
    DefElem    *dpassword = NULL;
    DefElem    *dissuper = NULL;
    DefElem    *dinherit = NULL;
@@ -298,6 +304,31 @@ CreateRole(CreateRoleStmt *stmt)
                 errmsg("role \"%s\" already exists",
                        stmt->role)));
 
+   /* Convert validuntil to internal form */
+   if (validUntil)
+   {
+       validUntil_datum = DirectFunctionCall3(timestamptz_in,
+                                              CStringGetDatum(validUntil),
+                                              ObjectIdGetDatum(InvalidOid),
+                                              Int32GetDatum(-1));
+       validUntil_null = false;
+   }
+   else
+   {
+       validUntil_datum = (Datum) 0;
+       validUntil_null = true;
+   }
+
+   /*
+    * Call the password checking hook if there is one defined
+    */
+   if (check_password_hook && password)
+       (*check_password_hook) (stmt->role,
+                               password,
+                               isMD5(password) ? PASSWORD_TYPE_MD5 : PASSWORD_TYPE_PLAINTEXT,
+                               validUntil_datum,
+                               validUntil_null);
+
    /*
     * Build a tuple to insert
     */
@@ -333,15 +364,8 @@ CreateRole(CreateRoleStmt *stmt)
    else
        new_record_nulls[Anum_pg_authid_rolpassword - 1] = true;
 
-   if (validUntil)
-       new_record[Anum_pg_authid_rolvaliduntil - 1] =
-           DirectFunctionCall3(timestamptz_in,
-                               CStringGetDatum(validUntil),
-                               ObjectIdGetDatum(InvalidOid),
-                               Int32GetDatum(-1));
-
-   else
-       new_record_nulls[Anum_pg_authid_rolvaliduntil - 1] = true;
+   new_record[Anum_pg_authid_rolvaliduntil - 1] = validUntil_datum;
+   new_record_nulls[Anum_pg_authid_rolvaliduntil - 1] = validUntil_null;
 
    tuple = heap_form_tuple(pg_authid_dsc, new_record, new_record_nulls);
 
@@ -419,6 +443,8 @@ AlterRole(AlterRoleStmt *stmt)
    int         connlimit = -1; /* maximum connections allowed */
    List       *rolemembers = NIL;      /* roles to be added/removed */
    char       *validUntil = NULL;      /* time the login is valid until */
+   Datum       validUntil_datum;       /* same, as timestamptz Datum */
+   bool        validUntil_null;
    DefElem    *dpassword = NULL;
    DefElem    *dissuper = NULL;
    DefElem    *dinherit = NULL;
@@ -587,6 +613,33 @@ AlterRole(AlterRoleStmt *stmt)
                     errmsg("permission denied")));
    }
 
+   /* Convert validuntil to internal form */
+   if (validUntil)
+   {
+       validUntil_datum = DirectFunctionCall3(timestamptz_in,
+                                              CStringGetDatum(validUntil),
+                                              ObjectIdGetDatum(InvalidOid),
+                                              Int32GetDatum(-1));
+       validUntil_null = false;
+   }
+   else
+   {
+       /* fetch existing setting in case hook needs it */
+       validUntil_datum = SysCacheGetAttr(AUTHNAME, tuple,
+                                          Anum_pg_authid_rolvaliduntil,
+                                          &validUntil_null);
+   }
+
+   /*
+    * Call the password checking hook if there is one defined
+    */
+   if (check_password_hook && password)
+       (*check_password_hook) (stmt->role,
+                               password,
+                               isMD5(password) ? PASSWORD_TYPE_MD5 : PASSWORD_TYPE_PLAINTEXT,
+                               validUntil_datum,
+                               validUntil_null);
+
    /*
     * Build an updated tuple, perusing the information just obtained
     */
@@ -666,15 +719,9 @@ AlterRole(AlterRoleStmt *stmt)
    }
 
    /* valid until */
-   if (validUntil)
-   {
-       new_record[Anum_pg_authid_rolvaliduntil - 1] =
-           DirectFunctionCall3(timestamptz_in,
-                               CStringGetDatum(validUntil),
-                               ObjectIdGetDatum(InvalidOid),
-                               Int32GetDatum(-1));
-       new_record_repl[Anum_pg_authid_rolvaliduntil - 1] = true;
-   }
+   new_record[Anum_pg_authid_rolvaliduntil - 1] = validUntil_datum;
+   new_record_nulls[Anum_pg_authid_rolvaliduntil - 1] = validUntil_null;
+   new_record_repl[Anum_pg_authid_rolvaliduntil - 1] = true;
 
    new_tuple = heap_modify_tuple(tuple, pg_authid_dsc, new_record,
                                  new_record_nulls, new_record_repl);
index 01fb92c354619db4d58d2d55f4cf524596dfc484..ffef486b8362a864d866c1c8aaf4cc36a9873706 100644 (file)
@@ -4,7 +4,7 @@
  *   Commands for manipulating roles (formerly called users).
  *
  *
- * $PostgreSQL: pgsql/src/include/commands/user.h,v 1.30 2006/10/04 00:30:08 momjian Exp $
+ * $PostgreSQL: pgsql/src/include/commands/user.h,v 1.31 2009/11/18 21:57:56 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
 #include "nodes/parsenodes.h"
 
 
+/* Hook to check passwords in CreateRole() and AlterRole() */
+#define PASSWORD_TYPE_PLAINTEXT        0
+#define PASSWORD_TYPE_MD5          1
+
+typedef void (*check_password_hook_type) (const char *username, const char *password, int password_type, Datum validuntil_time, bool validuntil_null);
+
+extern PGDLLIMPORT check_password_hook_type check_password_hook;
+
 extern void CreateRole(CreateRoleStmt *stmt);
 extern void AlterRole(AlterRoleStmt *stmt);
 extern void AlterRoleSet(AlterRoleSetStmt *stmt);