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 int sr_offset[SASLREGEX_REPLACE+2]; /* offsets of $1,$2... in *replace */
39 static int nSaslRegexp = 0;
40 static SaslRegexp_t *SaslRegexp = NULL;
42 /* What SASL proxy authorization policies are allowed? */
43 #define SASL_AUTHZ_NONE 0
44 #define SASL_AUTHZ_FROM 1
45 #define SASL_AUTHZ_TO 2
47 static int authz_policy = SASL_AUTHZ_NONE;
49 int slap_sasl_setpolicy( const char *arg )
51 int rc = LDAP_SUCCESS;
53 if ( strcasecmp( arg, "none" ) == 0 ) {
54 authz_policy = SASL_AUTHZ_NONE;
55 } else if ( strcasecmp( arg, "from" ) == 0 ) {
56 authz_policy = SASL_AUTHZ_FROM;
57 } else if ( strcasecmp( arg, "to" ) == 0 ) {
58 authz_policy = SASL_AUTHZ_TO;
59 } else if ( strcasecmp( arg, "both" ) == 0 ) {
60 authz_policy = SASL_AUTHZ_FROM | SASL_AUTHZ_TO;
67 /* URI format: ldap://<host>/<base>[?[<attrs>][?[<scope>][?[<filter>]]]] */
69 static int slap_parseURI( Operation *op, struct berval *uri,
70 struct berval *searchbase, int *scope, Filter **filter )
76 assert( uri != NULL && uri->bv_val != NULL );
77 searchbase->bv_val = NULL;
78 searchbase->bv_len = 0;
83 LDAP_LOG( TRANSPORT, ENTRY,
84 "slap_parseURI: parsing %s\n", uri->bv_val, 0, 0 );
86 Debug( LDAP_DEBUG_TRACE, "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 should be empty */
117 /* attrs and extensions parts should 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, struct berval *assertDN, struct berval *authc )
392 slap_callback cb = { sasl_sc_smatch, NULL };
394 SlapReply rs = {REP_RESULT};
397 LDAP_LOG( TRANSPORT, ENTRY,
398 "slap_sasl_match: comparing DN %s to rule %s\n",
399 assertDN->bv_val, rule->bv_val,0 );
401 Debug( LDAP_DEBUG_TRACE,
402 "===>slap_sasl_match: comparing DN %s to rule %s\n",
403 assertDN->bv_val, rule->bv_val, 0 );
406 rc = slap_parseURI( opx, rule, &op.o_req_ndn, &op.oq_search.rs_scope, &op.oq_search.rs_filter );
407 if( rc != LDAP_SUCCESS ) goto CONCLUDED;
409 /* Massive shortcut: search scope == base */
410 if( op.oq_search.rs_scope == LDAP_SCOPE_BASE ) {
411 rc = regcomp(®, op.o_req_ndn.bv_val,
412 REG_EXTENDED|REG_ICASE|REG_NOSUB);
414 rc = regexec(®, assertDN->bv_val, 0, NULL, 0);
420 rc = LDAP_INAPPROPRIATE_AUTH;
425 /* Must run an internal search. */
428 LDAP_LOG( TRANSPORT, DETAIL1,
429 "slap_sasl_match: performing internal search (base=%s, scope=%d)\n",
430 op.o_req_ndn.bv_val, op.oq_search.rs_scope, 0 );
432 Debug( LDAP_DEBUG_TRACE,
433 "slap_sasl_match: performing internal search (base=%s, scope=%d)\n",
434 op.o_req_ndn.bv_val, op.oq_search.rs_scope, 0 );
437 op.o_bd = select_backend( &op.o_req_ndn, 0, 1 );
438 if(( op.o_bd == NULL ) || ( op.o_bd->be_search == NULL)) {
439 rc = LDAP_INAPPROPRIATE_AUTH;
447 op.o_tag = LDAP_REQ_SEARCH;
448 op.o_protocol = LDAP_VERSION3;
451 op.o_time = slap_get_time();
452 op.o_do_not_cache = 1;
453 op.o_is_auth_check = 1;
454 op.o_threadctx = opx->o_threadctx;
455 op.o_tmpmemctx = opx->o_tmpmemctx;
456 op.o_tmpmfuncs = opx->o_tmpmfuncs;
457 op.o_conn = opx->o_conn;
458 op.o_connid = opx->o_connid;
460 op.o_bd->be_search( &op, &rs );
465 rc = LDAP_INAPPROPRIATE_AUTH;
469 if( op.o_req_ndn.bv_len ) sl_free( op.o_req_ndn.bv_val, opx->o_tmpmemctx );
470 if( op.oq_search.rs_filter ) filter_free_x( opx, op.oq_search.rs_filter );
473 LDAP_LOG( TRANSPORT, ENTRY,
474 "slap_sasl_match: comparison returned %d\n", rc, 0, 0 );
476 Debug( LDAP_DEBUG_TRACE,
477 "<===slap_sasl_match: comparison returned %d\n", rc, 0, 0);
485 * This function answers the question, "Can this ID authorize to that ID?",
486 * based on authorization rules. The rules are stored in the *searchDN, in the
487 * attribute named by *attr. If any of those rules map to the *assertDN, the
488 * authorization is approved.
490 * The DNs should not have the dn: prefix
493 slap_sasl_check_authz( Connection *conn,
494 struct berval *searchDN,
495 struct berval *assertDN,
496 AttributeDescription *ad,
497 struct berval *authc )
503 LDAP_LOG( TRANSPORT, ENTRY,
504 "slap_sasl_check_authz: does %s match %s rule in %s?\n",
505 assertDN->bv_val, ad->ad_cname.bv_val, searchDN->bv_val);
507 Debug( LDAP_DEBUG_TRACE,
508 "==>slap_sasl_check_authz: does %s match %s rule in %s?\n",
509 assertDN->bv_val, ad->ad_cname.bv_val, searchDN->bv_val);
512 rc = backend_attribute( conn->c_sasl_bindop, NULL,
513 searchDN, ad, &vals );
514 if( rc != LDAP_SUCCESS ) goto COMPLETE;
516 /* Check if the *assertDN matches any **vals */
517 for( i=0; vals[i].bv_val != NULL; i++ ) {
518 rc = slap_sasl_match( conn->c_sasl_bindop, &vals[i], assertDN, authc );
519 if ( rc == LDAP_SUCCESS ) goto COMPLETE;
521 rc = LDAP_INAPPROPRIATE_AUTH;
524 if( vals ) ber_bvarray_free( vals );
527 LDAP_LOG( TRANSPORT, RESULTS,
528 "slap_sasl_check_authz: %s check returning %s\n",
529 ad->ad_cname.bv_val, rc, 0 );
531 Debug( LDAP_DEBUG_TRACE,
532 "<==slap_sasl_check_authz: %s check returning %d\n",
533 ad->ad_cname.bv_val, rc, 0);
540 * Given a SASL name (e.g. "UID=name,cn=REALM,cn=MECH,cn=AUTH")
541 * return the LDAP DN to which it matches. The SASL regexp rules in the config
542 * file turn the SASL name into an LDAP URI. If the URI is just a DN (or a
543 * search with scope=base), just return the URI (or its searchbase). Otherwise
544 * an internal search must be done, and if that search returns exactly one
545 * entry, return the DN of that one entry.
547 void slap_sasl2dn( Operation *opx,
548 struct berval *saslname, struct berval *sasldn )
551 slap_callback cb = { sasl_sc_sasl2dn, NULL };
553 SlapReply rs = {REP_RESULT};
554 struct berval regout = { 0, NULL };
557 LDAP_LOG( TRANSPORT, ENTRY,
558 "slap_sasl2dn: converting SASL name %s to DN.\n",
559 saslname->bv_val, 0, 0 );
561 Debug( LDAP_DEBUG_TRACE, "==>slap_sasl2dn: "
562 "converting SASL name %s to a DN\n",
563 saslname->bv_val, 0,0 );
566 sasldn->bv_val = NULL;
568 cb.sc_private = sasldn;
570 /* Convert the SASL name into a minimal URI */
571 if( !slap_sasl_regexp( saslname, ®out, opx->o_tmpmemctx ) ) {
575 rc = slap_parseURI( opx, ®out, &op.o_req_ndn, &op.oq_search.rs_scope, &op.oq_search.rs_filter );
576 if( regout.bv_val ) sl_free( regout.bv_val, opx->o_tmpmemctx );
577 if( rc != LDAP_SUCCESS ) {
581 /* Must do an internal search */
582 op.o_bd = select_backend( &op.o_req_ndn, 0, 1 );
584 /* Massive shortcut: search scope == base */
585 if( op.oq_search.rs_scope == LDAP_SCOPE_BASE ) {
586 *sasldn = op.o_req_ndn;
587 op.o_req_ndn.bv_len = 0;
588 op.o_req_ndn.bv_val = NULL;
593 LDAP_LOG( TRANSPORT, DETAIL1,
594 "slap_sasl2dn: performing internal search (base=%s, scope=%d)\n",
595 op.o_req_ndn.bv_val, op.oq_search.rs_scope, 0 );
597 Debug( LDAP_DEBUG_TRACE,
598 "slap_sasl2dn: performing internal search (base=%s, scope=%d)\n",
599 op.o_req_ndn.bv_val, op.oq_search.rs_scope, 0 );
602 if(( op.o_bd == NULL ) || ( op.o_bd->be_search == NULL)) {
606 op.o_conn = opx->o_conn;
607 op.o_connid = opx->o_connid;
608 op.o_tag = LDAP_REQ_SEARCH;
609 op.o_protocol = LDAP_VERSION3;
610 op.o_ndn = opx->o_conn->c_ndn;
612 op.o_time = slap_get_time();
613 op.o_do_not_cache = 1;
614 op.o_is_auth_check = 1;
615 op.o_threadctx = opx->o_threadctx;
616 op.o_tmpmemctx = opx->o_tmpmemctx;
617 op.o_tmpmfuncs = opx->o_tmpmfuncs;
618 op.oq_search.rs_deref = LDAP_DEREF_NEVER;
619 op.oq_search.rs_slimit = 1;
620 op.oq_search.rs_attrsonly = 1;
622 op.o_bd->be_search( &op, &rs );
625 if( sasldn->bv_len ) {
626 opx->o_conn->c_authz_backend = op.o_bd;
628 if( op.o_req_ndn.bv_len ) ch_free( op.o_req_ndn.bv_val );
629 if( op.oq_search.rs_filter ) filter_free_x( opx, op.oq_search.rs_filter );
632 LDAP_LOG( TRANSPORT, ENTRY,
633 "slap_sasl2dn: Converted SASL name to %s\n",
634 sasldn->bv_len ? sasldn->bv_val : "<nothing>", 0, 0 );
636 Debug( LDAP_DEBUG_TRACE, "<==slap_sasl2dn: Converted SASL name to %s\n",
637 sasldn->bv_len ? sasldn->bv_val : "<nothing>", 0, 0 );
644 /* Check if a bind can SASL authorize to another identity.
645 * The DNs should not have the dn: prefix
648 int slap_sasl_authorized( Connection *conn,
649 struct berval *authcDN, struct berval *authzDN )
651 int rc = LDAP_INAPPROPRIATE_AUTH;
653 /* User binding as anonymous */
654 if ( authzDN == NULL ) {
660 LDAP_LOG( TRANSPORT, ENTRY,
661 "slap_sasl_authorized: can %s become %s?\n",
662 authcDN->bv_val, authzDN->bv_val, 0 );
664 Debug( LDAP_DEBUG_TRACE,
665 "==>slap_sasl_authorized: can %s become %s?\n",
666 authcDN->bv_val, authzDN->bv_val, 0 );
669 /* If person is authorizing to self, succeed */
670 if ( dn_match( authcDN, authzDN ) ) {
675 /* Allow the manager to authorize as any DN. */
676 if( conn->c_authz_backend && be_isroot( conn->c_authz_backend, authcDN )) {
681 /* Check source rules */
682 if( authz_policy & SASL_AUTHZ_TO ) {
683 rc = slap_sasl_check_authz( conn, authcDN, authzDN,
684 slap_schema.si_ad_saslAuthzTo, authcDN );
685 if( rc == LDAP_SUCCESS ) {
690 /* Check destination rules */
691 if( authz_policy & SASL_AUTHZ_FROM ) {
692 rc = slap_sasl_check_authz( conn, authzDN, authcDN,
693 slap_schema.si_ad_saslAuthzFrom, authcDN );
694 if( rc == LDAP_SUCCESS ) {
699 rc = LDAP_INAPPROPRIATE_AUTH;
704 LDAP_LOG( TRANSPORT, RESULTS, "slap_sasl_authorized: return %d\n", rc,0,0 );
706 Debug( LDAP_DEBUG_TRACE,
707 "<== slap_sasl_authorized: return %d\n", rc, 0, 0 );