From 56877e333ba38da2bb23d68fdafa37d7597366aa Mon Sep 17 00:00:00 2001 From: =?utf8?q?Ond=C5=99ej=20Kuzn=C3=ADk?= Date: Tue, 7 Jun 2022 09:35:45 +0100 Subject: [PATCH] ITS#9817 Introduce DN and filter manipulation tools --- doc/man/man5/slapo-rwm.5 | 43 +++++- libraries/librewrite/Makefile.in | 4 +- libraries/librewrite/escapemap.c | 221 +++++++++++++++++++++++++++++++ libraries/librewrite/map.c | 6 + tests/data/rewrite.conf | 23 ++++ tests/data/rewrite.out | 20 +++ tests/scripts/test087-librewrite | 89 +++++++++++++ 7 files changed, 402 insertions(+), 4 deletions(-) create mode 100644 libraries/librewrite/escapemap.c create mode 100644 tests/data/rewrite.conf create mode 100644 tests/data/rewrite.out create mode 100755 tests/scripts/test087-librewrite diff --git a/doc/man/man5/slapo-rwm.5 b/doc/man/man5/slapo-rwm.5 index 69912d66a5..39d247168b 100644 --- a/doc/man/man5/slapo-rwm.5 +++ b/doc/man/man5/slapo-rwm.5 @@ -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 diff --git a/libraries/librewrite/Makefile.in b/libraries/librewrite/Makefile.in index 9e8dc3ff34..f40f1118be 100644 --- a/libraries/librewrite/Makefile.in +++ b/libraries/librewrite/Makefile.in @@ -17,11 +17,11 @@ ## 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 index 0000000000..70ac9e0622 --- /dev/null +++ b/libraries/librewrite/escapemap.c @@ -0,0 +1,221 @@ +/* $OpenLDAP$ */ +/* This work is part of OpenLDAP Software . + * + * 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 + * . + */ +/* ACKNOWLEDGEMENT: + * This work was initially developed by Ondřej Kuzník for inclusion in OpenLDAP + * Software. + */ + +#include + +#define LDAP_DEPRECATED 1 +#include "rewrite-int.h" +#include "rewrite-map.h" + +#include + +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 +}; diff --git a/libraries/librewrite/map.c b/libraries/librewrite/map.c index 3fa5863ce8..c5243a0e50 100644 --- a/libraries/librewrite/map.c +++ b/libraries/librewrite/map.c @@ -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; irm_name )) return mappers[i]; diff --git a/tests/data/rewrite.conf b/tests/data/rewrite.conf new file mode 100644 index 0000000000..eef4d93a7f --- /dev/null +++ b/tests/data/rewrite.conf @@ -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 index 0000000000..2b681a3b88 --- /dev/null +++ b/tests/data/rewrite.out @@ -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 index 0000000000..02178eb64d --- /dev/null +++ b/tests/scripts/test087-librewrite @@ -0,0 +1,89 @@ +#! /bin/sh +# $OpenLDAP$ +## This work is part of OpenLDAP Software . +## +## 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 +## . + +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 -- 2.47.3