3 * Copyright 1998-2003 The OpenLDAP Foundation, All Rights Reserved.
4 * COPYING RESTRICTIONS APPLY, see COPYRIGHT file
7 * Copyright (c) 2000, Mark Adamson, Carnegie Mellon. All rights reserved.
8 * This software is not subject to any license of Carnegie Mellon University.
10 * Redistribution and use in source and binary forms are permitted without
11 * restriction or fee of any kind as long as this notice is preserved.
13 * The name "Carnegie Mellon" must not be used to endorse or promote
14 * products derived from this software without prior written permission.
22 #include <ac/stdlib.h>
23 #include <ac/string.h>
31 #define SASLREGEX_REPLACE 10
33 typedef struct sasl_regexp {
34 char *sr_match; /* regexp match pattern */
35 char *sr_replace; /* regexp replace pattern */
36 regex_t sr_workspace; /* workspace for regexp engine */
37 int sr_offset[SASLREGEX_REPLACE+2]; /* offsets of $1,$2... in *replace */
40 static int nSaslRegexp = 0;
41 static SaslRegexp_t *SaslRegexp = NULL;
43 /* What SASL proxy authorization policies are allowed? */
44 #define SASL_AUTHZ_NONE 0
45 #define SASL_AUTHZ_FROM 1
46 #define SASL_AUTHZ_TO 2
48 static int authz_policy = SASL_AUTHZ_NONE;
50 int slap_sasl_setpolicy( const char *arg )
52 int rc = LDAP_SUCCESS;
54 if ( strcasecmp( arg, "none" ) == 0 ) {
55 authz_policy = SASL_AUTHZ_NONE;
56 } else if ( strcasecmp( arg, "from" ) == 0 ) {
57 authz_policy = SASL_AUTHZ_FROM;
58 } else if ( strcasecmp( arg, "to" ) == 0 ) {
59 authz_policy = SASL_AUTHZ_TO;
60 } else if ( strcasecmp( arg, "both" ) == 0 ) {
61 authz_policy = SASL_AUTHZ_FROM | SASL_AUTHZ_TO;
68 static int slap_parseURI( Operation *op, struct berval *uri,
69 struct berval *searchbase, int *scope, Filter **filter )
75 assert( uri != NULL && uri->bv_val != NULL );
76 searchbase->bv_val = NULL;
77 searchbase->bv_len = 0;
82 LDAP_LOG( TRANSPORT, ENTRY,
83 "slap_parseURI: parsing %s\n", uri->bv_val, 0, 0 );
85 Debug( LDAP_DEBUG_TRACE,
86 "slap_parseURI: parsing %s\n", uri->bv_val, 0, 0 );
89 /* If it does not look like a URI, assume it is a DN */
90 if( !strncasecmp( uri->bv_val, "dn:", sizeof("dn:")-1 ) ) {
91 bv.bv_val = uri->bv_val + sizeof("dn:")-1;
92 bv.bv_val += strspn( bv.bv_val, " " );
94 is_dn: bv.bv_len = uri->bv_len - (bv.bv_val - uri->bv_val);
96 rc = dnNormalize( 0, NULL, NULL, &bv, searchbase, op->o_tmpmemctx );
97 if( rc == LDAP_SUCCESS ) {
98 *scope = LDAP_SCOPE_BASE;
103 rc = ldap_url_parse( uri->bv_val, &ludp );
104 if ( rc == LDAP_URL_ERR_BADSCHEME ) {
105 bv.bv_val = uri->bv_val;
109 if ( rc != LDAP_URL_SUCCESS ) {
110 return LDAP_PROTOCOL_ERROR;
113 if (( ludp->lud_host && *ludp->lud_host )
114 || ludp->lud_attrs || ludp->lud_exts )
116 /* host part must be empty */
117 /* attrs and extensions parts must be empty */
118 return LDAP_PROTOCOL_ERROR;
122 *scope = ludp->lud_scope;
124 /* Grab the filter */
125 if ( ludp->lud_filter ) {
126 *filter = str2filter_x( op, ludp->lud_filter );
127 if ( *filter == NULL ) {
128 rc = LDAP_PROTOCOL_ERROR;
133 /* Grab the searchbase */
134 bv.bv_val = ludp->lud_dn;
135 bv.bv_len = strlen( bv.bv_val );
136 rc = dnNormalize( 0, NULL, NULL, &bv, searchbase, op->o_tmpmemctx );
139 if( rc != LDAP_SUCCESS ) {
140 if( *filter ) filter_free_x( op, *filter );
143 ldap_free_urldesc( ludp );
147 static int slap_sasl_rx_off(char *rep, int *off)
152 /* Precompile replace pattern. Find the $<n> placeholders */
155 for ( c = rep; *c; c++ ) {
156 if ( *c == '\\' && c[1] ) {
161 if ( n == SASLREGEX_REPLACE ) {
163 LDAP_LOG( TRANSPORT, ERR,
164 "slap_sasl_rx_off: \"%s\" has too many $n "
165 "placeholders (max %d)\n", rep, SASLREGEX_REPLACE, 0 );
167 Debug( LDAP_DEBUG_ANY,
168 "SASL replace pattern %s has too many $n "
169 "placeholders (max %d)\n",
170 rep, SASLREGEX_REPLACE, 0 );
173 return( LDAP_OTHER );
180 /* Final placeholder, after the last $n */
184 return( LDAP_SUCCESS );
187 int slap_sasl_regexp_config( const char *match, const char *replace )
192 SaslRegexp = (SaslRegexp_t *) ch_realloc( (char *) SaslRegexp,
193 (nSaslRegexp + 1) * sizeof(SaslRegexp_t) );
195 reg = &SaslRegexp[nSaslRegexp];
197 reg->sr_match = ch_strdup( match );
198 reg->sr_replace = ch_strdup( replace );
200 /* Precompile matching pattern */
201 rc = regcomp( ®->sr_workspace, reg->sr_match, REG_EXTENDED|REG_ICASE );
204 LDAP_LOG( TRANSPORT, ERR,
205 "slap_sasl_regexp_config: \"%s\" could not be compiled.\n",
206 reg->sr_match, 0, 0 );
208 Debug( LDAP_DEBUG_ANY,
209 "SASL match pattern %s could not be compiled by regexp engine\n",
210 reg->sr_match, 0, 0 );
213 return( LDAP_OTHER );
216 rc = slap_sasl_rx_off( reg->sr_replace, reg->sr_offset );
217 if ( rc != LDAP_SUCCESS ) return rc;
220 return( LDAP_SUCCESS );
224 /* Perform replacement on regexp matches */
225 static void slap_sasl_rx_exp(
229 const char *saslname,
233 int i, n, len, insert;
235 /* Get the total length of the final URI */
239 while( off[n] >= 0 ) {
240 /* Len of next section from replacement string (x,y,z above) */
241 len += off[n] - off[n-1] - 2;
245 /* Len of string from saslname that matched next $i (b,d above) */
246 i = rep[ off[n] + 1 ] - '0';
247 len += str[i].rm_eo - str[i].rm_so;
250 out->bv_val = sl_malloc( len + 1, ctx );
253 /* Fill in URI with replace string, replacing $i as we go */
256 while( off[n] >= 0) {
257 /* Paste in next section from replacement string (x,y,z above) */
258 len = off[n] - off[n-1] - 2;
259 strncpy( out->bv_val+insert, rep + off[n-1] + 2, len);
264 /* Paste in string from saslname that matched next $i (b,d above) */
265 i = rep[ off[n] + 1 ] - '0';
266 len = str[i].rm_eo - str[i].rm_so;
267 strncpy( out->bv_val+insert, saslname + str[i].rm_so, len );
273 out->bv_val[insert] = '\0';
276 /* Take the passed in SASL name and attempt to convert it into an
277 LDAP URI to find the matching LDAP entry, using the pattern matching
278 strings given in the saslregexp config file directive(s) */
280 static int slap_sasl_regexp( struct berval *in, struct berval *out, void *ctx )
282 char *saslname = in->bv_val;
284 regmatch_t sr_strings[SASLREGEX_REPLACE]; /* strings matching $1,$2 ... */
287 memset( out, 0, sizeof( *out ) );
290 LDAP_LOG( TRANSPORT, ENTRY,
291 "slap_sasl_regexp: converting SASL name %s\n", saslname, 0, 0 );
293 Debug( LDAP_DEBUG_TRACE, "slap_sasl_regexp: converting SASL name %s\n",
297 if (( saslname == NULL ) || ( nSaslRegexp == 0 )) {
301 /* Match the normalized SASL name to the saslregexp patterns */
302 for( reg = SaslRegexp,i=0; i<nSaslRegexp; i++,reg++ ) {
303 if ( regexec( ®->sr_workspace, saslname, SASLREGEX_REPLACE,
304 sr_strings, 0) == 0 )
308 if( i >= nSaslRegexp ) return( 0 );
311 * The match pattern may have been of the form "a(b.*)c(d.*)e" and the
312 * replace pattern of the form "x$1y$2z". The returned string needs
313 * to replace the $1,$2 with the strings that matched (b.*) and (d.*)
315 slap_sasl_rx_exp( reg->sr_replace, reg->sr_offset,
316 sr_strings, saslname, out, ctx );
319 LDAP_LOG( TRANSPORT, ENTRY,
320 "slap_sasl_regexp: converted SASL name to %s\n",
321 out->bv_len ? out->bv_val : "", 0, 0 );
323 Debug( LDAP_DEBUG_TRACE,
324 "slap_sasl_regexp: converted SASL name to %s\n",
325 out->bv_len ? out->bv_val : "", 0, 0 );
331 /* This callback actually does some work...*/
332 static int sasl_sc_sasl2dn( Operation *o, SlapReply *rs )
334 struct berval *ndn = o->o_callback->sc_private;
336 if (rs->sr_type != REP_SEARCH) return 0;
338 /* We only want to be called once */
340 o->o_tmpfree(ndn->bv_val, o->o_tmpmemctx);
344 LDAP_LOG( TRANSPORT, DETAIL1,
345 "slap_sasl2dn: search DN returned more than 1 entry\n", 0, 0, 0 );
347 Debug( LDAP_DEBUG_TRACE,
348 "slap_sasl2dn: search DN returned more than 1 entry\n", 0,0,0 );
353 ber_dupbv_x(ndn, &rs->sr_entry->e_nname, o->o_tmpmemctx);
358 typedef struct smatch_info {
363 static int sasl_sc_smatch( Operation *o, SlapReply *rs )
365 smatch_info *sm = o->o_callback->sc_private;
367 if (rs->sr_type != REP_SEARCH) return 0;
369 if (dn_match(sm->dn, &rs->sr_entry->e_nname)) {
371 return -1; /* short-circuit the search */
378 * Map a SASL regexp rule to a DN. If the rule is just a DN or a scope=base
379 * URI, just strcmp the rule (or its searchbase) to the *assertDN. Otherwise,
380 * the rule must be used as an internal search for entries. If that search
381 * returns the *assertDN entry, the match is successful.
383 * The assertDN should not have the dn: prefix
387 int slap_sasl_match( Operation *opx, struct berval *rule,
388 struct berval *assertDN, struct berval *authc )
393 slap_callback cb = { sasl_sc_smatch, NULL };
395 SlapReply rs = {REP_RESULT};
398 LDAP_LOG( TRANSPORT, ENTRY,
399 "slap_sasl_match: comparing DN %s to rule %s\n",
400 assertDN->bv_val, rule->bv_val,0 );
402 Debug( LDAP_DEBUG_TRACE,
403 "===>slap_sasl_match: comparing DN %s to rule %s\n",
404 assertDN->bv_val, rule->bv_val, 0 );
407 rc = slap_parseURI( opx, rule,
408 &op.o_req_ndn, &op.oq_search.rs_scope, &op.oq_search.rs_filter );
409 if( rc != LDAP_SUCCESS ) goto CONCLUDED;
411 /* Massive shortcut: search scope == base */
412 if( op.oq_search.rs_scope == LDAP_SCOPE_BASE ) {
413 rc = regcomp(®, op.o_req_ndn.bv_val,
414 REG_EXTENDED|REG_ICASE|REG_NOSUB);
416 rc = regexec(®, assertDN->bv_val, 0, NULL, 0);
422 rc = LDAP_INAPPROPRIATE_AUTH;
427 /* Must run an internal search. */
430 LDAP_LOG( TRANSPORT, DETAIL1,
431 "slap_sasl_match: performing internal search (base=%s, scope=%d)\n",
432 op.o_req_ndn.bv_val, op.oq_search.rs_scope, 0 );
434 Debug( LDAP_DEBUG_TRACE,
435 "slap_sasl_match: performing internal search (base=%s, scope=%d)\n",
436 op.o_req_ndn.bv_val, op.oq_search.rs_scope, 0 );
439 op.o_bd = select_backend( &op.o_req_ndn, 0, 1 );
440 if(( op.o_bd == NULL ) || ( op.o_bd->be_search == NULL)) {
441 rc = LDAP_INAPPROPRIATE_AUTH;
449 op.o_tag = LDAP_REQ_SEARCH;
450 op.o_protocol = LDAP_VERSION3;
453 op.o_time = slap_get_time();
454 op.o_do_not_cache = 1;
455 op.o_is_auth_check = 1;
456 op.o_threadctx = opx->o_threadctx;
457 op.o_tmpmemctx = opx->o_tmpmemctx;
458 op.o_tmpmfuncs = opx->o_tmpmfuncs;
459 op.o_conn = opx->o_conn;
460 op.o_connid = opx->o_connid;
462 op.o_bd->be_search( &op, &rs );
467 rc = LDAP_INAPPROPRIATE_AUTH;
471 if( op.o_req_ndn.bv_len ) sl_free( op.o_req_ndn.bv_val, opx->o_tmpmemctx );
472 if( op.oq_search.rs_filter ) filter_free_x( opx, op.oq_search.rs_filter );
475 LDAP_LOG( TRANSPORT, ENTRY,
476 "slap_sasl_match: comparison returned %d\n", rc, 0, 0 );
478 Debug( LDAP_DEBUG_TRACE,
479 "<===slap_sasl_match: comparison returned %d\n", rc, 0, 0);
487 * This function answers the question, "Can this ID authorize to that ID?",
488 * based on authorization rules. The rules are stored in the *searchDN, in the
489 * attribute named by *attr. If any of those rules map to the *assertDN, the
490 * authorization is approved.
492 * The DNs should not have the dn: prefix
495 slap_sasl_check_authz( Operation *op,
496 struct berval *searchDN,
497 struct berval *assertDN,
498 AttributeDescription *ad,
499 struct berval *authc )
505 LDAP_LOG( TRANSPORT, ENTRY,
506 "slap_sasl_check_authz: does %s match %s rule in %s?\n",
507 assertDN->bv_val, ad->ad_cname.bv_val, searchDN->bv_val);
509 Debug( LDAP_DEBUG_TRACE,
510 "==>slap_sasl_check_authz: does %s match %s rule in %s?\n",
511 assertDN->bv_val, ad->ad_cname.bv_val, searchDN->bv_val);
514 rc = backend_attribute( op, NULL,
515 searchDN, ad, &vals );
516 if( rc != LDAP_SUCCESS ) goto COMPLETE;
518 /* Check if the *assertDN matches any **vals */
520 for( i=0; vals[i].bv_val != NULL; i++ ) {
521 rc = slap_sasl_match( op, &vals[i], assertDN, authc );
522 if ( rc == LDAP_SUCCESS ) goto COMPLETE;
525 rc = LDAP_INAPPROPRIATE_AUTH;
528 if( vals ) ber_bvarray_free_x( vals, op->o_tmpmemctx );
531 LDAP_LOG( TRANSPORT, RESULTS,
532 "slap_sasl_check_authz: %s check returning %s\n",
533 ad->ad_cname.bv_val, rc, 0 );
535 Debug( LDAP_DEBUG_TRACE,
536 "<==slap_sasl_check_authz: %s check returning %d\n",
537 ad->ad_cname.bv_val, rc, 0);
544 * Given a SASL name (e.g. "UID=name,cn=REALM,cn=MECH,cn=AUTH")
545 * return the LDAP DN to which it matches. The SASL regexp rules in the config
546 * file turn the SASL name into an LDAP URI. If the URI is just a DN (or a
547 * search with scope=base), just return the URI (or its searchbase). Otherwise
548 * an internal search must be done, and if that search returns exactly one
549 * entry, return the DN of that one entry.
551 void slap_sasl2dn( Operation *opx,
552 struct berval *saslname, struct berval *sasldn )
555 slap_callback cb = { sasl_sc_sasl2dn, NULL };
557 SlapReply rs = {REP_RESULT};
558 struct berval regout = { 0, NULL };
561 LDAP_LOG( TRANSPORT, ENTRY,
562 "slap_sasl2dn: converting SASL name %s to DN.\n",
563 saslname->bv_val, 0, 0 );
565 Debug( LDAP_DEBUG_TRACE, "==>slap_sasl2dn: "
566 "converting SASL name %s to a DN\n",
567 saslname->bv_val, 0,0 );
570 sasldn->bv_val = NULL;
572 cb.sc_private = sasldn;
574 /* Convert the SASL name into a minimal URI */
575 if( !slap_sasl_regexp( saslname, ®out, opx->o_tmpmemctx ) ) {
579 rc = slap_parseURI( opx, ®out,
580 &op.o_req_ndn, &op.oq_search.rs_scope, &op.oq_search.rs_filter );
581 if( regout.bv_val ) sl_free( regout.bv_val, opx->o_tmpmemctx );
582 if( rc != LDAP_SUCCESS ) {
586 /* Must do an internal search */
587 op.o_bd = select_backend( &op.o_req_ndn, 0, 1 );
589 /* Massive shortcut: search scope == base */
590 if( op.oq_search.rs_scope == LDAP_SCOPE_BASE ) {
591 *sasldn = op.o_req_ndn;
592 op.o_req_ndn.bv_len = 0;
593 op.o_req_ndn.bv_val = NULL;
598 LDAP_LOG( TRANSPORT, DETAIL1,
599 "slap_sasl2dn: performing internal search (base=%s, scope=%d)\n",
600 op.o_req_ndn.bv_val, op.oq_search.rs_scope, 0 );
602 Debug( LDAP_DEBUG_TRACE,
603 "slap_sasl2dn: performing internal search (base=%s, scope=%d)\n",
604 op.o_req_ndn.bv_val, op.oq_search.rs_scope, 0 );
607 if(( op.o_bd == NULL ) || ( op.o_bd->be_search == NULL)) {
611 op.o_conn = opx->o_conn;
612 op.o_connid = opx->o_connid;
613 op.o_tag = LDAP_REQ_SEARCH;
614 op.o_protocol = LDAP_VERSION3;
615 op.o_ndn = opx->o_conn->c_ndn;
617 op.o_time = slap_get_time();
618 op.o_do_not_cache = 1;
619 op.o_is_auth_check = 1;
620 op.o_threadctx = opx->o_threadctx;
621 op.o_tmpmemctx = opx->o_tmpmemctx;
622 op.o_tmpmfuncs = opx->o_tmpmfuncs;
623 op.oq_search.rs_deref = LDAP_DEREF_NEVER;
624 op.oq_search.rs_slimit = 1;
625 op.oq_search.rs_attrsonly = 1;
627 op.o_bd->be_search( &op, &rs );
630 if( sasldn->bv_len ) {
631 opx->o_conn->c_authz_backend = op.o_bd;
633 if( op.o_req_ndn.bv_len ) ch_free( op.o_req_ndn.bv_val );
634 if( op.oq_search.rs_filter ) filter_free_x( opx, op.oq_search.rs_filter );
637 LDAP_LOG( TRANSPORT, ENTRY,
638 "slap_sasl2dn: Converted SASL name to %s\n",
639 sasldn->bv_len ? sasldn->bv_val : "<nothing>", 0, 0 );
641 Debug( LDAP_DEBUG_TRACE, "<==slap_sasl2dn: Converted SASL name to %s\n",
642 sasldn->bv_len ? sasldn->bv_val : "<nothing>", 0, 0 );
649 /* Check if a bind can SASL authorize to another identity.
650 * The DNs should not have the dn: prefix
653 int slap_sasl_authorized( Operation *op,
654 struct berval *authcDN, struct berval *authzDN )
656 int rc = LDAP_INAPPROPRIATE_AUTH;
658 /* User binding as anonymous */
659 if ( authzDN == NULL ) {
665 LDAP_LOG( TRANSPORT, ENTRY,
666 "slap_sasl_authorized: can %s become %s?\n",
667 authcDN->bv_val, authzDN->bv_val, 0 );
669 Debug( LDAP_DEBUG_TRACE,
670 "==>slap_sasl_authorized: can %s become %s?\n",
671 authcDN->bv_val, authzDN->bv_val, 0 );
674 /* If person is authorizing to self, succeed */
675 if ( dn_match( authcDN, authzDN ) ) {
680 /* Allow the manager to authorize as any DN. */
681 if( op->o_conn->c_authz_backend && be_isroot( op->o_conn->c_authz_backend, authcDN )) {
686 /* Check source rules */
687 if( authz_policy & SASL_AUTHZ_TO ) {
688 rc = slap_sasl_check_authz( op, 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( op, authzDN, authcDN,
698 slap_schema.si_ad_saslAuthzFrom, authcDN );
699 if( rc == LDAP_SUCCESS ) {
704 rc = LDAP_INAPPROPRIATE_AUTH;
709 LDAP_LOG( TRANSPORT, RESULTS, "slap_sasl_authorized: return %d\n", rc,0,0 );
711 Debug( LDAP_DEBUG_TRACE,
712 "<== slap_sasl_authorized: return %d\n", rc, 0, 0 );