2 * Copyright 1998-2002 The OpenLDAP Foundation, All Rights Reserved.
3 * COPYING RESTRICTIONS APPLY, see COPYRIGHT file
6 * Copyright (c) 2000, Mark Adamson, Carnegie Mellon. All rights reserved.
7 * This software is not subject to any license of Carnegie Mellon University.
9 * Redistribution and use in source and binary forms are permitted without
10 * restriction or fee of any kind as long as this notice is preserved.
12 * The name "Carnegie Mellon" must not be used to endorse or promote
13 * products derived from this software without prior written permission.
21 #include <ac/stdlib.h>
22 #include <ac/string.h>
26 #ifdef HAVE_CYRUS_SASL
29 #ifdef HAVE_SASL_SASL_H
30 #include <sasl/sasl.h>
37 #define SASLREGEX_REPLACE 10
39 typedef struct sasl_regexp {
40 char *sr_match; /* regexp match pattern */
41 char *sr_replace; /* regexp replace pattern */
42 regex_t sr_workspace; /* workspace for regexp engine */
43 regmatch_t sr_strings[SASLREGEX_REPLACE]; /* strings matching $1,$2 ... */
44 int sr_offset[SASLREGEX_REPLACE+2]; /* offsets of $1,$2... in *replace */
47 static int nSaslRegexp = 0;
48 static SaslRegexp_t *SaslRegexp = NULL;
50 /* What SASL proxy authorization policies are allowed? */
51 #define SASL_AUTHZ_NONE 0
52 #define SASL_AUTHZ_FROM 1
53 #define SASL_AUTHZ_TO 2
55 static int authz_policy = SASL_AUTHZ_NONE;
57 int slap_sasl_setpolicy( const char *arg )
59 int rc = LDAP_SUCCESS;
61 if ( strcasecmp( arg, "none" ) == 0 )
62 authz_policy = SASL_AUTHZ_NONE;
63 else if ( strcasecmp( arg, "from" ) == 0 )
64 authz_policy = SASL_AUTHZ_FROM;
65 else if ( strcasecmp( arg, "to" ) == 0 )
66 authz_policy = SASL_AUTHZ_TO;
67 else if ( strcasecmp( arg, "both" ) == 0 )
68 authz_policy = SASL_AUTHZ_FROM | SASL_AUTHZ_TO;
74 /* URI format: ldap://<host>/<base>[?[<attrs>][?[<scope>][?[<filter>]]]] */
76 static int slap_parseURI( struct berval *uri,
77 struct berval *searchbase, int *scope, Filter **filter )
83 assert( uri != NULL && uri->bv_val != NULL );
84 searchbase->bv_val = NULL;
85 searchbase->bv_len = 0;
90 LDAP_LOG( TRANSPORT, ENTRY,
91 "slap_parseURI: parsing %s\n", uri->bv_val, 0, 0 );
93 Debug( LDAP_DEBUG_TRACE, "slap_parseURI: parsing %s\n", uri->bv_val, 0, 0 );
96 /* If it does not look like a URI, assume it is a DN */
97 if( !strncasecmp( uri->bv_val, "dn:", sizeof("dn:")-1 ) ) {
98 bv.bv_val = uri->bv_val + sizeof("dn:")-1;
99 bv.bv_val += strspn( bv.bv_val, " " );
101 is_dn: bv.bv_len = uri->bv_len - (bv.bv_val - uri->bv_val);
103 rc = dnNormalize2( NULL, &bv, searchbase );
104 if( rc == LDAP_SUCCESS ) {
105 *scope = LDAP_SCOPE_BASE;
110 rc = ldap_url_parse( uri->bv_val, &ludp );
111 if ( rc == LDAP_URL_ERR_BADSCHEME ) {
112 bv.bv_val = uri->bv_val;
116 if ( rc != LDAP_URL_SUCCESS ) {
117 return LDAP_PROTOCOL_ERROR;
120 if (( ludp->lud_host && *ludp->lud_host )
121 || ludp->lud_attrs || ludp->lud_exts )
123 /* host part should be empty */
124 /* attrs and extensions parts should be empty */
125 return LDAP_PROTOCOL_ERROR;
129 *scope = ludp->lud_scope;
131 /* Grab the filter */
132 if ( ludp->lud_filter ) {
133 *filter = str2filter( ludp->lud_filter );
134 if ( *filter == NULL ) {
135 rc = LDAP_PROTOCOL_ERROR;
140 /* Grab the searchbase */
141 bv.bv_val = ludp->lud_dn;
142 bv.bv_len = strlen( bv.bv_val );
143 rc = dnNormalize2( NULL, &bv, searchbase );
146 if( rc != LDAP_SUCCESS ) {
147 if( *filter ) filter_free( *filter );
150 ldap_free_urldesc( ludp );
154 static int slap_sasl_rx_off(char *rep, int *off)
159 /* Precompile replace pattern. Find the $<n> placeholders */
162 for ( c = rep; *c; c++ ) {
163 if ( *c == '\\' && c[1] ) {
168 if ( n == SASLREGEX_REPLACE ) {
170 LDAP_LOG( TRANSPORT, ERR,
171 "slap_sasl_rx_off: \"%s\" has too many $n "
172 "placeholders (max %d)\n", rep, SASLREGEX_REPLACE, 0 );
174 Debug( LDAP_DEBUG_ANY,
175 "SASL replace pattern %s has too many $n "
176 "placeholders (max %d)\n",
177 rep, SASLREGEX_REPLACE, 0 );
180 return( LDAP_OTHER );
187 /* Final placeholder, after the last $n */
191 return( LDAP_SUCCESS );
194 int slap_sasl_regexp_config( const char *match, const char *replace )
199 SaslRegexp = (SaslRegexp_t *) ch_realloc( (char *) SaslRegexp,
200 (nSaslRegexp + 1) * sizeof(SaslRegexp_t) );
202 reg = &SaslRegexp[nSaslRegexp];
204 reg->sr_match = ch_strdup( match );
205 reg->sr_replace = ch_strdup( replace );
207 /* Precompile matching pattern */
208 rc = regcomp( ®->sr_workspace, reg->sr_match, REG_EXTENDED|REG_ICASE );
211 LDAP_LOG( TRANSPORT, ERR,
212 "slap_sasl_regexp_config: \"%s\" could not be compiled.\n",
213 reg->sr_match, 0, 0 );
215 Debug( LDAP_DEBUG_ANY,
216 "SASL match pattern %s could not be compiled by regexp engine\n",
217 reg->sr_match, 0, 0 );
220 return( LDAP_OTHER );
223 rc = slap_sasl_rx_off( reg->sr_replace, reg->sr_offset );
224 if ( rc != LDAP_SUCCESS ) return rc;
227 return( LDAP_SUCCESS );
231 /* Perform replacement on regexp matches */
232 static void slap_sasl_rx_exp(
236 const char *saslname,
239 int i, n, len, insert;
241 /* Get the total length of the final URI */
245 while( off[n] >= 0 ) {
246 /* Len of next section from replacement string (x,y,z above) */
247 len += off[n] - off[n-1] - 2;
251 /* Len of string from saslname that matched next $i (b,d above) */
252 i = rep[ off[n] + 1 ] - '0';
253 len += str[i].rm_eo - str[i].rm_so;
256 out->bv_val = ch_malloc( len + 1 );
259 /* Fill in URI with replace string, replacing $i as we go */
262 while( off[n] >= 0) {
263 /* Paste in next section from replacement string (x,y,z above) */
264 len = off[n] - off[n-1] - 2;
265 strncpy( out->bv_val+insert, rep + off[n-1] + 2, len);
270 /* Paste in string from saslname that matched next $i (b,d above) */
271 i = rep[ off[n] + 1 ] - '0';
272 len = str[i].rm_eo - str[i].rm_so;
273 strncpy( out->bv_val+insert, saslname + str[i].rm_so, len );
279 out->bv_val[insert] = '\0';
282 /* Take the passed in SASL name and attempt to convert it into an
283 LDAP URI to find the matching LDAP entry, using the pattern matching
284 strings given in the saslregexp config file directive(s) */
286 static int slap_sasl_regexp( struct berval *in, struct berval *out )
288 char *saslname = in->bv_val;
292 memset( out, 0, sizeof( *out ) );
295 LDAP_LOG( TRANSPORT, ENTRY,
296 "slap_sasl_regexp: converting SASL name %s\n", saslname, 0, 0 );
298 Debug( LDAP_DEBUG_TRACE, "slap_sasl_regexp: converting SASL name %s\n",
302 if (( saslname == NULL ) || ( nSaslRegexp == 0 ))
305 /* Match the normalized SASL name to the saslregexp patterns */
306 for( reg = SaslRegexp,i=0; i<nSaslRegexp; i++,reg++ ) {
307 if ( regexec( ®->sr_workspace, saslname, SASLREGEX_REPLACE,
308 reg->sr_strings, 0) == 0 )
312 if( i >= nSaslRegexp )
316 * The match pattern may have been of the form "a(b.*)c(d.*)e" and the
317 * replace pattern of the form "x$1y$2z". The returned string needs
318 * to replace the $1,$2 with the strings that matched (b.*) and (d.*)
320 slap_sasl_rx_exp( reg->sr_replace, reg->sr_offset,
321 reg->sr_strings, saslname, out );
324 LDAP_LOG( TRANSPORT, ENTRY,
325 "slap_sasl_regexp: converted SASL name to %s\n",
326 out->bv_len ? out->bv_val : "", 0, 0 );
328 Debug( LDAP_DEBUG_TRACE,
329 "slap_sasl_regexp: converted SASL name to %s\n",
330 out->bv_len ? out->bv_val : "", 0, 0 );
336 /* Two empty callback functions to avoid sending results */
337 void slap_cb_null_response( Connection *conn, Operation *o, ber_tag_t tag,
338 ber_int_t msgid, ber_int_t err, const char *matched,
339 const char *text, BerVarray ref, const char *resoid,
340 struct berval *resdata, struct berval *sasldata, LDAPControl **c)
344 void slap_cb_null_sresult( Connection *conn, Operation *o, ber_int_t err,
345 const char *matched, const char *text, BerVarray refs, LDAPControl **c,
350 /* This callback actually does some work...*/
351 static int sasl_sc_sasl2dn( BackendDB *be, Connection *conn, Operation *o,
352 Entry *e, AttributeName *an, int ao, LDAPControl **c)
354 struct berval *ndn = o->o_callback->sc_private;
356 /* We only want to be called once */
362 LDAP_LOG( TRANSPORT, DETAIL1,
363 "slap_sasl2dn: search DN returned more than 1 entry\n", 0, 0, 0 );
365 Debug( LDAP_DEBUG_TRACE,
366 "slap_sasl2dn: search DN returned more than 1 entry\n", 0,0,0 );
371 ber_dupbv(ndn, &e->e_nname);
376 * Given a SASL name (e.g. "UID=name,cn=REALM,cn=MECH,cn=AUTH")
377 * return the LDAP DN to which it matches. The SASL regexp rules in the config
378 * file turn the SASL name into an LDAP URI. If the URI is just a DN (or a
379 * search with scope=base), just return the URI (or its searchbase). Otherwise
380 * an internal search must be done, and if that search returns exactly one
381 * entry, return the DN of that one entry.
384 void slap_sasl2dn( Connection *conn,
385 struct berval *saslname, struct berval *sasldn )
389 struct berval dn = { 0, NULL };
390 int scope = LDAP_SCOPE_BASE;
391 Filter *filter = NULL;
392 slap_callback cb = {slap_cb_null_response, slap_cb_null_sresult, sasl_sc_sasl2dn, NULL};
394 struct berval regout = { 0, NULL };
397 LDAP_LOG( TRANSPORT, ENTRY,
398 "slap_sasl2dn: converting SASL name %s to DN.\n",
399 saslname->bv_val, 0, 0 );
401 Debug( LDAP_DEBUG_TRACE, "==>slap_sasl2dn: "
402 "converting SASL name %s to a DN\n",
403 saslname->bv_val, 0,0 );
406 sasldn->bv_val = NULL;
408 cb.sc_private = sasldn;
410 /* Convert the SASL name into a minimal URI */
411 if( !slap_sasl_regexp( saslname, ®out ) ) {
415 rc = slap_parseURI( ®out, &dn, &scope, &filter );
416 if( rc != LDAP_SUCCESS ) {
420 /* Must do an internal search */
421 be = select_backend( &dn, 0, 1 );
423 /* Massive shortcut: search scope == base */
424 if( scope == LDAP_SCOPE_BASE ) {
432 LDAP_LOG( TRANSPORT, DETAIL1,
433 "slap_sasl2dn: performing internal search (base=%s, scope=%d)\n",
434 dn.bv_val, scope, 0 );
436 Debug( LDAP_DEBUG_TRACE,
437 "slap_sasl2dn: performing internal search (base=%s, scope=%d)\n",
438 dn.bv_val, scope, 0 );
441 if(( be == NULL ) || ( be->be_search == NULL)) {
444 suffix_alias( be, &dn );
446 op.o_tag = LDAP_REQ_SEARCH;
447 op.o_protocol = LDAP_VERSION3;
448 op.o_ndn = *saslname;
450 op.o_time = slap_get_time();
451 op.o_do_not_cache = 1;
452 op.o_threadctx = conn->c_sasl_bindop->o_threadctx;
454 (*be->be_search)( be, conn, &op, NULL, &dn,
455 scope, LDAP_DEREF_NEVER, 1, 0,
456 filter, NULL, NULL, 1 );
459 if( sasldn->bv_len ) {
460 conn->c_authz_backend = be;
462 if( dn.bv_len ) ch_free( dn.bv_val );
463 if( filter ) filter_free( filter );
466 LDAP_LOG( TRANSPORT, ENTRY,
467 "slap_sasl2dn: Converted SASL name to %s\n",
468 sasldn->bv_len ? sasldn->bv_val : "<nothing>", 0, 0 );
470 Debug( LDAP_DEBUG_TRACE, "<==slap_sasl2dn: Converted SASL name to %s\n",
471 sasldn->bv_len ? sasldn->bv_val : "<nothing>", 0, 0 );
477 typedef struct smatch_info {
482 static int sasl_sc_smatch( BackendDB *be, Connection *conn, Operation *o,
483 Entry *e, AttributeName *an, int ao, LDAPControl **c)
485 smatch_info *sm = o->o_callback->sc_private;
487 if (dn_match(sm->dn, &e->e_nname)) {
489 return -1; /* short-circuit the search */
496 * Map a SASL regexp rule to a DN. If the rule is just a DN or a scope=base
497 * URI, just strcmp the rule (or its searchbase) to the *assertDN. Otherwise,
498 * the rule must be used as an internal search for entries. If that search
499 * returns the *assertDN entry, the match is successful.
501 * The assertDN should not have the dn: prefix
505 int slap_sasl_match(Connection *conn, struct berval *rule, struct berval *assertDN, struct berval *authc )
507 struct berval searchbase = {0, NULL};
513 slap_callback cb = { slap_cb_null_response, slap_cb_null_sresult, sasl_sc_smatch, NULL };
517 LDAP_LOG( TRANSPORT, ENTRY,
518 "slap_sasl_match: comparing DN %s to rule %s\n",
519 assertDN->bv_val, rule->bv_val,0 );
521 Debug( LDAP_DEBUG_TRACE,
522 "===>slap_sasl_match: comparing DN %s to rule %s\n", assertDN->bv_val, rule->bv_val, 0 );
525 rc = slap_parseURI( rule, &searchbase, &scope, &filter );
526 if( rc != LDAP_SUCCESS )
529 /* Massive shortcut: search scope == base */
530 if( scope == LDAP_SCOPE_BASE ) {
531 rc = regcomp(®, searchbase.bv_val,
532 REG_EXTENDED|REG_ICASE|REG_NOSUB);
534 rc = regexec(®, assertDN->bv_val, 0, NULL, 0);
540 rc = LDAP_INAPPROPRIATE_AUTH;
544 /* Must run an internal search. */
547 LDAP_LOG( TRANSPORT, DETAIL1,
548 "slap_sasl_match: performing internal search (base=%s, scope=%d)\n",
549 searchbase.bv_val, scope,0 );
551 Debug( LDAP_DEBUG_TRACE,
552 "slap_sasl_match: performing internal search (base=%s, scope=%d)\n",
553 searchbase.bv_val, scope, 0 );
556 be = select_backend( &searchbase, 0, 1 );
557 if(( be == NULL ) || ( be->be_search == NULL)) {
558 rc = LDAP_INAPPROPRIATE_AUTH;
561 suffix_alias( be, &searchbase );
567 op.o_tag = LDAP_REQ_SEARCH;
568 op.o_protocol = LDAP_VERSION3;
571 op.o_time = slap_get_time();
572 op.o_do_not_cache = 1;
573 op.o_threadctx = conn->c_sasl_bindop->o_threadctx;
575 (*be->be_search)( be, conn, &op, /*base=*/NULL, &searchbase,
576 scope, /*deref=*/1, /*sizelimit=*/0, /*time=*/0, filter, /*fstr=*/NULL,
577 /*attrs=*/NULL, /*attrsonly=*/0 );
582 rc = LDAP_INAPPROPRIATE_AUTH;
585 if( searchbase.bv_len ) ch_free( searchbase.bv_val );
586 if( filter ) filter_free( filter );
588 LDAP_LOG( TRANSPORT, ENTRY,
589 "slap_sasl_match: comparison returned %d\n", rc, 0, 0 );
591 Debug( LDAP_DEBUG_TRACE,
592 "<===slap_sasl_match: comparison returned %d\n", rc, 0, 0);
600 * This function answers the question, "Can this ID authorize to that ID?",
601 * based on authorization rules. The rules are stored in the *searchDN, in the
602 * attribute named by *attr. If any of those rules map to the *assertDN, the
603 * authorization is approved.
605 * The DNs should not have the dn: prefix
608 slap_sasl_check_authz( Connection *conn,
609 struct berval *searchDN,
610 struct berval *assertDN,
611 AttributeDescription *ad,
612 struct berval *authc )
618 LDAP_LOG( TRANSPORT, ENTRY,
619 "slap_sasl_check_authz: does %s match %s rule in %s?\n",
620 assertDN->bv_val, ad->ad_cname.bv_val, searchDN->bv_val);
622 Debug( LDAP_DEBUG_TRACE,
623 "==>slap_sasl_check_authz: does %s match %s rule in %s?\n",
624 assertDN->bv_val, ad->ad_cname.bv_val, searchDN->bv_val);
627 rc = backend_attribute( NULL, NULL, conn->c_sasl_bindop, NULL, searchDN, ad, &vals );
628 if( rc != LDAP_SUCCESS )
631 /* Check if the *assertDN matches any **vals */
632 for( i=0; vals[i].bv_val != NULL; i++ ) {
633 rc = slap_sasl_match( conn, &vals[i], assertDN, authc );
634 if ( rc == LDAP_SUCCESS )
637 rc = LDAP_INAPPROPRIATE_AUTH;
640 if( vals ) ber_bvarray_free( vals );
643 LDAP_LOG( TRANSPORT, RESULTS,
644 "slap_sasl_check_authz: %s check returning %s\n",
645 ad->ad_cname.bv_val, rc, 0 );
647 Debug( LDAP_DEBUG_TRACE,
648 "<==slap_sasl_check_authz: %s check returning %d\n", ad->ad_cname.bv_val, rc, 0);
653 #endif /* HAVE_CYRUS_SASL */
656 /* Check if a bind can SASL authorize to another identity.
657 * The DNs should not have the dn: prefix
660 int slap_sasl_authorized( Connection *conn,
661 struct berval *authcDN, struct berval *authzDN )
663 int rc = LDAP_INAPPROPRIATE_AUTH;
665 #ifdef HAVE_CYRUS_SASL
666 /* User binding as anonymous */
667 if ( authzDN == NULL ) {
673 LDAP_LOG( TRANSPORT, ENTRY,
674 "slap_sasl_authorized: can %s become %s?\n",
675 authcDN->bv_val, authzDN->bv_val, 0 );
677 Debug( LDAP_DEBUG_TRACE,
678 "==>slap_sasl_authorized: can %s become %s?\n", authcDN->bv_val, authzDN->bv_val, 0 );
681 /* If person is authorizing to self, succeed */
682 if ( dn_match( authcDN, authzDN ) ) {
687 /* Check source rules */
688 if( authz_policy & SASL_AUTHZ_TO ) {
689 rc = slap_sasl_check_authz( conn, authcDN, authzDN,
690 slap_schema.si_ad_saslAuthzTo, authcDN );
691 if( rc == LDAP_SUCCESS ) {
696 /* Check destination rules */
697 if( authz_policy & SASL_AUTHZ_FROM ) {
698 rc = slap_sasl_check_authz( conn, authzDN, authcDN,
699 slap_schema.si_ad_saslAuthzFrom, authcDN );
700 if( rc == LDAP_SUCCESS ) {
705 rc = LDAP_INAPPROPRIATE_AUTH;
711 LDAP_LOG( TRANSPORT, RESULTS, "slap_sasl_authorized: return %d\n", rc,0,0 );
713 Debug( LDAP_DEBUG_TRACE,
714 "<== slap_sasl_authorized: return %d\n", rc, 0, 0 );