From bdc9dbc51125b45ece3e77aedc4169908a68e357 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Kuzn=C3=ADk?= Date: Wed, 17 Jun 2020 10:22:29 +0100 Subject: [PATCH] ITS#8701 Implement account usability in ppolicy --- doc/man/man5/slapo-ppolicy.5 | 18 +++ servers/slapd/overlays/ppolicy.c | 259 +++++++++++++++++++++++++++++-- tests/scripts/test022-ppolicy | 24 ++- 3 files changed, 285 insertions(+), 16 deletions(-) diff --git a/doc/man/man5/slapo-ppolicy.5 b/doc/man/man5/slapo-ppolicy.5 index 401b182749..0931b83115 100644 --- a/doc/man/man5/slapo-ppolicy.5 +++ b/doc/man/man5/slapo-ppolicy.5 @@ -41,6 +41,10 @@ when considering a single-valued password attribute, while the userPassword attribute allows multiple values. This implementation enforces a single value for the userPassword attribute, despite its specification. +.P +In addition to supporting the IETF Password Policy, this module +supports the SunDS Account Usability control (1.3.6.1.4.1.42.2.27.9.5.8) +on search requests. .SH CONFIGURATION These @@ -977,6 +981,20 @@ policy option and when the lockout ends. USAGE directoryOperation ) .RE +.SH SUNDS ACCOUNT USABILITY CONTROL +.LP +If the SunDS Account Usability control is used with a search request, the +overlay will attach validity information to each entry provided all of the +following are met: +.IP \[bu] 2 +There is a password policy that applies to the entry +.IP \[bu] +The user has +.B read +access to the entry's password attribute. +.IP \[bu] +The configured password attribute is present in the entry + .SH EXAMPLES .LP .RS diff --git a/servers/slapd/overlays/ppolicy.c b/servers/slapd/overlays/ppolicy.c index e559e17ec9..63051fb9cd 100644 --- a/servers/slapd/overlays/ppolicy.c +++ b/servers/slapd/overlays/ppolicy.c @@ -67,6 +67,7 @@ typedef struct pw_conn { static pw_conn *pwcons; static int ppolicy_cid; +static int account_usability_cid; static int ov_count; typedef struct pass_policy { @@ -541,8 +542,6 @@ account_locked( Operation *op, Entry *e, { Attribute *la; - assert(mod != NULL); - if ( (la = attr_find( e->e_attrs, ad_pwdStartTime )) != NULL ) { BerVarray vals = la->a_nvals; time_t then, now = op->o_time; @@ -641,13 +640,15 @@ account_locked( Operation *op, Entry *e, if (now < then + pp->pwdLockoutDuration) return 1; - m = ch_calloc( sizeof(Modifications), 1 ); - m->sml_op = LDAP_MOD_DELETE; - m->sml_flags = 0; - m->sml_type = ad_pwdAccountLockedTime->ad_cname; - m->sml_desc = ad_pwdAccountLockedTime; - m->sml_next = *mod; - *mod = m; + if ( mod != NULL ) { + m = ch_calloc( sizeof(Modifications), 1 ); + m->sml_op = LDAP_MOD_DELETE; + m->sml_flags = 0; + m->sml_type = ad_pwdAccountLockedTime->ad_cname; + m->sml_desc = ad_pwdAccountLockedTime; + m->sml_next = *mod; + *mod = m; + } } } @@ -662,6 +663,7 @@ account_locked( Operation *op, Entry *e, #define PPOLICY_GRACE 0x81L /* primitive + 1 */ static const char ppolicy_ctrl_oid[] = LDAP_CONTROL_PASSWORDPOLICYRESPONSE; +static const char ppolicy_account_ctrl_oid[] = LDAP_CONTROL_X_ACCOUNT_USABILITY; static LDAPControl * create_passcontrol( Operation *op, int exptime, int grace, LDAPPasswordPolicyError err ) @@ -750,6 +752,65 @@ add_passcontrol( Operation *op, SlapReply *rs, LDAPControl *ctrl ) return oldctrls; } +static void +add_account_control( + Operation *op, + SlapReply *rs, + int available, + int remaining, + LDAPAccountUsabilityMoreInfo *more_info ) +{ + BerElementBuffer berbuf; + BerElement *ber = (BerElement *) &berbuf; + LDAPControl c = { 0 }, *cp = NULL, **ctrls; + int i = 0; + + BER_BVZERO( &c.ldctl_value ); + + ber_init2( ber, NULL, LBER_USE_DER ); + + if ( available ) { + ber_put_int( ber, remaining, LDAP_TAG_X_ACCOUNT_USABILITY_AVAILABLE ); + } else { + assert( more_info != NULL ); + + ber_start_seq( ber, LDAP_TAG_X_ACCOUNT_USABILITY_NOT_AVAILABLE ); + ber_put_boolean( ber, more_info->inactive, LDAP_TAG_X_ACCOUNT_USABILITY_INACTIVE ); + ber_put_boolean( ber, more_info->reset, LDAP_TAG_X_ACCOUNT_USABILITY_RESET ); + ber_put_boolean( ber, more_info->expired, LDAP_TAG_X_ACCOUNT_USABILITY_EXPIRED ); + ber_put_int( ber, more_info->remaining_grace, LDAP_TAG_X_ACCOUNT_USABILITY_REMAINING_GRACE ); + ber_put_int( ber, more_info->seconds_before_unlock, LDAP_TAG_X_ACCOUNT_USABILITY_UNTIL_UNLOCK ); + ber_put_seq( ber ); + } + + if (ber_flatten2( ber, &c.ldctl_value, 0 ) == -1) { + goto fail; + } + + if ( rs->sr_ctrls != NULL ) { + for ( ; rs->sr_ctrls[ i ] != NULL; i++ ) /* Count */; + } + + ctrls = op->o_tmprealloc( rs->sr_ctrls, sizeof(LDAPControl *)*( i + 2 ), op->o_tmpmemctx ); + if ( ctrls == NULL ) { + goto fail; + } + + cp = op->o_tmpalloc( sizeof( LDAPControl ) + c.ldctl_value.bv_len, op->o_tmpmemctx ); + cp->ldctl_oid = (char *)ppolicy_account_ctrl_oid; + cp->ldctl_iscritical = 0; + cp->ldctl_value.bv_val = (char *)&cp[1]; + cp->ldctl_value.bv_len = c.ldctl_value.bv_len; + AC_MEMCPY( cp->ldctl_value.bv_val, c.ldctl_value.bv_val, c.ldctl_value.bv_len ); + + ctrls[ i ] = cp; + ctrls[ i + 1 ] = NULL; + rs->sr_ctrls = ctrls; + +fail: + (void)ber_free_buf(ber); +} + static void ppolicy_get_default( PassPolicy *pp ) { @@ -1799,6 +1860,153 @@ ppolicy_restrict( return SLAP_CB_CONTINUE; } +static int +ppolicy_account_usability_entry_cb( Operation *op, SlapReply *rs ) +{ + slap_overinst *on = op->o_callback->sc_private; + BackendInfo *bi = op->o_bd->bd_info; + LDAPControl *ctrl = NULL; + PassPolicy pp; + Attribute *a; + Entry *e = NULL; + time_t pwtime = 0, seconds_until_expiry = -1, now = op->o_time; + int isExpired = 0, grace = -1; + + if ( rs->sr_type != REP_SEARCH ) { + return SLAP_CB_CONTINUE; + } + + if ( be_entry_get_rw( op, &rs->sr_entry->e_nname, NULL, NULL, 0, &e ) != LDAP_SUCCESS ) { + goto done; + } + + op->o_bd->bd_info = (BackendInfo *)on; + + if ( ppolicy_get( op, e, &pp ) != LDAP_SUCCESS ) { + /* TODO: If there is no policy, should we check if */ + goto done; + } + + if ( !access_allowed( op, e, pp.ad, NULL, ACL_COMPARE, NULL ) ) { + goto done; + } + + if ( attr_find( e->e_attrs, pp.ad ) == NULL ) { + goto done; + } + + if ((a = attr_find( e->e_attrs, ad_pwdChangedTime )) != NULL) { + pwtime = parse_time( a->a_nvals[0].bv_val ); + } + + if ( pp.pwdMaxAge && pwtime ) { + seconds_until_expiry = pwtime + pp.pwdMaxAge - now; + if ( seconds_until_expiry <= 0 ) isExpired = 1; + if ( pp.pwdGraceAuthNLimit ) { + if ( !pp.pwdGraceExpiry || seconds_until_expiry + pp.pwdGraceExpiry > 0 ) { + grace = pp.pwdGraceAuthNLimit; + if ( attr_find( e->e_attrs, ad_pwdGraceUseTime ) ) { + grace -= a->a_numvals; + } + } + } + } + if ( !isExpired && pp.pwdMaxIdle && (a = attr_find( e->e_attrs, ad_pwdLastSuccess )) ) { + time_t lastbindtime = pwtime; + + if ( (a = attr_find( e->e_attrs, ad_pwdLastSuccess )) != NULL ) { + lastbindtime = parse_time( a->a_nvals[0].bv_val ); + } + + if ( lastbindtime ) { + int remaining_idle = lastbindtime + pp.pwdMaxIdle - now; + if ( remaining_idle <= 0 ) { + isExpired = 1; + } else if ( seconds_until_expiry == -1 || remaining_idle < seconds_until_expiry ) { + seconds_until_expiry = remaining_idle; + } + } + } + + if ( isExpired || account_locked( op, e, &pp, NULL ) ) { + LDAPAccountUsabilityMoreInfo more_info = { 0, 0, 0, -1, -1 }; + time_t then, lockoutEnd = 0; + + if ( isExpired ) more_info.remaining_grace = grace; + + if ( (a = attr_find( e->e_attrs, ad_pwdAccountLockedTime )) != NULL ) { + then = parse_time( a->a_vals[0].bv_val ); + if ( then == 0 ) + lockoutEnd = -1; + + /* Still in the future? not yet in effect */ + if ( now < then ) + then = 0; + + if ( !pp.pwdLockoutDuration ) + lockoutEnd = -1; + + if ( now < then + pp.pwdLockoutDuration ) + lockoutEnd = then + pp.pwdLockoutDuration; + } + + a = attr_find( e->e_attrs, ad_pwdAccountLockedTime ); + if ( (a = attr_find( e->e_attrs, ad_pwdAccountTmpLockoutEnd )) != NULL ) { + then = parse_time( a->a_vals[0].bv_val ); + if ( lockoutEnd != -1 && then > lockoutEnd ) + lockoutEnd = then; + } + + if ( lockoutEnd > now ) { + more_info.inactive = 1; + more_info.seconds_before_unlock = lockoutEnd - now; + } + + if ( pp.pwdMustChange && + (a = attr_find( e->e_attrs, ad_pwdReset )) && + bvmatch( &a->a_nvals[0], &slap_true_bv ) ) + { + more_info.reset = 1; + } + + add_account_control( op, rs, 0, -1, &more_info ); + } else { + add_account_control( op, rs, 1, seconds_until_expiry, NULL ); + } + +done: + op->o_bd->bd_info = bi; + if ( e ) { + be_entry_release_r( op, e ); + } + return SLAP_CB_CONTINUE; +} + +static int +ppolicy_search( + Operation *op, + SlapReply *rs ) +{ + slap_overinst *on = (slap_overinst *)op->o_bd->bd_info; + int rc = ppolicy_restrict( op, rs ); + + if ( rc != SLAP_CB_CONTINUE ) { + return rc; + } + + if ( op->o_ctrlflag[account_usability_cid] ) { + slap_callback *cb; + + cb = op->o_tmpcalloc( sizeof(slap_callback), 1, op->o_tmpmemctx ); + + cb->sc_response = ppolicy_account_usability_entry_cb; + cb->sc_private = on; + overlay_callback_after_backover( op, cb, 1 ); + } + + return SLAP_CB_CONTINUE; +} + static int ppolicy_compare_response( Operation *op, @@ -2801,6 +3009,23 @@ ppolicy_parseCtrl( return LDAP_SUCCESS; } +static int +ppolicy_au_parseCtrl( + Operation *op, + SlapReply *rs, + LDAPControl *ctrl ) +{ + if ( !BER_BVISNULL( &ctrl->ldctl_value ) ) { + rs->sr_text = "account usability control value not absent"; + return LDAP_PROTOCOL_ERROR; + } + op->o_ctrlflag[account_usability_cid] = ctrl->ldctl_iscritical + ? SLAP_CONTROL_CRITICAL + : SLAP_CONTROL_NONCRITICAL; + + return LDAP_SUCCESS; +} + static int attrPretty( Syntax *syntax, @@ -2876,6 +3101,11 @@ ppolicy_db_open( ConfigReply *cr ) { + int rc; + + if ( (rc = overlay_register_control( be, LDAP_CONTROL_X_ACCOUNT_USABILITY )) != LDAP_SUCCESS ) { + return rc; + } return overlay_register_control( be, LDAP_CONTROL_PASSWORDPOLICYREQUEST ); } @@ -2887,6 +3117,7 @@ ppolicy_db_close( { #ifdef SLAP_CONFIG_DELETE overlay_unregister_control( be, LDAP_CONTROL_PASSWORDPOLICYREQUEST ); + overlay_unregister_control( be, LDAP_CONTROL_X_ACCOUNT_USABILITY ); #endif /* SLAP_CONFIG_DELETE */ return 0; @@ -2972,6 +3203,14 @@ int ppolicy_initialize() return code; } + code = register_supported_control( LDAP_CONTROL_X_ACCOUNT_USABILITY, + SLAP_CTRL_SEARCH, NULL, + ppolicy_au_parseCtrl, &account_usability_cid ); + if ( code != LDAP_SUCCESS ) { + Debug( LDAP_DEBUG_ANY, "Failed to register control %d\n", code ); + return code; + } + ldap_pvt_thread_mutex_init( &chk_syntax_mutex ); ppolicy.on_bi.bi_type = "ppolicy"; @@ -2985,7 +3224,7 @@ int ppolicy_initialize() ppolicy.on_bi.bi_op_compare = ppolicy_compare; ppolicy.on_bi.bi_op_delete = ppolicy_restrict; ppolicy.on_bi.bi_op_modify = ppolicy_modify; - ppolicy.on_bi.bi_op_search = ppolicy_restrict; + ppolicy.on_bi.bi_op_search = ppolicy_search; ppolicy.on_bi.bi_connection_destroy = ppolicy_connection_destroy; ppolicy.on_bi.bi_cf_ocs = ppolicyocs; diff --git a/tests/scripts/test022-ppolicy b/tests/scripts/test022-ppolicy index f94d785b55..543226ec53 100755 --- a/tests/scripts/test022-ppolicy +++ b/tests/scripts/test022-ppolicy @@ -89,8 +89,12 @@ if test $COUNT != 2 ; then exit 1 fi -echo "Waiting 20 seconds for lockout to reset..." -sleep 20 +DELAY=`$LDAPSEARCH -D "$MANAGERDN" -h $LOCALHOST -p $PORT1 -w $PASSWD \ + -b "$USER" -E accountUsability 1.1 | sed -n -e 's/.*seconds_before_unlock=\(\d*\)/\1/p'` + +echo "Waiting $DELAY seconds for lockout to reset..." +sleep $DELAY +sleep 1 $LDAPSEARCH -e ppolicy -h $LOCALHOST -p $PORT1 -D "$USER" -w $PASS \ -b "$BASEDN" -s base >> $SEARCHOUT 2>&1 @@ -101,9 +105,13 @@ if test $RC != 0 ; then exit $RC fi +DELAY=`$LDAPSEARCH -D "$MANAGERDN" -h $LOCALHOST -p $PORT1 -w $PASSWD \ + -b "$USER" -E accountUsability 1.1 | sed -n -e 's/.*expire=\(\d*\)/\1/p'` + echo "Testing password expiration" -echo "Waiting 20 seconds for password to expire..." -sleep 20 +echo "Waiting $DELAY seconds for password to expire..." +sleep $DELAY +sleep 1 $LDAPSEARCH -e ppolicy -h $LOCALHOST -p $PORT1 -D "$USER" -w $PASS \ -b "$BASEDN" -s base > $SEARCHOUT 2>&1 @@ -467,8 +475,12 @@ fi $LDAPSEARCH -e ppolicy -h $LOCALHOST -p $PORT1 -D "$USER" -w $PASS \ -b "$BASEDN" -s base > $SEARCHOUT 2>&1 -echo "Waiting 20 seconds for password to expire..." -sleep 20 +DELAY=`$LDAPSEARCH -D "$MANAGERDN" -h $LOCALHOST -p $PORT1 -w $PASSWD \ + -b "$USER" -E accountUsability 1.1 | sed -n -e 's/.*expire=\(\d*\)/\1/p'` + +echo "Waiting $DELAY seconds for password to expire..." +sleep $DELAY +sleep 1 $LDAPSEARCH -e ppolicy -h $LOCALHOST -p $PORT1 -D "$USER" -w $PASS \ -b "$BASEDN" -s base >> $SEARCHOUT 2>&1