]> git.ipfire.org Git - thirdparty/openldap.git/commitdiff
Proposing ppolicy extended module for OpenLDAP (issue #7832)
authorDavid Coutadeur <david.coutadeur@gmail.com>
Tue, 23 Feb 2021 20:23:23 +0000 (21:23 +0100)
committerQuanah Gibson-Mount <quanah@openldap.org>
Fri, 26 Mar 2021 15:01:36 +0000 (15:01 +0000)
contrib/slapd-modules/ppm/INSTALL.md [new file with mode: 0644]
contrib/slapd-modules/ppm/LICENSE [new file with mode: 0644]
contrib/slapd-modules/ppm/Makefile [new file with mode: 0644]
contrib/slapd-modules/ppm/NOTICES [new file with mode: 0644]
contrib/slapd-modules/ppm/README.md [new file with mode: 0644]
contrib/slapd-modules/ppm/ppm.c [new file with mode: 0644]
contrib/slapd-modules/ppm/ppm.conf [new file with mode: 0644]
contrib/slapd-modules/ppm/ppm.h [new file with mode: 0644]
contrib/slapd-modules/ppm/ppm_test.c [new file with mode: 0644]
contrib/slapd-modules/ppm/unit_tests.sh [new file with mode: 0755]

diff --git a/contrib/slapd-modules/ppm/INSTALL.md b/contrib/slapd-modules/ppm/INSTALL.md
new file mode 100644 (file)
index 0000000..9263354
--- /dev/null
@@ -0,0 +1,44 @@
+INSTALLATION
+============
+
+Build dependencies
+------------------
+OpenLDAP sources must be available. For an easier build, copy all ppm module
+into contrib/slapd-modules OpenLDAP source directory.
+
+Build
+-----
+Be sure to have copied ppm module into contrib/slapd-modules OpenLDAP source
+directory.
+
+Adapt the Makefile command to indicate:
+OLDAP_SOURCES : should point to OpenLDAP source directory
+CONFIG: where the ppm.conf example configuration file will finally stand
+        note: ppm configuration now lies into pwdCheckModuleArg password policy attribute
+              provided config file is only helpful as an example or for testing
+LIBDIR: where the library will be installed
+DEBUG: If defined, ppm logs its actions with syslog
+
+If necessary, you can also adapt some OpenLDAP source directories (if changed):
+LDAP_INC : OpenLDAP headers directory
+LDAP_LIBS : OpenLDAP built libraries directory
+
+then type:
+
+```
+make clean
+make CONFIG=/etc/openldap/ppm.conf OLDAP_SOURCES=../../..
+make test
+make install CONFIG=/etc/openldap/ppm.conf LIBDIR=/usr/lib/openldap
+```
+
+
+For LTB build, use rather:
+
+```
+make clean
+make "CONFIG=/usr/local/openldap/etc/openldap/ppm.conf" "OLDAP_SOURCES=.."
+make test
+make install CONFIG=/usr/local/openldap/etc/openldap/ppm.conf LIBDIR=/usr/local/openldap/lib64
+```
+
diff --git a/contrib/slapd-modules/ppm/LICENSE b/contrib/slapd-modules/ppm/LICENSE
new file mode 100644 (file)
index 0000000..03f692b
--- /dev/null
@@ -0,0 +1,50 @@
+OpenLDAP Public License
+
+The OpenLDAP Public License
+  Version 2.8.1, 25 November 2003
+
+Redistribution and use of this software and associated documentation
+("Software"), with or without modification, are permitted provided
+that the following conditions are met:
+
+1. Redistributions in source form must retain copyright statements
+   and notices,
+
+2. Redistributions in binary form must reproduce applicable copyright
+   statements and notices, this list of conditions, and the following
+   disclaimer in the documentation and/or other materials provided
+   with the distribution, and
+
+3. Redistributions must contain a verbatim copy of this document.
+
+The OpenLDAP Foundation may revise this license from time to time.
+Each revision is distinguished by a version number.  You may use
+this Software under terms of this license revision or under the
+terms of any subsequent revision of the license.
+
+THIS SOFTWARE IS PROVIDED BY THE OPENLDAP FOUNDATION AND ITS
+CONTRIBUTORS ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
+INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT
+SHALL THE OPENLDAP FOUNDATION, ITS CONTRIBUTORS, OR THE AUTHOR(S)
+OR OWNER(S) OF THE SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+
+The names of the authors and copyright holders must not be used in
+advertising or otherwise to promote the sale, use or other dealing
+in this Software without specific, written prior permission.  Title
+to copyright in this Software shall at all times remain with copyright
+holders.
+
+OpenLDAP is a registered trademark of the OpenLDAP Foundation.
+
+Copyright 1999-2003 The OpenLDAP Foundation, Redwood City,
+California, USA.  All rights reserved.  Permission to copy and
+distribute verbatim copies of this document is granted.
+
diff --git a/contrib/slapd-modules/ppm/Makefile b/contrib/slapd-modules/ppm/Makefile
new file mode 100644 (file)
index 0000000..ac58210
--- /dev/null
@@ -0,0 +1,67 @@
+# contrib/slapd-modules/ppm/Makefile
+# Copyright 2014 David Coutadeur, Paris. All Rights Reserved.
+#
+
+CC=gcc
+
+# Path of OpenLDAP sources
+OLDAP_SOURCES=../../..
+# Where the ppm configuration file should be installed
+CONFIG=/etc/openldap/ppm.conf
+# Path of OpenLDAP installed libs, where the ppm library should be installed
+LIBDIR=/usr/lib/openldap
+
+OPT=-g -O2 -Wall -fpic                                                 \
+       -DCONFIG_FILE="\"$(CONFIG)\""                           \
+       -DDEBUG
+
+# Where to find the OpenLDAP headers.
+
+LDAP_INC=-I$(OLDAP_SOURCES)/include \
+        -I$(OLDAP_SOURCES)/servers/slapd
+
+# Where to find the OpenLDAP libraries.
+
+LDAP_LIBS=-L$(OLDAP_SOURCES)/libraries/liblber/.libs \
+         -L$(OLDAP_SOURCES)/libraries/libldap/.libs
+
+CRACK_INC=-DCRACKLIB
+
+INCS=$(LDAP_INC) $(CRACK_INC)
+
+LDAP_LIB=-lldap -llber
+
+CRACK_LIB=-lcrack
+
+LIBS=$(LDAP_LIB) $(CRACK_LIB)
+
+TESTS=./unit_tests.sh
+
+
+
+all:   ppm ppm_test
+
+ppm_test: 
+       $(CC) -g $(LDAP_INC) $(LDAP_LIBS) -Wl,-rpath=. -o ppm_test ppm_test.c ppm.so $(LIBS)
+
+ppm.o:
+       $(CC) $(OPT) -c $(INCS) ppm.c
+
+ppm: ppm.o
+       $(CC) $(LDAP_INC) -shared -o ppm.so ppm.o $(CRACK_LIB)
+
+install: ppm
+       cp -f ppm.so $(LIBDIR)
+       cp -f ppm_test $(LIBDIR)
+       cp -f ppm.conf $(CONFIG)
+
+.PHONY: clean
+
+clean:
+       $(RM) -f ppm.o ppm.so ppm.lo ppm_test
+       $(RM) -rf .libs
+
+test: ppm ppm_test
+       $(TESTS)
+
+
diff --git a/contrib/slapd-modules/ppm/NOTICES b/contrib/slapd-modules/ppm/NOTICES
new file mode 100644 (file)
index 0000000..b1d4c57
--- /dev/null
@@ -0,0 +1,5 @@
+The attached modifications to OpenLDAP Software are subject to the following notice:
+
+Copyright 2021 David Coutadeur
+Redistribution and use in source and binary forms, with or without modification, are permitted only as authorized by the OpenLDAP Public License.
+
diff --git a/contrib/slapd-modules/ppm/README.md b/contrib/slapd-modules/ppm/README.md
new file mode 100644 (file)
index 0000000..2464336
--- /dev/null
@@ -0,0 +1,292 @@
+
+ppm.c - OpenLDAP password policy module
+
+version 2.0
+
+ppm.c is an OpenLDAP module for checking password quality when they are modified.
+Passwords are checked against the presence or absence of certain character classes.
+
+This module is used as an extension of the OpenLDAP password policy controls,
+see slapo-ppolicy(5) section pwdCheckModule.
+
+contributions
+-------------
+
+* 2014 - 2021 - David Coutadeur <david.coutadeur@gmail.com> - maintainer
+* 2015 - Daly Chikhaoui - Janua <dchikhaoui@janua.fr> - contribution on RDN checks
+* 2017 - tdb - Tim Bishop - contribution on some compilation improvements
+
+
+INSTALLATION
+------------
+
+See INSTALL file
+
+
+USAGE
+-----
+
+Create a password policy entry and indicate the fresh compiled
+library ppm.so:
+
+dn: cn=default,ou=policies,dc=my-domain,dc=com
+objectClass: pwdPolicy
+objectClass: pwdPolicyChecker
+objectClass: person
+objectClass: top
+cn: default
+sn: default
+pwdAttribute: userPassword
+pwdCheckQuality: 2
+...
+pwdCheckModule: /path/to/new/ppm.so
+pwdCheckModuleArg: [see configuration section]
+
+
+See slapo-ppolicy for more information, but to sum up:
+- enable ppolicy overlay in your database.
+This example show the activation for a slapd.conf file
+(see slapd-config and slapo-ppolicy for more information for
+ cn=config configuration)
+
+```
+overlay ppolicy
+ppolicy_default "cn=default,ou=policies,dc=my-domain,dc=com"
+#ppolicy_use_lockout   # for having more infos about the lockout
+```
+
+- define a default password policy in OpenLDAP configuration or
+use pwdPolicySubentry attribute to point to the given policy.
+
+
+
+
+Password checks
+---------------
+
+- 4 character classes are defined by default:
+upper case, lower case, digits and special characters.
+
+- more character classes can be defined, just write your own.
+
+- passwords must match the amount of quality points.
+A point is validated when at least m characters of the corresponding
+character class are present in the password.
+
+- passwords must have at least n of the corresponding character class
+present, else they are rejected.
+
+- the two previous criterias are checked against any specific character class
+defined.
+
+- if a password contains any of the forbidden characters, then it is
+rejected.
+
+- if a password contains tokens from the RDN, then it is rejected.
+
+- if a password is too long, it can be rejected.
+
+- if a password does not pass cracklib check, it can be rejected.
+
+
+Configuration
+-------------
+
+Since OpenLDAP 2.5 version, ppm configuration is held in a binary
+attribute of the password policy: pwdCheckModuleArg
+The configuration file (/etc/openldap/ppm.conf by default) is to be
+considered as an example configuration, to import in the pwdCheckModuleArg
+attribute. It is also used for testing passwords with the test program
+provided.
+If for some reasons, any parameter is not found, it will be given its
+default value.
+
+Note: you can still compile ppm to use the configuration file, by enabling
+PPM_READ_FILE in ppm.h (but this is deprecated now). If you decide to do so,
+you can use the PPM_CONFIG_FILE environment variable for overloading the
+configuration file path.
+
+The syntax of a configuration line is:
+parameter value [min] [minForPoint]
+
+with spaces being delimiters and Line Feed (LF) ending the line.
+Parameter names ARE case sensitive.
+
+The default configuration is the following:
+
+```
+# minQuality parameter
+# Format:
+# minQuality [NUMBER]
+# Description:
+# One point is granted for each class for which MIN_FOR_POINT criteria is fulfilled.
+# defines the minimum point numbers for the password to be accepted.
+minQuality 3
+
+# checkRDN parameter
+# Format:
+# checkRDN [0 | 1]
+# Description:
+# If set to 1, password must not contain a token from the RDN.
+# Tokens are separated by the following delimiters : space tabulation _ - , ; £
+checkRDN 0
+
+# forbiddenChars parameter
+# Format:
+# forbiddenChars [CHARACTERS_FORBIDDEN]
+# Description:
+# Defines the forbidden characters list (no separator).
+# If one of them is found in the password, then it is rejected.
+forbiddenChars
+
+# maxConsecutivePerClass parameter
+# Format:
+# maxConsecutivePerClass [NUMBER]
+# Description:
+# Defines the maximum number of consecutive character allowed for any class
+maxConsecutivePerClass 0
+
+# useCracklib parameter
+# Format:
+# useCracklib [0 | 1]
+# Description:
+# If set to 1, the password must pass the cracklib check
+useCracklib 0
+
+# cracklibDict parameter
+# Format:
+# cracklibDict [path_to_cracklib_dictionnary]
+# Description:
+# directory+filename-prefix that your version of CrackLib will go hunting for
+# For example, /var/pw_dict resolves as /var/pw_dict.pwd,
+# /var/pw_dict.pwi and /var/pw_dict.hwm dictionnary files
+cracklibDict /var/cache/cracklib/cracklib_dict
+
+# classes parameter
+# Format:
+# class-[CLASS_NAME] [CHARACTERS_DEFINING_CLASS] [MIN] [MIN_FOR_POINT]
+# Description:
+# [CHARACTERS_DEFINING_CLASS]: characters defining the class (no separator)
+# [MIN]: If at least [MIN] characters of this class is not found in the password, then it is rejected
+# [MIN_FOR_POINT]: one point is granted if password contains at least [MIN_FOR_POINT] character numbers of this class
+class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 0 1
+class-lowerCase abcdefghijklmnopqrstuvwxyz 0 1
+class-digit 0123456789 0 1
+class-special <>,?;.:/!§ù%*µ^¨$£²&é~"#'{([-|è`_\ç^à@)]°=}+ 0 1
+```
+
+Example
+-------
+
+With this policy:
+```
+minQuality 4
+forbiddenChars .?,
+checkRDN 1
+class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 0 5
+class-lowerCase abcdefghijklmnopqrstuvwxyz 0 12
+class-digit 0123456789 0 1
+class-special <>,?;.:/!§ù%*µ^¨$£²&é~"#'{([-|è`_\ç^à@)]°=}+ 0 1
+class-myClass :) 1 1``
+```
+
+the password
+
+ThereIsNoCowLevel)
+
+is working, because,
+- it has 4 character classes validated : upper, lower, special, and myClass
+- it has no character among .?,
+- it has at least one character among : or )
+
+but it won't work for the user uid=John Cowlevel,ou=people,cn=example,cn=com,
+because the token "Cowlevel" from his RDN exists in the password (case insensitive).
+
+
+Logs
+----
+If a user password is rejected by ppm, the user will get this type of message:
+
+Typical user message from ldappasswd(5):
+  Result: Constraint violation (19)
+  Additional info: Password for dn=\"%s\" does not pass required number of strength checks (2 of 3)
+
+A more detailed message is written to the server log.
+
+Server log:
+
+```
+Jul 27 20:09:14 machine slapd[20270]: ppm: Opening file /etc/openldap/ppm.conf
+Jul 27 20:09:14 machine slapd[20270]: ppm: Param = minQuality, value = 3, min = (null), minForPoint= (null)
+Jul 27 20:09:14 machine slapd[20270]: ppm:  Accepted replaced value: 3
+Jul 27 20:09:14 machine slapd[20270]: ppm: Param = forbiddenChars, value = , min = (null), minForPoint= (null)
+Jul 27 20:09:14 machine slapd[20270]: ppm:  Accepted replaced value:
+Jul 27 20:09:14 machine slapd[20270]: ppm: Param = class-upperCase, value = ABCDEFGHIJKLMNOPQRSTUVWXYZ, min = 0, minForPoint= 5
+Jul 27 20:09:14 machine slapd[20270]: ppm:  Accepted replaced value: ABCDEFGHIJKLMNOPQRSTUVWXYZ
+Jul 27 20:09:14 machine slapd[20270]: ppm: Param = class-lowerCase, value = abcdefghijklmnopqrstuvwxyz, min = 0, minForPoint= 12
+Jul 27 20:09:14 machine slapd[20270]: ppm:  Accepted replaced value: abcdefghijklmnopqrstuvwxyz
+Jul 27 20:09:14 machine slapd[20270]: ppm: Param = class-digit, value = 0123456789, min = 0, minForPoint= 1
+Jul 27 20:09:14 machine slapd[20270]: ppm:  Accepted replaced value: 0123456789
+Jul 27 20:09:14 machine slapd[20270]: ppm: Param = class-special, value = <>,?;.:/!§ù%*µ^¨$£²&é~"#'{([-|è`_\ç^à@)]°=}+, min = 0, minForPoint= 1
+Jul 27 20:09:14 machine slapd[20270]: ppm:  Accepted replaced value: <>,?;.:/!§ù%*µ^¨$£²&é~"#'{([-|è`_\ç^à@)]°=}+
+Jul 27 20:09:14 machine slapd[20270]: ppm: Param = class-myClass, value = :), min = 1, minForPoint= 1
+Jul 27 20:09:14 machine slapd[20270]: ppm:  Accepted new value:
+Jul 27 20:09:14 machine slapd[20270]: ppm: 1 point granted for class class-upperCase
+Jul 27 20:09:14 machine slapd[20270]: ppm: 1 point granted for class class-lowerCase
+Jul 27 20:09:14 machine slapd[20270]: ppm: 1 point granted for class class-digit
+```
+
+
+Tests
+-----
+
+There is a unit test script: "unit_tests.sh" that illustrates checking some passwords.
+It is possible to test one particular password using directly the test program:
+
+```
+cd /usr/local/openldap/lib64
+LD_LIBRARY_PATH=. ./ppm_test "uid=test,ou=users,dc=my-domain,dc=com" "my_password" "/usr/local/openldap/etc/openldap/ppm.conf" && echo OK
+```
+
+
+
+HISTORY
+-------
+
+* 2021-02-23 David Coutadeur <david.coutadeur@gmail.com>
+  remove maxLength attribute (#21)
+  adapt the readme and documentation of ppm (#22)
+  prepare ppolicy10 in OpenLDAP 2.5 (#20, #23 and #24)
+  add pwdCheckModuleArg feature
+  Version 2.0
+* 2019-08-20 David Coutadeur <david.coutadeur@gmail.com>
+  adding debug symbols for ppm_test,
+  improve tests with the possibility to add username,
+  fix openldap crash when checkRDN=1 and username contains too short parts
+  Version 1.8
+* 2018-03-30 David Coutadeur <david.coutadeur@gmail.com>
+  various minor improvements provided by Tim Bishop (tdb) (compilation, test program,
+  imprvts in Makefile: new OLDAP_SOURCES variable pointing to OLDAP instal. directory
+  Version 1.7
+* 2017-05-19 David Coutadeur <david.coutadeur@gmail.com>
+  Adds cracklib support
+  Readme adaptations and cleaning
+  Version 1.6
+* 2017-02-07 David Coutadeur <david.coutadeur@gmail.com>
+  Adds maxConsecutivePerClass (idea from Trevor Vaughan / tvaughan@onyxpoint.com)
+  Version 1.5
+* 2016-08-22 David Coutadeur <david.coutadeur@gmail.com>
+  Get config file from environment variable
+  Version 1.4
+* 2014-12-20 Daly Chikhaoui <dchikhaoui@janua.fr>
+  Adding checkRDN parameter
+  Version 1.3
+* 2014-10-28 David Coutadeur <david.coutadeur@gmail.com>
+  Adding maxLength parameter
+  Version 1.2
+* 2014-07-27 David Coutadeur <david.coutadeur@gmail.com>
+  Changing the configuration file and the configuration data structure
+  Version 1.1
+* 2014-04-04 David Coutadeur <david.coutadeur@gmail.com>
+  Version 1.0
+
diff --git a/contrib/slapd-modules/ppm/ppm.c b/contrib/slapd-modules/ppm/ppm.c
new file mode 100644 (file)
index 0000000..a4422e4
--- /dev/null
@@ -0,0 +1,679 @@
+/*
+ * ppm.c for OpenLDAP
+ *
+ * See LICENSE, README and INSTALL files
+ */
+
+
+/*
+  password policy module is called with:
+  int check_password (char *pPasswd, char **ppErrStr, Entry *e, void *pArg)
+
+  *pPasswd: new password
+  **ppErrStr: pointer to the string containing the error message
+  *e: pointer to the current user entry
+  *pArg: pointer to a struct berval holding the value of pwdCheckModuleArg attr
+*/
+
+#include <stdlib.h>             // for type conversion, such as atoi...
+#include <regex.h>              // for matching allowedParameters / conf file
+#include <string.h>
+#include <ctype.h>
+#include <portable.h>
+#include <slap.h>
+#include <stdarg.h>             // for variable nb of arguments functions
+#include "ppm.h"
+
+#ifdef CRACKLIB
+#include "crack.h"              // use cracklib to check password
+#endif
+
+void
+ppm_log(int priority, const char *format, ...)
+{
+  // if DEBUG flag is set
+  // logs into syslog (for OpenLDAP) or to stdout (for tests)
+#if defined(DEBUG)
+  if(ppm_test != 1)
+  {
+    va_list syslog_args;
+    va_start(syslog_args, format);
+    vsyslog(priority, format, syslog_args);
+    va_end(syslog_args);
+  }
+  else
+  {
+    va_list stdout_args;
+    va_start(stdout_args, format);
+    vprintf(format, stdout_args);
+    printf("\n");
+    fflush(stdout);
+    va_end(stdout_args);
+  }
+#endif
+}
+
+void
+strcpy_safe(char *dest, char *src, int length_dest)
+{
+    if(src == NULL)
+    {
+        dest[0] = '\0';
+    }
+    else
+    {
+        int length_src = strlen(src);
+        int n = (length_dest < length_src) ? length_dest : length_src;
+        // Copy the string — don’t copy too many bytes.
+        strncpy(dest, src, n);
+        // Ensure null-termination.
+        dest[n] = '\0';
+    }
+}
+
+genValue*
+getValue(conf *fileConf, int numParam, char* param)
+{
+    int i = 0;
+
+    // First scan parameters
+    for (i = 0; i < numParam; i++) {
+        if ((strlen(param) == strlen(fileConf[i].param))
+            && (strncmp(param, fileConf[i].param, strlen(fileConf[i].param))
+                == 0)) {
+            return &(fileConf[i].value);
+        }
+    }
+    return NULL;
+}
+
+int maxConsPerClass(char *password, char *charClass)
+{
+  // find maximum number of consecutive class characters in the password
+
+  int bestMax = 0;
+  int max = 0;
+  int i;
+
+  for(i=0 ; i<strlen(password) ; i++)
+  {
+    if(strchr(charClass,password[i]) != NULL)
+    {
+      // current character is in class
+      max++;
+      // is the new max a better candidate to maxConsecutivePerClass?
+      if(max > bestMax)
+      {
+        // found a better maxConsecutivePerClass
+        bestMax = max;
+      }
+    }
+    else
+    {
+      // current character is not in class
+      // reinitialize max
+      max=0;
+    }
+  }
+  return bestMax;
+}
+
+void
+storeEntry(char *param, char *value, valueType valType, 
+           char *min, char *minForPoint, conf * fileConf, int *numParam)
+{
+    int i = 0;
+    int iMin;
+    int iMinForPoint;
+    if (min == NULL || strcmp(min,"") == 0)
+      iMin = 0;
+    else
+      iMin = atoi(min);
+
+    if (minForPoint == NULL || strcmp(minForPoint,"") == 0)
+      iMinForPoint = 0;
+    else
+      iMinForPoint = atoi(minForPoint);
+
+    // First scan parameters
+    for (i = 0; i < *numParam; i++) {
+        if ((strlen(param) == strlen(fileConf[i].param))
+            && (strncmp(param, fileConf[i].param, strlen(fileConf[i].param))
+                == 0)) {
+            // entry found, replace values
+            if(valType == typeInt)
+                fileConf[i].value.iVal = atoi(value);
+            else
+                strcpy_safe(fileConf[i].value.sVal, value, VALUE_MAX_LEN);
+            fileConf[i].min = iMin;
+            fileConf[i].minForPoint = iMinForPoint;
+            if(valType == typeInt)
+                ppm_log(LOG_NOTICE, "ppm:  Accepted replaced value: %d",
+                               fileConf[i].value.iVal);
+            else
+                ppm_log(LOG_NOTICE, "ppm:  Accepted replaced value: %s",
+                               fileConf[i].value.sVal);
+            return;
+        }
+    }
+    // entry not found, add values
+    strcpy_safe(fileConf[*numParam].param, param, PARAM_MAX_LEN);
+    fileConf[*numParam].iType = valType;
+    if(valType == typeInt)
+        fileConf[i].value.iVal = atoi(value);
+    else
+        strcpy_safe(fileConf[i].value.sVal, value, VALUE_MAX_LEN);
+    fileConf[*numParam].min = iMin;
+    fileConf[*numParam].minForPoint = iMinForPoint;
+    ++(*numParam);
+            if(valType == typeInt)
+                ppm_log(LOG_NOTICE, "ppm:  Accepted new value: %d",
+                               fileConf[*numParam].value.iVal);
+            else
+                ppm_log(LOG_NOTICE, "ppm:  Accepted new value: %s",
+                               fileConf[*numParam].value.sVal);
+}
+
+int
+typeParam(char* param)
+{
+    int i;
+    int n = sizeof(allowedParameters)/sizeof(params);
+
+    regex_t regex;
+    int reti;
+
+    for(i = 0 ; i < n ; i++ )
+    {
+        // Compile regular expression
+        reti = regcomp(&regex, allowedParameters[i].param, 0);
+        if (reti) {
+            ppm_log(LOG_ERR, "ppm: Cannot compile regex: %s",
+                   allowedParameters[i].param);
+            exit(EXIT_FAILURE);
+        }
+        
+        // Execute regular expression
+        reti = regexec(&regex, param, 0, NULL, 0);
+        if (!reti)
+        {
+            regfree(&regex);
+            return i;
+        } 
+        regfree(&regex);
+    }
+    return n;
+}
+
+#ifndef PPM_READ_FILE
+
+  /*
+   * read configuration into pwdCheckModuleArg attribute
+   * */
+  static void
+  read_config_attr(conf * fileConf, int *numParam, char *ppm_config_attr)
+  {
+    int nParam = 0;       // position of found parameter in allowedParameters
+    int sAllowedParameters = sizeof(allowedParameters)/sizeof(params);
+    char arg[260*256];
+    char *token;
+    char *saveptr1;
+    char *saveptr2;
+  
+    strcpy_safe(arg, ppm_config_attr, 260*256);
+    ppm_log(LOG_NOTICE, "ppm: Parsing pwdCheckModuleArg attribute");
+    token = strtok_r(arg, "\n", &saveptr1);
+  
+    while (token != NULL) {
+        ppm_log(LOG_NOTICE, "ppm: get line: %s",token);
+        char *start = token;
+        char *word, *value;
+        char *min, *minForPoint;;
+  
+        while (isspace(*start) && isascii(*start))
+            start++;
+  
+        if (!isascii(*start))
+        {
+            token = strtok_r(NULL, "\n", &saveptr1);
+            continue;
+        }
+        if (start[0] == '#')
+        {
+            token = strtok_r(NULL, "\n", &saveptr1);
+            continue;
+        }
+  
+        if ((word = strtok_r(start, " \t", &saveptr2))) {
+            if ((value = strtok_r(NULL, " \t", &saveptr2)) == NULL)
+            {
+                saveptr2 = NULL;
+                ppm_log(LOG_NOTICE, "ppm: No value, goto next parameter");
+                token = strtok_r(NULL, "\n", &saveptr1);
+                continue;
+            }
+            if (strchr(value, '\n') != NULL)
+                strchr(value, '\n')[0] = '\0';
+            min = strtok_r(NULL, " \t", &saveptr2);
+            if (min != NULL)
+                if (strchr(min, '\n') != NULL)
+                    strchr(min, '\n')[0] = '\0';
+            minForPoint = strtok_r(NULL, " \t", &saveptr2);
+            if (minForPoint != NULL)
+                if (strchr(minForPoint, '\n') != NULL)
+                    strchr(minForPoint, '\n')[0] = '\0';
+  
+  
+            nParam = typeParam(word); // search for param in allowedParameters
+            if (nParam != sAllowedParameters) // param has been found
+            {
+                ppm_log(LOG_NOTICE,
+                   "ppm: Param = %s, value = %s, min = %s, minForPoint= %s",
+                   word, value, min, minForPoint);
+  
+                storeEntry(word, value, allowedParameters[nParam].iType,
+                           min, minForPoint, fileConf, numParam);
+            }
+            else
+            {
+                ppm_log(LOG_NOTICE,
+                   "ppm: Parameter '%s' rejected", word);
+            }
+  
+        }
+        token = strtok_r(NULL, "\n", &saveptr1);
+    }
+  
+  }
+
+#endif
+
+#ifdef PPM_READ_FILE
+
+  /*
+   * read configuration file (DEPRECATED)
+   * */
+  static void
+  read_config_file(conf * fileConf, int *numParam, char *ppm_config_file)
+  {
+    FILE *config;
+    char line[260] = "";
+    int nParam = 0;       // position of found parameter in allowedParameters
+    int sAllowedParameters = sizeof(allowedParameters)/sizeof(params);
+  
+    ppm_log(LOG_NOTICE, "ppm: Opening file %s", ppm_config_file);
+    if ((config = fopen(ppm_config_file, "r")) == NULL) {
+        ppm_log(LOG_ERR, "ppm: Opening file %s failed", ppm_config_file);
+        exit(EXIT_FAILURE);
+    }
+  
+    while (fgets(line, 256, config) != NULL) {
+        char *start = line;
+        char *word, *value;
+        char *min, *minForPoint;;
+  
+        while (isspace(*start) && isascii(*start))
+            start++;
+  
+        if (!isascii(*start))
+            continue;
+        if (start[0] == '#')
+            continue;
+  
+        if ((word = strtok(start, " \t"))) {
+            if ((value = strtok(NULL, " \t")) == NULL)
+                continue;
+            if (strchr(value, '\n') != NULL)
+                strchr(value, '\n')[0] = '\0';
+            min = strtok(NULL, " \t");
+            if (min != NULL)
+                if (strchr(min, '\n') != NULL)
+                    strchr(min, '\n')[0] = '\0';
+            minForPoint = strtok(NULL, " \t");
+            if (minForPoint != NULL)
+                if (strchr(minForPoint, '\n') != NULL)
+                    strchr(minForPoint, '\n')[0] = '\0';
+  
+  
+            nParam = typeParam(word); // search for param in allowedParameters
+            if (nParam != sAllowedParameters) // param has been found
+            {
+                ppm_log(LOG_NOTICE,
+                   "ppm: Param = %s, value = %s, min = %s, minForPoint= %s",
+                   word, value, min, minForPoint);
+  
+                storeEntry(word, value, allowedParameters[nParam].iType,
+                           min, minForPoint, fileConf, numParam);
+            }
+            else
+            {
+                ppm_log(LOG_NOTICE,
+                   "ppm: Parameter '%s' rejected", word);
+            }
+  
+        }
+    }
+  
+    fclose(config);
+  }
+
+#endif
+
+static int
+realloc_error_message(char **target, int curlen, int nextlen)
+{
+    if (curlen < nextlen + MEMORY_MARGIN) {
+        ppm_log(LOG_WARNING,
+               "ppm: Reallocating szErrStr from %d to %d", curlen,
+               nextlen + MEMORY_MARGIN);
+        ber_memfree(*target);
+        curlen = nextlen + MEMORY_MARGIN;
+        *target = (char *) ber_memalloc(curlen);
+    }
+
+    return curlen;
+}
+
+// Does the password contains a token from the RDN ?
+int
+containsRDN(char* passwd, char* DN)
+{
+    char lDN[DN_MAX_LEN];
+    char * tmpToken;
+    char * token;
+    regex_t regex;
+    int reti;
+    strcpy_safe(lDN, DN, DN_MAX_LEN);
+    // Extract the RDN from the DN
+    tmpToken = strtok(lDN, ",+");
+    tmpToken = strtok(tmpToken, "=");
+    tmpToken = strtok(NULL, "=");
+    // Search for each token in the password */
+    token = strtok(tmpToken, TOKENS_DELIMITERS);
+    while (token != NULL)
+    {
+      if (strlen(token) > 2)
+      {
+        ppm_log(LOG_NOTICE, "ppm: Checking if %s part of RDN matches the password", token);
+        // Compile regular expression
+        reti = regcomp(&regex, token, REG_ICASE);
+        if (reti) {
+          ppm_log(LOG_ERR, "ppm: Cannot compile regex: %s", token);
+          exit(EXIT_FAILURE);
+        }
+        // Execute regular expression
+        reti = regexec(&regex, passwd, 0, NULL, 0);
+        if (!reti)
+        {
+          regfree(&regex);
+          return 1;
+        }
+        regfree(&regex);
+      }
+      else
+      {
+        ppm_log(LOG_NOTICE, "ppm: %s part of RDN is too short to be checked", token);
+      }
+      token = strtok(NULL, TOKENS_DELIMITERS);
+    }
+    return 0;
+}
+
+
+int
+check_password(char *pPasswd, char **ppErrStr, Entry *e, void *pArg)
+{
+
+    Entry *pEntry = e;
+    ppm_log(LOG_NOTICE, "ppm: entry %s", pEntry->e_nname.bv_val);
+
+    struct berval *pwdCheckModuleArg = pArg;
+    /* Determine if config file is to be read (DEPRECATED) */
+    #ifdef PPM_READ_FILE
+      ppm_log(LOG_NOTICE, "ppm: Not reading pwdCheckModuleArg attribute");
+      ppm_log(LOG_NOTICE, "ppm: instead, read configuration file (deprecated)");
+    #else
+      ppm_log(LOG_NOTICE, "ppm: Reading pwdCheckModuleArg attribute");
+      ppm_log(LOG_NOTICE, "ppm: RAW configuration: %s",
+                          (*(struct berval*)pwdCheckModuleArg).bv_val);
+    #endif
+
+    char *szErrStr = (char *) ber_memalloc(MEM_INIT_SZ);
+    int mem_len = MEM_INIT_SZ;
+    int numParam = 0; // Number of params in current configuration
+
+    int useCracklib;
+    char cracklibDict[VALUE_MAX_LEN];
+    char cracklibDictFiles[3][(VALUE_MAX_LEN+5)];
+    char const* cracklibExt[] = { ".hwm", ".pwd", ".pwi" };
+    FILE* fd;
+    char* res;
+    int minQuality;
+    int checkRDN;
+    char forbiddenChars[VALUE_MAX_LEN];
+    int nForbiddenChars = 0;
+    int nQuality = 0;
+    int maxConsecutivePerClass;
+    int nbInClass[CONF_MAX_SIZE];
+    int i,j;
+
+    /* Determine config file (DEPRECATED) */
+    #ifdef PPM_READ_FILE
+      char ppm_config_file[FILENAME_MAX_LEN];
+      strcpy_safe(ppm_config_file, getenv("PPM_CONFIG_FILE"), FILENAME_MAX_LEN);
+      if (ppm_config_file[0] == '\0') {
+        strcpy_safe(ppm_config_file, CONFIG_FILE, FILENAME_MAX_LEN);
+      }
+      ppm_log(LOG_NOTICE, "ppm: reading config file from %s", ppm_config_file);
+    #endif
+
+    for (i = 0; i < CONF_MAX_SIZE; i++)
+        nbInClass[i] = 0;
+
+    /* Set default values */
+    conf fileConf[CONF_MAX_SIZE] = {
+        {"minQuality", typeInt, {.iVal = DEFAULT_QUALITY}, 0, 0
+         }
+        ,
+        {"checkRDN", typeInt, {.iVal = 0}, 0, 0
+         }
+        ,
+        {"forbiddenChars", typeStr, {.sVal = ""}, 0, 0
+         }
+        ,
+        {"maxConsecutivePerClass", typeInt, {.iVal = 0}, 0, 0
+         }
+        ,
+        {"useCracklib", typeInt, {.iVal = 0}, 0, 0
+         }
+        ,
+        {"cracklibDict", typeStr, {.sVal = "/var/cache/cracklib/cracklib_dict"}, 0, 0
+         }
+        ,
+        {"class-upperCase", typeStr, {.sVal = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"}, 0, 1
+         }
+        ,
+        {"class-lowerCase", typeStr, {.sVal = "abcdefghijklmnopqrstuvwxyz"}, 0, 1
+         }
+        ,
+        {"class-digit", typeStr, {.sVal = "0123456789"}, 0, 1
+         }
+        ,
+        {"class-special", typeStr,
+         {.sVal = "<>,?;.:/!§ù%*µ^¨$£²&é~\"#'{([-|è`_\\ç^à@)]°=}+"}, 0, 1
+         }
+    };
+    numParam = 10;
+
+    #ifdef PPM_READ_FILE
+      /* Read configuration file (DEPRECATED) */
+      read_config_file(fileConf, &numParam, ppm_config_file);
+    #else
+      /* Read configuration attribute (pwdCheckModuleArg) */
+      read_config_attr(fileConf, &numParam, (*(struct berval*)pwdCheckModuleArg).bv_val);
+    #endif
+
+    minQuality = getValue(fileConf, numParam, "minQuality")->iVal;
+    checkRDN = getValue(fileConf, numParam, "checkRDN")->iVal;
+    strcpy_safe(forbiddenChars,
+                getValue(fileConf, numParam, "forbiddenChars")->sVal,
+                VALUE_MAX_LEN);
+    maxConsecutivePerClass = getValue(fileConf, numParam, "maxConsecutivePerClass")->iVal;
+    useCracklib = getValue(fileConf, numParam, "useCracklib")->iVal;
+    strcpy_safe(cracklibDict,
+                getValue(fileConf, numParam, "cracklibDict")->sVal,
+                VALUE_MAX_LEN);
+
+
+    /*The password must have at least minQuality strength points with one
+     * point granted if the password contains at least minForPoint characters for each class
+     * It must contains at least min chars of each class
+     * It must not contain any char in forbiddenChar */
+
+    for (i = 0; i < strlen(pPasswd); i++) {
+
+        int n;
+        for (n = 0; n < numParam; n++) {
+            if (strstr(fileConf[n].param, "class-") != NULL) {
+                if (strchr(fileConf[n].value.sVal, pPasswd[i]) != NULL) {
+                    ++(nbInClass[n]);
+                }
+            }
+        }
+        if (strchr(forbiddenChars, pPasswd[i]) != NULL) {
+            nForbiddenChars++;
+        }
+    }
+
+    // Password checking done, now loocking for minForPoint criteria
+    for (i = 0; i < CONF_MAX_SIZE; i++) {
+        if (strstr(fileConf[i].param, "class-") != NULL) {
+            if ((nbInClass[i] >= fileConf[i].minForPoint)
+                && strlen(fileConf[i].value.sVal) != 0) {
+                // 1 point granted
+                ++nQuality;
+                ppm_log(LOG_NOTICE, "ppm: 1 point granted for class %s",
+                       fileConf[i].param);
+            }
+        }
+    }
+
+    if (nQuality < minQuality) {
+        mem_len = realloc_error_message(&szErrStr, mem_len,
+                                        strlen(PASSWORD_QUALITY_SZ) +
+                                        strlen(pEntry->e_nname.bv_val) + 4);
+        sprintf(szErrStr, PASSWORD_QUALITY_SZ, pEntry->e_nname.bv_val,
+                nQuality, minQuality);
+        goto fail;
+    }
+    // Password checking done, now loocking for constraintClass criteria
+    for (i = 0; i < CONF_MAX_SIZE; i++) {
+        if (strstr(fileConf[i].param, "class-") != NULL) {
+            if ((nbInClass[i] < fileConf[i].min) &&
+                 strlen(fileConf[i].value.sVal) != 0) {
+                // constraint is not satisfied... goto fail
+                mem_len = realloc_error_message(&szErrStr, mem_len,
+                                                strlen(PASSWORD_CRITERIA) +
+                                                strlen(pEntry->e_nname.bv_val) + 
+                                                2 + PARAM_MAX_LEN);
+                sprintf(szErrStr, PASSWORD_CRITERIA, pEntry->e_nname.bv_val,
+                        fileConf[i].min, fileConf[i].param);
+                goto fail;
+            }
+        }
+    }
+
+    // Password checking done, now loocking for forbiddenChars criteria
+    if (nForbiddenChars > 0) {  // at least 1 forbidden char... goto fail
+        mem_len = realloc_error_message(&szErrStr, mem_len,
+                                        strlen(PASSWORD_FORBIDDENCHARS) +
+                                        strlen(pEntry->e_nname.bv_val) + 2 +
+                                        VALUE_MAX_LEN);
+        sprintf(szErrStr, PASSWORD_FORBIDDENCHARS, pEntry->e_nname.bv_val,
+                nForbiddenChars, forbiddenChars);
+        goto fail;
+    }
+
+    // Password checking done, now loocking for maxConsecutivePerClass criteria
+    for (i = 0; i < CONF_MAX_SIZE; i++) {
+        if (strstr(fileConf[i].param, "class-") != NULL) {
+            if ( maxConsecutivePerClass != 0 &&
+                (maxConsPerClass(pPasswd,fileConf[i].value.sVal)
+                                                 > maxConsecutivePerClass)) {
+                // Too much consecutive characters of the same class
+                ppm_log(LOG_NOTICE, "ppm: Too much consecutive chars for class %s",
+                       fileConf[i].param);
+                mem_len = realloc_error_message(&szErrStr, mem_len,
+                                        strlen(PASSWORD_MAXCONSECUTIVEPERCLASS) +
+                                        strlen(pEntry->e_nname.bv_val) + 2 +
+                                        PARAM_MAX_LEN);
+                sprintf(szErrStr, PASSWORD_MAXCONSECUTIVEPERCLASS, pEntry->e_nname.bv_val,
+                        maxConsecutivePerClass, fileConf[i].param);
+                goto fail;
+            }
+        }
+    }
+#ifdef CRACKLIB
+    // Password checking done, now loocking for cracklib criteria
+    if ( useCracklib > 0 ) {
+
+        for( j = 0 ; j < 3 ; j++) {
+            strcpy_safe(cracklibDictFiles[j], cracklibDict, VALUE_MAX_LEN);
+            strcat(cracklibDictFiles[j], cracklibExt[j]);
+            if (( fd = fopen ( cracklibDictFiles[j], "r")) == NULL ) {
+                ppm_log(LOG_NOTICE, "ppm: Error while reading %s file",
+                       cracklibDictFiles[j]);
+                mem_len = realloc_error_message(&szErrStr, mem_len,
+                                strlen(GENERIC_ERROR));
+                sprintf(szErrStr, GENERIC_ERROR);
+                goto fail;
+
+            }
+            else {
+                fclose (fd);
+            }
+        }
+        res = (char *) FascistCheck (pPasswd, cracklibDict);
+        if ( res != NULL ) {
+                ppm_log(LOG_NOTICE, "ppm: cracklib does not validate password for entry %s",
+                       pEntry->e_nname.bv_val);
+                mem_len = realloc_error_message(&szErrStr, mem_len,
+                                        strlen(PASSWORD_CRACKLIB) +
+                                        strlen(pEntry->e_nname.bv_val));
+                sprintf(szErrStr, PASSWORD_CRACKLIB, pEntry->e_nname.bv_val);
+                goto fail;
+        
+        }
+
+    }
+#endif
+
+    // Password checking done, now looking for checkRDN criteria
+    if (checkRDN == 1 && containsRDN(pPasswd, pEntry->e_nname.bv_val))
+    // RDN check enabled and a token from RDN is found in password: goto fail
+    {
+        mem_len = realloc_error_message(&szErrStr, mem_len,
+                                        strlen(RDN_TOKEN_FOUND) +
+                                        strlen(pEntry->e_nname.bv_val));
+        sprintf(szErrStr, RDN_TOKEN_FOUND, pEntry->e_nname.bv_val);
+
+        goto fail;
+    }
+
+    *ppErrStr = strdup("");
+    ber_memfree(szErrStr);
+    return (LDAP_SUCCESS);
+
+  fail:
+    *ppErrStr = strdup(szErrStr);
+    ber_memfree(szErrStr);
+    return (EXIT_FAILURE);
+
+}
diff --git a/contrib/slapd-modules/ppm/ppm.conf b/contrib/slapd-modules/ppm/ppm.conf
new file mode 100644 (file)
index 0000000..23993bc
--- /dev/null
@@ -0,0 +1,66 @@
+
+# minQuality parameter
+# Format:
+# minQuality [NUMBER]
+# Description:
+# One point is granted for each class for which MIN_FOR_POINT criteria is fulfilled.
+# defines the minimum point numbers for the password to be accepted.
+minQuality 3
+
+# maxLength parameter
+# Format:
+# maxLength [NUMBER]
+# Description:
+# The password must not be more than [NUMBER] long. 0 means no limit is set.
+maxLength 0
+
+# checkRDN parameter
+# Format:
+# checkRDN [0 | 1]
+# Description:
+# If set to 1, password must not contain a token from the RDN.
+# Tokens are separated by these delimiters : space tabulation _ - , ; £
+checkRDN 0
+
+# forbiddenChars parameter
+# Format:
+# forbiddenChars [CHARACTERS_FORBIDDEN]
+# Description:
+# Defines the forbidden characters list (no separator).
+# If one of them is found in the password, then it is rejected.
+forbiddenChars 
+
+# maxConsecutivePerClass parameter
+# Format:
+# maxConsecutivePerClass [NUMBER]
+# Description:
+# Defines the maximum number of consecutive character allowed for any class
+maxConsecutivePerClass 0
+
+# useCracklib parameter
+# Format:
+# useCracklib [0 | 1]
+# Description:
+# If set to 1, the password must pass the cracklib check
+useCracklib 0
+
+# cracklibDict parameter
+# Format:
+# cracklibDict [path_to_cracklib_dictionnary]
+# Description:
+# directory+filename-prefix that your version of CrackLib will go hunting for
+# For example, /var/pw_dict resolves as /var/pw_dict.pwd,
+# /var/pw_dict.pwi and /var/pw_dict.hwm dictionnary files
+cracklibDict /var/cache/cracklib/cracklib_dict
+
+# classes parameter
+# Format:
+# class-[CLASS_NAME] [CHARACTERS_DEFINING_CLASS] [MIN] [MIN_FOR_POINT]
+# Description:
+# [CHARACTERS_DEFINING_CLASS]: characters defining the class (no separator)
+# [MIN]: If at least [MIN] characters of this class is not found in the password, then it is rejected
+# [MIN_FOR_POINT]: one point is granted if password contains at least [MIN_FOR_POINT] character numbers of this class
+class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 0 1
+class-lowerCase abcdefghijklmnopqrstuvwxyz 0 1
+class-digit 0123456789 0 1
+class-special <>,?;.:/!§ù%*µ^¨$£²&é~"#'{([-|è`_\ç^à@)]°=}+ 0 1
diff --git a/contrib/slapd-modules/ppm/ppm.h b/contrib/slapd-modules/ppm/ppm.h
new file mode 100644 (file)
index 0000000..b9931cb
--- /dev/null
@@ -0,0 +1,125 @@
+/*
+ * ppm.h for OpenLDAP
+ *
+ * See LICENSE, README and INSTALL files
+ */
+
+#ifndef PPM_H_
+#define PPM_H_
+
+#include <stdlib.h>             // for type conversion, such as atoi...
+#include <regex.h>              // for matching allowedParameters / conf file
+#include <string.h>
+#include <ctype.h>
+#include <portable.h>
+#include <slap.h>
+
+#if defined(DEBUG)
+#include <syslog.h>
+#endif
+
+//#define PPM_READ_FILE 1       // old deprecated configuration mode
+                                // 1: (deprecated) don't read pwdCheckModuleArg
+                                //    attribute, instead read config file
+                                // 0: read pwdCheckModuleArg attribute
+
+/* config file parameters (DEPRECATED) */
+#ifndef CONFIG_FILE
+#define CONFIG_FILE                       "/etc/openldap/ppm.conf"
+#endif
+#define FILENAME_MAX_LEN                  512
+
+#define DEFAULT_QUALITY                   3
+#define MEMORY_MARGIN                     50
+#define MEM_INIT_SZ                       64
+#define DN_MAX_LEN                        512
+
+#define CONF_MAX_SIZE                      50
+#define PARAM_MAX_LEN                      32
+#define VALUE_MAX_LEN                      128
+#define ATTR_NAME_MAX_LEN                  150
+
+#define PARAM_PREFIX_CLASS                "class-"
+#define TOKENS_DELIMITERS                 " ,;-_£\t"
+
+
+#define DEBUG_MSG_MAX_LEN                 256
+
+#define PASSWORD_QUALITY_SZ \
+  "Password for dn=\"%s\" does not pass required number of strength checks (%d of %d)"
+#define PASSWORD_CRITERIA \
+  "Password for dn=\"%s\" has not reached the minimum number of characters (%d) for class %s"
+#define PASSWORD_MAXCONSECUTIVEPERCLASS \
+  "Password for dn=\"%s\" has reached the maximum number of characters (%d) for class %s"
+#define PASSWORD_FORBIDDENCHARS \
+  "Password for dn=\"%s\" contains %d forbidden characters in %s"
+#define RDN_TOKEN_FOUND \
+  "Password for dn=\"%s\" contains tokens from the RDN"
+#define GENERIC_ERROR \
+  "Error while checking password"
+#define PASSWORD_CRACKLIB \
+  "Password for dn=\"%s\" is too weak"
+#define BAD_PASSWORD_SZ \
+  "Bad password for dn=\"%s\" because %s"
+
+
+
+typedef union genValue {
+    int iVal;
+    char sVal[VALUE_MAX_LEN];
+} genValue;
+
+typedef enum {
+    typeInt,
+    typeStr
+} valueType;
+
+typedef struct params {
+    char param[PARAM_MAX_LEN];
+    valueType iType;
+} params;
+
+// allowed parameters loaded into configuration structure
+// it also contains the type of the corresponding value
+params allowedParameters[7] = {
+    {"^minQuality", typeInt},
+    {"^checkRDN", typeInt},
+    {"^forbiddenChars", typeStr},
+    {"^maxConsecutivePerClass", typeInt},
+    {"^useCracklib", typeInt},
+    {"^cracklibDict", typeStr},
+    {"^class-.*", typeStr}
+};
+
+
+// configuration structure, containing a parameter, a value,
+// a corresponding min and minForPoint indicators if necessary
+// and a type for the value (typeInt or typeStr)
+typedef struct conf {
+    char param[PARAM_MAX_LEN];
+    valueType iType;
+    genValue value;
+    int min;
+    int minForPoint;
+} conf;
+
+void ppm_log(int priority, const char *format, ...);
+int min(char *str1, char *str2);
+#ifndef PPM_READ_FILE
+  static void read_config_attr(conf * fileConf, int *numParam, char *ppm_config_attr);
+#endif
+#ifdef PPM_READ_FILE
+  static void read_config_file(conf * fileConf, int *numParam, char *ppm_config_file);
+#endif
+int check_password(char *pPasswd, char **ppErrStr, Entry *e, void *pArg);
+int maxConsPerClass(char *password, char *charClass);
+void storeEntry(char *param, char *value, valueType valType, 
+           char *min, char *minForPoint, conf * fileConf, int *numParam);
+int typeParam(char* param);
+genValue* getValue(conf *fileConf, int numParam, char* param);
+void strcpy_safe(char *dest, char *src, int length_dest);
+
+
+int ppm_test = 0;
+
+#endif
diff --git a/contrib/slapd-modules/ppm/ppm_test.c b/contrib/slapd-modules/ppm/ppm_test.c
new file mode 100644 (file)
index 0000000..520aa0a
--- /dev/null
@@ -0,0 +1,66 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include "ppm.h"
+
+int main(int argc, char *argv[])
+{
+  /*
+   * argv[1]: user
+   * argv[2]: password
+   * argv[3]: configuration file
+   */
+
+  int ret = 1;
+
+  if(argc > 2)
+  {
+    printf("Testing user %s password: '%s' against %s policy config file \n",
+            argv[1], argv[2], argv[3]
+          );
+
+    /* format user entry */
+    char *errmsg = NULL;
+    Entry pEntry;
+    pEntry.e_nname.bv_val=argv[1];
+    pEntry.e_name.bv_val=argv[1];
+
+    /* get configuration file content */
+    struct berval pArg;
+    FILE *fp;
+    if ((fp = fopen(argv[3],"r")) == NULL)
+    {
+      fprintf(stderr,"Unable to open config file for reading\n");
+      return ret;
+    }
+    char *fcontent = NULL;
+    fseek(fp, 0, SEEK_END);
+    long fsize = ftell(fp);
+    fseek(fp, 0, SEEK_SET);
+    fcontent = malloc(fsize);
+    fread(fcontent, 1, fsize, fp);
+    fclose(fp);
+    pArg.bv_val = fcontent;
+  
+    ppm_test=1; // enable ppm_test for informing ppm not to use syslog
+
+    ret = check_password(argv[2], &errmsg, &pEntry, &pArg);
+
+    if(ret == 0)
+    {
+      printf("Password is OK!\n");
+    }
+    else
+    {
+      printf("Password failed checks : %s\n", errmsg);
+    }
+
+    ber_memfree(errmsg);
+    return ret;
+
+  }
+
+  return ret;
+}
+
+
+
diff --git a/contrib/slapd-modules/ppm/unit_tests.sh b/contrib/slapd-modules/ppm/unit_tests.sh
new file mode 100755 (executable)
index 0000000..80d66e0
--- /dev/null
@@ -0,0 +1,117 @@
+#!/bin/bash
+
+# Launch unitary tests
+# 
+
+
+CONFIG_FILE="ppm.conf"
+
+OLDAP_SOURCES="../../.."
+CURRENT_DIR=$( dirname $0 )
+LIB_PATH="${LD_LIBRARY_PATH}:${CURRENT_DIR}:${OLDAP_SOURCES}/libraries/liblber/.libs:${OLDAP_SOURCES}/libraries/libldap/.libs"
+
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+NC='\033[0m'
+
+RESULT=0
+
+PPM_CONF_1='minQuality 3
+checkRDN 0
+forbiddenChars 
+maxConsecutivePerClass 0
+useCracklib 0
+cracklibDict /var/cache/cracklib/cracklib_dict
+class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 0 1
+class-lowerCase abcdefghijklmnopqrstuvwxyz 0 1
+class-digit 0123456789 0 1
+class-special <>,?;.:/!§ù%*µ^¨$£²&é~"#'\''{([-|è`_\ç^à@)]°=}+ 0 1'
+
+PPM_CONF_2='minQuality 3
+checkRDN 0
+forbiddenChars à
+maxConsecutivePerClass 5
+useCracklib 0
+cracklibDict /var/cache/cracklib/cracklib_dict
+class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 2 4
+class-lowerCase abcdefghijklmnopqrstuvwxyz 3 4
+class-digit 0123456789 2 4
+class-special <>,?;.:/!§ù%*µ^¨$£²&é~"#'\''{([-|è`_\ç^à@)]°=}+ 0 4'
+
+PPM_CONF_3='minQuality 3
+checkRDN 1
+forbiddenChars 
+maxConsecutivePerClass 0
+useCracklib 0
+cracklibDict /var/cache/cracklib/cracklib_dict
+class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 0 1
+class-lowerCase abcdefghijklmnopqrstuvwxyz 0 1
+class-digit 0123456789 0 1
+class-special <>,?;.:/!§ù%*µ^¨$£²&é~"#'\''{([-|è`_\ç^à@)]°=}+ 0 1'
+
+
+echo "$PPM_CONF_1" > ppm1.conf
+echo "$PPM_CONF_2" > ppm2.conf
+echo "$PPM_CONF_3" > ppm3.conf
+
+
+launch_test()
+{
+  # launch tests
+  # FORMAT: launch_test [conf_file] [password] [expected_result]
+  # [expected_result] = [PASS|FAIL]
+
+  local CONF="$1"
+  local USER="$2"
+  local PASS="$3"
+  local EXPECT="$4"
+
+  [[ $EXPECT == "PASS" ]] && EXP="0" || EXP="1"
+
+  LD_LIBRARY_PATH="${LIB_PATH}" ./ppm_test "${USER}" "${PASS}" "${CONF}"
+  RES="$?"
+
+  if [ "$RES" -eq "$EXP" ] ; then
+    echo -e "conf=${CONF} user=${USER} pass=${PASS} expect=${EXPECT}... ${GREEN}PASS${NC}"
+  else
+    echo -e "conf=${CONF} user=${USER} pass=${PASS} expect=${EXPECT}... ${RED}FAIL${NC}"
+    ((RESULT+=1))
+  fi
+
+  echo
+}
+
+
+
+
+launch_test "ppm1.conf" "uid=test,ou=users,dc=my-domain,dc=com" "azerty" "FAIL"
+launch_test "ppm1.conf" "uid=test,ou=users,dc=my-domain,dc=com" "azeRTY" "FAIL"
+launch_test "ppm1.conf" "uid=test,ou=users,dc=my-domain,dc=com" "azeRTY123" "PASS"
+launch_test "ppm1.conf" "uid=test,ou=users,dc=my-domain,dc=com" "azeRTY." "PASS"
+
+
+launch_test "ppm2.conf" "uid=test,ou=users,dc=my-domain,dc=com" "AAaaa01AAaaa01AAaaa0" "PASS"
+# forbidden char
+launch_test "ppm2.conf" "uid=test,ou=users,dc=my-domain,dc=com" "AAaaa01AAaaa01AAaaaà" "FAIL"
+# too much consecutive for upper
+launch_test "ppm2.conf" "uid=test,ou=users,dc=my-domain,dc=com" "AAaaa01AAaaa01AAAAAA" "FAIL"
+# not enough upper
+launch_test "ppm2.conf" "uid=test,ou=users,dc=my-domain,dc=com" "Aaaaa01aaaaa01aa.;.;" "FAIL"
+# not enough lower
+launch_test "ppm2.conf" "uid=test,ou=users,dc=my-domain,dc=com" "aaAAA01BB0123AAA.;.;" "FAIL"
+# not enough digit
+launch_test "ppm2.conf" "uid=test,ou=users,dc=my-domain,dc=com" "1AAAA.;BBB.;.;AA.;.;" "FAIL"
+# not enough points (no point for digit)
+launch_test "ppm2.conf" "uid=test,ou=users,dc=my-domain,dc=com" "AAaaaBBBBaaa01AAaaaa" "FAIL"
+
+# password in RDN
+launch_test "ppm3.conf" "uid=User_Password10-test,ou=users,dc=my-domain,dc=com" "Password10" "FAIL"
+launch_test "ppm3.conf" "uid=User_Passw0rd-test,ou=users,dc=my-domain,dc=com" "Password10" "PASS"
+launch_test "ppm3.conf" "uid=User-Pw-Test,ou=users,dc=my-domain,dc=com" "Password10" "PASS"
+
+
+echo "${RESULT} error(s) encountered"
+
+rm ppm1.conf ppm2.conf ppm3.conf
+exit ${RESULT}
+