]> git.ipfire.org Git - thirdparty/openldap.git/commitdiff
ITS#9470 Add homedir overlay
authorOndřej Kuzník <ondra@mistotebe.net>
Tue, 16 Mar 2021 17:06:55 +0000 (17:06 +0000)
committerQuanah Gibson-Mount <quanah@openldap.org>
Fri, 26 Mar 2021 01:27:09 +0000 (01:27 +0000)
12 files changed:
configure.ac
doc/man/man5/slapo-homedir.5 [new file with mode: 0644]
servers/slapd/overlays/Makefile.in
servers/slapd/overlays/homedir.c [new file with mode: 0644]
tests/data/homedir/skel/.dotfile [new file with mode: 0644]
tests/data/homedir/skel/directory/broken link [new symlink]
tests/data/homedir/skel/symlink [new symlink]
tests/data/slapd-homedir.conf [new file with mode: 0644]
tests/run.in
tests/scripts/conf.sh
tests/scripts/defines.sh
tests/scripts/test085-homedir [new file with mode: 0755]

index 2945f278bbdde38ca40b3920120d71fcee5bc5d5..c648b7943b89340fcce3f5debb9cc88b77ea5a90 100644 (file)
@@ -349,6 +349,7 @@ Overlays="accesslog \
        deref \
        dyngroup \
        dynlist \
+       homedir \
        memberof \
        ppolicy \
        proxycache \
@@ -388,6 +389,8 @@ OL_ARG_ENABLE(dyngroup, [AS_HELP_STRING([--enable-dyngroup], [Dynamic Group over
        no, [no yes mod], ol_enable_overlays)
 OL_ARG_ENABLE(dynlist, [AS_HELP_STRING([--enable-dynlist], [Dynamic List overlay])],
        no, [no yes mod], ol_enable_overlays)
+OL_ARG_ENABLE(homedir, [AS_HELP_STRING([--enable-homedir], [Home Directory Management overlay])],
+       no, [no yes mod], ol_enable_overlays)
 OL_ARG_ENABLE(memberof, [AS_HELP_STRING([--enable-memberof], [Reverse Group Membership overlay])],
        no, [no yes mod], ol_enable_overlays)
 OL_ARG_ENABLE(ppolicy, [AS_HELP_STRING([--enable-ppolicy], [Password Policy overlay])],
@@ -587,6 +590,7 @@ BUILD_DEREF=no
 BUILD_DYNGROUP=no
 BUILD_DYNLIST=no
 BUILD_LASTMOD=no
+BUILD_HOMEDIR=no
 BUILD_MEMBEROF=no
 BUILD_PPOLICY=no
 BUILD_PROXYCACHE=no
@@ -2838,6 +2842,18 @@ if test "$ol_enable_dynlist" != no ; then
        AC_DEFINE_UNQUOTED(SLAPD_OVER_DYNLIST,$MFLAG,[define for Dynamic List overlay])
 fi
 
+if test "$ol_enable_homedir" != no ; then
+       BUILD_HOMEDIR=$ol_enable_homedir
+       if test "$ol_enable_homedir" = mod ; then
+               MFLAG=SLAPD_MOD_DYNAMIC
+               SLAPD_DYNAMIC_OVERLAYS="$SLAPD_DYNAMIC_OVERLAYS homedir.la"
+       else
+               MFLAG=SLAPD_MOD_STATIC
+               SLAPD_STATIC_OVERLAYS="$SLAPD_STATIC_OVERLAYS homedir.o"
+       fi
+       AC_DEFINE_UNQUOTED(SLAPD_OVER_HOMEDIR,$MFLAG,[define for Home Directory Management overlay])
+fi
+
 if test "$ol_enable_memberof" != no ; then
        BUILD_MEMBEROF=$ol_enable_memberof
        if test "$ol_enable_memberof" = mod ; then
@@ -3110,6 +3126,7 @@ dnl overlays
   AC_SUBST(BUILD_DYNGROUP)
   AC_SUBST(BUILD_DYNLIST)
   AC_SUBST(BUILD_LASTMOD)
+  AC_SUBST(BUILD_HOMEDIR)
   AC_SUBST(BUILD_MEMBEROF)
   AC_SUBST(BUILD_PPOLICY)
   AC_SUBST(BUILD_PROXYCACHE)
diff --git a/doc/man/man5/slapo-homedir.5 b/doc/man/man5/slapo-homedir.5
new file mode 100644 (file)
index 0000000..6d57c6b
--- /dev/null
@@ -0,0 +1,130 @@
+.TH SLAPO-HOMEDIR 5 "RELEASEDATE" "OpenLDAP LDVERSION"
+.\" Copyright 1998-2021 The OpenLDAP Foundation, All Rights Reserved.
+.\" Copying restrictions apply.  See the COPYRIGHT file.
+.\" $OpenLDAP$
+.SH NAME
+slapo\-homedir \- Home directory provisioning overlay
+.SH SYNOPSIS
+ETCDIR/slapd.conf
+.SH DESCRIPTION
+The
+.B homedir
+overlay causes
+.BR slapd (8)
+to notice changes involving RFC-2307bis style user-objects and make
+appropriate changes to the local filesystem.  This can be performed
+on both master and replica systems, so it is possible to perform
+remote home directory provisioning.
+.SH CONFIGURATION
+Both slapd.conf and back-config style configuration is supported.
+.TP
+.B overlay homedir
+This directive adds the homedir overlay to the current database,
+or to the frontend, if used before any database instantiation; see
+.BR slapd.conf (5)
+for details.
+.TP
+.B homedir\-skeleton\-path <pathname>
+.TP
+.B olcSkeletonPath: pathname
+These options set the path to the skeleton account directory.
+(Generally, /etc/skel) Files in this directory will be copied into
+newly created home directories.  Copying is recursive and handles
+symlinks and fifos, but will skip most specials.
+.TP
+.B homedir\-min\-uidnumber <user id number>
+.TP
+.B olcMinimumUidNumber: number
+These options configure the minimum userid to use in any home
+directory attempt.  This is a basic safety measure to prevent
+accidently using system accounts.  See REPLICATION for more flexible
+options for selecting accounts.
+.TP
+.B homedir\-regexp <regexp> <path>
+.TP
+.B olcHomedirRegexp: regexp path
+These options configure a set of regular expressions to use for
+matching and optionally remapping incoming
+.B homeDirectory
+attribute values to pathnames on the local filesystem.  $number
+expansion is supported to access values captured in parentheses.
+
+For example, to accept any directory starting with \/home and use it
+verbatim on the local filesystem:
+
+.B homedir-regexp ^(/home/[\-_/a\-z0\-9]+)$ $1
+
+To match the same set of directories, but create them instead under
+\/export\/home, as is popular on Solaris NFS servers:
+
+.B homedir-regexp ^(/home/[\-_/a\-z0\-9]+)$ /export$1
+.TP
+.B homedir\-delete\-style style
+.TP
+.B olcHomedirDeleteStyle: style
+These options configure how deletes of posixAccount entries or their
+attributes are handled; valid styles are
+.B IGNORE,
+which does nothing, and
+.B DELETE,
+which immediately performs a recursive delete on the home directory,
+and
+.B ARCHIVE,
+which archives the home directory contents in a TAR file for later
+examination.  The default is IGNORE.  Use with caution.  ARCHIVE
+requires homedir-archive-path to be set, or it functions similar to
+IGNORE.
+.TP
+.B homedir\-archive\-path <pathname>
+.TP
+.B olcArchivePath: pathname
+These options specify the destination path for TAR files created by
+the ARCHIVE delete style.
+.SH REPLICATION
+The homedir overlay can operate on either master or replica systems
+with no changes.  See
+.BR slapd.conf (5)
+or
+.BR slapd\-config (5)
+for more information on configure syncrepl.
+
+Partial replication (e.g. with filters) is especially useful for
+providing different provisioning options to different sets of users.
+.SH BUGS
+DELETE, MOD, and MODRDN operations that remove the unix attributes
+when delete style is set to DELETE will recursively delete the (regex
+modified) home directory from the disk.  Please be careful when
+deleting or changing values.
+
+MOD and MODRDN will correctly respond to homeDirectory changes and
+perform a non-destructive rename() operation on the filesystem, but
+this does not correctly retry with a recursive copy when moving
+between filesystems.
+
+The recursive copy/delete/chown/tar functions are not aware of ACLs,
+extended attributes, forks, sparse files, or hard links.  Block and
+character device archival is non-portable, but should not be an issue
+in home directories, hopefully.
+
+Copying and archiving may not support files larger than 2GiB on some
+architectures.  Bare POSIX UStar archives cannot support internal
+files larger than 8GiB.  The current tar generator does not attempt to
+resolve uid/gid into symbolic names.
+
+No attempt is made to try to mkdir() the parent directories needed for
+a given home directory or archive path.
+.SH FILES
+.TP
+ETCDIR/slapd.conf
+default slapd configuration file
+.TP
+/etc/skel (or similar)
+source of new homedir files.
+.SH SEE ALSO
+.BR slapd.conf (5),
+.BR slapd\-config (5),
+.BR slapd (8),
+RFC-2307, RFC-2307bis.
+.SH ACKNOWLEDGEMENTS
+.P
+This module was written in 2009 by Emily Backes for Symas Corporation.
index 6613e7a9f8560572779470024edb06f1ca7d3faa..12ee746c88f374e3a7a8cdbe21b4f963b0ef44a5 100644 (file)
@@ -22,6 +22,7 @@ SRCS = overlays.c \
        deref.c \
        dyngroup.c \
        dynlist.c \
+       homedir.c \
        memberof.c \
        pcache.c \
        collect.c \
@@ -88,6 +89,9 @@ dyngroup.la : dyngroup.lo
 dynlist.la : dynlist.lo
        $(LTLINK_MOD) -module -o $@ dynlist.lo version.lo $(LINK_LIBS)
 
+homedir.la : homedir.lo
+       $(LTLINK_MOD) -module -o $@ homedir.lo version.lo $(LINK_LIBS)
+
 memberof.la : memberof.lo
        $(LTLINK_MOD) -module -o $@ memberof.lo version.lo $(LINK_LIBS)
 
diff --git a/servers/slapd/overlays/homedir.c b/servers/slapd/overlays/homedir.c
new file mode 100644 (file)
index 0000000..d1ad5b5
--- /dev/null
@@ -0,0 +1,2074 @@
+/* homedir.c - create/remove user home directories */
+/* $OpenLDAP$ */
+/* This work is part of OpenLDAP Software <http://www.openldap.org/>.
+ *
+ * Copyright 2009-2010 The OpenLDAP Foundation.
+ * Portions copyright 2009-2010 Symas Corporation.
+ * 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>.
+ */
+/* ACKNOWLEDGEMENTS:
+ * This work was initially developed by Emily Backes at Symas
+ * Corp. for inclusion in OpenLDAP Software.
+ */
+
+#include "portable.h"
+
+#ifdef SLAPD_OVER_HOMEDIR
+
+#define _FILE_OFFSET_BITS 64
+
+#include <stdio.h>
+#include <fcntl.h>
+
+#include <ac/string.h>
+#include <ac/ctype.h>
+#include <ac/errno.h>
+#include <sys/stat.h>
+#include <ac/unistd.h>
+#include <ac/dirent.h>
+#include <ac/time.h>
+
+#include "slap.h"
+#include "slap-config.h"
+
+#define DEFAULT_MIN_UID ( 100 )
+#define DEFAULT_SKEL ( LDAP_DIRSEP "etc" LDAP_DIRSEP "skel" )
+
+typedef struct homedir_regexp {
+       char *match;
+       char *replace;
+       regex_t compiled;
+       struct homedir_regexp *next;
+} homedir_regexp;
+
+typedef enum {
+       DEL_IGNORE,
+       DEL_DELETE,
+       DEL_ARCHIVE
+} delete_style;
+
+typedef struct homedir_data {
+       char *skeleton_path;
+       unsigned min_uid;
+       AttributeDescription *home_ad;
+       AttributeDescription *uidn_ad;
+       AttributeDescription *gidn_ad;
+       homedir_regexp *regexps;
+       delete_style style;
+       char *archive_path;
+} homedir_data;
+
+typedef struct homedir_cb_data {
+       slap_overinst *on;
+       Entry *entry;
+} homedir_cb_data;
+
+typedef struct name_list {
+       char *name;
+       struct stat st;
+       struct name_list *next;
+} name_list;
+
+typedef struct name_list_list {
+       name_list *list;
+       struct name_list_list *next;
+} name_list_list;
+
+typedef enum {
+       TRAVERSE_CB_CONTINUE,
+       TRAVERSE_CB_DONE,
+       TRAVERSE_CB_FAIL
+} traverse_cb_ret;
+
+/* private, file info, context */
+typedef traverse_cb_ret (*traverse_cb_func)(
+               void *,
+               const char *,
+               const struct stat *,
+               void * );
+typedef struct traverse_cb {
+       traverse_cb_func pre_func;
+       traverse_cb_func post_func;
+       void *pre_private;
+       void *post_private;
+} traverse_cb;
+
+typedef struct copy_private {
+       int source_prefix_len;
+       const char *dest_prefix;
+       int dest_prefix_len;
+       uid_t uidn;
+       gid_t gidn;
+} copy_private;
+
+typedef struct chown_private {
+       uid_t old_uidn;
+       uid_t new_uidn;
+       gid_t old_gidn;
+       gid_t new_gidn;
+} chown_private;
+
+typedef struct ustar_header {
+       char name[100];
+       char mode[8];
+       char uid[8];
+       char gid[8];
+       char size[12];
+       char mtime[12];
+       char checksum[8];
+       char typeflag[1];
+       char linkname[100];
+       char magic[6];
+       char version[2];
+       char uname[32];
+       char gname[32];
+       char devmajor[8];
+       char devminor[8];
+       char prefix[155];
+       char pad[12];
+} ustar_header;
+
+typedef struct tar_private {
+       FILE *file;
+       const char *name;
+} tar_private;
+
+/* FIXME: This mutex really needs to be executable-global, but this
+ * will have to do for now.
+ */
+static ldap_pvt_thread_mutex_t readdir_mutex;
+static ConfigDriver homedir_regexp_cfg;
+static ConfigDriver homedir_style_cfg;
+static slap_overinst homedir;
+
+static ConfigTable homedircfg[] = {
+       { "homedir-skeleton-path", "pathname", 2, 2, 0,
+               ARG_STRING|ARG_OFFSET,
+               (void *)offsetof(homedir_data, skeleton_path),
+               "( OLcfgCtAt:8.1 "
+                       "NAME 'olcSkeletonPath' "
+                       "DESC 'Pathname for home directory skeleton template' "
+                       "SYNTAX OMsDirectoryString "
+                       "SINGLE-VALUE )",
+               NULL, { .v_string = DEFAULT_SKEL }
+       },
+
+       { "homedir-min-uidnumber", "uid number", 2, 2, 0,
+               ARG_UINT|ARG_OFFSET,
+               (void *)offsetof(homedir_data, min_uid),
+               "( OLcfgCtAt:8.2 "
+                       "NAME 'olcMinimumUidNumber' "
+                       "DESC 'Minimum uidNumber attribute to consider' "
+                       "SYNTAX OMsInteger "
+                       "SINGLE-VALUE )",
+               NULL, { .v_uint = DEFAULT_MIN_UID }
+       },
+
+       { "homedir-regexp", "regexp> <path", 3, 3, 0,
+               ARG_MAGIC,
+               homedir_regexp_cfg,
+               "( OLcfgCtAt:8.3 "
+                       "NAME 'olcHomedirRegexp' "
+                       "DESC 'Regular expression for matching and transforming paths' "
+                       "SYNTAX OMsDirectoryString "
+                       "X-ORDERED 'VALUES' )",
+               NULL, NULL
+       },
+
+       { "homedir-delete-style", "style", 2, 2, 0,
+               ARG_MAGIC,
+               homedir_style_cfg,
+               "( OLcfgCtAt:8.4 "
+                       "NAME 'olcHomedirDeleteStyle' "
+                       "DESC 'Action to perform when removing a home directory' "
+                       "SYNTAX OMsDirectoryString "
+                       "SINGLE-VALUE )",
+               NULL, NULL
+       },
+
+       { "homedir-archive-path", "pathname", 2, 2, 0,
+               ARG_STRING|ARG_OFFSET,
+               (void *)offsetof(homedir_data, archive_path),
+               "( OLcfgCtAt:8.5 "
+                       "NAME 'olcHomedirArchivePath' "
+                       "DESC 'Pathname for home directory archival' "
+                       "SYNTAX OMsDirectoryString "
+                       "SINGLE-VALUE )",
+               NULL, NULL
+       },
+
+       { NULL, NULL, 0, 0, 0, ARG_IGNORED }
+};
+
+static ConfigOCs homedirocs[] = {
+       { "( OLcfgCtOc:8.1 "
+               "NAME 'olcHomedirConfig' "
+                       "DESC 'Homedir configuration' "
+                       "SUP olcOverlayConfig "
+                       "MAY ( olcSkeletonPath $ olcMinimumUidNumber "
+                               "$ olcHomedirRegexp $ olcHomedirDeleteStyle "
+                               "$ olcHomedirArchivePath ) )",
+               Cft_Overlay, homedircfg },
+
+       { NULL, 0, NULL }
+};
+
+static int
+homedir_regexp_cfg( ConfigArgs *c )
+{
+       slap_overinst *on = (slap_overinst *)c->bi;
+       homedir_data *data = (homedir_data *)on->on_bi.bi_private;
+       int rc = ARG_BAD_CONF;
+
+       assert( data != NULL );
+
+       switch ( c->op ) {
+               case SLAP_CONFIG_EMIT: {
+                       int i;
+                       homedir_regexp *r;
+                       struct berval bv;
+                       char buf[4096];
+
+                       bv.bv_val = buf;
+                       for ( i = 0, r = data->regexps; r != NULL; ++i, r = r->next ) {
+                               bv.bv_len = snprintf( buf, sizeof(buf), "{%d}%s %s", i,
+                                               r->match, r->replace );
+                               if ( bv.bv_len >= sizeof(buf) ) {
+                                       Debug( LDAP_DEBUG_ANY, "homedir_regexp_cfg: "
+                                                       "emit serialization failed: size %lu\n",
+                                                       (unsigned long)bv.bv_len );
+                                       return ARG_BAD_CONF;
+                               }
+                               value_add_one( &c->rvalue_vals, &bv );
+                       }
+                       rc = 0;
+               } break;
+
+               case LDAP_MOD_DELETE:
+                       if ( c->valx < 0 ) { /* delete all values */
+                               homedir_regexp *r, *rnext;
+
+                               for ( r = data->regexps; r != NULL; r = rnext ) {
+                                       rnext = r->next;
+                                       ch_free( r->match );
+                                       ch_free( r->replace );
+                                       regfree( &r->compiled );
+                                       ch_free( r );
+                               }
+                               data->regexps = NULL;
+                               rc = 0;
+
+                       } else { /* delete value by index*/
+                               homedir_regexp **rp, *r;
+                               int i;
+
+                               for ( i = 0, rp = &data->regexps; i < c->valx;
+                                               ++i, rp = &(*rp)->next )
+                                       ;
+
+                               r = *rp;
+                               *rp = r->next;
+                               ch_free( r->match );
+                               ch_free( r->replace );
+                               regfree( &r->compiled );
+                               ch_free( r );
+
+                               rc = 0;
+                       }
+                       break;
+
+               case LDAP_MOD_ADD:              /* fallthrough */
+               case SLAP_CONFIG_ADD: { /* add values */
+                       char *match = c->argv[1];
+                       char *replace = c->argv[2];
+                       regex_t compiled;
+                       homedir_regexp **rp, *r;
+
+                       memset( &compiled, 0, sizeof(compiled) );
+                       rc = regcomp( &compiled, match, REG_EXTENDED );
+                       if ( rc ) {
+                               regerror( rc, &compiled, c->cr_msg, sizeof(c->cr_msg) );
+                               regfree( &compiled );
+                               return ARG_BAD_CONF;
+                       }
+
+                       r = ch_calloc( 1, sizeof(homedir_regexp) );
+                       r->match = strdup( match );
+                       r->replace = strdup( replace );
+                       r->compiled = compiled;
+
+                       if ( c->valx == -1 ) { /* append */
+                               for ( rp = &data->regexps; ( *rp ) != NULL;
+                                               rp = &(*rp)->next )
+                                       ;
+                               *rp = r;
+
+                       } else { /* insert at valx */
+                               int i;
+                               for ( i = 0, rp = &data->regexps; i < c->valx;
+                                               rp = &(*rp)->next, ++i )
+                                       ;
+                               r->next = *rp;
+                               *rp = r;
+                       }
+                       rc = 0;
+                       break;
+               }
+               default:
+                       abort();
+       }
+
+       return rc;
+}
+
+static int
+homedir_style_cfg( ConfigArgs *c )
+{
+       slap_overinst *on = (slap_overinst *)c->bi;
+       homedir_data *data = (homedir_data *)on->on_bi.bi_private;
+       int rc = ARG_BAD_CONF;
+       struct berval bv;
+
+       assert( data != NULL );
+
+       switch ( c->op ) {
+               case SLAP_CONFIG_EMIT:
+                       bv.bv_val = data->style == DEL_IGNORE ? "IGNORE" :
+                                       data->style == DEL_DELETE         ? "DELETE" :
+                                                                                                       "ARCHIVE";
+                       bv.bv_len = strlen( bv.bv_val );
+                       rc = value_add_one( &c->rvalue_vals, &bv );
+                       if ( rc != 0 ) return ARG_BAD_CONF;
+                       break;
+
+               case LDAP_MOD_DELETE:
+                       data->style = DEL_IGNORE;
+                       rc = 0;
+                       break;
+
+               case LDAP_MOD_ADD:        /* fallthrough */
+               case SLAP_CONFIG_ADD: /* add values */
+                       if ( strcasecmp( c->argv[1], "IGNORE" ) == 0 )
+                               data->style = DEL_IGNORE;
+                       else if ( strcasecmp( c->argv[1], "DELETE" ) == 0 )
+                               data->style = DEL_DELETE;
+                       else if ( strcasecmp( c->argv[1], "ARCHIVE" ) == 0 )
+                               data->style = DEL_ARCHIVE;
+                       else {
+                               Debug( LDAP_DEBUG_ANY, "homedir_style_cfg: "
+                                               "unrecognized style keyword\n" );
+                               return ARG_BAD_CONF;
+                       }
+                       rc = 0;
+                       break;
+
+               default:
+                       abort();
+       }
+
+       return rc;
+}
+
+#define HOMEDIR_NULLWRAP(x) ( ( x ) == NULL ? "unknown" : (x) )
+static void
+report_errno( const char *parent_func, const char *func, const char *filename )
+{
+       int save_errno = errno;
+       char ebuf[1024];
+
+       Debug( LDAP_DEBUG_ANY, "homedir: "
+                       "%s: %s: \"%s\": %d (%s)\n",
+                       HOMEDIR_NULLWRAP(parent_func), HOMEDIR_NULLWRAP(func),
+                       HOMEDIR_NULLWRAP(filename), save_errno,
+                       AC_STRERROR_R( save_errno, ebuf, sizeof(ebuf) ) );
+}
+
+static int
+copy_link(
+               const char *dest_file,
+               const char *source_file,
+               const struct stat *st,
+               uid_t uidn,
+               gid_t gidn,
+               void *ctx )
+{
+       char *buf = NULL;
+       int rc;
+
+       assert( dest_file != NULL );
+       assert( source_file != NULL );
+       assert( st != NULL );
+       assert( (st->st_mode & S_IFMT) == S_IFLNK );
+
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "copy_link: %s to %s\n",
+                       source_file, dest_file );
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "copy_link: %s uid %ld gid %ld\n",
+                       dest_file, (long)uidn, (long)gidn );
+
+       /* calloc +1 for terminator */
+       buf = ber_memcalloc_x( 1, st->st_size + 1, ctx );
+       if ( buf == NULL ) {
+               Debug( LDAP_DEBUG_ANY, "homedir: "
+                               "copy_link: alloc failed\n" );
+               return 1;
+       }
+       rc = readlink( source_file, buf, st->st_size );
+       if ( rc == -1 ) {
+               report_errno( "copy_link", "readlink", source_file );
+               goto fail;
+       }
+       rc = symlink( buf, dest_file );
+       if ( rc ) {
+               report_errno( "copy_link", "symlink", dest_file );
+               goto fail;
+       }
+       rc = lchown( dest_file, uidn, gidn );
+       if ( rc ) {
+               report_errno( "copy_link", "lchown", dest_file );
+               goto fail;
+       }
+       goto out;
+
+fail:
+       rc = 1;
+
+out:
+       if ( buf != NULL ) ber_memfree_x( buf, ctx );
+       return rc;
+}
+
+static int
+copy_blocks(
+               FILE *source,
+               FILE *dest,
+               const char *source_file,
+               const char *dest_file )
+{
+       char buf[4096];
+       size_t nread = 0;
+       int done = 0;
+
+       while ( !done ) {
+               nread = fread( buf, 1, sizeof(buf), source );
+               if ( nread == 0 ) {
+                       if ( feof( source ) ) {
+                               done = 1;
+                       } else if ( ferror( source ) ) {
+                               if ( source_file != NULL )
+                                       Debug( LDAP_DEBUG_ANY, "homedir: "
+                                                       "read error on %s\n",
+                                                       source_file );
+                               goto fail;
+                       }
+               } else {
+                       size_t nwritten = 0;
+                       nwritten = fwrite( buf, 1, nread, dest );
+                       if ( nwritten < nread ) {
+                               if ( dest_file != NULL )
+                                       Debug( LDAP_DEBUG_ANY, "homedir: "
+                                                       "write error on %s\n",
+                                                       dest_file );
+                               goto fail;
+                       }
+               }
+       }
+       return 0;
+fail:
+       return 1;
+}
+
+static int
+copy_file(
+               const char *dest_file,
+               const char *source_file,
+               uid_t uid,
+               gid_t gid,
+               int mode )
+{
+       FILE *source = NULL;
+       FILE *dest = NULL;
+       int rc;
+
+       assert( dest_file != NULL );
+       assert( source_file != NULL );
+
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "copy_file: %s to %s mode 0%o\n",
+                       source_file, dest_file, mode );
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "copy_file: %s uid %ld gid %ld\n",
+                       dest_file, (long)uid, (long)gid );
+
+       source = fopen( source_file, "rb" );
+       if ( source == NULL ) {
+               report_errno( "copy_file", "fopen", source_file );
+               goto fail;
+       }
+       dest = fopen( dest_file, "wb" );
+       if ( dest == NULL ) {
+               report_errno( "copy_file", "fopen", dest_file );
+               goto fail;
+       }
+
+       rc = copy_blocks( source, dest, source_file, dest_file );
+       if ( rc != 0 ) goto fail;
+
+       fclose( source );
+       source = NULL;
+       rc = fclose( dest );
+       dest = NULL;
+       if ( rc != 0 ) {
+               report_errno( "copy_file", "fclose", dest_file );
+               goto fail;
+       }
+
+       /* set owner/permission */
+       rc = lchown( dest_file, uid, gid );
+       if ( rc != 0 ) {
+               report_errno( "copy_file", "lchown", dest_file );
+               goto fail;
+       }
+       rc = chmod( dest_file, mode );
+       if ( rc != 0 ) {
+               report_errno( "copy_file", "chmod", dest_file );
+               goto fail;
+       }
+
+       rc = 0;
+       goto out;
+fail:
+       rc = 1;
+out:
+       if ( source != NULL ) fclose( source );
+       if ( dest != NULL ) fclose( dest );
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "copy_file: %s to %s exit %d\n",
+                       source_file, dest_file, rc );
+       return rc;
+}
+
+static void
+free_name_list( name_list *names, void *ctx )
+{
+       name_list *next;
+
+       while ( names != NULL ) {
+               next = names->next;
+               if ( names->name != NULL ) ber_memfree_x( names->name, ctx );
+               ber_memfree_x( names, ctx );
+               names = next;
+       }
+}
+
+static int
+grab_names( const char *dir_path, name_list **names, void *ctx )
+{
+       int locked = 0;
+       DIR *dir = NULL;
+       struct dirent *entry = NULL;
+       name_list **tail = NULL;
+       int dir_path_len = 0;
+       int rc = 0;
+
+       assert( dir_path != NULL );
+       assert( names != NULL );
+       assert( *names == NULL );
+
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "grab_names: %s\n", dir_path );
+
+       tail = names;
+       dir_path_len = strlen( dir_path );
+       ldap_pvt_thread_mutex_lock( &readdir_mutex );
+       locked = 1;
+
+       dir = opendir( dir_path );
+       if ( dir == NULL ) {
+               report_errno( "grab_names", "opendir", dir_path );
+               goto fail;
+       }
+
+       while ( ( entry = readdir( dir ) ) != NULL ) {
+               /* no d_namelen in ac/dirent.h */
+               int d_namelen = strlen( entry->d_name );
+               int full_len;
+
+               /* Skip . and .. */
+               if ( ( d_namelen == 1 && entry->d_name[0] == '.' ) ||
+                               ( d_namelen == 2 && entry->d_name[0] == '.' &&
+                                               entry->d_name[1] == '.' ) ) {
+                       continue;
+               }
+
+               *tail = ber_memcalloc_x( 1, sizeof(**tail), ctx );
+               if ( *tail == NULL ) {
+                       Debug( LDAP_DEBUG_ANY, "homedir: "
+                                       "grab_names: list alloc failed\n" );
+                       goto fail;
+               }
+               (*tail)->next = NULL;
+
+               /* +1 for dirsep, +1 for term */
+               full_len = dir_path_len + 1 + d_namelen + 1;
+               (*tail)->name = ber_memalloc_x( full_len, ctx );
+               if ( (*tail)->name == NULL ) {
+                       Debug( LDAP_DEBUG_ANY, "homedir: "
+                                       "grab_names: name alloc failed\n" );
+                       goto fail;
+               }
+               snprintf( (*tail)->name, full_len, "%s" LDAP_DIRSEP "%s",
+                               dir_path, entry->d_name );
+               Debug( LDAP_DEBUG_TRACE, "homedir: "
+                               "grab_names: found \"%s\"\n",
+                               (*tail)->name );
+
+               rc = lstat( (*tail)->name, &(*tail)->st );
+               if ( rc ) {
+                       report_errno( "grab_names", "lstat", (*tail)->name );
+                       goto fail;
+               }
+
+               tail = &(*tail)->next;
+       }
+       closedir( dir );
+       ldap_pvt_thread_mutex_unlock( &readdir_mutex );
+       locked = 0;
+
+       dir = NULL;
+       goto success;
+
+success:
+       rc = 0;
+       goto out;
+fail:
+       rc = 1;
+       goto out;
+out:
+       if ( dir != NULL ) closedir( dir );
+       if ( locked ) ldap_pvt_thread_mutex_unlock( &readdir_mutex );
+       if ( rc != 0 && *names != NULL ) {
+               free_name_list( *names, ctx );
+               *names = NULL;
+       }
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "grab_names: %s exit %d\n",
+                       dir_path, rc );
+       return rc;
+}
+
+static int
+traverse( const char *path, const traverse_cb *cb, void *ctx )
+{
+       name_list *next_name = NULL;
+       name_list_list *dir_stack = NULL;
+       name_list_list *next_dir;
+       int rc = 0;
+
+       assert( path != NULL );
+       assert( cb != NULL );
+       assert( cb->pre_func || cb->post_func );
+
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "traverse: %s\n", path );
+
+       dir_stack = ber_memcalloc_x( 1, sizeof(*dir_stack), ctx );
+       if ( dir_stack == NULL ) goto alloc_fail;
+       dir_stack->next = NULL;
+       dir_stack->list = ber_memcalloc_x( 1, sizeof(name_list), ctx );
+       if ( dir_stack->list == NULL ) goto alloc_fail;
+       rc = lstat( path, &dir_stack->list->st );
+       if ( rc != 0 ) {
+               report_errno( "traverse", "lstat", path );
+               goto fail;
+       }
+       dir_stack->list->next = NULL;
+       dir_stack->list->name = ber_strdup_x( path, ctx );
+       if ( dir_stack->list->name == NULL ) goto alloc_fail;
+
+       while ( dir_stack != NULL ) {
+               while ( dir_stack->list != NULL ) {
+                       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                                       "traverse: top of loop with \"%s\"\n",
+                                       dir_stack->list->name );
+
+                       if ( cb->pre_func != NULL ) {
+                               traverse_cb_ret cb_rc;
+                               cb_rc = cb->pre_func( cb->pre_private, dir_stack->list->name,
+                                               &dir_stack->list->st, ctx );
+
+                               if ( cb_rc == TRAVERSE_CB_DONE ) goto cb_done;
+                               if ( cb_rc == TRAVERSE_CB_FAIL ) goto cb_fail;
+                       }
+                       if ( (dir_stack->list->st.st_mode & S_IFMT) == S_IFDIR ) {
+                               /* push dir onto stack */
+                               next_dir = dir_stack;
+                               dir_stack = ber_memalloc_x( sizeof(*dir_stack), ctx );
+                               if ( dir_stack == NULL ) {
+                                       dir_stack = next_dir;
+                                       goto alloc_fail;
+                               }
+                               dir_stack->list = NULL;
+                               dir_stack->next = next_dir;
+                               rc = grab_names(
+                                               dir_stack->next->list->name, &dir_stack->list, ctx );
+                               if ( rc != 0 ) {
+                                       Debug( LDAP_DEBUG_ANY, "homedir: "
+                                                       "traverse: grab_names %s failed\n",
+                                                       dir_stack->next->list->name );
+                                       goto fail;
+                               }
+                       } else {
+                               /* just a file */
+                               if ( cb->post_func != NULL ) {
+                                       traverse_cb_ret cb_rc;
+                                       cb_rc = cb->post_func( cb->post_private,
+                                                       dir_stack->list->name, &dir_stack->list->st, ctx );
+
+                                       if ( cb_rc == TRAVERSE_CB_DONE ) goto cb_done;
+                                       if ( cb_rc == TRAVERSE_CB_FAIL ) goto cb_fail;
+                               }
+                               next_name = dir_stack->list->next;
+                               ber_memfree_x( dir_stack->list->name, ctx );
+                               ber_memfree_x( dir_stack->list, ctx );
+                               dir_stack->list = next_name;
+                       }
+               }
+               /* Time to pop a directory off the stack */
+               next_dir = dir_stack->next;
+               ber_memfree_x( dir_stack, ctx );
+               dir_stack = next_dir;
+               if ( dir_stack != NULL ) {
+                       if ( cb->post_func != NULL ) {
+                               traverse_cb_ret cb_rc;
+                               cb_rc = cb->post_func( cb->post_private, dir_stack->list->name,
+                                               &dir_stack->list->st, ctx );
+
+                               if ( cb_rc == TRAVERSE_CB_DONE ) goto cb_done;
+                               if ( cb_rc == TRAVERSE_CB_FAIL ) goto cb_fail;
+                       }
+                       next_name = dir_stack->list->next;
+                       ber_memfree_x( dir_stack->list->name, ctx );
+                       ber_memfree_x( dir_stack->list, ctx );
+                       dir_stack->list = next_name;
+               }
+       }
+
+       goto success;
+
+cb_done:
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "traverse: cb signaled completion\n" );
+success:
+       rc = 0;
+       goto out;
+
+cb_fail:
+       Debug( LDAP_DEBUG_ANY, "homedir: "
+                       "traverse: cb signaled failure\n" );
+       goto fail;
+alloc_fail:
+       Debug( LDAP_DEBUG_ANY, "homedir: "
+                       "traverse: allocation failed\n" );
+fail:
+       rc = 1;
+       goto out;
+
+out:
+       while ( dir_stack != NULL ) {
+               free_name_list( dir_stack->list, ctx );
+               next_dir = dir_stack->next;
+               ber_memfree_x( dir_stack, ctx );
+               dir_stack = next_dir;
+       }
+       return rc;
+}
+
+static traverse_cb_ret
+traverse_copy_pre(
+               void *private,
+               const char *name,
+               const struct stat *st,
+               void *ctx )
+{
+       copy_private *cp = private;
+       char *dest_name = NULL;
+       int source_name_len;
+       int dest_name_len;
+       int rc;
+
+       assert( private != NULL );
+       assert( name != NULL );
+       assert( st != NULL );
+
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "traverse_copy_pre: %s entering\n",
+                       name );
+
+       assert( cp->source_prefix_len >= 0 );
+       assert( cp->dest_prefix != NULL );
+       assert( cp->dest_prefix_len > 1 );
+
+       source_name_len = strlen( name );
+       assert( source_name_len >= cp->source_prefix_len );
+       /* +1 for terminator */
+       dest_name_len =
+                       source_name_len + cp->dest_prefix_len - cp->source_prefix_len + 1;
+       dest_name = ber_memalloc_x( dest_name_len, ctx );
+       if ( dest_name == NULL ) goto alloc_fail;
+
+       snprintf( dest_name, dest_name_len, "%s%s", cp->dest_prefix,
+                       name + cp->source_prefix_len );
+
+       switch ( st->st_mode & S_IFMT ) {
+               case S_IFDIR:
+                       rc = mkdir( dest_name, st->st_mode & 06775 );
+                       if ( rc ) {
+                               int save_errno = errno;
+                               switch ( save_errno ) {
+                                       case EEXIST:
+                                               /* directory already present; nothing to do */
+                                               goto exists;
+                                               break;
+                                       case ENOENT:
+                                               /* FIXME: should mkdir -p here */
+                                               /* fallthrough for now */
+                                       default:
+                                               report_errno( "traverse_copy_pre", "mkdir", dest_name );
+                                               goto fail;
+                               }
+                       }
+                       rc = lchown( dest_name, cp->uidn, cp->gidn );
+                       if ( rc ) {
+                               report_errno( "traverse_copy_pre", "lchown", dest_name );
+                               goto fail;
+                       }
+                       rc = chmod( dest_name, st->st_mode & 07777 );
+                       if ( rc ) {
+                               report_errno( "traverse_copy_pre", "chmod", dest_name );
+                               goto fail;
+                       }
+                       break;
+               case S_IFREG:
+                       rc = copy_file(
+                                       dest_name, name, cp->uidn, cp->gidn, st->st_mode & 07777 );
+                       if ( rc ) goto fail;
+                       break;
+               case S_IFIFO:
+                       rc = mkfifo( dest_name, 0700 );
+                       if ( rc ) {
+                               report_errno( "traverse_copy_pre", "mkfifo", dest_name );
+                               goto fail;
+                       }
+                       rc = lchown( dest_name, cp->uidn, cp->gidn );
+                       if ( rc ) {
+                               report_errno( "traverse_copy_pre", "lchown", dest_name );
+                               goto fail;
+                       }
+                       rc = chmod( dest_name, st->st_mode & 07777 );
+                       if ( rc ) {
+                               report_errno( "traverse_copy_pre", "chmod", dest_name );
+                               goto fail;
+                       }
+                       break;
+               case S_IFLNK:
+                       rc = copy_link( dest_name, name, st, cp->uidn, cp->gidn, ctx );
+                       if ( rc ) goto fail;
+                       break;
+               default:
+                       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                                       "traverse_copy_pre: skipping special: %s\n",
+                                       name );
+       }
+
+       goto success;
+
+alloc_fail:
+       Debug( LDAP_DEBUG_ANY, "homedir: "
+                       "traverse_copy_pre: allocation failed\n" );
+fail:
+       rc = TRAVERSE_CB_FAIL;
+       goto out;
+
+exists:
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "traverse_copy_pre: \"%s\" already exists,"
+                       " skipping the rest\n",
+                       dest_name );
+       rc = TRAVERSE_CB_DONE;
+       goto out;
+
+success:
+       rc = TRAVERSE_CB_CONTINUE;
+out:
+       if ( dest_name != NULL ) ber_memfree_x( dest_name, ctx );
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "traverse_copy_pre: exit %d\n", rc );
+       return rc;
+}
+
+static int
+copy_tree(
+               const char *dest_path,
+               const char *source_path,
+               uid_t uidn,
+               gid_t gidn,
+               void *ctx )
+{
+       traverse_cb cb;
+       copy_private cp;
+       int rc;
+
+       assert( dest_path != NULL );
+       assert( source_path != NULL );
+
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "copy_tree: %s to %s entering\n",
+                       source_path, dest_path );
+
+       cb.pre_func = traverse_copy_pre;
+       cb.post_func = NULL;
+       cb.pre_private = &cp;
+       cb.post_private = NULL;
+
+       cp.source_prefix_len = strlen( source_path );
+       cp.dest_prefix = dest_path;
+       cp.dest_prefix_len = strlen( dest_path );
+       cp.uidn = uidn;
+       cp.gidn = gidn;
+
+       if ( cp.source_prefix_len <= cp.dest_prefix_len &&
+                       strncmp( source_path, dest_path, cp.source_prefix_len ) == 0 &&
+                       ( cp.source_prefix_len == cp.dest_prefix_len ||
+                                       dest_path[cp.source_prefix_len] == LDAP_DIRSEP[0] ) ) {
+               Debug( LDAP_DEBUG_ANY, "homedir: "
+                               "copy_tree: aborting: %s contains %s\n",
+                               source_path, dest_path );
+               return 1;
+       }
+
+       rc = traverse( source_path, &cb, ctx );
+
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "copy_tree: %s exit %d\n", source_path,
+                       rc );
+
+       return rc;
+}
+
+static int
+homedir_provision(
+               const char *dest_path,
+               const char *skel_path,
+               uid_t uidn,
+               gid_t gidn,
+               void *ctx )
+{
+       int rc;
+
+       assert( dest_path != NULL );
+
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "homedir_provision: %s from skeleton %s\n",
+                       dest_path, skel_path == NULL ? "(none)" : skel_path );
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "homedir_provision: %s uidn %ld gidn %ld\n",
+                       dest_path, (long)uidn, (long)gidn );
+
+       if ( skel_path == NULL ) {
+               rc = mkdir( dest_path, 0700 );
+               if ( rc ) {
+                       int save_errno = errno;
+                       switch ( save_errno ) {
+                               case EEXIST:
+                                       /* directory already present; nothing to do */
+                                       /* but down chown either */
+                                       rc = 0;
+                                       goto out;
+                                       break;
+                               default:
+                                       report_errno( "provision_homedir", "mkdir", dest_path );
+                                       goto fail;
+                       }
+               }
+               rc = lchown( dest_path, uidn, gidn );
+               if ( rc ) {
+                       report_errno( "provision_homedir", "lchown", dest_path );
+                       goto fail;
+               }
+
+       } else {
+               rc = copy_tree( dest_path, skel_path, uidn, gidn, ctx );
+       }
+
+       goto out;
+
+fail:
+       rc = 1;
+       goto out;
+out:
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "homedir_provision: %s to %s exit %d\n",
+                       skel_path, dest_path, rc );
+       return rc;
+}
+
+/* traverse func for rm -rf */
+static traverse_cb_ret
+traverse_remove_post(
+               void *private,
+               const char *name,
+               const struct stat *st,
+               void *ctx )
+{
+       int rc;
+
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "traverse_remove_post: %s entering\n",
+                       name );
+
+       if ( (st->st_mode & S_IFMT) == S_IFDIR ) {
+               rc = rmdir( name );
+               if ( rc != 0 ) {
+                       report_errno( "traverse_remove_post", "rmdir", name );
+                       goto fail;
+               }
+       } else {
+               rc = unlink( name );
+               if ( rc != 0 ) {
+                       report_errno( "traverse_remove_post", "unlink", name );
+                       goto fail;
+               }
+       }
+
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "traverse_remove_post: %s exit continue\n",
+                       name );
+       return TRAVERSE_CB_CONTINUE;
+
+fail:
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "traverse_remove_post: %s exit failure\n",
+                       name );
+       return TRAVERSE_CB_FAIL;
+}
+
+static int
+delete_tree( const char *path, void *ctx )
+{
+       const static traverse_cb cb = { NULL, traverse_remove_post, NULL, NULL };
+       int rc;
+
+       assert( path != NULL );
+
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "delete_tree: %s entering\n", path );
+
+       rc = traverse( path, &cb, ctx );
+
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "delete_tree: %s exit %d\n", path, rc );
+
+       return rc;
+}
+
+static int
+get_tar_name(
+               const char *path,
+               const char *tar_path,
+               char *tar_name,
+               int name_size )
+{
+       int rc = 0;
+       const char *ch;
+       int fd = -1;
+       int counter = 0;
+       time_t now;
+
+       assert( path != NULL );
+       assert( tar_path != NULL );
+       assert( tar_name != NULL );
+
+       for ( ch = path + strlen( path );
+                       *ch != LDAP_DIRSEP[0] && ch > path;
+                       --ch )
+               ;
+       if ( ch <= path || strlen( ch ) < 2 ) {
+               Debug( LDAP_DEBUG_ANY, "homedir: "
+                               "get_tar_name: unable to construct a tar name from input "
+                               "path \"%s\"\n",
+                               path );
+               goto fail;
+       }
+       ++ch; /* skip past sep */
+       time( &now );
+
+       while ( fd < 0 ) {
+               snprintf( tar_name, name_size, "%s" LDAP_DIRSEP "%s-%ld-%d.tar",
+                               tar_path, ch, (long)now, counter );
+               fd = open( tar_name, O_WRONLY|O_CREAT|O_EXCL, 0600 );
+               if ( fd < 0 ) {
+                       int save_errno = errno;
+                       if ( save_errno != EEXIST ) {
+                               report_errno( "get_tar_name", "open", tar_name );
+                               goto fail;
+                       }
+                       ++counter;
+               }
+       }
+
+       rc = 0;
+       goto out;
+
+fail:
+       rc = 1;
+       *tar_name = '\0';
+out:
+       if ( fd >= 0 ) close( fd );
+       return rc;
+}
+
+/* traverse func for rechown */
+static traverse_cb_ret
+traverse_chown_pre(
+               void *private,
+               const char *name,
+               const struct stat *st,
+               void *ctx )
+{
+       int rc;
+       chown_private *cp = private;
+       uid_t set_uidn = -1;
+       gid_t set_gidn = -1;
+
+       assert( private != NULL );
+       assert( name != NULL );
+       assert( st != NULL );
+
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "traverse_chown_pre: %s entering\n",
+                       name );
+
+       if ( st->st_uid == cp->old_uidn ) set_uidn = cp->new_uidn;
+       if ( st->st_gid == cp->old_gidn ) set_gidn = cp->new_gidn;
+
+       if ( set_uidn != (uid_t)-1 || set_gidn != (gid_t)-1 ) {
+               rc = lchown( name, set_uidn, set_gidn );
+               if ( rc ) {
+                       report_errno( "traverse_chown_pre", "lchown", name );
+                       goto fail;
+               }
+       }
+
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "traverse_chown_pre: %s exit continue\n",
+                       name );
+       return TRAVERSE_CB_CONTINUE;
+
+fail:
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "traverse_chown_pre: %s exit failure\n",
+                       name );
+       return TRAVERSE_CB_FAIL;
+}
+
+static int
+chown_tree(
+               const char *path,
+               uid_t old_uidn,
+               uid_t new_uidn,
+               gid_t old_gidn,
+               gid_t new_gidn,
+               void *ctx )
+{
+       traverse_cb cb;
+       chown_private cp;
+       int rc;
+
+       assert( path != NULL );
+
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "chown_tree: %s entering\n", path );
+
+       cb.pre_func = traverse_chown_pre;
+       cb.post_func = NULL;
+       cb.pre_private = &cp;
+       cb.post_private = NULL;
+
+       cp.old_uidn = old_uidn;
+       cp.new_uidn = new_uidn;
+       cp.old_gidn = old_gidn;
+       cp.new_gidn = new_gidn;
+
+       rc = traverse( path, &cb, ctx );
+
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "chown_tree: %s exit %d\n", path, rc );
+
+       return rc;
+}
+
+static int
+homedir_rename( const char *source_path, const char *dest_path )
+{
+       int rc = 0;
+
+       assert( source_path != NULL );
+       assert( dest_path != NULL );
+
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "homedir_rename: %s to %s\n",
+                       source_path, dest_path );
+       rc = rename( source_path, dest_path );
+       if ( rc != 0 ) {
+               char ebuf[1024];
+               int save_errno = errno;
+
+               Debug( LDAP_DEBUG_ANY, "homedir: "
+                               "homedir_rename: rename(\"%s\", \"%s\"): (%s)\n",
+                               source_path, dest_path,
+                               AC_STRERROR_R( save_errno, ebuf, sizeof(ebuf) ) );
+       }
+
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "homedir_rename: %s to %s exit %d\n",
+                       source_path, dest_path, rc );
+       return rc;
+}
+
+/* FIXME: This assumes ASCII; needs fixing for z/OS */
+static int
+tar_set_header( ustar_header *tar, const struct stat *st, const char *name )
+{
+       int name_len;
+       int rc;
+       const char *ch, *end;
+
+       assert( tar != NULL );
+       assert( st != NULL );
+       assert( name != NULL );
+       assert( sizeof(*tar) == 512 );
+       assert( sizeof(tar->name) == 100 );
+       assert( sizeof(tar->prefix) == 155 );
+       assert( sizeof(tar->checksum) == 8 );
+
+       memset( tar, 0, sizeof(*tar) );
+
+       assert( name[0] == LDAP_DIRSEP[0] );
+       name += 1; /* skip leading / */
+
+       name_len = strlen( name );
+
+       /* fits in tar->name? */
+       /* Yes, name and prefix do not need a trailing nul. */
+       if ( name_len <= 100 ) {
+               strncpy( tar->name, name, 100 );
+
+               /* try fit in tar->name + tar->prefix */
+       } else {
+               /* try to find something to stick into tar->name */
+               for ( ch = name + name_len - 100, end = name + name_len;
+                               ch < end && *ch != LDAP_DIRSEP[0];
+                               ++ch )
+                       ;
+               if ( end - ch > 0 ) /* +1 skip past sep */
+                       ch++;
+               else {
+                       /* reset; name too long for UStar */
+                       Debug( LDAP_DEBUG_ANY, "homedir: "
+                                       "tar_set_header: name too long: \"%s\"\n",
+                                       name );
+                       ch = name + name_len - 100;
+               }
+               strncpy( tar->name, ch + 1, 100 );
+               {
+                       int prefix_len = ( ch - 1 ) - name;
+                       if ( prefix_len > 155 ) prefix_len = 155;
+                       strncpy( tar->prefix, name, prefix_len );
+               }
+       }
+
+       snprintf( tar->mode, 8, "%06lo ", (long)st->st_mode & 07777 );
+       snprintf( tar->uid, 8, "%06lo ", (long)st->st_uid );
+       snprintf( tar->gid, 8, "%06lo ", (long)st->st_gid );
+       snprintf( tar->mtime, 12, "%010lo ", (long)st->st_mtime );
+       snprintf( tar->size, 12, "%010lo ", (long)0 );
+       switch ( st->st_mode & S_IFMT ) {
+               case S_IFREG:
+                       tar->typeflag[0] = '0';
+                       snprintf( tar->size, 12, "%010lo ", (long)st->st_size );
+                       break;
+               case S_IFLNK:
+                       tar->typeflag[0] = '2';
+                       rc = readlink( name - 1, tar->linkname, 99 );
+                       if ( rc == -1 ) {
+                               report_errno( "tar_set_header", "readlink", name );
+                               goto fail;
+                       }
+                       break;
+               case S_IFCHR:
+                       tar->typeflag[0] = '3';
+                       /* FIXME: this is probably wrong but shouldn't likely be an issue */
+                       snprintf( tar->devmajor, 8, "%06lo ", (long)st->st_rdev >> 16 );
+                       snprintf( tar->devminor, 8, "%06lo ", (long)st->st_rdev & 0xffff );
+                       break;
+               case S_IFBLK:
+                       tar->typeflag[0] = '4';
+                       /* FIXME: this is probably wrong but shouldn't likely be an issue */
+                       snprintf( tar->devmajor, 8, "%06lo ", (long)st->st_rdev >> 16 );
+                       snprintf( tar->devminor, 8, "%06lo ", (long)st->st_rdev & 0xffff );
+                       break;
+               case S_IFDIR:
+                       tar->typeflag[0] = '5';
+                       break;
+               case S_IFIFO:
+                       tar->typeflag[0] = '6';
+                       break;
+               default:
+                       goto fail;
+       }
+       snprintf( tar->magic, 6, "ustar" );
+       tar->version[0] = '0';
+       tar->version[1] = '0';
+
+       {
+               unsigned char *uch = (unsigned char *)tar;
+               unsigned char *uend = uch + 512;
+               unsigned long sum = 0;
+
+               memset( &tar->checksum, ' ', sizeof(tar->checksum) );
+
+               for ( ; uch < uend; ++uch )
+                       sum += *uch;
+
+               /* zero-padded, six octal digits, followed by NUL then space (!) */
+               /* Yes, that's terminated exactly reverse of the others. */
+               snprintf( tar->checksum, sizeof(tar->checksum) - 1, "%06lo", sum );
+       }
+
+       return 0;
+fail:
+       return 1;
+}
+
+static traverse_cb_ret
+traverse_tar_pre(
+               void *private,
+               const char *name,
+               const struct stat *st,
+               void *ctx )
+{
+       int rc;
+       traverse_cb_ret cbrc;
+       tar_private *tp = private;
+       ustar_header tar;
+       FILE *source = NULL;
+
+       assert( private != NULL );
+       assert( name != NULL );
+       assert( st != NULL );
+
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "traverse_tar_pre: %s entering\n", name );
+
+       switch ( st->st_mode & S_IFMT ) {
+               case S_IFREG:
+                       if ( sizeof(st->st_size) > 4 && ( st->st_size >> 33 ) >= 1 ) {
+                               Debug( LDAP_DEBUG_TRACE, "homedir: "
+                                               "traverse_tar_pre: %s is larger than 8GiB POSIX UStar "
+                                               "file size limit\n",
+                                               name );
+                               goto fail;
+                       }
+                       /* fallthrough */
+               case S_IFDIR:
+               case S_IFLNK:
+               case S_IFIFO:
+               case S_IFCHR:
+               case S_IFBLK:
+                       rc = tar_set_header( &tar, st, name );
+                       if ( rc ) goto fail;
+                       break;
+               default:
+                       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                                       "traverse_tar_pre: skipping \"%s\" mode %o\n",
+                                       name, st->st_mode );
+                       goto done;
+       }
+
+       rc = fwrite( &tar, 1, 512, tp->file );
+       if ( rc != 512 ) {
+               Debug( LDAP_DEBUG_TRACE, "homedir: "
+                               "traverse_tar_pre: write error in tar header\n" );
+               goto fail;
+       }
+
+       if ( (st->st_mode & S_IFMT) == S_IFREG ) {
+               source = fopen( name, "rb" );
+               if ( source == NULL ) {
+                       report_errno( "traverse_tar_pre", "fopen", name );
+                       goto fail;
+               }
+               rc = copy_blocks( source, tp->file, name, tp->name );
+               if ( rc != 0 ) goto fail;
+               fclose( source );
+               source = NULL;
+       }
+
+       { /* advance to end of record */
+               off_t pos = ftello( tp->file );
+               if ( pos == -1 ) {
+                       report_errno( "traverse_tar_pre", "ftello", tp->name );
+                       goto fail;
+               }
+               pos += ( 512 - ( pos % 512 ) ) % 512;
+               rc = fseeko( tp->file, pos, SEEK_SET );
+               if ( rc != 0 ) {
+                       report_errno( "traverse_tar_pre", "fseeko", tp->name );
+                       goto fail;
+               }
+       }
+
+done:
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "traverse_tar_pre: %s exit continue\n",
+                       name );
+       cbrc = TRAVERSE_CB_CONTINUE;
+       goto out;
+fail:
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "traverse_tar_pre: %s exit failure\n",
+                       name );
+       cbrc = TRAVERSE_CB_FAIL;
+
+out:
+       if ( source != NULL ) fclose( source );
+       return cbrc;
+}
+
+static int
+tar_tree( const char *path, const char *tar_name, void *ctx )
+{
+       traverse_cb cb;
+       tar_private tp;
+       int rc;
+
+       assert( path != NULL );
+       assert( tar_name != NULL );
+
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "tar_tree: %s into %s entering\n", path,
+                       tar_name );
+
+       cb.pre_func = traverse_tar_pre;
+       cb.post_func = NULL;
+       cb.pre_private = &tp;
+       cb.post_private = NULL;
+
+       tp.name = tar_name;
+       tp.file = fopen( tar_name, "wb" );
+       if ( tp.file == NULL ) {
+               report_errno( "tar_tree", "fopen", tar_name );
+               goto fail;
+       }
+
+       rc = traverse( path, &cb, ctx );
+       if ( rc != 0 ) goto fail;
+
+       {
+               off_t pos = ftello( tp.file );
+               if ( pos == -1 ) {
+                       report_errno( "tar_tree", "ftello", tp.name );
+                       goto fail;
+               }
+               pos += 1024; /* two zero records */
+               pos += ( 10240 - ( pos % 10240 ) ) % 10240;
+               rc = ftruncate( fileno( tp.file ), pos );
+               if ( rc != 0 ) {
+                       report_errno( "tar_tree", "ftrunctate", tp.name );
+                       goto fail;
+               }
+       }
+
+       rc = fclose( tp.file );
+       tp.file = NULL;
+       if ( rc != 0 ) {
+               report_errno( "tar_tree", "fclose", tp.name );
+               goto fail;
+       }
+       goto out;
+
+fail:
+       rc = 1;
+out:
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "tar_tree: %s exit %d\n", path, rc );
+       if ( tp.file != NULL ) fclose( tp.file );
+       return rc;
+}
+
+static int
+homedir_deprovision( const homedir_data *data, const char *path, void *ctx )
+{
+       int rc = 0;
+       char tar_name[1024];
+
+       assert( data != NULL );
+       assert( path != NULL );
+
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "homedir_deprovision: %s entering\n",
+                       path );
+
+       switch ( data->style ) {
+               case DEL_IGNORE:
+                       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                                       "homedir_deprovision: style is ignore\n" );
+                       break;
+               case DEL_ARCHIVE:
+                       if ( data->archive_path == NULL ) {
+                               Debug( LDAP_DEBUG_ANY, "homedir: "
+                                               "homedir_deprovision: archive path not set\n" );
+                               goto fail;
+                       }
+                       rc = get_tar_name( path, data->archive_path, tar_name, 1024 );
+                       if ( rc != 0 ) goto fail;
+                       rc = tar_tree( path, tar_name, ctx );
+                       if ( rc != 0 ) {
+                               Debug( LDAP_DEBUG_ANY, "homedir: "
+                                               "homedir_deprovision: archive failed, not deleting\n" );
+                               goto fail;
+                       }
+                       /* fall-through */
+               case DEL_DELETE:
+                       rc = delete_tree( path, ctx );
+                       break;
+               default:
+                       abort();
+       }
+
+       rc = 0;
+       goto out;
+
+fail:
+       rc = 1;
+out:
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "homedir_deprovision: %s leaving\n",
+                       path );
+
+       return rc;
+}
+
+/* FIXME: This assumes ASCII; needs fixing for z/OS */
+/* FIXME: This should also be in a slapd library function somewhere */
+#define MAX_MATCHES ( 10 )
+static int
+homedir_match(
+               const homedir_regexp *r,
+               const char *homedir,
+               char *result,
+               size_t result_size )
+{
+       int rc;
+       int n;
+       regmatch_t matches[MAX_MATCHES];
+       char *resc, *repc;
+
+       assert( r != NULL );
+       assert( homedir != NULL );
+       assert( result_size > 1 );
+
+       memset( matches, 0, sizeof(matches) );
+       rc = regexec( &r->compiled, homedir, MAX_MATCHES, matches, 0 );
+       if ( rc ) {
+               if ( rc != REG_NOMATCH ) {
+                       char msg[256];
+                       regerror( rc, &r->compiled, msg, sizeof(msg) );
+                       Debug( LDAP_DEBUG_ANY, "homedir_match: "
+                                       "%s\n", msg );
+               }
+               return rc;
+       }
+
+       for ( resc = result, repc = r->replace;
+                       result_size > 1 && *repc != '\0';
+                       ++repc, ++resc, --result_size ) {
+               switch ( *repc ) {
+                       case '$':
+                               ++repc;
+                               n = ( *repc ) - '0';
+                               if ( n < 0 || n > ( MAX_MATCHES - 1 ) ||
+                                               matches[n].rm_so < 0 ) {
+                                       Debug( LDAP_DEBUG_ANY, "homedir: "
+                                                       "invalid regex term expansion in \"%s\" "
+                                                       "at char %ld, n is %d\n",
+                                                       r->replace, (long)( repc - r->replace ), n );
+                                       return 1;
+                               }
+                               {
+                                       size_t match_len = matches[n].rm_eo - matches[n].rm_so;
+                                       const char *match_start = homedir + matches[n].rm_so;
+                                       if ( match_len >= result_size ) goto too_long;
+
+                                       memcpy( resc, match_start, match_len );
+                                       result_size -= match_len;
+                                       resc += match_len - 1;
+                               }
+                               break;
+
+                       case '\\':
+                               ++repc;
+                               /* fallthrough */
+
+                       default:
+                               *resc = *repc;
+               }
+       }
+       *resc = '\0';
+       if ( *repc != '\0' ) goto too_long;
+
+       return 0;
+
+too_long:
+       Debug( LDAP_DEBUG_ANY, "homedir: "
+                       "regex expansion of %s too long\n",
+                       r->replace );
+       *result = '\0';
+       return 1;
+}
+
+/* Sift through an entry for intersting values
+ * return 0 on success and set vars
+ * return 1 if homedir is not present or not valid
+ * sets presence if any homedir attributes are noticed
+ */
+static int
+harvest_values(
+               const homedir_data *data,
+               const Entry *e,
+               char *home_buf,
+               int home_buf_size,
+               uid_t *uidn,
+               gid_t *gidn,
+               int *presence )
+{
+       Attribute *a;
+       char *homedir = NULL;
+
+       assert( data != NULL );
+       assert( e != NULL );
+       assert( home_buf != NULL );
+       assert( home_buf_size > 1 );
+       assert( uidn != NULL );
+       assert( gidn != NULL );
+       assert( presence != NULL );
+
+       *presence = 0;
+       if ( e == NULL ) return 1;
+       *uidn = 0;
+       *gidn = 0;
+
+       for ( a = e->e_attrs; a->a_next != NULL; a = a->a_next ) {
+               if ( a->a_desc == data->home_ad ) {
+                       homedir = a->a_vals[0].bv_val;
+                       *presence = 1;
+               } else if ( a->a_desc == data->uidn_ad ) {
+                       *uidn = (uid_t)strtol( a->a_vals[0].bv_val, NULL, 10 );
+                       *presence = 1;
+               } else if ( a->a_desc == data->gidn_ad ) {
+                       *gidn = (gid_t)strtol( a->a_vals[0].bv_val, NULL, 10 );
+                       *presence = 1;
+               }
+       }
+       if ( homedir != NULL ) {
+               homedir_regexp *r;
+
+               for ( r = data->regexps; r != NULL; r = r->next ) {
+                       int rc = homedir_match( r, homedir, home_buf, home_buf_size );
+                       if ( rc == 0 ) return 0;
+               }
+       }
+
+       return 1;
+}
+
+static int
+homedir_mod_cleanup( Operation *op, SlapReply *rs )
+{
+       slap_callback *cb = NULL;
+       slap_callback **cbp = NULL;
+       homedir_cb_data *cb_data = NULL;
+       Entry *e = NULL;
+
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "homedir_mod_cleanup: entering\n" );
+
+       for ( cbp = &op->o_callback;
+                       *cbp != NULL && (*cbp)->sc_cleanup != homedir_mod_cleanup;
+                       cbp = &(*cbp)->sc_next )
+               ;
+
+       if ( *cbp == NULL ) goto out;
+       cb = *cbp;
+
+       cb_data = (homedir_cb_data *)cb->sc_private;
+       e = cb_data->entry;
+
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "homedir_mod_cleanup: found <%s>\n",
+                       e->e_nname.bv_val );
+       entry_free( e );
+       op->o_tmpfree( cb_data, op->o_tmpmemctx );
+       *cbp = cb->sc_next;
+       op->o_tmpfree( cb, op->o_tmpmemctx );
+
+out:
+
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "homedir_mod_cleanup: leaving\n" );
+       return SLAP_CB_CONTINUE;
+}
+
+static int
+homedir_mod_response( Operation *op, SlapReply *rs )
+{
+       slap_overinst *on = NULL;
+       homedir_data *data = NULL;
+       slap_callback *cb = NULL;
+       homedir_cb_data *cb_data = NULL;
+       Entry *e = NULL;
+       int rc = SLAP_CB_CONTINUE;
+
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "homedir_mod_response: entering\n" );
+
+       if ( rs->sr_err != LDAP_SUCCESS ) {
+               Debug( LDAP_DEBUG_TRACE, "homedir: "
+                               "homedir_mod_response: op was not successful\n" );
+               goto out;
+       }
+
+       /* Retrieve stashed entry */
+       for ( cb = op->o_callback;
+                       cb != NULL && cb->sc_cleanup != homedir_mod_cleanup;
+                       cb = cb->sc_next )
+               ;
+       if ( cb == NULL ) goto out;
+       cb_data = (homedir_cb_data *)cb->sc_private;
+       e = cb_data->entry;
+       on = cb_data->on;
+       data = on->on_bi.bi_private;
+       assert( e != NULL );
+       assert( data != NULL );
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "homedir_mod_response: found <%s>\n",
+                       e->e_nname.bv_val );
+
+       switch ( op->o_tag ) {
+               case LDAP_REQ_DELETE: {
+                       char home_buf[1024];
+                       uid_t uidn = 0;
+                       gid_t gidn = 0;
+                       int presence;
+
+                       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                                       "homedir_mod_response: successful delete found\n" );
+                       rc = harvest_values( data, e, home_buf, sizeof(home_buf), &uidn,
+                                       &gidn, &presence );
+                       if ( rc == 0 && uidn >= data->min_uid ) {
+                               homedir_deprovision( data, home_buf, op->o_tmpmemctx );
+                       } else {
+                               Debug( LDAP_DEBUG_TRACE, "homedir: "
+                                               "homedir_mod_response: skipping\n" );
+                       }
+                       rc = SLAP_CB_CONTINUE;
+                       break;
+               }
+
+               case LDAP_REQ_MODIFY:
+               case LDAP_REQ_MODRDN: {
+                       Operation nop = *op;
+                       Entry *old_entry = e;
+                       Entry *new_entry = NULL;
+                       Entry *etmp;
+                       char old_home[1024];
+                       char new_home[1024];
+                       uid_t old_uidn, new_uidn;
+                       uid_t old_gidn, new_gidn;
+                       int old_valid = 0;
+                       int new_valid = 0;
+                       int old_presence, new_presence;
+
+                       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                                       "homedir_mod_response: successful modify/modrdn found\n" );
+
+                       /* retrieve the revised entry */
+                       nop.o_bd = on->on_info->oi_origdb;
+                       rc = overlay_entry_get_ov(
+                                       &nop, &op->o_req_ndn, NULL, NULL, 0, &etmp, on );
+                       if ( etmp != NULL ) {
+                               new_entry = entry_dup( etmp );
+                               overlay_entry_release_ov( &nop, etmp, 0, on );
+                       }
+                       if ( rc || new_entry == NULL ) {
+                               Debug( LDAP_DEBUG_ANY, "homedir: "
+                                               "homedir_mod_response: unable to get revised <%s>\n",
+                                               op->o_req_ndn.bv_val );
+                               if ( new_entry != NULL ) {
+                                       entry_free( new_entry );
+                                       new_entry = NULL;
+                               }
+                       }
+
+                       /* analyze old and new */
+                       rc = harvest_values( data, old_entry, old_home, 1024, &old_uidn,
+                                       &old_gidn, &old_presence );
+                       if ( rc == 0 && old_uidn >= data->min_uid ) old_valid = 1;
+                       if ( new_entry != NULL ) {
+                               rc = harvest_values( data, new_entry, new_home, 1024, &new_uidn,
+                                               &new_gidn, &new_presence );
+                               if ( rc == 0 && new_uidn >= data->min_uid ) new_valid = 1;
+                               entry_free( new_entry );
+                               new_entry = NULL;
+                       }
+
+                       if ( new_valid && !old_valid ) { /* like an add */
+                               if ( old_presence )
+                                       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                                                       "homedir_mod_response: old entry is now valid\n" );
+                               Debug( LDAP_DEBUG_TRACE, "homedir: "
+                                               "homedir_mod_response: treating like an add\n" );
+                               homedir_provision( new_home, data->skeleton_path, new_uidn,
+                                               new_gidn, op->o_tmpmemctx );
+
+                       } else if ( old_valid && !new_valid &&
+                                       !new_presence ) { /* like a del */
+                               Debug( LDAP_DEBUG_TRACE, "homedir: "
+                                               "homedir_mod_response: treating like a del\n" );
+                               homedir_deprovision( data, old_home, op->o_tmpmemctx );
+
+                       } else if ( new_valid && old_valid ) { /* change */
+                               int did_something = 0;
+
+                               if ( strcmp( old_home, new_home ) != 0 ) {
+                                       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                                                       "homedir_mod_response: treating like a rename\n" );
+                                       homedir_rename( old_home, new_home );
+                                       did_something = 1;
+                               }
+                               if ( old_uidn != new_uidn || old_gidn != new_gidn ) {
+                                       Debug( LDAP_DEBUG_ANY, "homedir: "
+                                                       "homedir_mod_response: rechowning\n" );
+                                       chown_tree( new_home, old_uidn, new_uidn, old_gidn,
+                                                       new_gidn, op->o_tmpmemctx );
+                                       did_something = 1;
+                               }
+                               if ( !did_something ) {
+                                       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                                                       "homedir_mod_response: nothing to do\n" );
+                               }
+                       } else if ( old_presence || new_presence ) {
+                               Debug( LDAP_DEBUG_ANY, "homedir: "
+                                               "homedir_mod_response: <%s> values present "
+                                               "but invalid; ignoring\n",
+                                               op->o_req_ndn.bv_val );
+                       }
+                       rc = SLAP_CB_CONTINUE;
+                       break;
+               }
+
+               default:
+                       rc = SLAP_CB_CONTINUE;
+       }
+
+out:
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "homedir_mod_response: leaving\n" );
+       return rc;
+}
+
+static int
+homedir_op_mod( Operation *op, SlapReply *rs )
+{
+       slap_overinst *on = (slap_overinst *)op->o_bd->bd_info;
+       slap_callback *cb = NULL;
+       homedir_cb_data *cb_data = NULL;
+       Entry *e = NULL;
+       Entry *se = NULL;
+       Operation nop = *op;
+       int rc;
+
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "homedir_op_mod: entering\n" );
+
+       /* retrieve the entry */
+       nop.o_bd = on->on_info->oi_origdb;
+       rc = overlay_entry_get_ov( &nop, &op->o_req_ndn, NULL, NULL, 0, &e, on );
+       if ( e != NULL ) {
+               se = entry_dup( e );
+               overlay_entry_release_ov( &nop, e, 0, on );
+               e = se;
+       }
+       if ( rc || e == NULL ) {
+               Debug( LDAP_DEBUG_ANY, "homedir: "
+                               "homedir_op_mod: unable to get <%s>\n",
+                               op->o_req_ndn.bv_val );
+               goto out;
+       }
+
+       /* Allocate the callback to hold the entry */
+       cb = op->o_tmpalloc( sizeof(slap_callback), op->o_tmpmemctx );
+       cb_data = op->o_tmpalloc( sizeof(homedir_cb_data), op->o_tmpmemctx );
+       cb->sc_cleanup = homedir_mod_cleanup;
+       cb->sc_response = homedir_mod_response;
+       cb->sc_private = cb_data;
+       cb_data->entry = e;
+       e = NULL;
+       cb_data->on = on;
+       cb->sc_next = op->o_callback;
+       op->o_callback = cb;
+
+out:
+       if ( e != NULL ) entry_free( e );
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "homedir_op_mod: leaving\n" );
+       return SLAP_CB_CONTINUE;
+}
+
+static int
+homedir_response( Operation *op, SlapReply *rs )
+{
+       slap_overinst *on = (slap_overinst *)op->o_bd->bd_info;
+       homedir_data *data = on->on_bi.bi_private;
+
+       Debug( LDAP_DEBUG_TRACE, "homedir: "
+                       "homedir_response: entering\n" );
+       if ( rs->sr_err != LDAP_SUCCESS || data == NULL ) return SLAP_CB_CONTINUE;
+
+       switch ( op->o_tag ) {
+               case LDAP_REQ_ADD: { /* Check for new homedir */
+                       char home_buf[1024];
+                       uid_t uidn = 0;
+                       gid_t gidn = 0;
+                       int rc, presence;
+
+                       rc = harvest_values( data, op->ora_e, home_buf, sizeof(home_buf),
+                                       &uidn, &gidn, &presence );
+                       if ( rc == 0 && uidn >= data->min_uid ) {
+                               homedir_provision( home_buf, data->skeleton_path, uidn, gidn,
+                                               op->o_tmpmemctx );
+                       }
+                       return SLAP_CB_CONTINUE;
+               }
+
+               default:
+                       return SLAP_CB_CONTINUE;
+       }
+
+       return SLAP_CB_CONTINUE;
+}
+
+static int
+homedir_db_init( BackendDB *be, ConfigReply *cr )
+{
+       slap_overinst *on = (slap_overinst *)be->bd_info;
+       homedir_data *data = ch_calloc( 1, sizeof(homedir_data) );
+       const char *text;
+
+       if ( slap_str2ad( "homeDirectory", &data->home_ad, &text ) ||
+                       slap_str2ad( "uidNumber", &data->uidn_ad, &text ) ||
+                       slap_str2ad( "gidNumber", &data->gidn_ad, &text ) ) {
+               Debug( LDAP_DEBUG_ANY, "homedir: "
+                               "nis schema not available\n" );
+               return 1;
+       }
+
+       data->skeleton_path = strdup( DEFAULT_SKEL );
+       data->min_uid = DEFAULT_MIN_UID;
+       data->archive_path = NULL;
+
+       on->on_bi.bi_private = data;
+       return 0;
+}
+
+static int
+homedir_db_destroy( BackendDB *be, ConfigReply *cr )
+{
+       slap_overinst *on = (slap_overinst *)be->bd_info;
+       homedir_data *data = on->on_bi.bi_private;
+       homedir_regexp *r, *rnext;
+
+       if ( data != NULL ) {
+               for ( r = data->regexps; r != NULL; r = rnext ) {
+                       rnext = r->next;
+                       ch_free( r->match );
+                       ch_free( r->replace );
+                       regfree( &r->compiled );
+                       ch_free( r );
+               }
+               data->regexps = NULL;
+               if ( data->skeleton_path != NULL ) ch_free( data->skeleton_path );
+               if ( data->archive_path != NULL ) ch_free( data->archive_path );
+               ch_free( data );
+       }
+
+       return 0;
+}
+
+int
+homedir_initialize()
+{
+       int rc;
+
+       assert( ' ' == 32 ); /* Lots of ASCII requirements for now */
+
+       memset( &homedir, 0, sizeof(homedir) );
+
+       homedir.on_bi.bi_type = "homedir";
+       homedir.on_bi.bi_db_init = homedir_db_init;
+       homedir.on_bi.bi_db_destroy = homedir_db_destroy;
+       homedir.on_bi.bi_op_delete = homedir_op_mod;
+       homedir.on_bi.bi_op_modify = homedir_op_mod;
+       homedir.on_response = homedir_response;
+
+       homedir.on_bi.bi_cf_ocs = homedirocs;
+       rc = config_register_schema( homedircfg, homedirocs );
+       if ( rc ) return rc;
+
+       ldap_pvt_thread_mutex_init( &readdir_mutex );
+
+       return overlay_register( &homedir );
+}
+
+int
+homedir_terminate()
+{
+       ldap_pvt_thread_mutex_destroy( &readdir_mutex );
+       return 0;
+}
+
+#if SLAPD_OVER_HOMEDIR == SLAPD_MOD_DYNAMIC && defined(PIC)
+int
+init_module( int argc, char *argv[] )
+{
+       return homedir_initialize();
+}
+
+int
+term_module()
+{
+       return homedir_terminate();
+}
+#endif
+
+#endif /* SLAPD_OVER_HOMEDIR */
diff --git a/tests/data/homedir/skel/.dotfile b/tests/data/homedir/skel/.dotfile
new file mode 100644 (file)
index 0000000..ec11f7d
--- /dev/null
@@ -0,0 +1 @@
+some config
diff --git a/tests/data/homedir/skel/directory/broken link b/tests/data/homedir/skel/directory/broken link
new file mode 120000 (symlink)
index 0000000..78bc337
--- /dev/null
@@ -0,0 +1 @@
+../target
\ No newline at end of file
diff --git a/tests/data/homedir/skel/symlink b/tests/data/homedir/skel/symlink
new file mode 120000 (symlink)
index 0000000..6d0450c
--- /dev/null
@@ -0,0 +1 @@
+directory
\ No newline at end of file
diff --git a/tests/data/slapd-homedir.conf b/tests/data/slapd-homedir.conf
new file mode 100644 (file)
index 0000000..6241aa6
--- /dev/null
@@ -0,0 +1,57 @@
+# stand-alone slapd config -- for testing (with deref overlay)
+# $OpenLDAP$
+## This work is part of OpenLDAP Software <http://www.openldap.org/>.
+##
+## Copyright 2004-2021 The OpenLDAP Foundation.
+## 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>.
+
+include                @SCHEMADIR@/core.schema
+include                @SCHEMADIR@/cosine.schema
+include                @SCHEMADIR@/inetorgperson.schema
+include                @SCHEMADIR@/openldap.schema
+include                @SCHEMADIR@/nis.schema
+
+#
+pidfile                @TESTDIR@/slapd.1.pid
+argsfile       @TESTDIR@/slapd.1.args
+
+#mod#modulepath        ../servers/slapd/back-@BACKEND@/
+#mod#moduleload        back_@BACKEND@.la
+#homedirmod#moduleload ../servers/slapd/overlays/homedir.la
+
+#######################################################################
+# database definitions
+#######################################################################
+
+database       @BACKEND@
+suffix         "dc=example,dc=com"
+rootdn         "cn=Manager,dc=example,dc=com"
+rootpw         secret
+#null#bind             on
+#~null~#directory      @TESTDIR@/db.1.a
+#indexdb#index         objectClass     eq
+#indexdb#index         cn,sn,uid       pres,eq,sub
+#mdb#maxsize   33554432
+#ndb#dbname db_1
+#ndb#include @DATADIR@/ndb.conf
+
+overlay homedir
+
+homedir-min-uidnumber @MINUID@
+homedir-skeleton-path @DATADIR@/homedir/skel
+homedir-regexp ^(/home/[-_/a-z0-9]+)$ @TESTDIR@/$1
+homedir-delete-style ARCHIVE
+homedir-archive-path @TESTDIR@/archive
+
+database config
+include        @TESTDIR@/configpw.conf
+
+database       monitor
index ba649aaf00fd6eb662f7d13d13a832b23fdfaae3..3b58d0c23499dc752db3a85fc6b4385eae779a39 100644 (file)
@@ -47,6 +47,7 @@ AC_constraint=constraint@BUILD_CONSTRAINT@
 AC_dds=dds@BUILD_DDS@
 AC_deref=deref@BUILD_DEREF@
 AC_dynlist=dynlist@BUILD_DYNLIST@
+AC_homedir=homedir@BUILD_HOMEDIR@
 AC_memberof=memberof@BUILD_MEMBEROF@
 AC_pcache=pcache@BUILD_PROXYCACHE@
 AC_ppolicy=ppolicy@BUILD_PPOLICY@
@@ -78,8 +79,8 @@ if test "${AC_asyncmeta}" = "asyncmetamod" && test "${AC_LIBS_DYNAMIC}" = "stati
        AC_meta="asyncmetano"
 fi
 export AC_ldap AC_mdb AC_meta AC_asyncmeta AC_monitor AC_null AC_perl AC_relay AC_sql \
-       AC_accesslog AC_argon2 AC_autoca AC_constraint AC_dds AC_deref AC_dynlist AC_memberof \
-       AC_pcache AC_ppolicy AC_refint AC_remoteauth \
+       AC_accesslog AC_argon2 AC_autoca AC_constraint AC_dds AC_deref AC_dynlist \
+       AC_homedir AC_memberof AC_pcache AC_ppolicy AC_refint AC_remoteauth \
        AC_retcode AC_rwm AC_unique AC_syncprov AC_translucent \
        AC_valsort \
        AC_lloadd \
index dcd3ea9a5eb22183aef66936a5e7cc64b2143ec0..0b79dc0adfb757511bed2770cbe41823fac8bfac 100755 (executable)
@@ -40,6 +40,7 @@ sed -e "s/@BACKEND@/${BACKEND}/"                      \
        -e "s/^#${AC_dds}#//"                           \
        -e "s/^#${AC_deref}#//"                         \
        -e "s/^#${AC_dynlist}#//"                       \
+       -e "s/^#${AC_homedir}#//"                       \
        -e "s/^#${AC_pcache}#//"                        \
        -e "s/^#${AC_ppolicy}#//"                       \
        -e "s/^#${AC_refint}#//"                        \
index 77b094c63ebdac8937949f0a2f1597924c155097..82d64499b8c54c35a2eb44035c63d186d6297095 100755 (executable)
@@ -35,6 +35,7 @@ CONSTRAINT=${AC_constraint-constraintno}
 DDS=${AC_dds-ddsno}
 DEREF=${AC_deref-derefno}
 DYNLIST=${AC_dynlist-dynlistno}
+HOMEDIR=${AC_homedir-homedirno}
 MEMBEROF=${AC_memberof-memberofno}
 PROXYCACHE=${AC_pcache-pcacheno}
 PPOLICY=${AC_ppolicy-ppolicyno}
@@ -150,6 +151,7 @@ ACICONF=$DATADIR/slapd-aci.conf
 VALSORTCONF=$DATADIR/slapd-valsort.conf
 DEREFCONF=$DATADIR/slapd-deref.conf
 DYNLISTCONF=$DATADIR/slapd-dynlist.conf
+HOMEDIRCONF=$DATADIR/slapd-homedir.conf
 RCONSUMERCONF=$DATADIR/slapd-repl-consumer-remote.conf
 PLSRCONSUMERCONF=$DATADIR/slapd-syncrepl-consumer-persist-ldap.conf
 PLSRPROVIDERCONF=$DATADIR/slapd-syncrepl-multiproxy.conf
diff --git a/tests/scripts/test085-homedir b/tests/scripts/test085-homedir
new file mode 100755 (executable)
index 0000000..7119dd8
--- /dev/null
@@ -0,0 +1,139 @@
+#! /bin/sh
+# $OpenLDAP$
+## This work is part of OpenLDAP Software <http://www.openldap.org/>.
+##
+## Copyright 2021 The OpenLDAP Foundation.
+## 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>.
+
+echo "running defines.sh"
+. $SRCDIR/scripts/defines.sh
+
+if test $DEREF = homedirno; then
+       echo "Homedir overlay not available, test skipped"
+       exit 0
+fi
+
+mkdir -p $TESTDIR $DBDIR1 $TESTDIR/home $TESTDIR/archive
+
+$SLAPPASSWD -g -n >$CONFIGPWF
+echo "rootpw `$SLAPPASSWD -T $CONFIGPWF`" >$TESTDIR/configpw.conf
+
+echo "Running slapadd to build slapd database..."
+. $CONFFILTER $BACKEND < $HOMEDIRCONF | sed "s/@MINUID@/`id -u`/" > $CONF1
+$SLAPADD -f $CONF1 -l $LDIF
+RC=$?
+if test $RC != 0 ; then
+       echo "slapadd failed ($RC)!"
+       exit $RC
+fi
+
+echo "Starting slapd on TCP/IP port $PORT1..."
+$SLAPD -f $CONF1 -h $URI1 -d $LVL > $LOG1 2>&1 &
+PID=$!
+if test $WAIT != 0 ; then
+    echo PID $PID
+    read foo
+fi
+KILLPIDS="$PID"
+
+sleep 1
+
+echo "Using ldapsearch to check that slapd is running..."
+for i in 0 1 2 3 4 5; do
+       $LDAPSEARCH -s base -b "$MONITOR" -H $URI1 \
+               'objectclass=*' > /dev/null 2>&1
+       RC=$?
+       if test $RC = 0 ; then
+               break
+       fi
+       echo "Waiting 5 seconds for slapd to start..."
+       sleep 5
+done
+if test $RC != 0 ; then
+       echo "ldapsearch failed ($RC)!"
+       test $KILLSERVERS != no && kill -HUP $KILLPIDS
+       exit $RC
+fi
+
+echo "Adding a new user..."
+$LDAPADD -D "$MANAGERDN" -H $URI1 -w $PASSWD <<EOMOD >> $TESTOUT 2>&1
+dn: uid=user1,ou=People,$BASEDN
+objectClass: account
+objectClass: posixAccount
+uid: user1
+cn: One user
+uidNumber: `id -u`
+gidNumber: `id -g`
+homeDirectory: /home/user1
+EOMOD
+RC=$?
+if test $RC != 0 ; then
+       echo "ldapadd failed ($RC)!"
+       test $KILLSERVERS != no && kill -HUP $KILLPIDS
+       exit $RC
+fi
+
+sleep 1
+
+if ! test -e $TESTDIR/home/user1 ; then
+       echo "Home directory for user1 not created!"
+       test $KILLSERVERS != no && kill -HUP $KILLPIDS
+       exit 1
+fi
+
+echo "Moving home directory for user1..."
+$LDAPMODIFY -D "$MANAGERDN" -H $URI1 -w $PASSWD <<EOMOD >> $TESTOUT 2>&1
+dn: uid=user1,ou=People,$BASEDN
+changetype: modify
+replace: homeDirectory
+homeDirectory: /home/user1_new
+EOMOD
+RC=$?
+if test $RC != 0 ; then
+       echo "ldapadd failed ($RC)!"
+       test $KILLSERVERS != no && kill -HUP $KILLPIDS
+       exit $RC
+fi
+
+sleep 1
+
+if test -e $TESTDIR/home/user1 || ! test -e $TESTDIR/home/user1_new ; then
+       echo "Home directory for user1 not moved!"
+       test $KILLSERVERS != no && kill -HUP $KILLPIDS
+       exit 1
+fi
+
+echo "Removing user1, should get archived..."
+$LDAPDELETE -D "$MANAGERDN" -H $URI1 -w $PASSWD \
+    "uid=user1,ou=People,$BASEDN" >> $TESTOUT
+RC=$?
+if test $RC != 0 ; then
+       echo "ldapdelete failed ($RC)!"
+       test $KILLSERVERS != no && kill -HUP $KILLPIDS
+       exit $RC
+fi
+
+sleep 1
+
+if test -e $TESTDIR/home/user1_new || \
+               ! test -e $TESTDIR/archive/user1_new-*-0.tar ; then
+       echo "Home directory for user1 not archived properly!"
+       test $KILLSERVERS != no && kill -HUP $KILLPIDS
+       exit 1
+fi
+
+test $KILLSERVERS != no && kill -HUP $KILLPIDS
+
+test $KILLSERVERS != no && wait
+
+echo ">>>>> Test succeeded"
+
+exit 0