]> git.ipfire.org Git - thirdparty/openldap.git/commitdiff
ITS#8701 Implement account usability in ppolicy
authorOndřej Kuzník <ondra@mistotebe.net>
Wed, 17 Jun 2020 09:22:29 +0000 (10:22 +0100)
committerOndřej Kuzník <ondra@mistotebe.net>
Tue, 7 Jul 2020 15:43:37 +0000 (16:43 +0100)
doc/man/man5/slapo-ppolicy.5
servers/slapd/overlays/ppolicy.c
tests/scripts/test022-ppolicy

index 401b182749abcf67075456be9d31d3009f3ace17..0931b83115c3c5c4ed245a5b929c96308715dcb1 100644 (file)
@@ -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
index e559e17ec947dfe5032f3b640347dbd0a249fd39..63051fb9cdab9b1ca86f0e8b956511620677bccc 100644 (file)
@@ -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;
index f94d785b5538dee737ac8274e037a65134da845b..543226ec53968156ebf7b24dbce63660386242f4 100755 (executable)
@@ -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