]> git.ipfire.org Git - thirdparty/openldap.git/commitdiff
ITS#9817 Introduce DN and filter manipulation tools
authorOndřej Kuzník <ondra@mistotebe.net>
Tue, 7 Jun 2022 08:35:45 +0000 (09:35 +0100)
committerOndřej Kuzník <ondra@mistotebe.net>
Tue, 7 Jun 2022 08:47:41 +0000 (09:47 +0100)
doc/man/man5/slapo-rwm.5
libraries/librewrite/Makefile.in
libraries/librewrite/escapemap.c [new file with mode: 0644]
libraries/librewrite/map.c
tests/data/rewrite.conf [new file with mode: 0644]
tests/data/rewrite.out [new file with mode: 0644]
tests/scripts/test087-librewrite [new file with mode: 0755]

index 69912d66a56d85a59ffec3a7d7f56ecfad7b44c3..39d247168b04c9ceef43ab2705ab23b6591e5358 100644 (file)
@@ -243,7 +243,7 @@ code set to
 .BR n ;
 or, in other words, `@' is equivalent to `U{0}'.
 Positive errors are allowed, indicating the related LDAP error codes
-as specified in \fIdraft-ietf-ldapbis-protocol\fP.
+as specified in \fIRFC4511\fP.
 .LP
 The ordering of the flags can be significant.
 For instance: `IG{2}' means ignore errors and jump two lines ahead
@@ -492,6 +492,37 @@ LDAP map, the
 portion must contain exactly one attribute, and if
 a multi-valued attribute is used, only the first value is considered.
 
+.TP
+.B escape [escape2dn|escape2filter|unescapedn|unescapefilter]...
+The
+.B escape
+map makes it possible use DNs or their parts in filter strings and vice versa.
+It processes a value according to the operations listed in order. Supported
+operations include:
+
+.RS
+.TP
+.B escape2dn
+takes a string and escapes it so it can safely be pasted in a DN
+.TP
+.B escape2filter
+takes a string and escapes it so it can safely be pasted in a filter
+.TP
+.B unescapedn
+takes a string and undoes DN escaping
+.TP
+.B unescapefilter
+takes a string and undoes filter escaping
+.RE
+
+.RS
+It is advised that each
+.B escape
+map ends with an
+.B escape
+operation as that is the only safe way to handle arbitrary strings.
+.RE
+
 .SH "REWRITE CONFIGURATION EXAMPLES"
 .nf
 # set to `off' to disable rewriting
@@ -566,6 +597,9 @@ rwm\-rewriteContext  searchEntryDN
 rwm\-rewriteRule     "(.*[^ ],)?[ ]?dc=OpenLDAP,[ ]?dc=org$"
                 "${>eatBlanks($1)}dc=home,dc=net"    ":"
 
+# Transform a DN value such that it can be used in a filter
+rwm\-rewriteMap escape dn2filter unescapedn escape2filter
+
 # Bind with email instead of full DN: we first need
 # an ldap map that turns attributes into a DN (the
 # argument used when invoking the map is appended to 
@@ -579,8 +613,13 @@ rwm\-rewriteMap ldap attr2dn "ldap://host/dc=my,dc=org?dn?sub"
 # to real naming contexts, we also need to rewrite
 # regular DNs, because the definition of a bindDN
 # rewrite context overrides the default definition.
+#
+# While actual email addresses tend not to contain filter
+# special characters, the provided Bind DN has no such
+# restrictions.
 rwm\-rewriteContext bindDN
-rwm\-rewriteRule "^mail=[^,]+@[^,]+$" "${attr2dn($0)}" ":@I"
+rwm\-rewriteRule "^(mail=)([^,]+@[^,]+)$"
+                "${attr2dn($1${dn2filter($2)})}" ":@I"
 
 # This is a rather sophisticated example. It massages a
 # search filter in case who performs the search has
index 9e8dc3ff342122d6926770ee7b14caab9b6c84cc..f40f1118be8fbcef2c167564cf483390b6f32708 100644 (file)
 ##
 
 SRCS = config.c context.c info.c ldapmap.c map.c params.c rule.c \
-       session.c subst.c var.c xmap.c \
+       session.c subst.c var.c xmap.c escapemap.c \
        parse.c rewrite.c
 XSRCS = version.c
 OBJS = config.o context.o info.o ldapmap.o map.o params.o rule.o \
-       session.o subst.o var.o xmap.o
+       session.o subst.o var.o xmap.o escapemap.o
 
 LDAP_INCDIR= ../../include       
 LDAP_LIBDIR= ../../libraries
diff --git a/libraries/librewrite/escapemap.c b/libraries/librewrite/escapemap.c
new file mode 100644 (file)
index 0000000..70ac9e0
--- /dev/null
@@ -0,0 +1,221 @@
+/* $OpenLDAP$ */
+/* This work is part of OpenLDAP Software <http://www.openldap.org/>.
+ *
+ * Copyright 2000-2022 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>.
+ */
+/* ACKNOWLEDGEMENT:
+ * This work was initially developed by Ondřej Kuzník for inclusion in OpenLDAP
+ * Software.
+ */
+
+#include <portable.h>
+
+#define LDAP_DEPRECATED 1
+#include "rewrite-int.h"
+#include "rewrite-map.h"
+
+#include <ldap_pvt.h>
+
+typedef int (escape_fn)( struct berval *input, struct berval *output );
+
+/*
+ * Map configuration, a NULL-terminated list of escape_fn pointers
+ */
+struct escape_map_data {
+       escape_fn **fn;
+};
+
+/*
+ * (un)escape functions
+ */
+
+static int
+map_escape_to_filter( struct berval *input, struct berval *output )
+{
+       return ldap_bv2escaped_filter_value( input, output );
+}
+
+static int
+map_unescape_filter( struct berval *input, struct berval *output )
+{
+       ber_slen_t len;
+
+       if ( ber_dupbv( output, input ) == NULL ) {
+               return REWRITE_ERR;
+       }
+
+       len = ldap_pvt_filter_value_unescape( output->bv_val );
+       if ( len < 0 ) return REWRITE_ERR;
+       output->bv_len = len;
+
+       return LDAP_SUCCESS;
+}
+
+static int
+map_escape_to_dn( struct berval *input, struct berval *output )
+{
+       LDAPAVA ava = { .la_attr = BER_BVC("uid"),
+                                       .la_value = *input,
+                                       .la_flags = LDAP_AVA_STRING },
+                       *ava_[] = { &ava, NULL };
+       LDAPRDN rdn[] = { ava_, NULL };
+       LDAPDN dn = rdn;
+       struct berval dnstr;
+       char *p;
+       int rc;
+
+       rc = ldap_dn2bv( dn, &dnstr, LDAP_DN_FORMAT_LDAPV3 );
+       if ( rc != LDAP_SUCCESS ) {
+               return REWRITE_ERR;
+       }
+
+       p = strchr( dnstr.bv_val, '=' );
+       p++;
+
+       output->bv_len = dnstr.bv_len - ( p - dnstr.bv_val );
+       output->bv_val = malloc( output->bv_len + 1 );
+       if ( output->bv_val == NULL ) {
+               free( dnstr.bv_val );
+               return REWRITE_ERR;
+       }
+       memcpy( output->bv_val, p, output->bv_len );
+       output->bv_val[output->bv_len] = '\0';
+
+       free( dnstr.bv_val );
+       return REWRITE_SUCCESS;
+}
+
+static int
+map_unescape_dn( struct berval *input, struct berval *output )
+{
+       LDAPDN dn;
+       struct berval fake_dn;
+       char *p;
+       int rc = REWRITE_SUCCESS;
+
+       fake_dn.bv_len = STRLENOF("uid=") + input->bv_len;
+       fake_dn.bv_val = p = malloc( fake_dn.bv_len );
+       if ( p == NULL ) {
+               return REWRITE_ERR;
+       }
+
+       memcpy( p, "uid=", STRLENOF("uid=") );
+       p += STRLENOF("uid=");
+       memcpy( p, input->bv_val, input->bv_len );
+
+       if ( ldap_bv2dn( &fake_dn, &dn, LDAP_DN_FORMAT_LDAPV3 ) != LDAP_SUCCESS ) {
+               return REWRITE_ERR;
+       }
+       if ( ber_dupbv( output, &dn[0][0]->la_value ) == NULL ) {
+               rc = REWRITE_ERR;
+       }
+       ldap_dnfree( dn );
+       return rc;
+}
+
+/* Registered callbacks */
+
+static void *
+map_escape_parse(
+               const char *fname,
+               int lineno,
+               int argc,
+               char **argv
+)
+{
+       escape_fn **fns;
+       int i;
+
+       assert( fname != NULL );
+       assert( argv != NULL );
+
+       if ( argc < 1 ) {
+               Debug( LDAP_DEBUG_ANY,
+                               "[%s:%d] escape map needs at least one operation\n",
+                               fname, lineno );
+               return NULL;
+       }
+
+       fns = calloc( sizeof(escape_fn *), argc + 1 );
+       if ( fns == NULL ) {
+               return NULL;
+       }
+
+       for ( i = 0; i < argc; i++ ) {
+               if ( strcasecmp( argv[i], "escape2dn" ) == 0 ) {
+                       fns[i] = map_escape_to_dn;
+               } else if ( strcasecmp( argv[i], "escape2filter" ) == 0 ) {
+                       fns[i] = map_escape_to_filter;
+               } else if ( strcasecmp( argv[i], "unescapedn" ) == 0 ) {
+                       fns[i] = map_unescape_dn;
+               } else if ( strcasecmp( argv[i], "unescapefilter" ) == 0 ) {
+                       fns[i] = map_unescape_filter;
+               } else {
+                       Debug( LDAP_DEBUG_ANY,
+                               "[%s:%d] unknown option %s (ignored)\n",
+                               fname, lineno, argv[i] );
+                       free( fns );
+                       return NULL;
+               }
+       }
+
+       return (void *)fns;
+}
+
+static int
+map_escape_apply(
+               void *private,
+               const char *input,
+               struct berval *output )
+{
+       escape_fn **fns = private;
+       struct berval tmpin, tmpout;
+       int i;
+
+       assert( private != NULL );
+       assert( input != NULL );
+       assert( output != NULL );
+
+       ber_str2bv( input, 0, 1, &tmpin );
+
+       for ( i=0; fns[i]; i++ ) {
+               int rc = fns[i]( &tmpin, &tmpout );
+               free( tmpin.bv_val );
+               if ( rc != REWRITE_SUCCESS ) {
+                       return rc;
+               }
+               tmpin = tmpout;
+       }
+       *output = tmpin;
+
+       return REWRITE_SUCCESS;
+}
+
+static int
+map_escape_destroy(
+               void *private
+)
+{
+       struct ldap_map_data *data = private;
+
+       assert( private != NULL );
+       free( data );
+
+       return 0;
+}
+
+const rewrite_mapper rewrite_escape_mapper = {
+       "escape",
+       map_escape_parse,
+       map_escape_apply,
+       map_escape_destroy
+};
index 3fa5863ce805c2ae431c1aeae237e56eb0499f92..c5243a0e508d24e568534efa5254c2644b758b7d 100644 (file)
@@ -528,6 +528,9 @@ rewrite_map_destroy(
 /* ldapmap.c */
 extern const rewrite_mapper rewrite_ldap_mapper;
 
+/* escapemap.c */
+extern const rewrite_mapper rewrite_escape_mapper;
+
 const rewrite_mapper *
 rewrite_mapper_find(
        const char *name
@@ -538,6 +541,9 @@ rewrite_mapper_find(
        if ( !strcasecmp( name, "ldap" ))
                return &rewrite_ldap_mapper;
 
+       if ( !strcasecmp( name, "escape" ))
+               return &rewrite_escape_mapper;
+
        for (i=0; i<num_mappers; i++)
                if ( !strcasecmp( name, mappers[i]->rm_name ))
                        return mappers[i];
diff --git a/tests/data/rewrite.conf b/tests/data/rewrite.conf
new file mode 100644 (file)
index 0000000..eef4d93
--- /dev/null
@@ -0,0 +1,23 @@
+rewriteEngine on
+
+# We have an attribute value from a DN that we want to paste into a filter
+#
+# N.B. Do not use ${escape2filter(${unescapedn($1)})}, but chain them inside
+# the same rewriteMap like this, since only that is safe in the presence of
+# arbitrary data
+rewriteMap escape dn2filter unescapedn escape2filter
+
+# We have a DN that we want to paste into a filter
+rewriteMap escape dn2dnfilter escape2filter
+
+# We have a filter value and want to construct a DN
+rewriteMap escape fitler2dn unescapefilter escape2dn
+
+rewriteContext testdn2filter
+rewriteRule "^([^=]*)=([^,+]*)((\+[^,]*)?,(.*))?" "(&($1=${dn2filter($2)})(entryDN:dnOneLevelMatch:=$5))" ":"
+
+rewriteContext testdn2dnfilter
+rewriteRule ".*" "entryDN=${dn2dnfilter($0)}" ":"
+
+rewriteContext testfilter2dn
+rewriteRule "^\(([^=]*)=([^)]*)\)" "$1=${fitler2dn($2)},dc=example,dc=com" ":"
diff --git a/tests/data/rewrite.out b/tests/data/rewrite.out
new file mode 100644 (file)
index 0000000..2b681a3
--- /dev/null
@@ -0,0 +1,20 @@
+# making a filter from a DN
+uid=test\20\2c\31\\ -> (&(uid=test ,1\5C)(entryDN:dnOneLevelMatch:=)) [0:ok]
+uid=test,ou=People,dc=example,dc=com -> (&(uid=test)(entryDN:dnOneLevelMatch:=ou=People,dc=example,dc=com)) [0:ok]
+cn=test\00)(uid=test,dc=example,dc=com -> (&(cn=test\00\29\28uid=test)(entryDN:dnOneLevelMatch:=dc=example,dc=com)) [0:ok]
+cn=* -> (&(cn=\2A)(entryDN:dnOneLevelMatch:=)) [0:ok]
+cn=*\\ -> (&(cn=\2A\5C)(entryDN:dnOneLevelMatch:=)) [0:ok]
+cn=*\ -> (null) [-1:error]
+
+# pasting a DN into a filter
+uid=test\20\31\\ -> entryDN=uid=test\5C20\5C31\5C\5C [0:ok]
+cn=test)(uid=test,dc=example,dc=com -> entryDN=cn=test\29\28uid=test,dc=example,dc=com [0:ok]
+cn=* -> entryDN=cn=\2A [0:ok]
+cn=*\\ -> entryDN=cn=\2A\5C\5C [0:ok]
+cn=*\ -> entryDN=cn=\2A\5C [0:ok]
+
+# pasting a filter into a DN
+(uid=test) -> uid=test,dc=example,dc=com [0:ok]
+(cn=something ,\29+\28 \2A=) -> cn=something \2C)\2B( *\3D,dc=example,dc=com [0:ok]
+(description=test\20\31*) -> (null) [-1:error]
+(description=test\20\31) -> description=test 1,dc=example,dc=com [0:ok]
diff --git a/tests/scripts/test087-librewrite b/tests/scripts/test087-librewrite
new file mode 100755 (executable)
index 0000000..02178eb
--- /dev/null
@@ -0,0 +1,89 @@
+#! /bin/sh
+# $OpenLDAP$
+## This work is part of OpenLDAP Software <http://www.openldap.org/>.
+##
+## Copyright 1998-2022 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 $RWM = rwmno ; then
+       echo "rwm (Rewrite/remap) overlay not available, test skipped"
+       exit 0
+fi
+
+echo "" >>$TESTOUT
+echo "# making a filter from a DN" > $TESTOUT
+echo "Testing DN unescaping then escaping for use in a filter..."
+for input in \
+       "uid=test\\20\\2c\\31\\\\" \
+       "uid=test,ou=People,dc=example,dc=com" \
+       "cn=test\\00)(uid=test,dc=example,dc=com" \
+       "cn=*" \
+       "cn=*\\\\" \
+       "cn=*\\" \
+       ; do
+
+       $TESTWD/../libraries/librewrite/rewrite -f $DATADIR/rewrite.conf \
+               -r testdn2filter "$input" >>$TESTOUT 2>/dev/null
+       if test $? != 0 ; then
+               echo "rewriting failed"
+               exit $?
+       fi
+done
+
+echo "" >>$TESTOUT
+echo "# pasting a DN into a filter" >> $TESTOUT
+echo "Testing filter escaping..."
+for input in \
+       "uid=test\\20\\31\\\\" \
+       "cn=test)(uid=test,dc=example,dc=com" \
+       "cn=*" \
+       "cn=*\\\\" \
+       "cn=*\\" \
+       ; do
+
+       $TESTWD/../libraries/librewrite/rewrite -f $DATADIR/rewrite.conf \
+               -r testdn2dnfilter "$input" >>$TESTOUT 2>/dev/null
+       if test $? != 0 ; then
+               echo "rewriting failed"
+               exit $?
+       fi
+done
+
+echo "" >>$TESTOUT
+echo "# pasting a filter into a DN" >> $TESTOUT
+echo "Testing filter unescaping then escaping the value into a DN..."
+for input in \
+       "(uid=test)" \
+       "(cn=something ,\\29+\\28 \\2A=)" \
+       "(description=test\\20\\31*)" \
+       "(description=test\\20\\31)" \
+       ; do
+
+       $TESTWD/../libraries/librewrite/rewrite -f $DATADIR/rewrite.conf \
+               -r testfilter2dn "$input" >>$TESTOUT 2>/dev/null
+       if test $? != 0 ; then
+               echo "rewriting failed"
+               exit $?
+       fi
+done
+
+$CMP $DATADIR/rewrite.out $TESTOUT > $CMPOUT
+
+if test $? != 0 ; then
+       echo "comparison failed - rewriting did not complete correctly"
+       exit 1
+fi
+
+echo ">>>>> Test succeeded"
+exit 0