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;
290 char *scope[] = { "base", "one", "sub" };
295 memset( out, 0, sizeof( *out ) );
298 LDAP_LOG( TRANSPORT, ENTRY,
299 "slap_sasl_regexp: converting SASL name %s\n", saslname, 0, 0 );
301 Debug( LDAP_DEBUG_TRACE, "slap_sasl_regexp: converting SASL name %s\n",
305 if (( saslname == NULL ) || ( nSaslRegexp == 0 ))
308 /* Match the normalized SASL name to the saslregexp patterns */
309 for( reg = SaslRegexp,i=0; i<nSaslRegexp; i++,reg++ ) {
310 if ( regexec( ®->sr_workspace, saslname, SASLREGEX_REPLACE,
311 reg->sr_strings, 0) == 0 )
315 if( i >= nSaslRegexp )
319 * The match pattern may have been of the form "a(b.*)c(d.*)e" and the
320 * replace pattern of the form "x$1y$2z". The returned string needs
321 * to replace the $1,$2 with the strings that matched (b.*) and (d.*)
323 slap_sasl_rx_exp( reg->sr_replace, reg->sr_offset,
324 reg->sr_strings, saslname, out );
327 LDAP_LOG( TRANSPORT, ENTRY,
328 "slap_sasl_regexp: converted SASL name to %s\n",
329 out->bv_len ? out->bv_val : "", 0, 0 );
331 Debug( LDAP_DEBUG_TRACE,
332 "slap_sasl_regexp: converted SASL name to ldap:///%s??%s?%s\n",
333 out->bv_len ? out->bv_val : "", 0, 0 );
339 /* Two empty callback functions to avoid sending results */
340 static void sasl_sc_r( Connection *conn, Operation *o, ber_tag_t tag,
341 ber_int_t msgid, ber_int_t err, const char *matched,
342 const char *text, BerVarray ref, const char *resoid,
343 struct berval *resdata, struct berval *sasldata, LDAPControl **c)
347 static void sasl_sc_s( Connection *conn, Operation *o, ber_int_t err,
348 const char *matched, const char *text, BerVarray refs, LDAPControl **c,
353 /* This callback actually does some work...*/
354 static int sasl_sc_sasl2dn( BackendDB *be, Connection *conn, Operation *o,
355 Entry *e, AttributeName *an, int ao, LDAPControl **c)
357 struct berval *ndn = o->o_callback->sc_private;
359 /* We only want to be called once */
365 LDAP_LOG( TRANSPORT, DETAIL1,
366 "slap_sasl2dn: search DN returned more than 1 entry\n", 0, 0, 0 );
368 Debug( LDAP_DEBUG_TRACE,
369 "slap_sasl2dn: search DN returned more than 1 entry\n", 0,0,0 );
374 ber_dupbv(ndn, &e->e_nname);
379 * Given a SASL name (e.g. "UID=name,cn=REALM,cn=MECH,cn=AUTH")
380 * return the LDAP DN to which it matches. The SASL regexp rules in the config
381 * file turn the SASL name into an LDAP URI. If the URI is just a DN (or a
382 * search with scope=base), just return the URI (or its searchbase). Otherwise
383 * an internal search must be done, and if that search returns exactly one
384 * entry, return the DN of that one entry.
387 void slap_sasl2dn( Connection *conn,
388 struct berval *saslname, struct berval *sasldn )
392 struct berval dn = { 0, NULL };
393 int scope = LDAP_SCOPE_BASE;
394 Filter *filter = NULL;
395 slap_callback cb = {sasl_sc_r, sasl_sc_s, sasl_sc_sasl2dn, NULL};
397 struct berval regout = { 0, NULL };
400 LDAP_LOG( TRANSPORT, ENTRY,
401 "slap_sasl2dn: converting SASL name %s to DN.\n",
402 saslname->bv_val, 0, 0 );
404 Debug( LDAP_DEBUG_TRACE, "==>slap_sasl2dn: "
405 "converting SASL name %s to a DN\n",
406 saslname->bv_val, 0,0 );
409 sasldn->bv_val = NULL;
411 cb.sc_private = sasldn;
413 /* Convert the SASL name into a minimal URI */
414 if( !slap_sasl_regexp( saslname, ®out ) ) {
418 rc = slap_parseURI( ®out, &dn, &scope, &filter );
419 if( rc != LDAP_SUCCESS ) {
423 /* Must do an internal search */
424 be = select_backend( &dn, 0, 1 );
426 /* Massive shortcut: search scope == base */
427 if( scope == LDAP_SCOPE_BASE ) {
435 LDAP_LOG( TRANSPORT, DETAIL1,
436 "slap_sasl2dn: performing internal search (base=%s, scope=%d)\n",
437 dn.bv_val, scope, 0 );
439 Debug( LDAP_DEBUG_TRACE,
440 "slap_sasl2dn: performing internal search (base=%s, scope=%d)\n",
441 dn.bv_val, scope, 0 );
444 if(( be == NULL ) || ( be->be_search == NULL)) {
447 suffix_alias( be, &dn );
449 op.o_tag = LDAP_REQ_SEARCH;
450 op.o_protocol = LDAP_VERSION3;
451 op.o_ndn = *saslname;
453 op.o_time = slap_get_time();
454 op.o_do_not_cache = 1;
456 (*be->be_search)( be, conn, &op, NULL, &dn,
457 scope, LDAP_DEREF_NEVER, 1, 0,
458 filter, NULL, NULL, 1 );
461 if( sasldn->bv_len ) {
462 conn->c_authz_backend = be;
464 if( dn.bv_len ) ch_free( dn.bv_val );
465 if( filter ) filter_free( filter );
468 LDAP_LOG( TRANSPORT, ENTRY,
469 "slap_sasl2dn: Converted SASL name to %s\n",
470 sasldn->bv_len ? sasldn->bv_val : "<nothing>", 0, 0 );
472 Debug( LDAP_DEBUG_TRACE, "<==slap_sasl2dn: Converted SASL name to %s\n",
473 sasldn->bv_len ? sasldn->bv_val : "<nothing>", 0, 0 );
479 typedef struct smatch_info {
484 static int sasl_sc_smatch( BackendDB *be, Connection *conn, Operation *o,
485 Entry *e, AttributeName *an, int ao, LDAPControl **c)
487 smatch_info *sm = o->o_callback->sc_private;
489 if (dn_match(sm->dn, &e->e_nname)) {
491 return -1; /* short-circuit the search */
498 * Map a SASL regexp rule to a DN. If the rule is just a DN or a scope=base
499 * URI, just strcmp the rule (or its searchbase) to the *assertDN. Otherwise,
500 * the rule must be used as an internal search for entries. If that search
501 * returns the *assertDN entry, the match is successful.
503 * The assertDN should not have the dn: prefix
507 int slap_sasl_match(Connection *conn, struct berval *rule, struct berval *assertDN, struct berval *authc )
509 struct berval searchbase = {0, NULL};
515 slap_callback cb = { sasl_sc_r, sasl_sc_s, sasl_sc_smatch, NULL };
519 LDAP_LOG( TRANSPORT, ENTRY,
520 "slap_sasl_match: comparing DN %s to rule %s\n",
521 assertDN->bv_val, rule->bv_val,0 );
523 Debug( LDAP_DEBUG_TRACE,
524 "===>slap_sasl_match: comparing DN %s to rule %s\n", assertDN->bv_val, rule->bv_val, 0 );
527 rc = slap_parseURI( rule, &searchbase, &scope, &filter );
528 if( rc != LDAP_SUCCESS )
531 /* Massive shortcut: search scope == base */
532 if( scope == LDAP_SCOPE_BASE ) {
533 rc = regcomp(®, searchbase.bv_val,
534 REG_EXTENDED|REG_ICASE|REG_NOSUB);
536 rc = regexec(®, assertDN->bv_val, 0, NULL, 0);
542 rc = LDAP_INAPPROPRIATE_AUTH;
546 /* Must run an internal search. */
549 LDAP_LOG( TRANSPORT, DETAIL1,
550 "slap_sasl_match: performing internal search (base=%s, scope=%d)\n",
551 searchbase.bv_val, scope,0 );
553 Debug( LDAP_DEBUG_TRACE,
554 "slap_sasl_match: performing internal search (base=%s, scope=%d)\n",
555 searchbase.bv_val, scope, 0 );
558 be = select_backend( &searchbase, 0, 1 );
559 if(( be == NULL ) || ( be->be_search == NULL)) {
560 rc = LDAP_INAPPROPRIATE_AUTH;
563 suffix_alias( be, &searchbase );
569 op.o_tag = LDAP_REQ_SEARCH;
570 op.o_protocol = LDAP_VERSION3;
573 op.o_time = slap_get_time();
574 op.o_do_not_cache = 1;
576 (*be->be_search)( be, conn, &op, /*base=*/NULL, &searchbase,
577 scope, /*deref=*/1, /*sizelimit=*/0, /*time=*/0, filter, /*fstr=*/NULL,
578 /*attrs=*/NULL, /*attrsonly=*/0 );
583 rc = LDAP_INAPPROPRIATE_AUTH;
586 if( searchbase.bv_len ) ch_free( searchbase.bv_val );
587 if( filter ) filter_free( filter );
589 LDAP_LOG( TRANSPORT, ENTRY,
590 "slap_sasl_match: comparison returned %d\n", rc, 0, 0 );
592 Debug( LDAP_DEBUG_TRACE,
593 "<===slap_sasl_match: comparison returned %d\n", rc, 0, 0);
601 * This function answers the question, "Can this ID authorize to that ID?",
602 * based on authorization rules. The rules are stored in the *searchDN, in the
603 * attribute named by *attr. If any of those rules map to the *assertDN, the
604 * authorization is approved.
606 * The DNs should not have the dn: prefix
609 slap_sasl_check_authz( Connection *conn,
610 struct berval *searchDN,
611 struct berval *assertDN,
612 AttributeDescription *ad,
613 struct berval *authc )
619 LDAP_LOG( TRANSPORT, ENTRY,
620 "slap_sasl_check_authz: does %s match %s rule in %s?\n",
621 assertDN->bv_val, ad->ad_cname.bv_val, searchDN->bv_val);
623 Debug( LDAP_DEBUG_TRACE,
624 "==>slap_sasl_check_authz: does %s match %s rule in %s?\n",
625 assertDN->bv_val, ad->ad_cname.bv_val, searchDN->bv_val);
628 rc = backend_attribute( NULL, NULL, NULL, NULL, searchDN, ad, &vals );
629 if( rc != LDAP_SUCCESS )
632 /* Check if the *assertDN matches any **vals */
633 for( i=0; vals[i].bv_val != NULL; i++ ) {
634 rc = slap_sasl_match( conn, &vals[i], assertDN, authc );
635 if ( rc == LDAP_SUCCESS )
638 rc = LDAP_INAPPROPRIATE_AUTH;
641 if( vals ) ber_bvarray_free( vals );
644 LDAP_LOG( TRANSPORT, RESULTS,
645 "slap_sasl_check_authz: %s check returning %s\n",
646 ad->ad_cname.bv_val, rc, 0 );
648 Debug( LDAP_DEBUG_TRACE,
649 "<==slap_sasl_check_authz: %s check returning %d\n", ad->ad_cname.bv_val, rc, 0);
654 #endif /* HAVE_CYRUS_SASL */
657 /* Check if a bind can SASL authorize to another identity.
658 * The DNs should not have the dn: prefix
661 int slap_sasl_authorized( Connection *conn,
662 struct berval *authcDN, struct berval *authzDN )
664 int rc = LDAP_INAPPROPRIATE_AUTH;
666 #ifdef HAVE_CYRUS_SASL
667 /* User binding as anonymous */
668 if ( authzDN == NULL ) {
674 LDAP_LOG( TRANSPORT, ENTRY,
675 "slap_sasl_authorized: can %s become %s?\n",
676 authcDN->bv_val, authzDN->bv_val, 0 );
678 Debug( LDAP_DEBUG_TRACE,
679 "==>slap_sasl_authorized: can %s become %s?\n", authcDN->bv_val, authzDN->bv_val, 0 );
682 /* If person is authorizing to self, succeed */
683 if ( dn_match( authcDN, authzDN ) ) {
688 /* Check source rules */
689 if( authz_policy & SASL_AUTHZ_TO ) {
690 rc = slap_sasl_check_authz( conn, authcDN, authzDN,
691 slap_schema.si_ad_saslAuthzTo, authcDN );
692 if( rc == LDAP_SUCCESS ) {
697 /* Check destination rules */
698 if( authz_policy & SASL_AUTHZ_FROM ) {
699 rc = slap_sasl_check_authz( conn, authzDN, authcDN,
700 slap_schema.si_ad_saslAuthzFrom, authcDN );
701 if( rc == LDAP_SUCCESS ) {
706 rc = LDAP_INAPPROPRIATE_AUTH;
712 LDAP_LOG( TRANSPORT, RESULTS, "slap_sasl_authorized: return %d\n", rc,0,0 );
714 Debug( LDAP_DEBUG_TRACE,
715 "<== slap_sasl_authorized: return %d\n", rc, 0, 0 );