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;
289 char *scope[] = { "base", "one", "sub" };
293 memset( out, 0, sizeof( *out ) );
296 LDAP_LOG( TRANSPORT, ENTRY,
297 "slap_sasl_regexp: converting SASL name %s\n", saslname, 0, 0 );
299 Debug( LDAP_DEBUG_TRACE, "slap_sasl_regexp: converting SASL name %s\n",
303 if (( saslname == NULL ) || ( nSaslRegexp == 0 ))
306 /* Match the normalized SASL name to the saslregexp patterns */
307 for( reg = SaslRegexp,i=0; i<nSaslRegexp; i++,reg++ ) {
308 if ( regexec( ®->sr_workspace, saslname, SASLREGEX_REPLACE,
309 reg->sr_strings, 0) == 0 )
313 if( i >= nSaslRegexp )
317 * The match pattern may have been of the form "a(b.*)c(d.*)e" and the
318 * replace pattern of the form "x$1y$2z". The returned string needs
319 * to replace the $1,$2 with the strings that matched (b.*) and (d.*)
321 slap_sasl_rx_exp( reg->sr_replace, reg->sr_offset,
322 reg->sr_strings, saslname, out );
325 LDAP_LOG( TRANSPORT, ENTRY,
326 "slap_sasl_regexp: converted SASL name to %s\n",
327 out->bv_len ? out->bv_val : "", 0, 0 );
329 Debug( LDAP_DEBUG_TRACE,
330 "slap_sasl_regexp: converted SASL name to ldap:///%s??%s?%s\n",
331 out->bv_len ? out->bv_val : "", 0, 0 );
337 /* Two empty callback functions to avoid sending results */
338 static void sasl_sc_r( Connection *conn, Operation *o, ber_tag_t tag,
339 ber_int_t msgid, ber_int_t err, const char *matched,
340 const char *text, BerVarray ref, const char *resoid,
341 struct berval *resdata, struct berval *sasldata, LDAPControl **c)
345 static void sasl_sc_s( Connection *conn, Operation *o, ber_int_t err,
346 const char *matched, const char *text, BerVarray refs, LDAPControl **c,
351 /* This callback actually does some work...*/
352 static int sasl_sc_sasl2dn( BackendDB *be, Connection *conn, Operation *o,
353 Entry *e, AttributeName *an, int ao, LDAPControl **c)
355 struct berval *ndn = o->o_callback->sc_private;
357 /* We only want to be called once */
363 LDAP_LOG( TRANSPORT, DETAIL1,
364 "slap_sasl2dn: search DN returned more than 1 entry\n", 0, 0, 0 );
366 Debug( LDAP_DEBUG_TRACE,
367 "slap_sasl2dn: search DN returned more than 1 entry\n", 0,0,0 );
372 ber_dupbv(ndn, &e->e_nname);
377 * Given a SASL name (e.g. "UID=name,cn=REALM,cn=MECH,cn=AUTH")
378 * return the LDAP DN to which it matches. The SASL regexp rules in the config
379 * file turn the SASL name into an LDAP URI. If the URI is just a DN (or a
380 * search with scope=base), just return the URI (or its searchbase). Otherwise
381 * an internal search must be done, and if that search returns exactly one
382 * entry, return the DN of that one entry.
385 void slap_sasl2dn( Connection *conn,
386 struct berval *saslname, struct berval *sasldn )
390 struct berval dn = { 0, NULL };
391 int scope = LDAP_SCOPE_BASE;
392 Filter *filter = NULL;
393 slap_callback cb = {sasl_sc_r, sasl_sc_s, sasl_sc_sasl2dn, NULL};
395 struct berval regout = { 0, NULL };
398 LDAP_LOG( TRANSPORT, ENTRY,
399 "slap_sasl2dn: converting SASL name %s to DN.\n",
400 saslname->bv_val, 0, 0 );
402 Debug( LDAP_DEBUG_TRACE, "==>slap_sasl2dn: "
403 "converting SASL name %s to a DN\n",
404 saslname->bv_val, 0,0 );
407 sasldn->bv_val = NULL;
409 cb.sc_private = sasldn;
411 /* Convert the SASL name into a minimal URI */
412 if( !slap_sasl_regexp( saslname, ®out ) ) {
416 rc = slap_parseURI( ®out, &dn, &scope, &filter );
417 if( rc != LDAP_SUCCESS ) {
421 /* Must do an internal search */
422 be = select_backend( &dn, 0, 1 );
424 /* Massive shortcut: search scope == base */
425 if( scope == LDAP_SCOPE_BASE ) {
433 LDAP_LOG( TRANSPORT, DETAIL1,
434 "slap_sasl2dn: performing internal search (base=%s, scope=%d)\n",
435 dn.bv_val, scope, 0 );
437 Debug( LDAP_DEBUG_TRACE,
438 "slap_sasl2dn: performing internal search (base=%s, scope=%d)\n",
439 dn.bv_val, scope, 0 );
442 if(( be == NULL ) || ( be->be_search == NULL)) {
445 suffix_alias( be, &dn );
447 op.o_tag = LDAP_REQ_SEARCH;
448 op.o_protocol = LDAP_VERSION3;
449 op.o_ndn = *saslname;
451 op.o_time = slap_get_time();
452 op.o_do_not_cache = 1;
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 = { sasl_sc_r, sasl_sc_s, 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;
574 (*be->be_search)( be, conn, &op, /*base=*/NULL, &searchbase,
575 scope, /*deref=*/1, /*sizelimit=*/0, /*time=*/0, filter, /*fstr=*/NULL,
576 /*attrs=*/NULL, /*attrsonly=*/0 );
581 rc = LDAP_INAPPROPRIATE_AUTH;
584 if( searchbase.bv_len ) ch_free( searchbase.bv_val );
585 if( filter ) filter_free( filter );
587 LDAP_LOG( TRANSPORT, ENTRY,
588 "slap_sasl_match: comparison returned %d\n", rc, 0, 0 );
590 Debug( LDAP_DEBUG_TRACE,
591 "<===slap_sasl_match: comparison returned %d\n", rc, 0, 0);
599 * This function answers the question, "Can this ID authorize to that ID?",
600 * based on authorization rules. The rules are stored in the *searchDN, in the
601 * attribute named by *attr. If any of those rules map to the *assertDN, the
602 * authorization is approved.
604 * The DNs should not have the dn: prefix
607 slap_sasl_check_authz( Connection *conn,
608 struct berval *searchDN,
609 struct berval *assertDN,
610 AttributeDescription *ad,
611 struct berval *authc )
617 LDAP_LOG( TRANSPORT, ENTRY,
618 "slap_sasl_check_authz: does %s match %s rule in %s?\n",
619 assertDN->bv_val, ad->ad_cname.bv_val, searchDN->bv_val);
621 Debug( LDAP_DEBUG_TRACE,
622 "==>slap_sasl_check_authz: does %s match %s rule in %s?\n",
623 assertDN->bv_val, ad->ad_cname.bv_val, searchDN->bv_val);
626 rc = backend_attribute( NULL, NULL, NULL, NULL, searchDN, ad, &vals );
627 if( rc != LDAP_SUCCESS )
630 /* Check if the *assertDN matches any **vals */
631 for( i=0; vals[i].bv_val != NULL; i++ ) {
632 rc = slap_sasl_match( conn, &vals[i], assertDN, authc );
633 if ( rc == LDAP_SUCCESS )
636 rc = LDAP_INAPPROPRIATE_AUTH;
639 if( vals ) ber_bvarray_free( vals );
642 LDAP_LOG( TRANSPORT, RESULTS,
643 "slap_sasl_check_authz: %s check returning %s\n",
644 ad->ad_cname.bv_val, rc, 0 );
646 Debug( LDAP_DEBUG_TRACE,
647 "<==slap_sasl_check_authz: %s check returning %d\n", ad->ad_cname.bv_val, rc, 0);
652 #endif /* HAVE_CYRUS_SASL */
655 /* Check if a bind can SASL authorize to another identity.
656 * The DNs should not have the dn: prefix
659 int slap_sasl_authorized( Connection *conn,
660 struct berval *authcDN, struct berval *authzDN )
662 int rc = LDAP_INAPPROPRIATE_AUTH;
664 #ifdef HAVE_CYRUS_SASL
665 /* User binding as anonymous */
666 if ( authzDN == NULL ) {
672 LDAP_LOG( TRANSPORT, ENTRY,
673 "slap_sasl_authorized: can %s become %s?\n",
674 authcDN->bv_val, authzDN->bv_val, 0 );
676 Debug( LDAP_DEBUG_TRACE,
677 "==>slap_sasl_authorized: can %s become %s?\n", authcDN->bv_val, authzDN->bv_val, 0 );
680 /* If person is authorizing to self, succeed */
681 if ( dn_match( authcDN, authzDN ) ) {
686 /* Check source rules */
687 if( authz_policy & SASL_AUTHZ_TO ) {
688 rc = slap_sasl_check_authz( conn, authcDN, authzDN,
689 slap_schema.si_ad_saslAuthzTo, authcDN );
690 if( rc == LDAP_SUCCESS ) {
695 /* Check destination rules */
696 if( authz_policy & SASL_AUTHZ_FROM ) {
697 rc = slap_sasl_check_authz( conn, authzDN, authcDN,
698 slap_schema.si_ad_saslAuthzFrom, authcDN );
699 if( rc == LDAP_SUCCESS ) {
704 rc = LDAP_INAPPROPRIATE_AUTH;
710 LDAP_LOG( TRANSPORT, RESULTS, "slap_sasl_authorized: return %d\n", rc,0,0 );
712 Debug( LDAP_DEBUG_TRACE,
713 "<== slap_sasl_authorized: return %d\n", rc, 0, 0 );