summaryrefslogtreecommitdiffstats
path: root/contrib/slapd-modules/ppm
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/slapd-modules/ppm')
-rw-r--r--contrib/slapd-modules/ppm/CHANGELOG.md48
-rw-r--r--contrib/slapd-modules/ppm/CONTRIBUTIONS.md5
-rw-r--r--contrib/slapd-modules/ppm/INSTALL.md51
-rw-r--r--contrib/slapd-modules/ppm/LICENSE50
-rw-r--r--contrib/slapd-modules/ppm/Makefile97
-rw-r--r--contrib/slapd-modules/ppm/README.md1
-rw-r--r--contrib/slapd-modules/ppm/ppm.c895
-rw-r--r--contrib/slapd-modules/ppm/ppm.example97
-rw-r--r--contrib/slapd-modules/ppm/ppm.h145
-rw-r--r--contrib/slapd-modules/ppm/ppm.md334
-rw-r--r--contrib/slapd-modules/ppm/ppm_test.c80
-rw-r--r--contrib/slapd-modules/ppm/slapm-ppm.5353
-rwxr-xr-xcontrib/slapd-modules/ppm/unit_tests.sh126
13 files changed, 2282 insertions, 0 deletions
diff --git a/contrib/slapd-modules/ppm/CHANGELOG.md b/contrib/slapd-modules/ppm/CHANGELOG.md
new file mode 100644
index 0000000..e3d2ac0
--- /dev/null
+++ b/contrib/slapd-modules/ppm/CHANGELOG.md
@@ -0,0 +1,48 @@
+# CHANGELOG
+
+* 2022-05-17 David Coutadeur <david.coutadeur@gmail.com>
+ implement a maximum number of characters for each class #18
+ upgrade documentation for new olcPPolicyCheckModule in OpenLDAP 2.6 #30
+ Make one unique code of development for 2.5 and 2.6 OpenLDAP versions #35
+ fix segmentation fault in ppm_test #36
+ various minor fixes and optimizations
+ Version 2.2
+* 2022-03-22 David Coutadeur <david.coutadeur@gmail.com>
+ Reject password if it contains tokens from an attribute of the LDAP entry #17
+ Version 2.1
+* 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 install. 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/CONTRIBUTIONS.md b/contrib/slapd-modules/ppm/CONTRIBUTIONS.md
new file mode 100644
index 0000000..3d8b8ac
--- /dev/null
+++ b/contrib/slapd-modules/ppm/CONTRIBUTIONS.md
@@ -0,0 +1,5 @@
+# CONTRIBUTIONS
+
+* 2014 - 2022 - 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
diff --git a/contrib/slapd-modules/ppm/INSTALL.md b/contrib/slapd-modules/ppm/INSTALL.md
new file mode 100644
index 0000000..b4cba40
--- /dev/null
+++ b/contrib/slapd-modules/ppm/INSTALL.md
@@ -0,0 +1,51 @@
+INSTALLATION
+============
+
+Dependencies
+------------------
+ppm is provided along with OpenLDAP sources. By default, it is available into contrib/slapd-modules.
+ - make sure both OpenLDAP sources and ppm are available for building.
+ - install cracklib development files if you want to test passwords against cracklib
+ - install pandoc if you want to build the man page
+
+
+Build
+-----
+Enter contrib/slapd-modules/ppm directory
+
+You can optionally customize some variables if you don't want the default ones:
+- prefix: prefix of the path where ppm is to be installed (defaults to /usr/local)
+- ldap_subdir: OpenLDAP specific subdirectory for modules and configurations (defaults to openldap )
+- moduledir: where the ppm module is to be deployed (defaults to $prefix/$libexecdir/$ldap_subdir)
+- etcdir: used to compose default sysconfdir location (defaults to $prefix/etc)
+- sysconfdir: where the ppm example policy is to be deployed (defaults to $prefix/$etcdir/$ldap_subdir)
+- LDAP_SRC: path to OpenLDAP source directory
+- Options in DEFS variable:
+ CONFIG_FILE: (DEPRECATED) path to a ppm configuration file (see PPM_READ_FILE in ppm.h)
+ note: ppm configuration now lies into pwdCheckModuleArg password policy attribute
+ provided example file is only helpful as an example or for testing
+ CRACKLIB: if defined, link against cracklib
+ DEBUG: If defined, ppm logs its actions with syslog
+
+
+To build ppm, simply run these commands:
+(based upon the default prefix /usr/local of OpenLDAP)
+
+```
+make clean
+make
+make test
+make doc
+make install
+```
+
+Here is an illustrative example showing how to overload some options:
+
+```
+make clean
+make LDAP_SRC=../../.. prefix=/usr/local libdir=/usr/local/lib
+make test LDAP_SRC=../../..
+make doc prefix=/usr/local
+make install prefix=/usr/local libdir=/usr/local/lib
+```
+
diff --git a/contrib/slapd-modules/ppm/LICENSE b/contrib/slapd-modules/ppm/LICENSE
new file mode 100644
index 0000000..03f692b
--- /dev/null
+++ b/contrib/slapd-modules/ppm/LICENSE
@@ -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
index 0000000..835dd67
--- /dev/null
+++ b/contrib/slapd-modules/ppm/Makefile
@@ -0,0 +1,97 @@
+# $OpenLDAP$
+# Copyright 2014 David Coutadeur, Paris.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted only as authorized by the OpenLDAP
+# Public License.
+#
+# A copy of this license is available in the file LICENSE in the
+# top-level directory of the distribution or, alternatively, at
+# <http://www.OpenLDAP.org/license.html>.
+
+LDAP_SRC = ../../..
+LDAP_BUILD = $(LDAP_SRC)
+LDAP_INC = -I$(LDAP_BUILD)/include -I$(LDAP_SRC)/include -I$(LDAP_SRC)/servers/slapd
+LDAP_LIB = $(LDAP_BUILD)/libraries/liblber/liblber.la $(LDAP_BUILD)/libraries/libldap/libldap.la
+
+LIBTOOL = $(LDAP_BUILD)/libtool
+INSTALL = /usr/bin/install
+CC = gcc
+OPT = -g -O2 -fpic
+
+# To skip linking against CRACKLIB make CRACK=no
+CRACK=yes
+CRACKDEF_yes= -DCRACKLIB
+CRACKDEF_no=
+
+CRACKLIB_yes= -lcrack
+CRACKLIB_no=
+
+CRACKDEF=$(CRACKDEF_$(CRACK))
+CRACKLIB=$(CRACKLIB_$(CRACK))
+
+DEFS = -DDEBUG $(CRACKDEF)
+# Define if using a config file:
+# -DCONFIG_FILE="\"$(sysconfdir)/$(EXAMPLE)\""
+
+INCS = $(LDAP_INC)
+LIBS = $(LDAP_LIB)
+
+PROGRAMS=ppm.so
+LTVER = 0:0:0
+
+LDAP_LIBS = -L$(LDAP_BUILD)/libraries/liblber/.libs -L$(LDAP_BUILD)/libraries/libldap/.libs -lldap -llber
+
+prefix=/usr/local
+exec_prefix=$(prefix)
+ldap_subdir=/openldap
+
+libdir=$(exec_prefix)/lib
+libexecdir=$(exec_prefix)/libexec
+moduledir = $(libexecdir)$(ldap_subdir)
+mandir = $(exec_prefix)/share/man
+man5dir = $(mandir)/man5
+etcdir = $(exec_prefix)/etc
+sysconfdir = $(etcdir)$(ldap_subdir)
+
+TEST=ppm_test
+EXAMPLE=ppm.example
+TESTS=./unit_tests.sh
+
+MANDOC=slapm-ppm.5
+MDDOC=ppm.md
+
+all: ppm $(TEST)
+
+$(TEST): ppm
+ $(CC) $(CFLAGS) $(OPT) $(CPPFLAGS) $(DEFS) $(LDFLAGS) $(INCS) -Wl,-rpath=. -o $(TEST) ppm_test.c $(PROGRAMS) $(LDAP_LIBS) $(CRACKLIB)
+
+ppm.o:
+ $(CC) $(CFLAGS) $(OPT) $(CPPFLAGS) $(DEFS) -c $(INCS) ppm.c
+
+ppm: ppm.o
+ $(CC) $(LDFLAGS) $(INCS) -shared -o $(PROGRAMS) ppm.o $(CRACKLIB)
+
+install: ppm
+ mkdir -p $(DESTDIR)$(moduledir)
+ for p in $(PROGRAMS); do \
+ $(LIBTOOL) --mode=install cp $$p $(DESTDIR)/$(moduledir) ; \
+ done
+ $(INSTALL) -m 644 $(EXAMPLE) $(DESTDIR)$(sysconfdir)/
+ $(INSTALL) -m 644 $(MANDOC) $(DESTDIR)$(man5dir)/
+# $(INSTALL) -m 755 $(TEST) $(libdir)
+
+.PHONY: clean
+
+clean:
+ $(RM) -f ppm.o $(PROGRAMS) ppm.lo $(TEST)
+ $(RM) -rf .libs
+
+test: ppm $(TEST)
+ LDAP_SRC=$(LDAP_SRC) $(TESTS)
+
+doc:
+ pandoc $(MDDOC) -s -t man -o $(MANDOC)
+ sed -i -e 's#ETCDIR#$(DESTDIR)$(sysconfdir)#g' $(MANDOC)
+
diff --git a/contrib/slapd-modules/ppm/README.md b/contrib/slapd-modules/ppm/README.md
new file mode 100644
index 0000000..129f788
--- /dev/null
+++ b/contrib/slapd-modules/ppm/README.md
@@ -0,0 +1 @@
+See ppm.md manual and INSTALL.md
diff --git a/contrib/slapd-modules/ppm/ppm.c b/contrib/slapd-modules/ppm/ppm.c
new file mode 100644
index 0000000..de8a49c
--- /dev/null
+++ b/contrib/slapd-modules/ppm/ppm.c
@@ -0,0 +1,895 @@
+/*
+ * ppm.c for OpenLDAP
+ *
+ * See LICENSE, README and INSTALL files
+ */
+
+
+/*
+ password policy module is called with (openldap 2.6):
+ int check_password (char *pPasswd, struct berval *ppErrmsg, Entry *e, void *pArg)
+
+ password policy module is called with (openldap 2.5):
+ int check_password (char *pPasswd, char **ppErrStr, Entry *e, void *pArg)
+
+ *pPasswd: new password
+ **ppErrStr: pointer to the string containing the error message
+ *ppErrmsg: pointer to a struct berval containing space for an error message of length bv_len
+ *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, char *max, conf * fileConf,
+ int *numParam)
+{
+ int i = 0;
+ int iMin;
+ int iMinForPoint;
+ int iMax;
+ if (min == NULL || strcmp(min,"") == 0)
+ iMin = 0;
+ else
+ iMin = atoi(min);
+
+ if (minForPoint == NULL || strcmp(minForPoint,"") == 0)
+ iMinForPoint = 0;
+ else
+ iMinForPoint = atoi(minForPoint);
+
+ if (max == NULL || strcmp(max,"") == 0)
+ iMax = 0;
+ else
+ iMax = atoi(max);
+
+ // 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;
+ fileConf[i].max = iMax;
+ 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;
+ fileConf[*numParam].max = iMax;
+ ++(*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);
+ return n;
+ }
+
+ // 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, *max;
+
+ 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';
+ max = strtok_r(NULL, " \t", &saveptr2);
+ if (max != NULL)
+ if (strchr(max, '\n') != NULL)
+ strchr(max, '\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, max = %s",
+ word, value, min, minForPoint, max);
+
+ storeEntry(word, value, allowedParameters[nParam].iType,
+ min, minForPoint, max, 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, *max;
+
+ 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';
+ max = strtok(NULL, " \t");
+ if (max != NULL)
+ if (strchr(max, '\n') != NULL)
+ strchr(max, '\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, max = %s",
+ word, value, min, minForPoint, max);
+
+ storeEntry(word, value, allowedParameters[nParam].iType,
+ min, minForPoint, max, fileConf, numParam);
+ }
+ else
+ {
+ ppm_log(LOG_NOTICE,
+ "ppm: Parameter '%s' rejected", word);
+ }
+
+ }
+ }
+
+ fclose(config);
+ }
+
+#endif
+
+static int
+#if OLDAP_VERSION == 0x0205
+realloc_error_message(char **target, int curlen, int nextlen)
+#else
+realloc_error_message(const char *orig, char **target, int curlen, int nextlen)
+#endif
+{
+ if (curlen < nextlen + MEMORY_MARGIN) {
+ ppm_log(LOG_WARNING,
+ "ppm: Reallocating szErrStr from %d to %d", curlen,
+ nextlen + MEMORY_MARGIN);
+#if OLDAP_VERSION == 0x0205
+ ber_memfree(*target);
+#else
+ if (*target != orig)
+ ber_memfree(*target);
+#endif
+ 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);
+ return 0;
+ }
+
+ // 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;
+}
+
+// Does the password contains a token from an attribute?
+int
+containsAttributes(char* passwd, Entry* pEntry, char* checkAttributes)
+{
+ char checkAttrs[VALUE_MAX_LEN];
+ char val[VALUE_MAX_LEN];
+ char * token;
+ char * tk;
+ Attribute *a;
+ int i;
+ regex_t regex;
+ int reti;
+
+ // Parse all attributes in the LDAP entry
+ for ( a = pEntry->e_attrs; a != NULL; a = a->a_next ) {
+
+ // Parse all attributes in checkAttributes parameter
+ strcpy_safe(checkAttrs, checkAttributes, VALUE_MAX_LEN);
+ token = strtok(checkAttrs, ",\0");
+ while (token != NULL)
+ {
+ // if attribute to check is found in LDAP entry
+ if( strcmp(a->a_desc->ad_cname.bv_val, token) == 0 )
+ {
+ ppm_log(LOG_NOTICE, "ppm: check password against %s attribute", token);
+ // parse attribute values
+ for ( i = 0; a->a_vals[i].bv_val != NULL; i++ )
+ {
+ strcpy_safe(val, a->a_vals[i].bv_val, VALUE_MAX_LEN);
+ ppm_log(LOG_NOTICE,
+ "ppm: check password against %s attribute value",
+ a->a_vals[i].bv_val);
+
+ // Search for each token in the attribute
+ tk = strtok(val, ATTR_TOKENS_DELIMITERS);
+
+ while (tk != NULL)
+ {
+ if (strlen(tk) > 2)
+ {
+ ppm_log(LOG_NOTICE,
+ "ppm: Checking if %s part of value %s of attribute %s matches the password",
+ tk,
+ a->a_vals[i].bv_val,
+ a->a_desc->ad_cname.bv_val);
+ // Compile regular expression
+ reti = regcomp(&regex, tk, REG_ICASE);
+ if (reti) {
+ ppm_log(LOG_ERR, "ppm: Cannot compile regex: %s", tk);
+ return 0;
+ }
+
+ // Execute regular expression: does password
+ // contains the token extracted from the attr value?
+ reti = regexec(&regex, passwd, 0, NULL, 0);
+ if (!reti)
+ {
+ regfree(&regex);
+ return 1;
+ }
+
+ regfree(&regex);
+ }
+ else
+ {
+ ppm_log(LOG_NOTICE,
+ "ppm: %s part of value %s of attribute %s is too short to be checked",
+ tk,
+ a->a_vals[i].bv_val,
+ a->a_desc->ad_cname.bv_val);
+ }
+ tk = strtok(NULL, ATTR_TOKENS_DELIMITERS);
+ }
+ }
+ }
+ token = strtok(NULL, ",\0");
+ }
+ }
+
+ return 0;
+}
+
+
+int
+#if OLDAP_VERSION == 0x0205
+check_password(char *pPasswd, char **ppErrStr, Entry *e, void *pArg)
+#else
+check_password(char *pPasswd, struct berval *ppErrmsg, Entry *e, void *pArg)
+#endif
+{
+
+ Entry *pEntry = e;
+ struct berval *pwdCheckModuleArg = pArg;
+#if OLDAP_VERSION == 0x0205
+ char *szErrStr = (char *) ber_memalloc(MEM_INIT_SZ);
+ int mem_len = MEM_INIT_SZ;
+#else
+ char *origmsg = ppErrmsg->bv_val;
+ char *szErrStr = origmsg;
+ int mem_len = ppErrmsg->bv_len;
+#endif
+ 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 checkAttributes[VALUE_MAX_LEN];
+ char forbiddenChars[VALUE_MAX_LEN];
+ int nForbiddenChars = 0;
+ int nQuality = 0;
+ int maxConsecutivePerClass;
+ int nbInClass[CONF_MAX_SIZE];
+ int i,j;
+
+ ppm_log(LOG_NOTICE, "ppm: entry %s", pEntry->e_nname.bv_val);
+
+#ifdef PPM_READ_FILE
+ /* Determine if config file is to be read (DEPRECATED) */
+ char ppm_config_file[FILENAME_MAX_LEN];
+
+ ppm_log(LOG_NOTICE, "ppm: Not reading pwdCheckModuleArg attribute");
+ ppm_log(LOG_NOTICE, "ppm: instead, read configuration file (deprecated)");
+
+ 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);
+#else
+ if ( !pwdCheckModuleArg || !pwdCheckModuleArg->bv_val ) {
+ ppm_log(LOG_ERR, "ppm: No config provided in pwdCheckModuleArg");
+#if OLDAP_VERSION == 0x0205
+ mem_len = realloc_error_message(&szErrStr, mem_len,
+#else
+ mem_len = realloc_error_message(origmsg, &szErrStr, mem_len,
+#endif
+ strlen(GENERIC_ERROR));
+ sprintf(szErrStr, GENERIC_ERROR);
+ goto fail;
+ }
+
+ ppm_log(LOG_NOTICE, "ppm: Reading pwdCheckModuleArg attribute");
+ ppm_log(LOG_NOTICE, "ppm: RAW configuration: %s", pwdCheckModuleArg->bv_val);
+#endif
+
+ /* Set default values */
+ conf fileConf[CONF_MAX_SIZE] = {
+ {"minQuality", typeInt, {.iVal = DEFAULT_QUALITY}, 0, 0, 0
+ }
+ ,
+ {"checkRDN", typeInt, {.iVal = 0}, 0, 0, 0
+ }
+ ,
+ {"forbiddenChars", typeStr, {.sVal = ""}, 0, 0, 0
+ }
+ ,
+ {"maxConsecutivePerClass", typeInt, {.iVal = 0}, 0, 0, 0
+ }
+ ,
+ {"useCracklib", typeInt, {.iVal = 0}, 0, 0, 0
+ }
+ ,
+ {"cracklibDict", typeStr, {.sVal = "/var/cache/cracklib/cracklib_dict"}, 0, 0, 0
+ }
+ ,
+ {"class-upperCase", typeStr, {.sVal = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"}, 0, 1, 0
+ }
+ ,
+ {"class-lowerCase", typeStr, {.sVal = "abcdefghijklmnopqrstuvwxyz"}, 0, 1, 0
+ }
+ ,
+ {"class-digit", typeStr, {.sVal = "0123456789"}, 0, 1, 0
+ }
+ ,
+ {"class-special", typeStr,
+ {.sVal = "<>,?;.:/!§ù%*µ^¨$£²&é~\"#'{([-|è`_\\ç^à@)]°=}+"}, 0, 1, 0
+ }
+ ,
+ {"checkAttributes", typeStr, {.sVal = ""}, 0, 0, 0
+ }
+ };
+ numParam = 11;
+
+ #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);
+ strcpy_safe(checkAttributes,
+ getValue(fileConf, numParam, "checkAttributes")->sVal,
+ VALUE_MAX_LEN);
+
+ for (i = 0; i < numParam; i++)
+ nbInClass[i] = 0;
+
+
+ /*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 contains at most max 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 < numParam; 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) {
+#if OLDAP_VERSION == 0x0205
+ mem_len = realloc_error_message(&szErrStr, mem_len,
+#else
+ mem_len = realloc_error_message(origmsg, &szErrStr, mem_len,
+#endif
+ 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 minimum criteria
+ for (i = 0; i < numParam; 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
+#if OLDAP_VERSION == 0x0205
+ mem_len = realloc_error_message(&szErrStr, mem_len,
+#else
+ mem_len = realloc_error_message(origmsg, &szErrStr, mem_len,
+#endif
+ strlen(PASSWORD_MIN_CRITERIA) +
+ strlen(pEntry->e_nname.bv_val) +
+ 2 + PARAM_MAX_LEN);
+ sprintf(szErrStr, PASSWORD_MIN_CRITERIA, pEntry->e_nname.bv_val,
+ fileConf[i].min, fileConf[i].param);
+ goto fail;
+ }
+ }
+ }
+
+ // Password checking done, now loocking for maximum criteria
+ for (i = 0; i < numParam; i++) {
+ if (strstr(fileConf[i].param, "class-") != NULL) {
+ if ( (fileConf[i].max != 0) &&
+ (nbInClass[i] > fileConf[i].max) &&
+ strlen(fileConf[i].value.sVal) != 0) {
+ // constraint is not satisfied... goto fail
+#if OLDAP_VERSION == 0x0205
+ mem_len = realloc_error_message(&szErrStr, mem_len,
+#else
+ mem_len = realloc_error_message(origmsg, &szErrStr, mem_len,
+#endif
+ strlen(PASSWORD_MAX_CRITERIA) +
+ strlen(pEntry->e_nname.bv_val) +
+ 2 + PARAM_MAX_LEN);
+ sprintf(szErrStr, PASSWORD_MAX_CRITERIA, pEntry->e_nname.bv_val,
+ fileConf[i].max, fileConf[i].param);
+ goto fail;
+ }
+ }
+ }
+
+ // Password checking done, now loocking for forbiddenChars criteria
+ if (nForbiddenChars > 0) { // at least 1 forbidden char... goto fail
+#if OLDAP_VERSION == 0x0205
+ mem_len = realloc_error_message(&szErrStr, mem_len,
+#else
+ mem_len = realloc_error_message(origmsg, &szErrStr, mem_len,
+#endif
+ 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 < numParam; 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);
+#if OLDAP_VERSION == 0x0205
+ mem_len = realloc_error_message(&szErrStr, mem_len,
+#else
+ mem_len = realloc_error_message(origmsg, &szErrStr, mem_len,
+#endif
+ 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]);
+#if OLDAP_VERSION == 0x0205
+ mem_len = realloc_error_message(&szErrStr, mem_len,
+#else
+ mem_len = realloc_error_message(origmsg, &szErrStr, mem_len,
+#endif
+ 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);
+#if OLDAP_VERSION == 0x0205
+ mem_len = realloc_error_message(&szErrStr, mem_len,
+#else
+ mem_len = realloc_error_message(origmsg, &szErrStr, mem_len,
+#endif
+ 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
+ {
+#if OLDAP_VERSION == 0x0205
+ mem_len = realloc_error_message(&szErrStr, mem_len,
+#else
+ mem_len = realloc_error_message(origmsg, &szErrStr, mem_len,
+#endif
+ strlen(RDN_TOKEN_FOUND) +
+ strlen(pEntry->e_nname.bv_val));
+ sprintf(szErrStr, RDN_TOKEN_FOUND, pEntry->e_nname.bv_val);
+
+ goto fail;
+ }
+
+ // Password checking done, now looking for checkAttributes criteria
+ if ( strcmp(checkAttributes, "") !=0 &&
+ containsAttributes(pPasswd, pEntry, checkAttributes))
+ // A token from an attribute is found in password: goto fail
+ {
+#if OLDAP_VERSION == 0x0205
+ mem_len = realloc_error_message(&szErrStr, mem_len,
+#else
+ mem_len = realloc_error_message(origmsg, &szErrStr, mem_len,
+#endif
+ strlen(ATTR_TOKEN_FOUND) +
+ strlen(pEntry->e_nname.bv_val));
+ sprintf(szErrStr, ATTR_TOKEN_FOUND, pEntry->e_nname.bv_val);
+
+ goto fail;
+ }
+
+#if OLDAP_VERSION == 0x0205
+ *ppErrStr = strdup("");
+ ber_memfree(szErrStr);
+#else
+ szErrStr[0] = '\0';
+#endif
+ return (LDAP_SUCCESS);
+
+ fail:
+#if OLDAP_VERSION == 0x0205
+ *ppErrStr = strdup(szErrStr);
+ ber_memfree(szErrStr);
+#else
+ ppErrmsg->bv_val = szErrStr;
+ ppErrmsg->bv_len = mem_len;
+#endif
+ return (EXIT_FAILURE);
+
+}
diff --git a/contrib/slapd-modules/ppm/ppm.example b/contrib/slapd-modules/ppm/ppm.example
new file mode 100644
index 0000000..9507348
--- /dev/null
+++ b/contrib/slapd-modules/ppm/ppm.example
@@ -0,0 +1,97 @@
+# Example of ppm configuration
+
+# Such configuration must be stored into pwdCheckModuleArg attribute
+# of a password policy entry
+# See slapo-ppolicy for more details
+# Here is an example of such password policy:
+# dn: cn=default,ou=policies,dc=my-domain,dc=com
+# objectClass: pwdPolicy
+# objectClass: top
+# objectClass: pwdPolicyChecker
+# objectClass: person
+# pwdCheckQuality: 2
+# pwdAttribute: userPassword
+# sn: default
+# cn: default
+# pwdMinLength: 6
+# pwdCheckModule: /usr/local/lib/ppm.so
+# pwdCheckModuleArg:: bWluUXVhbGl0eSAzCmNoZWNrUkROIDAKY2hlY2tBdHRyaWJ1dGVzCmZvcmJpZGRlbkNoYXJzCm1heENvbnNlY3V0aXZlUGVyQ2xhc3MgMAp1c2VDcmFja2xpYiAwCmNyYWNrbGliRGljdCAvdmFyL2NhY2hlL2NyYWNrbGliL2NyYWNrbGliX2RpY3QKY2xhc3MtdXBwZXJDYXNlIEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaIDAgMSAwCmNsYXNzLWxvd2VyQ2FzZSBhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5eiAwIDEgMApjbGFzcy1kaWdpdCAwMTIzNDU2Nzg5IDAgMSAwCmNsYXNzLXNwZWNpYWwgPD4sPzsuOi8hwqfDuSUqwrVewqgkwqPCsibDqX4iIyd7KFstfMOoYF9cw6dew6BAKV3CsD19KyAwIDEgMAo=
+#
+# Different parameters are separated by a linefeed (\n)
+# Parameters starting with a # are ignored
+# Use a base64 tool to code / decode the content of pwdCheckModuleArg
+
+
+
+# Parameters
+
+# 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 these delimiters : space tabulation _ - , ; £
+checkRDN 0
+
+# checkAttributes parameter
+# Format:
+# checkAttributes [ATTR1,ATTR2,...]
+# Description:
+# Password must not contain a token from the values in the given list of attributes
+# Tokens are substrings of the values of the given attributes,
+# delimited by: space tabulation _ - , ; @
+# For example, if uid="the wonderful entry",
+# password must not contain "the", nor "wonderful", nor "entry"
+checkAttributes
+
+# 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_dictionary]
+# 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 dictionary 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
+# [MAX]: if > [MAX] occurrences of characters from this class are found, then the password is rejected (0 means no maximum)
+class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 0 1 0
+class-lowerCase abcdefghijklmnopqrstuvwxyz 0 1 0
+class-digit 0123456789 0 1 0
+class-special <>,?;.:/!§ù%*µ^¨$£²&é~"#'{([-|è`_\ç^à@)]°=}+ 0 1 0
diff --git a/contrib/slapd-modules/ppm/ppm.h b/contrib/slapd-modules/ppm/ppm.h
new file mode 100644
index 0000000..5e07d98
--- /dev/null
+++ b/contrib/slapd-modules/ppm/ppm.h
@@ -0,0 +1,145 @@
+/*
+ * 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
+
+// Get OpenLDAP version
+#define OLDAP_VERSION ((LDAP_VENDOR_VERSION_MAJOR << 8) | LDAP_VENDOR_VERSION_MINOR)
+// OLDAP_VERSION = 0x0205 // (v2.5)
+// OLDAP_VERSION = 0x0206 // (v2.6)
+
+//#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.example"
+#endif
+#define FILENAME_MAX_LEN 512
+
+#define DEFAULT_QUALITY 3
+#define MEMORY_MARGIN 50
+#if OLDAP_VERSION == 0x0205
+ #define MEM_INIT_SZ 64
+#endif
+#define DN_MAX_LEN 512
+
+#define CONF_MAX_SIZE 50
+#define PARAM_MAX_LEN 32
+#define VALUE_MAX_LEN 512
+#define ATTR_NAME_MAX_LEN 150
+
+#define PARAM_PREFIX_CLASS "class-"
+#define TOKENS_DELIMITERS " ,;-_£\t"
+#define ATTR_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_MIN_CRITERIA \
+ "Password for dn=\"%s\" has not reached the minimum number of characters (%d) for class %s"
+#define PASSWORD_MAX_CRITERIA \
+ "Password for dn=\"%s\" has reached the maximum 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 ATTR_TOKEN_FOUND \
+ "Password for dn=\"%s\" is too simple: it contains part of an attribute"
+#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[8] = {
+ {"^minQuality", typeInt},
+ {"^checkRDN", typeInt},
+ {"^checkAttributes", typeStr},
+ {"^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;
+ int max;
+} 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
+
+#if OLDAP_VERSION == 0x0205
+ int check_password(char *pPasswd, char **ppErrStr, Entry *e, void *pArg);
+#else
+ int check_password(char *pPasswd, struct berval *ppErrmsg, Entry *e, void *pArg);
+#endif
+int maxConsPerClass(char *password, char *charClass);
+void storeEntry(char *param, char *value, valueType valType,
+ char *min, char *minForPoint, char *max, 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.md b/contrib/slapd-modules/ppm/ppm.md
new file mode 100644
index 0000000..f5ec921
--- /dev/null
+++ b/contrib/slapd-modules/ppm/ppm.md
@@ -0,0 +1,334 @@
+---
+title: ppm
+section: 5
+header: File Formats Manual
+footer: ppm
+date: August 24, 2021
+---
+
+# NAME
+
+ppm (Password Policy Module) - extension of the password policy overlay
+
+# SYNOPSIS
+
+ETCDIR/ppm.example
+
+# DESCRIPTION
+
+**ppm** 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**.
+
+
+# USAGE
+
+Create a password policy entry and indicate the path of the ppm.so library
+and the content of the desired policy.
+Use a base64 tool to code / decode the content of the policy stored into
+**pwdCheckModuleArg**.
+
+Here is an example for OpenLDAP 2.6:
+
+```
+dn: cn=default,ou=policies,dc=my-domain,dc=com
+objectClass: pwdPolicy
+objectClass: top
+objectClass: pwdPolicyChecker
+objectClass: person
+pwdCheckQuality: 2
+pwdAttribute: userPassword
+sn: default
+cn: default
+pwdMinLength: 6
+pwdCheckModuleArg:: bWluUXVhbGl0eSAzCmNoZWNrUkROIDAKY2hlY2tBdHRyaWJ1dGVzCmZvcmJpZGRlbkNoYXJzCm1heENvbnNlY3V0aXZlUGVyQ2xhc3MgMAp1c2VDcmFja2xpYiAwCmNyYWNrbGliRGljdCAvdmFyL2NhY2hlL2NyYWNrbGliL2NyYWNrbGliX2RpY3QKY2xhc3MtdXBwZXJDYXNlIEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaIDAgMSAwCmNsYXNzLWxvd2VyQ2FzZSBhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5eiAwIDEgMApjbGFzcy1kaWdpdCAwMTIzNDU2Nzg5IDAgMSAwCmNsYXNzLXNwZWNpYWwgPD4sPzsuOi8hwqfDuSUqwrVewqgkwqPCsibDqX4iIyd7KFstfMOoYF9cw6dew6BAKV3CsD19KyAwIDEgMAoK
+pwdUseCheckModule: TRUE
+```
+
+For OpenLDAP 2.5, you must add a **pwdCheckModule** attribute pointing
+to the ppm module (for example /usr/local/lib/ppm.so),
+and remove the **pwdUseCheckModule** attribute.
+
+
+
+See **slapo-ppolicy** for more information, but to sum up:
+
+- enable ppolicy overlay in your database.
+- define a default password policy in OpenLDAP configuration or use pwdPolicySubentry attribute to point to the given policy.
+
+This example show the activation for a **slapd.conf** file for OpenLDAP 2.6
+(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_check_module /usr/local/openldap/libexec/openldap/ppm.so
+#ppolicy_use_lockout # for having more infos about the lockout
+```
+
+For OpenLDAP 2.5, you must remove **ppolicy_check_module** parameter as
+it is managed in the password policy definition
+
+
+# FEATURES
+
+Here are the main features:
+
+- 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.
+
+- passwords must have at the most x occurrences of characters from the
+corresponding character class, else they are rejected.
+
+- the three previous criteria 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 contains tokens from defined attributes, then it is rejected.
+
+- if a password does not pass cracklib check, then it is rejected.
+
+
+# CONFIGURATION
+
+Since OpenLDAP 2.5 version, ppm configuration is held in a binary
+attribute of the password policy: **pwdCheckModuleArg**
+
+The example file (**ETCDIR/ppm.example** 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] [max]
+```
+
+with spaces being delimiters and Line Feed (LF) ending the line.
+
+Parameter names **are** case sensitive.
+
+Lines beginning by a **#** are considered as comments.
+
+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
+
+# checkAttributes parameter
+# Format:
+# checkAttributes [ATTR1,ATTR2,...]
+# Description:
+# Password must not contain a token from the values in the given list of attributes
+# Tokens are substrings of the values of the given attributes,
+# delimited by: space tabulation _ - , ; @
+# For example, if uid="the wonderful entry",
+# password must not contain "the", nor "wonderful", nor "entry"
+checkAttributes
+
+# 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_dictionary]
+# 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 dictionary files
+cracklibDict /var/cache/cracklib/cracklib_dict
+
+# classes parameter
+# Format:
+# class-[CLASS_NAME] [CHARACTERS_DEFINING_CLASS] [MIN] [MIN_FOR_POINT] [MAX]
+# 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
+# [MAX]: if > [MAX] occurrences of characters from this class are found, then the password is rejected (0 means no maximum)
+class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 0 1 0
+class-lowerCase abcdefghijklmnopqrstuvwxyz 0 1 0
+class-digit 0123456789 0 1 0
+class-special <>,?;.:/!§ù%*µ^¨$£²&é~"#'{([-|è`_\ç^à@)]°=}+ 0 1 0
+```
+
+# EXAMPLE
+
+With this policy:
+```
+minQuality 4
+forbiddenChars .?,
+checkRDN 1
+checkAttributes mail
+class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 0 5 0
+class-lowerCase abcdefghijklmnopqrstuvwxyz 0 12 0
+class-digit 0123456789 0 1 0
+class-special <>,?;.:/!§ù%*µ^¨$£²&é~"#'{([-|è`_\ç^à@)]°=}+ 0 1 0
+class-myClass :) 1 1 0
+```
+
+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).
+
+Also, it won't work for a mail attribute containing: "thereis@domain.com"
+because the part "thereis" matches the password.
+
+
+# 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.
+
+While evaluating a password change, you should observe something looking at this in the logs:
+
+```
+ppm: entry uid=jack.oneill,ou=people,dc=my-domain,dc=com
+ppm: Reading pwdCheckModuleArg attribute
+ppm: RAW configuration: minQuality 3#012checkRDN 0#012checkAttributes mail,uid#012forbiddenChars#012maxConsecutivePerClass 0#012useCracklib 0#012cracklibDict /var/cache/cracklib/cracklib_dict#012class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 0 1 0#012class-lowerCase abcdefghijklmnopqrstuvwxyz 0 1 0#012class-digit 0123456789 0 1 0#012class-special <>,?;.:/!§ù%*µ^¨$£²&é~"#'{([-|è`_\ç^à@)]°=}+ 0 1 0
+ppm: Parsing pwdCheckModuleArg attribute
+ppm: get line: minQuality 3
+ppm: Param = minQuality, value = 3, min = (null), minForPoint= (null)
+ppm: Accepted replaced value: 3
+ppm: get line: checkRDN 0
+ppm: Param = checkRDN, value = 0, min = (null), minForPoint= (null)
+ppm: Accepted replaced value: 0
+ppm: get line: checkAttributes mail,uid
+ppm: Param = checkAttributes, value = mail,uid, min = (null), minForPoint= (null)
+ppm: Accepted replaced value: mail,uid
+ppm: get line: forbiddenChars
+ppm: No value, goto next parameter
+ppm: get line: maxConsecutivePerClass 0
+ppm: Param = maxConsecutivePerClass, value = 0, min = (null), minForPoint= (null)
+ppm: Accepted replaced value: 0
+ppm: get line: useCracklib 0
+ppm: Param = useCracklib, value = 0, min = (null), minForPoint= (null)
+ppm: Accepted replaced value: 0
+ppm: get line: cracklibDict /var/cache/cracklib/cracklib_dict
+ppm: Param = cracklibDict, value = /var/cache/cracklib/cracklib_dict, min = (null), minForPoint= (null)
+ppm: Accepted replaced value: /var/cache/cracklib/cracklib_dict
+ppm: get line: class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 0 1 0
+ppm: Param = class-upperCase, value = ABCDEFGHIJKLMNOPQRSTUVWXYZ, min = 0, minForPoint = 1, max = 0
+ppm: Accepted replaced value: ABCDEFGHIJKLMNOPQRSTUVWXYZ
+ppm: get line: class-lowerCase abcdefghijklmnopqrstuvwxyz 0 1 0
+ppm: Param = class-lowerCase, value = abcdefghijklmnopqrstuvwxyz, min = 0, minForPoint = 1, max = 0
+ppm: Accepted replaced value: abcdefghijklmnopqrstuvwxyz
+ppm: get line: class-digit 0123456789 0 1 0
+ppm: Param = class-digit, value = 0123456789, min = 0, minForPoint = 1, max = 0
+ppm: Accepted replaced value: 0123456789
+ppm: get line: class-special <>,?;.:/!§ù%*µ^¨$£²&é~"#'{([-|è`_\ç^à@)]°=}+ 0 1 0
+ppm: Param = class-special, value = <>,?;.:/!§ù%*µ^¨$£²&é~"#'{([-|è`_\ç^à@)]°=}+, min = 0, minForPoint = 1, max = 0
+ppm: Accepted replaced value: <>,?;.:/!§ù%*µ^¨$£²&é~"#'{([-|è`_\ç^à@)]°=}+
+ppm: 1 point granted for class class-upperCase
+ppm: 1 point granted for class class-lowerCase
+ppm: Reallocating szErrStr from 64 to 179
+check_password_quality: module error: (/usr/local/openldap/libexec/openldap/ppm.so) Password for dn="uid=jack.oneill,ou=people,dc=my-domain,dc=com" does not pass required number of strength checks (2 of 3).[1]
+```
+
+
+# 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/lib
+LD_LIBRARY_PATH=. ./ppm_test "uid=test,ou=users,dc=my-domain,dc=com" "my_password" "/usr/local/etc/openldap/ppm.example" && echo OK
+```
+
+
+# FILES
+
+**ETCDIR/ppm.example**
+
+> example of ppm configuration to be inserted in **pwdCheckModuleArg** attribute of given password policy
+
+**ppm.so**
+
+> ppm library, loaded by the **pwdCheckModule** attribute of given password policy (OpenLDAP 2.5)
+> or by the **ppolicy_check_module** / **olcPPolicyCheckModule** parameters of the ppolicy overlay (OpenLDAP 2.6)
+
+**ppm_test**
+
+> small test program for checking password in a command-line
+
+
+# SEE ALSO
+
+**slapo-ppolicy**(5), **slapd-config**(5), **slapd.conf**(5)
+
+# ACKNOWLEDGEMENTS
+
+This module was developed in 2014-2022 by David Coutadeur.
diff --git a/contrib/slapd-modules/ppm/ppm_test.c b/contrib/slapd-modules/ppm/ppm_test.c
new file mode 100644
index 0000000..637c13c
--- /dev/null
+++ b/contrib/slapd-modules/ppm/ppm_test.c
@@ -0,0 +1,80 @@
+#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 */
+#if OLDAP_VERSION == 0x0205
+ char *errmsg = NULL;
+#else
+ char errbuf[256];
+ struct berval errmsg = { sizeof(errbuf)-1, errbuf };
+#endif
+ 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
+ {
+#if OLDAP_VERSION == 0x0205
+ printf("Password failed checks : %s\n", errmsg);
+#else
+ printf("Password failed checks : %s\n", errmsg.bv_val);
+#endif
+ }
+
+#if OLDAP_VERSION == 0x0205
+ ber_memfree(errmsg);
+#else
+ if (errmsg.bv_val != errbuf)
+ ber_memfree(errmsg.bv_val);
+#endif
+ return ret;
+
+ }
+
+ return ret;
+}
+
+
+
diff --git a/contrib/slapd-modules/ppm/slapm-ppm.5 b/contrib/slapd-modules/ppm/slapm-ppm.5
new file mode 100644
index 0000000..a852037
--- /dev/null
+++ b/contrib/slapd-modules/ppm/slapm-ppm.5
@@ -0,0 +1,353 @@
+.\" Automatically generated by Pandoc 2.9.2.1
+.\"
+.TH "ppm" "5" "August 24, 2021" "ppm" "File Formats Manual"
+.hy
+.SH NAME
+.PP
+ppm (Password Policy Module) - extension of the password policy overlay
+.SH SYNOPSIS
+.PP
+ETCDIR/ppm.example
+.SH DESCRIPTION
+.PP
+\f[B]ppm\f[R] is an OpenLDAP module for checking password quality when
+they are modified.
+Passwords are checked against the presence or absence of certain
+character classes.
+.PP
+This module is used as an extension of the OpenLDAP password policy
+controls, see slapo-ppolicy(5) section \f[B]pwdCheckModule\f[R].
+.SH USAGE
+.PP
+Create a password policy entry and indicate the path of the ppm.so
+library and the content of the desired policy.
+Use a base64 tool to code / decode the content of the policy stored into
+\f[B]pwdCheckModuleArg\f[R].
+.PP
+Here is an example for OpenLDAP 2.6:
+.IP
+.nf
+\f[C]
+dn: cn=default,ou=policies,dc=my-domain,dc=com
+objectClass: pwdPolicy
+objectClass: top
+objectClass: pwdPolicyChecker
+objectClass: person
+pwdCheckQuality: 2
+pwdAttribute: userPassword
+sn: default
+cn: default
+pwdMinLength: 6
+pwdCheckModuleArg:: bWluUXVhbGl0eSAzCmNoZWNrUkROIDAKY2hlY2tBdHRyaWJ1dGVzCmZvcmJpZGRlbkNoYXJzCm1heENvbnNlY3V0aXZlUGVyQ2xhc3MgMAp1c2VDcmFja2xpYiAwCmNyYWNrbGliRGljdCAvdmFyL2NhY2hlL2NyYWNrbGliL2NyYWNrbGliX2RpY3QKY2xhc3MtdXBwZXJDYXNlIEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaIDAgMSAwCmNsYXNzLWxvd2VyQ2FzZSBhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5eiAwIDEgMApjbGFzcy1kaWdpdCAwMTIzNDU2Nzg5IDAgMSAwCmNsYXNzLXNwZWNpYWwgPD4sPzsuOi8hwqfDuSUqwrVewqgkwqPCsibDqX4iIyd7KFstfMOoYF9cw6dew6BAKV3CsD19KyAwIDEgMAoK
+pwdUseCheckModule: TRUE
+\f[R]
+.fi
+.PP
+For OpenLDAP 2.5, you must add a \f[B]pwdCheckModule\f[R] attribute
+pointing to the ppm module (for example /usr/local/lib/ppm.so), and
+remove the \f[B]pwdUseCheckModule\f[R] attribute.
+.PP
+See \f[B]slapo-ppolicy\f[R] for more information, but to sum up:
+.IP \[bu] 2
+enable ppolicy overlay in your database.
+.IP \[bu] 2
+define a default password policy in OpenLDAP configuration or use
+pwdPolicySubentry attribute to point to the given policy.
+.PP
+This example show the activation for a \f[B]slapd.conf\f[R] file for
+OpenLDAP 2.6 (see \f[B]slapd-config\f[R] and \f[B]slapo-ppolicy\f[R] for
+more information for \f[B]cn=config\f[R] configuration)
+.IP
+.nf
+\f[C]
+overlay ppolicy
+ppolicy_default \[dq]cn=default,ou=policies,dc=my-domain,dc=com\[dq]
+ppolicy_check_module /usr/local/openldap/libexec/openldap/ppm.so
+#ppolicy_use_lockout # for having more infos about the lockout
+\f[R]
+.fi
+.PP
+For OpenLDAP 2.5, you must remove \f[B]ppolicy_check_module\f[R]
+parameter as it is managed in the password policy definition
+.SH FEATURES
+.PP
+Here are the main features:
+.IP \[bu] 2
+4 character classes are defined by default: upper case, lower case,
+digits and special characters.
+.IP \[bu] 2
+more character classes can be defined, just write your own.
+.IP \[bu] 2
+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.
+.IP \[bu] 2
+passwords must have at least n of the corresponding character class
+present, else they are rejected.
+.IP \[bu] 2
+passwords must have at the most x occurrences of characters from the
+corresponding character class, else they are rejected.
+.IP \[bu] 2
+the three previous criteria are checked against any specific character
+class defined.
+.IP \[bu] 2
+if a password contains any of the forbidden characters, then it is
+rejected.
+.IP \[bu] 2
+if a password contains tokens from the RDN, then it is rejected.
+.IP \[bu] 2
+if a password contains tokens from defined attributes, then it is
+rejected.
+.IP \[bu] 2
+if a password does not pass cracklib check, then it is rejected.
+.SH CONFIGURATION
+.PP
+Since OpenLDAP 2.5 version, ppm configuration is held in a binary
+attribute of the password policy: \f[B]pwdCheckModuleArg\f[R]
+.PP
+The example file (\f[B]/usr/local/etc/openldap/ppm.example\f[R] by default) is to be
+considered as an example configuration, to import in the
+\f[B]pwdCheckModuleArg\f[R] attribute.
+It is also used for testing passwords with the test program provided.
+.PP
+If for some reasons, any parameter is not found, it will be given its
+default value.
+.PP
+Note: you can still compile ppm to use the configuration file, by
+enabling \f[B]PPM_READ_FILE\f[R] in \f[B]ppm.h\f[R] (but this is
+deprecated now).
+If you decide to do so, you can use the \f[B]PPM_CONFIG_FILE\f[R]
+environment variable for overloading the configuration file path.
+.PP
+The syntax of a configuration line is:
+.IP
+.nf
+\f[C]
+parameter value [min] [minForPoint] [max]
+\f[R]
+.fi
+.PP
+with spaces being delimiters and Line Feed (LF) ending the line.
+.PP
+Parameter names \f[B]are\f[R] case sensitive.
+.PP
+Lines beginning by a \f[B]#\f[R] are considered as comments.
+.PP
+The default configuration is the following:
+.IP
+.nf
+\f[C]
+# 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 _ - , ; \[Po]
+checkRDN 0
+
+# checkAttributes parameter
+# Format:
+# checkAttributes [ATTR1,ATTR2,...]
+# Description:
+# Password must not contain a token from the values in the given list of attributes
+# Tokens are substrings of the values of the given attributes,
+# delimited by: space tabulation _ - , ; \[at]
+# For example, if uid=\[dq]the wonderful entry\[dq],
+# password must not contain \[dq]the\[dq], nor \[dq]wonderful\[dq], nor \[dq]entry\[dq]
+checkAttributes
+
+# 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_dictionary]
+# 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 dictionary files
+cracklibDict /var/cache/cracklib/cracklib_dict
+
+# classes parameter
+# Format:
+# class-[CLASS_NAME] [CHARACTERS_DEFINING_CLASS] [MIN] [MIN_FOR_POINT] [MAX]
+# 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
+# [MAX]: if > [MAX] occurrences of characters from this class are found, then the password is rejected (0 means no maximum)
+class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 0 1 0
+class-lowerCase abcdefghijklmnopqrstuvwxyz 0 1 0
+class-digit 0123456789 0 1 0
+class-special <>,?;.:/!\[sc]\[`u]%*\[mc]\[ha]\[ad]$\[Po]\[S2]&\['e]\[ti]\[dq]#\[aq]{([-|\[`e]\[ga]_\[rs]\[,c]\[ha]\[`a]\[at])]\[de]=}+ 0 1 0
+\f[R]
+.fi
+.SH EXAMPLE
+.PP
+With this policy:
+.IP
+.nf
+\f[C]
+minQuality 4
+forbiddenChars .?,
+checkRDN 1
+checkAttributes mail
+class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 0 5 0
+class-lowerCase abcdefghijklmnopqrstuvwxyz 0 12 0
+class-digit 0123456789 0 1 0
+class-special <>,?;.:/!\[sc]\[`u]%*\[mc]\[ha]\[ad]$\[Po]\[S2]&\['e]\[ti]\[dq]#\[aq]{([-|\[`e]\[ga]_\[rs]\[,c]\[ha]\[`a]\[at])]\[de]=}+ 0 1 0
+class-myClass :) 1 1 0
+\f[R]
+.fi
+.PP
+the password \f[B]ThereIsNoCowLevel)\f[R] is working, because:
+.IP \[bu] 2
+it has 4 character classes validated : upper, lower, special, and
+myClass
+.IP \[bu] 2
+it has no character among .?,
+.IP \[bu] 2
+it has at least one character among : or )
+.PP
+but it won\[cq]t work for the user uid=John
+Cowlevel,ou=people,cn=example,cn=com, because the token
+\[lq]Cowlevel\[rq] from his RDN exists in the password (case
+insensitive).
+.PP
+Also, it won\[cq]t work for a mail attribute containing:
+\[lq]thereis\[at]domain.com\[rq] because the part \[lq]thereis\[rq]
+matches the password.
+.SH LOGS
+.PP
+If a user password is rejected by \f[B]ppm\f[R], the user will get this
+type of message:
+.PP
+Typical user message from ldappasswd(5):
+.IP
+.nf
+\f[C]
+ Result: Constraint violation (19)
+ Additional info: Password for dn=\[rs]\[dq]%s\[rs]\[dq] does not pass required number of strength checks (2 of 3)
+\f[R]
+.fi
+.PP
+A more detailed message is written to the server log.
+.PP
+While evaluating a password change, you should observe something looking
+at this in the logs:
+.IP
+.nf
+\f[C]
+ppm: entry uid=jack.oneill,ou=people,dc=my-domain,dc=com
+ppm: Reading pwdCheckModuleArg attribute
+ppm: RAW configuration: minQuality 3#012checkRDN 0#012checkAttributes mail,uid#012forbiddenChars#012maxConsecutivePerClass 0#012useCracklib 0#012cracklibDict /var/cache/cracklib/cracklib_dict#012class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 0 1 0#012class-lowerCase abcdefghijklmnopqrstuvwxyz 0 1 0#012class-digit 0123456789 0 1 0#012class-special <>,?;.:/!\[sc]\[`u]%*\[mc]\[ha]\[ad]$\[Po]\[S2]&\['e]\[ti]\[dq]#\[aq]{([-|\[`e]\[ga]_\[rs]\[,c]\[ha]\[`a]\[at])]\[de]=}+ 0 1 0
+ppm: Parsing pwdCheckModuleArg attribute
+ppm: get line: minQuality 3
+ppm: Param = minQuality, value = 3, min = (null), minForPoint= (null)
+ppm: Accepted replaced value: 3
+ppm: get line: checkRDN 0
+ppm: Param = checkRDN, value = 0, min = (null), minForPoint= (null)
+ppm: Accepted replaced value: 0
+ppm: get line: checkAttributes mail,uid
+ppm: Param = checkAttributes, value = mail,uid, min = (null), minForPoint= (null)
+ppm: Accepted replaced value: mail,uid
+ppm: get line: forbiddenChars
+ppm: No value, goto next parameter
+ppm: get line: maxConsecutivePerClass 0
+ppm: Param = maxConsecutivePerClass, value = 0, min = (null), minForPoint= (null)
+ppm: Accepted replaced value: 0
+ppm: get line: useCracklib 0
+ppm: Param = useCracklib, value = 0, min = (null), minForPoint= (null)
+ppm: Accepted replaced value: 0
+ppm: get line: cracklibDict /var/cache/cracklib/cracklib_dict
+ppm: Param = cracklibDict, value = /var/cache/cracklib/cracklib_dict, min = (null), minForPoint= (null)
+ppm: Accepted replaced value: /var/cache/cracklib/cracklib_dict
+ppm: get line: class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 0 1 0
+ppm: Param = class-upperCase, value = ABCDEFGHIJKLMNOPQRSTUVWXYZ, min = 0, minForPoint = 1, max = 0
+ppm: Accepted replaced value: ABCDEFGHIJKLMNOPQRSTUVWXYZ
+ppm: get line: class-lowerCase abcdefghijklmnopqrstuvwxyz 0 1 0
+ppm: Param = class-lowerCase, value = abcdefghijklmnopqrstuvwxyz, min = 0, minForPoint = 1, max = 0
+ppm: Accepted replaced value: abcdefghijklmnopqrstuvwxyz
+ppm: get line: class-digit 0123456789 0 1 0
+ppm: Param = class-digit, value = 0123456789, min = 0, minForPoint = 1, max = 0
+ppm: Accepted replaced value: 0123456789
+ppm: get line: class-special <>,?;.:/!\[sc]\[`u]%*\[mc]\[ha]\[ad]$\[Po]\[S2]&\['e]\[ti]\[dq]#\[aq]{([-|\[`e]\[ga]_\[rs]\[,c]\[ha]\[`a]\[at])]\[de]=}+ 0 1 0
+ppm: Param = class-special, value = <>,?;.:/!\[sc]\[`u]%*\[mc]\[ha]\[ad]$\[Po]\[S2]&\['e]\[ti]\[dq]#\[aq]{([-|\[`e]\[ga]_\[rs]\[,c]\[ha]\[`a]\[at])]\[de]=}+, min = 0, minForPoint = 1, max = 0
+ppm: Accepted replaced value: <>,?;.:/!\[sc]\[`u]%*\[mc]\[ha]\[ad]$\[Po]\[S2]&\['e]\[ti]\[dq]#\[aq]{([-|\[`e]\[ga]_\[rs]\[,c]\[ha]\[`a]\[at])]\[de]=}+
+ppm: 1 point granted for class class-upperCase
+ppm: 1 point granted for class class-lowerCase
+ppm: Reallocating szErrStr from 64 to 179
+check_password_quality: module error: (/usr/local/openldap/libexec/openldap/ppm.so) Password for dn=\[dq]uid=jack.oneill,ou=people,dc=my-domain,dc=com\[dq] does not pass required number of strength checks (2 of 3).[1]
+\f[R]
+.fi
+.SH TESTS
+.PP
+There is a unit test script: \f[B]unit_tests.sh\f[R] that illustrates
+checking some passwords.
+.PP
+It is possible to test one particular password using directly the test
+program:
+.IP
+.nf
+\f[C]
+cd /usr/local/lib
+LD_LIBRARY_PATH=. ./ppm_test \[dq]uid=test,ou=users,dc=my-domain,dc=com\[dq] \[dq]my_password\[dq] \[dq]/usr/local/etc/openldap/ppm.example\[dq] && echo OK
+\f[R]
+.fi
+.SH FILES
+.PP
+\f[B]/usr/local/etc/openldap/ppm.example\f[R]
+.RS
+.PP
+example of ppm configuration to be inserted in
+\f[B]pwdCheckModuleArg\f[R] attribute of given password policy
+.RE
+.PP
+\f[B]ppm.so\f[R]
+.RS
+.PP
+ppm library, loaded by the \f[B]pwdCheckModule\f[R] attribute of given
+password policy (OpenLDAP 2.5) or by the \f[B]ppolicy_check_module\f[R]
+/ \f[B]olcPPolicyCheckModule\f[R] parameters of the ppolicy overlay
+(OpenLDAP 2.6)
+.RE
+.PP
+\f[B]ppm_test\f[R]
+.RS
+.PP
+small test program for checking password in a command-line
+.RE
+.SH SEE ALSO
+.PP
+\f[B]slapo-ppolicy\f[R](5), \f[B]slapd-config\f[R](5),
+\f[B]slapd.conf\f[R](5)
+.SH ACKNOWLEDGEMENTS
+.PP
+This module was developed in 2014-2022 by David Coutadeur.
diff --git a/contrib/slapd-modules/ppm/unit_tests.sh b/contrib/slapd-modules/ppm/unit_tests.sh
new file mode 100755
index 0000000..31a3aee
--- /dev/null
+++ b/contrib/slapd-modules/ppm/unit_tests.sh
@@ -0,0 +1,126 @@
+#!/bin/bash
+
+# Launch unitary tests
+#
+
+
+CONFIG_FILE="ppm.example"
+
+LDAP_SRC="${LDAP_SRC:-../../..}"
+LDAP_BUILD=${LDAP_BUILD:-${LDAP_SRC}}
+CURRENT_DIR=$( dirname $0 )
+LIB_PATH="${LD_LIBRARY_PATH}:${CURRENT_DIR}:${LDAP_BUILD}/libraries/liblber/.libs:${LDAP_BUILD}/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 0
+class-lowerCase abcdefghijklmnopqrstuvwxyz 0 1 0
+class-digit 0123456789 0 1 0
+class-special <>,?;.:/!§ù%*µ^¨$£²&é~"#'\''{([-|è`_\ç^à@)]°=}+ 0 1 0'
+
+PPM_CONF_2='minQuality 3
+checkRDN 0
+forbiddenChars à
+maxConsecutivePerClass 5
+useCracklib 0
+cracklibDict /var/cache/cracklib/cracklib_dict
+class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 2 4 10
+class-lowerCase abcdefghijklmnopqrstuvwxyz 3 4 12
+class-digit 0123456789 2 4 10
+class-special <>,?;.:/!§ù%*µ^¨$£²&é~"#'\''{([-|è`_\ç^à@)]°=}+ 0 4 10'
+
+PPM_CONF_3='minQuality 3
+checkRDN 1
+forbiddenChars
+maxConsecutivePerClass 0
+useCracklib 0
+cracklibDict /var/cache/cracklib/cracklib_dict
+class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 0 1 0
+class-lowerCase abcdefghijklmnopqrstuvwxyz 0 1 0
+class-digit 0123456789 0 1 0
+class-special <>,?;.:/!§ù%*µ^¨$£²&é~"#'\''{([-|è`_\ç^à@)]°=}+ 0 1 0'
+
+
+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"
+# too much upper
+launch_test "ppm2.conf" "uid=test,ou=users,dc=my-domain,dc=com" "AAaa01AAaa01AA..AA..AAAA" "FAIL"
+# too much lower
+launch_test "ppm2.conf" "uid=test,ou=users,dc=my-domain,dc=com" "AAaaa01AAaaa01AAaaa0aaaa" "FAIL"
+# too much digit
+launch_test "ppm2.conf" "uid=test,ou=users,dc=my-domain,dc=com" "AA11aa11AA11aa11..11..11" "FAIL"
+# too much special
+launch_test "ppm2.conf" "uid=test,ou=users,dc=my-domain,dc=com" "AA..aa..11..AA..aa..11.." "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}
+