2 * Copyright (c) 2000, Mark Adamson, Carnegie Mellon. All rights reserved.
3 * This software is not subject to any license of Carnegie Mellon University.
5 * Redistribution and use in source and binary forms are permitted without
6 * restriction or fee of any kind as long as this notice is preserved.
8 * The name "Carnegie Mellon" must not be used to endorse or promote
9 * products derived from this software without prior written permission.
17 #include <ac/stdlib.h>
18 #include <ac/string.h>
22 #ifdef HAVE_CYRUS_SASL
24 #ifdef HAVE_CYRUS_SASL2
25 #include <sasl/sasl.h>
32 /* URI format: ldap://<host>/<base>[?[<attrs>][?[<scope>][?[<filter>]]]] */
34 static int slap_parseURI( char *uri,
35 struct berval *searchbase, int *scope, Filter **filter )
42 assert( uri != NULL );
43 searchbase->bv_val = NULL;
44 searchbase->bv_len = 0;
49 LDAP_LOG(( "sasl", LDAP_LEVEL_ENTRY,
50 "slap_parseURI: parsing %s\n", uri ));
52 Debug( LDAP_DEBUG_TRACE, "slap_parseURI: parsing %s\n", uri, 0, 0 );
55 /* If it does not look like a URI, assume it is a DN */
56 if( !strncasecmp( uri, "dn:", sizeof("dn:")-1 ) ) {
57 uri += sizeof("dn:")-1;
58 uri += strspn( uri, " " );
60 /* FIXME: if dnNormalize actually uses input bv_len we
61 * will have to make this right.
64 rc = dnNormalize2( NULL, &bv, searchbase );
65 if (rc == LDAP_SUCCESS) {
66 *scope = LDAP_SCOPE_BASE;
71 /* FIXME: should use ldap_url_parse() */
72 if( strncasecmp( uri, "ldap://", sizeof("ldap://")-1 ) ) {
77 end = strchr( uri + (sizeof("ldap://")-1), '/' );
79 return( LDAP_PROTOCOL_ERROR );
81 /* could check the hostname here */
83 /* Grab the searchbase */
85 end = strchr( start, '?' );
89 return dnNormalize2( NULL, &bv, searchbase );
92 bv.bv_len = end - start;
93 rc = dnNormalize2( NULL, &bv, searchbase );
95 if (rc != LDAP_SUCCESS)
100 end = strchr( start, '?' );
102 return( LDAP_SUCCESS );
107 if( !strncasecmp( start, "base?", sizeof("base?")-1 )) {
108 *scope = LDAP_SCOPE_BASE;
109 start += sizeof("base?")-1;
111 else if( !strncasecmp( start, "one?", sizeof("one?")-1 )) {
112 *scope = LDAP_SCOPE_ONELEVEL;
113 start += sizeof("one?")-1;
115 else if( !strncasecmp( start, "sub?", sizeof("sub?")-1 )) {
116 *scope = LDAP_SCOPE_SUBTREE;
117 start += sizeof("sub?")-1;
120 free( searchbase->bv_val );
121 searchbase->bv_val = NULL;
122 return( LDAP_PROTOCOL_ERROR );
125 /* Grab the filter */
126 *filter = str2filter( start );
128 return( LDAP_SUCCESS );
132 int slap_sasl_regexp_config( const char *match, const char *replace )
134 #ifdef HAVE_CYRUS_SASL
138 struct berval bv, nbv;
140 SaslRegexp = (SaslRegexp_t *) ch_realloc( (char *) SaslRegexp,
141 (nSaslRegexp + 1) * sizeof(SaslRegexp_t) );
142 reg = &( SaslRegexp[nSaslRegexp] );
143 ber_str2bv( match, 0, 0, &bv );
144 rc = dnNormalize2( NULL, &bv, &nbv );
147 LDAP_LOG(( "sasl", LDAP_LEVEL_ERR,
148 "slap_sasl_regexp_config: \"%s\" could not be normalized.\n",
151 Debug( LDAP_DEBUG_ANY,
152 "SASL match pattern %s could not be normalized.\n",
157 reg->sr_match = nbv.bv_val;
159 ber_str2bv( replace, 0, 0, &bv );
160 rc = dnNormalize2( NULL, &bv, &nbv );
163 LDAP_LOG(( "sasl", LDAP_LEVEL_ERR,
164 "slap_sasl_regexp_config: \"%s\" could not be normalized.\n",
167 Debug( LDAP_DEBUG_ANY,
168 "SASL replace pattern %s could not be normalized.\n",
173 reg->sr_replace = nbv.bv_val;
175 /* Precompile matching pattern */
176 rc = regcomp( ®->sr_workspace, reg->sr_match, REG_EXTENDED|REG_ICASE );
179 LDAP_LOG(( "sasl", LDAP_LEVEL_ERR,
180 "slap_sasl_regexp_config: \"%s\" could not be compiled.\n",
183 Debug( LDAP_DEBUG_ANY,
184 "SASL match pattern %s could not be compiled by regexp engine\n",
185 reg->sr_match, 0, 0 );
188 return( LDAP_OPERATIONS_ERROR );
191 /* Precompile replace pattern. Find the $<n> placeholders */
192 reg->sr_offset[0] = -2;
194 for ( c = reg->sr_replace; *c; c++ ) {
200 if ( n == SASLREGEX_REPLACE ) {
202 LDAP_LOG(( "sasl", LDAP_LEVEL_ERR,
203 "slap_sasl_regexp_config: \"%s\" has too many $n "
204 "placeholders (max %d)\n",
205 reg->sr_replace, SASLREGEX_REPLACE ));
207 Debug( LDAP_DEBUG_ANY,
208 "SASL replace pattern %s has too many $n "
209 "placeholders (max %d)\n",
210 reg->sr_replace, SASLREGEX_REPLACE, 0 );
213 return( LDAP_OPERATIONS_ERROR );
215 reg->sr_offset[n] = c - reg->sr_replace;
220 /* Final placeholder, after the last $n */
221 reg->sr_offset[n] = c - reg->sr_replace;
223 reg->sr_offset[n] = -1;
227 return( LDAP_SUCCESS );
231 #ifdef HAVE_CYRUS_SASL
233 /* Take the passed in SASL name and attempt to convert it into an
234 LDAP URI to find the matching LDAP entry, using the pattern matching
235 strings given in the saslregexp config file directive(s) */
237 char *slap_sasl_regexp( char *saslname )
240 int i, n, len, insert;
244 LDAP_LOG(( "sasl", LDAP_LEVEL_ENTRY,
245 "slap_sasl_regexp: converting SASL name %s\n", saslname ));
247 Debug( LDAP_DEBUG_TRACE, "slap_sasl_regexp: converting SASL name %s\n",
251 if (( saslname == NULL ) || ( nSaslRegexp == 0 ))
254 /* Match the normalized SASL name to the saslregexp patterns */
255 for( reg = SaslRegexp,i=0; i<nSaslRegexp; i++,reg++ ) {
256 if ( regexec( ®->sr_workspace, saslname, SASLREGEX_REPLACE,
257 reg->sr_strings, 0) == 0 )
261 if( i >= nSaslRegexp )
265 * The match pattern may have been of the form "a(b.*)c(d.*)e" and the
266 * replace pattern of the form "x$1y$2z". The returned string needs
267 * to replace the $1,$2 with the strings that matched (b.*) and (d.*)
271 /* Get the total length of the final URI */
275 while( reg->sr_offset[n] >= 0 ) {
276 /* Len of next section from replacement string (x,y,z above) */
277 len += reg->sr_offset[n] - reg->sr_offset[n-1] - 2;
278 if( reg->sr_offset[n+1] < 0)
281 /* Len of string from saslname that matched next $i (b,d above) */
282 i = reg->sr_replace[ reg->sr_offset[n] + 1 ] - '0';
283 len += reg->sr_strings[i].rm_eo - reg->sr_strings[i].rm_so;
286 uri = ch_malloc( len + 1 );
288 /* Fill in URI with replace string, replacing $i as we go */
291 while( reg->sr_offset[n] >= 0) {
292 /* Paste in next section from replacement string (x,y,z above) */
293 len = reg->sr_offset[n] - reg->sr_offset[n-1] - 2;
294 strncpy( uri+insert, reg->sr_replace + reg->sr_offset[n-1] + 2, len);
296 if( reg->sr_offset[n+1] < 0)
299 /* Paste in string from saslname that matched next $i (b,d above) */
300 i = reg->sr_replace[ reg->sr_offset[n] + 1 ] - '0';
301 len = reg->sr_strings[i].rm_eo - reg->sr_strings[i].rm_so;
302 strncpy( uri+insert, saslname + reg->sr_strings[i].rm_so, len );
310 LDAP_LOG(( "sasl", LDAP_LEVEL_ENTRY,
311 "slap_sasl_regexp: converted SASL name to %s\n", uri ));
313 Debug( LDAP_DEBUG_TRACE,
314 "slap_sasl_regexp: converted SASL name to %s\n", uri, 0, 0 );
320 /* Two empty callback functions to avoid sending results */
321 static void sasl_sc_r( Connection *conn, Operation *o, ber_tag_t tag,
322 ber_int_t msgid, ber_int_t err, const char *matched,
323 const char *text, BerVarray ref, const char *resoid,
324 struct berval *resdata, struct berval *sasldata, LDAPControl **c)
328 static void sasl_sc_s( Connection *conn, Operation *o, ber_int_t err,
329 const char *matched, const char *text, BerVarray refs, LDAPControl **c,
334 /* This callback actually does some work...*/
335 static int sasl_sc_sasl2dn( BackendDB *be, Connection *conn, Operation *o,
336 Entry *e, AttributeName *an, int ao, LDAPControl **c)
338 struct berval *ndn = o->o_callback->sc_private;
340 /* We only want to be called once */
345 LDAP_LOG(( "sasl", LDAP_LEVEL_DETAIL1,
346 "slap_sasl2dn: search DN returned more than 1 entry\n" ));
348 Debug( LDAP_DEBUG_TRACE,
349 "slap_sasl2dn: search DN returned more than 1 entry\n", 0,0,0 );
353 ber_dupbv(ndn, &e->e_nname);
359 * Given a SASL name (e.g. "UID=name,cn=REALM,cn=MECH,cn=AUTH")
360 * return the LDAP DN to which it matches. The SASL regexp rules in the config
361 * file turn the SASL name into an LDAP URI. If the URI is just a DN (or a
362 * search with scope=base), just return the URI (or its searchbase). Otherwise
363 * an internal search must be done, and if that search returns exactly one
364 * entry, return the DN of that one entry.
367 void slap_sasl2dn( struct berval *saslname, struct berval *dn )
370 struct berval searchbase = {0, NULL};
374 slap_callback cb = {sasl_sc_r, sasl_sc_s, sasl_sc_sasl2dn, NULL};
378 LDAP_LOG(( "sasl", LDAP_LEVEL_ENTRY,
379 "slap_sasl2dn: converting SASL name %s to DN.\n", saslname->bv_val ));
381 Debug( LDAP_DEBUG_TRACE,
382 "==>slap_sasl2dn: Converting SASL name %s to a DN\n", saslname->bv_val, 0,0 );
388 /* Convert the SASL name into an LDAP URI */
389 uri = slap_sasl_regexp( saslname->bv_val );
393 rc = slap_parseURI( uri, &searchbase, &scope, &filter );
398 /* Massive shortcut: search scope == base */
399 if( scope == LDAP_SCOPE_BASE ) {
401 searchbase.bv_len = 0;
402 searchbase.bv_val = NULL;
406 /* Must do an internal search */
409 LDAP_LOG(( "sasl", LDAP_LEVEL_DETAIL1,
410 "slap_sasl2dn: performing internal search (base=%s, scope=%d)\n",
411 searchbase.bv_val, scope ));
413 Debug( LDAP_DEBUG_TRACE,
414 "slap_sasl2dn: performing internal search (base=%s, scope=%d)\n",
415 searchbase.bv_val, scope, 0 );
418 be = select_backend( &searchbase, 0, 1 );
419 if(( be == NULL ) || ( be->be_search == NULL))
421 suffix_alias( be, &searchbase );
423 ldap_pvt_thread_mutex_init( &op.o_abandonmutex );
424 op.o_tag = LDAP_REQ_SEARCH;
425 op.o_protocol = LDAP_VERSION3;
426 op.o_ndn = *saslname;
428 op.o_time = slap_get_time();
430 (*be->be_search)( be, /*conn*/NULL, &op, /*base*/NULL, &searchbase,
431 scope, /*deref=*/1, /*sizelimit=*/1, /*time=*/0, filter, /*fstr=*/NULL,
432 /*attrs=*/NULL, /*attrsonly=*/0 );
434 ldap_pvt_thread_mutex_destroy( &op.o_abandonmutex );
437 if( searchbase.bv_len ) ch_free( searchbase.bv_val );
438 if( filter ) filter_free( filter );
439 if( uri ) ch_free( uri );
442 LDAP_LOG(( "sasl", LDAP_LEVEL_ENTRY,
443 "slap_sasl2dn: Converted SASL name to %s\n",
444 dn->bv_len ? dn->bv_val : "<nothing>" ));
446 Debug( LDAP_DEBUG_TRACE, "<==slap_sasl2dn: Converted SASL name to %s\n",
447 dn->bv_len ? dn->bv_val : "<nothing>", 0, 0 );
453 typedef struct smatch_info {
458 static int sasl_sc_smatch( BackendDB *be, Connection *conn, Operation *o,
459 Entry *e, AttributeName *an, int ao, LDAPControl **c)
461 smatch_info *sm = o->o_callback->sc_private;
463 if (dn_match(sm->dn, &e->e_nname)) {
465 return -1; /* short-circuit the search */
472 * Map a SASL regexp rule to a DN. If the rule is just a DN or a scope=base
473 * URI, just strcmp the rule (or its searchbase) to the *assertDN. Otherwise,
474 * the rule must be used as an internal search for entries. If that search
475 * returns the *assertDN entry, the match is successful.
477 * The assertDN should not have the dn: prefix
481 int slap_sasl_match( char *rule, struct berval *assertDN, struct berval *authc )
483 struct berval searchbase = {0, NULL};
489 slap_callback cb = {sasl_sc_r, sasl_sc_s, sasl_sc_smatch, &sm};
493 LDAP_LOG(( "sasl", LDAP_LEVEL_ENTRY,
494 "slap_sasl_match: comparing DN %s to rule %s\n", assertDN->bv_val, rule ));
496 Debug( LDAP_DEBUG_TRACE,
497 "===>slap_sasl_match: comparing DN %s to rule %s\n", assertDN->bv_val, rule, 0 );
500 rc = slap_parseURI( rule, &searchbase, &scope, &filter );
501 if( rc != LDAP_SUCCESS )
504 /* Massive shortcut: search scope == base */
505 if( scope == LDAP_SCOPE_BASE ) {
506 rc = regcomp(®, searchbase.bv_val,
507 REG_EXTENDED|REG_ICASE|REG_NOSUB);
509 rc = regexec(®, assertDN->bv_val, 0, NULL, 0);
515 rc = LDAP_INAPPROPRIATE_AUTH;
519 /* Must run an internal search. */
522 LDAP_LOG(( "sasl", LDAP_LEVEL_DETAIL1,
523 "slap_sasl_match: performing internal search (base=%s, scope=%d)\n",
524 searchbase.bv_val, scope ));
526 Debug( LDAP_DEBUG_TRACE,
527 "slap_sasl_match: performing internal search (base=%s, scope=%d)\n",
528 searchbase.bv_val, scope, 0 );
531 be = select_backend( &searchbase, 0, 1 );
532 if(( be == NULL ) || ( be->be_search == NULL)) {
533 rc = LDAP_INAPPROPRIATE_AUTH;
536 suffix_alias( be, &searchbase );
541 ldap_pvt_thread_mutex_init( &op.o_abandonmutex );
542 op.o_tag = LDAP_REQ_SEARCH;
543 op.o_protocol = LDAP_VERSION3;
546 op.o_time = slap_get_time();
548 (*be->be_search)( be, /*conn=*/NULL, &op, /*base=*/NULL, &searchbase,
549 scope, /*deref=*/1, /*sizelimit=*/0, /*time=*/0, filter, /*fstr=*/NULL,
550 /*attrs=*/NULL, /*attrsonly=*/0 );
552 ldap_pvt_thread_mutex_destroy( &op.o_abandonmutex );
557 rc = LDAP_INAPPROPRIATE_AUTH;
560 if( searchbase.bv_len ) ch_free( searchbase.bv_val );
561 if( filter ) filter_free( filter );
563 LDAP_LOG(( "sasl", LDAP_LEVEL_ENTRY,
564 "slap_sasl_match: comparison returned %d\n", rc ));
566 Debug( LDAP_DEBUG_TRACE,
567 "<===slap_sasl_match: comparison returned %d\n", rc, 0, 0);
575 * This function answers the question, "Can this ID authorize to that ID?",
576 * based on authorization rules. The rules are stored in the *searchDN, in the
577 * attribute named by *attr. If any of those rules map to the *assertDN, the
578 * authorization is approved.
580 * DN's passed in should have a dn: prefix
583 slap_sasl_check_authz(struct berval *searchDN, struct berval *assertDN, struct berval *attr, struct berval *authc)
588 AttributeDescription *ad=NULL;
592 LDAP_LOG(( "sasl", LDAP_LEVEL_ENTRY,
593 "slap_sasl_check_authz: does %s match %s rule in %s?\n",
594 assertDN->bv_val, attr->bv_val, searchDN->bv_val ));
596 Debug( LDAP_DEBUG_TRACE,
597 "==>slap_sasl_check_authz: does %s match %s rule in %s?\n",
598 assertDN->bv_val, attr->bv_val, searchDN->bv_val);
601 rc = slap_bv2ad( attr, &ad, &errmsg );
602 if( rc != LDAP_SUCCESS )
605 bv.bv_val = searchDN->bv_val + 3;
606 bv.bv_len = searchDN->bv_len - 3;
607 rc = backend_attribute( NULL, NULL, NULL, NULL, &bv, ad, &vals );
608 if( rc != LDAP_SUCCESS )
611 bv.bv_val = assertDN->bv_val + 3;
612 bv.bv_len = assertDN->bv_len - 3;
613 /* Check if the *assertDN matches any **vals */
614 for( i=0; vals[i].bv_val != NULL; i++ ) {
615 rc = slap_sasl_match( vals[i].bv_val, &bv, authc );
616 if ( rc == LDAP_SUCCESS )
619 rc = LDAP_INAPPROPRIATE_AUTH;
622 if( vals ) ber_bvarray_free( vals );
625 LDAP_LOG(( "sasl", LDAP_LEVEL_ENTRY,
626 "slap_sasl_check_authz: %s check returning %s\n", attr->bv_val, rc ));
628 Debug( LDAP_DEBUG_TRACE,
629 "<==slap_sasl_check_authz: %s check returning %d\n", attr->bv_val, rc, 0);
634 #endif /* HAVE_CYRUS_SASL */
637 /* Check if a bind can SASL authorize to another identity.
638 Accepts authorization DN's with "dn:" prefix */
640 static struct berval sasl_authz_src = {
641 sizeof(SASL_AUTHZ_SOURCE_ATTR)-1, SASL_AUTHZ_SOURCE_ATTR };
643 static struct berval sasl_authz_dst = {
644 sizeof(SASL_AUTHZ_DEST_ATTR)-1, SASL_AUTHZ_DEST_ATTR };
646 int slap_sasl_authorized( struct berval *authcDN, struct berval *authzDN )
648 int rc = LDAP_INAPPROPRIATE_AUTH;
650 #ifdef HAVE_CYRUS_SASL
651 /* User binding as anonymous */
652 if ( authzDN == NULL ) {
658 LDAP_LOG(( "sasl", LDAP_LEVEL_ENTRY,
659 "slap_sasl_authorized: can %s become %s?\n", authcDN->bv_val, authzDN->bv_val ));
661 Debug( LDAP_DEBUG_TRACE,
662 "==>slap_sasl_authorized: can %s become %s?\n", authcDN->bv_val, authzDN->bv_val, 0 );
665 /* If person is authorizing to self, succeed */
666 if ( dn_match( authcDN, authzDN ) ) {
671 /* Check source rules */
672 rc = slap_sasl_check_authz( authcDN, authzDN, &sasl_authz_src,
674 if( rc == LDAP_SUCCESS ) {
678 /* Check destination rules */
679 rc = slap_sasl_check_authz( authzDN, authcDN, &sasl_authz_dst,
681 if( rc == LDAP_SUCCESS ) {
685 rc = LDAP_INAPPROPRIATE_AUTH;
691 LDAP_LOG(( "sasl", LDAP_LEVEL_ENTRY,
692 "slap_sasl_authorized: return %d\n", rc ));
694 Debug( LDAP_DEBUG_TRACE,
695 "<== slap_sasl_authorized: return %d\n", rc, 0, 0 );