2 * Copyright 1998-2003 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>
30 #define SASLREGEX_REPLACE 10
32 typedef struct sasl_regexp {
33 char *sr_match; /* regexp match pattern */
34 char *sr_replace; /* regexp replace pattern */
35 regex_t sr_workspace; /* workspace for regexp engine */
36 regmatch_t sr_strings[SASLREGEX_REPLACE]; /* strings matching $1,$2 ... */
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 /* URI format: ldap://<host>/<base>[?[<attrs>][?[<scope>][?[<filter>]]]] */
70 static int slap_parseURI( Operation *op, struct berval *uri,
71 struct berval *searchbase, int *scope, Filter **filter )
77 assert( uri != NULL && uri->bv_val != NULL );
78 searchbase->bv_val = NULL;
79 searchbase->bv_len = 0;
84 LDAP_LOG( TRANSPORT, ENTRY,
85 "slap_parseURI: parsing %s\n", uri->bv_val, 0, 0 );
87 Debug( LDAP_DEBUG_TRACE, "slap_parseURI: parsing %s\n", uri->bv_val, 0, 0 );
90 /* If it does not look like a URI, assume it is a DN */
91 if( !strncasecmp( uri->bv_val, "dn:", sizeof("dn:")-1 ) ) {
92 bv.bv_val = uri->bv_val + sizeof("dn:")-1;
93 bv.bv_val += strspn( bv.bv_val, " " );
95 is_dn: bv.bv_len = uri->bv_len - (bv.bv_val - uri->bv_val);
97 rc = dnNormalize2( NULL, &bv, searchbase, op->o_tmpmemctx );
98 if( rc == LDAP_SUCCESS ) {
99 *scope = LDAP_SCOPE_BASE;
104 rc = ldap_url_parse( uri->bv_val, &ludp );
105 if ( rc == LDAP_URL_ERR_BADSCHEME ) {
106 bv.bv_val = uri->bv_val;
110 if ( rc != LDAP_URL_SUCCESS ) {
111 return LDAP_PROTOCOL_ERROR;
114 if (( ludp->lud_host && *ludp->lud_host )
115 || ludp->lud_attrs || ludp->lud_exts )
117 /* host part should be empty */
118 /* attrs and extensions parts should be empty */
119 return LDAP_PROTOCOL_ERROR;
123 *scope = ludp->lud_scope;
125 /* Grab the filter */
126 if ( ludp->lud_filter ) {
127 *filter = str2filter_x( op, ludp->lud_filter );
128 if ( *filter == NULL ) {
129 rc = LDAP_PROTOCOL_ERROR;
134 /* Grab the searchbase */
135 bv.bv_val = ludp->lud_dn;
136 bv.bv_len = strlen( bv.bv_val );
137 rc = dnNormalize2( NULL, &bv, searchbase, op->o_tmpmemctx );
140 if( rc != LDAP_SUCCESS ) {
141 if( *filter ) filter_free_x( op, *filter );
144 ldap_free_urldesc( ludp );
148 static int slap_sasl_rx_off(char *rep, int *off)
153 /* Precompile replace pattern. Find the $<n> placeholders */
156 for ( c = rep; *c; c++ ) {
157 if ( *c == '\\' && c[1] ) {
162 if ( n == SASLREGEX_REPLACE ) {
164 LDAP_LOG( TRANSPORT, ERR,
165 "slap_sasl_rx_off: \"%s\" has too many $n "
166 "placeholders (max %d)\n", rep, SASLREGEX_REPLACE, 0 );
168 Debug( LDAP_DEBUG_ANY,
169 "SASL replace pattern %s has too many $n "
170 "placeholders (max %d)\n",
171 rep, SASLREGEX_REPLACE, 0 );
174 return( LDAP_OTHER );
181 /* Final placeholder, after the last $n */
185 return( LDAP_SUCCESS );
188 int slap_sasl_regexp_config( const char *match, const char *replace )
193 SaslRegexp = (SaslRegexp_t *) ch_realloc( (char *) SaslRegexp,
194 (nSaslRegexp + 1) * sizeof(SaslRegexp_t) );
196 reg = &SaslRegexp[nSaslRegexp];
198 reg->sr_match = ch_strdup( match );
199 reg->sr_replace = ch_strdup( replace );
201 /* Precompile matching pattern */
202 rc = regcomp( ®->sr_workspace, reg->sr_match, REG_EXTENDED|REG_ICASE );
205 LDAP_LOG( TRANSPORT, ERR,
206 "slap_sasl_regexp_config: \"%s\" could not be compiled.\n",
207 reg->sr_match, 0, 0 );
209 Debug( LDAP_DEBUG_ANY,
210 "SASL match pattern %s could not be compiled by regexp engine\n",
211 reg->sr_match, 0, 0 );
214 return( LDAP_OTHER );
217 rc = slap_sasl_rx_off( reg->sr_replace, reg->sr_offset );
218 if ( rc != LDAP_SUCCESS ) return rc;
221 return( LDAP_SUCCESS );
225 /* Perform replacement on regexp matches */
226 static void slap_sasl_rx_exp(
230 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 = ch_malloc( len + 1 );
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 )
282 char *saslname = in->bv_val;
286 memset( out, 0, sizeof( *out ) );
289 LDAP_LOG( TRANSPORT, ENTRY,
290 "slap_sasl_regexp: converting SASL name %s\n", saslname, 0, 0 );
292 Debug( LDAP_DEBUG_TRACE, "slap_sasl_regexp: converting SASL name %s\n",
296 if (( saslname == NULL ) || ( nSaslRegexp == 0 )) {
300 /* Match the normalized SASL name to the saslregexp patterns */
301 for( reg = SaslRegexp,i=0; i<nSaslRegexp; i++,reg++ ) {
302 if ( regexec( ®->sr_workspace, saslname, SASLREGEX_REPLACE,
303 reg->sr_strings, 0) == 0 )
307 if( i >= nSaslRegexp ) return( 0 );
310 * The match pattern may have been of the form "a(b.*)c(d.*)e" and the
311 * replace pattern of the form "x$1y$2z". The returned string needs
312 * to replace the $1,$2 with the strings that matched (b.*) and (d.*)
314 slap_sasl_rx_exp( reg->sr_replace, reg->sr_offset,
315 reg->sr_strings, saslname, out );
318 LDAP_LOG( TRANSPORT, ENTRY,
319 "slap_sasl_regexp: converted SASL name to %s\n",
320 out->bv_len ? out->bv_val : "", 0, 0 );
322 Debug( LDAP_DEBUG_TRACE,
323 "slap_sasl_regexp: converted SASL name to %s\n",
324 out->bv_len ? out->bv_val : "", 0, 0 );
330 /* This callback actually does some work...*/
331 static int sasl_sc_sasl2dn( Operation *o, SlapReply *rs )
333 struct berval *ndn = o->o_callback->sc_private;
335 if (rs->sr_type != REP_SEARCH) return 0;
337 /* We only want to be called once */
343 LDAP_LOG( TRANSPORT, DETAIL1,
344 "slap_sasl2dn: search DN returned more than 1 entry\n", 0, 0, 0 );
346 Debug( LDAP_DEBUG_TRACE,
347 "slap_sasl2dn: search DN returned more than 1 entry\n", 0,0,0 );
352 ber_dupbv(ndn, &rs->sr_entry->e_nname);
357 typedef struct smatch_info {
362 static int sasl_sc_smatch( Operation *o, SlapReply *rs )
364 smatch_info *sm = o->o_callback->sc_private;
366 if (rs->sr_type != REP_SEARCH) return 0;
368 if (dn_match(sm->dn, &rs->sr_entry->e_nname)) {
370 return -1; /* short-circuit the search */
377 * Map a SASL regexp rule to a DN. If the rule is just a DN or a scope=base
378 * URI, just strcmp the rule (or its searchbase) to the *assertDN. Otherwise,
379 * the rule must be used as an internal search for entries. If that search
380 * returns the *assertDN entry, the match is successful.
382 * The assertDN should not have the dn: prefix
386 int slap_sasl_match(Operation *opx, struct berval *rule, struct berval *assertDN, struct berval *authc )
391 slap_callback cb = { sasl_sc_smatch };
393 SlapReply rs = {REP_RESULT};
396 LDAP_LOG( TRANSPORT, ENTRY,
397 "slap_sasl_match: comparing DN %s to rule %s\n",
398 assertDN->bv_val, rule->bv_val,0 );
400 Debug( LDAP_DEBUG_TRACE,
401 "===>slap_sasl_match: comparing DN %s to rule %s\n",
402 assertDN->bv_val, rule->bv_val, 0 );
405 rc = slap_parseURI( opx, rule, &op.o_req_ndn, &op.oq_search.rs_scope, &op.oq_search.rs_filter );
406 if( rc != LDAP_SUCCESS ) goto CONCLUDED;
408 /* Massive shortcut: search scope == base */
409 if( op.oq_search.rs_scope == LDAP_SCOPE_BASE ) {
410 rc = regcomp(®, op.o_req_ndn.bv_val,
411 REG_EXTENDED|REG_ICASE|REG_NOSUB);
413 rc = regexec(®, assertDN->bv_val, 0, NULL, 0);
419 rc = LDAP_INAPPROPRIATE_AUTH;
424 /* Must run an internal search. */
427 LDAP_LOG( TRANSPORT, DETAIL1,
428 "slap_sasl_match: performing internal search (base=%s, scope=%d)\n",
429 op.o_req_ndn.bv_val, op.oq_search.rs_scope, 0 );
431 Debug( LDAP_DEBUG_TRACE,
432 "slap_sasl_match: performing internal search (base=%s, scope=%d)\n",
433 op.o_req_ndn.bv_val, op.oq_search.rs_scope, 0 );
436 op.o_bd = select_backend( &op.o_req_ndn, 0, 1 );
437 if(( op.o_bd == NULL ) || ( op.o_bd->be_search == NULL)) {
438 rc = LDAP_INAPPROPRIATE_AUTH;
446 op.o_tag = LDAP_REQ_SEARCH;
447 op.o_protocol = LDAP_VERSION3;
450 op.o_time = slap_get_time();
451 op.o_do_not_cache = 1;
452 op.o_is_auth_check = 1;
453 op.o_threadctx = opx->o_threadctx;
454 op.o_tmpmemctx = opx->o_tmpmemctx;
455 op.o_tmpmfuncs = opx->o_tmpmfuncs;
456 op.o_conn = opx->o_conn;
457 op.o_connid = opx->o_connid;
459 op.o_bd->be_search( &op, &rs );
464 rc = LDAP_INAPPROPRIATE_AUTH;
468 if( op.o_req_ndn.bv_len ) ch_free( op.o_req_ndn.bv_val );
469 if( op.oq_search.rs_filter ) filter_free_x( opx, op.oq_search.rs_filter );
472 LDAP_LOG( TRANSPORT, ENTRY,
473 "slap_sasl_match: comparison returned %d\n", rc, 0, 0 );
475 Debug( LDAP_DEBUG_TRACE,
476 "<===slap_sasl_match: comparison returned %d\n", rc, 0, 0);
484 * This function answers the question, "Can this ID authorize to that ID?",
485 * based on authorization rules. The rules are stored in the *searchDN, in the
486 * attribute named by *attr. If any of those rules map to the *assertDN, the
487 * authorization is approved.
489 * The DNs should not have the dn: prefix
492 slap_sasl_check_authz( Connection *conn,
493 struct berval *searchDN,
494 struct berval *assertDN,
495 AttributeDescription *ad,
496 struct berval *authc )
502 LDAP_LOG( TRANSPORT, ENTRY,
503 "slap_sasl_check_authz: does %s match %s rule in %s?\n",
504 assertDN->bv_val, ad->ad_cname.bv_val, searchDN->bv_val);
506 Debug( LDAP_DEBUG_TRACE,
507 "==>slap_sasl_check_authz: does %s match %s rule in %s?\n",
508 assertDN->bv_val, ad->ad_cname.bv_val, searchDN->bv_val);
511 rc = backend_attribute( conn->c_sasl_bindop, NULL,
512 searchDN, ad, &vals );
513 if( rc != LDAP_SUCCESS ) goto COMPLETE;
515 /* Check if the *assertDN matches any **vals */
516 for( i=0; vals[i].bv_val != NULL; i++ ) {
517 rc = slap_sasl_match( conn->c_sasl_bindop, &vals[i], assertDN, authc );
518 if ( rc == LDAP_SUCCESS ) goto COMPLETE;
520 rc = LDAP_INAPPROPRIATE_AUTH;
523 if( vals ) ber_bvarray_free( vals );
526 LDAP_LOG( TRANSPORT, RESULTS,
527 "slap_sasl_check_authz: %s check returning %s\n",
528 ad->ad_cname.bv_val, rc, 0 );
530 Debug( LDAP_DEBUG_TRACE,
531 "<==slap_sasl_check_authz: %s check returning %d\n",
532 ad->ad_cname.bv_val, rc, 0);
539 * Given a SASL name (e.g. "UID=name,cn=REALM,cn=MECH,cn=AUTH")
540 * return the LDAP DN to which it matches. The SASL regexp rules in the config
541 * file turn the SASL name into an LDAP URI. If the URI is just a DN (or a
542 * search with scope=base), just return the URI (or its searchbase). Otherwise
543 * an internal search must be done, and if that search returns exactly one
544 * entry, return the DN of that one entry.
546 void slap_sasl2dn( Operation *opx,
547 struct berval *saslname, struct berval *sasldn )
550 slap_callback cb = { sasl_sc_sasl2dn };
552 SlapReply rs = {REP_RESULT};
553 struct berval regout = { 0, NULL };
556 LDAP_LOG( TRANSPORT, ENTRY,
557 "slap_sasl2dn: converting SASL name %s to DN.\n",
558 saslname->bv_val, 0, 0 );
560 Debug( LDAP_DEBUG_TRACE, "==>slap_sasl2dn: "
561 "converting SASL name %s to a DN\n",
562 saslname->bv_val, 0,0 );
565 sasldn->bv_val = NULL;
567 cb.sc_private = sasldn;
569 /* Convert the SASL name into a minimal URI */
570 if( !slap_sasl_regexp( saslname, ®out ) ) {
574 rc = slap_parseURI( opx, ®out, &op.o_req_ndn, &op.oq_search.rs_scope, &op.oq_search.rs_filter );
575 if( regout.bv_val ) ch_free( regout.bv_val );
576 if( rc != LDAP_SUCCESS ) {
580 /* Must do an internal search */
581 op.o_bd = select_backend( &op.o_req_ndn, 0, 1 );
583 /* Massive shortcut: search scope == base */
584 if( op.oq_search.rs_scope == LDAP_SCOPE_BASE ) {
585 *sasldn = op.o_req_ndn;
586 op.o_req_ndn.bv_len = 0;
587 op.o_req_ndn.bv_val = NULL;
592 LDAP_LOG( TRANSPORT, DETAIL1,
593 "slap_sasl2dn: performing internal search (base=%s, scope=%d)\n",
594 op.o_req_ndn.bv_val, op.oq_search.rs_scope, 0 );
596 Debug( LDAP_DEBUG_TRACE,
597 "slap_sasl2dn: performing internal search (base=%s, scope=%d)\n",
598 op.o_req_ndn.bv_val, op.oq_search.rs_scope, 0 );
601 if(( op.o_bd == NULL ) || ( op.o_bd->be_search == NULL)) {
605 op.o_conn = opx->o_conn;
606 op.o_connid = opx->o_connid;
607 op.o_tag = LDAP_REQ_SEARCH;
608 op.o_protocol = LDAP_VERSION3;
609 op.o_ndn = opx->o_conn->c_ndn;
611 op.o_time = slap_get_time();
612 op.o_do_not_cache = 1;
613 op.o_is_auth_check = 1;
614 op.o_threadctx = opx->o_threadctx;
615 op.o_tmpmemctx = opx->o_tmpmemctx;
616 op.o_tmpmfuncs = opx->o_tmpmfuncs;
617 op.oq_search.rs_deref = LDAP_DEREF_NEVER;
618 op.oq_search.rs_slimit = 1;
619 op.oq_search.rs_attrsonly = 1;
621 op.o_bd->be_search( &op, &rs );
624 if( sasldn->bv_len ) {
625 opx->o_conn->c_authz_backend = op.o_bd;
627 if( op.o_req_ndn.bv_len ) ch_free( op.o_req_ndn.bv_val );
628 if( op.oq_search.rs_filter ) filter_free_x( opx, op.oq_search.rs_filter );
631 LDAP_LOG( TRANSPORT, ENTRY,
632 "slap_sasl2dn: Converted SASL name to %s\n",
633 sasldn->bv_len ? sasldn->bv_val : "<nothing>", 0, 0 );
635 Debug( LDAP_DEBUG_TRACE, "<==slap_sasl2dn: Converted SASL name to %s\n",
636 sasldn->bv_len ? sasldn->bv_val : "<nothing>", 0, 0 );
643 /* Check if a bind can SASL authorize to another identity.
644 * The DNs should not have the dn: prefix
647 int slap_sasl_authorized( Connection *conn,
648 struct berval *authcDN, struct berval *authzDN )
650 int rc = LDAP_INAPPROPRIATE_AUTH;
652 /* User binding as anonymous */
653 if ( authzDN == NULL ) {
659 LDAP_LOG( TRANSPORT, ENTRY,
660 "slap_sasl_authorized: can %s become %s?\n",
661 authcDN->bv_val, authzDN->bv_val, 0 );
663 Debug( LDAP_DEBUG_TRACE,
664 "==>slap_sasl_authorized: can %s become %s?\n",
665 authcDN->bv_val, authzDN->bv_val, 0 );
668 /* If person is authorizing to self, succeed */
669 if ( dn_match( authcDN, authzDN ) ) {
674 /* Allow the manager to authorize as any DN. */
675 if( conn->c_authz_backend && be_isroot( conn->c_authz_backend, authcDN )) {
680 /* Check source rules */
681 if( authz_policy & SASL_AUTHZ_TO ) {
682 rc = slap_sasl_check_authz( conn, authcDN, authzDN,
683 slap_schema.si_ad_saslAuthzTo, authcDN );
684 if( rc == LDAP_SUCCESS ) {
689 /* Check destination rules */
690 if( authz_policy & SASL_AUTHZ_FROM ) {
691 rc = slap_sasl_check_authz( conn, authzDN, authcDN,
692 slap_schema.si_ad_saslAuthzFrom, authcDN );
693 if( rc == LDAP_SUCCESS ) {
698 rc = LDAP_INAPPROPRIATE_AUTH;
703 LDAP_LOG( TRANSPORT, RESULTS, "slap_sasl_authorized: return %d\n", rc,0,0 );
705 Debug( LDAP_DEBUG_TRACE,
706 "<== slap_sasl_authorized: return %d\n", rc, 0, 0 );