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,
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;
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 reg->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 reg->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 };
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 };
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 );