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 /* 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 = dnNormalize( 0, NULL, 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 = dnNormalize( 0, NULL, 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,
234 int i, n, len, insert;
236 /* Get the total length of the final URI */
240 while( off[n] >= 0 ) {
241 /* Len of next section from replacement string (x,y,z above) */
242 len += off[n] - off[n-1] - 2;
246 /* Len of string from saslname that matched next $i (b,d above) */
247 i = rep[ off[n] + 1 ] - '0';
248 len += str[i].rm_eo - str[i].rm_so;
251 out->bv_val = sl_malloc( len + 1, ctx );
254 /* Fill in URI with replace string, replacing $i as we go */
257 while( off[n] >= 0) {
258 /* Paste in next section from replacement string (x,y,z above) */
259 len = off[n] - off[n-1] - 2;
260 strncpy( out->bv_val+insert, rep + off[n-1] + 2, len);
265 /* Paste in string from saslname that matched next $i (b,d above) */
266 i = rep[ off[n] + 1 ] - '0';
267 len = str[i].rm_eo - str[i].rm_so;
268 strncpy( out->bv_val+insert, saslname + str[i].rm_so, len );
274 out->bv_val[insert] = '\0';
277 /* Take the passed in SASL name and attempt to convert it into an
278 LDAP URI to find the matching LDAP entry, using the pattern matching
279 strings given in the saslregexp config file directive(s) */
281 static int slap_sasl_regexp( struct berval *in, struct berval *out, void *ctx )
283 char *saslname = in->bv_val;
285 regmatch_t sr_strings[SASLREGEX_REPLACE]; /* strings matching $1,$2 ... */
288 memset( out, 0, sizeof( *out ) );
291 LDAP_LOG( TRANSPORT, ENTRY,
292 "slap_sasl_regexp: converting SASL name %s\n", saslname, 0, 0 );
294 Debug( LDAP_DEBUG_TRACE, "slap_sasl_regexp: converting SASL name %s\n",
298 if (( saslname == NULL ) || ( nSaslRegexp == 0 )) {
302 /* Match the normalized SASL name to the saslregexp patterns */
303 for( reg = SaslRegexp,i=0; i<nSaslRegexp; i++,reg++ ) {
304 if ( regexec( ®->sr_workspace, saslname, SASLREGEX_REPLACE,
305 sr_strings, 0) == 0 )
309 if( i >= nSaslRegexp ) return( 0 );
312 * The match pattern may have been of the form "a(b.*)c(d.*)e" and the
313 * replace pattern of the form "x$1y$2z". The returned string needs
314 * to replace the $1,$2 with the strings that matched (b.*) and (d.*)
316 slap_sasl_rx_exp( reg->sr_replace, reg->sr_offset,
317 sr_strings, saslname, out, ctx );
320 LDAP_LOG( TRANSPORT, ENTRY,
321 "slap_sasl_regexp: converted SASL name to %s\n",
322 out->bv_len ? out->bv_val : "", 0, 0 );
324 Debug( LDAP_DEBUG_TRACE,
325 "slap_sasl_regexp: converted SASL name to %s\n",
326 out->bv_len ? out->bv_val : "", 0, 0 );
332 /* This callback actually does some work...*/
333 static int sasl_sc_sasl2dn( Operation *o, SlapReply *rs )
335 struct berval *ndn = o->o_callback->sc_private;
337 if (rs->sr_type != REP_SEARCH) return 0;
339 /* We only want to be called once */
341 o->o_tmpfree(ndn->bv_val, o->o_tmpmemctx);
345 LDAP_LOG( TRANSPORT, DETAIL1,
346 "slap_sasl2dn: search DN returned more than 1 entry\n", 0, 0, 0 );
348 Debug( LDAP_DEBUG_TRACE,
349 "slap_sasl2dn: search DN returned more than 1 entry\n", 0,0,0 );
354 ber_dupbv_x(ndn, &rs->sr_entry->e_nname, o->o_tmpmemctx);
359 typedef struct smatch_info {
364 static int sasl_sc_smatch( Operation *o, SlapReply *rs )
366 smatch_info *sm = o->o_callback->sc_private;
368 if (rs->sr_type != REP_SEARCH) return 0;
370 if (dn_match(sm->dn, &rs->sr_entry->e_nname)) {
372 return -1; /* short-circuit the search */
379 * Map a SASL regexp rule to a DN. If the rule is just a DN or a scope=base
380 * URI, just strcmp the rule (or its searchbase) to the *assertDN. Otherwise,
381 * the rule must be used as an internal search for entries. If that search
382 * returns the *assertDN entry, the match is successful.
384 * The assertDN should not have the dn: prefix
388 int slap_sasl_match(Operation *opx, struct berval *rule, 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, &op.o_req_ndn, &op.oq_search.rs_scope, &op.oq_search.rs_filter );
408 if( rc != LDAP_SUCCESS ) goto CONCLUDED;
410 /* Massive shortcut: search scope == base */
411 if( op.oq_search.rs_scope == LDAP_SCOPE_BASE ) {
412 rc = regcomp(®, op.o_req_ndn.bv_val,
413 REG_EXTENDED|REG_ICASE|REG_NOSUB);
415 rc = regexec(®, assertDN->bv_val, 0, NULL, 0);
421 rc = LDAP_INAPPROPRIATE_AUTH;
426 /* Must run an internal search. */
429 LDAP_LOG( TRANSPORT, DETAIL1,
430 "slap_sasl_match: performing internal search (base=%s, scope=%d)\n",
431 op.o_req_ndn.bv_val, op.oq_search.rs_scope, 0 );
433 Debug( LDAP_DEBUG_TRACE,
434 "slap_sasl_match: performing internal search (base=%s, scope=%d)\n",
435 op.o_req_ndn.bv_val, op.oq_search.rs_scope, 0 );
438 op.o_bd = select_backend( &op.o_req_ndn, 0, 1 );
439 if(( op.o_bd == NULL ) || ( op.o_bd->be_search == NULL)) {
440 rc = LDAP_INAPPROPRIATE_AUTH;
448 op.o_tag = LDAP_REQ_SEARCH;
449 op.o_protocol = LDAP_VERSION3;
452 op.o_time = slap_get_time();
453 op.o_do_not_cache = 1;
454 op.o_is_auth_check = 1;
455 op.o_threadctx = opx->o_threadctx;
456 op.o_tmpmemctx = opx->o_tmpmemctx;
457 op.o_tmpmfuncs = opx->o_tmpmfuncs;
458 op.o_conn = opx->o_conn;
459 op.o_connid = opx->o_connid;
461 op.o_bd->be_search( &op, &rs );
466 rc = LDAP_INAPPROPRIATE_AUTH;
470 if( op.o_req_ndn.bv_len ) sl_free( op.o_req_ndn.bv_val, opx->o_tmpmemctx );
471 if( op.oq_search.rs_filter ) filter_free_x( opx, op.oq_search.rs_filter );
474 LDAP_LOG( TRANSPORT, ENTRY,
475 "slap_sasl_match: comparison returned %d\n", rc, 0, 0 );
477 Debug( LDAP_DEBUG_TRACE,
478 "<===slap_sasl_match: comparison returned %d\n", rc, 0, 0);
486 * This function answers the question, "Can this ID authorize to that ID?",
487 * based on authorization rules. The rules are stored in the *searchDN, in the
488 * attribute named by *attr. If any of those rules map to the *assertDN, the
489 * authorization is approved.
491 * The DNs should not have the dn: prefix
494 slap_sasl_check_authz( Operation *op,
495 struct berval *searchDN,
496 struct berval *assertDN,
497 AttributeDescription *ad,
498 struct berval *authc )
504 LDAP_LOG( TRANSPORT, ENTRY,
505 "slap_sasl_check_authz: does %s match %s rule in %s?\n",
506 assertDN->bv_val, ad->ad_cname.bv_val, searchDN->bv_val);
508 Debug( LDAP_DEBUG_TRACE,
509 "==>slap_sasl_check_authz: does %s match %s rule in %s?\n",
510 assertDN->bv_val, ad->ad_cname.bv_val, searchDN->bv_val);
513 rc = backend_attribute( op, NULL,
514 searchDN, ad, &vals );
515 if( rc != LDAP_SUCCESS ) goto COMPLETE;
517 /* Check if the *assertDN matches any **vals */
519 for( i=0; vals[i].bv_val != NULL; i++ ) {
520 rc = slap_sasl_match( op, &vals[i], assertDN, authc );
521 if ( rc == LDAP_SUCCESS ) goto COMPLETE;
524 rc = LDAP_INAPPROPRIATE_AUTH;
527 if( vals ) ber_bvarray_free_x( vals, op->o_tmpmemctx );
530 LDAP_LOG( TRANSPORT, RESULTS,
531 "slap_sasl_check_authz: %s check returning %s\n",
532 ad->ad_cname.bv_val, rc, 0 );
534 Debug( LDAP_DEBUG_TRACE,
535 "<==slap_sasl_check_authz: %s check returning %d\n",
536 ad->ad_cname.bv_val, rc, 0);
543 * Given a SASL name (e.g. "UID=name,cn=REALM,cn=MECH,cn=AUTH")
544 * return the LDAP DN to which it matches. The SASL regexp rules in the config
545 * file turn the SASL name into an LDAP URI. If the URI is just a DN (or a
546 * search with scope=base), just return the URI (or its searchbase). Otherwise
547 * an internal search must be done, and if that search returns exactly one
548 * entry, return the DN of that one entry.
550 void slap_sasl2dn( Operation *opx,
551 struct berval *saslname, struct berval *sasldn )
554 slap_callback cb = { sasl_sc_sasl2dn, NULL };
556 SlapReply rs = {REP_RESULT};
557 struct berval regout = { 0, NULL };
560 LDAP_LOG( TRANSPORT, ENTRY,
561 "slap_sasl2dn: converting SASL name %s to DN.\n",
562 saslname->bv_val, 0, 0 );
564 Debug( LDAP_DEBUG_TRACE, "==>slap_sasl2dn: "
565 "converting SASL name %s to a DN\n",
566 saslname->bv_val, 0,0 );
569 sasldn->bv_val = NULL;
571 cb.sc_private = sasldn;
573 /* Convert the SASL name into a minimal URI */
574 if( !slap_sasl_regexp( saslname, ®out, opx->o_tmpmemctx ) ) {
578 rc = slap_parseURI( opx, ®out, &op.o_req_ndn, &op.oq_search.rs_scope, &op.oq_search.rs_filter );
579 if( regout.bv_val ) sl_free( regout.bv_val, opx->o_tmpmemctx );
580 if( rc != LDAP_SUCCESS ) {
584 /* Must do an internal search */
585 op.o_bd = select_backend( &op.o_req_ndn, 0, 1 );
587 /* Massive shortcut: search scope == base */
588 if( op.oq_search.rs_scope == LDAP_SCOPE_BASE ) {
589 *sasldn = op.o_req_ndn;
590 op.o_req_ndn.bv_len = 0;
591 op.o_req_ndn.bv_val = NULL;
596 LDAP_LOG( TRANSPORT, DETAIL1,
597 "slap_sasl2dn: performing internal search (base=%s, scope=%d)\n",
598 op.o_req_ndn.bv_val, op.oq_search.rs_scope, 0 );
600 Debug( LDAP_DEBUG_TRACE,
601 "slap_sasl2dn: performing internal search (base=%s, scope=%d)\n",
602 op.o_req_ndn.bv_val, op.oq_search.rs_scope, 0 );
605 if(( op.o_bd == NULL ) || ( op.o_bd->be_search == NULL)) {
609 op.o_conn = opx->o_conn;
610 op.o_connid = opx->o_connid;
611 op.o_tag = LDAP_REQ_SEARCH;
612 op.o_protocol = LDAP_VERSION3;
613 op.o_ndn = opx->o_conn->c_ndn;
615 op.o_time = slap_get_time();
616 op.o_do_not_cache = 1;
617 op.o_is_auth_check = 1;
618 op.o_threadctx = opx->o_threadctx;
619 op.o_tmpmemctx = opx->o_tmpmemctx;
620 op.o_tmpmfuncs = opx->o_tmpmfuncs;
621 op.oq_search.rs_deref = LDAP_DEREF_NEVER;
622 op.oq_search.rs_slimit = 1;
623 op.oq_search.rs_attrsonly = 1;
625 op.o_bd->be_search( &op, &rs );
628 if( sasldn->bv_len ) {
629 opx->o_conn->c_authz_backend = op.o_bd;
631 if( op.o_req_ndn.bv_len ) ch_free( op.o_req_ndn.bv_val );
632 if( op.oq_search.rs_filter ) filter_free_x( opx, op.oq_search.rs_filter );
635 LDAP_LOG( TRANSPORT, ENTRY,
636 "slap_sasl2dn: Converted SASL name to %s\n",
637 sasldn->bv_len ? sasldn->bv_val : "<nothing>", 0, 0 );
639 Debug( LDAP_DEBUG_TRACE, "<==slap_sasl2dn: Converted SASL name to %s\n",
640 sasldn->bv_len ? sasldn->bv_val : "<nothing>", 0, 0 );
647 /* Check if a bind can SASL authorize to another identity.
648 * The DNs should not have the dn: prefix
651 int slap_sasl_authorized( Operation *op,
652 struct berval *authcDN, struct berval *authzDN )
654 int rc = LDAP_INAPPROPRIATE_AUTH;
656 /* User binding as anonymous */
657 if ( authzDN == NULL ) {
663 LDAP_LOG( TRANSPORT, ENTRY,
664 "slap_sasl_authorized: can %s become %s?\n",
665 authcDN->bv_val, authzDN->bv_val, 0 );
667 Debug( LDAP_DEBUG_TRACE,
668 "==>slap_sasl_authorized: can %s become %s?\n",
669 authcDN->bv_val, authzDN->bv_val, 0 );
672 /* If person is authorizing to self, succeed */
673 if ( dn_match( authcDN, authzDN ) ) {
678 /* Allow the manager to authorize as any DN. */
679 if( op->o_conn->c_authz_backend && be_isroot( op->o_conn->c_authz_backend, authcDN )) {
684 /* Check source rules */
685 if( authz_policy & SASL_AUTHZ_TO ) {
686 rc = slap_sasl_check_authz( op, authcDN, authzDN,
687 slap_schema.si_ad_saslAuthzTo, authcDN );
688 if( rc == LDAP_SUCCESS ) {
693 /* Check destination rules */
694 if( authz_policy & SASL_AUTHZ_FROM ) {
695 rc = slap_sasl_check_authz( op, authzDN, authcDN,
696 slap_schema.si_ad_saslAuthzFrom, authcDN );
697 if( rc == LDAP_SUCCESS ) {
702 rc = LDAP_INAPPROPRIATE_AUTH;
707 LDAP_LOG( TRANSPORT, RESULTS, "slap_sasl_authorized: return %d\n", rc,0,0 );
709 Debug( LDAP_DEBUG_TRACE,
710 "<== slap_sasl_authorized: return %d\n", rc, 0, 0 );