]> granicus.if.org Git - postgresql/commitdiff
Add a hook to CREATE/ALTER ROLE to allow an external module to check the
authorTom Lane <tgl@sss.pgh.pa.us>
Wed, 18 Nov 2009 21:57:56 +0000 (21:57 +0000)
committerTom Lane <tgl@sss.pgh.pa.us>
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 <heikki@enterprisedb.com>
 
+passwordcheck -
+       Simple password strength checker
+       Laurenz Albe <laurenz.albe@wien.gv.at>
+
 pg_buffercache -
        Real time queries on the shared buffer cache
        by Mark Kirkwood <markir@paradise.net.nz>
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 <laurenz.albe@wien.gv.at>
+ *
+ * IDENTIFICATION
+ *       $PostgreSQL: pgsql/contrib/passwordcheck/passwordcheck.c,v 1.1 2009/11/18 21:57:56 tgl Exp $
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include <ctype.h>
+
+#ifdef USE_CRACKLIB
+#include <crack.h>
+#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 @@
-<!-- $PostgreSQL: pgsql/doc/src/sgml/contrib.sgml,v 1.14 2009/08/18 10:34:39 teodor Exp $ -->
+<!-- $PostgreSQL: pgsql/doc/src/sgml/contrib.sgml,v 1.15 2009/11/18 21:57:56 tgl Exp $ -->
 
 <appendix id="contrib">
  <title>Additional Supplied Modules</title>
@@ -98,6 +98,7 @@ psql -d dbname -f <replaceable>SHAREDIR</>/contrib/<replaceable>module</>.sql
  &ltree;
  &oid2name;
  &pageinspect;
+ &passwordcheck;
  &pgbench;
  &pgbuffercache;
  &pgcrypto;
index bee66008b6695a4dd99d5f48980a56ae810f052a..2ceee79cb98f14f2f970524df343d02c66e49d2f 100644 (file)
@@ -1,4 +1,4 @@
-<!-- $PostgreSQL: pgsql/doc/src/sgml/filelist.sgml,v 1.64 2009/08/18 10:34:39 teodor Exp $ -->
+<!-- $PostgreSQL: pgsql/doc/src/sgml/filelist.sgml,v 1.65 2009/11/18 21:57:56 tgl Exp $ -->
 
 <!entity history    SYSTEM "history.sgml">
 <!entity info       SYSTEM "info.sgml">
 <!entity ltree           SYSTEM "ltree.sgml">
 <!entity oid2name        SYSTEM "oid2name.sgml">
 <!entity pageinspect     SYSTEM "pageinspect.sgml">
+<!entity passwordcheck   SYSTEM "passwordcheck.sgml">
 <!entity pgbench         SYSTEM "pgbench.sgml">
 <!entity pgbuffercache   SYSTEM "pgbuffercache.sgml">
 <!entity pgcrypto        SYSTEM "pgcrypto.sgml">
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 @@
+<!-- $PostgreSQL: pgsql/doc/src/sgml/passwordcheck.sgml,v 1.1 2009/11/18 21:57:56 tgl Exp $ -->
+
+<sect1 id="passwordcheck">
+ <title>passwordcheck</title>
+
+ <indexterm zone="passwordcheck">
+  <primary>passwordcheck</primary>
+ </indexterm>
+
+ <para>
+  The <filename>passwordcheck</filename> module checks users' passwords
+  whenever they are set with
+  <xref linkend="SQL-CREATEROLE" endterm="SQL-CREATEROLE-title"> or
+  <xref linkend="SQL-ALTERROLE" endterm="SQL-ALTERROLE-title">.
+  If a password is considered too weak, it will be rejected and
+  the command will terminate with an error.
+ </para>
+
+ <para>
+  To enable this module, add <literal>'$libdir/passwordcheck'</literal>
+  to <xref linkend="guc-shared-preload-libraries"> in
+  <filename>postgresql.conf</filename>, then restart the server.
+ </para>
+
+ <para>
+  You can adapt this module to your needs by changing the source code.
+  For example, you can use
+  <ulink url="http://sourceforge.net/projects/cracklib/">CrackLib</ulink>
+  to check passwords &mdash; this only requires uncommenting
+  two lines in the <filename>Makefile</filename> and rebuilding the
+  module.  (We cannot include <productname>CrackLib</productname>
+  by default for license reasons.)
+  Without <productname>CrackLib</productname>, the module enforces a few
+  simple rules for password strength, which you can modify or extend
+  as you see fit.
+ </para>
+
+ <caution>
+  <para>
+   To prevent unencrypted passwords from being sent across the network,
+   written to the server log or otherwise stolen by a database administrator,
+   <productname>PostgreSQL</productname> 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.
+  </para>
+  <para>
+   This limits the usefulness of the <filename>passwordcheck</filename>
+   module, because in that case it can only try to guess the password.
+   For this reason, <filename>passwordcheck</filename> is not
+   recommendable if your security requirements are high.
+   It is more secure to use an external authentication method such as Kerberos
+   (see <xref linkend="client-authentication">) than to rely on
+   passwords within the database.
+  </para>
+  <para>
+   Alternatively, you could modify <filename>passwordcheck</filename>
+   to reject pre-encrypted passwords, but forcing users to set their
+   passwords in clear text carries its own security risks.
+  </para>
+ </caution>
+
+</sect1>
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);